[汇编与C语言关系]1.函数调用。函数调用栈 剖析+图解[转]

  对于以下顺序:


int bar(int c, int d)
{
    int e = c + d;
    return e;
}
int foo(int a, int b)
{
    return bar(a, b);
}
int main(void)
{
    foo(2, 3);
    return 0;
}

栈:

每当函数调用时,第一只进栈的凡主函数惨遭函数调用后的下同样长指令(函数调用语句的生一样漫长可实施语句)的地方,然后是函数的次第参数,在大多数的C编译器中,参数是由右为左入栈的,然后是函数中的有的变量。注意静态变量是休入栈的。
当本次函数调用结束后,局部变量先来库,然后是参数,最后栈顶指针指为最好开始满怀的地点,也不怕是主函数着的生一样条指令,程序由该点继续运行。

当起函数调用的上,栈空间被存放的数量是这样的:

  • 1、调用者函数把于调函数所急需之参数按照与被调函数的形参顺序相反的逐一压入栈中,即:从右侧为左依次将为调函数所欲的参数压入栈;
  • 2、调用者函数使用call指令调用被调函数,并拿call指令的下同样长指令的地点正是返回地址压入栈中(这个压栈操作隐含在call指令中);
  • 3、在叫调函数惨遭,被调函数会见事先保存调用者函数的栈底地址(push
    ebp),然后再保存调用者函数的栈顶地址,即:当前深受调函数的栈底地址(mov
    ebp,esp);
  • 4、在让调函数吃,从ebp的位置处在起存放于调函数惨遭的局部变量和临时变量,并且这些变量的地点按定义时的顺序依次减多少,即:这些变量的地方是以栈的延长趋势排列的,先定义之变量先入栈,后定义的变量后可栈;

生函数调用时,入栈的逐条吗:

参数N<br /> 参数N-1 <br />参数N-1<br />参数N-2<br
/>…..<br />参数3<br />参数2<br />参数1<br
/>函数返回地址<br />上同重合调用函数的EBP/BP<br
/>局部变量1<br />局部变量2<br />….<br
/>局部变量N<br />

函数调用栈如下图所示:

必威 1

解释:
//EBP
基址指针,是保留调用者函数的地点,总是指为函数仓库栈底,ESP被调函数的指针,总是指为函数库栈顶。

先,将调用者函数的EBP入栈(pushebp),然后用调用者函数的栈顶指针ESP赋值给于调函数的EBP(作为被调函数的栈底,movebp,esp),此时,EBP寄存器处于一个很重要的职位,该寄存器中存放着一个地点(原EBP入栈后的栈顶),以该地址也原则,向上(栈底方向)能收获返回地址、参数值,向下(栈顶方向)能得函数的一些变量值,而该地址处又存着齐亦然叠函数调用时的EBP值;
相似规律,SS:[ebp+4]处在吧让调函数的返地址,SS:[EBP+8]远在呢传送让吃调函数的率先单参数(最后一个入栈的参数,此处假要该占据4字节内存)的价值,SS:[EBP-4]处呢被调函数惨遭的第一只有变量,SS:[EBP]处于呢上一层EBP值;由于EBP中的地点处总是”上等同重叠函数调用时之EBP值”,而当列一样叠函数调用中,都能由此这的EBP值”向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能收获给调函数的一部分变量值”;
这样递归,就形成了函数调用栈;
Eg函数内局部变量布局示例:

#include <stdio.h>
#include <string.h>
struct C
{
  int a;
  int b;
  int c;
};
int test2(int x, int y, int z)
{
  printf("hello,test2\n");
  return 0;
}
int test(int x, int y, int z)
{
  int a = 1;
  int b = 2;
  int c = 3;
  struct C st;
  printf("addr x = %u\n",(unsigned int)(&x));
  printf("addr y = %u\n",(unsigned int)(&y));
  printf("addr z = %u\n",(unsigned int)(&z));
  printf("addr a = %u\n",(unsigned int)(&a));
  printf("addr b = %u\n",(unsigned int)(&b));
  printf("addr c = %u\n",(unsigned int)(&c));
  printf("addr st = %u\n",(unsigned int)(&st));
  printf("addr st.a = %u\n",(unsigned int)(&st.a));
  printf("addr st.b = %u\n",(unsigned int)(&st.b));
  printf("addr st.c = %u\n",(unsigned int)(&st.c));
  return 0;
} int main(int argc, char** argv)
{
  int x = 1;
  int y = 2;
  int z = 3;
  test(x,y,z);
  printf("x = %d; y = %d; z = %d;\n", x,y,z);
  memset(&y, 0, 8);
  printf("x = %d; y = %d; z = %d;\n", x,y,z);
  return 0;
}

打印输出如下:

addr x = 3220024704
addr y = 3220024708
addr z = 3220024712
addr a = 3220024684
addr b = 3220024680
addr c = 3220024676
addr st = 3220024664
addr st.a = 3220024664
addr st.b = 3220024668
addr st.c = 3220024672
x = 1; y = 2; z = 3;
x = 0; y = 0; z = 3;

局部变量在栈中布局示意图:

必威 2

拖欠图被的有的变量都是于该示例被定义之:

必威 3

本条图片被体现的是一个典型的函数调用栈的内存布局;
<div style=”color:blue”>访问函数的一对变量和做客函数参数的区别:

一对变量总是通过将ebp减去偏移量来访问,函数参数总是通过以ebp加上偏移量来拜访。对于32号变量而言,第一独片变量位于ebp-4,第二个位于ebp-8,以此类推,32各项有变量在栈中形成一个逆序数组;第一只函数参数位于ebp+8,第二只厕ebp+12,以此类推,32员函数参数在栈中形成一个正序数组。</div>
Eg、研究函数调用过程:

#include <stdio.h>

int bar(int c,int d)
{
        int e=c+d;
        return e;
}

int foo(int a,int b)
{
        return bar(a,b);
}

int main(int argc,int argv)
{
        foo(2,3);
        return 0;
}

方是一个老大简短的函数调用过程,整个程序的行进程是main调用foo,foo调用bar。
//查看反汇编文件(要查看编译后的汇编代码,其实还有一样种植方式是gcc -S
text_stack.c,这样才怪成集编代码text_stack.s,而非充分成二进制的对象文件。)

root@wangye:/home/wangye# gcc text_stack.c -g
root@wangye:/home/wangye# objdump -dS a.out 

反汇编结果十分丰富,下面就排有我们关注的有的。

08048394 <bar>:
#include <stdio.h>

int bar(int c,int d)
{
 8048394:   55                      push   %ebp
 8048395:   89 e5                   mov    %esp,%ebp
 8048397:   83 ec 10                sub    $0x10,%esp
    int e=c+d;
 804839a:   8b 45 0c                mov    0xc(%ebp),%eax
 804839d:   8b 55 08                mov    0x8(%ebp),%edx
 80483a0:   8d 04 02                lea    (%edx,%eax,1),%eax
 80483a3:   89 45 fc                mov    %eax,-0x4(%ebp)
    return e;
 80483a6:   8b 45 fc                mov    -0x4(%ebp),%eax
}
 80483a9:   c9                      leave  
 80483aa:   c3                      ret    

080483ab <foo>:

int foo(int a,int b)
{
 80483ab:   55                      push   %ebp
 80483ac:   89 e5                   mov    %esp,%ebp
 80483ae:   83 ec 08                sub    $0x8,%esp
    return bar(a,b);
 80483b1:   8b 45 0c                mov    0xc(%ebp),%eax
 80483b4:   89 44 24 04             mov    %eax,0x4(%esp)
 80483b8:   8b 45 08                mov    0x8(%ebp),%eax
 80483bb:   89 04 24                mov    %eax,(%esp)
 80483be:   e8 d1 ff ff ff          call   8048394 <bar>
}
 80483c3:   c9                      leave  
 80483c4:   c3                      ret    

080483c5 <main>:

int main(int argc,int argv)
{
 80483c5:   55                      push   %ebp
 80483c6:   89 e5                   mov    %esp,%ebp
 80483c8:   83 ec 08                sub    $0x8,%esp
    foo(2,3);
 80483cb:   c7 44 24 04 03 00 00    movl   $0x3,0x4(%esp)
 80483d2:   00 
 80483d3:   c7 04 24 02 00 00 00    movl   $0x2,(%esp)
 80483da:   e8 cc ff ff ff          call   80483ab <foo>
    return 0;
 80483df:   b8 00 00 00 00          mov    $0x0,%eax
}

//我们为此gdb跟踪程序的推行,直到bar函数中的int e = c +
d;语句执行了准备回来时,这时在gdb中打印函数栈帧。

wangye@wangye:~$ gdb text_stack 
GNU gdb (GDB) 7.0.1-debian
Copyright (C) 2009 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i486-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/wangye/text_stack...done.
(gdb) start
Temporary breakpoint 1 at 0x80483cb: file text_stack.c, line 16.
Starting program: /home/wangye/text_stack 

Temporary breakpoint 1, main (argc=1, argv=-1073744732) at text_stack.c:16
16      foo(2,3);
(gdb) s
foo (a=2, b=3) at text_stack.c:11
11      return bar(a,b);
(gdb) s
bar (c=2, d=3) at text_stack.c:5
5       int e=c+d;
(gdb) disassemble 
Dump of assembler code for function bar:
0x08048394 <bar+0>: push   %ebp
0x08048395 <bar+1>: mov    %esp,%ebp
0x08048397 <bar+3>: sub    $0x10,%esp
0x0804839a <bar+6>: mov    0xc(%ebp),%eax
0x0804839d <bar+9>: mov    0x8(%ebp),%edx
0x080483a0 <bar+12>:    lea    (%edx,%eax,1),%eax
0x080483a3 <bar+15>:    mov    %eax,-0x4(%ebp)
0x080483a6 <bar+18>:    mov    -0x4(%ebp),%eax
0x080483a9 <bar+21>:    leave  
0x080483aa <bar+22>:    ret    
End of assembler dump.
(gdb) si
0x0804839d  5       int e=c+d;
(gdb) si
0x080483a0  5       int e=c+d;
(gdb) si
0x080483a3  5       int e=c+d;
(gdb) si
6       return e;
(gdb) si
7   }
(gdb) bt
#0  bar (c=2, d=3) at text_stack.c:7
#1  0x080483c3 in foo (a=2, b=3) at text_stack.c:11
#2  0x080483df in main (argc=1, argv=-1073744732) at text_stack.c:16
(gdb) info re
record     registers  
(gdb) info regi
eax            0x5  5
ecx            0x4c2f5d43   1278172483
edx            0x2  2
ebx            0xb7fcaff4   -1208176652
esp            0xbffff3c8   0xbffff3c8
ebp            0xbffff3d8   0xbffff3d8
esi            0x0  0
edi            0x0  0
eip            0x80483a9    0x80483a9 <bar+21>
eflags         0x282    [ SF IF ]
cs             0x73 115
ss             0x7b 123
ds             0x7b 123
es             0x7b 123
fs             0x0  0
gs             0x33 51
(gdb) info regi
eax            0x5  5
ecx            0x4c2f5d43   1278172483
edx            0x2  2
ebx            0xb7fcaff4   -1208176652
esp            0xbffff3c8   0xbffff3c8
ebp            0xbffff3d8   0xbffff3d8
esi            0x0  0
edi            0x0  0
eip            0x80483a9    0x80483a9 <bar+21>
eflags         0x282    [ SF IF ]
cs             0x73 115
ss             0x7b 123
ds             0x7b 123
es             0x7b 123
fs             0x0  0
gs             0x33 51
(gdb) x/20 $esp
0xbffff3c8: -1073744904 134513689   -1208175868 5
0xbffff3d8: -1073744920 134513603   2   3
0xbffff3e8: -1073744904 134513631   2   3
0xbffff3f8: -1073744776 -1209406298 1   -1073744732
0xbffff408: -1073744724 -1208084392 -1073744800 -1

此地我们同时因此了几乎单新的gdb命令,简单解释一下:info
registers可以显得有寄存器的此时此刻价。在gdb中代表寄存器名时前面要加个$,例如p
$esp可以打印esp寄存器的价值,在上例中esp寄存器的价是0xbffff3c8,所以x/20
$esp命令查看内存中从0xbffff3c8
地址开始的20个32各数。在执行顺序时,操作系统为经过分配一片栈空间来保存函数栈帧,esp寄存器总是指为栈顶,在x86平台达成这个栈是从高地址为小地址增长的,我们懂得每次调用一个函数都要分配一个栈帧来保存参数和有变量,现在我们详细分析这些数据以栈空间的布局,根据gdb的输出结果图示如下:

必威 4

贪图备受每个微方格表示4独字节的内存单元,例如b:
3这个略带方格占的内存地址是0xbffff3f4~0xbffff3f7,把地点写在每个微方格的下面界线上,是为着强调该地方是内存单冠之开端地址。我们由main函数的此处开始看起:

foo(2,3);  
80483cb:    c7 44 24 04 03 00 00    movl   $0x3,0x4(%esp)  
80483d2:    00   
80483d3:    c7 04 24 02 00 00 00    movl   $0x2,(%esp)  
80483da:    e8 cc ff ff ff          call   80483ab <foo>  
return 0;  
80483df:    b8 00 00 00 00          mov    $0x0,%eax  

若果调用函数foo先要将参数准备好,第二只参数保存在esp+4指向的内存位置,第一个参数保存于esp指向的内存位置,可见参数是起右侧为左依次压栈的。然后实施call指令,这个命令发出零星独作用:

  • foo函数调用完以后要回到call的下一样长长的指令继续执行,所以将call的生一致长条指令的地方134513631杀栈,同时把esp的价值减4,esp的价现在凡0xbffff3ec。
  • 修改程序计数器eip,跳反至foo函数的开端执行。

如今扣foo函数的汇编代码:

080483ab <foo>:  

int foo(int a,int b)  
{  
 80483ab:   55                      push   %ebp  
 80483ac:   89 e5                   mov    %esp,%ebp  
 80483ae:   83 ec 08                sub    $0x8,%esp  

push
%ebp指令把ebp寄存器的值压栈,同时将esp的价值减4。esp的价值现在凡是0xbff1c414,下一样条指令把这值传送给ebp寄存器。这点儿久指令合起来是拿原先ebp的值保存在栈上,然后以被ebp赋了新值。在每个函数的栈帧中,ebp指向栈底,而esp指向栈顶,在函数执行过程中esp随着压栈和出栈操作随时变动,而ebp是勿动的,函数的参数与有些变量都是经ebp的价长一个偏移量来聘,例如foo函数的参数a和b分别通过ebp+8和ebp+12来访问。所以下面的命把参数a和b再次压栈,为调用bar函数做准备,然后把返回地址压栈,调用bar函数:

return bar(a,b);  
 80483b1:   8b 45 0c                mov    0xc(%ebp),%eax  
 80483b4:   89 44 24 04             mov    %eax,0x4(%esp)  
 80483b8:   8b 45 08                mov    0x8(%ebp),%eax  
 80483bb:   89 04 24                mov    %eax,(%esp)  
 80483be:   e8 d1 ff ff ff          call   8048394 <bar>  
}  
 80483c3:   c9                      leave    
 80483c4:   c3                      ret   

今昔看bar函数的下令:

int bar(int c,int d)  
{  
 8048394:   55                      push   %ebp  
 8048395:   89 e5                   mov    %esp,%ebp  
 8048397:   83 ec 10                sub    $0x10,%esp  
    int e=c+d;  
 804839a:   8b 45 0c                mov    0xc(%ebp),%eax  
 804839d:   8b 55 08                mov    0x8(%ebp),%edx  
 80483a0:   8d 04 02                lea    (%edx,%eax,1),%eax  
 80483a3:   89 45 fc                mov    %eax,-0x4(%ebp)  

这次又把foo函数的ebp压栈保存,然后为ebp赋了新值,指向bar函数栈帧的栈底,通过ebp+8和ebp+12独家可以拜参数c和d。bar函数还有一个组成部分变量e,可以通过ebp-4来访问。所以后面几长指令的意是将参数c和d取出来有寄存器中开加法,计算结果保存于eax寄存器中,再将eax寄存器存回局部变量e的内存单元。
每当gdb中可以据此bt命令和frame命令查看每层栈帧上的参数和有变量,现在足说明其的干活规律了:如果自己眼前在bar函数中,我可由此ebp找到bar函数的参数和一些变量,也足以找到foo函数的ebp保存在栈上的价值,有矣foo函数的ebp,又可找到其的参数与有些变量,也可找到main函数的ebp保存在栈上的价值,因此各国层函数栈帧通过保留于栈上的ebp的值串起来了。
当今看bar函数的归指令:

return e;  
 80483a6:   8b 45 fc                mov    -0x4(%ebp),%eax  
}  
 80483a9:   c9                      leave    
 80483aa:   c3                      ret   

bar函数有一个int型的归来值,这个返回值是经过eax寄存器传递的,所以率先将e的值读到eax寄存器中。然后实施leave指令,这个令是函数开头的push
%ebp和mov %esp,%ebp的逆操作:

  • 将ebp的值赋给esp,现在esp的价值是0xbffff3d8。
  • 现在esp所指向的栈顶保存在foo函数栈帧的ebp,把这个价恢复吃ebp,同时esp增加4,esp的值变成0xbffff3dc。

最后是ret指令,它是call指令的逆操作:

  • 今esp所指向的栈顶保存在赶回地址,把这个价恢复给eip,同时esp增加4,esp的值变成0xbffff3e0。
  • 改了次计数器eip,因此超过反到回地址0x80483c2继续执行。

地址0x80483c2处是foo函数的返指令:

80483c3:    c9                      leave    
80483c4:    c3                      ret   

双重雷同的经过,又返到了main函数。注意函数调用和归过程被的这些规则:

  • 参数压栈传递,并且是从右边为左依次压栈。
  • ebp总是指于当前栈帧的栈底。
  • 回来值通过eax必威寄存器传递。

这些规则并无是网布局所强加的,ebp寄存器并无是得这样用,函数的参数和返回值也未是必须这么传,只是操作系统和编译器选择了因为这样的方贯彻C代码中之函数调用,这名CallingConvention,Calling
Convention是操作系统二前行制接口规范(ABI,Application
BinaryInterface)的均等组成部分。

  于编译时添加-g选项,用objdump反汇编时可以管C代码和汇编代码通过插起来显示:

必威 5

反汇编的结果充分丰富以下是截取要分析的局部:

必威 6

必威 7

必威 8

  整个程序的尽过程是main调用foo, foo调用bar,
用gdb跟踪程序的行,直到bar函数中的int e = c +
d;语词执行了准备赶回时,这时在gdb中打印函数栈帧。

必威 9

必威 10

 

disassemble可以反汇编当前函数或者指定的函数,单独用disassemble是倒汇编当前函数,如果disassemble后止跟函数称呼或地点则反汇编指定的函数。

s(step)命令可以一行代码一行代码的单步调试,而si命令可以同条指令一条指令的单步调试。bt
列出调用栈

info
registers可以显示有寄存器的当前价值。在gdb中意味着寄存器名时前面要加以个$,例如p
$esp命令查看esp寄存器的值(上图无展示该令),在上例中esp寄存器的价值是0xbff1c3f4,所以x/20
$esp命令查看内存中从0xbff1c3f4地址开始的20独32各项数。在执行顺序时,操作系统也经过分配一片栈空间来囤函数栈帧,esp寄存器总是指于栈顶,,在x86平台达成之栈是从高地址为小地址增长之,每次调用一个函数都如分配一个栈帧来囤参数与有些变量,现在我们分析这些多少是怎么存储的,根据gdb的出口结果图示如下:

必威 11

  途中每个微方格占4独字节,例如b:3此方格的内存地址是0xbf822d20~0xbf822d23。我们从main函数的此处开始看打:

必威 12

  要调用函数foo先要拿参数准备好,第二单参数保存于esp+4所针对的内存位置,第一独参数保存在esp所指向的内存位置,可见参数是自从右侧为左一赖压栈的。然后实施call指令,这个令发出少数只作用:

    1.
foo函数调用完以后如果赶回call的产一致长条指令继续执行,所以管call的下同样长指令的地址0x80483e9压栈,同时将esp的值  减4,esp的值现在是0xbf822d18。

    2. 改动程序计数器eip, 跳转到foo函数的启幕执行。

  现在拘留foo函数的汇编代码:

必威 13

  首先以ebp寄存器的价值压栈,同时将esp的价更减4,esp的值现在是0xbf822d14,然后把这个值传送给ebp寄存器。换句话说即是管原ebp的值保存在栈上,然后还要为ebp赋了新值。在每个函数的栈帧中,ebp指向栈底,esp指向栈顶,在函数执行进程中esp随着压栈和出栈操作随时变动,而ebp是免动的,函数的参数和片变量都是由此ebp的值长一个偏移量来聘的,例如foo函数的参数a和b分别通过ebp+8和ebp+12来拜访,所以下的下令把参数a和b再次压栈,为调用bar函数做准备,然后将返回地址压栈,调用bar函数:

必威 14

必威 15

  现在看bar函数的通令:

必威 16

  这次以拿foo函数的ebp压栈保存,然后于ebp赋了新值,指向bar函数栈帧的栈底,通过ebp+8和ebp+12分头可以拜参数c和d。bar函数还有一个有变量e,可以由此ebp-4来拜会。所以后面几漫漫指令的意是将参数c和d取出来有寄存器中开加法,add指令的测算结果保存于eax寄存器中,再把eax寄存器存回局部变量e的内存单元。

  现在足讲为何以gdb中可以为此bt命令和frame命令查看每个栈帧上之参数与部分变量了:如果自己眼前在bar函数中,我好透过ebp找到bar函数的参数和有变量,也堪找到foo函数的ebp保存在栈上的值,有只foo函数的ebp,又可以找到其的参数与一部分变量,也足以找到main函数的ebp保存在栈上的值,因此各级函数的栈帧通过保留在栈上的ebp的价串起来了。现在看bar函数的回到命令:

必威 17

  bar函数有一个int型的返值,这个返回值是通过eax寄存器传递的,所以首先把e的价读到eax寄存器中。然后实施leave指令,这个令是函数开头的push
%ebp和mov %esp, %ebp的逆操作:

    1. 管ebp的值赋给esp,现在esp的价值是0xbf822d04。

    2.
本esp所指向的栈顶保存在foo函数栈帧的ebp,把这个价值恢复给ebp,同时esp增加4,现在esp的值是0xbf822d08。

  最后是ret指令,它是call指令的逆操作:

    1.
现行esp所指向的栈顶保存在返回地址,把这价恢复为eip,同时esp增加4,现在esp的值是0xbf822d0c。

    2. 修改了程序计数器eip,因此过反至回地址0x80483c2继续执行。

  地址0x80483c2处于是foo函数的回来指令:

必威 18

  还相同的长河,就又返到了main函数。注意函数调用和归经过遭到之这些规则:

    1. 参数压栈传递,并且是自从右侧为左依次压栈。
    2.  ebp 总是指于栈帧的栈底。
    3. 返回值通过 eax 寄存器传递。
  这些规则并无是网布局所强加的, ebp
寄存器并无是须这样用,函数的参数和返回值也未是得这么传,只是操作系统和编译器选择了因为这样的计贯彻C代码中之函数调用,这名Calling
Convention,除了Calling
Convention之外,操作系统还亟需确定多C代码和二进制指令中的接口规范,统称为ABI(Application
Binary Interface)。

相关文章