blog · git · desktop · images · contact
2012-05-31
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.
~/.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?
„state
“: Kapselt „info registers
“ und „disassemble $pc
“. Der
erste Befehl zeigt den aktuellen Zustand der Register an und der
zweite zeigt die aktuelle Position im Code mitsamt einigen Zeilen
Kontext.
„stack
“ ist schon interessanter. Mit dem Stack hat man ständig zu
tun und deswegen möchte ich auch die Daten dort möglichst einfach
anschauen können. „x/16xw $fp - 64
“ ist wie folgt zu lesen: Der
„x
“-Befehl dient zum Betrachten einer Speicherstelle. Nach dem
Schrägstrich folgt das Format, in dem der Speicher angezeigt werden
soll. In diesem Fall ist das Format „16xw
“, was 16 Worte („w
“)
im Hex-Format („x
“) meint. Ein „Wort“ sind in diesem Kontext 4
Byte – das heißt, es sollen 64 Byte angezeigt werden. Die
anzuzeigende Adresse ist „$fp - 64
“: Der aktuelle Frame Pointer
(siehe vorherige Postings) minus 64 Byte, also werden de facto 64
Byte bis hin zum Frame Pointer angezeigt. Danach folgt mit „printf
...
“ eine Trennlinie und es werden 64 Byte ab dem Frame Pointer
dargestellt – im selben Format. „$fp
“ ist übrigens unabhängig von
der Architektur und kann auf 32 Bit und 64 Bit verwendet werden.
„n
“ ist nur ein Shortcut: Gehe genau eine Instruktion vorwärts
(steige also gegebenenfalls in Funktionen hinab) und zeige dann den
aktuellen Status wieder an.
Die „up*
“-Befehle dienen zum schnellen Beginn des Debuggens. Sie
setzen einen Breakpoint an eine geeignete Stelle, starten dann das
Programm, welches am Breakpoint anhalten wird, und geben dann den
Status aus. Der Unterschied zwischen „*_start
“ und „_start
“ ist,
dass die Variante mit dem Stern exakt die Adresse von „_start
“
bezeichnet. Ohne Stern hingegen ist einen Funktion gemeint und es
wird „Initialisierungscode“ übersprungen. Gemeint ist damit zum
Beispiel das Aufsetzen eines Frame Pointers. Ich fand es am Anfang
übersichtlicher, immer die Varianten mit Stern zu verwenden, damit
man nichts verpasst.
gdb hat übrigens eine ganz gute eingebaute Hilfe. Man sollte sich
deswegen zum Beispiel mal „help x
“ und die anderen Hilfeseiten
durchlesen.
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.
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)
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)
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ß!