1、栈和函数调用的基本概念

栈(FIFO):在数据结构中是一个特殊的容器,遵守先入栈的数据后出栈。在计算机系统中是一个具有以上属性的动态内存区域。栈总是向下增长,压栈操作使栈顶地址减小,弹出操作使栈顶地址增大。
每个进程都会有自己的栈空间,而进程中的各个函数也会维护自己本身的一个栈的区域,这个区域包含了函数调用所需要维护的信息,这个区域常常被称为栈帧或活动记录,堆栈帧一般包含如下几个方面:
1、函数返回地址和参数
2、临时变量:包含函数的非静态局部变量以及编译器自动生成的其他临时变量
3、保存存的上下文:包含在函数调用前后需要保持不变的寄存器
在x86中,常用ebp和esp记录范围,esp记录栈顶位置,ebp指向栈帧里面的一个固定位置,通过ebp加减地址,访问栈帧位置
函数调用过程:
1、把所有或一部分参数压入栈中,如果有其他参数没有入栈,那么使用某些特定的寄存器传递。
2、把当前指令的下一条指令地址压入栈中。
3、跳转到函数体执行。

2、示例

2.1、通用栈帧结构

上一个栈帧RBP/EBP存储的是调用者(上一个函数的rbp地址)。当前的rbp寄存器存储的是上一个栈帧RBP/EBP的地址(类似二级指针)

android 查看调用栈 调用栈信息_c++


在x86系统的CPU中,rsp是栈指针寄存器,这个寄存器中存储着栈顶的地址。 rbp中存储着栈底的地址。 函数栈空间主要是由这两个寄存器来确定的

当程序运行时,栈指针RSP可以移动,栈指针和帧指针rbp一次只能存储一个地址,所以,任何时候,这一对指针指向的是同一个函数的栈帧结构。

而帧指针rbp是不移动的,访问栈中的元素可以用-4(%rbp)或者8(%rbp)访问%rbp指针下面或者上面的元素。

2.2、源代码与反汇编代码

源代码:
#include <stdio.h>

int sum (int a,int b)
{
 int c = a + b;
 return c;
}

int main()
{
 int x = 5,y = 10,z = 0;
 z = sum(x,y);
 printf("%d\r\n",z);
 return 0;
}

反汇编代码:

0000000000000000 <sum>:
   0: 55                    push   %rbp 
   1: 48 89 e5              mov    %rsp,%rbp
   4: 89 7d ec              mov    %edi,-0x14(%rbp) # 参数传递
   7: 89 75 e8              mov    %esi,-0x18(%rbp) # 参数传递
   a: 8b 55 ec              mov    -0x14(%rbp),%edx
   d: 8b 45 e8              mov    -0x18(%rbp),%eax
  10: 01 d0                 add    %edx,%eax 
  12: 89 45 fc              mov    %eax,-0x4(%rbp) # 局部变量
  15: 8b 45 fc              mov    -0x4(%rbp),%eax # 存储结果
  18: 5d                    pop    %rbp
  19: c3                    retq   
000000000000001a <main>:
  1a: 55                    push   %rbp # 保存%rbp。rbp,栈底的地址
  1b: 48 89 e5              mov    %rsp,%rbp # 设置新的栈指针。rsp 栈指针,指向栈顶的地址
  1e: 48 83 ec 10           sub    $0x10,%rsp # 分配 16字节栈空间。%rsp = %rsp-16
  22: c7 45 f4 05 00 00 00  movl   $0x5,-0xc(%rbp) # 赋值
  29: c7 45 f8 0a 00 00 00  movl   $0xa,-0x8(%rbp) # 赋值
  30: c7 45 fc 00 00 00 00  movl   $0x0,-0x4(%rbp) # 赋值
  37: 8b 55 f8              mov    -0x8(%rbp),%edx  
  3a: 8b 45 f4              mov    -0xc(%rbp),%eax 
  3d: 89 d6                 mov    %edx,%esi # 参数传递 ,从右向左
  3f: 89 c7                 mov    %eax,%edi # 参数传递
  41: e8 00 00 00 00        callq  46 <main+0x2c> # 调用sum
  46: 89 45 fc              mov    %eax,-0x4(%rbp) 
  49: 8b 45 fc              mov    -0x4(%rbp),%eax # 存储计算结果
  4c: 89 c6                 mov    %eax,%esi
  4e: 48 8d 3d 00 00 00 00  lea    0x0(%rip),%rdi        # 55 <main+0x3b>
  55: b8 00 00 00 00        mov    $0x0,%eax
  5a: e8 00 00 00 00        callq  5f <main+0x45>
  5f: b8 00 00 00 00        mov    $0x0,%eax 
  64: c9                    leaveq 
  65: c3                    retq

2.3、分析

2.3.1、main函数分析

1)建立了main函数的栈
main函数调用前,调用者会为main函数做准备。首先,函数栈上开辟了16字节的空间,存储定义的3个int型变量,建立了main函数的栈1@:main函数分配,sum函数未分配,sub $0x10,%rsp )

push   %rbp # 保存%rbp。rbp,栈底的地址
mov    %rsp,%rbp # 设置新的栈指针。rsp 栈指针,指向栈顶的地址
sub    $0x10,%rsp # 分配 16字节栈空间。%rsp = %rsp-16

android 查看调用栈 调用栈信息_c++_02


2)会给三个变量进行赋值

接着,先把z压栈,然后y然后x

movl   $0x5,-0xc(%rbp) # 赋值   x=5
movl   $0xa,-0x8(%rbp) # 赋值 y=10
movl   $0x0,-0x4(%rbp) # 赋值 z = 0

android 查看调用栈 调用栈信息_c语言_03


3)参数传递:

我们可以看到是函数参数是倒序传入的:先传入第N个参数,再传入第N-1个参数(CDECL约定)

2@:esi 与edi不能直接接受内存地址的数据(猜测,未验证)

mov    -0x8(%rbp),%edx   # 把y值存到edx
mov    -0xc(%rbp),%eax  # 把x值存到到eax
mov    %edx,%esi # 参数传递 ,从右向左
mov    %eax,%edi # 参数传递

4)调用sum函数执行到call指令处,CALL指令内部其实还暗含了一个将返回地址(即CALL指令下一条指令的地址)压栈的动作(由硬件完成)。具体来说,call指令执行时,先把下一条指令的地址入栈,再跳转到对应函数执行的起始处。

callq  46 <main+0x2c> # 调用sum
2.3.2、sum函数分析

1)压栈rbp,赋值rbp

push %rbp

mov %rsp,%rbp

重要:此时,原rbp寄存器的入栈,新的rbp寄存器的地址指向栈顶。rbp处于一个很重要的位置。

%rbp+4处为返回地址,%rbp+8处为第一个参数值(最后一个入栈的参数值,此处假设其占用4字节内存),%rbp-4处为第一个局部变量,%rbp处为上一层rbp值。

由于rbp中的地址处总是“上一层函数调用时的rbp值”,而在每一层函数调用中,都能通过当时的%rbp值“向上(栈底方向)”能获取返回地址、参数值,“向下(栈顶方向)”能获取函数局部变量值。

android 查看调用栈 调用栈信息_c++

2)传递参数上述指令通过rbp加偏移量的方式将main传递给sum的两个参数保存在当前栈帧的合适位置,然后又取出来放入寄存器,看着有点儿多此一举,这是因为在编译时未给gcc指定优化级别,而gcc编译程序时,默认不做任何优化,所以看起来比较啰嗦。

空出来的4个字节,是为了保证8字节对齐

mov %edi,-0x14(%rbp) # 参数传递 b 这是对的
 mov %esi,-0x18(%rbp) # 参数传递 a 这是对的
 mov -0x14(%rbp),%edx
 mov -0x18(%rbp),%eax

android 查看调用栈 调用栈信息_赋值_05


3)执行运算,并存储结果

第一条指令负责执行加法运算并将并将结果存入eax中,第二条指令将eax中的值存入局部变量c所在的内存,第三条指令将局部变量c的值读取到eax中

add %edx,%eax
 mov %eax,-0x4(%rbp) # 局部变量 c
 mov -0x4(%rbp),%eax # 存储结果

android 查看调用栈 调用栈信息_赋值_06

4)函数返回

pop %rbp
retq

相当于这三条指令

mov %rbp,%rsp
pop %rbp
pop %rip

1.1)首先把rsp赋值,它的值是存储调用函数rbp的值的地址通过出栈操作,来找回调用函数的rbp
1.2)rbp上面就是调用函数调用被调用函数的下一条指令的执行地址,所以需要赋值给rip(指令寄存器
整个函数跳转回main,他的rsp,rbp都会变回原来的main函数的栈指针

2.3.3、sum函数调用后

1)函数结果存储在eax寄存器里,把函数结果赋值给main函数的局部变量的z中

mov %eax,-0x4(%rbp) # 把sum函数的返回值赋给变量z z = sum(x,y);

android 查看调用栈 调用栈信息_c语言_03

2)上述指令首先为printf准备参数,然后调用printf,具体过程和调用sum的过程相似,让CPU直接执行到main倒数第二条leave指令处。

mov    -0x4(%rbp),%eax  #传参
mov    %eax,%esi   #传参
lea    0x0(%rip),%rdi  
mov    $0x0,%eax
callq  5f <main+0x45>

3)将main的返回值赋值给eax

mov    $0x0,%eax

4)恢复main函数调用前的状态
leave指令首先将rbp的值复制给rsp,rsp就指向rbp所指的栈单元。之后leave指令将该栈单元的值pop给rbp,如此,rsp和rbp就恢复成刚进入main时的状态
leaveq
retq
这两条指令相当于,与sum函数退出一样
mov %rbp, %rsp
pop %rbp