Asembler x86/Jak używać debuggera ALD
Wprowadzenie
[edytuj]Debugger jest programem służącym do analizy kodu wykonywalnego. Pozwala zrozumieć to, co dzieje się w trakcie wykonywania skompilowanego programu jak również odnaleźć ewentualne błędy (z ang. bugs). Wykorzystamy w tym celu program ald (Assembly Language Debugger). Jest to przykładowy debugger przeznaczony dla języka asembler. Pozwala on śledzić wykonywanie programu krok po kroku, jak również zatrzymać się tylko w konkretnych miejscach (tzw. pułapkach), by oglądnąć zawartość rejestrów, pamięci i stosu. Dzięki temu w prosty sposób możemy odnaleźć miejsca programu, gdzie jego działanie poszło nie po naszej myśli.
Instalacja
[edytuj]Posiadając świeżą instalację sytemu
Fedora Linux zanim rozpoczniemy proces
konfiguracji i kompilacji należy wcześniej
doinstalować pakiety gcc oraz readline-devel
za pomocą poleceń:
yum install gcc
yum install readline-devel
W przypadku Ubuntu należy doinstalować pakiet
libreadline-dev przy pomocy Synaptic
Package Manager znajdującego się w menu
System/Administration, lub wykonać polecenie:
sudo apt-get install libreadline-dev.
Źródła aktualnej wersji programu dostępne są na stronie projektu (http://sourceforge.net/projects/ald/). Wewnątrz rozpakowanego folderu ald-x.y.z wykonujemy jako zwykły użytkownik polecenia konfiguracji i kompilacji pakietu:
./configure
make
Następnie w celu zainstalowania aplikacji wraz z dokumentacją man w naszym systemie jako administrator wydajemy polecenie:
make install
W niektórych dystrybucjach należy wydać polecenie:
sudo make install
W przypadku, gdy nie posiadamy konta administratora, skompilowany plik wykonywalny ald znajdujemy w katalogu ald-x.y.z/source, skąd możemy go skopiować do katalogu, z którego będziemy go wykonywać. W praktyce może to być ten sam katalog, w którym znajdują się pliki asm. Należy wówczas pamiętać, że w celu uruchomienia debugera wydajemy polecenie:
./ald
Możemy zatem przejść do następnego rozdziału.
Pierwsze uruchomienie
[edytuj]Dla pewności, czy proces instalacji zakończył się pomyślnie możemy zapoznać się z podstawową dokumentacją pakietu wydając polecenie man ald, jak również uruchomić aplikację poleceniem ald bez żadnych argumentów.
[myself@localhost ~]$ ald
Assembly Language Debugger 0.1.7
Copyright (C) 2000-2004 Patrick Alken
ald>
Chcąc zapoznać się z podstawową pomocą wewnątrz aplikacji wydajemy polecenie help. Aby opuścić aplikację wydajemy polecenie quit.
Prosta deasemblacja
[edytuj]Wykorzystamy do tego celu skompilowaną wersję "pierwszego programu" wyświetlającego napis Hello world!. Uruchamiamy debugger podając nazwę pliku wykonywalnego jako argument:
ald hello
W efekcie czego otrzymamy podstawowe informacje dotyczące pliku wykonywalnego hello.
user@myhost:~$ ald hello
Assembly Language Debugger 0.1.7
Copyright (C) 2000-2004 Patrick Alken
hello: ELF Intel 80386 (32 bit), LSB - little endian, Executable, Version 1 (Current)
Loading debugging symbols...(10 symbols loaded)
ald>
Aby deasemblować dany plik z poziomu ALD wprowadzamy polecenie d i potwierdzamy klawiszem ENTER.
ald> d
08048080:<_start> B804000000 mov eax, 0x4
08048085 BB01000000 mov ebx, 0x1
0804808A B9A4900408 mov ecx, 0x80490a4
0804808F BA0E000000 mov edx, 0xe
08048094 CD80 int 0x80
08048096 B801000000 mov eax, 0x1
0804809B BB05000000 mov ebx, 0x5
080480A0 CD80 int 0x80
080480A2 0000 add byte [eax], al
080480A4 48 dec eax
080480A5 656C insb
080480A7 6C insb
080480A8 6F outsd
080480A9 2C20 sub al, 0x20
080480AB 776F ja +0x6f (0x804811c)
080480AD 726C jc +0x6c (0x804811b)
080480AF 64210A and dword [fs:edx], ecx
080480B2 002E add byte [esi], ch
080480B4 7379 jnc +0x79 (0x804812f)
080480B6 6D insd
080480B7 7461 je +0x61 (0x804811a)
080480B9 6200 bound eax, dword [eax]
080480BB 2E7374 jnc +0x74 (0x8048131)
080480BE 7274 jc +0x74 (0x8048134)
Hit <return> to continue, or <q> to quit
Widzimy, że pierwsze wiersze przykładowego listingu w ostatniej kolumnie do złudzenia przypominają niedawno kompilowany kod źródłowy napisany w języku asembler. Zauważamy jedynie, że używane etykiety często zastąpione są już konkretnymi adresami liczbowymi w używanej pamięci. Lewa kolumna zawiera kolejno adresy pamięci zajmowanej przez dane polecenie. Środkowa kolumna zawiera polecenia w kodzie maszynowym. W tym momencie nie chcemy znać zawartości dalszej pamięci, więc naciskamy klawisz q a następnie ENTER.
Wykonanie krok-po-kroku
[edytuj]Aby wykonać pierwszą instrukcję, naciskamy n po czym klawisz ENTER.
ald> n
eax = 0x00000004 ebx = 0x00000000 ecx = 0x00000000 edx = 0x00000000
esp = 0xBFE4B130 ebp = 0x00000000 esi = 0x00000000 edi = 0x00000000
ds = 0x007B es = 0x007B fs = 0x0000 gs = 0x0000
ss = 0x007B cs = 0x0073 eip = 0x08048085 eflags = 0x00200212
Flags: AF IF ID
08048085 BB01000000 mov ebx, 0x1
ald>
Wartość rejestru EIP zawiera adres następnej instrukcji czekającej na wykonanie przez procesor. Ostatni wiersz powyższego listingu zawiera tę instrukcję. Aby ją wykonać, wystarczy już tylko nacisnąć ENTER.
ald>
eax = 0x00000004 ebx = 0x00000001 ecx = 0x00000000 edx = 0x00000000
esp = 0xBFE4B130 ebp = 0x00000000 esi = 0x00000000 edi = 0x00000000
ds = 0x007B es = 0x007B fs = 0x0000 gs = 0x0000
ss = 0x007B cs = 0x0073 eip = 0x0804808A eflags = 0x00200212
Flags: AF IF ID
0804808A B9A4900408 mov ecx, 0x80490a4
ald>
.
.
.
Możemy w ten sposób prześledzić, jak zmienia się zawartość rejestrów w trakcie wykonywania programu. Po wykonaniu ostatniej instrukcji powinniśmy otrzymać komunikat o zakończeniu działania programu.
.
.
.
ald>
eax = 0x00000001 ebx = 0x00000005 ecx = 0x080490A4 edx = 0x0000000E
esp = 0xBFE4B130 ebp = 0x00000000 esi = 0x00000000 edi = 0x00000000
ds = 0x007B es = 0x007B fs = 0x0000 gs = 0x0000
ss = 0x007B cs = 0x0073 eip = 0x080480A0 eflags = 0x00200212
Flags: AF IF ID
080480A0 CD80 int 0x80
ald>
Program terminated normally (Exit status: 0x0005)
ald>
Warto zwrócić uwagę, że kod błędu zwracany przez program zawiera się w rejestrze EBX.
Wstrzymanie wykonania
[edytuj]W przypadku dłuższych i bardziej skomplikowanych algorytmów wykonywanie krok po kroku może być uciążliwe i czasochłonne. Aby usprawnić ten proces możemy ustawić pułapkę (tzw. breakpoint) tylko w miejscu, w którym chcielibyśmy wykonanie programu wstrzymać, by odczytać jego stan i następnie prześledzić krok po kroku, w którym miejscu następuje błąd wykonania.
Przykładowo poniższy program miał za zadanie wyświetlenie zawartości rejestru EAX w systemie dziesiętnym.
;; decimalPrint.asm
;; Wypisuje zawartość EAX w systemie dziesiętnym
;;
segment .text
global _start
_start:
mov eax,0x0007
call _printEAXdecimal
;wyjscie
mov eax,1
mov ebx,5
int 0x80
segment .data
msg db ' '
segment .text
_printEAXdecimal:
push edx
push ecx
push ebx
push eax
mov ebx,10
mov ecx,10
xor edx,edx
N1:
div ebx
add edx,48
dec ecx
mov [msg+ecx],dl
cmp ecx,0
jne N1
mov eax,4
mov ebx,1
mov ecx,msg
mov edx,10
int 0x80
pop eax
pop ebx
pop ecx
pop edx
ret
Na pierwszy rzut oka wszystko wydaje się być w porządku. Kompilacja wraz z linkowaniem przebiegła pomyślnie. Próba wykonania programu daje jednak poniższy wynik:
user@myhost:~$ ./decimalPrint
Floating point exception
user@myhost:~$
Program nie miał wykonywać żadnych operacji zmiennoprzecinkowych. Przypuszczamy jednak, że problem może mieć miejsce w wierszu, w którym wykonujemy operację dzielenia całkowitego:
div ebx
Chcielibyśmy zatem wstrzymać działanie programu tuż przed wykonaniem tej operacji, by dokładnie sprawdzić argumenty instrukcji oraz wynik jej działania. W tym celu uruchamiamy debugger poleceniem:
user@myhost:~$ ald decimalPrint
Assembly Language Debugger 0.1.7
Copyright (C) 2000-2004 Patrick Alken
decimalPrint: ELF Intel 80386 (32 bit), LSB - little endian, Executable, Version 1 (Current)
Loading debugging symbols...(11 symbols loaded)
ald>
Następnie przeprowadzamy deasemblację (klawisz d i ENTER) w celu poznania adresu wskaźnika do interesującej na instrukcji:
ald> d
08048080:<_start> B807000000 mov eax, 0x7
08048085 E80C000000 call near +0xc (0x8048096:_printEAXdecimal)
0804808A B801000000 mov eax, 0x1
0804808F BB05000000 mov ebx, 0x5
08048094 CD80 int 0x80
08048096:<_printEAXdecimal> 52 push edx
08048097 51 push ecx
08048098 53 push ebx
08048099 50 push eax
0804809A BB0A000000 mov ebx, 0xa
0804809F B90A000000 mov ecx, 0xa
080480A4 31D2 xor edx, edx
080480A6:<N1> F7F3 div ebx
080480A8 81C230000000 add edx, 0x30
080480AE 49 dec ecx
080480AF 8891D8900408 mov byte [ecx+0x80490d8], dl
080480B5 81F900000000 cmp ecx, 0x0
080480BB 75E9 jne +0xe9 (0x80481a6)
080480BD B804000000 mov eax, 0x4
080480C2 BB01000000 mov ebx, 0x1
080480C7 B9D8900408 mov ecx, 0x80490d8
080480CC BA0A000000 mov edx, 0xa
080480D1 CD80 int 0x80
080480D3 58 pop eax
Hit <return> to continue, or <q> to quit
Naciskamy klawisz q oraz ENTER, by nie wyświetlać dalszych obszarów pamięci. Szukany adres to 0x080480A6. Trzeba zatem pod wskazanym adresem ustawić pułapkę. Do tego celu służy polecenie break:
ald> break 0x080480a6
Breakpoint 1 set for 0x080480A6
ald>
Dla pewności możemy wyświetlić wszystkie zdefiniowane pułapki, aby mieć pewność, czy program nie będzie dodatkowo wstrzymany w żadnym innym miejscu. Do tego celu służy polecenie lbreak:
ald> lbreak
Num Type Enabled Address IgnoreCount HitCount
1 Breakpoint y 0x080480A6 none 0 (N1+0x0)
ald>
Jeśli wszystko się zgadza, możemy uruchomić program poleceniem run:
ald> run
Starting program: decimalPrint
Breakpoint 1 encountered at 0x080480A6
eax = 0x00000007 ebx = 0x0000000A ecx = 0x0000000A edx = 0x00000000
esp = 0xBFC9251C ebp = 0x00000000 esi = 0x00000000 edi = 0x00000000
ds = 0x007B es = 0x007B fs = 0x0000 gs = 0x0000
ss = 0x007B cs = 0x0073 eip = 0x080480A6 eflags = 0x00000246
Flags: PF ZF IF
080480A6:<N1> F7F3 div ebx
ald>
Przede wszystkim interesują nas zawartości rejestrów EAX, EBX oraz EDX, gdyż tylko one są argumentami powyższej instrukcji. Dzielna znajduje się w rejestrach EDX:EAX, natomiast dzielnik umieszczony jest w rejestrze EBX. Na tym etapie nie widać żadnych potencjalnych problemów, możemy więc kontynuować wykonanie programu krok po kroku używając polecenia n:
ald> n
eax = 0x00000000 ebx = 0x0000000A ecx = 0x0000000A edx = 0x00000007
esp = 0xBFC9251C ebp = 0x00000000 esi = 0x00000000 edi = 0x00000000
ds = 0x007B es = 0x007B fs = 0x0000 gs = 0x0000
ss = 0x007B cs = 0x0073 eip = 0x080480A8 eflags = 0x00000246
Flags: PF ZF IF
080480A8 81C230000000 add edx, 0x30
ald>
Dzielenie całkowite zostało wykonane poprawnie. Wynik dzielenia znajduje się w rejestrze EAX, natomiast reszta z dzielenia w rejestrze EDX. Być może w następnym przebiegu pętli ujawnią się jakieś problemy. Kontynuujemy zatem wykonanie programu poleceniem continue (lub w skrócie c):
ald> c
Breakpoint 1 encountered at 0x080480A6
eax = 0x00000000 ebx = 0x0000000A ecx = 0x00000009 edx = 0x00000037
esp = 0xBFC9251C ebp = 0x00000000 esi = 0x00000000 edi = 0x00000000
ds = 0x007B es = 0x007B fs = 0x0000 gs = 0x0000
ss = 0x007B cs = 0x0073 eip = 0x080480A6 eflags = 0x00000206
Flags: PF IF
080480A6:<N1> F7F3 div ebx
ald>
Na pierwszy rzut oka tym razem również wszystko jest w porządku. Jednak po wnikliwej analizie zawartości rejestrów zauważymy, że w rejestrze EDX znajdują się pozostałości z poprzedniego obiegu pętli. Aby uniknąć błędnych wyników, należy poprawić kod tak, aby zerowanie rejestru EDX przeprowadzać na początku każdego obiegu pętli przed wykonaniem operacji dzielenia. Spróbujemy mimo wszystko kontynuować działanie programu krok po kroku:
ald> n
Program received signal SIGFPE (Arithmetic exception)
Location: 0x080480A6
eax = 0x00000000 ebx = 0x0000000A ecx = 0x00000009 edx = 0x00000037
esp = 0xBFBA9ADC ebp = 0x00000000 esi = 0x00000000 edi = 0x00000000
ds = 0x007B es = 0x007B fs = 0x0000 gs = 0x0000
ss = 0x007B cs = 0x0073 eip = 0x080480A6 eflags = 0x00010206
Flags: PF IF RF
080480A6:<N1> F7F3 div ebx
ald>
Widzimy, że operacja dzielenia nie została wykonana i zgłoszony jest wyjątek za pomocą sygnału SIGFPE. Kolejna próba wykonania operacji skończy się zamknięciem programu wraz ze zwróceniem kodu błędu:
ald> n
Program terminated with signal SIGFPE (Arithmetic exception)
ald>
Po wprowadzeniu stosownych poprawek prawidłowy kod źródłowy będzie miał postać:
;;
;; Wypisuje zawartość EAX w systemie dziesiętnym
;;
segment .text
global _start
_start:
mov eax,0x0007
call _printEAXdecimal
;wyjscie
mov eax,1
mov ebx,5
int 0x80
segment .data
msg db ' '
segment .text
_printEAXdecimal:
push edx
push ecx
push ebx
push eax
mov ebx,10
mov ecx,10
N1:
xor edx,edx
div ebx
add edx,48
dec ecx
mov [msg+ecx],dl
cmp ecx,0
jne N1
mov eax,4
mov ebx,1
mov ecx,msg
mov edx,10
int 0x80
pop eax
pop ebx
pop ecx
pop edx
ret
Po udanej kompilacji i linkowaniu możemy uruchomić program:
user@myhost:~$ ./decimalPrint
0000000007user@myhost:~$
Możemy także przetestować go z innymi wartościami rejestru EAX. W tym celu zmieniamy linijkę kodu:
mov eax,0x0007
na inną przykładową:
mov eax,0xffffffff
Po udanej kompilacji i linkowaniu możemy znowu uruchomić program:
user@myhost:~$ ./decimalPrint
4294967295user@myhost:~$
Skoro odnaleźliśmy i usunęliśmy przyczynę błędu wykonania, możemy przystąpić do rozbudowy naszego programu tak, aby pomijane były zera wiodące, ewentualnie aby za wyświetlaną liczbą umieszczany był dodatkowo znak końca linii.