歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

函數棧幀(用匯編來剖析)

這次講解一下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. 這是func頭兩步的匯編指令
    1 00A11770  push        ebp  
    2 00A11771  mov         ebp,esp  

    分別是把返回main函數的地址就是push ebp啦,壓棧!,然後把棧頂指針賦值給棧底指針,就把棧底挪過來了,這就是新的棧底了!!因為main棧幀已經告一段落了

  2.  這就是擴展函數棧幀的方式啦,將棧頂指針往後挪動一定的位置1 00A11773 sub esp,0D8h  ,這裡挪動了D8(16進制),剩下的部分就是保存寄存器狀態了,我就不講了

     

簡單來說,兩個棧幀的大概情況就是這樣的

所以很簡單,我們不必通過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了~ 

Copyright © Linux教程網 All Rights Reserved