Pourquoi trois threads de travail inattendus existent-ils au démarrage d’une application console Win32?

Voici la capture d’écran de la situation!

Voici la capture d'écran!

J’ai créé une application console Visual C ++ Win32 avec VS2010. Lorsque j’ai lancé l’application, j’ai constaté qu’il y avait quatre threads: un “Thread principal” et trois threads de travail (je n’ai écrit aucun code).

Je ne sais pas d’où ces trois fils de travail sont venus.
Je voudrais savoir le rôle de ces trois fils.

Merci d’avance!

Dans Windows 10, une nouvelle méthode de chargement des DLL a été mise en œuvre. À présent, dans ce processus, plusieurs threads de travail ( LdrpWorkCallback ) sont LdrpWorkCallback . tous les processus Windows 10 ont maintenant plusieurs threads

avant le système win10 (ntdll.dll), chargez toujours les DLL dans un seul thread. mais à partir de win10, ce comportement a changé. maintenant “chargeur parallèle” existe dans ntdll. cette conception pour “paralléliser” le processus de chargement de dll. maintenant certaines tâches ( NTSTATUS LdrpSnapModule(LDRP_LOAD_CONTEXT* LoadContext) ) peuvent être exécutées dans des threads de travail. presque chaque DLL est imscope (DLL de dépendance). Ainsi, lorsque la DLL est chargée – les DLL de dépendance sont également chargées et ce processus est récursif (les DLL de dépendance ont leurs propres dépendances). fonction void LdrpMapAndSnapDependency(LDRP_LOAD_CONTEXT* LoadContext) marche par le tableau d’importation de DLL chargé et le charge directement (niveau 1-st) dépendant des DLL appelez LdrpLoadDependentModule (en interne, LdrpLoadDependentModule appelez LdrpMapAndSnapDependency pour la nouvelle DLL chargée – pour que ce processus soit récursif). et finalement, LdrpMapAndSnapDependency doit appeler NTSTATUS LdrpSnapModule(LDRP_LOAD_CONTEXT* LoadContext) pour une importation de liaison vers des DLL déjà chargées. LdrpSnapModule exécuté pour de nombreuses DLL dans le processus de chargement de DLL de niveau supérieur. et ce processus est indépendant pour chaque DLL – c’est donc un bon point pour la parallélisation … ( LdrpSnapModule dans la plupart des cas, ne charge pas de nouvelles DLL, mais ne lie que l’importation à l’exportation déjà chargée. mais si l’importation est résolue en exportation transférée (cela se produit rarement) – nouvelle DLL transmise, chargée.)

quelques détails d’implémentation en cours:


1) tout d’abord, regardons la struct _RTL_USER_PROCESS_PARAMETERS nouveau champ – ULONG LoaderThreads; existe maintenant dans struct. Cet effet LoaderThreads (s’il est défini sur une valeur différente de zéro) LoaderThreads / désactive le “chargeur parallèle” dans le nouveau processus. lorsque nous créons un nouveau processus par ZwCreateUserProcess – le neuvième argument est PRTL_USER_PROCESS_PARAMETERS ProcessParameters . mais si nous utilisons CreateProcess[Internal]W – nous ne pouvons pas transmettre directement PRTL_USER_PROCESS_PARAMETERS – uniquement STARTUPINFO . comment facile peut visualiser RTL_USER_PROCESS_PARAMETERS partielle initialisée à partir de STARTUPINFO , mais ULONG LoaderThreads; nous ne contrôlons pas. et ce sera toujours zéro (si nous ZwCreateUserProcess pas ZwCreateUserProcess ou ne ZwCreateUserProcess pas hook à cette routine)

2.) dans la nouvelle phase d’initialisation du processus, LdrpInitializeExecutionOptions est appelée (à partir de LdrpInitializeProcess ). cette routine a examiné les HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\ pour plusieurs valeurs (si la sous-clé existe – généralement, elle n’existe pas), y compris MaxLoaderThreads ( REG_DWORD ) – si MaxLoaderThreads existe – valeur à partir de celui-ci, remplace RTL_USER_PROCESS_PARAMETERS.LoaderThreads .

3.) LdrpCreateLoaderEvents() appelé. cette routine doit créer 2 événements globaux HANDLE LdrpWorkCompleteEvent, LdrpLoadCompleteEvent; ces événements utilisés pour la synchronisation.

 NTSTATUS LdrpCreateLoaderEvents() { NTSTATUS status = ZwCreateEvent(&LdrpWorkCompleteEvent, EVENT_ALL_ACCESS, 0, SynchronizationEvent, TRUE); if (0 <= status) { status = ZwCreateEvent(&LdrpLoadCompleteEvent, EVENT_ALL_ACCESS, 0, SynchronizationEvent, TRUE); } return status; } 

4) de LdrpInitializeProcess appelé void LdrpDetectDetour() . pense que ce nom parle pour lui-même. il ne retourne pas de valeur mais initialise la variable globale BOOLEAN LdrpDetourExist . cette routine vérifie d'abord certaines routines critiques du chargeur est accrochée - maintenant, il s'agit de 5 routines

 "NtOpenFile", "NtCreateSection", "NtQueryAtsortingbutesFile", "NtOpenSection", "NtMapViewOfSection", 

si oui - LdrpDetourExist = TRUE; si non connecté - ThreadDynamicCodePolicyInfo interrogé - code complet:

 void LdrpDetectDetour() { if (LdrpDetourExist) return ; static PVOID LdrpCriticalLoaderFunctions[] = { NtOpenFile, NtCreateSection, ZwQueryAtsortingbutesFile, ZwOpenSection, ZwMapViewOfSection, }; static M128A LdrpThunkSignature[5] = { //*** }; ULONG n = RTL_NUMBER_OF(LdrpCriticalLoaderFunctions); M128A* ppv = (M128A*)LdrpCriticalLoaderFunctions; M128A* pps = LdrpThunkSignature; do { if (ppv->Low != pps->Low || ppv->High != pps->High) { if (LdrpDebugFlags & 5) { DbgPrint("!!! Detour detected, disable parallel loading\n"); LdrpDetourExist = TRUE; return; } } } while (pps++, ppv++, --n); BOOL DynamicCodePolicy; if (0 <= ZwQueryInformationThread(NtCurrentThread(), ThreadDynamicCodePolicyInfo, &DynamicCodePolicy, sizeof(DynamicCodePolicy), 0)) { if (LdrpDetourExist = (DynamicCodePolicy == 1)) { if (LdrpMapAndSnapWork) { WaitForThreadpoolWorkCallbacks(LdrpMapAndSnapWork, TRUE);//TpWaitForWork TpReleaseWork(LdrpMapAndSnapWork);//CloseThreadpoolWork LdrpMapAndSnapWork = 0; TpReleasePool(LdrpThreadPool);//CloseThreadpool LdrpThreadPool = 0; } } } } 

5) à partir de LdrpInitializeProcess appelé NTSTATUS LdrpEnableParallelLoading (ULONG LoaderThreads) - en tant que LdrpEnableParallelLoading(ProcessParameters->LoaderThreads)

 NTSTATUS LdrpEnableParallelLoading (ULONG LoaderThreads) { LdrpDetectDetour(); if (LoaderThreads) { LoaderThreads = min(LoaderThreads, 16);// not more than 16 threads allowed if (LoaderThreads <= 1) return STATUS_SUCCESS; } else { if (RtlGetSuiteMask() & 0x10000) return STATUS_SUCCESS; LoaderThreads = 4;// default for 4 threads } if (LdrpDetourExist) return STATUS_SUCCESS; NTSTATUS status = TpAllocPool(&LdrpThreadPool, 1);//CreateThreadpool if (0 <= status) { TpSetPoolWorkerThreadIdleTimeout(LdrpThreadPool, -300000000);// 30 second idle timeout TpSetPoolMaxThreads(LdrpThreadPool, LoaderThreads - 1);//SetThreadpoolThreadMaximum TP_CALLBACK_ENVIRON CallbackEnviron = { }; CallbackEnviron->CallbackPriority = TP_CALLBACK_PRIORITY_NORMAL; CallbackEnviron->Size = sizeof(TP_CALLBACK_ENVIRON); CallbackEnviron->Pool = LdrpThreadPool; CallbackEnviron->Version = 3; status = TpAllocWork(&LdrpMapAndSnapWork, LdrpWorkCallback, 0, &CallbackEnviron);//CreateThreadpoolWork } return status; } 

pool de chargeur spécial ainsi créé - LdrpThreadPool avec LoaderThreads - 1 threads max. définir le délai d'inactivité dans les 30 secondes (après la sortie du thread) et allouer PTP_WORK LdrpMapAndSnapWork qui a ensuite utilisé la void LdrpQueueWork(LDRP_LOAD_CONTEXT* LoadContext)

6) toutes les variables globales utilisées par le chargeur parallèle

 HANDLE LdrpWorkCompleteEvent, LdrpLoadCompleteEvent; CRITICAL_SECTION LdrpWorkQueueLock; LIST_ENTRY LdrpWorkQueue = { &LdrpWorkQueue, &LdrpWorkQueue }; ULONG LdrpWorkInProgress; BOOLEAN LdrpDetourExist; PTP_POOL LdrpThreadPool; PTP_WORK LdrpMapAndSnapWork; enum DRAIN_TASK { WaitLoadComplete, WaitWorkComplete }; struct LDRP_LOAD_CONTEXT { UNICODE_STRING BaseDllName; PVOID somestruct; ULONG Flags;//some unknown flags NTSTATUS* pstatus; //final status of load _LDR_DATA_TABLE_ENTRY* ParentEntry; // of 'parent' loading dll _LDR_DATA_TABLE_ENTRY* Entry; // this == Entry->LoadContext LIST_ENTRY WorkQueueListEntry; _LDR_DATA_TABLE_ENTRY* ReplacedEntry; _LDR_DATA_TABLE_ENTRY** pvImports;// in same ordef as in IMAGE_IMPORT_DESCRIPTOR piid ULONG ImportDllCount;// count of pvImports LONG TaskCount; PVOID pvIAT; ULONG SizeOfIAT; ULONG CurrentDll; // 0 <= CurrentDll < ImportDllCount PIMAGE_IMPORT_DESCRIPTOR piid; ULONG OriginalIATProtect; PVOID GuardCFCheckFunctionPointer; PVOID* pGuardCFCheckFunctionPointer; }; 

* Malheureusement, LDRP_LOAD_CONTEXT n'est pas développé dans les fichiers .pdb. donc seulement des définitions partielles avec mes noms (ne sais pas l'original)

 struct { ULONG MaxWorkInProgress;//4 - values from explorer.exe at some moment ULONG InLoaderWorker;//7a (this mean LdrpSnapModule called from worker thread) ULONG InLoadOwner;//87 (LdrpSnapModule called direct, in same thread as `LdrpMapAndSnapDependency`) } LdrpStatistics; // for statistics void LdrpUpdateStatistics() { LdrpStatistics.MaxWorkInProgress = max(LdrpStatistics.MaxWorkInProgress, LdrpWorkInProgress); NtCurrentTeb()->LoaderWorker ? LdrpStatistics.InLoaderWorker++ : LdrpStatistics.InLoadOwner++ } 

dans TEB.CrossTebFlags - existent maintenant 2 nouveaux drapeaux:

 USHORT LoadOwner : 01; // 0x1000; USHORT LoaderWorker : 01; // 0x2000; 

2 derniers bits sont disponibles ( USHORT SpareSameTebBits : 02; // 0xc000 )

7) dans le code suivant LdrpMapAndSnapDependency(LDRP_LOAD_CONTEXT* LoadContext)

 LDR_DATA_TABLE_ENTRY* Entry = LoadContext->CurEntry; if (LoadContext->pvIAT) { Entry->DdagNode->State = LdrModulesSnapping; if (LoadContext->PrevEntry)// if recursive call { LdrpQueueWork(LoadContext); // !!! } else { status = LdrpSnapModule(LoadContext); } } else { Entry->DdagNode->State = LdrModulesSnapped; } 

Donc, si LoadContext->PrevEntry (disons que nous chargeons user32.dll . dans le premier appel LdrpMapAndSnapDependency LoadContext->PrevEntry sera toujours 0 (lorsque CurEntry pointe vers user32.dll ), mais lorsque nous appelons de manière récursive LdrpMapAndSnapDependency pour sa dépendance gdi32.dll - PrevEntry sera pour user32.dll et CurEntry pour gdi32.dll ) nous LdrpSnapModule(LoadContext); pas directement LdrpSnapModule(LoadContext); mais LdrpQueueWork(LoadContext);

8) LdrpQueueWork est assez simple:

 void LdrpQueueWork(LDRP_LOAD_CONTEXT* LoadContext) { if (0 <= ctx->pstatus) { EnterCriticalSection(&LdrpWorkQueueLock); InsertHeadList(&LdrpWorkQueue, &LoadContext->WorkQueueListEntry); LeaveCriticalSection(&LdrpWorkQueueLock); if (LdrpMapAndSnapWork && !RtlGetCurrentPeb()->Ldr->ShutdownInProgress) { SubmitThreadpoolWork(LdrpMapAndSnapWork);//TpPostWork } } } 

nous insérons LoadContext dans LdrpWorkQueue et si "Parallel Loader" est démarré ( LdrpMapAndSnapWork != 0 ) et non pas ShutdownInProgress - nous soumettons le travail au pool de chargement. mais même si le pool n'est pas initialisé (par exemple, les détours existent) - il n'y aura pas d'erreur - nous traitons cette tâche dans LdrpDrainWorkQueue

9) dans le rappel du thread de travail exécuté:

 void LdrpWorkCallback() { if (LdrpDetourExist) return; EnterCriticalSection(&LdrpWorkQueueLock); PLIST_ENTRY Entry = RemoveEntryList(&LdrpWorkQueue); if (Entry != &LdrpWorkQueue) { ++LdrpWorkInProgress; LdrpUpdateStatistics() } LeaveCriticalSection(&LdrpWorkQueueLock); if (Entry != &LdrpWorkQueue) { LdrpProcessWork(CONTAINING_RECORD(Entry, LDRP_LOAD_CONTEXT, WorkQueueListEntry), FALSE); } } 

nous LdrpWorkQueue simplement une entrée à partir de LdrpWorkQueue , la convertissons en LDRP_LOAD_CONTEXT* ( CONTAINING_RECORD(Entry, LDRP_LOAD_CONTEXT, WorkQueueListEntry) ) et appelons void LdrpProcessWork(LDRP_LOAD_CONTEXT* LoadContext, BOOLEAN LoadOwner) CONTAINING_RECORD(Entry, LDRP_LOAD_CONTEXT, WorkQueueListEntry) ) et void LdrpProcessWork(LDRP_LOAD_CONTEXT* LoadContext, BOOLEAN LoadOwner)

10) void LdrpProcessWork(LDRP_LOAD_CONTEXT* ctx, BOOLEAN LoadOwner) dans l’appel général LdrpSnapModule(LoadContext) et à la fin du code suivant:

 if (!LoadOwner) { EnterCriticalSection(&LdrpWorkQueueLock); BOOLEAN bSetEvent = --LdrpWorkInProgress == 1 && IsListEmpty(&LdrpWorkQueue); LeaveCriticalSection(&LdrpWorkQueueLock); if (bSetEvent) ZwSetEvent(LdrpWorkCompleteEvent, 0); } 

donc, si nous ne LoadOwner pas LoadOwner (dans le fil travaillé), nous décrémentons LdrpWorkInProgress et si LdrpWorkQueue est vide, LdrpWorkCompleteEvent ( LoadOwner peut attendre).

11) et enfin LdrpDrainWorkQueue appelé à partir de LoadOwner (thread principal) pour "drainer" WorkQueue. il peut éventuellement LdrpQueueWork et exécuter directement des tâches poussées vers LdrpWorkQueue par LdrpQueueWork et non encore affichées par des threads travaillés ou parce que le chargeur parallèle est désactivé (dans ce cas, LdrpQueueWork également LDRP_LOAD_CONTEXT mais pas vraiment après le travail) et attend enfin (si besoin est) sur les événements LdrpWorkCompleteEvent ou LdrpLoadCompleteEvent .

 enum DRAIN_TASK { WaitLoadComplete, WaitWorkComplete }; void LdrpDrainWorkQueue(DRAIN_TASK task) { BOOLEAN LoadOwner = FALSE; HANDLE hEvent = task ? LdrpWorkCompleteEvent : LdrpLoadCompleteEvent; for(;;) { PLIST_ENTRY Entry; EnterCriticalSection(&LdrpWorkQueueLock); if (LdrpDetourExist && task == WaitLoadComplete) { if (!LdrpWorkInProgress) { LdrpWorkInProgress = 1; LoadOwner = TRUE; } Entry = &LdrpWorkQueue; } else { Entry = RemoveHeadList(&LdrpWorkQueue); if (Entry == &LdrpWorkQueue) { if (!LdrpWorkInProgress) { LdrpWorkInProgress = 1; LoadOwner = TRUE; } } else { if (!LdrpDetourExist) { ++LdrpWorkInProgress; } LdrpUpdateStatistics(); } } LeaveCriticalSection(&LdrpWorkQueueLock); if (LoadOwner) { NtCurrentTeb()->LoadOwner = 1; return; } if (Entry != &LdrpWorkQueue) { LdrpProcessWork(CONTAINING_RECORD(Entry, LDRP_LOAD_CONTEXT, WorkQueueListEntry), FALSE); } else { ZwWaitForSingleObject(hEvent, 0, 0); } } } 

12)

 void LdrpDropLastInProgressCount() { NtCurrentTeb()->LoadOwner = 0; EnterCriticalSection(&LdrpWorkQueueLock); LdrpWorkInProgress = 0; LeaveCriticalSection(&LdrpWorkQueueLock); ZwSetEvent(LdrpLoadCompleteEvent); }