Warum können wir in C außerhalb der Grenzen schreiben?

Lesezeit: 11 Minuten

Ich habe kürzlich das Lesen über virtuellen Speicher beendet und habe eine Frage dazu, wie malloc innerhalb des virtuellen Adressraums und des physischen Speichers funktioniert.

Zum Beispiel (Code von einem anderen SO-Post kopiert)

void main(){
int *p;
p=malloc(sizeof(int));
p[500]=999999;
printf("p[0]=%d\n",p[500]); //works just fine. 
}

Warum darf das passieren? Oder warum ist diese Adresse auf S[500] sogar beschreibbar?

Hier ist meine Vermutung.

Wenn malloc aufgerufen wird, entscheidet sich das Betriebssystem möglicherweise dafür, dem Prozess eine ganze Seite zu geben. Ich gehe einfach davon aus, dass jede Seite 4 KB Speicherplatz wert ist. Ist das Ganze als beschreibbar markiert? Aus diesem Grund können Sie bis zu 500 * sizeof (int) in die Seite gehen (unter der Annahme eines 32-Bit-Systems, bei dem int eine Größe von 4 Bytes hat).

Ich sehe das, wenn ich versuche, mit einem größeren Wert zu bearbeiten …

   p[500000]=999999; // EXC_BAD_ACCESS according to XCode

Seg-Fehler.

Wenn ja, bedeutet das, dass es Seiten gibt, die Ihrem Code / Ihren Anweisungen / Textsegmenten gewidmet und als nicht beschreibbar markiert sind, vollständig getrennt von Ihren Seiten, auf denen sich Ihr Stack / Ihre Variablen befinden (wo sich Dinge ändern) und als beschreibbar markiert sind ? Natürlich geht der Prozess davon aus, dass sie sich auf einem 32-Bit-System neben jeder Bestellung im 4-GB-Adressraum befinden.

  • Es ist ein undefiniertes Verhalten. Scheinbar gesundes Verhalten ist auch undefiniertes Verhalten. Verlassen Sie sich nicht darauf, dass es die ganze Zeit funktioniert.

    – R Sahu

    19. März 2015 um 2:36 Uhr

  • C ist keine sichere Sprache, es ist eine schnelle Sprache. Das Überprüfen von Grenzen “verschwendet” Ausführungszeit, wenn Sie vorsichtig genug sind, um nicht außerhalb der Grenzen zu gehen

    – asies

    19. März 2015 um 2:37 Uhr

  • Siehe auch Ist der Zugriff auf ein globales Array außerhalb seines gebundenen undefinierten Verhaltens?

    – Shafik Yaghmour

    19. März 2015 um 13:00 Uhr

  • @asimes, es ist nicht so, dass C unsicher ist. Vielmehr verpflichtet die Norm nicht zur Sicherheit. Kompatible Implementierungen dürfen gebundene Prüfungen durchführen, und sie tun dies häufig, wenn dies erforderlich ist.

    – tstanisl

    5. Mai um 18:49 Uhr


Benutzer-Avatar
Chux – Wiedereinsetzung von Monica

“Warum darf das passieren?” (außerhalb der Grenzen schreiben)

C benötigt nicht die zusätzlichen CPU-Anweisungen, die normalerweise erforderlich wären, um diesen Zugriff außerhalb des Bereichs zu verhindern.

Das ist die Geschwindigkeit von C – es vertraut dem Programmierer und gibt dem Programmierer alle Seile, die er braucht, um die Aufgabe auszuführen – einschließlich genug Seil, um sich selbst aufzuhängen.

  • Verdammt, warum kann ich eine Antwort nicht als Lieblingsantwort markieren.

    – Emlai

    21. März 2015 um 4:15 Uhr

Benutzer-Avatar
4566976

Betrachten Sie den folgenden Code für Linux:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int staticvar;
const int constvar = 0;

int main(void)
{
        int stackvar;
        char buf[200];
        int *p;

        p = malloc(sizeof(int));
        sprintf(buf, "cat /proc/%d/maps", getpid());
        system(buf);

        printf("&staticvar=%p\n", &staticvar);
        printf("&constvar=%p\n", &constvar);
        printf("&stackvar=%p\n", &stackvar);
        printf("p=%p\n", p);
        printf("undefined behaviour: &p[500]=%p\n", &p[500]);
        printf("undefined behaviour: &p[50000000]=%p\n", &p[50000000]);

        p[500] = 999999; //undefined behaviour
        printf("undefined behaviour: p[500]=%d\n", p[500]);
        return 0;
}

Es druckt die Speicherabbildung des Prozesses und die Adressen einiger anderer Speichertypen.

[[email protected] ~]$ gcc tmp.c -g -static -Wall -Wextra -m32
[[email protected] ~]$ ./a.out
08048000-080ef000 r-xp 00000000 fd:00 919429                /home/osboxes/a.out
080ef000-080f2000 rw-p 000a6000 fd:00 919429                /home/osboxes/a.out
080f2000-080f3000 rw-p 00000000 00:00 0
0824d000-0826f000 rw-p 00000000 00:00 0                     [heap]
f779c000-f779e000 r--p 00000000 00:00 0                     [vvar]
f779e000-f779f000 r-xp 00000000 00:00 0                     [vdso]
ffe4a000-ffe6b000 rw-p 00000000 00:00 0                     [stack]
&staticvar=0x80f23a0
&constvar=0x80c2fcc
&stackvar=0xffe69b88
p=0x824e2a0
undefined behaviour: &p[500]=0x824ea70
undefined behaviour: &p[50000000]=0x1410a4a0
undefined behaviour: p[500]=999999

Oder warum ist diese Adresse auf S[500] sogar beschreibbar?

Heap ist von 0824d000-0826f000 und &p[500] ist zufällig 0x824ea70, also ist der Speicher beschreib- und lesbar, aber dieser Speicherbereich kann echte Daten enthalten, die verändert werden! Im Fall des Beispielprogramms ist es sehr wahrscheinlich, dass es nicht verwendet wird, sodass das Schreiben in diesen Speicher für das Funktionieren des Prozesses nicht schädlich ist.

&p[50000000] ist zufällig 0x1410a4a0, was sich nicht auf einer Seite befindet, die der Kernel dem Prozess zugeordnet hat, und daher nicht schreibbar und nicht lesbar ist, daher der Seg-Fehler.

Wenn Sie es mit kompilieren -fsanitize=address Speicherzugriffe werden überprüft und viele, aber nicht alle illegalen Speicherzugriffe werden gemeldet AddressSanitizer. Die Verlangsamung ist etwa zweimal langsamer als ohne AddressSanitizer.

[[email protected] ~]$ gcc tmp.c -g -Wall -Wextra -m32 -fsanitize=address
[[email protected] ~]$ ./a.out
[...]
undefined behaviour: &p[500]=0xf5c00fc0
undefined behaviour: &p[50000000]=0x1abc9f0
=================================================================
==2845==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xf5c00fc0 at pc 0x8048972 bp 0xfff44568 sp 0xfff44558
WRITE of size 4 at 0xf5c00fc0 thread T0
    #0 0x8048971 in main /home/osboxes/tmp.c:24
    #1 0xf70a4e7d in __libc_start_main (/lib/libc.so.6+0x17e7d)
    #2 0x80486f0 (/home/osboxes/a.out+0x80486f0)

AddressSanitizer can not describe address in more detail (wild memory access suspected).
SUMMARY: AddressSanitizer: heap-buffer-overflow /home/osboxes/tmp.c:24 main
[...]
==2845==ABORTING

Wenn ja, bedeutet das, dass es Seiten gibt, die Ihrem Code / Ihren Anweisungen / Textsegmenten gewidmet und als nicht beschreibbar markiert sind, vollständig getrennt von Ihren Seiten, auf denen sich Ihr Stack / Ihre Variablen befinden (wo sich Dinge ändern) und als beschreibbar markiert sind ?

Ja, siehe die Ausgabe der Speicherzuordnung des Prozesses oben. r-xp bedeutet lesbar und ausführbar, rw-p bedeutet lesbar und beschreibbar.

  • Wow, ich markiere das als Antwort. Ich liebe es, Code-Demonstrationen zu sehen, was vor sich geht, und ich habe so viel gelernt. Ich wusste bis jetzt nichts über system(buff) und AddressSanitizer, also danke dafür.

    – Pfeil

    21. März 2015 um 4:19 Uhr

  • Vereinbart mit OP – das ist eine erstaunliche Antwort

    – HFBräunung

    22. März 2018 um 21:25 Uhr

Benutzer-Avatar
Jeremy Friesner

Warum darf das passieren?

Eines der primären Entwurfsziele der Sprachen C (und C++) ist es, so laufzeiteffizient wie möglich zu sein. Die Designer von C (oder C++) hätten sich entscheiden können, eine Regel in die Sprachspezifikation aufzunehmen, die besagt, dass “das Schreiben außerhalb der Grenzen eines Arrays dazu führen muss, dass X passiert” (wobei X ein wohldefiniertes Verhalten ist, wie z. B. ein Absturz oder ausgelöste Ausnahme) … aber wenn sie das getan hätten, wäre es jeder C-Compiler gewesen erforderlich um für jeden Array-Zugriff, den das C-Programm ausführt, Code zur Begrenzungsprüfung zu generieren. Abhängig von der Zielhardware und der Cleverness des Compilers könnte die Durchsetzung einer solchen Regel jedes C- (oder C++-) Programm leicht 5-10 Mal langsamer machen, als es derzeit sein kann.

Anstatt also vom Compiler zu verlangen, Array-Grenzen zu erzwingen, gaben sie einfach an, dass das Schreiben außerhalb der Grenzen des Arrays liegt undefiniertes Verhalten — das heißt, Sie sollten es nicht tun, aber wenn Sie tun tun, dann gibt es keine Garantie dafür, was passieren wird, und alles, was passiert, was Ihnen nicht gefällt, ist Ihr Problem, nicht ihres.

Implementierungen in der realen Welt können dann tun, was sie wollen – auf einem Betriebssystem mit Speicherschutz werden Sie beispielsweise wahrscheinlich ein seitenbasiertes Verhalten wie von Ihnen beschrieben sehen, oder auf einem eingebetteten Gerät (oder auf älteren Betriebssystemen wie MacOS 9, MS -DOS oder AmigaDOS) kann der Computer Sie einfach überall im Speicher schreiben lassen, da sonst der Computer zu langsam werden würde.

Als Low-Level-Sprache (nach modernen Standards) erwartet C (C++), dass der Programmierer die Regeln befolgt, und setzt diese Regeln nur dann mechanisch durch, wenn dies ohne Laufzeitaufwand möglich ist.

Benutzer-Avatar
Emlai

Undefiniertes Verhalten.

Das ist es. Du kann versuchen, außerhalb der Grenzen zu schreiben, aber es ist nicht garantiert arbeiten. Es könnte funktionieren, es könnte nicht. Was passiert, ist völlig undefiniert.

Warum darf das passieren?

Weil es die C- und C++-Standards zulassen. Die Sprachen sind darauf ausgelegt schnell. Das Prüfen auf Zugriffe außerhalb der Grenzen würde eine Laufzeitoperation erfordern, die das Programm verlangsamen würde.

Warum ist diese Adresse auf p[500] sogar beschreibbar?

Es gerade passiert zu sein. Undefiniertes Verhalten.

Ich sehe das, wenn ich versuche, mit einem größeren Wert zu bearbeiten …

Sehen? Wieder es gerade passiert zu segfault.

Wenn malloc aufgerufen wird, entscheidet sich das Betriebssystem möglicherweise dafür, dem Prozess eine ganze Seite zu geben.

Vielleicht, aber die C- und C++-Standards verlangen kein solches Verhalten. Sie erfordern lediglich, dass das Betriebssystem mindestens die angeforderte Speichermenge für die Verwendung durch das Programm zur Verfügung stellt. (Wenn Speicher verfügbar ist.)

Es ist einfach so, dass das Konzept eines Arrays in C ziemlich einfach ist.

Die Zuordnung zu S[] ist in C dasselbe wie:

*(p+500)=999999;

und alles, was der Compiler tut, um das zu implementieren, ist:

fetch p;
calculate offset : multiply '500' by the sizeof(*p) -- e.g. 4 for int;
add p and the offset to get the memory address
write to that address.

In vielen Architekturen ist dies in ein oder zwei Anweisungen implementierbar.

Beachten Sie, dass der Compiler nicht nur nicht weiß, dass der Wert 500 nicht im Array enthalten ist, er kennt auch die Array-Größe von Anfang an nicht!

In C99 und später wurde einige Arbeit geleistet, um Arrays sicherer zu machen, aber im Grunde ist C eine Sprache, die darauf ausgelegt ist, schnell zu kompilieren und schnell auszuführen, nicht sicher.

Anders ausgedrückt. In Pascal verhindert der Compiler, dass Sie sich in den Fuß schießen. In C++ bietet der Compiler Möglichkeiten, das Schießen mit dem Fuß zu erschweren, während der Compiler in C nicht einmal weiß, dass Sie einen Fuß haben.

  • C++ gibt Ihnen Möglichkeiten, einen sichereren Fuß zu bauen, während er immer noch voll schießbare Füße im C-Stil unterstützt.

    – Keith Thompson

    19. März 2015 um 19:41 Uhr

Es ist ein undefiniertes Verhalten …

  • Wenn Sie versuchen, außerhalb der Grenzen zuzugreifen, kann alles passieren, einschließlich SIGEGV oder Beschädigungen an anderer Stelle im Stapel, die dazu führen, dass Ihr Programm falsche Ergebnisse liefert, hängen bleibt, später abstürzt usw.

  • die Erinnerung kann ohne offensichtlichen Fehler bei einem bestimmten Lauf für einen bestimmten Compiler/Flags/Betriebssystem/Wochentag usw. beschreibbar sein, weil:

    • malloc() könnte tatsächlich einen größeren zugewiesenen Block zuweisen, wobei [500] kann geschrieben werden (aber bei einem anderen Lauf des Programms möglicherweise nicht) oder
    • [500] möglicherweise nach dem zugewiesenen Block, aber immer noch Speicher, auf den das Programm zugreifen kann
      • es ist wahrscheinlich, dass [500] – da es sich um ein relativ kleines Inkrement handelt – immer noch auf dem Haufen wäre, was sich möglicherweise über die Adressen hinaus erstreckt malloc Aufrufe sind bisher aufgrund einer früheren Reservierung von Heap-Speicher (z. B. using sbrk()) in Vorbereitung auf den voraussichtlichen Einsatz
      • es ist vage möglich, dass [500] ist “am Ende” des Haufens, und Sie schreiben am Ende in einen anderen Speicherbereich, wo z. B. über statische Daten, Thread-spezifische Daten (einschließlich des Stapels)

Warum durfte das passieren?

Dazu gibt es zwei Aspekte:

  • Das Überprüfen der Indizes bei jedem Zugriff würde aufblähen (zusätzliche Maschinencodeanweisungen hinzufügen) und die Ausführung des Programms verlangsamen, und im Allgemeinen kann der Programmierer eine minimale Validierung der Indizes durchführen (z. B. einmal validieren, wenn eine Funktion eingegeben wird, und dann den Index so oft verwenden). ) oder die Indizes so generieren, dass ihre Gültigkeit gewährleistet ist (z. B. Schleifen von 0 auf die Array-Größe)

  • Die äußerst präzise Verwaltung des Speichers, sodass Zugriffe außerhalb der Grenzen durch einen CPU-Fehler gemeldet werden, ist stark hardwareabhängig und im Allgemeinen nur an Seitengrenzen möglich (z. B. Granularität im Bereich von 1k bis 4k) und nimmt zusätzliche Kosten in Anspruch Anleitung (ob innerhalb einiger erweiterter malloc Funktion oder in einigen malloc-Wrapping-Code) und Zeit zum Orchestrieren.

  • C++ gibt Ihnen Möglichkeiten, einen sichereren Fuß zu bauen, während er immer noch voll schießbare Füße im C-Stil unterstützt.

    – Keith Thompson

    19. März 2015 um 19:41 Uhr

Benutzer-Avatar
Superkatze

In der im 1974 C Reference Manual beschriebenen Sprache hat die Bedeutung von int arr[10]; im Dateibereich war “einen Bereich aufeinanderfolgender Speicherorte reservieren, der groß genug ist, um 10 Werte des Typs aufzunehmen intund binden Sie den Namen arr an die Adresse am Anfang dieser Region. Die Bedeutung des Ausdrucks arr[someInt] wäre dann “multiplizieren someInt um die Größe eines intaddieren Sie diese Anzahl von Bytes zur Basisadresse von arrund auf was auch immer zugreifen int geschieht unter der resultierenden Adresse gespeichert. Ob someInt im Bereich 0..9 liegt, fällt die resultierende Adresse in den Bereich, der reserviert wurde, als arr wurde deklariert, aber die Sprache war agnostisch, ob der Wert in diesen Bereich fallen würde. Wenn auf einer Plattform wo int zwei Bytes waren, wusste ein Programmierer zufällig, dass die Adresse eines Objekts x lag 200 Bytes hinter der Startadresse von arrdann ein Zugriff auf arr[100] wäre ein Zugang zu x. Woher sollte ein Programmierer das wissen? x war 200 Bytes nach dem Beginn von arroder warum der Programmierer den Ausdruck verwenden möchte arr[100] eher, als x zugreifen xwar das Design der Sprache solchen Dingen völlig agnostisch.

Der C-Standard erlaubt, erfordert aber nicht, dass sich Implementierungen bedingungslos wie oben beschrieben verhalten, selbst in Fällen, in denen die Adresse außerhalb der Grenzen des indizierten Array-Objekts liegen würde. Code, der auf einem solchen Verhalten beruht, ist oft nicht portierbar, kann aber auf einigen Plattformen einige Aufgaben effizienter ausführen, als dies sonst möglich wäre.

1012960cookie-checkWarum können wir in C außerhalb der Grenzen schreiben?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy