Dans ce tutorial, vous allons voir ce que Win32 offre comme mise au point primaires aux développeurs. Lorsque vous en aurez finis avec ce tutorial, vous saurez comment debugger un programme.
Downloadez l'exemple.
Win32 possède plusieurs APIs qui permettent aux programmeurs d'utiliser certaines des fonctions d'un débugger. On les appelle, les 'Win32 Debug APIs' ou 'primitives'. Avec elles, vous pouvez :
Bref, vous pouvez fabriquer un programme qui sert à debugger, grâce à ces APIs là (les Win32 Debug APIs). Puisque ce sujet est vaste, je le divise en plusieurs parties : ce tutorial étant la première partie. Dans cette première, j'expliquerai les concepts de base ainsi que la structure générale de l'utilisation des 'Win32 Debug APIs'.
Les étapes pour l'utilisation des 'Win32 Debug APIs' sont :
WaitForDebugEvent Proto lpDebugEvent:DWORD, dwMilliseconds:DWORD
lpDebugEvent est l'adresse de la structure debug_EVENT, laquelle sera remplie de l'information d'un 'debugging événement' qui arrive dans le debugger.
dwMilliseconds est la durée en millisecondes que cette fonction attendra le 'debugging événement' pour arriver. Si ce temps est écoulé et qu'aucun événement (servant à debugger) n'est arrivé, WaitForDebugEvent revient au Call. D'autre part, si vous mettez la constante INFINITE dans cet argument, la fonction ne retournera pas avant qu'un 'debugging événement' ne soit arrivé.
Maintenant on va examiner la structure DEBUG_EVENT plus en détail.
DEBUG_EVENT STRUCT
dwDebugEventCode dd ?
dwProcessId dd ?
dwThreadId dd ?
u DEBUGSTRUCT <>
DEBUG_EVENT ENDS
dwDebugEventCode contient la valeur qui indique quel type de debugging événement arrive. Bref, il peut y avoir plusieurs types d'événements, votre programme a besoin de vérifier la valeur de cet élément pour qu'il sache de quel genre il est, et lui répondre convenablement. Les valeurs possibles sont les suivantes:
Valeurs | Significations |
---|---|
CREATE_PROCESS_DEBUG_EVENT | Un process est créé. Cet événement sera envoyé lorsque le processus de debugger vient juste d'être créé (et n'est pas encore en fonction) ou lorsque votre programme s'attache juste à un process (un programme) en cours avec DebugActiveProcess. C'est le tout premier événement que votre programme recevra. |
EXIT_PROCESS_DEBUG_EVENT | Un process vient de se terminer. |
CREATE_THEAD_DEBUG_EVENT | Un nouveau lien est créé dans le processus du debugger ou quand votre programme s'attache d'abord à un process en cours. Notez que vous ne recevrez pas cet avis lorsque le lien primaire du debugger est déjà créé. |
EXIT_THREAD_DEBUG_EVENT | Un lien dans le debugger s'occupe des sorties. Votre programme ne recevra pas cet événement pour le lien primaire. Bref, vous pouvez penser que le lien primaire du debugger est un équivalent du processus du debugger lui-même. Ainsi, quand votre programme voit CREATE_PROCESS_DEBUG_EVENT, ça revient à CREATE_THREAD_DEBUG_EVENT pour le lien primaire. |
LOAD_DLL_DEBUG_EVENT | Le debugger charge un DLL. Vous recevrez cet événement quand le PE Loader résout les premières liaisons du DLL que vous appelez grâce à CreateProcess pour charger le debugger, et au moment où le debugger appelle LoadLibrary. |
UNLOAD_DLL_DEBUG_EVENT | Un DLL est déchargé du processus du debugger. |
EXCEPTION_DEBUG_EVENT | Une exception vient d'arriver dans le process de debugger. Important: Cet événement arrivera uniquement avant que le debugger ne commence à exécuter sa première instruction. L'exception est en réalité une pause debugging (int 3h) = (c'est un Break Point). Quand vous voulez reprendre le debugger, appelez ContinueDebugEvent avec le flag DBG_CONTINUE. N'utilisez pas le flag DBG_EXCEPTION_NOT_HANDLED sinon le debugger refusera de repartir sous NT (sur Win98, ça marche très bien). |
OUTPUT_DEBUG_STRING_EVENT | Cet événement est produit quand le debugger appelle la fonction DebugOutputString pour envoyer une chaîne de caractères en tant que message à votre programme. |
RIP_EVENT | Une erreur de debugging s'est produite. |
dwProcessId et dwThreadId sont les IDs du process et du lien, du process pour lequel un 'debugging événement' se produit. Vous pouvez employer ces valeurs en tant qu'identificateurs du process et du lien auquel vous vous intéressez. Rappelez-vous que si vous employez CreateProcess pour charger le debugger, vous obtenez aussi les IDs du process et du lien du debugger dans la structure PROCESS_INFO. Vous pouvez utiliser ces valeurs pour différencier les événements qui arrive dans le debugger de ceux de ses Child Process (au cas où vous n'auriez pas spécifié le Flag DEBUG_ONLY_THIS_PROCESS).
u est une union qui contient plus d'informations sur l''debugging événement'. ça peut être une des structures suivantes selon la valeur de dwDebugEventCode vu plus haut.
Valeur dans dwDebugEventCode | Interprétation de 'u' |
---|---|
CREATE_PROCESS_DEBUG_EVENT | La structure CREATE_PROCESS_DEBUG_INFO nommée CreateProcessInfo |
EXIT_PROCESS_DEBUG_EVENT | La structure EXIT_PROCESS_DEBUG_INFO nommée ExitProcess |
CREATE_THREAD_DEBUG_EVENT | La structure CREATE_THREAD_DEBUG_INFO nommée CreateThread |
EXIT_THREAD_DEBUG_EVENT | La structure EXIT_THREAD_DEBUG_EVENT nommée ExitThread |
LOAD_DLL_DEBUG_EVENT | La structure LOAD_DLL_DEBUG_INFO nommée LoadDll |
UNLOAD_DLL_DEBUG_EVENT | La structure UNLOAD_DLL_DEBUG_INFO nommée UnloadDll |
EXCEPTION_DEBUG_EVENT | La structure EXCEPTION_DEBUG_INFO nommée Exception |
OUTPUT_DEBUG_STRING_EVENT | La structure OUTPUT_DEBUG_STRING_INFO nommée DebugString |
RIP_EVENT | La structure RIP_INFO nommée RipInfo |
Je n'entrerai pas dans les détails de toutes ces structures dans ce tutorial, on va seulement s'attarder un peu sur la structure CREATE_PROCESS_debug_INFO.
Pour s'assurer que notre programme appelle WaitForDebugEvent et qu'il continue son déroulement ensuite, la première chose à faire est d'examiner la valeur dans dwDebugEventCode pour voir quel type d''debugging événement' est arrivé dans le process du debugger. Par exemple, si la valeur dans dwDebugEventCode est CREATE_PROCESS_DEBUG_EVENT, on peut interpréter le membre dans u comme étant CreateProcessInfo et y accéder grâce à u.CreateProcessInfo .
ContinueDebugEvent proto dwProcessId:DWORD, dwThreadId:DWORD, dwContinueStatus:DWORD
Cette fonction reprend le lien qui a été précédemment suspendu à cause du fait qu'un 'debugging événement' soit arrivé.
dwProcessId et dwThreadId
sont les 'IDs du process et du lien', du lien qui sera repris. On obtient d'habitude ces deux valeurs des membres dwProcessId et dwThreadId de la structure debug_EVENT.
dwContinueStatus indique comment continuer le lien qui a annoncé l''debugging événement'. Il y a deux valeurs possibles: DBG_CONTINUE et DBG_EXCEPTION_NOT_HANDLED. En ce qui concerne tous les autres 'debugging événements', ces deux valeurs font la même chose : reprendre le lien. L'exception est EXCEPTION_debug_EVENT. Si le lien annonce un 'événement exception' de debugging, ça signifie qu'une exception est arrivée dans un lien du debugger. Si vous mettez DBG_CONTINUE ,
Le lien ignorera son propre traitement d'exception et continuera son exécution. Dans ce scénario, votre programme doit examiner et résoudre l'exception lui-même avant de pouvoir reprendre son lien avec DBG_CONTINUE sinon l'exception arrivera de nouveau à maintes reprises....
Si vous mettez DBG_EXCEPTION_NOT_HANDLED, Votre programme indique à Windows qu'il n'a pas récupéré l'handle de l'exception : Windows devra utiliser le manipulateur d'exception par défaut du debugger, pour s'occuper de cette exception.
Pour conclure, si le 'debugging événement' se réfère à une exception dans le processus du debugger, vous devez appeler ContinueDebugEvent avec le flag DBG_CONTINUE si votre programme a déjà éradiqué la cause de l'exception. Autrement, votre programme doit appeler ContinueDebugEvent avec le flag DBG_EXCEPTION_NOT_HANDLED.
Sauf dans un cas, pour lequel vous devez toujours utiliser le flag DBG_CONTINUE: le premier EXCEPTION_debug_EVENT a la valeur EXCEPTION_BREAKPOINT dans le membre ExceptionCode. Quand le debugger va exécuter sa toute première instruction, votre programme recevra un événement d'exception de debugging. C'est en réalité une pause debugging (int 3h) (un Break Point). Si vous répondez en appelant ContinueDebugEvent avec le flag DBG_EXCEPTION_NOT_HANDLED, Windows NT refusera de redémarrer le debugger (parce que personne ne se soucie de ça). Vous devez toujours employer le flag DBG_CONTINUE dans ce cas, pour dire à Windows que vous voulez que le lien reprenne.
.while TRUE
invoke WaitForDebugEvent, addr DebugEvent, INFINITE
.break .if DebugEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
; S'occupe des événements de debugging.
invoke ContinueDebugEvent, DebugEvent.dwProcessId, DebugEvent.dwThreadId,
DBG_EXCEPTION_NOT_HANDLED
.endw
Une fois le programme (à debugger) pris au piège : vous commencez à le debugger, vous ne pouvez plus vous en détacher (en sortir) tant qu'il ne s'est pas terminé (tant qu'on arrive pas à un des ses Exits).
On va récapituler les étapes à nouveau :
Cet exemple debugge un programme win32 et affiche les informations importantes telles que son handle, son ID, son Image Base, etc...
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib
.data
AppName db "Win32 Debug Example no.1",0
ofn OPENFILENAME <>
FilterString db "Executable Files",0,"*.exe",0
db
"All Files",0,"*.*",0,0
ExitProc db "The debuggee exits",0
NewThread db "A new thread is created",0
EndThread db "A thread is destroyed",0
ProcessInfo db "File Handle: %lx ",0dh,0Ah
db "Process
Handle: %lx",0Dh,0Ah
db "Thread
Handle: %lx",0Dh,0Ah
db "Image
Base: %lx",0Dh,0Ah
db "Start
Address: %lx",0
.data?
buffer db 512 dup(?)
startinfo STARTUPINFO <>
pi PROCESS_INFORMATION <>
DBEvent DEBUG_EVENT <>
.code
start:
mov ofn.lStructSize,sizeof ofn
mov ofn.lpstrFilter, offset FilterString
mov ofn.lpstrFile, offset buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER
or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke GetStartupInfo,addr startinfo
invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS,
NULL, NULL, addr startinfo, addr pi
.while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE
.if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
invoke MessageBox, 0, addr ExitProc, addr
AppName, MB_OK+MB_ICONINFORMATION
.break
.elseif DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
invoke wsprintf, addr buffer, addr ProcessInfo,
DBEvent.u.CreateProcessInfo.hFile, DBEvent.u.CreateProcessInfo.hProcess, DBEvent.u.CreateProcessInfo.hThread,
DBEvent.u.CreateProcessInfo.lpBaseOfImage, DBEvent.u.CreateProcessInfo.lpStartAddress
invoke MessageBox,0, addr buffer, addr
AppName, MB_OK+MB_ICONINFORMATION
.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
.if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
invoke ContinueDebugEvent,
DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
.continue
.endif
.elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr NewThread, addr
AppName, MB_OK+MB_ICONINFORMATION
.elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr EndThread, addr
AppName, MB_OK+MB_ICONINFORMATION
.endif
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,
DBG_EXCEPTION_NOT_HANDLED
.endw
invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread
.endif
invoke ExitProcess, 0
end start
Le programme remplit la structure OPENFILENAME et appelle ensuite GetOpenFileName pour laisser l'utilisateur choisir un programme à debugger.
invoke GetStartupInfo,addr startinfo
invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS,
NULL, NULL, addr startinfo, addr pi
Quand l'utilisateur en a choisi un, on appelle CreateProcess pour charger ce programme. on appelle GetStartupInfo pour remplir la structure STARTUPINFO avec ses valeurs par défaut. Remarquez que nous employons debug_PROCESS combiné avec le flag debug_ONLY_THIS_PROCESS pour ne debugger que ce programme, et ne pas toucher à ses Child Process.
.while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE
Quand le debugger est activé, nous entrons dans la boucle de debugging infinie, en appelant WaitForDebugEvent.WaitForDebugEvent ne retournera pas avant qu'un 'debugging événement' ne soit arrivé dans le debugger parce que nous avons mis INFINITE en tant que deuxième paramètre. Quand un 'debugging événement' est arrivé, WaitForDebugEvent retourne et 'DBEvent' (DB-Event) est rempli de l'information avec le 'debugging événement'.
.if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
invoke MessageBox, 0, addr ExitProc, addr
AppName, MB_OK+MB_ICONINFORMATION
.break
Nous vérifions d'abord la valeur dans dwDebugEventCode. Si c'est EXIT_PROCESS_DEBUG_EVENT, nous affichons une MessageBox disant "The debuggee exits" et ensuite on sort de la boucle de debugging.
.elseif DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
invoke wsprintf, addr buffer, addr ProcessInfo,
DBEvent.u.CreateProcessInfo.hFile, DBEvent.u.CreateProcessInfo.hProcess, DBEvent.u.CreateProcessInfo.hThread,
DBEvent.u.CreateProcessInfo.lpBaseOfImage, DBEvent.u.CreateProcessInfo.lpStartAddress
invoke MessageBox,0, addr buffer, addr
AppName, MB_OK+MB_ICONINFORMATION
Si la valeur dans dwDebugEventCode est CREATE_PROCESS_DEBUG_EVENT, alors nous affichons plusieurs informations intéressantes à propos du debuggee dans une MessageBox. Nous obtenons ces informations de u.CreateProcessInfo. CreateProcessInfo est une structure de type CREATE_PROCESS_DEBUG_INFO. Vous pouvez obtenir plus de renseignements sur cette structure en regardant votre référence Win32 API.
.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
.if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
invoke ContinueDebugEvent,
DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
.continue
.endif
Si la valeur dans dwDebugEventCode est EXCEPTION_DEBUG_EVENT, nous devons vérifier plus loin si le type d'exception est exact. C'est une longue ligne de références emboîtées mais vous pouvez obtenir le type d'exception grâce au membre ExceptionCode. Si la valeur dans ExceptionCode est EXCEPTION_BREAKPOINT et que c'est la première fois (ou bien que nous sommes sûrs que le debugger ne s'est jamais servit de l'interruption 3H du Dos (int 3h)), alors nous pouvons dire sans risque que cette exception s'est produite lorsque le debugger a voulut exécuter sa toute première instruction. Quand on en a fini avec le traitement de cette exception, nous devons appeler ContinueDebugEvent avec le Flag DBG_CONTINUE pour permettre au debugger de reprendre là où il s'était arrêté. Après ça, nous retournons pour attendre le prochain 'debugging événement'.
.elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr NewThread, addr
AppName, MB_OK+MB_ICONINFORMATION
.elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr EndThread, addr
AppName, MB_OK+MB_ICONINFORMATION
.endif
Si la valeur dans dwDebugEventCode est CREATE_THREAD_DEBUG_EVENT ou bien EXIT_THREAD_DEBUG_EVENT, on affiche une MessageBox disant que le lien est créé ou bien détruit.
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLEDExcepté pour le cas EXCEPTION_debug_EVENT, vu au-dessus, nous appelons ContinueDebugEvent avec le flag DBG_EXCEPTION_NOT_HANDLED pour reprendre le cours du debugger.
invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread
Quand le debugger Exite, nous sommes déjà en dehors de la boucle de debugging et devons fermer à la fois le processus du debugger et les handles de ses liens. La simple fermeture de ses handles ne signifie pas que nous détruisons le rapport process/lien. Ça signifie seulement que nous ne souhaitons plus désormais employer ces handles pour faire la relation entre process et lien.