Возврат из точки входа exe не прекращает процесс в Windows 10 - PullRequest
2 голосов
/ 25 октября 2019

Моя попытка

Я создал минимальный, свободный от CRT, исчерпываемый зависимостями исполняемый файл с Microsoft Visual Studio, указав флаг компилятора /GS- и флаг компоновщика /NoDefaultLib и назвав основную функцию mainCRTStartup. Приложение не создает дополнительные потоки и возвращает значение mainCRTStartup через <5 секунд, но для завершения процесса требуется всего 30 секунд. </p>

Описание проблемы

По моему опыту, еслиприложение, выполняемое в Windows 10, зависит только от динамических библиотек, которые по умолчанию загружаются в каждый процесс Windows, с именами ntdll.dll, KernelBase.dll и kernel32.dll, процесс обычно завершается, когда основной поток возвращается из mainCRTStartupfunction.

Если другие библиотеки загружаются статически или динамически (например, путем вызова 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 работают некорректно.

  1. Есть ли чистое решение этой проблемы?
  2. Для чего нужны потоки загрузчика, если последний созданный пользователем поток завершает работу? AFAIK невозможно в этот момент загрузить любые другие библиотеки.

1 Ответ

0 голосов
/ 25 октября 2019

Я ожидал, что процесс завершится, если он не создаст дополнительные потоки и вернется из основной функции.

Процесс может неявно создавать дополнительные потоки. загрузчик например. и нужно понимать, что означает

возврат из main функции

здесь означает функцию, которая вызывается из стандартной CRT mainCRTStartup функция. после этого mainCRTStartup звоните ExitProcess. поэтому не любая exe запись реальная функция точки входа, но некоторая подфункция вызывается из точки входа. но в точку входа позвоните ExitProcess than.

, если мы не используем CRT - нам нужно позвонить ExitProcess самостоятельно. если мы просто вернемся из точки входа - будет RtlExitUserThread, который не вызывает ExitProcess, за исключением того, что это последний поток в процессе (AmILastThread) (и здесь также может быть гонка, если 2 или более потоков в параллельном вызове ExitThread)

...