blog · git · desktop · images · contact & privacy · gopher


Assembler unter Linux: Debugging-Basics mit gdb

Wenn man frisch mit Assembler anfängt, hat man meistens ein Problem: Man hat keinen Überblick über den Speicher, die Register und überhaupt. Zumindest ging mir das so. Was genau steht jetzt an der Stelle „-4(%ebp)“? Kann ich mich darauf verlassen, dass ein uraltes Tutorial von irgendwo aus dem Internet heute immernoch stimmt? Vielleicht will ich mir mal lieber genauer anschauen, wie dieses oder jenes auf meinem System aussieht.

Das alles ist mit Assembler aber gar nicht so einfach. Selbst in C kann ich noch sehr einfach irgendwo ein „printf()“ einwerfen, um Vorgänge nachzuvollziehen. Natürlich kann ich mit Assembler auch „printf()“ aufrufen, aber am Anfang ist dieser Aufruf nicht unbedingt trivial. Und wer weiß: Vielleicht verfälscht er auf dieser niedrigen Ebene den Zustand.

Deswegen will ich hier versuchen, einen grundlegenden Überblick über den Umgang mit dem gdb zu geben. Es wird nicht um fortgeschrittene Themen gehen, sondern es soll nur ein Einstieg ermöglicht werden. Beim Assembler-Code lege ich daher auch Wert darauf, dass er leicht verständlich und nicht bis auf’s äußerste optimiert ist.

Da ich im Moment unterwegs bin und nur mein 64 Bit-Netbook zur Hand habe, sei betont, dass es diesmal im Gegensatz zu den vorherigen Postings 64 Bit-Code zu sehen gibt, der nur mit Anpassungen auf 32 Bit-Maschinen läuft.

Eigene Befehle in „~/.gdbinit

Es lohnt sich, den einen oder anderen Befehl zu definieren. Der Anfang meiner „~/.gdbinit“ sieht zum Beispiel so aus:

define state
printf "\n"
printf "---------------------------------------------------\n"
info registers
printf "---------------------------------------------------\n"
disassemble $pc
printf "---------------------------------------------------\n"
end

define stack
printf "--------------------------------------------------------------------------\n"
x/16xw $fp - 64
printf "--------------------------------------------------------------------------\n"
x/16xw $fp
printf "--------------------------------------------------------------------------\n"
end

define n
si
state
end

define upsa
break *_start
run
state
end

define upsf
break _start
run
state
end

define upma
break *main
run
state
end

define upmf
break main
run
state
end

Was hat es damit auf sich?

gdb hat übrigens eine ganz gute eingebaute Hilfe. Man sollte sich deswegen zum Beispiel mal „help x“ und die anderen Hilfeseiten durchlesen.

Beispiel 1: Lokationen nachvollziehen

Gegeben sei folgender Code:

.data
numbers:
    .long 0x10203040

.text
.global _start

_start:
    /* Frame Pointer. */
    pushq %rbp
    movq %rsp, %rbp

    /* Schaffe Platz für 4 Byte auf dem Stack. */
    subq $4, %rsp

    /* Schiebe die Zahl von "numbers" nach %eax und dann von dort auf
     * den Stack. */
    movl numbers, %eax
    movl %eax, (%rsp)

    /* Schaffe Platz für 4 weitere Byte und schiebe dann die Zahl
     * zusätzlich (nachdem 1 addiert wurde, damit man sie unterschieden
     * kann) vor die bereits existierende Zahl auf den Stack. */
    subq $4, %rsp
    addl $1, %eax
    movl %eax, (%rsp)

    /* _exit(0); */
    movl $1, %eax
    movl $0, %ebx
    int  $0x80

An „pushq“ kann man übrigens schon erkennen, dass es sich hierbei um Code für 64 Bit-Rechner handelt. Das ist nicht weiter dramatisch für meine einfachen Beispiele – man muss eben daran denken, dass Pointer jetzt 64 Bit lang sind. Dadurch hat man es mit „%rbp“ statt „%ebp“ zu tun und so weiter. Aber natürlich kann ich weiterhin „%eax“ für einen Integer verwenden, der 4 Byte lang ist (genau wie ich auch „%ah“ oder „%al“ als weitere Unterteilung des Registers verwenden könnte).

Zur Erinnerung, das Programm wird wie folgt übersetzt:

$ as -g -o hello.o hello.s
$ ld -o hello hello.o

Und dann starten wir es mal mit dem gdb:

$ gdb ./hello

Ich werde im Folgenden immer meinen „upsa“-Befehl verwenden, um alles in Gang zu bringen. Das sieht dann so aus:

(gdb) upsa
Breakpoint 1 at 0x4000b0: file hello.s, line 10.

Breakpoint 1, _start () at hello.s:10
10              pushq %rbp

---------------------------------------------------
rax            0x0      0
rbx            0x0      0
rcx            0x0      0
rdx            0x0      0
rsi            0x0      0
rdi            0x0      0
rbp            0x0      0x0
rsp            0x7fffffffe0e0   0x7fffffffe0e0
...
---------------------------------------------------
Dump of assembler code for function _start:
=> 0x00000000004000b0 <+0>:     push   %rbp
   0x00000000004000b1 <+1>:     mov    %rsp,%rbp
   0x00000000004000b4 <+4>:     sub    $0x4,%rsp
   0x00000000004000b8 <+8>:     mov    0x6000d8,%eax
   0x00000000004000bf <+15>:    mov    %eax,(%rsp)
   0x00000000004000c2 <+18>:    sub    $0x4,%rsp
   0x00000000004000c6 <+22>:    add    $0x1,%eax
   0x00000000004000c9 <+25>:    mov    %eax,(%rsp)
   0x00000000004000cc <+28>:    mov    $0x1,%eax
   0x00000000004000d1 <+33>:    mov    $0x0,%ebx
   0x00000000004000d6 <+38>:    int    $0x80
End of assembler dump.
---------------------------------------------------
(gdb)

Wir befinden uns erwartungsgemäß ganz am Anfang im Code und in den Registern steht nichts spannendes. Warum der gdb keine Größensuffixe anzeigt, ist mir ein kleines Rätsel – ich hätte „pushq“ statt „push“ erwartet. Natürlich ist das Code-Listing auch so noch eindeutig, aber es ist eine seltsame Mischung aus AT&T- und Intel-Syntax.

Ich benutze jetzt zwei Mal den „n“-Befehl, um die Initialisierung des Frame Pointers hinter mich zu bringen. Bei Interesse kann der „stack“-Befehl benutzt werden:

(gdb) stack
--------------------------------------------------------------------------
0x7fffffffe098: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffe0a8: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffe0b8: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffe0c8: 0x00000000      0x00000000      0x00000000      0x00000000
--------------------------------------------------------------------------
0x7fffffffe0d8: 0x00000000      0x00000000      0x00000001      0x00000000
0x7fffffffe0e8: 0xffffe425      0x00007fff      0x00000000      0x00000000
0x7fffffffe0f8: 0xffffe445      0x00007fff      0xffffe451      0x00007fff
0x7fffffffe108: 0xffffe466      0x00007fff      0xffffe48c      0x00007fff
--------------------------------------------------------------------------
(gdb)

Was sieht man hier? Da der Stack in „negative“ Richtung wächst (niedrigere Adressen liegen auf dem Stack weiter oben), sieht man in der oberen Hälfte also den Teil des Stacks, den wir gleich benutzen werden. Sprich, in diesem Bild wächst der Stack Zeile für Zeile nach oben und innerhalb einer Zeile von rechts nach links. In der unteren Hälfte sieht man das, was vor Eintritt in „_start“ schon auf dem Stack lag: „argc“, „argv“ und den Anfang von „envp“.

Durch das folgende „sub $0x4, %rsp“ wird effektiv der Stack um 4 Byte vergrößert. Das sorgt aber nur dafür, dass der Kernel uns dort Speicher zur Verfügung stellt, der eventuell im Moment noch gar nicht gemappt ist. Am Stackinhalt ändert sich nichts, wie man mit einem weiteren „stack“ nachvollziehen kann. Das Bild wird auch ganz genauso aussehen, denn die mittlere Trennlinie steht für den Base Pointer und damit den Beginn unseres Stack Frames.

Dann wird es schon interessanter: „mov 0x6000d8, %eax“. Im ursprünglichen Quellcode stand hier „movl numbers, %eax“. Das Label „numbers“ hat also die Adresse „0x6000d8“ erhalten und die Daten (4 Byte wegen „movl“, außerdem passt ins „%eax“-Register nicht mehr hinein), die dort stehen, werden dann geholt. Wir können uns mit dem „x“-Befehl direkt anschauen, wie die 4 Byte an dieser Adresse aussehen:

(gdb) x/1wx 0x6000d8
0x6000d8 <numbers>:     0x10203040
(gdb)

Wie zu erwarten! Und er sagt uns auch gleich, dass sich das „numbers“-Label dort befindet. Man kann sich auch einen größeren Überblick verschaffen, indem man „/proc/$(pgrep hello)/maps“ ausliest:

$ cat /proc/$(pgrep hello)/maps
00400000-00401000                 r-xp 00000000 08:02 6373424 /home/void/tmp/gdb-as-tut/hello
00600000-00601000                 rwxp 00000000 08:02 6373424 /home/void/tmp/gdb-as-tut/hello
7ffff7ffe000-7ffff7fff000         r-xp 00000000 00:00 0       [vdso]
7ffffffde000-7ffffffff000         rwxp 00000000 00:00 0       [stack]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0       [vsyscall]

Man sieht, wo sich die Adresse „0x6000d8“ befindet: Direkt oberhalb des Codes, dessen Adressen wir in der „state“-Ausgabe schon desöfteren gesehen haben. Der Stack hingegen ist am fast anderen Ende des virtuellen Adressraums.

Durch Eingabe von „n“ führen wir die Kopieraktion jetzt aus und sehen, wie die Daten im Register landen:

(gdb) n
19              movl %eax, (%rsp)

---------------------------------------------------
rax            0x10203040       270544960
rbx            0x0      0
rcx            0x0      0
...

Nach einem weiteren „n“ werden sie dann durch „mov %eax,(%rsp)“ vom Register auf den Stack kopiert, wie „stack“ zeigt:

(gdb) stack
--------------------------------------------------------------------------
0x7fffffffe098: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffe0a8: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffe0b8: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffe0c8: 0x00000000      0x00000000      0x00000000      0x10203040
--------------------------------------------------------------------------
0x7fffffffe0d8: 0x00000000      0x00000000      0x00000001      0x00000000
0x7fffffffe0e8: 0xffffe425      0x00007fff      0x00000000      0x00000000
0x7fffffffe0f8: 0xffffe445      0x00007fff      0xffffe451      0x00007fff
0x7fffffffe108: 0xffffe466      0x00007fff      0xffffe48c      0x00007fff
--------------------------------------------------------------------------
(gdb)

In der oberen Hälfte sind sie unten rechts.

Mit „n“ kommt man weiter und wird sehen, dass – nach der Addition der 1 – die Zahl ein zweites Mal auf dem Stack landet. Unterhalb der ersten Zahl, also an einer niedrigeren Adresse:

(gdb) stack
--------------------------------------------------------------------------
0x7fffffffe098: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffe0a8: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffe0b8: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffe0c8: 0x00000000      0x00000000      0x10203041      0x10203040
--------------------------------------------------------------------------
0x7fffffffe0d8: 0x00000000      0x00000000      0x00000001      0x00000000
0x7fffffffe0e8: 0xffffe425      0x00007fff      0x00000000      0x00000000
0x7fffffffe0f8: 0xffffe445      0x00007fff      0xffffe451      0x00007fff
0x7fffffffe108: 0xffffe466      0x00007fff      0xffffe48c      0x00007fff
--------------------------------------------------------------------------
(gdb)

Dann folgt der „_exit(0)“-Call und wir sind fertig.

Wir haben jetzt also einerseits den grundsätzlichen Programmablauf nachvollzogen und andererseits den Weg der Zahl „0x10203040“ vom Datensegment über das Register zweimal auf den Stack. Man sieht an der Position von „0x10203041“, in welche Richtung der Stack wächst. Und man sieht, dass „%rsp“ ständig auf eine andere Position zeigt – das ist etwas lästig. Vom Frame Pointer hat der Code nämlich gar keinen Gebrauch gemacht. Das soll im nächsten Beispiel anders werden.

Beispiel 2: Funktionsaufruf, Parameterübergabe, Byte-Order

Man denke sich in etwa folgendes Fragment an C-Code:

int sub(int a, int b)
{
    return a - b;
}

void _start()
{
    int r;
    r = sub(0x10203040, 10);

    _exit(0);
}

Auf dem Stack der Funktion „_start()“ haben wir also Platz für eine Variable und dorthin schreiben wir das Ergebnis der Funktion „sub()“. Ich habe ganz bewusst eine Subtraktion gewählt, damit die Reihenfolge der Funktionsparameter eine Rolle spielt. Das zu erwartende Ergebnis ist also „0x10203040 - 10 = 0x10203036“. Wenn wir uns an die cdecl-Konvention halten, dann könnte das in Assembler etwa so aussehen:

.text
.global _start

_start:
    /* Frame Pointer. */
    pushq %rbp
    movq %rsp, %rbp

    /* Schaffe auf dem Stack Platz für 4 Byte. Das ist der Platz, an dem
     * später der Inhalt der "Variablen" liegen wird. Diese wird über
     * -4(%rbp) adressierbar sein. */
    subq $4, %rsp

    /* Schaffe Platz für 8 Byte auf dem Stack und schiebe dann zwei
     * Zahlen dorthin. Das sind die Parameter für die Funktion. */
    subq $8, %rsp
    movl $10, -8(%rbp)
    movl $0x10203040, -12(%rbp)

    /* Rufe die "Funktion" sub() auf. */
    call sub

    /* Die Funktion ist fertig, zerstöre den Platz für die Parameter
     * wieder. */
    addq $8, %rsp

    /* Die Funktion gibt gemäß cdecl-Konvention den Rückgabewert im
     * ersten Register zurück. %eax reicht in diesem Fall. Schiebe das
     * Ergebnis zurück auf den Stack. Damit wird unsere "Variable" mit
     * dem Ergebnis von sub() belegt. */
    movl %eax, -4(%rbp)

    /* _exit(0); */
    movl $1, %eax
    movl $0, %ebx
    int  $0x80

sub:
    /* Frame Pointer. */
    pushq %rbp
    movq %rsp, %rbp

    /* Hole das erste Argument nach %eax, dann subtrahiere das zweite
     * Argument und speichere das Ergebnis in %eax. In %eax wird auch
     * der Rückgabewert stehen, also brauchen wir nichts weiter zu tun. */
    movl 16(%rbp), %eax
    subl 20(%rbp), %eax

    /* Frame Pointer abbauen, Rücksprung. */
    popq %rbp
    ret

Man möge das Programm übersetzen und mit dem gdb starten. Der Einstieg soll wieder über „upsa“ erfolgen. Dann taste man sich mit „n“ vor, bis man die „call“-Instruktion erreicht hat, und werfe dann einen Blick auf den Stack:

---------------------------------------------------
Dump of assembler code for function _start:
   0x0000000000400078 <+0>:     push   %rbp
   0x0000000000400079 <+1>:     mov    %rsp,%rbp
   0x000000000040007c <+4>:     sub    $0x4,%rsp
   0x0000000000400080 <+8>:     sub    $0x8,%rsp
   0x0000000000400084 <+12>:    movl   $0xa,-0x8(%rbp)
   0x000000000040008b <+19>:    movl   $0x10203040,-0xc(%rbp)
=> 0x0000000000400092 <+26>:    callq  0x4000aa <sub>
   0x0000000000400097 <+31>:    add    $0x8,%rsp
   0x000000000040009b <+35>:    mov    %eax,-0x4(%rbp)
   0x000000000040009e <+38>:    mov    $0x1,%eax
   0x00000000004000a3 <+43>:    mov    $0x0,%ebx
   0x00000000004000a8 <+48>:    int    $0x80
End of assembler dump.
---------------------------------------------------
(gdb) stack
--------------------------------------------------------------------------
0x7fffffffdf88: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffdf98: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffdfa8: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffdfb8: 0x00000000      0x10203040      0x0000000a      0x00000000
--------------------------------------------------------------------------
0x7fffffffdfc8: 0x00000000      0x00000000      0x00000001      0x00000000
0x7fffffffdfd8: 0xffffe340      0x00007fff      0x00000000      0x00000000
0x7fffffffdfe8: 0xffffe361      0x00007fff      0xffffe36d      0x00007fff
0x7fffffffdff8: 0xffffe382      0x00007fff      0xffffe3a8      0x00007fff
--------------------------------------------------------------------------
(gdb)

Die Adresse „0x7fffffffdfc8“ ist das, was aktuell im Register „%rbp“ steht. Man kann hier noch einmal gut die Adressierung über das Base Register nachvollziehen: Wir haben insgesamt den Wert 12 von „%rsp“ abgezogen und damit Platz für 12 Byte geschaffen, also 12 / 4 = 3 Integers (ein Integer ist ja immernoch 4 Byte lang). Über „$fp - 4“ kann der erste Integer adressiert werden. Das ist oben im C-Code die Variable „r“, die im Moment noch uninitialisiert ist. Bei „$fp - 8“ steht unsere 10 und bei „$fp - 12“ die große Zahl. Der Stack Pointer kann wachsen und schrumpfen, aber diese Adressierungen bleiben gleich. So vergewissert man sich, dass sie zutreffen:

(gdb) x/1xw $fp - 4
0x7fffffffdfc4: 0x00000000
(gdb) x/1xw $fp - 8
0x7fffffffdfc0: 0x0000000a
(gdb) x/1xw $fp - 12
0x7fffffffdfbc: 0x10203040
(gdb)

Da der Stack in negative Richtung wächst, liegt die große Zahl „0x10203040“ also ganz oben auf dem Stack und darunter die 10. Wir haben uns damit an die Konvention gehalten und die Funktionsparameter in umgekehrter Reihenfolge auf dem Stack abgelegt (der zweite Parameter 10 wurde zuerst abgelegt).

Betreten wir jetzt mit einem weiteren „n“ die Funktion „sub()“, danach noch zwei Mal „n“, um den Frame Pointer zu setzen. Wenn man jetzt den Überblick verloren haben sollte (ich hoffe, dass nicht), dann kann man „bt“ nutzen, um ein Backtrace zu machen:

(gdb) bt
#0  sub () at hello2.s:46
#1  0x0000000000400097 in _start () at hello2.s:21
(gdb)

Wie sieht der Stack jetzt eigentlich aus? Der Base Pointer hat sich geändert, der Stack Pointer auch. Schauen wir es uns an:

(gdb) stack
--------------------------------------------------------------------------
0x7fffffffdf6c: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffdf7c: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffdf8c: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffdf9c: 0x00000000      0x00000000      0x00000000      0x00000000
--------------------------------------------------------------------------
0x7fffffffdfac: 0xffffdfc8      0x00007fff      0x00400097      0x00000000
0x7fffffffdfbc: 0x10203040      0x0000000a      0x00000000      0x00000000
0x7fffffffdfcc: 0x00000000      0x00000001      0x00000000      0xffffe340
0x7fffffffdfdc: 0x00007fff      0x00000000      0x00000000      0xffffe361
--------------------------------------------------------------------------
(gdb)

Man sieht, dass in „Wachsrichtung“ des Stacks (obere Hälfte) wieder alles 0 ist. Diesen Bereich haben wir nämlich noch nie angerührt. Es ist der lokale Stack (Frame) der Funktion „sub()“ und der ist jetzt komplett leer.

Die untere Hälfte sollte uns aber bekannt vorkommen. Ein Bild aus dem Posting zum Frame Pointer auf 64 Bit angepasst:

:         :                        :
:         :                        :
|  Arg 2  |  <--   24(%rbp)        |   vorheriger
|  Arg 1  |  <--   20(%rbp)        |     Frame
|  Arg 0  |  <--   16(%rbp)        /
+---------+
|         |                        \
| Rücksp. |                        |
|         |                        |
| RBP alt |  <-- RBP               |
|  Var 0  |  <--   -4(%rbp)        |
|  Var 1  |  <--   -8(%rbp)        |   aktueller
|  Var 2  |  <--  -12(%rbp)        |     Frame
|  Var 3  |  <--  -16(%rbp)        |
:         :                        :
:         :                        :
| letzte  |  <-- RSP               /
+---------+

Eine Zeile stellt hier nach wie vor 4 Byte dar. Bei 64 Bit muss man beachten, dass sowohl „%rbp“ als auch die Rücksprungadresse 8 Byte lang sind. Auf die Variablen, die aktuellen Frame liegen, hat das keinen Einfluss. Wohl aber auf die Argumente der Funktion: Das erste Argument liegt jetzt 16 Byte zurück („16(%rbp)“) – bei 32 Bit liegt es nur 8 Byte zurück („8(%ebp)“). In der obigen Ausgabe von „stack“ ist also die gesamte Zeile hinter „0x7fffffffdfac“ durch den alten Base Pointer und die Rücksprungadresse belegt. Erst in der Zeile darunter findet sich unsere große Zahl, dann die 10 und dann die 4 Byte an leerem Platz für das spätere „r“.

Stimmt das wirklich? Argument 0 müsste unsere große Zahl sein und Argument 1 die 10:

(gdb) x/1xw $fp + 16
0x7fffffffdfbc: 0x10203040
(gdb) x/1xw $fp + 20
0x7fffffffdfc0: 0x0000000a
(gdb)

Alles klar! Aber schauen wir uns das nochmal an – in einem anderen Format. Ich will jetzt nicht ab der Adresse in „$fp“ ein Wort sehen, sondern 4 Byte. Wie sieht das dann aus?

(gdb) x/4xb $fp + 16
0x7fffffffdfbc: 0x40    0x30    0x20    0x10
(gdb)

Das kann auf den ersten Blick ziemlich verwirrend sein, denn man vergisst recht schnell, dass die heutigen Intel-Prozessoren Little Endian als Byte-Reihenfolge verwenden. Wenn der gdb also ein „0x10203040“ anzeigt (oder man das so im Quelltext schreibt), dann ist im Speicher die „0x40“ das erste Byte, wenn man in Richtung wachsender Adressen blickt.

Wann immer mehrere Byte zusammengefasst werden, wird also ihre Richtung umgekehrt. Das ist auch bei Halbworten (Gruppen von 2 Byte) so:

(gdb) x/2xh $fp + 16
0x7fffffffdfbc: 0x3040  0x1020
(gdb)

So viel dazu. Mit dem „n“-Befehl kann man nun beobachten, wie das erste Argument in das Register geholt und dann das zweite Argument vom ersten subtrahiert wird. Sobald wir die „pop“-Anweisung erledigt haben, schauen wir uns nochmal den Stack an:

---------------------------------------------------
Dump of assembler code for function sub:
   0x00000000004000aa <+0>:     push   %rbp
   0x00000000004000ab <+1>:     mov    %rsp,%rbp
   0x00000000004000ae <+4>:     mov    0x10(%rbp),%eax
   0x00000000004000b1 <+7>:     sub    0x14(%rbp),%eax
   0x00000000004000b4 <+10>:    pop    %rbp
=> 0x00000000004000b5 <+11>:    retq
End of assembler dump.
---------------------------------------------------
(gdb) stack
--------------------------------------------------------------------------
0x7fffffffdf88: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffdf98: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffdfa8: 0x00000000      0xffffdfc8      0x00007fff      0x00400097
0x7fffffffdfb8: 0x00000000      0x10203040      0x0000000a      0x00000000
--------------------------------------------------------------------------
0x7fffffffdfc8: 0x00000000      0x00000000      0x00000001      0x00000000
0x7fffffffdfd8: 0xffffe340      0x00007fff      0x00000000      0x00000000
0x7fffffffdfe8: 0xffffe361      0x00007fff      0xffffe36d      0x00007fff
0x7fffffffdff8: 0xffffe382      0x00007fff      0xffffe3a8      0x00007fff
--------------------------------------------------------------------------
(gdb)

In diesem Moment zeigt „%rsp“ auf die Rücksprungadresse (dass diese Adresse wirklich in den Code zeigt, kann man sich wieder klarmachen, indem man „/proc/$(pgrep hello2)/maps“ ausliest):

(gdb) x/1xg $rsp
0x7fffffffdfb4: 0x0000000000400097
(gdb)

Wenn man aufmerksam ist, merkt man, dass ich im Posting zum Frame Pointer geschrieben habe:

/* Frame Pointer wieder aufräumen. */
movl %ebp, %esp
popl %ebp

Das Verschieben des Base Pointers in den Stack Pointer fehlt diesmal. Das ist kein Problem: Die beiden Register enthalten ohnehin denselben Wert, wie man in der Ausgabe von „info registers“ (oder „state“) sieht.

Lassen wir mit „n“ den Rücksprung passieren und verlassen „sub()“. Der Stack wird nun um 8 Byte verkleinert und somit die Argumente für die Funktion verworfen – auch, wenn sie weiterhin im Speicher liegen. Zuguterletzt wird der Wert von „%eax“ auf den Stack geschoben, dessen finaler Zustand dann so aussieht (und testweise lesen wir noch einmal in verschiedenen Formaten unsere Variable aus – Erinnerung: „$fp - 4“ entspricht „-4(%rbp)“ im Code):

(gdb) stack
--------------------------------------------------------------------------
0x7fffffffdf88: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffdf98: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffdfa8: 0x00000000      0xffffdfc8      0x00007fff      0x00400097
0x7fffffffdfb8: 0x00000000      0x10203040      0x0000000a      0x10203036
--------------------------------------------------------------------------
0x7fffffffdfc8: 0x00000000      0x00000000      0x00000001      0x00000000
0x7fffffffdfd8: 0xffffe340      0x00007fff      0x00000000      0x00000000
0x7fffffffdfe8: 0xffffe361      0x00007fff      0xffffe36d      0x00007fff
0x7fffffffdff8: 0xffffe382      0x00007fff      0xffffe3a8      0x00007fff
--------------------------------------------------------------------------
(gdb) x/1xw $fp - 4
0x7fffffffdfc4: 0x10203036
(gdb) x/4xb $fp - 4
0x7fffffffdfc4: 0x36    0x30    0x20    0x10
(gdb) x/1dw $fp - 4
0x7fffffffdfc4: 270544950
(gdb)

Den Stack als Stack anzeigen

Bis hierhin habe ich den „x“-Befehl verwendet, um den Stack Frame anzuzeigen. Das habe ich absichtlich gemacht, damit man ein Gefühl dafür bekommt, was wo im Speicher liegt und in welche Richtungen sich die Dinge bewegen. Der gdb ist aber deutlich mächtiger als das. Wir können uns den Stack auch wirklich als Stack anzeigen lassen und nicht nur den Speicher in der Nähe des Base Pointers. Ich habe zusätzlich diese Befehle in meiner „~/.gdbinit“ definiert, was einen Ausblick auf die Möglichkeiten bietet:

define stackb
    printf "-----------\n"
    set $_c = $fp - $sp
    while ($_c > 0)
        printf "%p: 0x%02X\n", \
            (unsigned char*)($fp - $_c), \
            *(unsigned char*)($fp - $_c)
        set $_c--
    end
    printf "-----------\n"
end

define stackw
    printf "-----------\n"
    set $_c = ($fp - $sp) / sizeof(unsigned int)
    while ($_c > 0)
        printf "%p: 0x%08X\n", \
            ((unsigned int*)$fp - $_c), \
            *((unsigned int*)$fp - $_c)
        set $_c--
    end
    printf "-----------\n"
end

define stackg
    printf "-----------\n"
    set $_c = ($fp - $sp) / sizeof(unsigned long long int)
    while ($_c > 0)
        printf "%p: 0x%016llX\n", \
            ((unsigned long long int*)$fp - $_c), \
            *((unsigned long long int*)$fp - $_c)
        set $_c--
    end
    printf "-----------\n"
end

Das zeigt den Stack in Einheiten von Bytes, Worten (4 Byte) oder „giants“ (8 Byte) an. Die Casts und „printf“-Formate sind so gewählt, dass es auf i686 und x86_64 gleichermaßen funktioniert. Die Suffixe der Befehlsnamen entsprechen denen des „x“-Befehls, Halbworte habe ich mir aber gespart.

Es wird hierbei nicht nur der Frame Pointer berücksichtigt, sondern auch der aktuelle Stack Pointer. Dadurch sieht man wirklich nur den Teil des Speichers, der im Moment als Stack Frame gilt:

(gdb) stack
--------------------------------------------------------------------------
0x7fffffffdf88: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffdf98: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffdfa8: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffdfb8: 0x00000000      0x10203040      0x0000000a      0x00000000
--------------------------------------------------------------------------
0x7fffffffdfc8: 0x00000000      0x00000000      0x00000001      0x00000000
0x7fffffffdfd8: 0xffffe340      0x00007fff      0x00000000      0x00000000
0x7fffffffdfe8: 0xffffe361      0x00007fff      0xffffe36d      0x00007fff
0x7fffffffdff8: 0xffffe382      0x00007fff      0xffffe3a8      0x00007fff
--------------------------------------------------------------------------
(gdb) stackw
-----------
0x7fffffffdfbc: 0x10203040
0x7fffffffdfc0: 0x0000000A
0x7fffffffdfc4: 0x00000000
-----------
(gdb)

Ende. Aus. Und weiter geht’s!

Ich hoffe, der eine oder andere hat durch dieses Posting etwas mitnehmen können. Zumindest sollte der Einstieg in Assembler etwas leichter fallen, wenn man einen ganz groben Überblick der hier gezeigten Art hat.

Ich wünsche noch viel Spaß!