为什么Win32控制台应用程序启动时有三个意外的工作线程?

liu*_*liu 7 c++ winapi multithreading

这是情况的截图!

这是截图!

我用VS2010创建了一个Visual C++ Win32控制台应用程序.当我启动应用程序时,我发现有四个线程:一个'主线程'和三个工作线程(我没有编写任何代码).

我不知道这三个工作线程来自哪里.
我想知道这三个线程的作用.

提前致谢!

RbM*_*bMm 25

Windows 10实现了一种加载DLL的新方法 - 几个工作线程并行执行(LdrpWorkCallback).所有Windows 10进程现在都有几个这样的线程.

在win10之前,系统(ntdll.dll)总是在单线程中加载DLL,但从win10开始,这种行为发生了变化.现在ntdll中存在"并行加载器".现在NTSTATUS LdrpSnapModule(LDRP_LOAD_CONTEXT* LoadContext)可以在工作线程中执行加载任务().几乎每个DLL都有导入(依赖DLL),所以当加载DLL时 - 它的依赖DLL也被加载,这个过程是递归的(依赖DLL有自己的依赖).

该函数void LdrpMapAndSnapDependency(LDRP_LOAD_CONTEXT* LoadContext)遍历当前加载的DLL导入表并通过调用LdrpLoadDependentModule(内部调用LdrpMapAndSnapDependency新加载的DLL )加载其直接(第1级)依赖DLL - 因此此过程是递归的.最后LdrpMapAndSnapDependency需要调用NTSTATUS LdrpSnapModule(LDRP_LOAD_CONTEXT* LoadContext)绑定导入到已加载的DLL.LdrpSnapModule在顶级DLL加载过程中为许多DLL执行,并且此过程对于每个DLL都是独立的 - 因此这是并行化的好地方.LdrpSnapModule在大多数情况下,不会加载新的dll,只会将导入绑定到已加载的导出.但是如果导入被解析为转发导出(很少发生) - 将加载新的转发DLL.

一些当前的实施细节:


  1. 首先让我们来看看struct _RTL_USER_PROCESS_PARAMETERS新领域 - ULONG LoaderThreads.this LoaderThreads(如果设置为非零)在新进程中启用或禁用"Parallel loader".当我们通过ZwCreateUserProcess创建一个新进程时- 第9个参数是 PRTL_USER_PROCESS_PARAMETERS ProcessParameters.但如果我们使用CreateProcess[Internal]W- 我们不能PRTL_USER_PROCESS_PARAMETERS直接传递- 只STARTUPINFO.RTL_USER_PROCESS_PARAMETERS部分初始化STARTUPINFO,但我们不控制ULONG LoaderThreads,它将始终为零(如果我们不调用ZwCreateUserProcess或设置此例程的钩子)

  2. 在新进程初始化阶段LdrpInitializeExecutionOptions被调用(from LdrpInitializeProcess).此例程检查HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\<app name>多个值(如果子键存在 - 通常不存在),包括MaxLoaderThreads(REG_DWORD) - 如果MaxLoaderThreads存在 - 其值将覆盖RTL_USER_PROCESS_PARAMETERS.LoaderThreads.

  3. LdrpCreateLoaderEvents()叫做.此例程必须创建2个全局事件:HANDLE LdrpWorkCompleteEvent, LdrpLoadCompleteEvent;用于同步.

    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;
    
    Run Code Online (Sandbox Code Playgroud)

    }

  4. LdrpInitializeProcess电话void LdrpDetectDetour().这个名字不言而喻.它不返回值但初始化全局变量BOOLEAN LdrpDetourExist.这个例程首先检查一些加载器关键例程是否被挂钩 - 目前这些是5个例程

    • "NtOpenFile"
    • "NtCreateSection"
    • "NtQueryAttributesFile"
    • "NtOpenSection"
    • "NtMapViewOfSection".

如果是 - LdrpDetourExist = TRUE;如果没有上钩 - ThreadDynamicCodePolicyInfo查询 - 完整代码:

void LdrpDetectDetour()
{
    if (LdrpDetourExist) return ;

    static PVOID LdrpCriticalLoaderFunctions[] = {
        NtOpenFile,
        NtCreateSection,
        ZwQueryAttributesFile,
        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;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)
  1. LdrpInitializeProcess电话NTSTATUS LdrpEnableParallelLoading (ULONG LoaderThreads)- 如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;
    
    Run Code Online (Sandbox Code Playgroud)

    }

创建一个特殊的加载器线程池 - LdrpThreadPool具有LoaderThreads - 1最大线程.空闲超时设置为30秒(之后线程退出)并分配PTP_WORK LdrpMapAndSnapWork,然后使用void LdrpQueueWork(LDRP_LOAD_CONTEXT* LoadContext)

  1. 并行加载器使用的全局变量:

    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;
};
Run Code Online (Sandbox Code Playgroud)

遗憾的LDRP_LOAD_CONTEXT是,它不包含在已发布的pdb文件中,因此我的定义仅包含部分名称

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++
}
Run Code Online (Sandbox Code Playgroud)

在TEB.CrossTebFlags中 - 现在存在2个新标志:

USHORT LoadOwner : 01; // 0x1000;
USHORT LoaderWorker : 01; // 0x2000;
Run Code Online (Sandbox Code Playgroud)

最后2位是备用(USHORT SpareSameTebBits : 02; // 0xc000)

  1. LdrpMapAndSnapDependency(LDRP_LOAD_CONTEXT* LoadContext) 包括以下代码:

    LDR_DATA_TABLE_ENTRY*Entry = LoadContext-> CurEntry; if(LoadContext-> pvIAT){Entry-> DdagNode-> State = LdrModulesSnapping; if(LoadContext-> PrevEntry)//如果递归调用{LdrpQueueWork(LoadContext); // !!! } else {status = LdrpSnapModule(LoadContext); }} else {Entry-> DdagNode-> State = LdrModulesSnapped; }

所以如果LoadContext->PrevEntry(假设我们加载user32.dll.在第一次调用时LdrpMapAndSnapDependency LoadContext->PrevEntry将始终为0(当CurEntry指向时user32.dll),但是当我们递归调用LdrpMapAndSnapDependency它依赖时gdi32.dll- PrevEntry将为for user32.dllCurEntryfor gdi32.dll)我们不直接调用LdrpSnapModule(LoadContext);但是LdrpQueueWork(LoadContext);

  1. LdrpQueueWork 很简单:

    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
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    }

我们插入LoadContextLdrpWorkQueue,如果"并行加载器"开始(LdrpMapAndSnapWork != 0),而不是ShutdownInProgress-我们提交的工作装载机池.但即使池未初始化(比如存在弯路) - 也不会出错 - 我们会处理此任务LdrpDrainWorkQueue

  1. 在工作线程回调中执行:

    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);
    }
    
    Run Code Online (Sandbox Code Playgroud)

    }

我们只需弹出条目LdrpWorkQueue,将其转换为LDRP_LOAD_CONTEXT*(CONTAINING_RECORD(Entry, LDRP_LOAD_CONTEXT, WorkQueueListEntry))并调用void LdrpProcessWork(LDRP_LOAD_CONTEXT* LoadContext, BOOLEAN LoadOwner)

  1. void LdrpProcessWork(LDRP_LOAD_CONTEXT* ctx, BOOLEAN LoadOwner) 在一般调用LdrpSnapModule(LoadContext)和最后的下一个代码:

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

所以,如果我们没有LoadOwner(在工作线程中)我们减少LdrpWorkInProgress,如果LdrpWorkQueue是空信号LdrpWorkCompleteEvent(LoadOwner可以等待它)

  1. 最后LdrpDrainWorkQueueLoadOwner(主线程)调用"drain"WorkQueue.它可以能够流行和推到直接执行任务LdrpWorkQueueLdrpQueueWork,但不是由工作线程POP操作,或因为并行加载器被禁用(在这种情况下LdrpQueueWork也推LDRP_LOAD_CONTEXT,但没有真正发布工作的工作线程),最后等待(如果需要)上LdrpWorkCompleteEventLdrpLoadCompleteEvent事件.

    枚举DRAIN_TASK {WaitLoadComplete,WaitWorkComplete};

    void LdrpDrainWorkQueue(DRAIN_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);
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    }

12.

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

  • 如果它是有组织的,这似乎是一个有用的答案. (2认同)
  • @BenVoigt - 是的,我在几个月前研究"并行加载器"实现,并且有更多的数据和代码片段.但不幸的是,我有太纯粹的英语来描述这个并且不确定 - 这对某些人来说是否有趣.用简短的话说 - 现在存在特殊的线程工作池,ntdll用于加载dll.所以每个进程中的绝对正常+2/3工作线程(但是如果30+秒不再加载进程中的dll,则此线程退出) (2认同)