Den Assemblercode verstehen, der von einem einfachen C-Programm generiert wird

Den Assemblercode verstehen, der von einem einfachen C-Programm generiert wird


Ich versuche, den Code auf Assemblerebene für ein einfaches C-Programm zu verstehen, indem ich ihn mit dem Disassembler von gdb inspiziere.


Es folgt der C-Code:


#include <stdio.h>
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
}
void main() {
function(1,2,3);
}

Es folgt der Demontagecode für beide main und function


gdb) disass main
Dump of assembler code for function main:
0x08048428 <main+0>: push %ebp
0x08048429 <main+1>: mov %esp,%ebp
0x0804842b <main+3>: and $0xfffffff0,%esp
0x0804842e <main+6>: sub $0x10,%esp
0x08048431 <main+9>: movl $0x3,0x8(%esp)
0x08048439 <main+17>: movl $0x2,0x4(%esp)
0x08048441 <main+25>: movl $0x1,(%esp)
0x08048448 <main+32>: call 0x8048404 <function>
0x0804844d <main+37>: leave
0x0804844e <main+38>: ret
End of assembler dump.
(gdb) disass function
Dump of assembler code for function function:
0x08048404 <function+0>: push %ebp
0x08048405 <function+1>: mov %esp,%ebp
0x08048407 <function+3>: sub $0x28,%esp
0x0804840a <function+6>: mov %gs:0x14,%eax
0x08048410 <function+12>: mov %eax,-0xc(%ebp)
0x08048413 <function+15>: xor %eax,%eax
0x08048415 <function+17>: mov -0xc(%ebp),%eax
0x08048418 <function+20>: xor %gs:0x14,%eax
0x0804841f <function+27>: je 0x8048426 <function+34>
0x08048421 <function+29>: call 0x8048340 <[email protected]>
0x08048426 <function+34>: leave
0x08048427 <function+35>: ret
End of assembler dump.

Ich suche Antworten für folgende Dinge:



  1. wie die Adressierung funktioniert, ich meine (main+0) , (main+1), (main+3)

  2. Warum wird im Wesentlichen $0xffffff0,%esp verwendet

  3. Warum wird in der Funktion %gs:0x14,%eax , %eax,-0xc(%ebp) verwendet.

  4. Wenn jemand erklären kann, wie Schritt für Schritt vor sich geht, wird das sehr geschätzt.


Antworten:


Der Grund für die "seltsamen" Adressen wie main+0 , main+1 , main+3 , main+6 usw. liegt daran, dass jeder Befehl eine variable Anzahl von Bytes belegt. Zum Beispiel:


main+0: push %ebp

ist eine Ein-Byte-Anweisung, also ist die nächste Anweisung bei main+1 . Andererseits


main+3: and $0xfffffff0,%esp

ist eine Drei-Byte-Anweisung, also ist die nächste Anweisung danach bei main+6 .


Und da fragst du in den Kommentaren warum movl scheint eine variable Anzahl von Bytes zu nehmen, die Erklärung dafür ist wie folgt.


Die Befehlslänge hängt nicht nur vom Opcode ab (z. B. movl ), sondern auch die Adressierungsmodi für die Operanden auch (die Dinge, auf denen der Opcode operiert). Ich habe nicht speziell nach Ihrem Code gesucht, aber ich vermute das


movl $0x1,(%esp)

Die Anweisung ist wahrscheinlich kürzer, weil kein Offset beteiligt ist - sie verwendet nur esp als Adresse. Während so etwas wie:


movl $0x2,0x4(%esp)

erfordert alles, was movl $0x1,(%esp) tut, plus ein zusätzliches Byte für den Offset 0x4 .


Tatsächlich ist hier eine Debug-Sitzung, die zeigt, was ich meine:


Microsoft Windows XP [Version 5.1.2600]
(C) Copyright 1985-2001 Microsoft Corp.
c:\pax> debug
-a
0B52:0100 mov word ptr [di],7
0B52:0104 mov word ptr [di+2],8
0B52:0109 mov word ptr [di+0],7
0B52:010E
-u100,10d
0B52:0100 C7050700 MOV WORD PTR [DI],0007
0B52:0104 C745020800 MOV WORD PTR [DI+02],0008
0B52:0109 C745000700 MOV WORD PTR [DI+00],0007
-q
c:\pax> _

Sie können sehen, dass sich die zweite Anweisung mit einem Offset tatsächlich von der ersten ohne Offset unterscheidet. Es ist ein Byte länger (5 Bytes statt 4, um den Offset zu halten) und hat tatsächlich eine andere Codierung c745 statt c705 .


Sie können auch sehen, dass Sie die erste und dritte Anweisung auf zwei verschiedene Arten codieren können, aber sie tun im Grunde dasselbe.



Die and $0xfffffff0,%esp Anweisung ist eine Möglichkeit, esp zu erzwingen an einer bestimmten Grenze sein. Dies wird verwendet, um eine korrekte Ausrichtung der Variablen sicherzustellen. Viele Speicherzugriffe auf modernen Prozessoren sind effizienter, wenn sie den Ausrichtungsregeln folgen (z. B. dass ein 4-Byte-Wert an einer 4-Byte-Grenze ausgerichtet werden muss). Einige moderne Prozessoren lösen sogar einen Fehler aus, wenn Sie diese Regeln nicht befolgen.


Nach dieser Anweisung ist Ihnen garantiert, dass esp ist kleiner oder gleich seinem vorherigen Wert und an einer 16-Byte-Grenze ausgerichtet.



Die gs: Präfix bedeutet einfach, den gs zu verwenden Segmentregister für den Zugriff auf den Speicher anstelle des Standardwerts.


Die Anweisung mov %eax,-0xc(%ebp) bedeutet, den Inhalt von ebp zu nehmen registrieren, 12 abziehen (0xc ) und geben Sie dann den Wert eax ein in diesen Speicherplatz.



Re die Erklärung des Codes. Ihr function Funktion ist im Grunde ein großes No-Op. Die generierte Assemblierung beschränkt sich auf den Stack-Frame-Aufbau und -Abbau, zusammen mit einigen Stack-Frame-Korruptionsprüfungen, die den oben erwähnten %gs:14 verwenden Speicherort.


Es lädt den Wert von diesem Speicherort (wahrscheinlich so etwas wie 0xdeadbeef ) in den Stapelrahmen, erledigt seine Aufgabe und überprüft dann den Stapel, um sicherzustellen, dass er nicht beschädigt wurde.


Seine Aufgabe ist in diesem Fall nichts. Sie sehen also nur die Funktionsverwaltung.


Der Stapelaufbau erfolgt zwischen function+0 und function+12 . Alles danach richtet den Rückkehrcode in eax ein und Abbau des Stapelrahmens, einschließlich der Korruptionsprüfung.


Ebenso main bestehen aus dem Stack-Frame-Setup und dem Pushen der Parameter für function , Aufruf von function , den Stapelrahmen herunterreißen und beenden.


Kommentare wurden in den folgenden Code eingefügt:


0x08048428 <main+0>:    push   %ebp                 ; save previous value.
0x08048429 <main+1>: mov %esp,%ebp ; create new stack frame.
0x0804842b <main+3>: and $0xfffffff0,%esp ; align to boundary.
0x0804842e <main+6>: sub $0x10,%esp ; make space on stack.
0x08048431 <main+9>: movl $0x3,0x8(%esp) ; push values for function.
0x08048439 <main+17>: movl $0x2,0x4(%esp)
0x08048441 <main+25>: movl $0x1,(%esp)
0x08048448 <main+32>: call 0x8048404 <function> ; and call it.
0x0804844d <main+37>: leave ; tear down frame.
0x0804844e <main+38>: ret ; and exit.
0x08048404 <func+0>: push %ebp ; save previous value.
0x08048405 <func+1>: mov %esp,%ebp ; create new stack frame.
0x08048407 <func+3>: sub $0x28,%esp ; make space on stack.
0x0804840a <func+6>: mov %gs:0x14,%eax ; get sentinel value.
0x08048410 <func+12>: mov %eax,-0xc(%ebp) ; put on stack.
0x08048413 <func+15>: xor %eax,%eax ; set return code 0.
0x08048415 <func+17>: mov -0xc(%ebp),%eax ; get sentinel from stack.
0x08048418 <func+20>: xor %gs:0x14,%eax ; compare with actual.
0x0804841f <func+27>: je <func+34> ; jump if okay.
0x08048421 <func+29>: call <_stk_chk_fl> ; otherwise corrupted stack.
0x08048426 <func+34>: leave ; tear down frame.
0x08048427 <func+35>: ret ; and exit.


Ich denke, der Grund für die %gs:0x14 kann von oben ersichtlich sein, aber für alle Fälle werde ich hier näher darauf eingehen.


Es verwendet diesen Wert (einen Sentinel), um den aktuellen Stack-Frame einzufügen, damit, falls etwas in der Funktion etwas Dummes tut, wie 1024 Bytes in ein 20-Byte-Array schreiben, das auf dem Stack erstellt wurde, oder, in Ihrem Fall:


char buffer1[5];
strcpy (buffer1, "Hello there, my name is Pax.");

dann wird der Sentinel überschrieben und die Überprüfung am Ende der Funktion wird dies erkennen, die Fehlerfunktion aufrufen, um Sie darüber zu informieren, und dann wahrscheinlich abbrechen, um weitere Probleme zu vermeiden.


Wenn es 0xdeadbeef platziert hat auf den Stapel und dies wurde zu etwas anderem geändert, dann zu einem xor mit 0xdeadbeef würde einen Wert ungleich Null erzeugen, der im Code mit dem je erkannt wird Anleitung.


Das relevante Bit ist hier paraphrasiert:


          mov    %gs:0x14,%eax     ; get sentinel value.
mov %eax,-0xc(%ebp) ; put on stack.
;; Weave your function
;; magic here.
mov -0xc(%ebp),%eax ; get sentinel back from stack.
xor %gs:0x14,%eax ; compare with original value.
je stack_ok ; zero/equal means no corruption.
call stack_bad ; otherwise corrupted stack.
stack_ok: leave ; tear down frame.