PE格式第八講,TLS表(線程局部存儲)
作者:IBinary
出處:http://www.cnblogs.com/iBinary/
版權所有,歡迎保留原文鏈接進行轉載:)
一丶復習線程相關知識
首先講解TLS的時候,需要復習線程相關知識, (thread local storage )
1.了解經典同步問題
首先我們先寫一段C++代碼,開辟兩個線程去跑,看看會不會出現同步問題.
看結果得知,結果並不是正確的,造成同步的問題的原因是兩個線程都對同一個變量進行訪問.
解決問題:
1.使用同步對象. (自旋鎖 自加鎖 互斥體 事件 信號燈 臨界區.....等等都可以.)
這里使用自加鎖解決(當然可以用別的)
InterlockedIncrement API
原型:
LONG InterlockedIncrement( LPLONG volatile lpAddend // variable to increment );
只需要把全局變量的地址給它,強轉為long * 類型即可.
使用之后結果是正確的
二丶何為TLS (Thread local storage)
所謂TLS,意思就是指,每個線程都有自己的空間,局部存儲,什么意思?
比如上方我們對一個g_dwNumber進行操作,那么我們就要使用同步對象,我們不妨這樣去想,每個線程,開辟一個空間
當對A線程進行操作的時候,操作的是A線程的g_dwNumber,當對B線程進行操作的時候,是對B線程的g_dwNumber進行操作.
其實很簡單,介紹一下TLS的API
總共4個
分別是:
TlsAlloc 分配線程局部存儲空間
TlsFree 釋放線程局部存儲空間
TlsGetValue 獲得線程局部存儲空間里面的值
TlsSetValue 設置線程局部存儲空間的值
三丶TLSAPI的使用
1.首先是TlsAlloc的使用
DWORD TlsAlloc(VOID); 函數原型
調用一次TlsAlloc則會分配4個字節的空間,不管你在哪里調用,如果在main里面(主線程)中調用,那么當你創建線程的時候
線程會默認有4個字節的控件
返回值是一個索引, 這個索引是查FS寄存器數組的值當然,這個一會講解.只需要知道,當我們為每一個線程申請了4個字節的空間
那么索引是一樣的,但是索引操作的數據是不一樣的
比如 你申請的索引是1
那么在A線程中,操作1索引的時候,那么操作的是A線程的,那么如果在B線程操作索引1的時候,那么操作的是B線程的數據
舉例子:
比如有個電話號碼是 12345678
中國: 12345678
外國: 12345678 (把電話號碼看做是索引)
我們知道,電話號碼是一樣的,但是你打這個電話的時候,人是不一樣的
比如我在中國打123456 那么接聽人是張三
我在外國打123456 那么接聽人是李四
其中張三李四就是表達了對同一數據的不同操作.看下代碼
再比如:
我們使用tlsAlloc申請了4個字節的空間
索引就是nindex (看做是g_dwNumber);
那么訪問不同線程的索引,那么索引里面的值是不同的.
1.Tls的動態使用方法,設置全局變量
動態使用就是PE中不建立TLS表格了,同樣完成同步
首先,我們為每個線程開辟了4個字節的空間
然后返回一個索引(這個索引看做是g_dwNumber,其實這個索引是去數組里面去取出成員來,比如現在是第1個,那么去數組里面取出第一項來,當做g_dwNumber)
TlsSetValue(索引,設置的值)
這樣寫其實就是根據索引找到數組里面的值,設置一下.
TlsGetValue(索引)則是根據下標索引,去數組里面取出g_dwNumber的值.
然后下方重新設置回去了.在1索引的位置,設置了g_dwNumber的值.
如果對齊數據結構不理解,可以看下手工寫的圖
AThread (當前索引為1)
數組: [0][1][2][3]..... 數組首地址: 00401000
BThread (當前索引為1)
數組: [0][1][2][3]..... 數組首地址: 00402000
其實每個線程可以理解為索引雖然一樣,但是在數組里面取出來的值是不一樣的.
比如A線程的索引為1,里面的成員是A線程的g_dwNumber 比如現在它的值是5
現在切換到了B線程了,那么還是根據索引去找值,但是數組不同了,所以再次找1找的則是B數組的g_dwNumber了.
其實API的作用就相當於你手工的去給數組第幾個元素賦值,取值.等等.
只不過這個是操作系統封裝的數組,所以給你提供API
按照我們的寫法,可能會下面那樣做,偽代碼,便於理解
AThread[1] = 0;
DWORD g_dwNumber = AThread[1];
printf(g_dwNumber);
AThread[1] = g_dwNumber++;
替換成API則是
TlsSetValue(索引,值)
TlsGetValue(索引);
現在看下那張圖,那么已經實現了同步.線程也切換了,操作的就是自己的數據.
2.動態使用Tls之結構體的設置
上面我們說的是數組里面設置的是全局變量,現在我們要設置一下結構體了.
結構體其實是一樣的,我們讓數組里面存指針就行.
比如看下方代碼:
很簡單
1.我們定義一個p指針,指向了一塊new的內存
2.初始化的時候,設置數組索引的當前索引的值為p的指針
3.從索引中獲得p指針
4.修改p指向的m_dwCount的值
注意,這里因為p是一個指針,我們修改的只是它空間成員變量的值,所以不用重新再設置回去了.
到了現在感覺TLS是不是有點難用了.其實使用TLS 比使用任何同步對象都快,就相當於沒同步的時候的速度.
但是TLS的真正的語法不是這樣用的.(上面是動態使用不會生成TLS表)
3.Tls的靜態使用(真正用法)
其實TLS真正的用法是靜態使用,操作系統已經幫你集成了語法了
看下用法,以及語法;
語法:
__declspec(thread) 類型 變量名
然后tls就會自動生成表了,操作系統幫你升成上面動態使用的代碼.(所以為啥要理解動態使用)
用的時候還是正常使用.
我們的代碼都不用變的.
但其實匯編代碼還是會編譯為上面的動態使用.
如果變為結構體,那么是一樣的,只需要把類型變成結構體的類型即可.
四丶PE中TLS表的設計
了解了上方的原理了,那么如果讓你設計表格你要怎么設計?
1.我們全局變量初始化為0了,那么我們肯定有地方存儲了這個全局變量的數據 ,所以我會設計一段分為存儲這個值.
2.我們常用的nindex索引,那么我覺着也要存儲一下
廢話不說了,看下真是的結構體
ypedef struct _IMAGE_TLS_DIRECTORY32 { DWORD StartAddressOfRawData; TLS初始化數據的起始地址 DWORD EndAddressOfRawData; TLS初始化數據的結束地址 兩個正好定位一個范圍,范圍放初始化的值 DWORD AddressOfIndex; TLS 索引的位置 DWORD AddressOfCallBacks; Tls回調函數的數組指針 DWORD SizeOfZeroFill; 填充0的個數 union { DWORD Characteristics; 保留 struct { DWORD Reserved0 : 20; DWORD Alignment : 4; DWORD Reserved1 : 8; } DUMMYSTRUCTNAME; } DUMMYUNIONNAME; } IMAGE_TLS_DIRECTORY32;
首先介紹前兩個成員,
起始地址 結束地址 定位了一個范圍,那么這個范圍內存放的就是初始化的值(注意只有靜態使用才有TLS表)也就是上方我們定義的g_dwNumber = 0;存放了0,但是因為0不好看,這里我重新賦值為12345678 代碼不貼了.
我們查看下PE定位一下Tls的位置.
注意,因為我是VS2015編寫的程序,隨機基址懶得去了,直接在PE中修改了,把文件頭的文件屬性修改了即可.
以前是02,現在改成03即可.
首先查看下數據目錄的第9項
得出RVA = 000176FC
查看下模塊首地址. 首地址是 00400000
看下屬於哪個節
命中在.rdata節,RVA = 00016000
上面的RVA減去現在的RVA = 偏移
000176FC - 00016000 = 16FC
節中的文件偏移 + 偏移 = 文件中的位置.
文件偏移是下方的第二個成員
5400 + 16FC = 6AFC
查看6AFC定位Tls表的位置.
前面兩個成員分別指向的是
0041B000 0041B208的位置 結束地址 - 起始地址 = 范圍.
尋找起始地址的FA
時間關系,這里命中的節是 Rva = 001B000
那么轉為文件偏移
FA = 8400h直接計算出來了
起始地址是8400h 那么+208就是8608 ,那么8400h 到8608的位置就存放的初始值,現在已經看到上圖畫出來的12345678了(小尾方式讀取)
第3個成員: 索引的值,這個你可以自己轉化查看.
五丶TLS結構體第四個成員,回調函數的數組指針
這個怎么理解,是這樣的,還記到動態使用的時候,我們不是在主線程中 TlsAlloc 和TlsFree嗎
現在我們可以注冊回調函數,操作系統會調用這個回調函數.
怎么注冊?
關鍵字: 加段,必須添加到特定的段中
首先先看下回調的函數原型.
typedef VOID (NTAPI *PIMAGE_TLS_CALLBACK) (PVOID DllHandle, DWORD Reason,PVOID Reserved );
PIMAGE_TLS_CALLBACK 其中這個回調是從結構體中第四個成員里面,注釋得到的
首先我們自己寫一個
請看注釋,其實這里才是真正的申請和釋放,注意,這個回調函數操作系統會從問價那種讀取地址,然后執行一遍,沒有申請內存,所以這里面可以藏代碼的.
注意,雖然回調我們寫了,但是要讓操作系統調用,那么我們需要添加一個特定的節.
語法:
#pragma data_seg(".CRT$XLB") 其中關於.CRT$XLB 為什么是這個節,我發下連接看雪論壇的,自己看下吧,很簡單了.https://bbs.pediy.com/thread-108015.htm
/*中間寫代碼,定義函數回調數組*/
PIMAGE_TLS_CALLBACK ary[] = {MyTlsCallBack,0}; //0結尾,那么操作系統就會在文件中找到這個位置,調用一下這個回調.如果多個,里面可以寫多個,0結尾即可.
#pragma data_seg();
發現1已經成功彈出來了,那么現在結構體的第四個成員,就是指向這個數組首地址的.PE加載的時候,會默認調用,然后依次執行一遍..
請注意,只會在文件中存儲,如果你跑到內存中查看,這個地址是沒有的.
太晚了,快4點了,剩下的字節明天說.
作者:IBinary
出處:http://www.cnblogs.com/iBinary/
版權所有,歡迎保留原文鏈接進行轉載:)