一丶了解什么是結構體,以及計算結構體成員的對其值以及總大小(類也是這樣算)
結構體的特性
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匯編代碼一樣.