Univerzita Karlova v Praze Matematicko-fyzikální fakulta BAKALÁŘSKÁ PRÁCE Jiří Václavík Ladící prostředí pro Windows Katedra softwarového inženýrství Vedoucí bakalářské práce: RNDr. Jakub Yaghob, Ph.D. Studijní program: Informatika, programování 2008
Děkuji panu RNDr. Jakubu Yaghobovi, Ph.D. za odborné vedení mé práce, za rady a za čas, který mi v průběhu jejího vypracovávání věnoval. Také bych chtěl poděkovat svému bratru Janovi za průběžné čtení práce a za jeho připomínky k nedostatkům. Dále děkuji všem svým blízkým za jejich podporu při studiu. Prohlašuji, že jsem svou bakalářskou práci napsal samostatně a výhradně s použitím citovaných pramenů. Souhlasím se zapůjčováním práce a jejím zveřejňováním. V Praze dne 30. května 2008 Jiří Václavík 2
Obsah 1 Úvod 6 2 Analýza 8 2.1 Základ debuggeru............................. 8 2.2 Překlad strojového kódu......................... 9 2.3 Vlastnosti architektury IA-32...................... 10 2.3.1 Krokování............................. 11 2.3.2 Breakpointy............................ 11 2.3.3 Krokování typu step over..................... 12 2.3.4 Problém s identifikací počátečních adres instrukcí....... 13 2.4 Ladicí informace............................. 13 3 Implementace 15 3.1 Jednovláknový vs. vícevláknový přístup................. 15 3.2 Struktura projektu............................ 16 3.2.1 Modul dbg1.cpp.......................... 16 3.2.2 Modul dbg jadro.cpp....................... 17 3.2.3 Modul disasm.cpp......................... 19 3.2.4 Modul vyrazy.cpp......................... 20 3.3 Přidružené projekty............................ 21 4 Srovnání 22 4.1 Borland Turbo C++ 2006........................ 23 4.2 Microsoft Visual Studio 2005....................... 24 4.3 OllyDbg.................................. 27 3
5 Závěr 29 Reference 31 A Obsah přiloženého CD 33 B Uživatelská příručka 34 B.1 Instalační příručka............................ 34 B.1.1 Prerekvizity............................ 34 B.1.2 Instalace.............................. 34 B.2 Základní funkce.............................. 34 B.3 Hlavní okno................................ 35 B.4 Podrobný návod jak ladit proces..................... 36 B.4.1 Zahájení ladění.......................... 36 B.4.2 Zastavený proces......................... 37 B.4.3 Kód................................ 38 B.4.4 Data................................ 38 B.4.5 Breakpointy............................ 39 B.4.6 Ukončení ladění.......................... 40 B.4.7 Moduly.............................. 40 B.5 Výrazy................................... 41 C Popis vstupní instrukční sady pro generátor 43 4
Název práce: Ladící prostředí pro Windows Autor: Jiří Václavík Katedra (ústav): Katedra softwarového inženýrství Vedoucí bakalářské práce: RNDr. Jakub Yaghob, Ph.D. e-mail vedoucího: Jakub.Yaghob@mff.cuni.cz Abstrakt: Tato práce se zabývá analýzou dostupných ladicích prostředků operačního systému Microsoft Windows XP na platformě IA-32 a následnou implementací aplikačního debuggeru. Výsledná implementace (projekt RazeDebugger) podporuje ladění více vláken, instrukce SIMD a zobrazení dostupných ladicích informací ze zdrojového kódu. Práce také zahrnuje srovnání projektu RazeDebugger a dalších existujících debuggerů. Srovnání se zaměřuje na tato hlediska debuggerů: dostupné funkce, uživatelskou přívětivost, zatížení procesoru, výkonnost, stabilitu a také reakce na zvláštní chování laděného programu. Klíčová slova: aplikační debugger, Windows, IA-32 Title: Debugger for Windows Author: Jiří Václavík Department: Department of Software Engineering Supervisor: RNDr. Jakub Yaghob, Ph.D. Supervisor s e-mail address: Jakub.Yaghob@mff.cuni.cz Abstract: This work deals with analysis of available debugging means of the operating system Microsoft Windows XP on the IA-32 platform and ensuing implementation of an application debugger. The resulting implementation (the project RazeDebugger) supports multithreaded debugging, SIMD instructions, and viewing of accessible debugging information from source code. The work also includes comparison of the project RazeDebugger and other existing debuggers. The comparison aims at these aspects of debuggers: available functions, user friendliness, CPU usage, performance, stability, and also reaction to special behavior of the debuggee. Keywords: application debugger, Windows, IA-32 5
Kapitola 1 Úvod Počítačový program je složitý a zároveň fascinující objekt. Jednou z možností jeho zkoumání je prosté otevření zdrojových souborů v příslušném editoru a postupné procházení kódu. Čím je však projekt rozsáhlejší, tím obtížnější je se v něm vyznat. Navíc se často programy distribuují pouze ve formě spustitelných souborů bez zdrojových kódů, což znemožňuje použití této metody. Jinou možností je použít pro tento účel speciálně vytvořený druh softwaru debugger. Tento speciální nástroj umožňuje detailně analyzovat laděný program bez nutnosti mít k němu doplňující ladicí informace a zdrojové kódy. Na druhé straně, pokud jsou tyto doplňky k dispozici, debugger je umí využít a zpřehlednit tak ladění. Schopnosti debuggeru nekončí u pouhé analýzy laděného programu. Stav laděného programu jde také měnit to představuje nezbytnou funkci při crackingu (snaze o prolomení ochran zabudovaných do programu) a hledání chyb (angl. debugging). Ladění programu bez dostupného zdrojového kódu probíhá v jazyku nejnižší úrovně (assembleru), který přímo popisuje nativní instrukce procesoru. Na původním programovacím jazyku, v kterém byl program napsán, tedy v tomto ohledu nezáleží. Debugger tak představuje univerzální nástroj pro ladění všech spustitelných programů, které vznikly v různých vývojových prostředích. Cílem této práce je analyzovat dostupné ladicí prostředky pro 32bitový operační systém Microsoft Windows XP na platformě IA-32 a na základě této analýzy vytvořit aplikační debugger pracující v uživatelském režimu (jinou kategorii zastupuje kernelový debugger pracující v privilegovaném režimu). Debugger bude sloužit k ladění 6
KAPITOLA 1. ÚVOD 7 nativních 32bitových procesů, které běží na stejném počítači a operačním systému jako debugger. Dále bude podporovat ladění vícevláknových procesů a bude zvládat některá pozdější rozšíření původní instrukční sady o instrukce typu SIMD (Simple Instruction, Multiple Data), a to MMX, 3DNow!, SSE a SSE2. Do kódu půjdou vkládat podmíněné a nepodmíněné ladicí body zastavení (breakpointy). Debugger bude podporovat některé rysy dostupných ladicích informací (spojitost kódu v assembleru s řádkami v původním zdrojovém kódu a umístění symbolů v kódu včetně jmen exportovaných funkcí ze zavedených modulů). Debugger bude ovládán pomocí grafického uživatelského rozhraní. Kapitola 2 analyzuje ladicí prostředky, které nabízí hardware a operační systém. Vedle toho se také zabývá problémem překladu strojového kódu a získáváním ladicích informací. Kapitola 3 popisuje software patřící k práci z pohledu programátora včetně diskuze důležitých rozhodnutí v průběhu implementace. Kapitola 4 srovnává výslednou implementaci s několika dalšími známými debuggery. V příloze práce lze nalézt obsah přiloženého CD, uživatelskou příručku a další doplňující informace.
Kapitola 2 Analýza 2.1 Základ debuggeru Vytvoření základní programové kostry aplikačního debuggeru je snadno realizovatelné s pomocí několika volání funkcí Windows API. Nejprve je třeba identifikovat proces, který bude debuggerem laděn. V úvahu přichází nějaký už běžící proces, resp. pro tento účel debuggerem nově vytvořený proces (spuštěný se speciálním příznakem). To lze zařídit voláním funkce DebugActiveProcess(id_procesu), resp. CreateProcess(...), kde se v parametru dwcreationflags nastaví příznak DEBUG_ON- LY_THIS_PROCESS, který zajistí, aby se volající proces stal debuggerem nově vzniklého procesu. Připojením k běžícímu procesu se zabývá [2]. Od debuggeru se následně očekává, že bude přijímat a zpracovávat ladicí události, ke kterým dojde v laděném procesu. Ladicí událost se přijímá voláním funkce WaitFor- DebugEvent(...). Informace o události funkce uloží do struktury typu DEBUG_EVENT. Typy ladicích událostí shrnuje tabulka 2.1. Laděný proces, v kterém nastala nějaká ladicí událost, neběží (má zablokovaná všechna vlákna). Po každém volání funkce WaitForDebugEvent(...) je třeba zavolat funkci ContinueDebugEvent(...), která zařídí, aby proces pokračoval v běhu. Ladění normálně končí po obdržení události typu EXIT_PROCESS_DEBUG_EVENT. Ta nastává jak při normálním ukončení procesu, tak i při jeho předčasném ukončení debuggerem (funkcí TerminateProcess(...)). Laděný proces je možné odpojit od debuggeru funkcí DebugActiveProcessStop- 8
KAPITOLA 2. ANALÝZA 9 Tabulka 2.1: Ladicí události (pro více informací viz [4]) Typ události Popis CREATE_PROCESS_DEBUG_EVENT Přináší informace o laděném procesu. Nastává hned po připojení debuggeru. CREATE_THREAD_DEBUG_EVENT Vytvoření nového vlákna v laděném procesu. EXCEPTION_DEBUG_EVENT Laděný proces způsobil výskyt výjimky. EXIT_PROCESS_DEBUG_EVENT Ukončení laděného procesu. EXIT_THREAD_DEBUG_EVENT Ukončení vlákna. LOAD_DLL_DEBUG_EVENT Nahrání dynamické knihovny. OUTPUT_DEBUG_STRING_EVENT Ladicí řetězec. RIP_EVENT Při ladění nastala systémová chyba. UNLOAD_DLL_DEBUG_EVENT Uvolnění nahrané dynamické knihovny. (id_procesu). V takovém případě však debugger událost typu EXIT_PROCESS_- DEBUG_EVENT následně neobdrží. Obsah paměti laděného procesu operační systém zpřístupňuje funkcemi ReadProcessMemory(...) a WriteProcessMemory(...). Přístup ke kontextu vláken zajišt ují funkce GetThreadContext(...) a SetThreadContext(...). Kontext zahrnuje veškeré registry procesoru. Popis hlavní smyčky debuggeru včetně vzorového příkladu lze nalézt v [3]. 2.2 Překlad strojového kódu V předchozí části většinu práce obstarávaly různé funkce Windows API. Pro překlad strojového kódu (instrukcí zakódovaných do podoby srozumitelné procesoru) do assembleru (instrukcí v čitelné formě pro člověka) však žádná funkce Windows API neexistuje. Proto nezbývá nic jiného než funkci vyrobit vlastními prostředky. Její ruční psaní je však více než nepraktické. Jako lepší řešení se ukazuje naprogramování jejího generátoru. Použití generátoru umožňuje snadnou a bezproblémovou modifikaci podporované instrukční sady kdykoliv v budoucnu. Stačí upravit vstup pro generátor, nechat ho jím zpracovat a výsledek vložit na příslušné místo do zdrojového souboru. Ve srovnání s tímto postupem by ruční upravování bylo komplikované. Další argument pro použití generátoru souvisí s výskytem chyb, a to jak v samotné vstupní instrukční sadě, tak i v jejím následném převádění do podoby funkce.
KAPITOLA 2. ANALÝZA 10 Obrázek 2.1: Formát instrukce architektur IA-32 a Intel 64 (převzato z [1]) Instrukční sada čítá stovky záznamů, a proto je pravděpodobné, že při jejich ručním přepisu dojde k nějaké chybě. Na druhé straně ani samotná referenční instrukční sada nemusí být úplně bez chyb (např. různé mnemoniky mohou mít stejný operační kód). Generátor lze uzpůsobit tak, aby tyto chyby pomáhal eliminovat. Vygenerovaný kód může např. jednotlivé bajty strojového kódu rozpoznávat pomocí vnořených programových konstrukcí switch(...){...}, které zajistí vznik chyby při překladu v případě nekonzistence instrukční sady. Pro generátor je důležitá podoba vstupní instrukční sady. Měla by obsahovat pouze informace podstatné pro generátor tedy mnemonickou zkratku instrukce, její operandy a operační kód (ne žádné další popisy). Manuál od výrobce procesoru se pro tyto účely příliš nehodí, protože obsahuje mnoho informací navíc. Vhodnou sadu lze však najít např. v [11]. Popis této sady se nalézá v příloze. O formátu instrukcí a přesných pravidlech jejich kódování se detailně pojednává v [1]. Každá instrukce se, stručně řečeno, skládá z volitelných prefixů, povinného operačního kódu a případných dalších položek v závislosti na druhu instrukce (viz obrázek 2.1). 2.3 Vlastnosti architektury IA-32 Architektura IA-32 nabízí snadný způsob realizace základních funkcí nezbytných při ladění (krokování a umist ování ladicích bodů breakpointů).
KAPITOLA 2. ANALÝZA 11 2.3.1 Krokování Podpora krokování je zabudovaná přímo v procesoru, a to přes speciální příznak trap flag. Ten se nachází v registru příznaků procesoru, tj. v registru efl(ags). Pokud je příznak nastavený, dojde po vykonání další instrukce k vyvolání výjimky typu STATUS_SINGLE_STEP a vynulování zmíněného příznaku. Tuto výjimku (ladicí událost) následně detekuje debugger a zajistí vykonání příslušných akcí. Pro pokračování v krokování je potřeba příznak vždy znovu nastavit. Takovéto krokování, které sleduje tok instrukcí za sebou přesně tak, jak je vykonává procesor, se v angličtině nazývá trace into, protože se při něm zabíhá do procedur a funkcí volaných instrukcí call. Jiná varianta krokování nazývaná step over naopak tato volání přeskakuje. Jejich realizace však není efektivně možná pouze nastavováním příznaku trap flag. Navíc totiž vyžaduje nasazení breakpointů. 2.3.2 Breakpointy Architektura IA-32 nabízí dva druhy breakpointů hardwarové a softwarové. Hardwarové se nastavují přímo ve speciálních registrech (debug registers) procesoru, zatímco softwarové se přímo umist ují do kódu (dochází k přepisu původního kódu). I když hardwarové breakpointy svými vlastnostmi vynikají nad softwarovými (minimální režie, možnost odlišného nastavení pro různá vlákna, breakpointy nejen před vykonáním určité instrukce, ale i na přístup do paměti), používají se mnohem méně než softwarové breakpointy. Důvodem je omezený počet současně nastavených hardwarových breakpointů pro jedno vlákno, který odpovídá počtu vyhrazených registrů pro uložení adres breakpointů. Tyto registry jsou však pouze čtyři. Naopak počet softwarových breakpointů není omezený žádným počtem registrů procesoru, protože tyto breakpointy spravuje software (debugger). Umístění softwarového breakpointu na určitou instrukci znamená její přepsání (se zapamatováním původního kódu) zvláštní instrukcí int3. Délka této instrukce je jeden bajt, který má ve strojovém kódu hodnotu zapsanou v hexadecimální soustavě (CC) 16. Stačí tedy přepsat a pamatovat si jen jeden bajt. Jakmile procesor vykoná instrukci int3, dojde k vyvolání výjimky typu STATUS_- BREAKPOINT. Obsluha této ladicí událost zahrnuje obnovení původní instrukce, vrácení
KAPITOLA 2. ANALÝZA 12 bodu provádění (v registru eip) na začátek obnovené instrukce, provedení jednoho kroku typu trace into a následné opětovné nastavení breakpointu. Na počítači s více procesory by teoreticky mohlo dojít k situaci, kdy při výskytu softwarového breakpointu ve fázi provádění jednoho kroku typu trace into ještě před opětovným nastavením breakpointu by se jiné vlákno na daném breakpointu nezastavilo, protože byl dočasně nahrazen původní instrukcí. Řešení situace spočívá v zablokování, resp. odblokování všech vláken laděného procesu kromě toho, u kterého došlo k aktivaci breakpointu, a to před fází provedení kroku typu trace into, resp. po ní. Vlákno lze zablokovat, resp. odblokovat voláním funkce SuspendThread(...), resp. ResumeThread(...). 2.3.3 Krokování typu step over Se schopností nastavovat breakpointy nyní nic nebrání zvládnutí druhé varianty krokování (typu step over). V případě, kdy bod provádění míří na instrukci call, se umístí breakpoint na následující instrukci a vyčká se, až se na ní proces zastaví. V případě ostatních instrukcí lze použít stejný mechanizmus jako u krokování typu trace into, tj. nastavení příznaku trap flag. Na breakpoint umístěný na následující instrukci za instrukci call je však třeba klást zvláštní požadavek. Breakpoint by se totiž měl vztahovat pouze k aktuálně krokovanému vláknu. Obyčejný softwarový breakpoint tedy použít nejde, protože ten se vždy vztahuje k všem vláknům. Jedním východiskem by bylo modifikovat režii okolo softwarového breakpointu tak, aby podporovala aktivaci pouze z některého vlákna. To teoreticky bude fungovat, ale v praxi může dojít k určité nestabilitě, protože ladicí událost bude stále nastávat i pro další vlákna (jen budou ignorována). Jako lepší řešení se ukazuje nasazení hardwarového breakpointu (na každé vlákno bude třeba současně maximálně jen jeden hardwarový breakpoint). Podrobnější popis registrů souvisejících s hardwarovými breakpointy lze najít v [12].
KAPITOLA 2. ANALÝZA 13 2.3.4 Problém s identifikací počátečních adres instrukcí Strojový kód pro architekturu IA-32 se obecně skládá z instrukcí různé délky (od jednoho bajtu až do teoretických sedmnácti bajtů). Počáteční adresa instrukcí však není nijak zvlášt vyznačená. Ale pro správné interpretování strojového kódu je nezbytné znát pozici počátku první překládané instrukce (počátky následujících instrukcí už lze dopočítat). Při krokování tato skutečnost nepředstavuje problém, protože aktuální bod provádění (registr eip) ukazuje právě na počáteční adresu další bezprostředně vykonávané instrukce. Problém se objevuje až v okamžiku, kdy je potřeba překládat instrukce před bodem provádění (na nižších adresách) nebo překládat kód na obecné adrese. Některé debuggery tento problém řeší tak, že před zahájením ladění přeloží celý laděný modul (se znalostí startovací adresy nebo počátečních adres exportovaných funkcí), a tím získají přehled o počátečních adresách instrukcí. Metodu je však vhodné rozšířit o dynamickou aktualizaci počátku instrukcí v průběhu ladění (případně používat výhradně dynamickou metodu). Startovací adresa nebo adresy exportovaných funkcí totiž nemusí poskytovat kompletní údaje o rozložení instrukcí. V úvahu je také nutné vzít vliv případného sebemodifikujícího kódu, který dokáže zcela zpřeházet původní představu o členění instrukcí ve strojovém kódu. 2.4 Ladicí informace Ladicí informace, byt nejsou nezbytné, významně zlepšují orientaci při ladění procesu. Jejich umístění se různí v závislosti na použitém vývojovém prostředí. Některá prostředí ladicí informace ukládají přímo do spustitelného souboru (s příponou.exe), ale jiná je oddělují do zvláštních souborů (např. Microsoft Visual Studio do souboru s příponou.pdb). Za ladicí informace lze považovat i seznam exportovaných symbolů dynamické knihovny (DLL), který se nachází přímo v souboru s knihovnou. Přístup k ladicím informacím zajišt ují funkce Windows API z knihovny dbghelp-.dll (využívá soubory s příponou.pdb). Operační systém sice standardně obsahuje tuto knihovnu, ale většinou jde o neaktuální verzi. Zastaralá verze knihovny však neumí korektně načíst dnešní formát souborů s příponou.pdb. Proto by se vždy
KAPITOLA 2. ANALÝZA 14 měla používat nejnovější verzi této knihovny (tu lze získat např. z [10]). Prvním krokem je inicializace správce symbolů, a to voláním funkce SymInitialize(...). Chování správce symbolů je možné volitelně přizpůsobit funkcí SymSetOptions(...), to je však třeba provést před samotnou inicializací správce. (Správce lze např. přimět k odložení nahrání symbolů až do okamžiku, kdy budou skutečně potřeba, k,,oddekorování jmen symbolů či k nahrání informací o souvislosti strojového kódu s řádkami ve zdrojových souborech.) O inicializaci správce symbolu podrobněji pojednává [5]. Seznam všech modulů (dynamických knihoven), které laděný proces nahrál do svého adresového prostoru, je přístupný přes volání funkce SymEnumerateModules- 64(...) (funkce pro každý modul zpětně volá přes parametr předanou callback funkci; vzorový příklad viz [6]). Seznam všech exportovaných symbolů daného modulu obdobným způsobem zprostředkovává funkce SymEnumSymbols(...) (vzorový příklad viz [7]). Určitou potíž však způsobuje fakt, že v okamžiku výskytu ladicí události typu LOAD_DLL_DEBUG_EVENT správce symbolů ještě daný modul neregistruje (událost zřejmě nastává ještě před samotným nahráním modulu). Jedním možným řešením problému je ukládat bázové adresy modulů do fronty a po určitém čase z ní postupně odebírat moduly, u kterých už bylo dokončeno nahrávání. To lze testovat např. funkcí GetModuleBaseName(...) z knihovny psapi.dll. V případě úspěchu funkce navíc vrací jméno modulu. Seznam zdrojových souborů, které patří k určitému modulu, lze zjistit podobným způsobem jako seznam modulů či symbolů jen s tím rozdílem, že se použije funkce SymEnumSourceFiles(...) (pro více informací o funkci viz [8]). Funkce SymGetFileLineOffsets64(...) (viz [9]) umožňuje pro každý zdrojový soubor dát do souvislosti jeho řádky s adresami ve strojovém kódu. Nakonec je potřeba uvolnit zdroje, které si správce symbolů naalokoval, a to voláním funkce SymCleanup(...).
Kapitola 3 Implementace Projekt RazeDebugger vznikl ve vývojovém prostředí Borland Turbo C++ 2006 Explorer (varianta produktu Borland C++ Builder; k dispozici zdarma z [14]). 3.1 Jednovláknový vs. vícevláknový přístup Program byl po úvaze nakonec koncipován jako jednovláknový. Použití více vláken by program totiž neúměrně komplikovalo vzhledem k nepatrnému zvýšení výkonu, které by vícevláknový přístup přinesl. V programu je použita technika, kterou se při vybírání ladicích zpráv od systému dosahuje velice podobného výkonu jako při nasazení více vláken. Tato technika je založená na myšlence shlukování ladicích zpráv jakmile přijde jedna zpráva, je velice pravděpodobné, že bud do několika milisekund dorazí další zprávy, nebo po delší dobu nedorazí žádná zpráva (stovky milisekund až sekundy). Po každé přijaté zprávě se tedy ještě čeká maximálně několik dalších desítek milisekund na případnou následující zprávu. V případě neúspěchu se přechází na periodickou kontrolu nových zpráv bez čekání. Pozorovatelný rozdíl mezi oběma přístupy by nastal v extrémní situaci, kdy RazeDebugger neustále zpracovává ladicí události s tím, že laděný proces se kvůli nim nezastavuje, ale hned pokračuje v běhu. Tato situace odpovídá dlouhé smyčce (trvání v řádu sekund), v které je nastavený breakpoint s podmínkou, která dlouho (případně po celou dobu) není splněná. V takovém případě RazeDebugger, který běží jen na jednom vláknu, vykazuje sníženou interaktivitu (ale pořád jde rozumně ovládat). Otázkou je, jestli uživatel opravdu potřebuje intenzivně pracovat s RazeDebuggerem, když se laděný proces točí ve smyčce, kam uživatel nastavil breakpoint s podmínkou. 15
KAPITOLA 3. IMPLEMENTACE 16 (Proces je možné zastavit a interaktivita RazeDebuggeru je opět normální.) Jednovláknový přístup má tedy výkon velice blízký vícevláknovému (až na zmíněnou extrémní situaci). Po zvážení této skutečnosti a komplikací spojených s více vlákny byl zvolen jednovláknový přístup. 3.2 Struktura projektu Projekt se skládá z několika modulů (souborů v C++), které se dají rozdělit do dvou skupin na vizuální a nevizuální. Moduly dbg1.cpp dbg8.cpp se převážně starají o grafické uživatelské rozhraní. Každý z nich je spojený s jedním oknem, jehož události zpracovává. Modul dbg1.cpp spravuje hlavní okno RazeDebuggeru a ostatní moduly další pomocná dialogová okna. Definici oken obsahují stejnojmenné soubory s příponou.dfm. Nejdůležitějším nevizuálním modulem je soubor dbg_jadro.cpp, který obsahuje třídu Debugger, jejímž prostřednictvím se řídí celý proces ladění. Dalším modulem je disasm.cpp, který slouží k překladu strojového kódu (reprezentujícího instrukce procesoru) do čitelné formy (do assembleru). Pomocný modul vyrazy.cpp se využívá pro vyhodnocování výrazů (výpočty adres, testování podmínek breakpointů). Soubor RazeDebugger.cpp se dá označit za hlavní program celého projektu, protože přímo či nepřímo používá všechny zmíněné moduly a také obsahuje funkci WinMain(...). Z hlediska použitého vývojového prostředí reprezentuje celý projekt soubor RazeDebugger.bdsproj. K projektu je ještě přiložená nejnovější verze knihovny dbghelp.dll (pro zajištění správného nahrávání ladicích informací) a k ní příslušné soubory (dbghelp.lib 1 a dbghelp.h). Uvedené soubory lze nalézt v [10]. 3.2.1 Modul dbg1.cpp Modul dbg1.cpp obstarává obsluhu událostí hlavního okna. Dále vytváří instanci třídy Debugger z modulu dbg_jadro.cpp, pomocí které řídí proces ladění (jejími metodami Debugger::pripoj_se_k_procesu(...), vytvor_proces(...), odpoj_se_od_- 1 Jde o jiný formát souboru s příponou.lib, než který používá Borland Turbo C++. Pro konverzi lze použít nástroj tohoto prostředí bin/coff2omf.exe.
KAPITOLA 3. IMPLEMENTACE 17 procesu(), zastav_proces(), rozbehni_proces(), trace_into(), step_over(), ukonci_proces(), pridej_breakpoint(...), smaz_breakpoint(...)...). Při ladění procesu je nutné odebírat od systému ladicí události (zprávy). Toho se v modulu dosahuje periodickým voláním (asi každých 50 ms) metody prijmi_udalosti() třídy Debugger, která pro každou přijatou událost volá určitou (callback) funkci, kterou si modul určil při vytváření instance třídy. Modul dbg1.cpp pro tento účel registruje funkci obsluha_udalosti(str), která zajišt uje zobrazení aktuálních informací v hlavním okně v souvislosti se vzniklou událostí (pomocí metod modulu zobraz_registry(), zobraz_kod(), zobraz_data(), zobraz_zasobnik(), zobraz_- breakpointy() a zobraz_vlakna()). 3.2.2 Modul dbg jadro.cpp Modul dbg_jadro.cpp obsahuje definice struktur pro uložení informací o vláknech a breakpointech a publikuje třídu Debugger, která představuje nadstavbu nad službami operačního systému souvisejícími s laděním procesu. Třída je nezávislá na použitém grafickém rozhraní a své služby nabízí s důrazem na jednoduchost používání. Při vytváření instance třídy Debugger je nutné do konstruktoru předat adresu (callback) funkce, kterou třída bude volat při výskytu ladicí události. Třída nabízí metodu vytvor_proces(...) pro spuštění nového procesu (je třeba zadat jméno modulu, parametry a pracovní adresář), který je možné volitelně ladit (parametr debug_flag). Pro ladění už běžícího procesu slouží metoda pripoj_se_- k_procesu(id) jako parametr vyžaduje identifikátor daného procesu. Odpojení, resp. ukončení, laděného procesu zařídí metoda odpoj_se_od_procesu(), resp. ukonci_proces(). Metody využívají systémové funkce CreateProcess(...), DebugActiveProcess(...), DebugActiveProcessStop(...) a TerminateProcess(...). Běh procesu lze řídit metodami zastav_proces(), rozbehni_proces(), trace_- into() a step_over(). O stavu procesu informují metody proces_bezi(), proces_- pripojeny() a proces_se_krokuje(). Identifikátor aktuálního vlákna (to, se kterým se naposledy pracovalo) vrací metoda vrat_id_akt_vlakna(). Aktuální vlákno je možné změnit voláním metody zmen_aktualni_vlakno(id). Zablokování, resp. odblokování vlákna, zajišt uje me-
KAPITOLA 3. IMPLEMENTACE 18 toda zablokuj_vlakno(id), resp. odblokuj_vlakno(id). Přístup k registrům procesoru (čtení i zápis) se dá realizovat přes metodu registr_akt_vlakna(id) (parametrem je identifikátor registru, funkce vrací ukazatel na hodnotu registru). Příznaky procesoru je možné pouze číst (metoda priznak_akt_- vlakna(id)) a invertovat (metoda invertuj_priznak_akt_vlakna(id)). Třída poskytuje širokou paletu metod pro práci s breakpointy, daty a symboly: pridej_breakpoint(adresa, podminka), smaz_breakpoint(adresa), invertuj_breakpoint(adresa) zmen_podminku_breakpointu(adresa, podminka), vrat_podminku_brakpointu(adresa) je_tam_breakpoint(adresa), je_tam_povoleny_breakpoint(adresa) zakaz_breakpoint(adresa), povol_breakpoint(adresa) precti_n_znaku(adr, n,...), precti_dword(adr, hodnota), zapis_dword(adr, hodnota), zapis_retezec(adr, str). vrat_jmeno_symbolu_na_adrese(adr), najdi_adresu_symbolu_podle_jmena(...), vrat_zdrojove_radky_na_adrese(adr) Tabulky vláken, breakpointů a modulů jsou ve třídě deklarované jako privátní (jde o důležité údaje, a proto nesmí dojít k jejich poškození). Přístup pro čtení je však možný pomocí metod zpristupni_vlakna(), zpristupni_breakpointy() a zpristupni_moduly(), které vracejí ukazatele na příslušné (konstantní) tabulky. Důležitou metodou, která musí být periodicky volána, je metoda prijmi_udalosti(). Tato metoda na jedno zavolání bud zpracuje určitý shluk událostí (tj. takové, které od sebe dělí maximálně desítky milisekund), nebo se neprodleně vrátí, pokud ve frontě není žádná událost. Metoda se celkově nikdy neblokuje na více než několik stovek milisekund. Pro každou ladicí událost zajistí metoda její řádné ošetření a navíc dá volajícímu modulu o události vědět (v případech, kdy to má dobrý význam), a to zpětným voláním určité funkce (callback), jejíž adresu registroval volající modul při vytváření instance třídy Debugger. Zpětně volaná funkce má jediný parametr
KAPITOLA 3. IMPLEMENTACE 19 řetězec typu AnsiString s textovým popisem události. Pro získávání události se používá systémová funkce WaitForDebugEvent(...). Metoda preloz_kod_do_asm(adresa, instrukce,...) slouží k překladu strojového kódu do čitelné formy (mnemonických zkratek). Metoda pracuje tak, že si nejdříve do dostatečně velkého bufferu načte odpovídající blok paměti laděného procesu na zadané adrese a potom v něm postupně dekóduje jednotlivé instrukce. Načtení bloku zajistí funkce operačního systému ReadProcessMemory(...) a pro dekódování jedné instrukce se opakovaně využívá funkce preloz_jednu_instrukci_- do_asm(...) modulu disasm.cpp. 3.2.3 Modul disasm.cpp Modul disasm.cpp definuje strukturu jedna_instrukce_v_asm_t, která obsahuje všechny potřebné informace o instrukci (její adresu v paměti, hexadecimální výpis jejího strojového kódu a její mnemonickou reprezentaci). Modul exportuje jedinou funkci preloz_jednu_instrukci_do_asm(buf,instrukce), která dekóduje instrukci z lokálního bufferu a výsledek uloží do příslušné datové struktury. Dekódování instrukce má dvě fáze dekódování prefixů a dekódování operačního kódu včetně případných dalších součástí instrukce. Prefixové bajty (pokud jsou přítomné) tvoří souvislý sled před začátkem operačního kódu instrukce. Druhou fázi dekódování provádí funkce dekoduj_opkod(...). Funkce pracuje podobně jako konečný automat z bufferu postupně čte jednotlivé bajty a podle jejich hodnot přechází mezi stavy. Jakmile automat rozpozná posloupnost bajtů jako operační kód určité instrukce, uloží její textovou reprezentaci jako výsledek dekódování a funkce končí. (U některých instrukcí je k tomu ještě třeba funkce na dekódování efektivní adresy dekoduj_ea(...).) V případě, že se instrukci nepodařilo identifikovat, je výsledkem db xy, kde xy je hexadecimální vyjádření prvního bajtu sekvence. Důvodem neúspěchu dekódování je bud fakt, že jde o instrukci, kterou RazeDebugger nezná, nebo to, že dekódovaná data vůbec nepředstavují instrukce (např. výplň kvůli zarovnávání). Přechod mezi stavy automatu je realizován pomocí konstrukce switch(...){...}, která má konstantní časovou složitost. Celkově tak časová složitost rozpoznání jedné
KAPITOLA 3. IMPLEMENTACE 20 instrukce závisí lineárně na délce jejího operačního kódu. Funkce dekoduj_opkod(...) má několik tisíc řádků a i automat v ní obsažený má velice mnoho stavů. Její ruční programování by bylo velice obtížné, náchylné k vzniku chyb a špatně udržovatelné a rozšiřovatelné (při nutnosti přidat nové instrukce). Proto byl vyvinut speciální pomocný nástroj pro generování této funkce ze seznamu instrukcí a jejich operačních kódů. Navíc vygenerovaný kód skládající se z mnoha konstrukcí switch(...){...} nepůjde přeložit v případě, že seznam instrukcí je nekonzistentní (různé instrukce mají stejný operační kód). 3.2.4 Modul vyrazy.cpp Pomocný modul vyrazy.cpp slouží k vyhodnocování výrazů. Používá se v modulu dbg_jadro.cpp při testování podmínek breakpointů a ve většině vizuálních modulů pro vyhodnocování výrazů, které zadá uživatel. K tomuto účelu modul exportuje funkci vyhodnot_vyraz(...). Funkce má pět parametrů. První parametr vyraz typu AnsiString představuje vyhodnocovaný výraz (může se skládat z čísel v hexadecimální soustavě bez prefixu, operátorů a registrů; více informací viz uživatelská příručka). Druhý parametr debugger je reference na instanci třídy Debugger (ta je potřeba pro zjišt ování hodnot registrů). Třetí parametr zprava_ ukazuje na řetězec typu AnsiString pro uložení případného chybového hlášení (hlášení lze ignorovat předáním hodnoty 0 místo adresy). Čtvrtý parametr vysledek slouží pro uložení vypočtené hodnoty výrazu (parametr je reference na typ DWORD). Pátý parametr ma_byt_logicky typu bool není povinný (pokud se neuvede, předpokládá se hodnota false). Parametrem se nastavuje akceptovaný typ výsledku výrazu (bud logický, nebo celočíselný), při neshodě se skutečným typem výsledku dojde k chybě při vyhodnocování. Návratovým typem funkce je výčtový typ vysledek_vyhodnoceni_t. Jeho hodnoty zahrnují vvok (výraz je v pořádku), vvnelzevyhodnotit (lexikálně a syntakticky dobře, ale došlo např. k dělení nulou, nepovolenému přístup do paměti...) a vvspatne (lexikální a/nebo syntaktické chyby např. špatný formát čísel, neznámé identifikátory, operátory, chybějící operandy, špatný typ výsledku...). Pouze s dvěma úrovněmi návratového typu by nebylo možné vystačit, protože např. syntakticky
KAPITOLA 3. IMPLEMENTACE 21 správně zadaná podmínka breakpointu nemusí jít vždy vyhodnotit (např. kvůli dělení registrem s aktuální hodnotou rovnou nule). Vyhodnocování funguje na principu převodu z infixového do postfixového zápisu sloučeného s vyhodocováním postfixu. K tomu se používají dva zásobníky jeden je na hodnoty a druhý na operátory. Časová složitost vyhodnocení je lineárně závislá na délce výrazu. 3.3 Přidružené projekty Součástí práce jsou vedle hlavního projektu (RazeDebugger) i dva projekty pro jeho testování a jeden pomocný projekt pro generování části kódu hlavního projektu. Projekt Test (vytvořený v [13]) slouží k testování výkonu, stability a dalších vlastností debuggeru. Projekt Test-pdb (vytvořený v [15]) prověřuje schopnost debuggeru nahrávat ladicí informace ze souborů ve formátu.pdb. Pomocný generátor Helper (vytvořený v [14]) zajišt uje převod vstupní instrukční sady do podoby zdrojového kódu v C++, který dokáže překládat strojový kód do assembleru.
Kapitola 4 Srovnání Tato kapitola srovnává existující debuggery a výslednou implementaci, která patří k této bakalářské práci (projekt RazeDebugger, dále pod zkrátkou RD). Text konfrontuje různé aspekty testovaných debuggerů dostupné funkce, uživatelskou přívětivost, zatížení procesoru, výkonnost, stabilitu, ale také reakce debuggeru na zvláštní chování laděného programu (krokování sebemodifikujícího kódu, snahu laděného programu detekovat debugger instrukcí int3). Testování zatížení procesoru spočívá ve sledování příslušné hodnoty v systémovém nástroji (Správci úloh systému Windows), a to v situaci, kdy je k testovanému debuggeru připojený jeden běžící proces (vždy stejný), který v daný okamžik nezpůsobuje vznik žádných ladicích události. V takové situaci je nežádoucí, aby debugger významně zatěžoval procesor. Test výkonu debuggeru zprostředkovává (k ladění určený) přiložený testovací program (Test.exe), a to měřením doby, po kterou trvá smyčka s padesáti tisíci cyklů, do které byl umístěn breakpoint s nesplnitelnou podmínkou (např. 1==2). Takováto smyčka způsobí vznik velkého množství ladicích událostí (stejného počtu jako cyklů). Ty však ve výsledku nezpůsobí zastavení laděného procesu z důvodu nesplnitelnosti podmínky breakpointu. Doba trvání smyčky tak odráží, s jakou efektivitou debugger vyřizuje ladicí události. (Test byl prováděn na počítači s touto konfigurací: procesor Intel Pentium M 1,6 GHz, operační pamět 1 GB RAM, pevný disk 60 GB IDE 5 400 RPM, operační systém Microsoft Windows XP Professional SP2.) Dalším prováděným testem je test stability krokování. Testem lze prokázat nerobustnost krokování typu step over, které je založené na softwarových breakpoin- 22
KAPITOLA 4. SROVNÁNÍ 23 tech. Tento test (stejně jako předchozí) zprostředkovává přiložený testovací program (Test.exe). Test spočívá ve vytvoření dvou vláken nad stejnou smyčkou, přičemž první vlákno se ve smyčce blokuje na určitý počet milisekund (voláním funkce Sleep- (...)), ale druhé blokování vždy přeskakuje a pokračuje bezprostředně za instrukcí call. Posuzovaným hlediskem testu je stabilita při krokování blokujícího se vlákna (step over na instrukci call) v situaci, kdy je současně aktivní i neblokující se vlákno (s rušivým vlivem). V závěru kapitoly se nachází tabulka 4.1, která přehledně srovnává testované debuggery ve vybraných rysech a uvádí přesné výsledky testů. 4.1 Borland Turbo C++ 2006 Integrovaný debugger prostředí Borland Turbo C++ 2006 [14] (dále pod zkratkou BTD; viz obr. 4.1) je primárně určený k ladění projektů vytvořených v tomto prostředí, ale na druhé straně lze použít také jako externí debugger k ladění procesů. BTD dovoluje stejně jako RD jednak ladit už běžící procesy a jednak pro ladění spouštět nové procesy. BTD navíc zvládá připojení více laděných procesů současně (RD pouze jeden současně připojený proces). Hlavní ladicí okno BTD přehledně zobrazuje podstatné údaje (kód, data, základní registry a zásobník). Takovýto design byl výchozí inspirací pro hlavní okno RD, které však ještě přidává další panely (rozšířené registry, breakpointy, vlákna a zprávy). Panel s výpisem paměti jde v BTD nastavit pouze na jednu pevně danou adresu, zatímco v RD lze pomocí záložek rychle přepínat mezi různými oblastmi paměti a místo pevné adresy lze zadat výraz, který se pokaždé znovu vyhodnocuje. Oba debuggery podporují krokování a umist ování softwarových breakpointů. BTD má vylepšené krokování (funkce run to cursor a run until return) a pokročilejší možnosti u breakpointů (počet průchodů pro aktivaci, breakpointy na přístup do paměti). BTD na rozdíl od RD nedovoluje uživateli zablokovat či odblokovat vlákna laděného procesu. BTD dále nabízí pokročilé funkce, které v RD zatím chybí. Jedná se zejména o zobrazení aktuální hierarchie volaných funkcí (angl. call stack) a zobrazení lokálních proměnných.
KAPITOLA 4. SROVNÁNÍ 24 BTD používá vlastní formát ladicích informací (soubory s příponou.tds), a proto při ladění ignoruje ladicí informace vytvořené prostředím Microsoft Visual Studio (soubory s příponou.pdb). Naopak RD používá ladicí informace právě ze souborů s příponou.pdb, protože jde o formát, který je přirozený knihovnám operačního systému Windows. Vyhledávání symbolů (funkcí) v kódu na základě jejich jména je v BTD možné za předpokladu, že uživatel zadá korektní jméno symbolu včetně velikosti písmen. V případě, že se stejně jmenující symbol nachází ve více modulech, uživateli specifikování konkrétního modulu nepomůže. (Řešení spočívá v nalezení daného symbolu v okně s moduly.) RD naopak akceptuje i jména symbolů se špatným rozlišením velkých a malých písmen. V případě nejednoznačnosti se však zobrazí chybové hlášení s výčtem symbolů, které připadají v úvahu. Hledaný symbol pak lze identifikovat přesným dodržením velikosti písmen, resp. přidáním jména příslušného modulu. BTD neregistruje některé změny způsobené sebemodifikujícím kódem (RD ano). V BTD nebyl nalezen způsob jak pro laděný program zamaskovat přítomnost debuggeru v případě testu, kdy laděný program vykoná instrukci int3 a čeká, zda tato akce způsobí výjimku (BTD výjimku vždy ošetří). RD dovoluje u každé výjimky jednoduše nastavit, zda ji má debugger ošetřit. Tím lze jednoduše zamaskovat přítomnost debuggeru. Zatížení procesoru je u BTD při situaci popsané v úvodu kapitoly minimální. Při vyřizování ladicích události dosahuje BTD o něco nižšího výkonu než RD (ale přibližně třináctkrát vyššího výkonu v porovnání s debuggerem prostředí Microsoft Visual Studio). BTD vykazuje nestabilitu při výše zmíněném testu krokování (skok přes instrukci call je nedeterministický a trvá řádově desítky sekund). Naopak RD se v této situaci chová deterministicky (protože používá hardwarový breakpoint) a rychlost krokování je stejná jako v případě krokování bez rušivého vlivu dalšího vlákna. 4.2 Microsoft Visual Studio 2005 Integrovaný debugger prostředí Microsoft Visual Studio 2005 [15] (dále pod zkratkou MVSD; viz obr. 4.2) jde stejně jako BTD použít jako externí debugger. Obecně lze
KAPITOLA 4. SROVNÁNÍ 25 Obrázek 4.1: BTD nedetekuje správně změny v sebemodifikujícím kódu (operand instrukce mov nesouhlasí s hodnotou registru eax) Obrázek 4.2: MVSD nedetekuje správně změny v sebemodifikujícím kódu (operand instrukce mov nesouhlasí s hodnotou registru eax)
KAPITOLA 4. SROVNÁNÍ 26 Obrázek 4.3: OllyDbg detekuje změny způsobené sebemodifikujícím kódem správně (operand instrukce mov souhlasí s hodnotou registru eax) konstatovat, že většina skutečností zmíněných v části o BTD platí i pro MVSD. Tyto dva debuggery jsou si velice podobné (hlavně co se týče funkcí). Nalezené odlišnosti mezi MVSD, BTD a RD jsou rozebrané níže. MVSD na rozdíl od RD a BTD dovoluje připojit pouze už běžící procesy (neumí pro ladění spouštět nové). Ladění v MVSD probíhá ve velkém množství oken. Většinu z nich je třeba explicitně otvírat. Ladění proto není tak přehledné jako v BTD. MVSD nabízí celkem čtyři panely s výpisem paměti, které lze však vždy nastavit pouze na jednu pevně danou adresu. Změna hodnot registů procesoru v MVSD je poměrně problematická (a pro některé registry snad nemožná). Naproti tomu v BTD a RD jdou registry měnit snadno včetně zadávání různých typů hodnot podle aktuální interpretace registru. MVSD (stejně jako RD) dovoluje uživateli na rozdíl od BTD zablokovat či odblokovat vlákna laděného procesu. MVSD používá ladicí informace uložené v souborech s příponou.pdb. Stejným způsobem funguje i RD.
KAPITOLA 4. SROVNÁNÍ 27 Symboly v kódu lze v MVSD vyhledávat stejně jako v BTD při přísném dodržení velikosti písmen. Hledání však probíhá pouze v aktuálně prohlíženém modulu (přidání jména modulu k symbolu není podporované). Okno s moduly v MVSD nezobrazuje pro zvolený modul seznam symbolů (na rozdíl od BTD a RD). Při vyřizování ladicích události dosahuje MVSD přibližné sedmnáctkrát nižšího výkonu než RD. 4.3 OllyDbg OllyDbg (zdarma dostupný z [16]; dále pod zkratkou OD; viz obr. 4.3) je stejně jako RD externí aplikační debugger. Jedná se o velice zdařilý projekt, který má v podstatě většinu funkcí BTD, MVSD a RD a ještě přidává mnoho svých vlastních navíc. Další text se soustřed uje právě na tato rozšíření, ale také na rozdíly oproti RD. Panel s pamětí v OD pracuje na stejném principu (jedna pevná adresa) jako v BTD či MVSD. Systém záložek a opětovné vyhodnocování zadaného výrazu implementuje pouze RD. Na druhé straně OD má větší možnosti interpretace zobrazovaných dat (unicode a desítková soustava). OD rozšiřuje možnosti krokování o automatickou variantu. V takovém případě debugger sám krokuje laděný kód (mnohem rychleji, než by zvládl uživatel). Automatické krokování končí po vzniku ladicí události, na explicitní žádost uživatele nebo jakmile současný stav procesu splňuje podmínky, které zadal uživatel. OD jako jediný z testovaných debuggerů dovoluje editovat laděný kód přímo v assembleru. Další jeho výjimečnou funkcí je přímé zadávání hardwarových breakpointů uživatelem. V OD byla nalezena chyba týkající se funkčnosti breakpointu po zablokování vlákna, které breakpoint aktivovalo. Takovýto breakpoint se pak totiž v OD chová jako zakázaný. RD proto nedovoluje zablokovat vlákno, které právě aktivovalo breakpoint. OD používá stejně jako MVSD a RD ladicí informace ze souborů s příponou.pdb. Neumí však ke kódu v assembleru zobrazovat řádky z příslušných zdrojových souborů (ostatní testované debuggery ano). Při vyhledávání symbolů v kódu OD podporuje přidání jména modulu k sym-
KAPITOLA 4. SROVNÁNÍ 28 Tabulka 4.1: Srovnání debuggerů Debugger BTD MVSD OD RD Doba trvání smyčky 6,3 s 82,2 s 5,4 s 4,7 s Zatížení procesoru minimální minimální maximální minimální Detekce sebemodif. kódu ne 1 ne 2 ano 3 ano Maskování debuggeru nelze nelze lze lze Stabilita krokování nestabilní nestabilní? stabilní bolu (pro rozlišení nejednoznačnosti). Jméno symbolu je však třeba zadávat včetně správné velikosti písmen. OD stejně jako RD správně registruje změny způsobené sebemodifikujícím kódem. Maskování přítomnosti debuggeru při testu laděného programu instrukcí int3 je možné nastavit jako pravidlo (v RD se nastavuje pro každou výjimku zvlášt ). Určitou nepříjemností při používaní OD je fakt, že tento debugger při situaci popsané v úvodu kapitoly maximálně zatěžuje procesor na rozdíl od všech ostatních testovaných debuggerů, které při dané situaci představují pro procesor jen minimální zátěž. Při vyřizování ladicích události dosahuje OD srovnatelného výkonu jako RD. Test stability krokování se však v OD nepodařilo provést, protože OD neumí zastavit testovací program po vytvoření příslušných vláken. 1 viz obr. 4.1 2 viz obr. 4.2 3 viz obr. 4.3
Kapitola 5 Závěr Jedním z cílů této práce bylo vytvoření aplikačního debuggeru. Velký důraz se přitom kladl na podporu ladění více vláken, podporu instrukcí typu SIMD a zobrazení dostupných ladicích informací ze zdrojového kódu. Tento cíl se při implementaci projektu RazeDebugger podařilo splnit. Při ladění vícevláknového procesu lze v debuggeru snadno přepínat mezi vlákny a také je jednoduše řídit (zablokovat a odblokovat). Debugger podporuje rozšíření instrukční sady o instrukce typu SIMD až do SSE2 (tj. MMX, 3DNow!, SSE a SSE2) včetně zobrazení hodnot příslušných registrů a možnosti jejich modifikace. Do kódu v assembleru debugger vkládá odpovídající řádky ze zdrojových souborů (pokud jsou k dispozici) a jména symbolů, které na dané adrese začínají. RazeDebugger byl vyvíjen s důrazem na výkon, stabilitu funkcí a uživatelskou přívětivost. Ve srovnání s testovanými debuggery dosahuje RazeDebugger nejlepšího výkonu při vyřizování velkého množství ladicích událostí. Debugger bez problému detekuje změny způsobené sebemodifikujícím kódem, dává uživateli možnost maskovat svou přítomnost a nabízí stabilní krokování bez rušivého vlivu ostatních vláken (stabilitu zajišt uje použití hardwarového breakpointu). Uživatelské rozhraní projektu dovoluje snadné a intuitivní ovládání. Panel s daty umožňuje prostřednictvím záložek rychlé přepínání mezi pamět ovými oblastmi včetně sledování paměti na adrese určené dynamicky vyhodnocovaným výrazem. Okno s moduly pak zlepšuje orientaci v symbolech, zdrojových souborech a řádkách každého modulu. V budoucnu by bylo vhodné debugger rozšířit o další pokročilé funkce, které nabízejí dnešní profesionální debuggery. Jedná se zejména o zobrazení aktuální hi- 29
KAPITOLA 5. ZÁVĚR 30 erarchie volaných funkcí (angl. call stack), větší využití ladicích informací (práce s typy a proměnnými), podporu datových breakpointů či možnost ladit 64bitové procesy.
Reference [1] Intel R 64 and IA-32 Architectures Software Developer s Manual [online]. 16. 11. 2006. 27 42. <http://www.intel.com/design/processor/manuals/253666.pdf> [2] Debugging a Running Process [online]. 10. 1. 2008. <http://msdn2.microsoft.com/en-us/library/ms679301(vs.85).aspx> [3] Writing the Debugger s Main Loop [online]. 10. 1. 2008. <http://msdn2.microsoft.com/en-us/library/ms681675(vs.85).aspx> [4] Debugging Events [online]. 10. 1. 2008. <http://msdn2.microsoft.com/en-us/library/ms679302(vs.85).aspx> [5] Initializing the Symbol Handler [online]. 10. 1. 2008. <http://msdn2.microsoft.com/en-us/library/ms680344(vs.85).aspx> [6] Enumerating Symbol Modules [online]. 10. 1. 2008. <http://msdn2.microsoft.com/en-us/library/ms679319(vs.85).aspx> [7] Enumerating Symbols [online]. 10. 1. 2008. <http://msdn2.microsoft.com/en-us/library/ms679318(vs.85).aspx> [8] SymEnumSourceFiles Function [online]. 10. 1. 2008. <http://msdn2.microsoft.com/en-us/library/ms680712(vs.85).aspx> [9] SymGetFileLineOffsets64 Function [online]. 10. 1. 2008. <http://msdn2.microsoft.com/en-us/library/ms681328(vs.85).aspx> 31
REFERENCE 32 [10] Microsoft Debugging Tools for Windows [počítačový program]. Ver. 6.8. Září 2007. Dostupný z: <http://www.microsoft.com/whdc/devtools/debugging/default.mspx> [11] Toal R. x86encode [online]. <http://www.cs.lmu.edu/~ray/notes/x86encode/> [12] Ludvig M. 12.2 Debug Registers [online]. <http://www.logix.cz/michal/doc/i386/chp12-02.htm> [13] Borland Turbo Delphi 2006 Explorer [počítačový program]. Dostupný z: <http://www.turboexplorer.com/delphi> [14] Borland Turbo C++ 2006 Explorer [počítačový program]. Dostupný z: <http://www.turboexplorer.com/cpp> [15] Microsoft Visual Studio 2005 Professional Edition [počítačový program]. [16] OllyDbg [počítačový program]. Ver. 1.10. Oleh Yuschuk, 2004. Dostupný z: <http://www.ollydbg.de/odbg110.zip>
Příloha A Obsah přiloženého CD V adresáři /bin se nacházejí spustitelné soubory a aktuální verze jedné knihovny: 1. RazeDebugger.exe výsledná implementace aplikačního debuggeru 2. dbghelp.dll nejnovější verze knihovny pro zajištění správného nahrávání nejnovějšího formátu ladicích informací 3. Test.exe program pro testování debuggeru 4. Test-pdb.exe program pro testování zobrazení ladicích informací (ve formě.pdb) v debuggeru 5. Helper.exe pomocný nástroj, který generuje kód pro překlad strojového kódu do assembleru V adresáři /src se nalézají další adresáře se zdrojovými soubory: 1. /src/razedebugger zdrojové soubory projektu 2. /src/test zdrojové soubory testovacího programu 3. /src/test-pdb zdrojové soubory dalšího testovacího programu 4. /src/helper zdrojové soubory pomocného generátoru V adresáři /text je uložený text bakalářské práce ve formátu PDF. Soubor /setup.exe zajistí nainstalování projektu RazeDebugger (včetně testovacích programů) do uživatelem zvolené složky. 33
Příloha B Uživatelská příručka B.1 Instalační příručka B.1.1 Prerekvizity Projekt RazeDebugger k svému běhu vyžaduje počítač s architekturou IA-32 (nebo kompatibilní) a operační systém Microsoft Windows XP (nebo vyšší). B.1.2 Instalace Instalace projektu je velice jednoduchá. Stačí spustit setup.exe, zvolit cestu pro nainstalování projektu a vyčkat, než se dokončí instalace projektu. Instalace obsahuje tři soubory: RazeDebugger.exe (vlastní projekt), Test.exe (testovací program pro zkoušení debuggeru) a Test-pdb.exe (test ladicích informací). Testovací program obsahuje několik tlačítek, pomocí kterých lze testovat funkčnost vybraných vlastností RazeDebuggeru. Pro ulehčení práce je na kód obsluhy některých tlačítek v původním kódu nastavený softwarový breakpoint. B.2 Základní funkce RazeDebugger slouží k lokálnímu ladění nativních 32bitových procesů (jak debugger, tak i laděný proces běží na stejném systému). K zahájení ladění nějakého procesu je nejdříve nutné k tomuto procesu RazeDebugger připojit. Toho lze docílit dvěma způsoby. Uživatel si může vybrat bud proces, který už v systému běží, nebo spustitelný soubor (s příponou.exe), který RazeDebugger následně spustí. Počet současně připojených procesů nemůže být větší než jedna. 34
PŘÍLOHA B. UŽIVATELSKÁ PŘÍRUČKA 35 Připojený proces jde zastavit a opět rozběhnout, krokovat nebo předčasně ukončit. Okno RazeDebuggeru obsahuje několik panelů, které zobrazují stav laděného procesu hodnoty registrů, výpis části paměti, výpis části kódu přeloženého do assembleru, výpis části zásobníku, seznam vláken, seznam nastavených breakpointů (bodů zastavení v kódu) a seznam událostí, které v laděném procesu nastaly. Při ladění vícevláknového procesu lze v RazeDebuggeru jednoduše přepínat mezi jednotlivými vlákny. Do kódu laděného procesu jdou nastavovat softwarové breakpointy (místa, kde se má proces zastavit, pokud tudy projde bod provádění libovolného vlákna), u kterých lze zastavení případně podmiňovat splněním logického výrazu. Pokud je laděný proces zastavený, může uživatel měnit hodnoty registrů. Ladění procesu končí v okamžiku zániku tohoto procesu (bud skončí normálně, nebo ho předčasně ukončí uživatel RazeDebuggerem) nebo také odpojením laděného procesu, kdy proces normálně pokračuje ve svém běhu bez další přítomnosti debuggeru. Po skončení ladění jednoho procesu je RazeDebugger připravený ladit další proces. Při ukončení RazeDebuggeru, který právě ladí nějaký proces, dochází zároveň i k ukončení připojeného procesu. B.3 Hlavní okno Hlavní okno (viz obr. B.1) se sestává z několika panelů, které jsou poskládané do dvou sloupců. V levém sloupci to jsou panely (shora dolů): s kódem, daty, breakpointy, vlákny a zprávami. V pravém sloupci se nachází vedle sebe panel se základními registry a panel s příznaky procesoru a pod nimi dva panely s dalšími registry (FPU/MMX a XMM). Zbytek pravého sloupce vyplňuje panel se zásobníkem. Velikost panelů jde podle potřeby přizpůsobovat. Pokud některý panel uživatele nezajímá, může ho skrýt (v menu Zobrazit) a tím zvětšit místo pro ostatní panely nebo redukovat velikost celého okna. Obsah panelů (u kterých to má smyl) lze postupně rolovat pomocí šipek na klávesnici nebo kolečka myši (o celé stránky pomocí kláves Page Up a Page Down). Panel s daty používá záložky, což umožňuje snadnější sledování více pamět ových lokací. V horní části okna (mezi menu a panely) se nachází lišta s tlačítky pro řízení
PŘÍLOHA B. UŽIVATELSKÁ PŘÍRUČKA 36 Obrázek B.1: Hlavní okno běhu procesu, zaškrtávací pole týkající se ošetřování výjimek a popisek indikující stav laděného procesu. B.4 Podrobný návod jak ladit proces B.4.1 Zahájení ladění Po spuštení ladicího prostředí RazeDebugger není připojený žádný proces. Nový proces uživatel spustí kliknutím na položku v menu (Proces Spustit nový...), popřípadě klávesovou zkratkou Ctrl+N. Pokud se uživatel chce připojit rovnou k nějakému běžícímu procesu, zvolí v menu Proces Připojit... (popř. zkratkou Ctrl+P). Obě možnosti vyvolají dialogové okno, v kterém si uživatel vybere příslušný soubor, resp. proces. K některým (zejména systémovým) procesům se nelze připojit z bezpečnostních důvodů nebo kvůli nedostatečným právům. O takovéto situaci bude RazeDebugger uživatele informovat.
PŘÍLOHA B. UŽIVATELSKÁ PŘÍRUČKA 37 Po připojení procesu dochází automaticky k jeho zastavení. Pokud jde o nově spuštený proces, zastaví se na své startovací adrese; naopak proces, který nějakou dobu běžel, obvykle uvízne uvnitř systémové funkce (toto zastavení způsobí speciální vlákno přidané operačním systémem pro tuto událost na přechodnou dobu). B.4.2 Zastavený proces Některé funkce, které RazeDebugger nabízí, se dají aplikovat pouze za podmínky, že proces neběží. Jedná se zejména o změnu hodnot registrů (ty je nemožné měnit, zatímco by laděný proces běžel), změnu aktuálního vlákna (to představuje aktuálně laděné vlákno), zablokování/odblokování vláken, krokování (trace into a step over) a také převzetí odpovědnosti za nastalou výjimku. Nové hodnoty registrů se nastavují ve speciálním dialogovém okénku, které se vyvolá dvojitým kliknutím na měněný registr nebo pomocí kontextového menu. (Jako novou hodnotu registru lze zadat libovolný výraz popis akceptovaných výrazů viz část Výrazy.) Aktuální vlákno se volí obdobně (dvojitým kliknutím nebo pomocí kontextového menu). Zablokované vlákno lze odblokovat a aktivní (neblokované) naopak zablokovat pomocí příslušných položek kontextového menu v okně s vlákny. Ladicí prostředí nabízí, jak je u debuggerů zvykem, krokování typu trace into (kl. zkr. F7 ) a step over (kl. zkr. F8 ). Trace into krokuje laděný proces instrukci za instrukcí přesně tak, jak je vykonává procesor. Step over se liší tím, že nezabíhá dovnitř procedur a funkcí volaných instrukcí call. K zastavení procesu dochází často tím, že při jeho běhu nastane výjimka. O takové skutečnosti je jako první informován RazeDebugger, který musí rozhodnout, jestli výjimku zpracuje sám, nebo ji předá k zpracování laděnému procesu. Důvodem, proč RazeDebugger ošetřuje některé výjimky, které nastaly v laděném procesu, je fakt, že za tyto výjimky RazeDebugger nese odpovědnost (např. softwarový breakpoint způsobí výjimku). Zda výjimka bude ošetřena RazeDebuggerem, indikuje odpovídající zaškrtávací pole v horní části okna (pod menu, napravo od lišty s tlačítky). V případech, kdy za výjimku RazeDebugger nenese odpovědnost, dostává uživatel volnost nastavit, kdo výjimku ošetří (výchozí hodnota představuje doporučení RazeDebuggeru pro danou situaci).
PŘÍLOHA B. UŽIVATELSKÁ PŘÍRUČKA 38 Zastavený proces se rozběhne klávesovou zkratkou F9, v menu Ladit Rozběhnout nebo tlačítkem s obrázkem zelené šipky. B.4.3 Kód Panel s kódem zobrazuje instrukce procesoru pomocí mnemonických zkratek čitelných pro člověka. Po zastavení procesu se vždy zobrazí instrukce z okolí aktuálního bodu provádění (to určuje registr eip). Výpis lze rolovat o jednu instrukci pomocí klávesových šipek nahoru a dolů a o celé stránky pomocí kláves Page Up a Page Down. Posun dolů většinou zobrazí nově odkryté instrukce správně (pokud data na těchto adresách opravdu reprezentují instrukce). Horší situace nastává při rolování nahoru, protože pro překlad instrukce ze strojového kódu do čitelného formátu je nutné znát začátek instrukcí, který se však velice špatně odhaduje. RazeDebugger se snaží si vypomáhat jednak znalostí ladicích informací a jednak zapamatováním dříve navštívených instrukcí. Pokud však ani to nepomůže, posunuje se adresa o jeden bajt (resp. o odhadnutou délku stránky). Při špatném nastavení adresy může dojít k chybné interpretaci instrukcí a zmizení šipky ukazující aktuální bod provádění. Pomocí kl. zkr. Ctrl+J nebo zvolením položky Jít na adresu... v kontextovém menu lze přímo nastavit určitou počáteční adresu. Adresu je možné vyjádřit pomocí libovolně složitého výrazu (převod jména symbolu na jeho adresu umožňuje operátor @; více viz část Výrazy). K vrácení se na adresu aktuálního bodu provádění slouží položka Jít na eip. Naopak bod provádění lze snadno přenést na zvolenou instrukci vybráním položky Nastavit jako nové eip. V kódu se standardně na určitých adresách objevují jména příslušných symbolů a příslušné zdrojové řádky (pokud je nahrávání řádek zapnuté). Zobrazování těchto informací lze přizpůsobit v kontextovém menu. Obsah panelu se zjišt uje dynamicky při ladění tzn., že RazeDebuggeru neuniknou změny provedené sebemodifikujícím kódem. B.4.4 Data Panel s daty slouží k prohlížení a měnění datového prostoru laděného procesu. Panel má tři sloupce: adresu paměti, datový výpis paměti a znakový výpis paměti. V kon-
PŘÍLOHA B. UŽIVATELSKÁ PŘÍRUČKA 39 textovém menu jde nastavit styl datového výpisu paměti (data zobrazovat jako typ byte, word, doubleword, quadword, float, double či long double). Předpokládá se, že data jsou v paměti uložená ve formátu little endian (jak je běžné pro architekturu IA-32). Panel používá systém záložek, což umožňuje snadné přeskakování z místa na místo. Se záložkami se pracuje obvyklým způsobem (kl. zkr. Ctrl+T vytvoří novou a Ctrl+W zavře aktuální; případně přes kontextové menu). Adresa záložky se nastavuje v dialogovém okně, které jde vyvolat kl. zkr. Ctrl+J nebo pomocí kontextového menu. V tomto okně lze adresu vyjádřit pomocí libovolně složitého výrazu (viz část Výrazy) a specifikovat, jestli se má adresa vzít jako aktuální hodnota výrazu, nebo jestli se má k tomu účelu výraz vyhodnocovat opakovaně. Obsah paměti se dá měnit přes kontextové menu vyvolané nad řádkem s adresou paměti, kterou chce uživatel měnit. Měněná hodnota může být jak celočíselného typu (dword), tak i typu čísla s pohyblivou desetinnou čárkou (typy float, double a long double). Další možností modifikace paměti je zadání sledu bajtů či znaků od určité adresy. Sled bajtů i znaků může mít libovolnou délku (přepíše se odpovídající část paměti). Znaky se zadávají jeden po druhém bez jakýchkoliv oddělovačů. Bajty se zapisují bud těsně za sebou bez oddělovačů (potom však každý bajt musí zaujímat přesně dvě hexadecimální číslice), nebo oddělené mezerami. B.4.5 Breakpointy Podpora umist ování ladicích bodů zastavení (breakpointů) představuje nezbytný nástroj pro ladění. RazeDebugger umožňuje volitelně k breakpointu připojit podmínku (logický výraz), za které se má breakpoint aktivovat. Nastavený breakpoint jde zakázat a následně opět povolit, aniž by o něm uživatel musel znovu zadávat jakékoliv údaje. Breakpoint na určité instrukci se nejsnáze nastaví kliknutím myší nalevo od její adresy v panelu s kódem (povolený, resp. zakázaný, breakpoint signalizuje červené, resp. šedé, kolečko vlevo vedle adresy). Další možností je vybrat příslušnou instrukci v panelu s kódem a stisknout klávesu F5 (popřípadě vyhledat tuto akci v kontextovém menu). Každý přidaný breakpoint se hned objeví v panelu s breakpointy, kde
PŘÍLOHA B. UŽIVATELSKÁ PŘÍRUČKA 40 je ho možné upravovat (měnit adresu a/nebo podmínku), povolit/zákázat, případně smazat (vše lze najít v kontextovém menu). Panel s breakpointy představuje alternativní možnost jak přidat breakpoint, pokud uživatel zná přesnou adresu (případně ji umí vyjádřit jako výraz). Na kód s nastaveným breakpointem se lze dostat dvojitým kliknutím na příslušnou položku v panelu s breakpointy. Po najetí myší na instrukci s nastaveným breakpointem se objeví podmínka tohoto breakpointu (má-li nějakou). B.4.6 Ukončení ladění Ladění normálně končí v okamžiku, kdy laděný proces skončí. Uživatel však má také možnost laděný proces ukončit předčasně (např. když se zasekne) v menu Ladit Ukončit (přip. kl. zkr. Ctrl+F2 ). Další možností je laděný proces od RazeDebuggeru odpojit v menu Proces Odpojit (laděný proces nesmí být zastavený). Proces a RazeDebugger od té chvíli už nemají nic společného proces pokračuje normálně ve svém běhu bez přítomnosti debuggeru a RazeDebugger je připravený ladit další proces. B.4.7 Moduly RazeDebugger nabízí pro větší přehlednost okno se seznamem všech modulů, které má laděný proces aktuálně nahrané. Okno se objeví po vybrání položky v menu Zobrazit Moduly (příp. kl. zkr. Ctrl+Alt+M ; viz obr. B.2). Po vybrání konkrétního modulu se v okně zobrazí seznam všech symbolů a zdrojových souborů, které přísluší k tomuto modulu. Po výběru zdrojového souboru se ukáže další seznam, který uvádí do souvislosti řádky souboru s adresami ve strojovém kódu laděného procesu. Dvojité kliknutí na určitý symbol či řádku způsobí zobrazení příslušné lokace v panelu s kódem. Informace o zdrojových souborech a řádkách jsou však dostupné pouze, pokud jsou k dispozici náležité ladicí informací a pokud je nahrávání řádek zapnuté. Tato funkce se zapíná příslušným zaškrtávacím polem v dialogovém okně Nastavení (v menu Ladit Nastavení). Okno také dovoluje přizpůsobit hledání zdrojových souborů a nastavit alternativní cesty pro hledání souborů se symboly (s příponou
PŘÍLOHA B. UŽIVATELSKÁ PŘÍRUČKA 41 Obrázek B.2: Okno s nahranými moduly.pdb). B.5 Výrazy Vyhodnocování výrazů přímo nesouvisí s prací debuggeru, ale je nezbytné pro funkčnost breakpointů s podmínkou. Navíc integrování této funkce do RazeDebuggeru přináší zjednodušení práce při zadávání adres a hodnot. Výraz se skládá z čísel (zadávají se vždy v hexadecimální soustavě bez prefixu), identifikátorů registrů procesoru, operátorů a kulatých závorek (pro stanovení specifické priority vyhodnocování). Při tvoření výrazu je třeba každý použitý operátor vhodně obalit správnými operandy (počet a typ). Každý správně utvořený výraz má svůj typ určený operátorem, který se ve výrazu vyhodnocuje jako poslední. Je třeba rozlišovat, na které místo zadat výraz kterého typu. Při zadávání adres a hodnot musí mít výraz celočíselnou hodnotu; naopak podmínka breakpointu musí mít logickou hodnotu. Vyhodnocování pracuje vnitřně s hodnotami jako s 32bitovými celými čísly bez znaménka. Výčet registrů, které se ve výrazech mohou vyskytovat, lze najít v tabulce B.1 a podrobnější popis operátorů v tabulkách B.2 a B.3. K zjištění adresy symbolu z jeho jména slouží unární operátor @. Jméno symbolu musí následovat za operátorem (lze oddělit mezerami). Pokud jméno symbolu obsahuje jiné znaky než alfanumerické, je lepší vyznačit začátek a konec jména pomocí uvozovek.