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
    

    .
    Das

    sub 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


Anmelden zum Antworten