[[:tux|{{ :linux.png?40|}}]]
===== "Hello World!" im Debugger =====
Das Debuggen eines Programms gehört sicherlich zu den anspruchsvollsten Angelegenheiten, kann aber unter gegebenen Umständen äußerst nützlich sein.
==== Der Quellcode ====
Das folgende in C geschriebene Programm wird bei der Ausführung 10x die Zeile >>Hello, World!<< auf der Standardausgabe anzeigen:
#include
int main()
{
int i;
for(i=0; i < 10; i++)
{
printf("Hello, World!\n");
}
return 0;
}
Kompiliert man diesen Code mit der Option >>-g<< werden Debug-Informationen in Form des Quellcodes mit in die binäre Datei einkompiliert, um eine spätere Analyse zu erleichtern:
$ gcc -g ~/firstprog.cOhne Angabe einer Ausgabedatei (>>-o //output_file//<<) wird daraus das binäre Programm >>a.out<<:
$ ~/a.out
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
==== Disassemble Binary ====
Mit dem Debugger >>gdb<< lässt sich nun dieses Programm untersuchen:
$ gdb -q ./a.out
Reading symbols from /home/pronto/booksrc/a.out...done.
(gdb) list
1 #include
2
3 int main()
4 {
5 int i;
6 for(i=0; i < 10; i++)
7 {
8 printf("Hello World!\n");
9 }
10 }
(gdb) disassemble main
Dump of assembler code for function main:
//0x080483b4 <+0>: push ebp
0x080483b5 <+1>: mov ebp,esp
0x080483b7 <+3>: and esp,0xfffffff0
0x080483ba <+6>: sub esp,0x20//
⇒ 0x080483bd <+9>: mov DWORD PTR [esp+0x1c],0x0
0x080483c5 <+17>: jmp 0x80483d8
0x080483c7 <+19>: mov DWORD PTR [esp],0x80484b0
0x080483ce <+26>: call 0x80482f0
0x080483d3 <+31>: add DWORD PTR [esp+0x1c],0x1
0x080483d8 <+36>: cmp DWORD PTR [esp+0x1c],0x9
0x080483dd <+41>: jle 0x80483c7
0x080483df <+43>: leave
0x080483e0 <+44>: ret
End of assembler dump.
(gdb) break main
Breakpoint 1 at 0x80483bd: file firstprog.c, line 6.
(gdb) run
Starting program: /home/pronto/booksrc/a.out
Breakpoint 1, main () at firstprog.c:6
6 for(i=0; i < 10; i++)
(gdb) i r $eip
eip 0x80483bd 0x80483bd
Die og Kommandos nach dem gdb-Prompt >>(gdb)<< haben folgende Bedeutung:
* >>list<<: Gibt den mit der Option >>-g<< einkompilierten Quellcode wieder.
* >>disassemble main<<: Disassemblierung der main() Funktion. Hier wird die main() Funktion in der Maschinensprache des vorliegenden Prozessors ausgegeben, wobei >>gdb<< hier auf die Intel Syntax eingestellt wurde (>>set disassembly-flavor intel<<)
* >>break main<<: Damit wird der Debugger angewiesen an einem bestimmten Punkt (hier >>main<<) anzuhalten.
* >>run<<: das eigentliche Programm wird gestartet, wobei der Haltepunkt am Anfang der main() Fuktion den Debugger schon zum Stehen bringt, noch bevor irgendwelche Anweisungen der main() Funktion ausgeführt werden.
* >>i r $eip<<: (Auch >>info register $eip) Damit der Prozessor ein Programm ausführt, muss sein Befehlszeiger auf den Anfang des Programms zeigen, also die Adresse der ersten Maschinencodeanweisung in das spezielle Register Befehlszeiger (instruction pointer eip) geladen werden. Der Prozessor wird dann den auf diese Weise bezeichneten Befehl ausführen und im Normalfall anschließend den Inhalt des Befehlszeigers um die Länge des Befehls im Speicher erhöhen, so dass er auf die nächste Maschinenanweisung zeigt. Bei einem Sprungbefehl wird der Befehlszeiger nicht um die Länge des Befehls, sondern um die angegebene relative Zieladresse erhöht oder erniedrigt. Das Kommando >>i r $eip<< zeigt auf diese Adresse (Blau dargestellt).
Der Programmablauf steht demnach aktuell bei Initialisierung der for-Schleife for(i=0; i < 10;i++) und das Befehlszeiger Register zeigt auf 0x080483bd <+9> in dem folgender Maschinenbefehl steht:
''mov DWORD PTR [esp+0x1c],0x0''
Diese Assembler-Instuktion bewegt den Wert >>0<< ([esp+0x1c],0x0) in die um >>0x1c<< erhöhte Speicheradresse ([esp+0x1c],0x0), die im ESP-Register steht ([esp+0x1c],0x0). Das ist die Stelle, an der die C-Variable >>i<< im Speicher abgelegt ist. >>i<< wird als Integerwert deklariert, der bei x86 Prozessoren 4 Byte im Speicher belegt. Lässt man sich den Inhalt dieser Adresse nun anzeigen, wird ein zufälliger Inhalt ausgegeben, weil der Programmablauf noch vor der Zuweisung der Variable >>i<< steht:
(gdb) x/4xb $esp + 0x1c
0xbffff6fc: 0xf4 0xbf 0x28 0x00
Dieses Kommando zählt zu der in >>$esp<< gespeicherten Adresse >>0x1c<< Adressen hinzu, was die Adresse >>0xbffff6fc<< ergibt und zeigt den Inhalt dieser Adresse (>>0xf4 0xbf 0x28 0x00<<) in vier hexadezimal dargestellten Byte >>4xb<< an. Das vorangestellte >>x<< bedeutet in diesem Kommando >>examine<< (untersuche). Um die weitere Analyse dieser Adresse zu vereinfachen, kann diese auch in eine Variable gespeichert werden:
(gdb) print $esp + 0x1c
$1 = (void *) 0xbffff6fc
Noch immer aber steht der Programmablauf noch vor dem Ausführen der ersten Instruktion, diese kann jetzt mit dem >>nexti<< (next instruction) Kommando ausgeführt werden:
(gdb) nexti
0x080483c5 6 for(i=0; i < 10; i++)
Das Auslesen der Speichersdresse >>0xbffff6fc<< ergibt jetzt erwartungsgemäß >>0x00<<, weil jetzt die Instruktion >>i=0<< durchgeführt wurde:
(gdb) x/4xb $1
0xbffff6fc: 0x00 0x00 0x00 0x00
''jmp 0x80483d8 ''
Der Befehlszeiger ist nun auf die nächste Adresse gesprungen und zeigt jetzt auf die Adresse >>0x80483c5<<, welche jedoch nur eine //Jump//-Instruktion >>jmp<< auf die Adresse >>0x80483d8<< ist.
(gdb) x/i $eip
=> 0x80483c5 : jmp 0x80483d8
(gdb) nexti
0x080483d8 6 for(i=0; i < 10; i++)
Danach rückt EIP weiter zur nächsten Instruktion, wobei die folgenden Instruktionen am besten im Zusammenhang erklärt werden.
=== if-then-else ===
Die erste Instruktion >>cmp<< ist eine Vergleichsinstruktion, die den von der C-Variablen >>i<< verwendeten Speicher mit dem Wert >>0x9<< vergleicht.
''0x080483d8 <+36>: cmp DWORD PTR [esp+0x1c],0x9''
Die nächste Instruktion >>jle<< steht für //Jump if less than or euqal to// (>>springe bei kleiner oder gleich zu<<). Sie verwendet das Ergebnis des vorangegangenen Vergleichs, welcher im EFLAGS-Register gespeichert wird, um EIP auf einen anderen Teil des Codes zeigen zu lassen, wenn das Ziel der Vergleichsoperation kleiner oder gleich der Quelle ist. In diesem Fall besagt die Instruktion, dass an die Adresse >>0x80483c7<< gesprungen werden soll, wenn der Wert für die variable >>i<< kleiner, gleich 9 ist.
''0x080483dd <+41>: jle 0x80483c7 ''
Ist das nicht der Fall, wird EIP zur nächsten Adresse >>0x080483df<< geschickt, welche in unserm Fall eine >>leave<< (verlassen) Instruktion beinhaltet.
''0x080483df <+43>: leave''
Die Kombination dieser drei Instruktionen ergibt eine //if-then-else//-Kontrollstruktur: //Wenn >>i<< kleiner oder gleich 9 ist, springe zur Adresse an >>0x80483c7<<, andernfalls springe eine Instruktion weiter zur Adresse >>0x080483df<<.//
Da wir wissen, dass der Wert in dem Speicher abgelegt wird, der mit dem Wert 9 verglichen wird und da wir wissen, dass 0 kleiner oder gleich 9 ist, sollte EIP bei >>0x80483c7<< liegen, nachdem diese beiden Instruktionen ausgeführt wurden:
(gdb) x/i $eip
=> 0x80483d8 : cmp DWORD PTR [esp+0x1c],0x9
(gdb) nexti
0x080483dd 6 for(i=0; i < 10; i++)
(gdb) x/i $eip
=> 0x80483dd : jle 0x80483c7
(gdb) nexti
8 printf("Hello World!\n");
(gdb) x/2i $eip
=> 0x80483c7 : mov DWORD PTR [esp],0x80484b0
=> 0x80483ce : call 0x80482f0
(gdb)
Erwartungsgemäß haben diese beiden vorausgegangenen Instruktionen die Programmausführung an >>0x80483c7<< springen lassen, was uns zu den nächsten zwei Instruktionen >>mov<< und >>call<< führt. Die erste >>mov<<-Instruktion schreibt die Adresse >>0x80484b0<< in die Adresse, welche im ESP-Register enthalten ist. Das ESP-Regsiter ist derzeit folgendermaßen belegt:
(gdb) i r esp
esp 0xbffff6e0 0xbffff6e0
Nicht weiter von Belang aber die mov-Instruktion möchte die Adresse >>0x80484b0<< dort hin schreiben. Aber warum? Was ist so besonders an der Speicheradresse >>0x80484b0< Finden wir es raus und lassen wir uns mal die ersten 8 Byte dieser Adresse anzeigen:
(gdb) x/8xb 0x80484b0
0x80484b0: 0x48 0x65 0x6c 0x6c 0x6f 0x20 0x57 0x6f
Mit etwas Fantasie kommt man darauf, dass die Byte-Werte im Bereich der druckbaren ASCII-Zeichen liegen und hier nun unser Text >>Hello, World!<< gelandet sein könnte. GDB kann bei Zweifeln aber auch mit der Format-Option >>c<< automatisch in der ASCII-Tabelle nachschlagen:
(gdb) x/8**c**b 0x80484b0
0x80484b0: 72 'H' 101 'e' 108 'l' 108 'l' 111 'o' 32 ' ' 87 'W' 111 'o'
Die Format-Option >>s<< gibt dann den gesamten String aus, der sich dahinter verbirgt:
(gdb) x/s 0x80484b0
0x80484b0: "Hello World!"
Damit wissen wir nun, dass das Argument der >>printf()<< Funktion, nämlich der String >>"Hello World!"<< an der Adresse >>0x80484b0<< gespeichert ist und diese Adresse in die in ESP gespeicherte Adresse >>0xbffff6e0<< mit der >>mov<<-Instruktion übertragen wird. Führen wir die >>mov<<-Instruktion aus und lassen wir uns dann den Inhalt der an ESP gespeicherten Adresse anzeigen:
(gdb) nexti
0x080483ce 8 printf("Hello World!\n");
(gdb) x/xw $esp
0xbffff6e0: 0x080484b0
Erwartungsgemäß ist der Inhalt der Adresse >>0xbffff6e0<< nun die Adresse >>0x080484b0<<, in welcher unser String >>"Hello World!"<< gespeichert ist.
Der Befehlszeiger >>EIP<< steht danach an Adresse >>0x80483ce<<, in welcher eine >>call<<-Instruktion gespeichert ist:
(gdb) x/i $eip
=> 0x80483ce : call 0x80482f0
An dieser Stelle erfolgt der eigentliche Aufruf der printf()-Funktion, welche den Datenstring "Hello World!" ausgibt:
(gdb) nexti
Hello World!
6 for(i=0; i < 10; i++)
Der Befehlszeiger ist zur nächsten Adresse >>0x80483d3<< gesprungen, in welcher eine >>add<<-Instruktion gespeichert ist:
(gdb) x/i $eip
=> 0x80483d3 : add DWORD PTR [esp+0x1c],0x1
Hier wird der Wert >>0x1<< zu dem uns bereits bekannten Wert >>0x0<< in Adresse >>[esp+0x1c]<< addiert. Somit wird die letzte Anweisung in der >>for()<<-Schleife -> >>i++<< ausgeführt und der Wert >>i<< um >>1<< inkrementiert:
(gdb) nexti
0x080483d8 6 for(i=0; i < 10; i++)
(gdb) x/4xb $esp + 0x1c
0xbffff6fc: 0x01 0x00 0x00 0x00
Der erste Durchlauf der >>for()<<-Schleife ist nun abgeschlossen und der Befehlszeiger springt zur nächsten Adresse, welche wieder die >>cmp<<-Instruktion enthält und einen neuen Durchlauf der >>for()<<-Schleife anstößt. Dieses ganze Prozedere wiederholt sich nun solange, wie der Wert >>i<< in Adresse >>$esp + 0x1c<< ≤ 9 ist.
(gdb) x/2i $eip
=> 0x80483d8 : cmp DWORD PTR [esp+0x1c],0x9
0x80483dd : jle 0x80483c7
Hier folgt nun der zweite Durchlauf der for()-Schleife im Zusammenhang. Die kursiv gestellte >>cmp<<-Instruktion repräsentiert dann bereits den übernächsten Durchlauf der for()-Schleife:
(gdb) x/i $eip
=> 0x80483d8 : cmp DWORD PTR [esp+0x1c],0x9
(gdb) nexti
0x080483dd 6 for(i=0; i < 10; i++)
(gdb) x/i $eip
=> 0x80483dd : jle 0x80483c7
(gdb) nexti
8 printf("Hello World!\n");
(gdb) x/i $eip
=> 0x80483c7 : mov DWORD PTR [esp],0x80484b0
(gdb) nexti
0x080483ce 8 printf("Hello World!\n");
(gdb) x/i $eip
=> 0x80483ce : call 0x80482f0
(gdb) nexti
Hello World!
6 for(i=0; i < 10; i++)
(gdb) x/i $eip
=> 0x80483d3 : add DWORD PTR [esp+0x1c],0x1
(gdb) nexti
0x080483d8 6 for(i=0; i < 10; i++)
(gdb) x/4xb $esp + 0x1c
0xbffff6fc: 0x02 0x00 0x00 0x00
//(gdb)x/i $eip
=> 0x80483d8 : cmp DWORD PTR [esp+0x1c],0x9
(gdb)//
Schauen wir uns nun noch einmal das gesamte Disassembling an, kann der Verlauf der Schleife bzw. der gesamte Programmverlauf nachvollzogen werden:
(gdb) disassemble main
Dump of assembler code for function main:
//0x080483b4 <+0>: push ebp
0x080483b5 <+1>: mov ebp,esp
0x080483b7 <+3>: and esp,0xfffffff0
0x080483ba <+6>: sub esp,0x20//
0x080483bd <+9>: mov DWORD PTR [esp+0x1c],0x0
0x080483c5 <+17>: jmp 0x80483d8
⇓ 0x080483c7 <+19>: mov DWORD PTR [esp],0x80484b0
0x080483ce <+26>: call 0x80482f0
0x080483d3 <+31>: add DWORD PTR [esp+0x1c],0x1
0x080483d8 <+36>: cmp DWORD PTR [esp+0x1c],0x9
0x080483dd <+41>: jle 0x80483c7
0x080483df <+43>: leave
0x080483e0 <+44>: ret
End of assembler dump.
Die dunklegrün markierte Zeile mit der ersten >>mov<<-Instruktion initialisiert die Variable >>i<< und belegt diese mit >>0x0<<, die pink markierte Zeile mit der >>jmp<<-Instruktion springt in die Schleife zur >>cmp<<-Instruktion und die blau markierten Zeilen repräsentieren die eigentliche >>for()<<-Schleife in dem zuerst verglichen wird >>cmp<< und solange das Ergebnis dieses Vergleichs ≤ 9 ist >>jle<< der Instruktion Pointer >>EIP<< zur zweiten >>mov<<-Instruktion springt, wo die Adresse >>0x80484b0<< in >>ESP<< geschrieben wird (Wir erinnern uns, in dieser Adresse ist der String >>"Hello World!<< gespeichert), danach springt >>EIP<< weiter an die nächste Adresse mit der >>call<<-Instruktion, welche den in ESP gespeicherten String >>"Hello World!"<< ausgibt. >>EIP<< springt nun weiter zu >>add<<-Instruktion, wo >>0x1<< zu der Adresse >>[esp+0x1c]<< hinzu addiert wird und der Vergleich dieser Adresse mit ≤ 9 wird erneut durchgeführt.
Der Vollständigkeit wegen sollte noch gezeigt werden, dass die Schleife auch wie vorgesehen verlassen (>>leave<<) wird, wenn die Variable >>i<< den Wert >>0xa<< (1010) erreicht hat:
(gdb) x/4xb $esp + 0x1c
0xbffff6fc: 0x0a 0x00 0x00 0x00
(gdb) x/i $eip
=> 0x80483d8 : cmp DWORD PTR [esp+0x1c],0x9
(gdb) nexti
0x080483dd 6 for(i=0; i < 10; i++)
(gdb) x/i $eip
=> 0x80483dd : jle 0x80483c7
(gdb) nexti
10 }
(gdb) x/i $eip
=> 0x80483df : leave
(gdb)
Mächtig viel Zenober für ein Programm, welches in gerade mal 2 Millisekunden vom Prozessor ausgeführt wird. Ich habe ein ganzes Wochenende mit diesem Artikel verbracht, habe aber viel dabei gelernt ;-)
$ time ./a.out
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
real 0m0.002s
user 0m0.000s
sys 0m0.000s
--- //pronto 2011/10/09 15:21//