Как работает Thread Local Storage

Дата публикации 7 мар 2026 | Редактировалось 31 мар 2026
Исследование TLS, загрузчика и CRT-инициализации в conhost.exe
Введение

Целью данного исследования было изучение того, как Thread Local Storage (TLS) используется в процессе conhost.exe, а также как TLS взаимодействует с:
  • механизмами загрузчика Windows,
  • стартовой последовательностью процесса,
  • инициализацией MSVC CRT,
  • и механизмом потокобезопасной инициализации локальных статических объектов.
Анализ проводился на живом процессе conhost.exe с использованием WinDbg.
Основной задачей было установить:
  • какую роль TLS играет до входа в main,
  • используется ли TLS для callback-инициализации,
  • и как именно TLS применяется в рантайме MSVC.
1. Начальное состояние отладчика

После подключения WinDbg к процессу conhost.exe был получен следующий стек:
ntdll!DbgBreakPoint
ntdll!DbgUiRemoteBreakin
KERNEL32!BaseThreadInitThunk
ntdll!RtlUserThreadStart

Это означает, что:
  • текущий поток — служебный поток, созданный отладчиком,
  • стек не относится к рабочему стеку приложения.
Поэтому дальнейший анализ проводился через структуры процесса.
2. Обход структур загрузчика

Были исследованы следующие структуры:
  • TEB
  • PEB
  • PEB->Ldr
  • списки загрузчика
  • _LDR_DATA_TABLE_ENTRY
Состояние загрузчика оказалось корректным.
Основной модуль процесса был определён как:
conhost.exe

3. Путь запуска приложения

Точка входа образа указывала на:
conhost!wWinMainCRTStartup

Полная цепочка запуска процесса выглядит следующим образом:
ntdll!LdrInitializeThunk
ntdll!LdrpInitializeProcess
conhost!wWinMainCRTStartup
conhost!__scrt_common_main_seh
conhost!wWinMain

После этого управление передаётся рабочей логике:
ConsoleCreateIoThreadLegacy

ConsoleIoThread

Именно этот поток выполняет основную работу консольного хоста.
4. TLS-директория образа

Файл conhost.exe содержит реальную TLS-директорию PE.
Обнаруженные символы:
conhost!tls_used
conhost!tls_start
conhost!tls_end
conhost!tls_index

Размер TLS-шаблона:
8 байт

Содержимое шаблона:
+0x0 = 0x00000000
+0x4 = conhost!Init_thread_epoch

Во время выполнения:
tls_index = 0

Это означает, что conhost использует первый слот TLS.
5. Чего TLS здесь не делает

Анализ показал отсутствие:
  • значимых TLS callbacks
  • CRT TLS initialization callbacks
Следовательно, TLS не используется как механизм выполнения кода до main.
6. Реальная роль TLS

TLS используется CRT MSVC для реализации:
thread-safe local static initialization

Были обнаружены следующие функции CRT:
Init_thread_header
Init_thread_footer
Init_thread_abort
Init_thread_wait
Init_global_epoch
Init_thread_epoch

Это стандартный механизм MSVC для защиты локальных статических объектов.
7. Примеры guarded static

Telemetry::Instance

Типичная схема:
  1. чтение TLS через gs:[58h]
  2. получение thread_epoch
  3. сравнение с guard-переменной
  4. вызов Init_thread_header
  5. конструирование singleton
  6. регистрация atexit
  7. Init_thread_footer
CommandLine::Instance

Использует тот же механизм.
Guard-переменная хранится:
__PchSym_+0x4

TermTelemetry::Instance

Использует аналогичный шаблон.
NtPrivApi::_Instance

Lazy-инициализация:
LoadLibraryExW("ntdll.dll")
GetProcAddress(...)

Также используется atexit.
Lazy-инициализация системных вызовов

Кэшируются указатели на:
NtOpenProcess
NtQueryInformationProcess
NtClose

8. Отличие от глобальных конструкторов

Глобальные конструкторы выполняются через стандартные CRT-таблицы:
dynamic initializer for ...

Это другой механизм, не связанный напрямую с TLS guarded static.
9. Наблюдение TLS в живых потоках

Глобальный epoch:
conhost!Init_global_epoch = 0x80000007

Поток ConsoleIoThread

TEB.ThreadLocalStoragePointer = 0x44c6e0
TLS slot 0 = 0x4366a0

Содержимое:
+0x0 = 0
+0x4 = 0x80000007

Render thread

TLS slot 0 = 0x02c3d720

Содержимое:
+0x0 = 0
+0x4 = 0x80000000

Это показывает, что epoch в разных потоках может отличаться.
10. Значения guard-переменных

Telemetry::Instance = 0x80000001
CommandLine::Instance = 0x80000004
TermTelemetry::Instance = 0x80000007
NtPrivApi::_Instance = 0

Интерпретация:
0 — не инициализировано
FFFFFFFF — инициализация выполняется
epoch value — объект уже создан

11. Работа функций CRT

Init_thread_header

  • входит в критическую секцию
  • если guard = 0 → устанавливает 0xFFFFFFFF
  • если guard = 0xFFFFFFFF → ожидает
  • иначе обновляет TLS epoch
Init_thread_footer

  • увеличивает Init_global_epoch
  • записывает новый epoch:
    • в guard
    • в TLS текущего потока
Init_thread_abort

Сбрасывает guard:
guard = 0

12. Итог по роли TLS

В данной сессии conhost.exe:
  1. загрузчик создаёт TLS до main
  2. TLS содержит epoch-состояние CRT
  3. CRT использует TLS для потокобезопасной инициализации локальных статических объектов
Таким образом, TLS здесь выступает механизмом синхронизации рантайма, а не способом выполнения кода до main.
13. Наблюдение lazy-инициализации NtPrivApi

В момент анализа состояние было:
module handle = 0
NtOpenProcess = 0
NtQueryInformationProcess = 0
NtClose = 0

Это означает, что lazy-инициализация ещё не выполнялась.
14. Когда происходит инициализация NtPrivApi

Её вызывает функция:
NtPrivApi::s_GetProcessParentId

Она выполняет:
NtOpenProcess
NtQueryInformationProcess
NtClose

15. Реальный вызывающий код

Статический анализ показал единственный вызов:
ApiDispatchers::ServerGenerateConsoleCtrlEvent

Алгоритм:
  1. поиск процесса в списке консоли
  2. если не найден
  3. запрос parent PID через NtPrivApi
  4. повторный поиск
16. Жизненный цикл TLS в загрузчике

LdrpInitializeTls

Функция:
  • проходит список модулей
  • ищет TLS-директории
  • создаёт записи TLS
LdrpAllocateTls

Для каждого потока:
  1. выделяет TLS-вектор
  2. копирует TLS-шаблон модуля
  3. записывает указатель в TEB+0x58
LdrpInitializeThread

При старте потока:
  1. вызывается LdrpAllocateTls
  2. затем выполняются TLS callbacks
LdrShutdownThread

При завершении потока:
  • вызываются TLS callbacks (THREAD_DETACH)
  • освобождается TLS память
17. Начальное значение TLS

Шаблон TLS:
00 00 00 00
00 00 00 80

То есть:
Init_thread_epoch = 0x80000000

Поток получает это значение при создании.
18. Особенность потока отладчика

Поток:
ntdll!DbgUiRemoteBreakin

имел:
ThreadLocalStoragePointer = 0

Причина:
SameTebFlags = 0x8

Этот флаг заставляет LdrpInitializeThread использовать специальный путь, который пропускает LdrpAllocateTls.
Поэтому поток отладчика не является нормальным примером TLS-инициализации.
19. Реальный путь создания потоков в conhost

Были обнаружены функции:
CreateConsoleInputThread
CreateAndStartSignalThread
HostSignalInputThread::Start
PtySignalInputThread::Start

Из них CreateConsoleInputThread может быть вызвана повторно.
Цепочка вызова:
ConsoleHandleConnectionRequest
→ ConsoleAllocateConsole
→ CreateConsoleInputThread

20. Практический триггер

Для вызова этого пути использовался:
AttachConsole(client_pid)

Важно:
AttachConsole(conhost_pid) → ошибка

Нужно передавать PID клиентского процесса, использующего консоль.
Итог

В результате исследования было установлено:
  • conhost.exe использует статический TLS
  • TLS-блок имеет размер 8 байт
  • загрузчик создаёт TLS-копию для каждого потока
  • MSVC CRT использует этот TLS для реализации guarded static initialization
  • TLS здесь выполняет роль механизма синхронизации и кэширования состояния CRT
Таким образом, в conhost.exe TLS не является механизмом ранней инициализации кода, а служит внутренней инфраструктурой рантайма MSVC для потокобезопасной ленивой инициализации объектов.
Если хочешь, я могу ещё:
  • сделать версию уровня Habr / блог-статьи (читается намного легче)
  • добавить диаграммы запуска процесса и TLS
  • или превратить это в очень сильную реверс-инженерную статью уровня RE community.

0 737
galenkane

galenkane
Active Member

Регистрация:
13 янв 2017
Публикаций:
1