Tutorial 28: Win32 Debug API Partie 1/3

Dans ce tutorial, vous allons voir ce que Win32 offre comme mises au point primaires pour les développeurs. Lorsque vous en aurez terminés avec ce tutorial, vous saurez comment debugger un programme.
Downloadez l'exemple.

Théorie:

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 celle-ci, 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 :

  1. Créez un process (dans W32Dasm c'est: Load Process) (Dans SoftIce c'est son: Loader) ou attachez votre programme à un process en cours de fonctionnement (Dans W32Dasm c'est: Attach to an Active Process) (Dans SoftIce c'est: CTRL+d). C'est la première étape de l'utilisation des 'Win32 debug APIs'. Puisque votre programme agira comme un DEBUGGER, vous aurez besoin d'un programme sur lequel faire vos essais. Un programme étant en train de se faire débugger (la cible) est appelé un debuggee (non je ne me suis pas trompé). Vous pouvez lancer une 'action debuggee' (action qui fait en sorte qu'on debug la cible, le debuggee) de deux façons :
  2. Attendez les 'debugging événements'. Après que votre programme n'ait acquis sa fonction de debugger, le lien primaire du debuggee est suspendu et continuera de l'être jusqu'à ce que vous appeliez WaitForDebugEvent dans votre programme. Cette fonction marche comme d'autres WaitForXXX fonctionnent, c'est-à-dire. Elle bloque le lien appelant tant que l'événement "attendu" ne se produise. Dans ce cas, elle attend que des 'debugging événements' soient envoyés par Windows. On va voir leur définition :

    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' quand un événement particulier est en train de se passer dans le debuggee.

    dwMilliseconds est le temps en millisecondes que cette fonction attendra qu'un 'debugging événement' ne se produise. 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 produit.

    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 (l'information) qui indique quel type de 'debugging événement' s'est produit dans la cible. 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:

  3. Valeurs Significations
    CREATE_PROCESS_DEBUG_EVENT Un process est créé. Cet événement sera envoyé lorsque le process du debuggee 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 process du debuggee 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 debuggee est déjà créé.
    EXIT_THREAD_DEBUG_EVENT Un lien dans le debuggee vient de se terminer. Votre programme ne recevra pas cet événement pour le lien primaire. Bref, vous pouvez penser que le lien primaire du debuggee est un équivalent du process du debuggee 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 debuggee charge une DLL. Vous recevrez cet événement quand le PE Loader résout les premiers liens de la DLL (vous appelez CreateProcess pour charger le debuggee), et au moment où le debuggee appelle LoadLibrary.
    UNLOAD_DLL_DEBUG_EVENT Une DLL est déchargé du process du debuggee.
    EXCEPTION_DEBUG_EVENT Une exception vient d'arriver dans le process du debuggee. Important: Cet événement arrivera uniquement avant que le debuggee 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 cours du debuggee, appelez ContinueDebugEvent avec le flag DBG_CONTINUE. N'utilisez pas le flag DBG_EXCEPTION_NOT_HANDLED sinon le debuggee refusera de repartir sous NT (sur Win98, ça marche très bien).
    OUTPUT_DEBUG_STRING_EVENT Cet événement est produit quand le debuggee 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 programme dans lequel un 'debugging événement' s'est 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 debuggee, vous obtenez aussi les IDs du process et du lien du debuggee dans la structure PROCESS_INFO. Vous pouvez utiliser ces valeurs pour différencier les événements qui se produisent dans le debuggee lui-même de ceux qui se produisent dans 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 à propos du '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 de retrouver quel 'debugging événement' s'est produit dans la cible'. Cette événement a été placé dans le membre dwDebugEventCode donc il suffit d'examiner la valeur dans dwDebugEventCode pour voir quel type de 'debugging événement' s'est produit dans le process du debuggee. 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 .

  4. Faites ce que vous voulez des réponses d'un 'debugging événement'. Lorsqu'on a un retour de WaitForDebugEvent, ça signifie qu'un 'debugging événement' vient juste de se dérouler dans le process du debuggee ou bien c'est que le temps d'attente s'est écoulé sans que rien ne se soit passé. Votre programme a besoin d'examiner la valeur dans dwDebugEventCode pour réagir convenablement à cet événement. À cet égard, ça se passe comme pour le traitement des 'Window Messages' (ex: on vient de détecter le déplacement de la souris, frappe clavier...) : vous choisissez d'en traiter certains mais on en ignore d'autres.
  5. Laissez le debuggee continuer son déroulement. Quand un 'debugging événement' se produit, Windows suspend le debuggee. Mais dès que vous avez terminé de traiter cet événement, vous avez besoin de le relancer (le debuggee) de nouveau, là où il s'était arrêté. On fait ça en appelant la fonction ContinueDebugEvent.

    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 programme cible qui va enfin être relancé. 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é le '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 s'est produite dans un lien du debuggee. 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 debuggee, pour s'occuper de cette exception.
    Pour conclure, si le 'debugging événement' se réfère à une exception dans le process du debuggee, 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 debuggee 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 debuggee (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.

  6. On continue ce cycle indéfiniment tant que le debuggee ne tombe pas sur son propre Exit Process . Votre programme (le debugger) doit présenter une boucle infinie un peu comme une boucle de message. La boucle ressemble à ça :

    .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 :

  1. On Créer un process ou on attache notre debugger à un process (un programme) en cours d'exécution..
  2. On attend que des 'debugging événements' ne se produisent.
  3. On fait ce qu'on veut (des réponses) des 'debugging événements'.
  4. Laissez le debuggee reprendre son exécution..
  5. On continue ce cycle (les points 2,3,4 vus précédemment) dans une boucle infinie tant qu'on ne tombe pas sur l'ExitProcess du debuggee.

Exemple:

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

Analyse:

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 debuggee est chargé, notre debugger entre dans sa boucle de debugging infinie, en appelant WaitForDebugEvent. WaitForDebugEvent ne retournera pas avant qu'un 'debugging événement' ne se soit produit dans le debuggee parce que nous avons mis INFINITE en tant que deuxième paramètre. Quand un 'debugging événement' se produit, WaitForDebugEvent retourne et 'DBEvent' (DB-Event) est rempli de l'information représentant le 'debugging événement'. (Par exemple: CREATE_PROCESS_DEBUG_EVENT, ou EXIT_THREAD_DEBUG_EVENT...)

   .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" (le debugee s'est arrêté car il est tombé sur sa fonction de terminaison ExitProcess) et ensuite on sort de la boucle de debugging (du debugger).

   .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 certains que le debuggee ne s'est jamais servit de l'interruption 3 du Dos (int 03h)), alors nous pouvons dire sans risque que cette exception s'est produite lorsque le debuggee 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 debuggee 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_HANDLED
.endw

Excepté pour le cas EXCEPTION_debug_EVENT, vu au-dessus, nous appelons ContinueDebugEvent avec le flag DBG_EXCEPTION_NOT_HANDLED pour reprendre le cours du debuggee.

invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread

Quand le debuggee se referme, nous sommes déjà en dehors de la boucle de debugging (de notre debugger)et devons fermer à la fois le process du debuggee 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.



[Iczelion's Win32 Assembly Homepage]


Traduit par Morgatte