Windows сама создаёт дополнительные потоки в твоей программе

Ты пишешь однопоточную программу для Windows, открываешь ProcessExplorer и удивляешься: у твоей программы на самом деле несколько потоков.

Посмотрим на потоки в программе notepad.exe. Их количество зависит от версии операционной системы.

Windows Бесять:

TIDCPUCycles deltaStart AddressPriority
6200combase.dll!RoGetServerActivatableClasses+0x1060Normal
6924notepad.exe+0x23db0Normal
2372ntdll.dll!TpReleaseCleanupGroupMembers+0x450Normal
2648ntdll.dll!TpReleaseCleanupGroupMembers+0x450Normal
9512ntdll.dll!TpReleaseCleanupGroupMembers+0x450Normal
10592ntdll.dll!TpReleaseCleanupGroupMembers+0x450Normal
12056ntdll.dll!TpReleaseCleanupGroupMembers+0x450Normal

Windows XP:

TIDCPUCycles deltaStart AddressPriority
1620notepad.exe+0x73a5Normal
1804ntdll.dll!RtlSetLastWin32ErrorAndNtStatusFromNtStatus+0x59Normal

Отложенная загрузка DLL

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

Каждый такой поток создаёт событие синхронизации и ждёт его в течении 30 секунд. По истечении этого времени поток завершается. Если основной поток завершается до этого времени, то дополнительные системные потоки продолжают ждать 30 секунд. После чего процесс завершается.

Функция ExitProcess

Функция ExitProcess(ExitCode) завершает процесс.

  1. Все потоки в процессе, кроме вызывающего, завершают своё выполнение (через TerminateThread), не получая уведомления DLL_THREAD_DETACH.
  2. Состояние всех завершённых потоков становятся сигнальными.
  3. Точки входа всех загруженных динамических библиотек вызываются с помощью DLL_PROCESS_DETACH.
  4. После того как все DLL выполнили код завершения, завершается текущий процесс, включая вызывающий поток (через ExitThread).
  5. Состояние вызывающего потока становится сигнальным.
  6. Все открытые дескрипторы объектов ядра закрываются.
  7. Статус завершения процесса изменяется с STILL_ACTIVE на значение ExitCode.
  8. Состояние объекта процесса становится сигнальным.

Стандартная стартовая функция точки входа в программу

Обычно функция, с которой начинается выполнение программы, лежит в библиотеке времени выполнения. Стартовая функция из библиотеки времени выполнения:

Как видно, в конце концов вызывается ExitProcess, что завершает все дополнительные потоки и основной.

Нестандартная стартовая функция точки входа в программу

Мы можем указать компоновщику свою стартовую функцию. После того, как функция отработала, в регистр eax помещается значение ExitCode, и происходит возврат внутрь кода, вызвавшего стартовую функцию. А там происходит вызов ExitThread с нашим параметром ExitCode. Текущий поток завершается.

Это означает, что если в процессе есть другие потоки, то они остаются работать. Для принудительного завершения потоков используют функции TerminateThread или ExitProcess, которая внутри себя вызывает TerminateThread для каждого потока.

Это также объясняет, почему программа будет висеть в списке процессов, когда главный поток приложения завершился, а дополнительные системные потоки — ещё нет. Чтобы процесс завершился, необходимо либо дождаться завершения всех системных потоков из ntdll.dll (подождать 30 секунд, пока не сработает таймаут), либо вызывать ExitProcess для принудительного завершения всех потоков.