函数栈帧(用汇编来剖析)


这次讲解一下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了~ 

本文永久更新链接地址

相关内容

    暂无相关文章