Partager via


Débogage d’un interblocage

Lorsqu’un thread a besoin d’un accès exclusif au code ou à une autre ressource, il demande un verrou. S’il le peut, Windows répond en donnant ce verrou au thread. À ce stade, rien d’autre dans le système ne peut accéder au code verrouillé. Cela se produit tout le temps et fait partie normale de toute application multithread bien écrite. Bien qu’un segment de code particulier ne puisse avoir qu’un seul verrou à la fois, plusieurs segments de code peuvent chacun avoir leur propre verrou.

Un interblocage se produit lorsque deux ou plusieurs threads ont demandé des verrous sur au moins deux ressources, dans une séquence incompatible. Par instance, supposons que Thread One a acquis un verrou sur la ressource A, puis demande l’accès à la ressource B. Pendant ce temps, thread 2 a acquis un verrou sur la ressource B, puis demande l’accès à la ressource A. Aucun des threads ne peut continuer jusqu’à ce que le verrou de l’autre thread soit abandonné et, par conséquent, aucun thread ne peut continuer.

Des interblocages en mode utilisateur se produisent lorsque plusieurs threads, généralement d’une seule application, se sont bloqués mutuellement l’accès à la même ressource. Toutefois, plusieurs threads de plusieurs applications peuvent également bloquer mutuellement l’accès à une ressource globale/partagée, telle qu’un événement global ou un sémaphore.

Des interblocages en mode noyau se produisent lorsque plusieurs threads (provenant du même processus ou de processus distincts) ont bloqué l’accès des autres threads à la même ressource de noyau.

La procédure utilisée pour déboguer un interblocage varie selon que le blocage se produit en mode utilisateur ou en mode noyau.

Débogage d’un interblocage User-Mode

Lorsqu’un interblocage se produit en mode utilisateur, utilisez la procédure suivante pour le déboguer :

  1. Émettez l’extension !ntsdexts.locks . En mode utilisateur, vous pouvez simplement taper !locks à l’invite du débogueur ; Le préfixe ntsdexts est supposé.

  2. Cette extension affiche toutes les sections critiques associées au processus actuel, ainsi que l’ID du thread propriétaire et le nombre de verrous pour chaque section critique. Si une section critique a un nombre de verrous égal à zéro, elle n’est pas verrouillée. Utilisez la commande ~ (État du thread) pour afficher des informations sur les threads qui possèdent les autres sections critiques.

  3. Utilisez la commande Kb (Display Stack Backtrace) pour chacun de ces threads afin de déterminer s’ils sont en attente d’autres sections critiques.

  4. À l’aide de la sortie de ces commandes kb , vous pouvez trouver l’interblocage : deux threads qui sont chacun en attente sur un verrou tenu par l’autre thread. Dans de rares cas, un interblocage peut être dû à plus de deux threads tenant des verrous dans un modèle circulaire, mais la plupart des interblocages impliquent seulement deux threads.

Voici une illustration de cette procédure. Vous commencez par l’extension !ntdexts.locks :

0:006>  !locks 
CritSec ftpsvc2!g_csServiceEntryLock+0 at 6833dd68
LockCount          0
RecursionCount     1
OwningThread       a7
EntryCount         0
ContentionCount    0
*** Locked

CritSec isatq!AtqActiveContextList+a8 at 68629100
LockCount          2
RecursionCount     1
OwningThread       a3
EntryCount         2
ContentionCount    2
*** Locked

CritSec +24e750 at 24e750
LockCount          6
RecursionCount     1
OwningThread       a9
EntryCount         6
ContentionCount    6
*** Locked

La première section critique affichée n’a pas de verrous et peut donc être ignorée.

La deuxième section critique affichée a un nombre de verrous de 2 et est donc une cause possible d’un blocage. Le thread propriétaire a un ID de thread de 0xA3.

Vous pouvez trouver ce thread en répertoriant tous les threads avec la commande ~ (État du thread) et en recherchant le thread avec cet ID :

0:006>  ~
   0  Id: 1364.1330 Suspend: 1 Teb: 7ffdf000 Unfrozen
   1  Id: 1364.17e0 Suspend: 1 Teb: 7ffde000 Unfrozen
   2  Id: 1364.135c Suspend: 1 Teb: 7ffdd000 Unfrozen
   3  Id: 1364.1790 Suspend: 1 Teb: 7ffdc000 Unfrozen
   4  Id: 1364.a3 Suspend: 1 Teb: 7ffdb000 Unfrozen
   5  Id: 1364.1278 Suspend: 1 Teb: 7ffda000 Unfrozen
.  6  Id: 1364.a9 Suspend: 1 Teb: 7ffd9000 Unfrozen
   7  Id: 1364.111c Suspend: 1 Teb: 7ffd8000 Unfrozen
   8  Id: 1364.1588 Suspend: 1 Teb: 7ffd7000 Unfrozen

Dans cet affichage, le premier élément est le numéro de thread interne du débogueur. Le deuxième élément (le Id champ) contient deux nombres hexadécimaux séparés par une virgule décimale. Le nombre avant la virgule décimale est l’ID de processus ; le nombre après la virgule décimale est l’ID de thread. Dans cet exemple, vous voyez que l’ID de thread 0xA3 correspond au numéro de thread 4.

Vous utilisez ensuite la commande Kb (Display Stack Backtrace) pour afficher la pile qui correspond au thread numéro 4 :

0:006>  ~4 kb
  4  id: 97.a3   Suspend: 0 Teb 7ffd9000 Unfrozen
ChildEBP RetAddr  Args to Child
014cfe64 77f6cc7b 00000460 00000000 00000000 ntdll!NtWaitForSingleObject+0xb
014cfed8 77f67456 0024e750 6833adb8 0024e750 ntdll!RtlpWaitForCriticalSection+0xaa 
014cfee0 6833adb8 0024e750 80000000 01f21cb8 ntdll!RtlEnterCriticalSection+0x46
014cfef4 6833ad8f 01f21cb8 000a41f0 014cff20 ftpsvc2!DereferenceUserDataAndKill+0x24
014cff04 6833324a 01f21cb8 00000000 00000079 ftpsvc2!ProcessUserAsyncIoCompletion+0x2a
014cff20 68627260 01f21e0c 00000000 00000079 ftpsvc2!ProcessAtqCompletion+0x32
014cff40 686249a5 000a41f0 00000001 686290e8 isatq!I_TimeOutContext+0x87
014cff5c 68621ea7 00000000 00000001 0000001e isatq!AtqProcessTimeoutOfRequests_33+0x4f
014cff70 68621e66 68629148 000ad1b8 686230c0 isatq!I_AtqTimeOutWorker+0x30
014cff7c 686230c0 00000000 00000001 000c000a isatq!I_AtqTimeoutCompletion+0x38
014cffb8 77f04f2c 00000000 00000001 000c000a isatq!SchedulerThread_297+0x2f
00000001 000003e6 00000000 00000001 000c000a kernel32!BaseThreadStart+0x51

Notez que ce thread a un appel à la fonction WaitForCriticalSection , ce qui signifie qu’il a non seulement un verrou, mais qu’il attend du code verrouillé par quelque chose d’autre. Nous pouvons déterminer la section critique que nous attendons en examinant le premier paramètre de l’appel à WaitForCriticalSection. Il s’agit de la première adresse sous Args to Child : « 24e750 ». Par conséquent, ce thread attend la section critique à l’adresse 0x24E750. Il s’agissait de la troisième section critique répertoriée par l’extension !locks que vous avez utilisée précédemment.

En d’autres termes, le thread 4, qui possède la deuxième section critique, attend la troisième section critique. Maintenant, concentrez-vous sur la troisième section critique, qui est également verrouillée. Le thread propriétaire a l’ID de thread 0xA9. Pour revenir à la sortie de la ~ commande que vous avez vue précédemment, notez que le thread avec cet ID est le thread numéro 6. Affichez le retour arrière de la pile pour ce thread :

0:006>  ~6 kb 
ChildEBP RetAddr  Args to Child
0155fe38 77f6cc7b 00000414 00000000 00000000 ntdll!NtWaitForSingleObject+0xb
0155feac 77f67456 68629100 6862142e 68629100 ntdll!RtlpWaitForCriticalSection+0xaa 
0155feb4 6862142e 68629100 0009f238 686222e1 ntdll!RtlEnterCriticalSection+0x46
0155fec0 686222e1 0009f25c 00000001 0009f238 isatq!ATQ_CONTEXT_LISTHEAD__RemoveFromList
0155fed0 68621412 0009f238 686213d1 0009f238 isatq!ATQ_CONTEXT__CleanupAndRelease+0x30
0155fed8 686213d1 0009f238 00000001 01f26bcc isatq!AtqpReuseOrFreeContext+0x3f
0155fee8 683331f7 0009f238 00000001 01f26bf0 isatq!AtqFreeContext+0x36
0155fefc 6833984b ffffffff 00000000 00000000 ftpsvc2!ASYNC_IO_CONNECTION__SetNewSocket
0155ff18 6833adcd 77f05154 01f26a58 00000000 ftpsvc2!USER_DATA__Cleanup+0x47
0155ff28 6833ad8f 01f26a58 000a3410 0155ff54 ftpsvc2!DereferenceUserDataAndKill+0x39
0155ff38 6833324a 01f26a58 00000000 00000040 ftpsvc2!ProcessUserAsyncIoCompletion+0x2a
0155ff54 686211eb 01f26bac 00000000 00000040 ftpsvc2!ProcessAtqCompletion+0x32
0155ff88 68622676 000a3464 00000000 000a3414 isatq!AtqpProcessContext+0xa7
0155ffb8 77f04f2c abcdef01 ffffffff 000ad1b0 isatq!AtqPoolThread+0x32
0155ffec 00000000 68622644 abcdef01 00000000 kernel32!BaseThreadStart+0x51

Ce thread, lui aussi, attend qu’une section critique soit libérée. Dans ce cas, il attend la section critique à 0x68629100. Il s’agissait de la deuxième section critique de la liste générée précédemment par l’extension !locks .

C’est l’interblocage. Le thread 4, qui possède la deuxième section critique, attend la troisième section critique. Le thread 6, qui possède la troisième section critique, attend la deuxième section critique.

Après avoir confirmé la nature de ce blocage, vous pouvez utiliser les techniques de débogage habituelles pour analyser les threads 4 et 6.

Débogage d’un interblocage Kernel-Mode

Il existe plusieurs extensions de débogueur qui sont utiles pour le débogage des interblocages en mode noyau :

  • L’extension !kdexts.locks affiche des informations sur tous les verrous conservés sur les ressources du noyau et les threads contenant ces verrous. (En mode noyau, vous pouvez simplement taper !locks à l’invite du débogueur ; le préfixe kdexts est supposé.)

  • L’extension !qlocks affiche l’état de tous les verrous de rotation mis en file d’attente.

  • L’extension !wdfkd.wdfspinlock affiche des informations sur un objet de verrouillage tournant Kernel-Mode Driver Framework (KMDF).

  • L’extension !deadlock est utilisée conjointement avec driver Verifier pour détecter l’utilisation incohérente de verrous dans votre code susceptibles de provoquer des interblocages.

Lorsqu’un interblocage se produit en mode noyau, utilisez l’extension !kdexts.locks pour répertorier tous les verrous actuellement acquis par les threads.

Vous pouvez généralement identifier l’interblocage en recherchant un thread non exécuté qui contient un verrou exclusif sur une ressource requise par un thread en cours d’exécution. La plupart des verrous sont partagés.