Inhaltsverzeichnis

"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:

~/firstprog.c

#include <stdio.h>
 
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.c
Ohne 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 <stdio.h>
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 <main+36> 0x080483c7 <+19>: mov DWORD PTR [esp],0x80484b0 0x080483ce <+26>: call 0x80482f0 <puts@plt> 0x080483d3 <+31>: add DWORD PTR [esp+0x1c],0x1 0x080483d8 <+36>: cmp DWORD PTR [esp+0x1c],0x9 0x080483dd <+41>: jle 0x80483c7 <main+19> 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 <main+9>

Die og Kommandos nach dem gdb-Prompt »(gdb)« haben folgende Bedeutung:

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 <main+36>

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.

<fc #800000>(gdb) x/i $eip
=> 0x80483c5 <main+17>:	jmp   0x80483d8 <main+36>
(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 <main+19>

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 <main+36>:	cmp    DWORD PTR [esp+0x1c],0x9
(gdb) nexti
0x080483dd	6	  for(i=0; i < 10; i++)
(gdb) x/i $eip
⇒ 0x80483dd <main+41>:	jle    0x80483c7 <main+19>
(gdb) nexti
8	    printf("Hello World!\n");
(gdb) x/2i $eip
=> 0x80483c7 <main+19>:	mov    DWORD PTR [esp],0x80484b0
=> 0x80483ce <main+26>:	call   0x80482f0 <puts@plt>
(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/8cb 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 <main+26>:	call   0x80482f0 <puts@plt>

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 <main+31>:	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 <main+36>:	cmp    DWORD PTR [esp+0x1c],0x9
   0x80483dd <main+41>:	jle    0x80483c7 <main+19>

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 <main+36>:	cmp    DWORD PTR [esp+0x1c],0x9
(gdb) nexti
0x080483dd	6	  for(i=0; i < 10; i++)
(gdb) x/i $eip
⇒ 0x80483dd <main+41>:	jle    0x80483c7 <main+19>
(gdb) nexti
8	    printf("Hello World!\n");
(gdb) x/i $eip
⇒ 0x80483c7 <main+19>:	mov    DWORD PTR [esp],0x80484b0
(gdb) nexti
0x080483ce	8	    printf("Hello World!\n");
(gdb) x/i $eip
⇒ 0x80483ce <main+26>:	call   0x80482f0 <puts@plt>
(gdb) nexti
Hello World!
6	  for(i=0; i < 10; i++)
(gdb) x/i $eip
⇒ 0x80483d3 <main+31>:	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 <main+36>:	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 <main+36>0x080483c7 <+19>: mov DWORD PTR [esp],0x80484b0 0x080483ce <+26>: call 0x80482f0 <puts@plt> 0x080483d3 <+31>: add DWORD PTR [esp+0x1c],0x1 0x080483d8 <+36>: cmp DWORD PTR [esp+0x1c],0x9 0x080483dd <+41>: jle 0x80483c7 <main+19> 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 <main+36>:	cmp    DWORD PTR [esp+0x1c],0x9
(gdb) nexti
0x080483dd	6	  for(i=0; i < 10; i++)
(gdb) x/i $eip
=> 0x80483dd <main+41>:	jle    0x80483c7 <main+19>
(gdb) nexti
10	}
(gdb) x/i $eip
=> 0x80483df <main+43>:	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