PE格式第八講,TLS表(線程局部存儲)


            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/
版權所有,歡迎保留原文鏈接進行轉載:)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM