這次講解一下C++函數調用,學了這麼久C語言,肯定聽說過棧(數據結構啊,地址空間的棧啊之類的),函數調用就和棧密切相關。
因為地址空間內的棧是從高地址向低地址生長的,也就是說壓棧順序靠後的反而地址比較低,棧底的地址高於棧頂的地址,下面貼上一段測試代碼
#include<stdio.h>
#include<stdlib.h>
void bug()
{
printf("haha I ma a bug!!");
exit(100);
}
int func(int x, int y)
{
int *p = &x;
p--;
*p = (int)bug;
printf("x:%d,y:%d\n", x, y);
int c = 0xcccc;
return c;
}
int main()
{
printf("I am main\n");
int a = 0xaaaa;
int b = 0xbbbb;
func(a, b);
printf("I should run here\n");
return 0;
}
這段代碼的運行結果,並沒有執行main函數的第二個printf,而是跑到了bug函數中執行,這是因為我修改了函數棧幀中的返回地址部分
本來是打算通過linux系統來看的,但是CentOS7的棧幀實現似乎有些不同,同樣的代碼在centos7上面跑不通。
下面是反匯編
int main()
{
00A118E0 push ebp
00A118E1 mov ebp,esp
00A118E3 sub esp,0D8h
00A118E9 push ebx
00A118EA push esi
00A118EB push edi
00A118EC lea edi,[ebp-0D8h]
00A118F2 mov ecx,36h
00A118F7 mov eax,0CCCCCCCCh
00A118FC rep stos dword ptr es:[edi]
printf("I am main\n");
00A118FE push offset string "I am main\n" (0A16CF0h)
00A11903 call _printf (0A1132Ah)
00A11908 add esp,4
int a = 0xaaaa;
00A1190B mov dword ptr [a],0AAAAh
int b = 0xbbbb;
00A11912 mov dword ptr [b],0BBBBh
func(a, b);
00A11919 mov eax,dword ptr [b]
00A1191C push eax
00A1191D mov ecx,dword ptr [a]
00A11920 push ecx
00A11921 call func (0A11366h)
00A11926 add esp,8
printf("I should run here\n");
00A11929 push offset string "I should run here\n" (0A16CFCh)
00A1192E call _printf (0A1132Ah)
00A11933 add esp,4
return 0;
00A11936 xor eax,eax
}
因為main函數本身真的是個函數!所以在執行我們編寫的程序之前操作系統需要保存當前它運行的狀態,就跟函數調用很類似
1 00A118E0 push ebp 這句話就是把操作系統的狀態壓棧
2 00A118E1 mov ebp,esp 然後把棧底指針挪到新的位置
3 00A118E3 sub esp,0D8h 擴展新的棧幀,你總不能讓新的棧底和棧頂挨在一起吧?
過程圖我會在講到func函數的時候給出來,更容易理解,之後的push之類的就是為了保存現場和執行前准備
1 printf("I am main\n"); 2 00A118FE push offset string "I am main\n" (0A16CF0h) 3 00A11903 call _printf (0A1132Ah) 4 00A11908 add esp,4
這部分就是調用printf的系統調用,因為庫函數更多是對操作系統調用的再一次調用(封裝?的說法也可以),因為我不是很懂這部分,也就不詳細解釋其中_printf的系統調用究竟怎麼工作了
int a = 0xaaaa; 00A1190B mov dword ptr [a],0AAAAh int b = 0xbbbb; 00A11912 mov dword ptr [b],0BBBBh
賦值階段,這裡給了雙字,所以是dword 通過指針賦值~,ptr就是指針,mov dst src就是把後面的給前面的,就是dst=src這樣的
1 func(a, b); 2 00A11919 mov eax,dword ptr [b] 3 00A1191C push eax 聯合上一句的賦值語句構成參數壓棧 y=b 4 00A1191D mov ecx,dword ptr [a] 5 00A11920 push ecx 聯合上一句的賦值語句構成參數壓棧 x=a 6 00A11921 call func (0A11366h) call函數調用,把fun函數的地址call一下 7 00A11926 add esp,8 push了這麼多不得把棧頂指針挪一挪?
重頭戲來了,這就是這次要講述的主要部分,函數調用時候的棧幀!令人驚訝的是傳的實參是放在main函數棧幀中的。我們來結合func的匯編看一下
1 int func(int x, int y) 2 { 3 00A11770 push ebp 4 00A11771 mov ebp,esp 5 00A11773 sub esp,0D8h 6 00A11779 push ebx 7 00A1177A push esi 8 00A1177B push edi 9 00A1177C lea edi,[ebp-0D8h] 10 00A11782 mov ecx,36h 11 00A11787 mov eax,0CCCCCCCCh 12 00A1178C rep stos dword ptr es:[edi] 13 int *p = &x; 14 00A1178E lea eax,[x] 15 00A11791 mov dword ptr [p],eax 16 p--; 17 00A11794 mov eax,dword ptr [p] 18 00A11797 sub eax,4 19 00A1179A mov dword ptr [p],eax 20 *p = (int)bug; 21 00A1179D mov eax,dword ptr [p] 22 00A117A0 mov dword ptr [eax],offset bug (0A1127Bh) 23 printf("x:%d,y:%d\n", x, y); 24 00A117A6 mov eax,dword ptr [y] 25 00A117A9 push eax 26 00A117AA mov ecx,dword ptr [x] 27 00A117AD push ecx 28 00A117AE push offset string "x:%d,y:%d\n" (0A16B3Ch) 29 00A117B3 call _printf (0A1132Ah) 30 00A117B8 add esp,0Ch 31 int c = 0xcccc; 32 00A117BB mov dword ptr [c],0CCCCh 33 return c; 34 00A117C2 mov eax,dword ptr [c] 35 }
1 int func(int x, int y) 2 { 3 00A11770 push ebp 4 00A11771 mov ebp,esp 5 00A11773 sub esp,0D8h 6 00A11779 push ebx 7 00A1177A push esi 8 00A1177B push edi 9 00A1177C lea edi,[ebp-0D8h] 10 00A11782 mov ecx,36h 11 00A11787 mov eax,0CCCCCCCCh 12 00A1178C rep stos dword ptr es:[edi]
沒錯了這一部分就是保存main函數的狀態了,至於它保存了哪些main函數的狀態,通過哪些寄存器保存的這裡就不詳細說明了(使用push命令的一般都是保存狀態用的),剛才說的在這裡上圖,按步驟閱讀更佳
1 00A11770 push ebp 2 00A11771 mov ebp,esp
分別是把返回main函數的地址就是push ebp啦,壓棧!,然後把棧頂指針賦值給棧底指針,就把棧底挪過來了,這就是新的棧底了!!因為main棧幀已經告一段落了
簡單來說,兩個棧幀的大概情況就是這樣的
所以很簡單,我們不必通過y=100這樣的語句就可以對y進行賦值改下代碼就好
1 int func(int x, int y) 2 { 3 int *p = &x; 4 p++; 5 *p = 100; 6 printf("x:%d,y:%d\n", x, y); 7 int c = 0xcccc; 8 return c; 9 }
別著急!還沒結束!匯編解釋來了!
1 int *p = &x; 2 0009178E lea eax,[x] 這就是取偏移地址,取得x對於當前ebp的偏移地址 3 00091791 mov dword ptr [p],eax 簡單賦值 4 p--; 5 00091794 mov eax,dword ptr [p] 看他把寄存器來回賦值的,其實就是將把地址減個4 6 00091797 sub eax,4 7 0009179A mov dword ptr [p],eax 8 *p = (int)bug; 9 0009179D mov eax,dword ptr [p] 把函數bug的地址傳過來賦值 10 000917A0 mov dword ptr [eax],offset bug (09127Bh) offset也是取偏移的作用還是和lea有些不同的 11 printf("x:%d,y:%d\n", x, y); 12 000917A6 mov eax,dword ptr [y] 這就不說了是個系統調用,因為我也不是很懂 13 000917A9 push eax 14 000917AA mov ecx,dword ptr [x] 15 000917AD push ecx 16 000917AE push offset string "x:%d,y:%d\n" (096B3Ch) 17 000917B3 call _printf (09132Ah) 18 000917B8 add esp,0Ch 19 int c = 0xcccc; 20 000917BB mov dword ptr [c],0CCCCh 創建的局部變量位置在ebp下面~看圖! 21 return c; 22 000917C2 mov eax,dword ptr [c]
沒看到形參對不對?就兩個實參,寫完了不就改了麼?不對哦~
x = 10; 000A178E mov dword ptr [x],0Ah y = 10; 000A1795 mov dword ptr [y],0Ah
我把代碼改成這樣看會變,這裡並沒有更改之前保存的寄存器裡的東西,是取得了新的部分哦
dword ptr [x]這個已經不是之前的eax或者是ebx了~