home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Amiga MA Magazine 1997 #3
/
amigamamagazinepolishissue03-1
/
ma_1995
/
01
/
ami012.txt
< prev
next >
Wrap
Text File
|
1997-04-06
|
18KB
|
359 lines
Co to jest debugger (cz. 2.)
----------------------------
ODPLUSKWIACZE
<lead> W pierwszej czëôci opisaîem dwa najbardziej znane
debuggery: Enforcera i Mungwalla. Dziô czas zajâê sië innymi, nie
tak znanymi ani rozbudowanymi, co jednak wcale nie znaczy: mniej
przydatnymi.
<a> Kamil Iskra
<txt> Na dysku 870 znanej wszystkim biblioteki Freda Fisha
znalazîem program StackCheck 1.0 autorstwa Guenthera Roehricha. W
niedîugi czas póúniej miaîem okazjë przekonaê sië o jego
wartoôci, kiedy to robocza wersja jednego z moich programów z
uporem maniaka zawieszaîa sië pod OS 3.0, podczas gdy pod OS 2.04
dziaîaîa w zasadzie prawidîowo. Próbowaîem wykryê bîâd Mungwallem
i innymi debuggerami i... nic -- kilkudniowe poszukiwania byîy
bezowocne. W odruchu ostatniej rozpaczy uruchomiîem StackChecka.
Program bez ûadnego ociâgania, krótko i dosadnie napisaî:
<l>
StackCheck V1.0 by Guenther Roehrich
This program is Public Domain. Press CTRL-C to abort.
Free stack area was cleared:
0038519C: 00000000 00000000 00000000 00000000
Task stack overflowed:
0038519C: BAD1BAD3 BEEFBEEF BEEFBEEF BEEFBEEF
Stacksize: 4096
<txt> Jednym sîowem, stwierdziî, ûe mój program po prostu
przepeîniî stos procesora. Przepeînienie stosu polega na
"wyjechaniu" poza jego granice -- zapisywany jest przypadkowy
obszar pamiëci. Moûna w tym momencie zadaê sobie pytanie,
dlaczego Mungwall tego bîëdu nie wykrywa, przecieû obkîada on
kaûdy przydzielony obszar pamiëci "ôcianami". Powód jest prosty.
Mungwall zgîasza bîâd tylko wtedy, gdy wykryje, ûe zwalniany
obszar jest obîoûony ôcianami. Tymczasem przepeîniajâc stos o
kilkaset bajtów czy kilobajt powodujemy, ûe ze ôcian ani
poprzedzajâcego ich specjalnego nagîówka nie zostaje najmniejszy
ôlad. Istnieje kilka innych programów sprawdzajâcych stan stosu,
wedîug mnie jednak ûaden z nich (mówië o tych, które widziaîem:
WatchStack, XOper) nie dorasta StackCheckowi do piët. Wynika to
z róûnych zasad dziaîania.
StackCheck uruchamia sië z jednym parametrem: nazwâ procesu,
który ma byê monitorowany (program ma teû kilka opcjonalnych
parametrów, zwykle nieistotnych). Jeûeli proces o podanej nazwie
nie istnieje, StackCheck czeka, aû sië pojawi (np. aû uruchomisz
program). Wolny obszar monitorowanego stosu jest wstëpnie
wypeîniany sekwencjami 0xBEEF, po czym pozostawia sië go "samemu
sobie". Raz na jakiô czas StackCheck sprawdza, jaki obszar stosu
byî w uûyciu -- rozpoznaje to po prostu po znikniëciu sekwencji
0xBEEF, które zostajâ zastâpione innymi wartoôciami,
przechowywanymi przez program na stosie. Wiëkszoôê programów
monitorujâcych stos dziaîa w taki sposób, ûe raz na jakiô czas
sprawdzajâ one bieûâcâ wartoôê wskaúnika poczâtku stosu
monitorowanego procesu: programy te nie sâ w stanie stwierdziê,
jakie byîo uûycie stosu pomiëdzy dwoma sprawdzeniami: moûe na
krótkâ chwilë zostaî uûyty bardzo duûy jego obszar? StackCheck
jest tej wady caîkowicie pozbawiony -- sprawdza po prostu
zawartoôê stosu, a nie bieûâcy wskaúnik jego poczâtku.
Chciaîbym w tym momencie ostrzec tych, którzy uwaûajâ, ûe ich
programom przepeînienie stosu nie grozi, gdyû nie uûywajâ
rekurencji (jak wiadomo, to ona jest gîównym "poûeraczem
stosów"), a poza tym kompilujâ programy z opcjami powodujâcymi
automatyczne sprawdzanie przepeînienia stosu przez program. Otóû
ja... teû tak uwaûaîem, wîaônie do czasu opisanego powyûej
incydentu: mój program (ciekawych informujë, ûe chodzi o
"Konwersjë") NIE uûywaî rekurencji i BYÎ skompilowany z
doîâczeniem procedur testujâcych stos. Procedury te sâ niestety
guzik warte, jeûeli przepeînienie stosu nastâpi w wywoîanej przez
program funkcji z jakiejô biblioteki -- te procedury sprawdzajâ
stos tylko na poczâtku kaûdej funkcji naleûâcej do programu. Stos
zaô "udaîo mi sië" przepeîniê dziëki uûyciu duûych zmiennych
lokalnych do obsîugi plików, no bo wypada daê 108 bajtów na nazwë
pliku, 255 bajtów na caîâ nazwë (ze ôcieûkâ dostëpu), 260 bajtów
to struktura FileInfoBlock (odkîadam jâ na stosie, uûywajâc
"__aligned"), kolejnych 400 bajtów przeznaczam na tekst bîëdu
("Bîâd odczytu pliku..." itd.), poza tym file-requestery teû do
oszczëdnych nie naleûâ (szczególnie w wypadku uûycia patternów:
systemowe funkcje MatchPattern[NoCase]() potrafiâ zjeôê nawet 1,5
KB stosu!). Kiedy sië to wszystko zsumuje, to okazuje sië, ûe o
przepeînienie stosu nietrudno. Najprostszym rozwiâzaniem problemu
jest definiowanie duûych tablic jako zmiennych klasy "static".
Powoduje to, co prawda, nieco wiëkszâ pamiëcioûernoôê programu,
ale kogo w dzisiejszych czasach obchodzâ te 2, 3 KB... Zwróê teû
uwagë na to, co powyûej napisaîem, ûe stos przepeîniaî sië pod OS
3.0, podczas gdy pod OS 2.04 nie (ôciôlej mówiâc: byî na granicy
przepeînienia). Funkcje róûnych wersji systemu operacyjnego mogâ
w róûnym stopniu "konsumowaê" stos, naleûy wiëc zawsze zapewniê
spory "margines bezpieczeïstwa".
Wszystkie opisane dotychczas programy mogîeô znaleúê na
stosunkowo îatwo dostëpnych dyskach Fisha. Z trzema pierwszymi
spoôród opisanych poniûej nie jest juû tak wesoîo: tych programów
firma Commodore nigdy u Fisha nie opublikowaîa. Ja znalazîem je
na dyskach "DevCon93" (materiaîy dla zarejestrowanych
developerów), które (przynajmniej w Krakowie) moûna zdobyê na
gieîdzie. Ich zdobycie tâ drogâ nie jest chyba w ôwietle znanej i
lubianej (?!) ustawy przestëpstwem, jako ûe w dokumentacji do
ûadnego z tych programów nie jest napisane, ûe nie wolno ich
rozpowszechniaê. Stosujâc zatem zasadë Zagîoby ("Bo widzisz,
moôci Kowalski, ûeby to byîo nie wolno, to byô miaî rozkaz nie
dawaê, a ûe nie masz rozkazu, wiëc dawaj" -- Henryk Sienkiewicz,
"Potop", tom I), moûna je kopiowaê i uûywaê ich.
Zgodnie ze swoim zwyczajem w artykule tym zamieszczaîem
dotychczas przykîadowe kody úródîowe napisane w jëzyku C. Co
bardziej obraûalscy maniacy asemblera pewnie z tego powodu
przestali juû ten artykuî czytaê (hej, jesteôcie tam?).
Niesîusznie, zupeînie niesîusznie: wszystkie opisane wczeôniej
debuggery przydajâ sië w takim samym stopniu przy programowaniu w
asemblerze, co w C. A moûe nawet w wiëkszym w C, jako ûe praktyka
wykazuje, ûe piszâc program w asemblerze popeînia sië wiëcej
bîëdów, niû piszâc go w jëzyku wyûszego poziomu.
No, ale wracajâc do tematu, "na osîodë" bëdzie coô specjalnie dla
maniaków asemblera: debugger, który programujâcym jedynie w
jëzykach wyûszego poziomu na nic sië nie przyda: SCRATCH. Scratch
zostaî napisany przez Billa Hawesa (skâd go znamy? Z ARexxa!).
Debugger ten jest klasycznym przykîadem kata torturujâcego
programiki. Jak wszystkim piszâcym w asemblerze powinno byê
wiadomo, funkcje biblioteczne majâ peîne prawo zmieniaê stan
rejestrów D0, D1, A0, i A1 -- z tego powodu rejestry te okreôla
sië mianem "scratch-registers". Wszystkie pozostaîe rejestry
adresowe i danych nie mogâ ulec zmianie (programy majâ
zagwarantowane przez twórców systemu prawo korzystania z tej
wîasnoôci). Potrzeba jednak sporo uwagi, aby program nie trzymaî
ûadnych danych na scratch-registers przy wywoîywaniu funkcji z
bibliotek. Popeîniony bîâd moûe sië np. nie ujawniaê na AMIDZE
autora programu (bo akurat przypadkowo funkcja z biblioteki nie
zmienia stanu jednego ze scratch-registers), a moûe powodowaê
nawet zawieszenie systemu na innej maszynie (ze wzglëdu na inny
system operacyjny, uruchomione inne programy itp). Scratch pomaga
wykryê takie bîëdnie dziaîajâce programy: funkcje z bibliotek
systemowych sâ "usprawniane" w taki sposób, ûe kaûda funkcja
wstawia w rejestry D1, A0 i A1 jakieô ômiecie -- bîëdnie napisane
programy zacznâ dziaîaê nieprawidîowo. Rejestr D0 nie jest,
niestety, zmieniany, jako ûe w wiëkszoôci funkcji systemu jest on
uûywany do przekazywania wyniku od funkcji. Scratch jest, rzecz
jasna, na tyle inteligentny, ûe pewne wyjâtkowe funkcje systemu,
dla których udokumentowany jest stan scratch-registers (np.
exec.library/ Forbid(), graphics.library/WaitBlit()), nie sâ
"usprawniane".
Znajdujâcy sië poniûej Przykîad 1. to prosty programik, napisany
w asemblerze, który bez Scratcha "dziaîa poprawnie" (przynajmniej
pod OS 3.0), natomiast ze Scratchem sië zawiesza (przy
uruchomieniu z Workbencha). Ja kompilowaîem go pakietem SAS/C 6.3
-- asembler i debugger sâ doôê "czepialskie", stâd uûycie
pewnych, normalnie niepotrzebnych, konstrukcji ("section", "END"
itp.).
Bîâd w tym programie chyba doôê îatwo zauwaûyê. Nie jest brane
pod uwagë, ûe po wykonaniu WaitPort() rejestr A0 moûe zawieraê
coô innego niû MsgPort naszego procesu -- przypadkowo jest to
prawda w obecnych systemach, dziëki czemu funkcja GetMsg()
dostaje taki parametr, jaki dostawaê powinna, ale jak bëdzie w
przyszîoôci (albo po uruchomieniu jakiegoô "patchera")?
Zazwyczaj najbardziej zawodnâ czëôciâ programu jest obsîuga
sytuacji wyjâtkowych, w szczególnoôci braku pamiëci. Powód
zawodnoôci tych procedur jest chyba doôê oczywisty:
przetestowanie ich jest doôê skomplikowane. Programiôci majâ po
prostu zwykle duûâ iloôê pamiëci i na ich Amigach w praktyce ich
programom pamiëci nigdy nie brakuje. Bîëdy ujawniajâ sië dopiero
u mniej zasobnych w pamiëê uûytkowników, którzy odchodzâ od
zmysîów, zastanawiajâc sië, jak moûna byîo opublikowaê taki
zawieszajâcy sië "na kaûdym kroku" program. Czësto zniechëcajâ
sië nie tylko do programu, ale i do komputera. Znam takie
przypadki: sprzedajâ Amigë, kupujâ peceta i juû tych problemów
nie majâ. Pytajâ: dlaczego tak nie moûe byê na Amidze? Nie
pomyôli jeden z drugim, ûe za peceta daî 30 baniek (bo trudno
kupiê dziô coô taïszego), a Amiga kosztowaîa 10, ûe na AMIDZE
miaî 1, 2 MB RAM, a na pececie ma 4 MB, a pod Windows jeszcze
dodatkowe 8 MB (pamiëê wirtualna), wiëc jak na pececie ma
brakowaê pamiëci?
No, wróêmy jednak do naszej szarej rzeczywistoôci: spójrz na
Przykîad 2. Jest to prosty programik, otwierajâcy równie proste
okienko. Poniewaû nie chcemy, aby choê jeden amigowiec "zmieniî
wiarë" po zawieszeniu sië naszego programu, wiëc mamy zamiar
sprawdziê, czy program zachowa sië poprawnie w sytuacji, gdy nie
starczy pamiëci na otwarcie okna.
Trzeba wiëc uruchomiê kupë róûnych pamiëcioûernych programów, w
stylu Opusów czy jakichô graficznych Reali, ADPro itp. Trzeba
przy tym byê subtelnym i pozostawiê wystarczajâco duûo pamiëci,
aby program, który chcemy przetestowaê, zdoîaî sië wczytaê, ale
aby okna juû otworzyê nie mógî. Wymaga to szeregu prób, jest
trudne i czasochîonne.
Pewnym uîatwieniem moûe byê uûycie specjalnego programu,
zajmujâcego sië "poûeraniem" ôciôle okreôlonej iloôci pamiëci.
Takich programów jest sporo, ja uûywam zwykle "Zjadacza",
autorstwa mojego kumpla Darka Ûbika. Ale i to nie jest tym, co
"tygrysy lubiâ najbardziej", gdyû w bardziej skomplikowanych
sytuacjach, np. w wypadku szeregu nastëpujâcych tuû po sobie
ûâdaï przydzielenia pamiëci trudno jest wychwyciê to konkretne,
które nas w danej chwili interesuje -- trzeba modyfikowaê kod,
wstawiajâc pomocnicze printf()y i Delay()'e, jednym sîowem,
koszmar.
Pierwszym powaûnym uîatwieniem, z którego moûna skorzystaê, jest
uûycie source-level debuggera (wspomniaîem o tego typu programach
w poprzedniej czëôci). Skompilujmy wiëc Przykîad 2., doîâczajâc
informacje dla debuggera (np. "DEBUG=SYMBOLFLUSH" w SAS/C) i
uruchommy debugger (np. "cpr przykîad2"). Otwiera sië ekran jak
na rysunku 1. Ustawiamy breakpoint w linii 29. (tam, gdzie
znajduje sië OpenWindow()) i puszczamy program "na ûywioî" (np.
"go"). Program zatrzymuje sië w linii 29. W tym momencie moûemy
"zeûreê" pamiëê w opisany powyûej sposób i sprawdziê, czy program
zadziaîa poprawnie.
Jest to sposób niezîy, ma jednak pewnâ wadë: "zbyt dobrze"
symuluje rzeczywistoôê. Po prostu brak pamiëci staje sië
globalnym problemem wszystkich procesów, a my chcemy testowaê
tylko nasz program, a nie sprawdzaê, jak zachowa sië w takiej
sytuacji system czy np. CED. To jednak byîoby do wytrzymania,
gdyby nie fakt, ûe pamiëci zacznie brakowaê równieû debuggerowi,
który przecieû ma sprawowaê peînâ kontrolë nad naszym programem:
czort wie, co moûe sië staê, gdy zabraknie mu pamiëci na jakieô
waûne rzeczy? Tak wiëc przydaîby sië program "selektywnie
wyîâczajâcy" pamiëê, tzn. powodujâcy, ûe jeden proces (ten
testowany) pamiëci nie dostaje, a drugi (debugger) nie ma z tym
problemów.
Takim programem jest MEMORATION autorstwa Billa Hewsa (skâd go
znamy? Ze Scratcha!). Program jest doôê rozbudowany, moûna mu
podaê masë parametrów przy uruchomieniu, moim zdaniem jednak
wiëkszoôê z nich to po prostu maîo przydatne w praktyce
"wodotryski" (no bo na co, w gruncie rzeczy, moûe sië przydaê
opcja powodujâca, ûe co n-te ûâdanie przydzielenia pamiëci
zakoïczy sië niepowodzeniem?). Uwaûam, ûe najwygodniej korzysta
sië z tego programu wîaônie w poîâczeniu z source-level
debuggerem.
Tak wiëc w opisywanej sytuacji (kiedy nasz Przykîad 2. ma wîaônie
wykonaê OpenWindow()) naleûy uruchomiê Memoration, podajâc nazwë
procesu, który nie ma otrzymywaê pamiëci:
<l>memoration TASK=przykîad2
<txt> Powinien ukazaê sië komunikat w tym stylu:
<l>Rationing task 33E360 "przykîad2" addresses 0 to FFFFFFFF sizes 0 to 2000000
<txt> Jeûeli nic nie jest "podpiëte" do serial portu, to uruchom
Sushi, gdyû Memoration wysyîa tam pewne komunikaty. Teraz moûna
juû wykonaê linië 29. (np. naciskajâc Return w CPR). Zgodnie z
naszym oczekiwaniem, okna nie udaîo sië otworzyê: program skacze
do linii 31. -- procedury obsîugi bîëdu. Sushi natomiast wypisuje
w okienku komunikat mniej wiëcej tej treôci:
<l>Rationed! Task 33E360 "przykîad2" from F81D00 denied AllocMem(238,10001)
<txt> Znaczenie komunikatu jest chyba oczywiste: wstrzymano
przydziaî 238 bajtów pamiëci o atrybutach MEMF_PUBLIC (bit 0) i
MEMF_CLEAR (bit 16 -- patrz definicje w "exec/memory.h"). Pamiëci
zaûyczyî sobie kod znajdujâcy sië pod adresem 0xF81D00, a wiëc w
pamiëci ROM (nic dziwnego -- "intuition.library" znajduje sië w
ROM-ie, a OpenWindow() to przecieû funkcja z tej biblioteki).
Teraz moûna juû bez problemu sprawdziê, czy procedura obsîugi
bîëdów wykonuje sië zgodnie z naszymi oczekiwaniami, po prostu
pozwalajâc testowanemu programowi wykonywaê sië dalej. Naleûy
przy tym pamiëtaê, ûe Memoration ciâgle dziaîa i nie pozwoli na
ûadne przydziaîy pamiëci: jeûeli jest nam to nie na rëkë, to
moûna ten program wyîâczyê:
<l>memoration OFF
<txt> Ostatniâ "grupâ" debuggerów, którâ chciaîbym w tym artykule
omówiê, sâ róûnorakie monitory. Nie chodzi mi przy tym o uûywane
przez róûnych kodero-hackerów programy umoûliwiajâce
disasemblacjë pamiëci w celu odkrycia cudzych sekretów, tylko po
prostu o programy, które na bieûâco informujâ o pewnych
operacjach, stanie systemu itp.
Takim programem jest choêby DEVMON. Umoûliwia on monitorowanie
aktywnoôci poszczególnych urzâdzeï (device'ów). Robi to
wyrzucajâc na port Serial doôê szczegóîowy komunikat za kaûdym
razem, gdy jakiô program stara sië wejôê z danym urzâdzeniem w
kontakt -- wypisuje nazwë programu i poszczególne pola
IORequesta. Chcâc np. upewniê sië, czy nasz program po uûyciu w
nim opcji Drukuj nie robi jakichô dziwnych rzeczy, wpisujemy:
<l>devmon printer.device 0 full remote hex
<txt> Pierwszy parametr to nazwa urzâdzenia, drugi to numer
unita, trzeci (full) powoduje produkowanie peîniejszych
informacji, "remote" powoduje wysyîanie informacji na port
Serial, zamiast zapamiëtywania ich w buforze i póúniejszego
zapisu do pliku (opcja warta uûywania -- dziëki niej otrzymujemy
informacje na bieûâco), "hex" zaô powoduje wypisywanie danych
liczbowych w kodzie szesnastkowym, a nie dziesiëtnym (teû warto
uûywaê -- co mówi np. adres pamiëci w kodzie dziesiëtnym?).
Analizujâc szczegóîowo raport, uzyskany z DevMona moûna wykryê
wiele nieprawidîowoôci, jak np. dwukrotne uûycie tego samego
IORequesta w tym samym czasie, brak WaitIO() po AbortIO() itp.
Innymi programami monitorujâcymi sâ choêby DOSTrace czy IconTrace
autorstwa Petera Stuera. Ich najnowsze znane mi wersje
(odpowiednio: 2.20 i 2.02) znajdujâ sië na 25. dysku Shareware
Magazynu AMIGA. Sâ one przydatne nie tylko do sprawdzania, z
powodu braku jakich plików cudze programy nie chcâ sië
uruchamiaê. Np. uwaûna analiza wyników uzyskanych przez DOSTrace
umoûliwia czësto zoptymalizowanie programu, np. przez usuniëcie
zbëdnych CurrentDir()ów, Lock()ów, CreateDir()ów itp. Umoûliwia
takûe wykrycie pewnych bîëdów, jak np. niezamykania (Close())
otwartych (Open()) plików itp.
Ostatnim programem-monitorem wartym wspomnienia jest Amiga Real
Time Monitor -- ARTM. Podstawowym zadaniem programu jest
wyôwietlanie najwaûniejszych list systemowych, takich jak:
procesy, biblioteki, okna, pamiëê itp. Umoûliwia to np.
stwierdzenie, czy nasz program, nad którym straciliômy kontrolë
(nie reaguje na naciskanie gadûetów), wszedî w nieskoïczonâ pëtlë
(ARTM wyôwietli jego "State" jako "Ready"), czy teû czeka na coô
(wtedy jego stanem bëdzie "Wait"). Umoûliwia on takûe pewne,
niezupeînie legalne, ale w praktyce doôê przydatne, czynnoôci,
jak np. zmiana priorytetu innych procesów, ich zatrzymanie czy
usuniëcie, zamykanie okien i wiele, wiele innych.
Na koniec maîy apel:
Odpluskwiaczy MOÛNA uûywaê w charakterze straûy poûarnej, tzn.
wîâczaê je dopiero wtedy, gdy uûytkownicy zgîoszâ bîëdy w naszych
programach. Gorâco jednak polecam inne podejôcie, tzn.
profilaktykë. Niektórzy np. uruchamiajâ te najwaûniejsze
debuggery -- Enforcera i Mungwalla -- na staîe, podczas startu
systemu. Jest to dziaîanie mâdre, bo w ten sposób niewiele bîëdów
im umknie. Ma to równieû wady, z których najwaûniejszymi sâ
dostrzegalne spowolnienie pracy systemu i wiëksze zapotrzebowanie
aplikacji na pamiëê. Jeûeli wiëc nie uruchamiasz tych debuggerów
na staîe, to przynajmniej niech Ci "wejdzie w krew", ûe dokîadne
przetestowanie programu z uûyciem debuggerów powinno stanowiê
integralnâ czëôê tworzenia programu. Lepszy jest program z
mniejszâ liczbâ opcji, ale dziaîajâcy pewnie, niû rozbudowana
kolubryna "wieszajâca sië" na kaûdym kroku. Tak wiëc KAÛDY
program testuj Mungwallem, StackCheckiem i, jeûeli masz takâ
moûliwoôê, Enforcerem, a i uûycie Memoration nie zaszkodzi (choê
jest doôê czasochîonne). Jeûeli program jest choê w czëôci
napisany w asemblerze, nie zapomnij o Scratchu. Jeûeli program
uûywa device'owego I/O, uûyj DevMona.
Uûytkownicy Twoich programów podziëkujâ Ci za to.