2007-04-05

Niskopoziomowy debugging w linuxie, czyli gdzie się podziały DR0-DR7?

Przypomniałem sobie, że przecierz i386 ma rejestry służące do debuggowania! A świat o nich zapomniał tak dawno.
Nie bardzo jeszcze wiem do czego może się przydać ich używanie, ale to nie jest ważne :)

No to do roboty!

Wiadomo że gdb potrafi używać sprzętowego debugowania, komunikaty w stylu poniższego już nie raz śniły mi się:
(gdb) awatch *(0x8049534)
Hardware access (read/write) watchpoint 1: *134518068


Pytanie tylko, jak to działa. To co wiem, to że na pewno ptrace
jest podstawą działania gdb. Choć z drugiej strony w dokumentacji
ptrace nie ma żadnej wzmianki na temat ustawiania rejestrów DR.

Skrobnijmy pierwszy programik:
#include <stdio.h>
int main(){
unsigned long db_regs;
asm volatile ("mov %%dr0, %0" : "=r"(db_regs));
printf("%08lx\n", db_regs);
return(0);
}


majek:~/mutex$ gcc -Wall a.c
majek:~/mutex$ ./a.out
Segmentation fault

No tak, to było do przewidzenia, przecierz w dokumentacji intela pisze jednoznacznie: "The debug registers are privileged resources; the MOV instructions that access them can only be executed at privilege level zero." Hmm, ale gdb potrafi ustawiać je bez roota.

Weźmy nowy, równie prosty program, ale tym razem interesuje nas, jak działa gdb:
#include <stdio.h>
int i;
int main(){
for(i=0; i<4; i++);
return(0);
}


majek:~/mutex$ gcc -Wall b.c
majek:~/mutex$ strace gdb ./a.out 2>log
(gdb) maint show-debug-regs on
(gdb) awatch i
Hardware access (read/write) watchpoint 1: i
(gdb) run
Starting program: a.out
[....]
watchpoint_hit (addr=8049534, len=-1, type=data-write):
CONTROL (DR7): 000f0101 STATUS (DR6): ffff4ff1
DR0: addr=0x08049534, ref.count=1 DR1: addr=0x0000000, ref.count=0
DR2: addr=0x00000000, ref.count=0 DR3: addr=0x0000000, ref.count=0
remove_watchpoint (addr=8049534, len=4, type=data-read/write):
CONTROL (DR7): 000f0100 STATUS (DR6): ffff4ff1
DR0: addr=0x00000000, ref.count=0 DR1: addr=0x0000000, ref.count=0
DR2: addr=0x00000000, ref.count=0 DR3: addr=0x0000000, ref.count=0
Hardware access (read/write) watchpoint 1: i

Więc gdb jednak jakoś odczytał zawartość DR0-DR7, a nawet je ustawił.
Zajrzyjmy do loga strace:
majek:~/mutex$ cat log <palka> grep ptrace <palka> grep USER
[....]
ptrace(PTRACE_PEEKUSER, 28498, offsetof(struct user, u_debugreg) + 24, [0xffff4ff1]) = 0
ptrace(PTRACE_POKEUSER, 28498, offsetof(struct user, u_debugreg) + 28, 0xf0100) = 0
ptrace(PTRACE_POKEUSER, 28498, offsetof(struct user, u_debugreg), 0) = 0
[....]

No to mamy już całą magię. Jeszcze rzut okiem na <sys/user.h> I mamy już pełną jasność, ptrace z pierwszej linijki odczytuje DR7, a kolejne dwie linijki zapisują dane do DR7 i DR0.

No to programik testowy, tym razem dłuższy więc w zamieszczam tylko w osobnym pliku.
majek:~/mutex$ gcc -Wall c.c && ./a.out
#28848 child started
#28847 parent started
before change: DR0=0x00000000 DR7=0x00000000
after change: DR0=0x08049a3c DR7=0x00050101
#28847 parent stopped
#28848 got SIGTRAP
#28848 child stopped

Hurray! Udało się. Zmieniając zmienną wywołujemy SIGTRAP dla naszego procesu, i SIGCHLD dla procesu który nas śledzi.

Dla czytelnika zostawiam pytanie do czego użyć prezentowanej techniki.

Myślę, że niektórzy mogą się domyślać co kombinuję, ale zostawmy to na kolejny wpis.



No comments: