TLS изнутри
[C] Bill / TPOC
🕛 08.02.2007, 12:41
Здравствуйте. Вот мы и вернулись. Сегодня я расскажу вам об одном крутом механизме, который называется TLS - Thread Local Storage - что по-русски - локальная память потока. Сия вещь широко применяется обычными гуишными программерами в многопоточных приложениях. Вы скажите: А зачем мне оно?? Отвечу - обычно сия вещь нужна для того, чтобы связать определенные данные с конкретным потоком. Например, дядя Рихтер приводит пример - с каждым потоком в TLS связывается дата и время, когда он был создан. В момент уничтожения потока можно посчитать время в течении, которого поток существовал.Сценарии, где есть данные, которые связанны одновременно и программой в целом, и с отдельным потоком вынуждают использовать TLS. Например, пусть процесс владеет некоторым массивом. Каждый элемент массива вместе с его содержимым соответствует отдельному потоку. Откуда поток узнает, какой индекс в глобальном массиве его? Да, можно передать функции потока ThreadProc параметр в виде индекса. Тогда индекс будет храниться в локальной переменной. Но представьте, что ThreadProc вызывает какую-то функцию потом еще одну, и так он может вызывать сотни функций с разными уровнями вложенности. Куда денется индекс, которым владеет поток?? Да, можно передавать индекс каждой функции параметром, но это очевидно будет сказываться на эффективности. Очевидным решением для кодеров из Micro$oft стало создание памяти специфичной для потока - TLS.
Но я непросто так взялся за этот механизм. TLS позволяет выполнять код до исполнения EP. Эту технику используют в качестве анти-отладочного механизма. Хоть он и сейчас довольно известен, думаю, это будет интересно. Более того я нигде не нашел нормального описания видов TLS - их отличий и свойств, поддержка TLS разными компиляторами, создание TLS ручками. Кое-что кое-где встречается, но этого явно не достаточно. Этим небольшим документом я решу это молчание. Даже дядя Джефф в своей книге очень поверхностно описывает TLS - судя по его книжке нельзя понять, чем отличаются статическая и динамическая TLS! Давайте, наконец, положим конец этому информационному беспределу!
Существует два вида локальной памяти потока - статическая и динамическая. Но это нигде явно не говорится, кроме все той же книги Джеффри Рихтера - но как я говорил различия там совершенно не описываются. Вот что пишет Джефф про различия статической и динамической TLS:
“Статическая локальная память основана на той же концепции, что и динамическая, - она предназначена для того, чтобы с потоком можно было сопоставить те или иные данные. Однако статическую TLS использовать гораздо проще, так как при этом не нужно обращаться к каким-либо функциям.”
Это утверждение неверно в корне. Хоть мы очень уважаем дядю Рихтера, но в этом случае он нагло недоговаривает истину, а в нашем случае это совсем не хорошо. Я вернусь к этому вопросу позже, после рассмотрения юзверьского использования обеих типов TLS.
Вообще действительно оба типа TLS преследуют одинаковую цель, о которой было сказано выше. Но у них разнятся возможности и их внутреннее устройство.
Динамическая TLS
Чтобы связать данные с динамической TLS поток может использовать четыре функции - TlsAlloc, TlsGetValue, TlsSetValue, TlsFree. Смысл в следующем - поток имеет определенное количество ячеек каждая из которых размером 4 байта. Количество ячеек разниться в зависимости от версии винды, но самое маленькое - это 64 ячейки для Windows 95. В более новых ОС количество доступных ячеек увеличивается. Вот таблица с информацией о максимальном количестве ячеек для процесса:ОС Предел
Windows 2000/XP 1088
Windows 98/Me 80
Windows 95
Windows NT 4.0 и младше 64
Т.о. пусть каждый поток имеет один специфический для себя указатель на какую-либо структуру - разработчики предполагают что в 95-ой винде потоков таких может быть не более 64 в одном процессе. И так далее по нарастающей версий. Но чтобы программа работала в любой ОС надо ориентироваться на самое маленькое значение - т.е. 64.
Итак, чтобы получить 4х байтный кусочек, мы вызываем функцию - TlsAlloc:
DWORD TlsAlloc(VOID)
Данная функция резервирует кусочек в локальной памяти потока и возвращает индекс этого DWORD’а. Далее этот индекс передают в функции TlsSetValue и TlsGetValue:
BOOL TlsSetValue( DWORD dwTlsIndex, // TLS index to set value for LPVOID lpvTlsValue // value to be stored );
LPVOID TlsGetValue( DWORD dwTlsIndex // TLS index to retrieve value for );
Функция TlsSetValue устанавливает значение в ячейке с данным индексом. Она принимает индекс возвращенный функцией TlsAlloc, а также значение для сохранения в ячейке с данным индексом. Функция возвращает 1 в случае успеха и 0 в противном случае. Для получения дополнительной информации в случае ошибки как обычно вызывайте функцую GetLastError.
Функция TlsGetValue соответственно возвращает значение указанное данным индексом. В случае ошибки возвращается 0. Чтобы различить нулевое значение в ячейке, с сигнализацией об ошибке вызывайте GetLastError. Если ошибки не было, то GetLastError вернет NO_ERROR.
Теперь откроем капот этих функций. Сперва TlsAlloc. Функция TlsAlloc устроена довольно просто, но при ее исследовании встречаешься со многими фундаментальными механизмами ОС Windows. Рассмотрим поведение TlsAlloc по пунктам:
На время работы функции устанавливается обработчик исключения SEH следующим в цепочку.
Чтобы получить свободный индекс функция TlsAlloc просматривает битовую карту индексов. Если бит установлен, то индекс свободен. Битовая карта находится в PEB (все необходимые структуры данных описываемые в этой статье можно получить из файлов символов с помощью утилиты PDBDUMP) по смещению 44h и называется TlsBitmapBits. Т.к. битовая карта индексов глобальна для процесса (потому что находится в PEB - Process Environment Block) до доступа к PEB входим в критическую секцию, чтобы не повредить эти глобальные данные (после поиска свободного индекса происходит запись в битовую карту). Т.к. поиск индексов ведется в TlsBitmapBits, а размер этого поля 8 байт во всех версиях винды, то получается всего 64 различный слота. Но где обещанные 80 слотов для Windows 98/Me и 1088 для Windows 2000/XP? Все просто. Сначала поиск действительно ведется в TlsBitmapBits, если в 2000/XP обнаруживается, что этом поле биты закончились, то поиск продолжается в другом расширенном поле, которое называется TlsExpansionBitmapBits, размер которого для Windows 2000/XP 32 байта - 1024 бита плюс 64 бита для TlsBitmapBits получается 1088 слотов.
Когда свободный индекс найден, соответствующий бит устанавливается. Поиск свободного бита и установка его значения выполняется внутренней недокументированной функцией NTDLL.DLL! RtlFindClearBitsAndSet. Один из параметров этой функции (и самый важный) - указатель на структуру RTL_BITMAP, которая описывается следующим образом:
struct _RTL_BITMAP
{
unsigned long SizeOfBitMap;
unsigned long* Buffer;
}
Здесь SizeOfBitMap количество бит в битовой карте, Buffer - битовая карта.
Сами ячейки TLS представлены массивом, который находится в TEB и называется TlsSlots для первых 64 ячеек и TlsExpansionSlots для индексов больше 64х. После получения свободного индекса, в массиве очищается (устанавливается в ноль) элемент соответствующий новому индексу (для этого и входит в критическую секцию).
Выходим из критической секции.
Убираем обработчик исключений.
Функции TlsSetValue и TlsGetValue работают, очевидно, теперь как, обращаясь к TlsSlots и к TlsExpansionSlots по индексу.
В принципе теперь можно реализовать свои функции для работы с TLS не обращаясь к системным API. Здесь смущает только синхронизация при обращении к нашей TlsXxx несколькими потоками, которую тоже можно реализовать самостоятельно, учитывая, что синхронизация производится с помощью функций RtlAcquirePebLock и RtlReleasePebLock, которые реализованы с помощью критических секций без использования ядерных функций.
Статическая TLS
Статическая локальная память для потока не использует API функций. Статическая локальная память потока опирается на механизмы загрузчика и свои собственные структуры. Если мы хотим использовать статическую TLS в своих программах на ассемблере, то придется реализовывать ее вручную. Компиляторы высокоуровневых языков предоставляют специальный синтаксис для работы со статической TLS. Так, компилятор Microsoft VC++ позволяет использовать следующий синтаксис для создания переменной специфичной для потока:
_declspec(thread) int tls_i = 1;
Этим кодом создается переменная tls_i локальная для потока, которая инициализируется значением 1. Переменная может быть любого типа. Такие переменные являются как бы глобальными, т.е. не размещаются в стеке, но для каждого потока соответствующие адреса размещения переменных TLS будут различны. Исходя из этих правил нельзя внутри функции определить TLS переменную, т.к. она тогда будет локальной для функции (это правило естественно для высокоуровневых компиляторов).
Для программистов на высокоуровневых языках предпочтительнее использовать именно статическую локальную память потока, т.к. не нужно вызывать никаких API - все сделает сам компилятор - автоматически создаст код для работы с TLS и инициализирует директорию TLS, данные которой обычно располагаются в секции с именем .tls. Статическая и динамическая TLS изнутри устроены совсем по разному - они используют разные внутренние механизмы и структуры. Статическая TLS поддерживает вызов TLS Callback функций для инициализации TLS переменных - то, что довольно часто используют протекторы и вирусы для антиотладки. Например, ExeCryptor располагает код TLS Callback функции, в результате отладчик пролетает EP.
Рассмотрим формат директории TLS в PE-файле:
typedef struct _IMAGE_TLS_DIRECTORY32 { DWORD StartAddressOfRawData; DWORD EndAddressOfRawData; DWORD AddressOfIndex; // PDWORD DWORD AddressOfCallBacks; // PIMAGE_TLS_CALLBACK * DWORD SizeOfZeroFill; DWORD Characteristics;
} IMAGE_TLS_DIRECTORY32;
typedef IMAGE_TLS_DIRECTORY32 * PIMAGE_TLS_DIRECTORY32;
DWORD StartAddressOfRawData; - VA начала области TLS данных. При создании потока данные из этого VA копируются в область специфичную для потока. Можно сказать, что данные в этой области являются инициализаторами данных специфичных для конкретного потока.
DWORD EndAddressOfRawData; - VA конца области TLS данных.
DWORD AddressOfIndex; - Область для получения TLS индекса. Является указателем на DWORD, располагающийся в секции данных. Обычно адрес AddressOfIndex имеет символическое имя _tls_index. Это поле заполняет загрузчик при анализе PE-файла.
DWORD AddressOfCallBacks; - Указатель на массив адресов TLS Callback функций. Конец массива помечается нулем. Чуть позже я расскажу о TLS Callback функциях подробно.
DWORD SizeOfZeroFill; - Количество байт, которые лоадеру необходимо заполнить нулями в промежутке от StartAddressOfRawData до EndAddressOfRawData.
DWORD Characteristics; - Зарезервировано для будущего использования.
Теперь расскажу, как это все работает. Посмотрите на пример программы, использующей статическую локальную память потока:
// TLS переменные
_declspec( thread ) int tls_i;
_declspec( thread ) char tls_char[25];
// Потоковая функция
DWORD WINAPI ThreadFunc( LPVOID lpParam )
{
_asm int 3 // Я использую эту инструкцию, чтобы затормозиться
// в ОллиДебуг. Также можно отметить Break on new
// threads в меню Debugging Options.Events tls_i = (int)lpParam; lstrcpy(tls_char,"af");
char szMsg[80];
wsprintf( szMsg, "Parameter = %d.", tls_i );
MessageBox( NULL, szMsg, "ThreadFunc", MB_OK );
return 0;
}
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
DWORD dwThreadId;
CreateThread( NULL, // default security attributes 0, // use default stack size ThreadFunc, // thread function (LPVOID)1, // argument to thread function 0, // use default creation flags &dwThreadId); // returns the thread identifier
CreateThread( NULL, // default security attributes 0, // use default stack size ThreadFunc, // thread function (LPVOID)2, // argument to thread function 0, // use default creation flags &dwThreadId); // returns the thread identifier
while (1);// Если функция WinMain завершается, то убивается процесс. Не // дадим WinMain завершиться return 0;
}
Когда компилятор видит использование статических TLS переменных он создает директорию TLS в результирующем PE-файле. Директория имеет 9 номер в массиве директорий IMAGE_DATA_DIRECTORY. Сами данные в директории служат для начальной инициализации буферов TLS массива для потока. Данные инициализации находятся по адресам от StartAddressOfRawData до EndAddressOfRawData. Также массив адресов TLS Callback функций содержит адреса функций, которые должны инициализировать значения TLS переменных. Когда компилятор видит следующую инструкцию в потоковой функции
tls_i = 1;
он генерирует примерно такой код:
mov ecx, [_tls_index] // Индекс, пусть равен нулю, т.е. в ecx - 0
mov edx, fs:[2Ch] // В EDX - адрес массива TLS - ThreadLocalStorage
mov eax, [edx+ecx*4]// В EAX - адрес буфера специфичного для потока
mov [eax+104h],1 // 104h - смещение переменной tls_i в буфере TLS
Для доступа к TLS имеется массив TLS, адрес которого находится в поле ThreadLocalStorage TEB’а. Массив специфичен для потока и в каждом элементе содержит адрес TLS буфера. AddressOfIndex - адрес индекса в массиве TLS. Видно, что при одном и том же коде буферы для разных потоков будут разные - ответственность по заполнению полей в TEB берет на себя загрузчик. Размер буферов каждого потока загрузчик вычисляет из формулы
SizeOfBuffers = (EndAddressOfRawData-StartAddressOfRawData).
Специфика реализации доступа к TLS налагает некоторые ограничения на использование статической TLS в высокоуровневых компиляторах. Вот некоторые из них:
Спецификатор _declspec( thread ) может быть использован только с данными.
Как было сказано выше, TLS можно применять только к статическим переменным - т.е. нелокальным.
Нельзя получить адрес переменной TLS, т.к. он не является константой.
Может возникнуть проблемы с DLL, которую динамически загружают с помощью LoadLibrary. Для DLL, которые могут быть загружены с помощью LoadLibrary и которые используют TLS рекомендуется вызывать функции TlsXxx, которые были описаны выше. Более подробно в “SDK -> Visual C++ Concepts: Adding Functionality -> Rules and Limitations for TLS”.
TLS Callback
Tls Callbacks - это функции, которые загрузчик вызывает до вызова EP. Директория TLS описывает набор этих функций. Каждая из функций имеет следующий прототип:
typedef VOID
(NTAPI *PIMAGE_TLS_CALLBACK) ( PVOID DllHandle, DWORD Reason, PVOID Reserved );
Этот параметр может принимать одно из следующих значений: DLL_PROCESS_ATTACH, DLL_THREAD_ATTACH, DLL_THREAD_DETACH, DLL_PROCESS_DETACH. Т.е. функция вызывается в зависимости от ситуации. Самое интересное значение DLL_PROCESS_ATTACH - вызов TLS функции, когда процесс только создался - даже до вызова EP.
О действиях загрузчика при вызове TLS Callback функциях можно почитать здесь http://www.microsoft.com/msj/0999/hood/hood0999.aspx. Хочу заметить, что высокоуровневые компиляторы Micro$oft (и другие насколько я знаю тоже) не поддерживают вызов TLS Callback функций. Линкер UniLink имеет файл unlfeat.h, который позволяет использовать TLS Callback функции. Используйте макрос TLS_CALLBACK, а после линкуйте файл линкером UniLink.
Создание программы с функциями TLS Callback вручную на ассемблере
format PE GUI
include 'include\win32a.inc'
entry $ invoke ExitProcess,0 ret
proc callback,handle,reason,reserved cmp [reason],DLL_PROCESS_ATTACH jnz @f invoke MessageBox,0,0,0,0
@@: ret
endp
data 9 dd a ; StartAddressOfRawData; dd a ; EndAddressOfRawData dd a ; AddressOfIndex dd c ; AddressOfCallBacks
a dd 0 ;
c dd callback ; Array Of Callbacks dd 0 ; NULL - end of Array Of Callbacks
end data
section '.idata' import data readable
library kernel,'KERNEL32.DLL',\ user,'USER32.DLL'
import kernel,\ ExitProcess,'ExitProcess' import user,\ MessageBox,'MessageBoxA'
Думаю, что этот код понятен без объяснений. Запустив программу в OllyDebug до вызова точки входа отобразиться MessageBox. Этот трюк используется в целях антиотладки. В OllyDebug в Debugging Options -> Event устанавливаем System Breakpoints. Также может помочь плагин для Olly, который называется OllyAdvanced - у него есть опция Break on TlsCallbacks. IDA может показать TlsCallback функции - нажмите Ctrl+E - отобразятся точки входа. Наравне с настоящей EP, здесь можно увидеть все входы в TlsCallbacks функции. Интересен факт, что в Windows XP, если в импорте нет USER32.DLL TLS Callback функции вызываться не будут, если Reason == DLL_PROCESS_ATTACH. Этой тонкостью можно воспользоваться при обломе антиотладчика, переименовывая в импорте USER32.DLL, но из-за этого может нарушиться остальная логика приложения. Ап ашипках и падобном велкам на мыло.