Επόμενο Προηγούμενο Περιεχόμενα
Στο μέρος αυτό θα εξετάσουμε τις δυνατότητες του GDB για
assembly debugging. Το case-study πρόγραμμα θα είναι το
ίδιο με την προηγούμενη φορά:
#include <stdio.h>
int main(int argc, char **argv)
{
int num;
if (argc<2) {
printf("Usage: %s <number>\n",argv[0]);
exit(1);
}
num=alf(argv[1]);
if (num>10)
printf("Ok!\n");
else
printf("Failed!\n");
}
int alf(char *s)
{
return atoi(s);
}
Κάντε compile με : gcc [-g] -o rce1 rce1.c
Αυτή τη φορά η παράμετρος "-g" δεν είναι απαραίτητη αλλά
και να την χρησιμοποιήσουμε δε μας ενοχλεί. Το φόρτωμα
του προγράμματος στον GDB γίνεται ως συνήθως:
bash$ gdb rce1
(gdb)
Αν δοκιμάσουμε να δούμε τον πηγαίο κώδικα του
προγράμματος χωρίς να έχουμε κάνει compile με -g:
(gdb) list
1 init.c: No such file or directory.
in init.c
(gdb)
O GBD δε βρίσκει το αρχείο "init.c". Ε, και τι
έγινε θα πείτε; Το δικό μας αρχείο είναι το "rce1.c"! Το
πρόβλημα είναι ότι το εκτελέσιμο δεν περιλαμβάνει καμία
πληροφορία για το ποιο είναι το πηγαίο αρχείο του και ο GDB
υποθέτει το όνομα "init.c". To "init.c" είναι το αρχείο
πηγαίου κώδικα που αντιστοιχεί στην αρχικοποίηση της libc.
Αν δημιουργήσουμε ένα αρχείο με το όνομα "init.c", τότε η
list θα μας δείξει το περιεχόμενο του αρχείου αυτού.
Αλλά και πάλι δεν μπορούμε να κάνουμε δουλειά, διότι ο
debugger δεν γνωρίζει ποιες εντολές assembly αντιστοιχούν
σε ποιες γραμμές C κώδικα. Αν πχ έχουμε αντιγράψει το
"rce1.c" σε "init.c":
bash$ cp rce1.c init.c
bash$ gdb -q rce1
(gdb) break main
Breakpoint 1 at 0x8048392
(gdb) r
Starting program: /home/alf/projects/magaz/issue1/rce1
Breakpoint 1, 0x08048392 in main ()
(gdb) n
Single stepping until exit from function main,which has no line number
information.
Usage: /home/alf/projects/magaz/issue1/rce1 <number>
Program exited with code 01.
(gdb)
Όταν πήγαμε να προχωρήσουμε μία γραμμή πηγαίου κώδικα με
την n, ο GDB παραπονέθηκε πως δεν έχει τις
απαραίτητες πληροφορίες και αποφάσισε να προχωρήσει μέχρι
το τέλος της main(). Για πλάκα μπορούμε να συγχύσουμε τον
GDB (και τους εαυτούς μας) αν κάνουμε compile με το -g
flag και μετά αντικαταστήσουμε το source αρχείο μας με
ένα άσχετο :)
Αφού λοιπόν δεν έχουμε τον πηγαίο κώδικα αυτό ήταν... ας
πάμε να παίξουμε τάβλι καλύτερα. Αλλά μια φωνή μέσα μας
(τουλάχιστον μέσα σε εμένα!) αρνείται να παραδώσει τα
όπλα. The gate is now open, welcome to the world of RCE!
Αφού λοιπόν δεν αποθαρυνθήκαμε, ας εξετάσουμε το assembly
listing της main. Αυτό γίνεται (κυρίως) με την εντολή
disassemble <διεύθυνση> [<τελική
διεύθυνση>]. Αν ορίσουμε μόνο μια παράμετρο, τότε
εμφανίζεται ο κώδικας όλης της συνάρτησης στην
οποία ανήκει η διεύθυνση. Το πρόβλημα είναι πως αν ο GDB
δε γνωρίζει σε ποια συνάρτηση ανήκει η διεύθυνση (πχ όταν
δεν υπάρχουν σύμβολα στο εκτελέσιμο), είτε θα παραπονεθεί
και δε θα τυπώσει τίποτα είτε θα συγχυστεί με προηγούμενα
σύμβολα και θα μας εκτυπώσει κατεβατά ολόκληρα. Για
παράδειγμα:
(gdb) disas main
Dump of assembler code for function main:
0x804838c <main>: push %ebp
0x804838d <main+1>: mov %esp,%ebp
0x804838f <main+3>: sub $0x8,%esp
0x8048392 <main+6>: and $0xfffffff0,%esp
0x8048395 <main+9>: mov $0x0,%eax
0x804839a <main+14>: sub %eax,%esp
0x804839c <main+16>: cmpl $0x1,0x8(%ebp)
0x80483a0 <main+20>: jg 0x80483c1 <main+53>
0x80483a2 <main+22>: sub $0x8,%esp
0x80483a5 <main+25>: mov 0xc(%ebp),%eax
0x80483a8 <main+28>: pushl (%eax)
0x80483aa <main+30>: push $0x8048464
0x80483af <main+35>: call 0x80482ac <printf>
0x80483b4 <main+40>: add $0x10,%esp
0x80483b7 <main+43>: sub $0xc,%esp
0x80483ba <main+46>: push $0x1
Οι εντολές:
(gdb) disas main+44
(gdb) disas 0x80483f7
και γενικά όσες διευθύνσεις περιέχονται στη
main θα έχουν ως αποτέλεσμα να εκτυπωθεί όλη η main
(ακριβώς όπως παραπάνω). Προσοχή πως το "main" είναι απλώς
ένα σύμβολο που αντιστοιχεί σε κάποια διεύθυνση, εδώ την
0x804838c. Η έκφραση main+44 είναι και αυτή μια διεύθυνση
(0x80483b8). Δεν έχει σημασία που δεν αποτελεί την αρχή
κάποιας εντολής ( είναι το δεύτερο byte της sub $0xc,%esp),
αρκεί που ανήκει μέσα στη συνάρτηση main.
Η ερώτηση που τίθεται είναι η
εξής: που ξέρει ο GDB που αρχίζει και τελειώνει μια
συνάρτηση; Και η απάντηση: ο GDB ξέρει μόνο που αρχίζει η
συνάρτηση και υποθέτει ότι συνεχίζει η ίδια συνάρτηση
μέχρι να βρει κάποιο άλλο σύμβολο που είναι σύμβολο
συνάρτησης.
Αν τα σύμβολα στο εκτελέσιμο κατά σειρά αύξουσας
διεύθυνσης είναι:
0804838c main
08048406 alf
0804841c __do_global_ctors_aux
O GDB θεωρεί πως ό,τι βρίσκεται μεταξύ των
διευθύνσεων main και alf ανήκει στη συνάρτηση main, ό,τι
βρίσκεται μεταξύ των alf και __do_global_ctors_aux ανήκει
στη συνάρτηση alf κτλ. Το γεγονός πως τα όρια της κάθε
συνάρτησης (για την ακρίβεια το τέλος) δεν είναι γνωστά,
προκαλεί το πρόβλημα που αναφέρθηκε παραπάνω (ο GDB δε
μπορεί να βρει σε ποια συνάρτηση ανήκει η διεύθυνση ή κάνει
λάθος). Ας δούμε το πρόβλημα στην πράξη:
bash$ strip -s rce1
bash$ gdb rce1
(no debugging symbols found)...
(gdb)
H εντολή strip "απογυμνώνει" ένα object αρχειο από όλα τα
σύμβολα που μπορεί. Γράφω "μπορεί", διότι υπάρχουν μερικά
που δε έχει νόημα να αφαιρέσει, όπως για παράδειγμα αυτά
που αναφέρονται σε εξωτερικές συναρτήσεις και δεδομένα. Ο
λόγος είναι ότι στο στάδιο του linking (είτε αυτό είναι
dynamic είτε όχι) δε θα μπορέσει να βρει τις διευθύνσεις
τους αν δε γνωρίζει το όνομα τους!
(gdb) disas main
No symbol table is loaded. Use the "file" command.
Το σύμβολο main δε βρέθηκε αλλά εμείς ξέρουμε
τη διεύθυνση του.
(gdb) disas 0x804838c
Dump of assembler code for function atoi:
0x80482cc <atoi>: jmp *0x804958c
0x80482d2 <atoi+6>: push $0x18
0x80482d7 <atoi+11>: jmp 0x804828c
0x80482dc <atoi+16>: xor %ebp,%ebp
0x80482de <atoi+18>: pop %esi
Και ιδού... Ο GDB τα "πήρε" :)
Το μόνο σύμβολο που υπάρχει αμέσως πριν τη διεύθυνση της
main είναι το atoi οπότε ο debugger θεωρεί πως η
διεύθυνση 0x804838c ανήκει στη συνάρτηση atoi(). Το
σύμβολο atoi δείχνει σε μια εξωτερική συνάρτηση για αυτό
και δεν αφαιρέθηκε. Σε αυτές τις περιπτώσεις είναι
χρήσιμη η εναλλακτική μορφή της disassemble στην οποία
ορίζουμε τόσο την αρχική όσο και την τελική διεύθυνση για
το disassembly :
(gdb) disas 0x804838c 0x80483a0
Dump of assembler code from 0x804838c to 0x80483a0:
0x804838c <atoi+192>: push %ebp
0x804838d <atoi+193>: mov %esp,%ebp
0x804838f <atoi+195>: sub $0x8,%esp
0x8048392 <atoi+198>: and $0xfffffff0,%esp
0x8048395 <atoi+201>: mov $0x0,%eax
0x804839a <atoi+206>: sub %eax,%esp
0x804839c <atoi+208>: cmpl $0x1,0x8(%ebp)
End of assembler dump.
Μπορεί ο GDB να πιστεύει πως βρισκόμαστε 192 bytes από
την αρχή της atoi αλλά εμείς ξέρουμε πως ουσιαστικά
είμαστε στην αρχή της main!
Κλείνοντας αυτό το κομμάτι θα ασχοληθούμε λίγο με τη
μορφή του listing. Όσοι έχετε ασχοληθεί με assembly στον
x86 η σύνταξη των προηγούμενων listing ίσως σας φανεί
λίγο παράξενη. Αυτή ονομάζεται AT&T syntax και ένα
βασικό χαρακτηριστικό της είναι ότι στις εντολές της έχει
ανάποδα την πηγή και τον προορισμό, σε σχέση με την άλλη
μορφή την Intel syntax. Πχ για να μετακινήσουμε το
περιεχόμενο του καταχωρητή ebx στον eax :
mov %ebx, %eax AT&T
mov eax, ebx Intel
Βέβαια υπάρχουν και άλλες διαφορές αλλά δε θα
μας απασχολήσουν εδώ. Επίσης υπάρχουν και παραλλαγές των
παραπάνω όπως η σύνταξη που χρησιμοποιεί ο Nasm (Netwide
Assembler) η οποία βασίζεται στην Intel αλλά κατά τη γνώμη
είναι πιο ξεκάθαρη Παρακάτω θα χρησιμοποιήσουμε τη σύνταξη
της Intel διότι είναι γενικά πιο διαδεδομένη για τους
επεξεργαστές της. Στον GDB η σύνταξη ορίζεται στην
εσωτερική μεταβλητή disassembly-flavor:
(gdb) set disassembly-flavor intel
(gdb) disas main
Dump of assembler code for function main:
0x804838c <main>: push ebp
0x804838d <main+1>: mov ebp,esp
0x804838f <main+3>: sub esp,0x8
0x8048392 <main+6>: and esp,0xfffffff0
0x8048395 <main+9>: mov eax,0x0
0x804839a <main+14>: sub esp,eax
0x804839c <main+16>: cmp DWORD PTR [ebp+8],0x1
0x80483a0 <main+20>: jg 0x80483c1 <main+53>
0x80483a2 <main+22>: sub esp,0x8
0x80483a5 <main+25>: mov eax,DWORD PTR [ebp+12]
0x80483a8 <main+28>: push DWORD PTR [eax]
0x80483aa <main+30>: push 0x8048464
0x80483af <main+35>: call 0x80482ac <printf>
0x80483b4 <main+40>: add esp,0x10
0x80483b7 <main+43>: sub esp,0xc
0x80483ba <main+46>: push 0x1
0x80483bc <main+48>: call 0x80482bc <exit>
0x80483c1 <main+53>: sub esp,0xc
0x80483c4 <main+56>: mov eax,DWORD PTR [ebp+12]
0x80483c7 <main+59>: add eax,0x4
0x80483ca <main+62>: push DWORD PTR [eax]
0x80483cc <main+64>: call 0x8048406 <alf>
0x80483d1 <main+69>: add esp,0x10
0x80483d4 <main+72>: mov DWORD PTR [ebp-4],eax
0x80483d7 <main+75>: cmp DWORD PTR [ebp-4],0xa
0x80483db <main+79>: jle 0x80483ef <main+99>
0x80483dd <main+81>: sub esp,0xc
0x80483e0 <main+84>: push 0x8048478
0x80483e5 <main+89>: call 0x80482ac <printf>
0x80483ea <main+94>: add esp,0x10
0x80483ed <main+97>: jmp 0x80483ff <main+115>
0x80483ef <main+99>: sub esp,0xc
0x80483f2 <main+102>: push 0x804847d
0x80483f7 <main+107>: call 0x80482ac <printf>
0x80483fc <main+112>: add esp,0x10
0x80483ff <main+115>: mov eax,0x1
0x8048404 <main+120>: leave
0x8048405 <main+121>: ret
End of assembler dump.
(gdb)
Τα δεδομένα που μας ενδιαφέρουν όταν ασχολούμαστε με
low-level debugging μπορούν να βρίσκονται είτε σε κάποιον
καταχωρητή είτε στη μνήμη.
Εξετάζοντας καταχωρητές
Ο βασικός τρόπος για να δούμε τα περιεχόμενα των
καταχωρητών είναι με την info registers/ i
r [reg]. Χωρίς όρισμα εκτυπώνει όλους τους ακέραιους
καταχωρητές με τα περιεχόμενα τους σε δεκαεξαδική και
δεκαδική μορφή, αλλιώς τυπώνει μόνο αυτόν που ορίσαμε.
(gdb) i r
eax 0x0 0
ecx 0x4 4
edx 0x4014f1ec 1075114476
ebx 0x40153234 1075130932
esp 0xbffff730 0xbffff730
ebp 0xbffff738 0xbffff738
esi 0x40014020 1073823776
edi 0xbffff794 -1073743980
eip 0x804839c 0x804839c
eflags 0x386 902
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x0 0
fctrl 0x37f 895
fstat 0x0 0
ftag 0xffff 65535
fiseg 0x0 0
fioff 0x0 0
foseg 0x0 0
fooff 0x0 0
fop 0x0 0
mxcsr 0x1f80 8064
orig_eax 0xffffffff -1
(gdb) i r edx
edx 0x4014f1ec 1075114476
(gdb)
Υπάρχει και η εντολή info all-registers η οποία
τυπώνει όλους τους καταχωρητές ( integer και
floating-point και ΜΜΧ και ΧΜΜ για x86).
To πρόβλημα με την εντολή i r είναι πως μας
επιτρέπει μόνο να δούμε τις τιμές των καταχωρητών, ενώ
αρκετά συχνά θέλουμε να τις αλλάξουμε ή να τις
χρησιμοποιήσουμε σε κάποια έκφραση. Στον GDB για κάθε
καταχωρήτη υπάρχει μια ψευδο-μεταβλητή της οποία το όνομα
αποτελείται από το '$' και το όνομα του καταχωρήτη πχ
$eax. Ο μηχανισμός αυτός προσφέρει μεγάλη ευελιξία και
ευκολία:
(gdb) print $eip
$1 = (void *) 0x804839c
(gdb) set $eip=$eip+1
(gdb) print $eip
$2 = (void *) 0x804839d
(gdb) set $eip--
(gdb) print $eip
$3 = (void *) 0x804839c
Εξετάζοντας τη μνήμη
Η προσπέλαση στη μνήμη γίνεται με την εντολή x. Η
πλήρης σύνταξη είναι x/FMT όπου FMT είναι μια
ακολουθία τριών στοιχείων <repeat
count><size><format>. To πρώτο ορίζει πόσα
αντικείμενα να εκτυπωθούν, το δεύτερο δηλώνει τι μέγεθος
θα έχει το κάθε αντικείμενο ( b(byte 1), h(halfword 2) ,
w(word 4) ,g(giant 8) και τέλος το format σε τι μορφή να
εκτυπωθούν ( o(octal), x(hex), d(decimal), u(unsigned
decimal), t(binary), f(float), a(address),
i(instruction), c(char) και s(string). )
Πχ Εκτύπωσε 5 bytes σε οκταδική μορφή αρχίζοντας από τη
διεύθυνση main
(gdb) x/5bo main
0x804838c <main>: 0125 0211 0345 0203 0354
Το ίδιο σε hex
(gdb) x/5bx main
0x804838c <main>: 0x55 0x89 0xe5 0x83 0xec
Εκτύπωσε 3 λέξεις (4 bytes η κάθε μία) σε
δεκαεξαδική μορφή αρχίζοντας από τη διεύθυνση main
(gdb) x/3wx main
0x804838c <main>: 0x83e58955 0xe48308ec 0x0000b8f0
Εκτύπωσε τις πρώτες δέκα εντολές της main( όχι
τις γνωστές δέκα...)
(gdb) x/10i main
0x804838c <main>: push %ebp
0x804838d <main+1>: mov %esp,%ebp
0x804838f <main+3>: sub $0x8,%esp
0x8048392 <main+6>: and $0xfffffff0,%esp
0x8048395 <main+9>: mov $0x0,%eax
0x804839a <main+14>: sub %eax,%esp
0x804839c <main+16>: cmpl $0x1,0x8(%ebp)
0x80483a0 <main+20>: jg 0x80483c1 <main+53>
0x80483a2 <main+22>: sub $0x8,%esp
0x80483a5 <main+25>: mov 0xc(%ebp),%eax
Η τελευταία εντολή έχει το ίδιο αποτέλεσμα με
την εντολή
disassemble main main+25.
Συχνά χρειάζεται να εξετάζουμε συνέχεια μια θέση μνήμης
και καταντάει κουραστικό να γράφουμε την εντολή
x/FMT . Σε τέτοιες περιπτώσεις βολεύει η εντολή
display που συντάσεται ακριβώς όπως η x/FMT
και μαζί με κάθε gdb prompt μας εμφανίζει τα δεδομένα που
της ζητήσαμε. Ένα πολύ χρήσιμο παράδειγμα της display
είναι το παρακάτω:
(gdb) display/5i $eip
(gdb) break main
Breakpoint 1 at 0x8048392
(gdb) r
Starting program: /home/alf/projects/magaz/issue1/rce1
Breakpoint 1, 0x08048392 in main ()
2: x/5i $eip
0x8048392 <main+6>: and $0xfffffff0,%esp
0x8048395 <main+9>: mov $0x0,%eax
0x804839a <main+14>: sub %eax,%esp
0x804839c <main+16>: cmpl $0x1,0x8(%ebp)
0x80483a0 <main+20>: jg 0x80483c1 <main+53>
(gdb) ni
0x08048395 in main ()
2: x/5i $eip
0x8048395 <main+9>: mov $0x0,%eax
0x804839a <main+14>: sub %eax,%esp
0x804839c <main+16>: cmpl $0x1,0x8(%ebp)
0x80483a0 <main+20>: jg 0x80483c1 <main+53>
0x80483a2 <main+22>: sub $0x8,%esp
Μετά από κάθε ni (next instruction, δείτε λίγο
παρακάτω) εμφανίζονται αυτόματα οι 5 επόμενες εντολές
assembly. Για να εξετάσουμε ποια auto-display έχουμε στο
σύστημα μας γράφουμε info display ενώ για να ακυρώσουμε
μια εντολή display χρησιμοποιούμε την undisplay
[#num] ή delete display [#n].
Είναι ωραίο να βλέπουμε τον κώδικα του προγράμματος μας
και τα δεδομένα του αλλά είναι καλύτερο να μπορούμε και
να το εκτελούμε! Για το σκοπό αυτό οι εντολές που
χρειαζόμαστε είναι οι ni/nexti (next
instruction) και si/stepi. Αυτές
λειτουργούν αντίστοιχα με τις next και step
αλλά σε επίπεδο εντολών assembly και όχι σε επίπεδο
γραμμών πηγαίου κώδικα. Οι nexti και stepi
εκτελούν την επόμενη εντολή assembly αλλά η nexti
δεν ακολουθεί τις κλήσεις συναρτήσεων.
(gdb) display/1i $eip
(gdb) ni
0x080483ca in main ()
2: x/i $eip
0x80483ca <main+62>: push DWORD PTR [eax]
(gdb) ni
0x080483cc in main ()
2: x/i $eip
0x80483cc <main+64>: call 0x8048406 <alf>
(gdb) ni
0x080483d1 in main ()
2: x/i $eip
0x80483d1 <main+69>: add esp,0x10
(gdb) ni
0x080483d4 in main ()
2: x/i $eip
0x80483d4 <main+72>: mov DWORD PTR [ebp-4],eax
(gdb) ni
0x080483ca in main ()
2: x/i $eip
0x80483ca <main+62>: push DWORD PTR [eax]
(gdb) ni
0x080483cc in main ()
2: x/i $eip
0x80483cc <main+64>: call 0x8048406 <alf>
(gdb) si
0x08048406 in alf ()
2: x/i $eip
0x8048406 <alf>: push ebp
(gdb) ni
0x08048407 in alf ()
2: x/i $eip
0x8048407 <alf+1>: mov ebp,esp
(gdb) ni
0x08048409 in alf ()
2: x/i $eip
0x8048409 <alf+3>: sub esp,0x8
(gdb) ni
0x0804840c in alf ()
2: x/i $eip
0x804840c <alf+6>: sub esp,0xc
Breakpoints-Watchpoints
Για τα breakpoints και τα watchpoints ισχύουν όσα είχαν
γραφτεί στο προηγούμενο τεύχος. Απλώς λόγω έλλειψης
συμβόλων χρησιμοποιείται αρκετά η μορφή που περιέχει απλή
διεύθυνση πχ break *0x8045333.
Η εντολή info files τυπώνει πληροφορίες για το
εκτελέσιμο:
(gdb) info files
Symbols from "/home/alf/projects/magaz/issue1/rce1".
Local exec file:
`/home/alf/projects/magaz/issue1/rce1', file type elf32-i386.
Entry point: 0x8048330
0x080480f4 - 0x08048107 is .interp
0x08048108 - 0x08048128 is .note.ABI-tag
0x08048128 - 0x08048160 is .hash
0x08048160 - 0x080481f0 is .dynsym
0x080481f0 - 0x0804824e is .dynstr
0x0804824e - 0x08048260 is .gnu.version
0x08048260 - 0x08048280 is .gnu.version_r
0x08048280 - 0x08048290 is .rel.dyn
0x08048290 - 0x080482b8 is .rel.plt
0x080482b8 - 0x080482cf is .init
0x080482d0 - 0x08048330 is .plt
0x08048330 - 0x080484ac is .text
0x080484ac - 0x080484c7 is .fini
0x080484c8 - 0x080484f6 is .rodata
0x080494f8 - 0x08049504 is .data
0x08049504 - 0x08049508 is .eh_frame
0x08049508 - 0x080495d0 is .dynamic
0x080495d0 - 0x080495d8 is .ctors
0x080495d8 - 0x080495e0 is .dtors
0x080495e0 - 0x080495e4 is .jcr
0x080495e4 - 0x08049608 is .got
0x08049608 - 0x08049610 is .bss
(gdb)
Καταρχάς δίνεται το path και το είδος του
εκτελέσιμου.
Αμέσως μετά δίνεται το entry point, δηλαδή η διεύθυνση
της πρώτης εντολής που θα εκτελεστεί όταν αρχίσει το
πρόγραμμα. Προσοχή: αυτή συνήθως δεν είναι η διεύθυνση
της main αλλά είναι η αρχή του κώδικα που αρχικοποιεί την
libc!
Όλα τα υπόλοιπα είναι τα sections του ELF και οι
διεύθυνσεις μνήμης που καταλαμβάνουν.
Εδώ θα αναφερθούμε σε δύο αρκετά χρήσιμες εντολές που
μάλλον θα ταίριαζαν περισσότερο στο προηγούμενο άρθρο.
Η πρώτη εντολή είναι η call η οποία
χρησιμοποιείται για να καλέσουμε μια οποιαδήποτε
συνάρτηση. Η σύνταξη που χρησιμοποιεί εξαρτάται από την
τρέχουσα γλώσσα και στη C είναι γνωστή η call
func(arg1,arg2,...).
bash$ gdb rce1
(gdb) call alf("123")
evaluation of this expression requires the target program to be active
(gdb) break main
Breakpoint 1 at 0x80483f0: file rce1.c, line 7.
(gdb) r
Starting program: /home/alf/magaz/issue1/rce1
Breakpoint 1, main (argc=1, argv=0xbffff7a4) at rce1.c:7
7 if (argc<2) {
(gdb) call alf("123")
$1 = 123
(gdb) call alf("747")
$2 = 747
(gdb) print $1
$3 = 123
(gdb) print $+1
$4 = 124
Η τιμή που επιστρέφει η συνάρτηση αποθηκεύεται
στη μεταβλητή που μας δείχνει ο GDB πχ $1. Η μεταβλητή $
περιέχει την τελευταία τιμή που παράχθηκε.
Η επόμενη εντολή είναι η info functions [name]. Η
εντολή αυτή εμφανίζει όλες τις συναρτήσεις που ξέρει ο
GDB μαζί με τη διεύθυνση τους (αμέτρητες!). Αν
καθορίσουμε κάποιο όνομα θα εμφανιστούν μόνο οι
συναρτήσεις που περιέχουν αυτό το όνομα. Αν και γενικά τα
πράγματα είναι απλά, υπάρχουν μερικά σκοτεινά σημεία που
ίσως σας προβληματίσουν.
Για παράδειγμα έστω ότι έχουμε ένα πρόγραμμα που καλεί
την fprintf(). Αφου το φορτώσουμε στο GDB έχουμε:
(gdb) info functions fprintf
All functions matching regular expression "fprintf":
Non-debugging symbols:
0x080482e0 fprintf
(gdb) break main
Breakpoint 1 at 0x80483f0: file rce1.c, line 7.
(gdb) r
Starting program: /home/alf/tst
Breakpoint 1, main (argc=1, argv=0xbffff7a4) at rce1.c:7
7 if (argc<2) {
(gdb) info functions fprintf
All functions matching regular expression "fprintf":
Non-debugging symbols:
0x080482e0 fprintf
0x4006e5c0 _IO_vfprintf
0x4006e5c0 _IO_vfprintf_internal
0x4006e5c0 __GI_vfprintf
0x4006e5c0 vfprintf
0x40072b40 buffered_vfprintf
0x400788f0 _IO_fprintf
0x400788f0 __GI_fprintf
0x400788f0 fprintf
0x4007c8d0 buffered_vfprintf
Αρχικά (πριν εκτελέσουμε το πρόγραμμα) το μόνο σύμβολο
fprintf που υπήρχε ήταν στη διεύθυνση 0x080483e0. Η
διεύθυνση αυτή ανήκει στο δικό μας πρόγραμμα και επομένως
δεν μπορεί να είναι η αρχή της fprintf()! Στην
πραγματικότητα είναι ένα jmp προς την κανονική fprintf().
Αφού τρέξουμε το πρόγραμμα, ο GDB συνειδητοποιεί πως
φορτώθηκαν βιβλιοθήκες και προσθέτει τα σύμβολα των
συναρτήσεων στη λίστα του. Μια από αυτές που φορτώθηκε
ήταν η libc έτσι προστέθηκε μεταξύ άλλων το σύμβολο
fprintf στη διεύθυνση 0x400788f0 που αποτελεί και τη
πραγματική αρχή της συνάρτησης.
bash$ gdb tst
(gdb) break fprintf
Breakpoint 1 at 0x80482e0
(gdb) r
Starting program: /home/alf/tst
Breakpoint 1 at 0x400788f6
Breakpoint 1, 0x400788f6 in fprintf () from /lib/libc.so.6
(gdb)
Παρατηρήστε ότι ενώ το αρχικό breakpoint ήταν στη
διεύθυνση 0x80482e0 το break έγινε στη διεύθυνση
0x400788f6. Ο GDB αναγνωρίζει ότι το σύμβολο fprintf στη
διεύθυνση 0x80482e0 έχει ειδική σημασία, και όταν
φορτωθεί η πραγματική fprintf() ανανεώνει το breakpoint.
Περισσότερα για το θέμα στο επόμενο τεύχος όταν
εξετάσουμε το ELF.
Επόμενο Προηγούμενο Περιεχόμενα