Stack
-
Hallo!
Ich habe drei Fragen bezüglich der Funktionsweise des Stacks. Undzwar habe ich folgendes Beispiel in C:
void test_function(int, int, int, int); int main(int argc, char **argv) { test_function(1, 2, 3, 4); } void test_function(int a, int b, int c, int d) { int flag; char buffer[10]; flag = 31337; buffer[0] = 'A'; }
Wie ihr sehen könnt, macht das Programm nichts weiter, außer eine Funktion mit 4 Parametern aufzurufen. Wie gesagt, es soll nur um die Demonstration des Stacks gehen. Das Beispiel habe ich aus einem Buch und dort wurde nur grob der Stack skizziert und ich wollte bewusst durch probieren und eigenes Untersuchen die Feinheiten verstehen (so kann ich persönlich besser lernen und es macht mehr Spaß :D). Hier sind die Ergebnisse und die Fragen dazu. Falls etwas falsch an meiner Ausführung ist, bitte korrigieren.
Als erstes habe ich die beiden Funktionen mit dem gdb angeguckt. Hier die main-Funktion:
0x08048344 <main+0>: push ebp 0x08048345 <main+1>: mov ebp,esp 0x08048347 <main+3>: sub esp,0x18 0x0804834a <main+6>: and esp,0xfffffff0 0x0804834d <main+9>: mov eax,0x0 0x08048352 <main+14>: sub esp,eax 0x08048354 <main+16>: mov DWORD PTR [esp+12],0x4 0x0804835c <main+24>: mov DWORD PTR [esp+8],0x3 0x08048364 <main+32>: mov DWORD PTR [esp+4],0x2 0x0804836c <main+40>: mov DWORD PTR [esp],0x1 0x08048373 <main+47>: call 0x0804837 <test_function> 0x08048378 <main+52>: leave 0x08048379 <main+53>: ret
Der Funktionsprolog der main-Funktion ist für mich nicht verständlich. Die Adresse, die in ebp vor dem push gespeichert war, war
0xbffff868
. Wenn ich jetzt die push Instruktion ausführe, wird esp (zu diesem Zeitpunkt mit der Adresse:0xbffff80c
) um 4 vermindert und und in der nächsten Instruktion wird 0x18 abgezogen. Dann komme ich auf die Adresse:0xbffff7f0
. Die restlichen Instruktionen verändern diesen Wert nicht mehr direkt (sowohl das and, als auch das weitere sub von 0). Warum sind sie trotzdem im Funktionsprolog enthalten und welche (mir noch nicht ersichtliche) Aufgabe haben sie?Und hier ist die test_function-Funktion:
0x0804837a <test_function+0>: push ebp 0x0804837b <test_function+1>: mov esp,ebp 0x0804837d <test_function+3>: sub esp,0x28 0x08048380 <test_function+6>: mov DWORD PTR [ebp-12],0x7a69 0x08048387 <test_function+13>: mov BYTE PTR [ebp-40],0x41 0x0804838b <test_function+17>: leave 0x0804838c <test_function+18>: ret
Nachdem ich das Programm dann mit run "test" (im gdb) gestartet habe und mir nach mehreren Breakpoints die Veränderungen (nach dem Funktionsprolog der test_function()) im Stack angeguckt habe, bin ich auf einen Aufbau gekommen, der auch kleine Ungereimtheiten hat. Für das bessere Verständnis, gebe ich zuerst die Register aus und danach den Bereich des Stacks, der interessant ist. Die wichtigen Stellen versehe ich mit Nummern, damit man sich besser zurechtfindet.
eip 0x08048380 0x08048380 <test_function+6> esp 0xbffff7c0 0xbffff7c0 ebp 0xbffff7e8 0xbffff7ea
0xbffff7c0: 0x00000000 0x08049548 0xbffff7d8 0x08048249 0xbffff7d0: 0xb7f9f729 0xb7fd6ff4 0xbffff808 0x080483b9 0xbffff7e0: 0xb7fd6ff4 0xbffff8a0 0xbffff808 (7) 0x08048378 (6) 0xbffff7f0: 0x00000001 0x00000002 0x00000003 0x00000004 (5) 0xbffff800: |0xb8000ce0| |0x080483a0| 0xbffff868 (4) 0xb7eafebc (3) 0xbffff810: 0x00000002 (2) 0xbffff894 (1)
Die Adresse (1) ist die Adresse, die in der argv Variable gespeichert ist. (2) ist die Anzahl argc. Wenn ich das richtig verstanden habe ist (3) die Rücksprungadresse, wenn main() beendet wird (zeigt auf die __libc_start_main(...) Funktion). (4) ist der SFP der eben genannten Funktion. Bis zu diesem Zeitpunkt besaß esp den Wert 0xbffff804 (schon nach der push Instruktion) und jetzt wird 0x18 abgezogen. Somit bleiben aber die 8 Bytes ab der Adresse 0xbffff800 ungenutzt (mit | makiert). Warum wird für den aktuellen Stackframe 8 Bytes zu viel reserviert? Hat dieser Puffer eine Bedeutung? In diesem Zusammenhang wollte ich nochmal den Begriff Stackframe aufgreifen. Beginnt der Stackframe der test_function() an (6), also der Rücksprungadresse in die main Funktion oder schon bereits an (5), also mit den Parametern für die test_function()?
Ich hoffe, ich habe mich einigermaßen verständlich ausgedrückt und lag mit meinen Beobachtungen (+ deren Schlussfolgerungen) nicht ganz falsch.
Ein schönes Wochenende,
Endian
-
Mein GCC 4.7.2 auf Debian Wheezy64 erstellt einen etwas anderen Assembler-Code. Ich versuche mich mal trotzdem an einer Antwort:
GCC "aligned" den Stack auf 16, d.h. ESP ist immer durch 16 teilbar. Während das für 64-bit-Systeme im jeweiligen "ABI" und den jeweiligen "calling conventions" vorgeschrieben ist, ist das für 32-bit-Systeme meines Wissens nur für MacOSX vorgeschrieben. Es kann aber nicht schaden. Der Sinn ist einerseits Geschwindigkeit (ein Speicherzugriff bringt in jedem Fall sämtliche Informationen, ein zweiter Speicherzugriff ist nicht erforderlich) und andererseits das erforderliche Alignment für bestimmte SSE-Instruktionen in der C-Bibliothek.
Zum Stackframe gehören sämtliche Adressen, auf die die Funktion zugreift (bzw. zugreifen darf), also: Argumente, Rücksprungadresse und lokale Variablen, in deinem Beispiel also (5). Eigentlich gehört dazu auch der zuviel reservierte Stack. Faustregel: Stackframes passen lückenlos aneinander.
Mein GCC produziert kein
mov eax,0x0 \ sub esp,eax
. Ich weiß auch nicht, wofür das gut sein soll. Ich vermute, dass es sich um einen Lückenfüller für eine optionale Sicherheitsmaßnahme gegen Stack-Overflows handelt.viele grüße
ralph
-
Der Sinn ist einerseits Geschwindigkeit (ein Speicherzugriff bringt in jedem Fall sämtliche Informationen, ein zweiter Speicherzugriff ist nicht erforderlich)
Wenn ich nur 8 Bytes habe, dürfte doch trotzdem auch nur ein Speicherzugriff benötigt werden. Wenn ich z.B. 17 Bytes habe und dann 32 Bytes reserviert werden, macht das doch keinen Geschwindigkeitsunterschied, oder? (Wahrscheinlich fehlt mir da ein Detail, um das zu verstehen).
Ansonsten danke für deine Antwort :).
-
push ebp mov ebp, esp
Ist die Vorbereitung für das
leave
am ende.
sub esp,0x18
verschiebt den Stackpointer und macht damit Platz für die vier Parameter und die return adresse des calls auf test_function.
Dann schiebt er die Konstanten auf den Stack und ruft die Funktion auf.Dein Programm ist ein 64 Bit Programm und es gibt kein
push
für 32 bits als Konstante. Deine Parameter sind aber 32 Bit Werte. Daher dieser Umweg über
mov
.
Dassub esp,eax
ist ein überbleibsel vom einrichten der lokalen Variablen auf dem Stack. Du hast keine, also wird der Stack um 0 Bytes verschoben.
leave
kopiert den base pointer wieder in den Stackpointer, womit der Ausgangzustand wieder hergestellt wird, auch wenn der Stackpointer zwischendurch zerstört/verändert worden wäre, und räumt damit auch die Parameter an test_function und die nicht vorhandenen lokalen Variablen vom Stack.
-
Endian schrieb:
Der Sinn ist einerseits Geschwindigkeit (ein Speicherzugriff bringt in jedem Fall sämtliche Informationen, ein zweiter Speicherzugriff ist nicht erforderlich)
Wenn ich nur 8 Bytes habe, dürfte doch trotzdem auch nur ein Speicherzugriff benötigt werden. Wenn ich z.B. 17 Bytes habe und dann 32 Bytes reserviert werden, macht das doch keinen Geschwindigkeitsunterschied, oder? (Wahrscheinlich fehlt mir da ein Detail, um das zu verstehen).
Ansonsten danke für deine Antwort :).
Wie gesagt ist ein so großes Alignment eigentlich nicht erforderlich, bei einem 32-bit Programm reicht normalerweise ein Alignment von 4. Das "Start"-Alignment wird mit der AND-Instruktion festgelegt, spätere Alignments erreicht der Compiler mit einer "zu großen" SUB-Instruktion. Der GCC sortiert zusätzlich lokale Variablen um, so dass sie jeweils ideal aligned sind.
Manchmal muss eine XMM/SSE-Variable auf 16 aligned werden. Ist sie das nicht, produzieren manche Befehle einen Absturz. Es liegt deshalb auf der Hand, gleich auf 16 zu alignen, insbesondere weil dadurch kein Mehraufwand entsteht. Wenn Dir dadurch (unwahrscheinlicherweise) der Stack nicht ausreicht, dann gibt es bei GCC die Kommandozeilenoption
-mno-stack-align
.viele grüße
ralph