逆向知識第十四講,(C語言完結)結構體在匯編中的表現形式


一丶了解什么是結構體,以及計算結構體成員的對其值以及總大小(類也是這樣算)

結構體的特性

  1.結構體(struct)是由一系列具有相同類型或不同類型的數據構成的數據集合

  2.在C語言中,結構體(struct)指的是一種數據結構,是C語言中聚合數據類型(aggregate data type)的一類。

       3. 結構體可以被聲明為變量指針數組等,用以實現較復雜的數據結構。結構體同時也是一些元素的集合,這些元素稱為結構體的成員(member),且這些成員可以為不同的類型,成員一般用名字訪問。[1]

高級代碼:

   

復制代碼
struct TagList { char ch; int number1; short int number2; double dbl; float flt; }; 
復制代碼

上面就是一個簡單的結構體,那么我們這個結構體在內存中的偏移要怎么計算.

公式:

下面是推理,如果不想看可以直接跳到總結去看總結.

  成員偏移量的公式

alg  設alg是編譯器的對其值,offset為結構體首地址的偏移,從0開始.

Member offset % min(alg,sizeof(member type) == 0; 這個公式是求成員位於結構體首地址的偏移

比如計算 成員 flt位與結構體首地址的偏移 ,要先從 第一個成員開始計算

 

設alg對齊值為4

offset % min(4,sizeof(ch)) == 0;

  0 % min(4,1) == 0  得出ch變量位於結構體首地址為0的偏移處,占1個字節      +0    1

offset % min(4,sizeof(number1)) == 0

  因為上面求出了ch占的大小,所以求出占1字節,所以偏移+1變為了1的位置

那么現在的offset = 1,繼續代入公式

1 % min(4,4) == 0,不成立,偏移繼續++

2%min(4,4) == 0,不成立,偏移繼續++

.....

一直到偏移為4的時候滿足,所以  偏移為4的地方,放number1             +4     4

計算 number2所在的偏移

offset % min(4,sizeof(member type)) == 0;

8 % min(4,2) == 0,成立                              +8     2

計算dbl所在的位置

offset % min(4,sizeof(member type)) == 0;

 

10 % (4,8) == 0,不成立

11%(4,8) == 0,不成立

12%(4,8) == 0;成立,所以在                           +12  8

 

計算float的位置

offset % min(4,sizeof(member type)) == 0;

 

20 % min(4,4) == 0;  成立                          +20 4

那么各成員的偏移已經計算出來了.

其中float成員位與結構體的 +20偏移,占4個字節大小.

 

計算結構體總體大小

  公式:

    

  sizeof(struct) % min( Max type size,alg);  

結構體的大小我們上面計算出來了,是 24個字節

MAX type,是結構體中最大成員的數據類型大小, 現在是double,也就是8個字節

alg是編譯器對齊值,現在是4

所以代入公式得到

  24 % 4 == 6...0

所以總體的大小是24個字節.

 

總結:

 

   編譯器對齊值,設置為 alg, MeMber offset 從0開始計算, 其中Member offset 要每次代入公式之后,加上自己成員所占的字節大小,繼續參與下次運算.

  設置或者查看編譯器對其值, VC6.0版本   Project (工程)  -> Settings(設置) -> C/C++ -> Category(種類) -> Code Generation(代碼生成) -> Struct  Member alignment(結構體對齊值)

  結構體成員偏移計算公式:  MeMber offset % min(alg,sizeof(Member type)) == 0

  結構體總大小計算公式:   sizeof(struct) % min(Max type size,alg) == 0;

程序內存查看.

根據內存窗口賦值,可以得出結構體成語位與結構體的偏移是多少

第一個成員,   +0 偏移位置, 占1個字節

第二個成員,   +4 偏移位置, 占4個字節

第三個成員    +8 偏移位置, 占2個字節

第四個成員 +12偏移位置,占8個字節

PS: 其中成員的Member offset 從零開始,當計算完畢之后,需要加上自己所占的字節大小,然后繼續參與運算,如果運算不成立,則偏移繼續增加,一直到偏移成立

比如:

  比如我們計算第二個成員位置的偏移

公式:

  Member offset % min(alg,sizeof(member type size) == 0;

  0 % 1 == 0   +0  放第一個成員

Member offset = Mmeber offset + 占的字節大小,(1)

求第二個成員位置

  1 % 4  ==0; 偏移為1的時候,不成立,則偏移繼續增加

  2 % 4 == 0,不成立繼續增加

  3 % 4 ==0,不成立繼續增加

  4%4 == 0;成立,所以在 +4位置,方放4個字節,也就是第二個成員位置.

二丶結構體當做參數傳遞,為指針的情況下

復制代碼
void MyFun(struct TagList *pThis) { pThis->ch = 'b'; } int main(int argc, char* argv[]) { struct TagList text = { 'a', 1, 2, 3.14, 0.0 }; MyFun(&text); printf("%d\r\n",text.number1); return 0; }
復制代碼

Debug下的匯編代碼

產生了尋址公式其中eax是數組首地址,ebp +8則是參數,外面傳入的是結構體首地址,所以ebp +8則是數組首

所以 ebp +8 則是結構體的首地址

mov byte ptr[eax],62h    這一句直接產生了 +0位置偏移,取內容賦值了字符

mov ecx,[ebp + 8]

mov dword ptr[ecx +4],2 這一句產生了 +4 偏移賦值為了2,所以可以確定

1.結構體首地址   ebp + 8 (參數1)

2.結構體第一個成員偏移  +0 賦值為字符

3.結構體第二個成員偏移 +4 賦值為2

 

Release下的匯編

main函數調用傳遞結構體地址的時候,只需要三行匯編

lea eax,[esp + 20h + Var_20] push eax call MyFun

上面都是流水線優化的匯編

看下MyFun內部

其結構和Debug差不多

1.獲得結構體的首地址

2.+0偏移位置賦值字符

3.+4偏移位置,賦值為2

三丶結構體當做參數傳遞,為結構體本身的的情況下

高級代碼:

  

復制代碼
void MyFun(struct TagList pThis)    //這個地方變了.不是指針了 { pThis.ch = 'b'; pThis.number1 = 2; } int main(int argc, char* argv[]) { struct TagList text = { 'a', 1, 2, 3.14, 0.0 }; MyFun(text);          //傳參不用取地址了 printf("%d\r\n",text.number1); return 0; }
復制代碼

Debug下的匯編

傳參之前的操作

很明顯

1.先抬棧

2.循環6次,每次4個字節4個字節的拷貝

3.獲得結構體的首地址

4.將棧頂賦值給edi,意思就是說,從棧頂開始復制.

5.執行串操作指令,rep movsd 將 esi的內容復制到棧頂位置處,

因為要復制 24個字節,所以棧頂要+24所以這一段就是存儲結構體成員的.

MyFun內部

1. 經過傳參之后,esp的位置為數組首地址的,也就是+0位置偏移處

2.進入函數后壓入返回地址,那么棧 esp -4, 然后push ebp,繼續esp -4

3.mov ebp,esp,保存尋址,現在的ebp + 8正好是外面我們進行串拷貝的時候的結構體的首地址.

4.mov byte ptr[ebp +8],62h,相當於就是給我們結構體成員的 +0成員賦值

5.mov dword ptr[ebp + 0ch],2 則正好是我們的第二個成員.

所以為了解釋這兩句匯編代碼,需要通過外面傳參的棧環境來看.

 

Release下的匯編

 

和Debug下一樣,也是要進行串拷貝

MyFun函數內部

發現我們沒有使用,所以直接給優化了.

 

三丶函數返回值為結構體的時候

1.返回為指針的時候,直接放到eax中

返回值,為結構體的情況

三種情況

1.當結構體大小小於(4這個數不確定)個字節,直接用eax返回

2.當結構大小小於(8這個數不確定)個字節,直接用 edx,eax返回

3.當結構體大小大於 8個字節以上(不確定,視編譯器而決定).

最后一種的高級代碼:

  

復制代碼
struct TagList MyFun() { struct TagList text = { 'a', 1, 2, 3.0, 4.0, }; return text; } int main(int argc, char* argv[]) { struct TagList text; text = MyFun(); printf("%c\r\n",text.ch); return 0; }
復制代碼

Debug下的匯編代碼

1.我們的函數沒有參數,但是Debug會生成上面的代碼,傳入進入, 為什么? 因為返回值eax等等都裝不下了,所以利用這塊內存區域當做返回值

 

2.函數退出之前,也會對它進行串操作指令,因為要返回這塊內存區域,所以寫入內存.

3.返回值以前會把首地址給 eax保存

4.看外面是否使用eax,如果使用可以可以判斷返回的是一個對象,(當然這一步可以省略,但是上面的三步少一步都不是返回對象)

參數問題:

  它會默認給我們生成一個參數傳入,那么我們有了參數,則會跟在后面.

Release匯編代碼一樣.


免責聲明!

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



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