Welcome 微信登录
编程资源 图片资源库 蚂蚁家优选 PDF转换器

首页 / 操作系统 / 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 ", x, y);
    int c = 0xcccc;
    return c;
} int main()
{    printf("I am main ");
    int a = 0xaaaa;
    int b = 0xbbbb;
    func(a, b);
    printf("I should run here ");
    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 ");
00A118FE  push        offset string "I am main " (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 ");
00A11929  push        offset string "I should run here " (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 ");2 00A118FEpushoffset string "I am main " (0A16CF0h)3 00A11903call_printf (0A1132Ah)4 00A11908add esp,4这部分就是调用printf的系统调用,因为库函数更多是对操作系统调用的再一次调用(封装?的说法也可以),因为我不是很懂这部分,也就不详细解释其中_printf的系统调用究竟怎么工作了int a = 0xaaaa;00A1190Bmov dword ptr [a],0AAAAhint b = 0xbbbb;00A11912mov dword ptr [b],0BBBBh赋值阶段,这里给了双字,所以是dword 通过指针赋值~,ptr就是指针,mov dst src就是把后面的给前面的,就是dst=src这样的1 func(a, b);2 00A11919mov eax,dword ptr [b]3 00A1191Cpusheax联合上一句的赋值语句构成参数压栈 y=b4 00A1191Dmov ecx,dword ptr [a]5 00A11920pushecx联合上一句的赋值语句构成参数压栈 x=a6 00A11921callfunc (0A11366h)call函数调用,把fun函数的地址call一下7 00A11926add esp,8push了这么多不得把栈顶指针挪一挪?重头戏来了,这就是这次要讲述的主要部分,函数调用时候的栈帧!令人惊讶的是传的实参是放在main函数栈帧中的。我们来结合func的汇编看一下 1 int func(int x, int y) 2 { 3 00A11770pushebp 4 00A11771mov ebp,esp 5 00A11773sub esp,0D8h 6 00A11779pushebx 7 00A1177Apushesi 8 00A1177Bpushedi 9 00A1177Clea edi,[ebp-0D8h]10 00A11782mov ecx,36h11 00A11787mov eax,0CCCCCCCCh12 00A1178Crep stosdword ptr es:[edi]13 int *p = &x;14 00A1178Elea eax,[x]15 00A11791mov dword ptr [p],eax16 p--;17 00A11794mov eax,dword ptr [p]18 00A11797sub eax,419 00A1179Amov dword ptr [p],eax20 *p = (int)bug;21 00A1179Dmov eax,dword ptr [p]22 00A117A0mov dword ptr [eax],offset bug (0A1127Bh)23 printf("x:%d,y:%d ", x, y);24 00A117A6mov eax,dword ptr [y]25 00A117A9pusheax26 00A117AAmov ecx,dword ptr [x]27 00A117ADpushecx28 00A117AEpushoffset string "x:%d,y:%d " (0A16B3Ch)29 00A117B3call_printf (0A1132Ah)30 00A117B8add esp,0Ch31 int c = 0xcccc;32 00A117BBmov dword ptr [c],0CCCCh33 return c;34 00A117C2mov eax,dword ptr [c]35 } 1 int func(int x, int y) 2 { 3 00A11770pushebp 4 00A11771mov ebp,esp 5 00A11773sub esp,0D8h 6 00A11779pushebx 7 00A1177Apushesi 8 00A1177Bpushedi 9 00A1177Clea edi,[ebp-0D8h]10 00A11782mov ecx,36h11 00A11787mov eax,0CCCCCCCCh12 00A1178Crep stosdword ptr es:[edi]没错了这一部分就是保存main函数的状态了,至于它保存了哪些main函数的状态,通过哪些寄存器保存的这里就不详细说明了(使用push命令的一般都是保存状态用的),刚才说的在这里上图,按步骤阅读更佳
  1. 这是func头两步的汇编指令1 00A11770pushebp2 00A11771mov 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 ", x, y);7 int c = 0xcccc;8 return c;9 }别着急!还没结束!汇编解释来了! 1 int *p = &x; 2 0009178Elea eax,[x]这就是取偏移地址,取得x对于当前ebp的偏移地址 3 00091791mov dword ptr [p],eax简单赋值 4 p--; 5 00091794mov eax,dword ptr [p]看他把寄存器来回赋值的,其实就是将把地址减个4 6 00091797sub eax,4 7 0009179Amov dword ptr [p],eax 8 *p = (int)bug; 9 0009179Dmov eax,dword ptr [p]把函数bug的地址传过来赋值10 000917A0mov dword ptr [eax],offset bug (09127Bh)offset也是取偏移的作用还是和lea有些不同的11 printf("x:%d,y:%d ", x, y);12 000917A6mov eax,dword ptr [y]这就不说了是个系统调用,因为我也不是很懂13 000917A9pusheax14 000917AAmov ecx,dword ptr [x]15 000917ADpushecx16 000917AEpushoffset string "x:%d,y:%d " (096B3Ch)17 000917B3call_printf (09132Ah)18 000917B8add esp,0Ch19 int c = 0xcccc;20 000917BBmov dword ptr [c],0CCCCh创建的局部变量位置在ebp下面~看图!21 return c;22 000917C2mov eax,dword ptr [c]没看到形参对不对?就两个实参,写完了不就改了么?不对哦~x = 10;000A178Emov dword ptr [x],0Ahy = 10;000A1795mov dword ptr [y],0Ah我把代码改成这样看会变,这里并没有更改之前保存的寄存器里的东西,是取得了新的部分哦dword ptr [x]这个已经不是之前的eax或者是ebx了~ 本文永久更新链接地址:http://www.linuxidc.com/Linux/2016-06/132586.htm