Konflikt zwischen einem Stanford-Tutorial und GCC

82

Laut diesem Film (ca. Minute 38) verwenden zwei Funktionen mit denselben lokalen Variablen denselben Speicherplatz. Also sollte das folgende Programm drucken 5. Kompilieren mit gccErgebnissen -1218960859. Warum?

Das Programm:

#include <stdio.h>

void A()
{
    int a;
    printf("%i",a);
}

void B()
{
    int a;
    a = 5;
}

int main()
{
    B();
    A();
    return 0;
}

Wie angefordert, ist hier die Ausgabe vom Disassembler:

0804840c <A>:
 804840c:   55                      push   ebp
 804840d:   89 e5                   mov    ebp,esp
 804840f:   83 ec 28                sub    esp,0x28
 8048412:   8b 45 f4                mov    eax,DWORD PTR [ebp-0xc]
 8048415:   89 44 24 04             mov    DWORD PTR [esp+0x4],eax
 8048419:   c7 04 24 e8 84 04 08    mov    DWORD PTR [esp],0x80484e8
 8048420:   e8 cb fe ff ff          call   80482f0 <printf@plt>
 8048425:   c9                      leave  
 8048426:   c3                      ret    

08048427 <B>:
 8048427:   55                      push   ebp
 8048428:   89 e5                   mov    ebp,esp
 804842a:   83 ec 10                sub    esp,0x10
 804842d:   c7 45 fc 05 00 00 00    mov    DWORD PTR [ebp-0x4],0x5
 8048434:   c9                      leave  
 8048435:   c3                      ret    

08048436 <main>:
 8048436:   55                      push   ebp
 8048437:   89 e5                   mov    ebp,esp
 8048439:   83 e4 f0                and    esp,0xfffffff0
 804843c:   e8 e6 ff ff ff          call   8048427 <B>
 8048441:   e8 c6 ff ff ff          call   804840c <A>
 8048446:   b8 00 00 00 00          mov    eax,0x0
 804844b:   c9                      leave  
 804844c:   c3                      ret    
 804844d:   66 90                   xchg   ax,ax
 804844f:   90                      nop
elyashiv
quelle
41
"Sie nutzen gut den gleichen Raum" - das ist falsch. Sie könnten. Oder sie könnten nicht. Und darauf kann man sich auch nicht verlassen.
Mat
17
Ich frage mich, welchen Nutzen dies als Übung hat. Wenn man dies im Produktionscode verwenden würde, würde man erschossen werden.
AndersK
12
@claptrap Vielleicht um zu erfahren, wie der Call Stack funktioniert und um zu verstehen, was der Computer unter der Haube tut? Die Leute nehmen diesen Weg zu ernst.
Jonathon Reinhart
9
@claptrap Wieder ist es eine Lernübung . Die "Reifen, durch die Sie springen müssen" sind alle sinnvoll, wenn Sie verstehen, was auf Baugruppenebene vor sich geht. Ich bezweifle ernsthaft, dass das OP die Absicht hat, so etwas in einem "echten" Programm zu verwenden (wenn er es tut, sollte er gekickt werden!)
Jonathon Reinhart
12
Das Beispiel ist irreführend für das Ahnungslose, da die beiden lokalen Variablen denselben Namen haben. Dies ist jedoch für das, was vor sich geht, irrelevant: Nur die Anzahl und Art der Variablen ist von Bedeutung. Unterschiedliche Namen sollten genau gleich funktionieren.
Alexis

Antworten:

130

Ja, ja, dies ist ein undefiniertes Verhalten , da Sie die Variable uninitialized 1 verwenden .

Auf der x86-Architektur 2 sollte dieses Experiment jedoch funktionieren . Der Wert wird nicht vom Stapel "gelöscht", und da er nicht initialisiert B()ist, sollte derselbe Wert immer noch vorhanden sein, vorausgesetzt, die Stapelrahmen sind identisch.

Ich wage das zu erraten, da int anicht wird verwendet , innerhalb von void B(), den Compiler optimiert , dass Code, und ein 5 wurde nie auf dem Stapel an diese Stelle geschrieben. Versuchen Sie auch, ein printfIn hinzuzufügen B()- es könnte einfach funktionieren.

Auch Compiler-Flags - nämlich die Optimierungsstufe - werden wahrscheinlich auch dieses Experiment beeinflussen. Versuchen Sie, Optimierungen zu deaktivieren, indem Sie -O0an gcc übergeben.

Bearbeiten: Ich habe gerade Ihren Code mit gcc -O0(64-Bit) kompiliert , und tatsächlich druckt das Programm 5, wie man es von dem mit dem Aufrufstapel vertrauten erwarten würde. In der Tat funktionierte es auch ohne -O0. Ein 32-Bit-Build kann sich anders verhalten.

Haftungsausschluss: Verwenden Sie niemals so etwas in "echtem" Code!

1 - Im Folgenden wird diskutiert, ob dies offiziell "UB" oder nur unvorhersehbar ist.

2 - Auch x64 und wahrscheinlich jede andere Architektur, die einen Aufrufstapel verwendet (mindestens eine mit einer MMU)


Schauen wir uns einen Grund an, warum es nicht funktioniert hat. Dies ist am besten in 32 Bit zu sehen, also werde ich mit kompilieren -m32.

$ gcc --version
gcc (GCC) 4.7.2 20120921 (Red Hat 4.7.2-2)

Ich habe kompiliert mit $ gcc -m32 -O0 test.c(Optimierungen deaktiviert). Wenn ich das ausführe, wird Müll gedruckt.

Betrachten $ objdump -Mintel -d ./a.out:

080483ec <A>:
 80483ec:   55                      push   ebp
 80483ed:   89 e5                   mov    ebp,esp
 80483ef:   83 ec 28                sub    esp,0x28
 80483f2:   8b 45 f4                mov    eax,DWORD PTR [ebp-0xc]
 80483f5:   89 44 24 04             mov    DWORD PTR [esp+0x4],eax
 80483f9:   c7 04 24 c4 84 04 08    mov    DWORD PTR [esp],0x80484c4
 8048400:   e8 cb fe ff ff          call   80482d0 <printf@plt>
 8048405:   c9                      leave  
 8048406:   c3                      ret    

08048407 <B>:
 8048407:   55                      push   ebp
 8048408:   89 e5                   mov    ebp,esp
 804840a:   83 ec 10                sub    esp,0x10
 804840d:   c7 45 fc 05 00 00 00    mov    DWORD PTR [ebp-0x4],0x5
 8048414:   c9                      leave  
 8048415:   c3                      ret    

Wir sehen, dass Bder Compiler 0x10 Bytes Stapelspeicher reserviert und unsere int aVariable auf [ebp-0x4]5 initialisiert hat .

In Aplatzierte sich der Compiler jedoch int abei [ebp-0xc]. In diesem Fall sind unsere lokalen Variablen also nicht am selben Ort gelandet! Wenn Sie ebenfalls einen printf()Aufruf hinzufügen, Awerden die Stapelrahmen für Aund Bidentisch und gedruckt 55.

Jonathon Reinhart
quelle
7
Guter Haftungsausschluss!
Tobias Wärre
5
Selbst wenn es einmal funktioniert, ist es auf einigen Architekturen nicht zuverlässig - eine Interrupt-Präambel lässt mich jederzeit alles unter dem Stapelzeiger wegblasen.
Martin James
6
So viele Stimmen für eine Antwort, die nicht einmal "undefiniertes Verhalten" erwähnt. Darüber hinaus wird es auch akzeptiert.
BЈовић
25
Es wird auch akzeptiert, weil es die Frage tatsächlich beantwortet .
Slebetman
8
@ BЈовић Hast du eines der Videos gesehen? Schauen Sie, jeder und sein Bruder wissen, dass Sie dies nicht in echtem Code tun sollten, und es ruft undefiniertes Verhalten hervor . Das ist nicht der Punkt. Der Punkt ist, dass ein Computer eine genau definierte, vorhersehbare Maschine ist. Auf einer x86-Box (und wahrscheinlich den meisten anderen Architekturen) mit einem vernünftigen Compiler und möglicherweise etwas Code- / Flag-Massage funktioniert dies wie erwartet. Dieser Code ist zusammen mit dem Video lediglich eine Demonstration der Funktionsweise des Aufrufstapels. Wenn es Sie so sehr stört, schlage ich vor, dass Sie woanders hingehen. Einige von uns neugierigen Typen verstehen gerne Dinge.
Jonathon Reinhart
36

Es ist undefiniertes Verhalten . Eine nicht initialisierte lokale Variable hat einen unbestimmten Wert, und ihre Verwendung führt zu undefiniertem Verhalten.

Ein Programmierer
quelle
6
Genauer gesagt ist die Verwendung einer einheitlichen Variablen, deren Adresse niemals verwendet wird, ein undefiniertes Verhalten.
Jens Gustedt
@JensGustedt Netter Kommentar. Haben Sie etwas zum Abschnitt „Das nächste Beispiel“ von blog.frama-c.com/index.php?post/2013/03/13/… zu sagen ?
Pascal Cuoq
@ PascalCuoq, dies scheint sogar eine laufende Diskussion im Normungsausschuss zu sein. Es gibt Situationen, in denen die Überprüfung des Speichers, den Sie über einen Zeiger erhalten, sinnvoll ist, auch wenn Sie nicht wissen können, ob er initialisiert ist oder nicht. Es ist zu restriktiv, es in allen Fällen einfach undefiniert zu machen.
Jens Gustedt
@JensGustedt: Wie führt die Verwendung der Adresse dazu, dass sie ein definiertes Verhalten verwendet? Sie weist { int uninit; &uninit; printf("%d\n", uninit); }immer noch ein undefiniertes Verhalten auf. Auf der anderen Seite können Sie jedes Objekt als Array von behandeln unsigned char. hast du das gedacht
Keith Thompson
@ KeithThompson, nein, es ist umgekehrt. Eine Variable so zu haben, dass ihre Adresse niemals genommen und nicht initialisiert wird, führt zu UB. Das Lesen eines unbestimmten Werts an sich ist kein undefiniertes Verhalten, der Inhalt ist einfach unvorhersehbar. Ab 6.3.2.1 p2: Wenn der l-Wert ein Objekt mit automatischer Speicherdauer bezeichnet, das mit der Registerspeicherklasse deklariert werden konnte (dessen Adresse nie vergeben wurde), und dieses Objekt nicht initialisiert ist (nicht mit einem Initialisierer deklariert und ihm nicht zugewiesen wurde) wurde vor der Verwendung durchgeführt), ist das Verhalten undefiniert.
Jens Gustedt
12

Eine wichtige Sache, an die Sie sich erinnern sollten - verlassen Sie sich niemals auf so etwas und verwenden Sie dies niemals in echtem Code! Es ist nur eine interessante Sache (die sogar nicht immer wahr ist), keine Funktion oder ähnliches. Stellen Sie sich vor, Sie versuchen, einen Fehler zu finden, der durch diese Art von "Feature" - Albtraum - hervorgerufen wird.

Übrigens. - C und C ++ sind voll von solchen "Funktionen". Hier ist eine großartige Diashow dazu: http://www.slideshare.net/olvemaudal/deep-c Wenn Sie also ähnliche "Funktionen" sehen möchten, verstehen Sie, was ist unter der Haube und wie es funktioniert, schauen Sie sich einfach diese Diashow an - Sie werden es nicht bereuen und ich bin sicher, dass selbst die meisten erfahrenen C / C ++ - Programmierer viel daraus lernen können.

Cyriel
quelle
7

In der Funktion Awird die Variable anicht initialisiert. Das Drucken ihres Werts führt zu undefiniertem Verhalten.

In einigen Compilern befinden sich die Variablen ain Aund ain Ban derselben Adresse, sodass sie möglicherweise gedruckt 5werden. Sie können sich jedoch auch hier nicht auf undefiniertes Verhalten verlassen.

Yu Hao
quelle
1
Das Tutorial ist zu 100% richtig, aber ob die Ergebnisse auf dem Originalposter s machine will be the same depends on the assembly generated by the compiler. As @JonathonReinhart pointed out the call to B () `möglicherweise optimiert wurden.
Lloyd Crawley
1
Ich habe ein Problem mit der Aussprache "Das Tutorial ist falsch". Hast du dir das Tutorial tatsächlich angesehen? Es geht nicht darum, Ihnen beizubringen, wie man so etwas Verrücktes macht, sondern zu demonstrieren, wie der Aufrufstapel funktioniert. In diesem Fall ist das Tutorial vollständig korrekt.
Jonathon Reinhart
@ JonathonReinhart Ich habe das Tutorial nicht gesehen, dachte, dieses Beispiel stammt aus dem Tutorial, ich werde diesen Teil entfernen.
Yu Hao
@LloydCrawley Ich habe den Teil über das Tutorial entfernt. Ich weiß, es geht um Stapelarchitektur, das habe ich damit gemeint, dass sie sich beim Drucken unter derselben Adresse befinden 5, aber anscheinend hat Jonathon Reinhart eine viel bessere Erklärung.
Yu Hao
7

Kompilieren Sie Ihren Code mit gcc -Wall filename.cSie werden diese Warnungen sehen.

In function 'B':
11:9: warning: variable 'a' set but not used [-Wunused-but-set-variable]

In function 'A':
6:11: warning: 'a' is used uninitialized in this function [-Wuninitialized]  

In c Das Drucken einer nicht initialisierten Variablen führt zu einem undefinierten Verhalten.

In Abschnitt 6.7.8 Initialisierung des C99-Standards heißt es

Wenn ein Objekt mit automatischer Speicherdauer nicht explizit initialisiert wird, ist sein Wert unbestimmt. Wenn ein Objekt mit statischer Speicherdauer nicht explizit initialisiert wird, gilt Folgendes:

 if it has pointer type, it is initialized to a null pointer;
 if it has arithmetic type, it is initialized to (positive or unsigned) zero;
 if it is an aggregate, every member is initialized (recursively) according to these rules;
 if it is a union, the first named member is initialized (recursively) according to these rules.

Edit1

As @Jonathon Reinhart Wenn Sie die Optimierung mit dem -OFlag Using deaktivieren, erhalten gcc-O0 Sie möglicherweise Ausgabe 5.

Dies ist jedoch überhaupt keine gute Idee. Verwenden Sie dies niemals im Produktionscode.

-Wuninitialized Dies ist eine der wertvollen Warnungen, die Sie in Betracht ziehen sollten. Sie sollten diese Warnung, die zu großen Schäden in der Produktion führt, wie z. B. Abstürze beim Ausführen von Dämonen, weder deaktivieren noch überspringen.


Edit2

Deep C- Folien erklärt Warum das Ergebnis 5 / Garbage ist. Hinzufügen dieser Informationen aus diesen Folien mit geringfügigen Änderungen, um diese Antwort etwas effektiver zu machen.

Fall 1: ohne Optimierung

$ gcc -O0 file.c && ./a.out  
5

Möglicherweise verfügt dieser Compiler über einen Pool benannter Variablen, die er wiederverwendet. ZB wurde die Variable a verwendet und freigegeben. B()Wenn A()ein ganzzahliger Name benötigt awird, erhält die Variable denselben Speicherort. Wenn Sie die Variable umbenennen in B(), sagen wir b, dann glaube ich nicht , erhalten Sie 5.

Fall 2: mit Optimierung

Wenn der Optimierer aktiviert wird, kann eine Menge passieren. In diesem Fall würde ich vermuten, dass der Aufruf von B()übersprungen werden kann, da er keine Nebenwirkungen hat. Auch wäre ich nicht überrascht, wenn das A()eingefügt ist main(), dh kein Funktionsaufruf. (Da A ()der Linker jedoch sichtbar ist, muss der Objektcode für die Funktion dennoch erstellt werden, falls eine andere Objektdatei mit der Funktion verknüpft werden soll.) Ich vermute jedenfalls, dass der gedruckte Wert etwas anderes ist, wenn Sie den Code optimieren.

gcc -O file.c && ./a.out
1606415608  

Müll!

Gangadhar
quelle
1
Ihre Logik in Edit 2, Fall 1 ist völlig falsch. Das ist nicht überhaupt , wie es funktioniert. Der Name der lokalen Variablen bedeutet absolut nichts.
Jonathon Reinhart
@ JonathonReinhart Wie in der Antwort erwähnt, habe ich dies von DeepC-Folien hinzugefügt. Bitte erläutern Sie, auf welcher Grundlage es falsch ist.
Gangadhar
3
Es gibt keine Zuordnung zwischen Stapelspeicher und Variablennamen. Das Beispiel beruht auf der Tatsache, dass konzeptionell der Stapelrahmen im zweiten Funktionsaufruf einfach den Stapelrahmen des zweiten Funktionsaufrufs überlagert. Es spielt keine Rolle, wie die Namen lauten. Solange beide Methodensignaturen identisch sind, kann dasselbe passieren. Wie andere bereits betont haben, würde der Stapel zufällige Werte enthalten, wenn er sich in einem eingebetteten System befindet und zwischen den Aufrufen von A () und B () ein Hardware-Interrupt bedient wird. Alte Tools wie Code Guard für Borland ermöglichten das Schreiben von Nullen vor jedem Aufruf.
Dan Haynes
@DanHaynes Ihr Kommentar überzeugt mich. Der Stapelrahmen im zweiten Funktionsaufruf kann den Stapelrahmen des ersten Funktionsaufrufs überlagern, sofern Variablentyp und Funktionsprototyp identisch sind. Ja, ich stimme auch zu, da es nichts mit Variablennamen zu tun hat.
Gangadhar