Επόμενο Προηγούμενο Περιεχόμενα
Τα breakpoints, όπως είχαμε πει και στο προηγούμενο
τεύχος, είναι σημεία στον κώδικα όπου διακόπτεται η
εκτέλεση του προγράμματος και ο έλεγχος επιστρέφει στον
debugger. Όσον αφορά στον τρόπο υλοποίησης τους μπορούν
να χωριστούν σε δύο κατηγορίες: τα software και τα
hardware.
Είναι το είδος που απαντάται πιο συχνά διότι δεν απαιτεί
κάποια υποστήριξη από τον επεξεργαστή. Όταν ορίζουμε ένα
software breakpoint σε κάποια διεύθυνση μνήμης τότε ο
debugger, αφού αποθηκεύσει την εντολή που βρίσκεται σε
εκείνο το σημείο την αντικαθιστά με μια εντολή trap
(software interrupt). Ο debugger έχει φροντίσει να
αποκτήσει τον έλεγχο του trap αυτού. Επομένως, όταν
εκτελεστεί η trap ο έλεγχος επιστρέφει στον debugger, ο
οποίος επανατοποθετεί τα bytes της αρχικής εντολής και
περιμένει οδηγίες. Αφού προχωρήσουμε στο πρόγραμμα
τοποθετείται πάλι η trap, αν πρόκειται για μόνιμο
breakpoint. Στους x86 επεξεργαστές ως trap
χρησιμοποιείται η "int 3" με opcode 0xCC.
Ας δούμε ένα παράδειγμα:
0x8048429 : 0xff 0x75 0xf0 push DWORD PTR [ebp-16]
0x804842c : 0xff 0x75 0xf4 push DWORD PTR [ebp-12]
0x804842f : 0xe8 0x8c 0xfe 0xff 0xff call 0x80482c0
0x8048434 : 0x83 0xc4 0x10 add esp,0x10
Έστω ότι τοποθετούμε ένα breakpoint στην
εντολή call 0x80482c0. Αυτό που συμβαίνει είναι ότι ο
debugger αντικαθιστά το πρώτο byte της εντολής με "0xcc"
αφού βέβαια σώσει κάπου το 0xe8. Οπότε τώρα έχουμε στην
πραγματικότητα:
0x804842f : 0xcc int 3
0x8048430 : 0x8c 0xfe 0xff 0xff ... (σκουπίδια)
0x8048434 : 0x83 0xc4 0x10 add esp,0x10
Τα bytes που απέμειναν από την call (0x8c -
0xff) ίσως να σχηματίζουν μια καινούργια εντολή αλλά πάντως
όχι μια που είναι επιθυμητή! Μην περιμένετε την παραπάνω
εικόνα της μνήμης να τη δείτε ποτέ μέσα από τον debugger με
τον οποίο έχετε τοποθετήσει το breakpoint. Αυτό διότι ο
ίδιος φροντίζει να μας εμφανίζει το "αυθεντικό" περιεχόμενο
της μνήμης όταν του το ζητάμε. Πάντως, παρόλο που δε
μπορείτε να τα δείτε εύκολα, τα software breakpoints όντως
υπάρχουν! Το παρακάτω πρόγραμμα είναι αρκετό για να πείσει
ακόμα και τους πιο "δύσκολους" από εσάς:
#include <stdio.h>
int main(void)
{
if (*((unsigned char *)main+6)==0xcc)
printf("Software Breakpoint detected at main()!\n");
else
printf("No software breakpoint at main()!\n");
return 0;
}
bash$ gcc -o bp_test bp_test.c
bash$ ./bp_test
No software breakpoint at main()!
bash$ gdb bp_test
(gdb) r
Starting program: /home/alf/projects/magaz/issue1/bp_test
No software breakpoint at main()!
Program exited with code 01.
(gdb) break main
Breakpoint 1 at 0x804832e
(gdb) r
Starting program: /home/alf/projects/magaz/issue1/bp_test
Breakpoint 1, 0x0804832e in main ()
(gdb) c
Continuing.
Software Breakpoint detected at main()!
Program exited with code 01.
Ο λόγος που ο έλεγχος γίνεται στο 7 byte της main()
(main+6) και όχι στο πρώτο (main), είναι ότι ο GDB
τοποθετεί τα breakpoints μετά από τον πρόλογο της συνάρτησης.
Τα hardware breakpoints βασίζονται στο υλικό και για αυτό
ο ακριβής τρόπος υλοποίησης διαφέρει ανάλογα με τον
επεξεργαστή. Στην αρχιτεκτονική x86 (386+) μέσα στον
επεξεργαστή υπάρχουν 4 debug registers DR0-DR3. Σε κάθε
έναν από αυτούς μπορεί να ανατεθεί ο έλεγχος μιας θέσης
μνήμης, ώστε να προκαλεί ένα interrupt όταν η διεύθυνση
μνήμης που τον αφορά διαβαστεί, γραφτεί, διαβαστεί ή
γραφτεί ή εκτελεστεί ως μέρος της τρέχουσας εντολής (R,
W, RW, X). Στα τρία πρώτα modes έχουμε λειτουργία
watchpoint και πρόκειται για τον μόνο πρακτικό τρόπο
υλοποίησης τους. Με software, το μόνο που θα μπορούσαμε
να κάνουμε είναι να ελέγχουμε κάθε εντολή για
προσπελάσεις στη μνήμη, διαδικασία εξαιρετικά
χρονοβόρα. Εκμεταλλευόμενοι το hardware, κάθε προσπέλαση
στη μνήμη συγκρίνεται σιωπηλά με τα περιεχόμενα των DR's
και αν υπάρξει κάποιο ταίριασμα επεμβαίνει ο debugger. Οι
έλεγχοι γίνονται παράλληλα με την υπόλοιπη λειτουργία του
επεξεργαστή και για αυτό δεν υπάρχει καμία καθυστέρηση.
Για να χρησιμοποιήσουμε hardware breakpoints στον GDB
αντί για την εντολή break χρησιμοποιούμε την
hbreak. H σύνταξη είναι ακριβώς όμοια με την
break.
Συχνά χρειάζεται να σταματήσουμε ένα κομμάτι κώδικα πριν
εκτελεστεί αλλά δεν μπορούμε να το κάνουμε με τη break
του GDB διότι πχ ο κώδικας βρίσκεται σε μια shared
βιβλιοθήκη που δεν έχει φορτωθεί ακόμα. Ένας τρόπος να
πετύχουμε το σκοπό μας είναι να τοποθετήσουμε μια εντολή
trap (0xCC για x86) στο σημείο που θέλουμε, αφού γράψουμε
κάπου πιο byte ήταν πριν εκεί. Αυτό βέβαια θα πρέπει να
γίνει στο αρχείο με έναν hex editor. Όταν τρέξει ο
επιθυμητός κώδικας θα δούμε κάτι σαν το παρακάτω:
Program received signal SIGTRAP, Trace/breakpoint trap.
0x0804835d in function ()
(gdb) print $eip$1 = (void *) 0x804835d
(gdb) set *(--(char *)($eip))=0x55
(gdb) r
...
Αυτό που κάνουμε είναι ουσιαστικά ότι ακριβώς
κάνει ο debugger όταν βρει ένα breakpoint που έχουμε
τοποθετήσει με την
break.Η εντολή
set *(--(char *)($eip))=0x55
Πιo καθαρά θα μπορούσε να γραφτεί:
(gdb) set $eip=$eip-1
(gdb) set *(char *)$eip=0x55 ή (gdb) set {char}$eip=0x55
Χρειάζεται να μειώσουμε τον eip, διότι όταν προκληθεί το
trap ο έλεγχος επιστρέφει στον GDB με τον eip να δείχνει
στην αμέσως επόμενη εντολή. Επομένως, πηγαίνουμε μια θέση
πίσω στη μνήμη και γράφουμε το αυθεντικό byte (εδώ το
0x55, push ebp). Προσοχή στο casting σε (char *). Είναι
απαραίτητο γιατί αλλιώς ο GDB νομίζει (από default) πως
θέλουμε να γράψουμε τον ακέραιο 0x55 (0x00000055)
και έτσι θα γράψει 4 bytes αντί 1 που θέλουμε εμείς!
Επόμενο Προηγούμενο Περιεχόμενα