1. 程式人生 > 實用技巧 >第45章:TLS回撥函式

第45章:TLS回撥函式

TLS(Thread Local Storage,執行緒區域性儲存)回撥函式(Callback Function)常用於反除錯。

TLS 回撥函式的呼叫執行要先於 EP 程式碼的執行。它是各執行緒獨立的資料儲存空間,可修改程序的全域性/靜態資料。

若在程式設計中啟用了 TLS,PE 標頭檔案中會設定 TLS 專案,即:IMAGE_TLS_Directory

其中比較重要的成員是:AddressOfCallBacks 它指向回撥函式陣列地址

自己找一下試試:

同時獲取到相應的節區資訊,計算: 9310 - 8000 + 6600 = 7910 .

第四個元素即為 AddressOfCallBacks .

408114 明顯超出了檔案的大小,因此判斷是加上了 ImageBase (即 VA ),在節區頭檢視 ImageBase 大小為 400000 .

因此 RVA 為 8114 ,8114 - 8000 + 6600 = 6714 .

引數 Reason 表示呼叫 TLS 回撥函式的原因:

TLS設計的本意,是為了解決多執行緒程式中變數同步的問題,是 Thread Local Storage 的縮寫,意為執行緒本地儲存。執行緒本身有獨立於其他執行緒的棧空間,因此執行緒中的區域性變數不用考慮同步問題。多執行緒同步問題在於對全域性變數的訪問,TLS在作業系統的支援下,通過把全域性變數打包到一個特殊的節,當每次建立執行緒時把這個節中的資料當做副本,拷貝到程序空閒的地址空間中。以後執行緒可以像訪問區域性變數一樣訪問該異於其他執行緒的全域性變數的副本,而不用加同步控制。

第二個程式的程式碼( TlsTest.cpp ):

#include <windows.h>

#pragma comment(linker, "/INCLUDE:__tls_used")//通過#pragma comment(Linker,"INCLUDE:_tls_used")顯示的申明crt庫中定義的_tls_used變數
                            // 以便向_tls_used!AddressOfCallBacks域添加回調函式。                              
void print_console(char* szMsg)
{
    HANDLE hStdout 
= GetStdHandle(STD_OUTPUT_HANDLE); WriteConsoleA(hStdout, szMsg, strlen(szMsg), NULL, NULL); } void NTAPI TLS_CALLBACK1(PVOID DllHandle, DWORD Reason, PVOID Reserved) { char szMsg[80] = {0,}; wsprintfA(szMsg, "TLS_CALLBACK1() : DllHandle = %X, Reason = %d\n", DllHandle, Reason); print_console(szMsg); } void NTAPI TLS_CALLBACK2(PVOID DllHandle, DWORD Reason, PVOID Reserved) { char szMsg[80] = {0,}; wsprintfA(szMsg, "TLS_CALLBACK2() : DllHandle = %X, Reason = %d\n", DllHandle, Reason); print_console(szMsg); } #pragma data_seg(".CRT$XLX") //CRT 表明使用 CRunTime 機制,X 表示標識名隨機,L 表示 TLScallback section, X 為 B-Y 之間的任意一個字母 PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[] = { TLS_CALLBACK1, TLS_CALLBACK2, 0 }; // 注意,此處將自定義函式放入了 TLS 表中。 #pragma data_seg() DWORD WINAPI ThreadProc(LPVOID lParam) { print_console("ThreadProc() start\n"); print_console("ThreadProc() end\n"); return 0; } int main(void) { HANDLE hThread = NULL; print_console("main() start\n"); hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL); WaitForSingleObject(hThread, 60*1000); CloseHandle(hThread); print_console("main() end\n"); return 0; }

程式執行截圖:

① DLL_Process_ATTACH 在主執行緒呼叫 Main()函式前,已經註冊的兩個函式會被呼叫執行。

② DLL_Thread_ATTACH 在 TLS 函式完成後,main()函式開始執行。在建立使用者執行緒前,TLS 回撥函式會再次被執行。

③ DLL_Thread_Detach TLS 函式執行完後,執行緒函式開始呼叫執行,執行緒函式結束後再次呼叫 TLS 函式。

④DLL_Process_Detach main()函式結束後,再次呼叫 TLS 函式。

使用 win xp sp3 進行實際除錯:

使用 OD ,將斷點設定為 TLS 斷點,程式會斷在此處:

對比程式碼,並觀察程式名可知,程式被斷在了 TLS_CallBack1 處。繼續執行程式至返回:

可以看到,通過間接呼叫,實現了 TLS CallBack1 的呼叫。繼續執行程式:

可以看到,函式執行到了第一次執行的位置,表明會迴圈執行,直至函式執行完:

手工新增 TLS 回撥函式

①將 TLS 結構體及 TLS 回撥函式放在最後一個節區 or 其它節區的空白地區 or 新增新節區。

屬性增加了 Write (方便在除錯時編寫程式碼),Execute(可執行),CNT_CODE(區段包含程式碼)。

②設定 TLS 表

③設定 IMAGE_TLS_DIRECTORY 結構體

StartAddressOfRawData:tls模板在記憶體中的起始VA,模板是用於建立執行緒時初始化TLS資料的;
EndAddressOfRawDataL:tls模板在記憶體中的結束VA;
AddressOfIndex:儲存TLS索引的位置;

這三個值都可以指向 NULL 區域。

第四個值 CallBack 指向一個數組,以 四個位元組的 NULL 結尾。陣列每四個位元組指向一個地址,儲存著函式。

有意思的是,修改後,在 win xp sp3 上無法執行。與作者手工修改的檔案進行比較: