Моя попытка
Я создал минимальный, свободный от CRT, исчерпываемый зависимостями исполняемый файл с Microsoft Visual Studio, указав флаг компилятора /GS-
и флаг компоновщика /NoDefaultLib
и назвав основную функцию mainCRTStartup
. Приложение не создает дополнительные потоки и возвращает значение mainCRTStartup
через <5 секунд, но для завершения процесса требуется всего 30 секунд. </p>
Описание проблемы
По моему опыту, еслиприложение, выполняемое в Windows 10, зависит только от динамических библиотек, которые по умолчанию загружаются в каждый процесс Windows, с именами ntdll.dll
, KernelBase.dll
и kernel32.dll
, процесс обычно завершается, когда основной поток возвращается из mainCRTStartup
function.
Если другие библиотеки загружаются статически или динамически (например, путем вызова LoadLibraryW
), возврат из основной функции оставит процесс живым: на 30 секунд при нормальной работе инеограниченное время при запуске в отладчике.
Context
При создании процесса загрузчик процессов Windows 10 создает дополнительные потоки для быстрой загрузки динамических библиотек, см .:
Упомянутые значения Cylance в Распределение параллельной загрузки Windows 10 :
Рабочий поток простаиваеттайм-аут установлен на 30 секунд. Программы, которые выполняются менее чем за 30 секунд, будут зависать из-за ntdll!TppWorkerThreadwaiting
для времени ожидания до завершения процесса.
Microsoft упоминает в Завершение процесса: как процессы завершаются :
Обратите внимание, что некоторые реализации библиотеки времени выполнения C (CRT) вызывают ExitProcess, если основной поток процесса возвращается.
С другой стороныMicrosoft упоминает в ExitProcess
:
Обратите внимание, что возврат из основной функции приложения приводит к вызову ExitProcess
.
Тестовый код
Это минимальный тестовый код, с которым я работал, я использовал kernel32!CloseHandle
и user32!CloseWindow
в качестве примеров, вызов к ним на самом деле ничего не делает:
#include <cstdint>
namespace windows {
typedef const intptr_t Handle;
typedef const void * Module;
constexpr Handle InvalidHandleValue = -1;
namespace kernel32 {
extern "C" uint32_t __stdcall CloseHandle(Handle);
extern "C" uint32_t __stdcall FreeLibrary(Module);
extern "C" Module __stdcall LoadLibraryW(const wchar_t *);
}
namespace user32 {
extern "C" uint32_t __stdcall CloseWindow(Handle);
}
}
int mainCRTStartup() {
// 0 seconds
// windows::kernel32::CloseHandle(windows::InvalidHandleValue);
// 30 seconds
// windows::user32::CloseWindow(windows::InvalidHandleValue);
// 0 seconds
// windows::kernel32::FreeLibrary(windows::kernel32::LoadLibraryW(L"kernel32.dll"));
// 30 seconds
// windows::kernel32::FreeLibrary(windows::kernel32::LoadLibraryW(L"user32.dll"));
// 0 seconds
// windows::kernel32::FreeLibrary(windows::kernel32::LoadLibraryW(L""));
return 0;
}
Отладка
Комментирование использования WinAPI в функции mainCRTStartup
приводит к временам выполнения, упомянутым выше соответствующего вызова WinAPI.
Это поток выполнения программы, отслеживаемый в aотладчик в псевдо C ++:
ntdll.RtlUserThreadStart() {
kernel32.BaseThreadInitThunk() {
const auto return_code = test.mainCRTStartup();
ntdll.RtlExitUserThread(return_code) {
if (ntdll.NtQueryInformationThread(CURRENT_THREAD, ThreadAmILastThread) != STATUS_SUCCESS || !AmILastThread) {
// Bad path - for `30 seconds`.
ntdll.LdrShutdownThread();
ntdll.TpCheckTerminateWorker(0);
ntdll.NtTerminateThread(0, return_code);
// The thread execution does not return from `NtTerminateThread`, but the process still runs.
} else {
// Good path - for `0 seconds`.
ntdll.RtlExitUserProcess(return_code) {
ntdll.EtwpShutdownPrivateLoggers();
ntdll.LdrpDrainWorkQueue(0);
ntdll.LdrpAcquireLoaderLock();
ntdll.RtlEnterCriticalSection(ntdll.FastPebLock);
ntdll.RtlLockHeap(peb.ProcessHeap);
ntdll.NtTerminateProcess(0, return_code);
ntdll.RtlUnlockProcessHeapOnProcessTerminate();
ntdll.RtlLeaveCriticalSection(ntdll.FastPebLock);
ntdll.RtlReportSilentProcessExit(CURRENT_PROCESS, return_code);
ntdll.LdrShutdownProcess();
ntdll.NtTerminateProcess(CURRENT_PROCESS, return_code);
// The thread execution does not return from `NtTerminateProcess` and the process is terminated.
}
}
}
}
}
Ожидаемые результаты
Я ожидал, что процесс завершится, если он не создаст дополнительные потоки и не вернется из основной функции.
Вызов ExitProcess
в конце основной функции завершает процесс, даже если вызван WinAPI, что привело к 30-секундному выполнению ранее. Использование этого API не всегда возможно, потому что проблемное приложение может быть не моим, а сторонним приложением (от Microsoft ), как здесь: Почему процесс зависает в RtlExitUserProcess / LdrpDrainWorkQueue?
Мне кажется, что загрузчик процессов Windows 10 не работает, даже если процессы Microsoft работают некорректно.
- Есть ли чистое решение этой проблемы?
- Для чего нужны потоки загрузчика, если последний созданный пользователем поток завершает работу? AFAIK невозможно в этот момент загрузить любые другие библиотеки.