Co to jest debugger (cz. 2.) ---------------------------- ODPLUSKWIACZE 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. Kamil Iskra 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î: 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 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: memoration TASK=przykîad2 Powinien ukazaê sië komunikat w tym stylu: Rationing task 33E360 "przykîad2" addresses 0 to FFFFFFFF sizes 0 to 2000000 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: Rationed! Task 33E360 "przykîad2" from F81D00 denied AllocMem(238,10001) 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ê: memoration OFF 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: devmon printer.device 0 full remote hex 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.