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.