TLS изнутри — Архив WASM.RU

Все статьи

TLS изнутри — Архив WASM.RU

e-mail: bill_prisoner@mail.ru

web: http://vx.netlux.org/tpoc

Здравствуйте. Вот мы и вернулись. Сегодня я расскажу вам об одном крутом механизме, который называется TLSThread 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. Т.к. битовая карта индексов глобальна для процесса (потому что находится в PEBProcess 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 в высокоуровневых компиляторах. Вот некоторые из них:

  1. Спецификатор __declspec( thread ) может быть использован только с данными.
  2. Как было сказано выше, TLS можно применять только к статическим переменным – т.е. нелокальным.
  3. Нельзя получить адрес переменной TLS, т.к. он не является константой.
  4. Может возникнуть проблемы с 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, но из-за этого может нарушиться остальная логика приложения. Ап ашипках и падобном велкам на мыло.

2002-2013 (c) wasm.ru