Tutorial 28: Win32 Debug API Partie 1/3

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.

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

  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 événements de debugging.. 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 avant que l'événement "attendu" n'arrive. Dans ce cas, elle attend que des 'debugging événements' soient envoyé par Windows. On va voir sa 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' 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:

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

  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 d'arrivé dans le processus du debugger ou bien 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 messages avec Windows : vous choisissez d'en traiter certains mais on en ignore d'autres.
  5. Laissez le debugger continuer son déroulement. Quand un 'debugging événement' arrive, Windows suspend le debugger. Quand vous avez terminé de traiter cet événement, vous avez besoin de le relancer (le debugger) 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 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.

  6. On continue ce cycle indéfiniment tant que le debugger ne trombe pas sur un Exit Process . Votre programme 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 événements de debugging arrivent.
  3. On fait ce qu'on veut (des réponses) des événements de debugging..
  4. Laissez le debugger 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'Exit du processus 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 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_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 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.



[Iczelion's Win32 Assembly Homepage]


Traduit par Morgatte