Ubuntu 15.10, Kernel 4.2.0, x86-64, GCC 5.2.1 Beispiel
Genug Standards, schauen wir uns eine Implementierung an :-)
Lokale Variable
Standards: undefiniertes Verhalten.
Implementierung: Das Programm weist Stapelspeicherplatz zu und verschiebt niemals etwas an diese Adresse. Daher wird alles verwendet, was zuvor vorhanden war.
#include <stdio.h>
int main() {
int i;
printf("%d\n", i);
}
kompilieren mit:
gcc -O0 -std=c99 a.c
Ausgänge:
0
und dekompiliert mit:
objdump -dr a.out
zu:
0000000000400536 <main>:
400536: 55 push %rbp
400537: 48 89 e5 mov %rsp,%rbp
40053a: 48 83 ec 10 sub $0x10,%rsp
40053e: 8b 45 fc mov -0x4(%rbp),%eax
400541: 89 c6 mov %eax,%esi
400543: bf e4 05 40 00 mov $0x4005e4,%edi
400548: b8 00 00 00 00 mov $0x0,%eax
40054d: e8 be fe ff ff callq 400410 <printf@plt>
400552: b8 00 00 00 00 mov $0x0,%eax
400557: c9 leaveq
400558: c3 retq
Aus unserem Wissen über x86-64-Aufrufkonventionen:
%rdi
ist das erste printf-Argument, also die Zeichenfolge "%d\n"
an der Adresse0x4005e4
%rsi
ist also das zweite printf-Argument i
.
Es kommt von -0x4(%rbp)
, was die erste lokale 4-Byte-Variable ist.
Zu diesem Zeitpunkt wurde rbp
auf der ersten Seite der Stapel vom Kernel zugewiesen. Um diesen Wert zu verstehen, sollten wir uns den Kernel-Code ansehen und herausfinden, auf was er diesen Wert setzt.
TODO Setzt der Kernel diesen Speicher auf etwas, bevor er ihn für andere Prozesse wiederverwendet, wenn ein Prozess stirbt? Wenn nicht, könnte der neue Prozess den Speicher anderer abgeschlossener Programme lesen und Daten verlieren. Siehe: Sind nicht initialisierte Werte jemals ein Sicherheitsrisiko?
Wir können dann auch mit unseren eigenen Stack-Modifikationen spielen und lustige Dinge schreiben wie:
#include <assert.h>
int f() {
int i = 13;
return i;
}
int g() {
int i;
return i;
}
int main() {
f();
assert(g() == 13);
}
Lokale Variable in -O3
Implementierungsanalyse unter: Was bedeutet <Wert optimiert aus> in GDB?
Globale Variablen
Standards: 0
Implementierung: .bss
Abschnitt.
#include <stdio.h>
int i;
int main() {
printf("%d\n", i);
}
gcc -00 -std=c99 a.c
kompiliert zu:
0000000000400536 <main>:
400536: 55 push %rbp
400537: 48 89 e5 mov %rsp,%rbp
40053a: 8b 05 04 0b 20 00 mov 0x200b04(%rip),%eax # 601044 <i>
400540: 89 c6 mov %eax,%esi
400542: bf e4 05 40 00 mov $0x4005e4,%edi
400547: b8 00 00 00 00 mov $0x0,%eax
40054c: e8 bf fe ff ff callq 400410 <printf@plt>
400551: b8 00 00 00 00 mov $0x0,%eax
400556: 5d pop %rbp
400557: c3 retq
400558: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
40055f: 00
# 601044 <i>
sagt das i
ist an adresse 0x601044
und:
readelf -SW a.out
enthält:
[25] .bss NOBITS 0000000000601040 001040 000008 00 WA 0 0 4
Das heißt, es 0x601044
befindet sich genau in der Mitte des .bss
Abschnitts, der bei 0x601040
8 Bytes beginnt und 8 Byte lang ist.
Der ELF-Standard garantiert dann, dass der genannte Abschnitt .bss
vollständig mit Nullen gefüllt ist:
.bss
Dieser Abschnitt enthält nicht initialisierte Daten, die zum Speicherbild des Programms beitragen. Per Definition initialisiert das System die Daten mit Nullen, wenn das Programm gestartet wird. Der Abschnitt belegt keinen Dateibereich, wie durch den Abschnittstyp angegeben SHT_NOBITS
.
Darüber hinaus ist der Typ SHT_NOBITS
effizient und belegt keinen Platz in der ausführbaren Datei:
sh_size
Dieses Mitglied gibt die Größe des Abschnitts in Bytes an. Sofern der SHT_NOBITS
Abschnittstyp nicht lautet , belegt der Abschnitt sh_size
Bytes in der Datei. Ein Abschnitt vom Typ SHT_NOBITS
kann eine Größe ungleich Null haben, belegt jedoch keinen Platz in der Datei.
Dann ist es an dem Linux-Kernel, diesen Speicherbereich auf Null zu setzen, wenn das Programm beim Start in den Speicher geladen wird.