2.2 Debugger

W tej części opisze jak pracować z debugerem. Jest to oczywiście docelowo program stosowany do znajdowania błędów w działaniu programu. Jednak daje on przede wszystkim możliwość wcielenia się w rolę samego procesora i sprawdzeniu jak on wykonuje poszczególne instrukcje podglądając w między czasie wartości wszystkich rejestrów. Do tego celu zamierzam użyć programu debug.exe, który jest dość starej daty, ale bardzo dobrze nadaje się do analizowania kodu programów napisanych z myślą o trybie rzeczywistym procesora.

Program oczywiście ściągamy i przegrywamy do głównego katalogu d:\asm\. Przeanalizujemy za jego pomocą pierwszy program napisany w asemblerze, który będzie wykonywał bardzo prosty algorytm dodawania trzech liczb 1, 2 i 3.

mov	ax,	1
add	ax,	2
add	ax,	3

Program kompilujemy oczywiście z poziomu DOSBoxa poleceniem:

nasm -f bin -o prog01.com prog01.asm

Kompilacja powinna przebiec bez zgłaszania żadnych informacji na konsole. W tym programie również nie zdefiniowaliśmy jeszcze końca programu. Jego analizę przeprowadzę za pomocą programu debug.exe. Program ten należy wywołać podając jako parametr nazwę pliku wykonywalnego:

debug prog01.com

Powinniśmy otrzymać znak zgłoszenia się programu "-". W tym momencie program debug oczekuje na wprowadzenie komendy. Wprowadźmy jedną z podstawowych jego komend "r" oznaczającą wypisanie aktualnego stanu procesora podczas wykonywania programu. Debug rozpoczyna wykonywanie naszego programu prog01.com w trybie pracy krokowej. To znaczy iż nie wykona on żadnej instrukcji procesora póki nie każemy mu tego zrobić. Więc debug załadował do pamięci pod pewien adres nasz program. Następnie ustawił procesorowi adres następnej wykonywanej instrukcji CS:IP na początek tego kodu. I teraz czeka aż wydamy mu następne polecenie.

C:\>debug prog01.com -r AX=FFFF BX=0000 CX=0009 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=075E ES=075E SS=075E CS=075E IP=0100 NV UP EI PL NZ NA PO NC 075E:0100 B80100 MOV AX,0001 -q

Po wpisaniu komendy "r" otrzymamy bardzo dokładny wydruk stanów rejestrów procesora tuż przed wykonaniem pierwszej instrukcji kodu programu. Pierwsze dwie linie przedstawiają wartości podstawowych rejestrów CPU. Rejestr flag został przedstawiony jako opis stanów poszczególnych bitów w postaci "NV UP EI PL NZ  NA PO NC". Widać od razu, że gdy procesor przechodzi do wykonywania naszego programu to rejestry zawierają różne wartości pochodzące z poprzednich instrukcji jakie wykonywał procesor. Ostatnia linia wydruku programu informuje o instrukcji która będzie wykonywana w następnym kroku przez procesor. Komenda "t" (lub komenda "p" działająca w bardzo podobny sposób) służy do wykonania kolejnej instrukcji procesora i ponownym wypisaniu stanu procesora. Po prześledzeniu kolejnych instrukcji powinniśmy otrzymać coś podobnego do tego wydruku:

-r AX=FFFF BX=0000 CX=0009 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=075E ES=075E SS=075E CS=075E IP=0100 NV UP EI PL NZ NA PO NC 075E:0100 B80100 MOV AX,0001 -t AX=0001 BX=0000 CX=0009 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=075E ES=075E SS=075E CS=075E IP=0103 NV UP EI PL NZ NA PO NC 075E:0103 83C002 ADD AX,+02 -t AX=0003 BX=0000 CX=0009 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=075E ES=075E SS=075E CS=075E IP=0106 NV UP EI PL NZ NA PE NC 075E:0106 83C003 ADD AX,+03 -t AX=0006 BX=0000 CX=0009 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=075E ES=075E SS=075E CS=075E IP=0109 NV UP EI PL NZ NA PE NC 075E:0109 333F XOR DI,[BX] DS:0000=20CD

Widać jak zmienia się zawartość rejestru ax podczas wykonywania kolejnych instrukcji. Jak można się domyślić po pokazanych wartościach innych rejestrów są to wartości w systemie hexadecymalnym. Dodatkowo warto przyjrzeć się trzeciej linii wydruku stanu procesora po każdym kroku. Na samym początku każdej linii podany jest adres segment:offset kolejnej instrukcji jaka będzie wykonywana. Co nie jest chyba zaskoczeniem są to wartości dokładnie takie jakie są zapisane w rejestrach CS:IP. Po wykonaniu naszego programu procesor automatycznie wykonuje instrukcje znajdujące się w dalszej części pamięci. Kolejną instrukcją jest "XOR DI,[BX]", która nie stanowiła części programu. Są to przypadkowe dane znajdujące się za właściwym programem i z powodu braku zakończenia programu procesor wykona całą masę takich przypadkowych instrukcji.

Ciekawszą rzeczą pokazywaną w trzeciej linii jest jeszcze kod maszynowy instrukcji. Znajduje się on między adresem instrukcji, a jej kodem asemblerowym. Tak więc instrukcja "mov ax, 1" została zakodowana w postaci 3 bajtów o wartościach 0xB8, 0x01, 0x00. Pierwszy bajt określa jednoznacznie typ instrukcji i rodzaj pobieranych przez nią parametrów. Niektóre instrukcje potrzebują kilku bajtów aby je oznaczyć w kodzie maszynowym. Dwa kolejne bajty w tym przypadku oznaczają drugi argument instrukcji czyli wartość bezpośrednią 1. Widać więc, że wartości liczbowe są na stałe włączane do kodu instrukcji podczas kompilacji.

Należało by również zwrócić uwagę na to, że wartość 16 bitowa która ma zostać wpisana do rejestru ax jest zapisana w odwrotnej kolejności jeżeli chodzi o reprezentację bajtową. Jest to konwencja w jakiej pracują komputery klasy PC. Przy zapisie wartości które składają się z kilku bajtów najpierw zapisywane są bajty odpowiadające najmniej znaczącym wartością. Więc gdybyśmy chcieli do rejestru ax, przesłać wartość 0x1234 to kod maszynowy tej instrukcji wyglądał by następująco: 0xB8 0x34 0x12.

Z ciekawszych komend programu debug muszę zaprezentować jeszcze dwie. Komenda "u" oznacza wykonanie operacji odwrotnej do kompilowania czyli deasemblacji pewnego obszaru pamięci. Komenda ta przyjmuje do dwóch parametrów. Pierwszy z nich jest adresem segment:offset początku pamięci. Jeżeli chcemy deasemblować większą partię kodu to jako drugi parametr trzeba podać koniec obszaru pamięci. Dla analizowanego przeze mnie przykładu wpisałem komende "u 75E:100".

-u 75E:100 075E:0100 B80100 MOV AX,0001 075E:0103 83C002 ADD AX,+02 075E:0106 83C003 ADD AX,+03 075E:0109 333F XOR DI,[BX] 075E:010B 3030 XOR [BX+SI],DH 075E:010D 3070F0 XOR [BX+SI-10],DH 075E:0110 E000 LOOPNZ 0112 075E:0112 0000 ADD [BX+SI],AL 075E:0114 0000 ADD [BX+SI],AL 075E:0116 7F63 JG 017B 075E:0118 7F63 JG 017D 075E:011A 63 DB 63 075E:011B 63 DB 63 075E:011C 67 DB 67 075E:011D E7E6 OUT E6,AX 075E:011F C0 DB C0

Adres 0x75E:0x100 oczywiście określa początek programu w pamięci. Wszystkie wartości w programie debug są wartościami szesnastkowymi nawet te które sami wprowadzamy. Widzimy na powyższym wydruku iż za naszym programem znajduje się obszar zawierający inne instrukcje i nie jest to na pewno część programu. Do przeglądania zawartości pamięci w postaci danych służy komenda "d". Jako parametr podajemy adres pamięci. Po wpisaniu komendy "d 75E:100" otrzymałem:

-d 75E:100 075E:0100 B8 01 00 83 C0 02 83 C0-03 33 3F 30 30 30 70 F0 .........3?000p. 075E:0110 E0 00 00 00 00 00 7F 63-7F 63 63 63 67 E7 E6 C0 .......c.cccg... 075E:0120 00 00 00 00 18 18 DB 3C-E7 3C DB 18 18 00 00 00 .......<.<...... 075E:0130 00 00 80 C0 E0 F8 FE F8-E0 C0 80 00 00 00 00 00 ................ 075E:0140 02 06 0E 3E FE 3E 0E 06-02 00 00 00 00 00 18 3C ...>.>.........< 075E:0150 7E 18 18 18 7E 3C 18 00-00 00 00 00 66 66 66 66 ~...~<......ffff 075E:0160 66 66 00 66 66 00 00 00-00 00 7F DB DB DB 7B 1B ff.ff.........{. 075E:0170 1B 1B 1B 00 00 00 7C C6-60 38 6C C6 6C 38 0C C6 ......|.`8l.l8..

Po lewej stronie każdej z linii otrzymujemy adres następnie mamy przedstawione wartości kolejnych 16 bajtów z pod tego adresu. W ostatniej kolumnie również przedstawione są wartości tych samych bajtów jednak w reprezentacji ich kodów ASCII. O tych kodach mam nadzieję wspomnę jeszcze szerzej w przyszłych rozdziałach. Powyżej widać bardzo wyraźnie jak wygląda program w pamięci. Jest to po prostu ciąg kolejnych bajtów.

Aby wyjść z programu debug należy wpisać komendę "q".

Dość zwięźle przedstawiłem podstawy programu debug.exe. Można by się zastanawiać po co poznawać jego możliwości skoro działa on najwyżej w Windows XP. Jednak nowsze debuggery kodu maszynowego posiadają praktycznie identyczne komendy lub funkcje więc warto je teraz poznać aby wiedzieć czego można oczekiwać od większości debuggerów.