X86_64 栈和函数调用

引言

  大家都知道函数调用是通过栈来实现的,而且知道在栈中存放着该函数的局部变量。但是对于栈的实现细节可能不一定清楚。本文将介绍一下在 x86 平台下函数栈是如何实现的。

1、x86-64 汇编

  Intel 系列处理器通常称为x86,目前常用的笔记本或台式机都是 64 位的处理器,这些处理器使用的机器语言一般都是 x86_64,我记得以前学习微机原理课的时候,学习的还是 8086 处理器上的汇编。8086 是Intel的第一代16位的处理器,只有8个16位的寄存器,而现在的 64 位处理器对其进行了扩展,共有16个64位的寄存器。

需要注意的是,这里采用的汇编代码是 ATT 格式的,与 Intel 格式的汇编码有些不同:

  1. Intel汇编码省略了指示大小的后缀,如ATT格式中的pushq,在Intel中为push
  2. Intel代码访问寄存器时省略了%,如ATT格式是%rbx,在Intel中为rbx
  3. ATT代码使用小括号( )来访问内存中的位置,而Intel代码使用中括号[ ]
  4. 操作数的顺序不同,如 ATT 中 movq %rax, %rbx,第一个操作数为源操作数,第二个为目的操作数,Intel格式正好相反。

64位处理器中 16 个寄存器对理解 x86-64 汇编十分重要,见下图(图源 CSAPP):

在这里插入图片描述

  • %rax 作为函数返回值使用。
  • %rsp 栈指针寄存器,指向栈顶
  • %rbp 栈桢指针,指向栈基
  • %rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数。。。
  • %rbx,%r12,%r13,%14,%15 用作数据存储,遵循被调用者使用规则,简单说就是随便用,调用子函数之前要备份它,以防他被修改
  • %r10,%r11 用作数据存储,遵循调用者使用规则,简单说就是使用之前要先保存原值
  • %rip: 相当于PC指针指向当前的指令地址,指向下一条要执行的指令

2、运行时栈

  考虑C语言中的函数调用问题,当在一个程序中调用另一个程序的时候,如在函数 P 中调用函数Q,至少需要做以下几件事情:

  • 首先需要将程序计数器指针指向 Q 的起始地址,在 Q 函数返回的时候需要将程序计数器指向 P 中函数 Q 后面的那条指令;
  • 其次需要完成函数参数传递问题,以及函数返回值的传递问题;
  • 最后是函数Q运行时可能需要保存额外的局部变量。

为了解决上述的问题,C语言调用时采用「运行时栈」来对函数的调用过程进行维护:
在这里插入图片描述
需要注意这里的栈是倒着画的,栈顶在下面,栈顶的内存地址是更小的,换句话说栈增长的方向是内存地址减小的方向。

从上面图中我们可以看出,在函数 P 调用 Q 时,首先在函数 P 的「栈帧」压入了返回地址(Return address),指示函数 Q 执行完成之后的断点位置。然后函数 Q 执行的时候会开辟它自己的栈帧(实际上就是将栈顶指针%rsp减小),并可能会做以下几件事情:

  • 将一些必要的寄存器的值进行保存——(Saved register)(这些寄存器称为被调用者保存的寄存器(Callee saved),也就是函数 Q 有义务保证自己运行前后这些寄存器的值是保持不变的,因此函数 P 可以放心的将变量存储在这些寄存器中)
  • 保存 Q 中的一些局部变量(Local variables)
  • 若 Q 需要调用其它函数,且该函数的参数大于6个,则在 Q 的栈帧中存储这些参数的值。(后面会有示例)

2.1 栈帧

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

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

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

2.2 代码讲解

/* proc.c */
void func1()
{
}

int func2(int a, long b, char *c)
{
    *c = a * b;
    func1();
    return a * b;
}

int main()
{
    char value;
    int rc = func2(1, 2, &value);
}

研究一个简单的函数示例对理解该过程有帮助.

liangjie@liangjie-virtual-machine:~/Desktop/cfp$ gcc proc.c -o proc
liangjie@liangjie-virtual-machine:~/Desktop/cfp$ objdump -d proc
......
0000000000001149 <func1>:
    1149:	f3 0f 1e fa          	endbr64 
    114d:	55                   	push   %rbp
    114e:	48 89 e5             	mov    %rsp,%rbp
    1151:	90                   	nop
    1152:	5d                   	pop    %rbp
    1153:	c3                   	ret    

0000000000001154 <func2>:
    1154:	f3 0f 1e fa          	endbr64 
    1158:	55                   	push   %rbp
    1159:	48 89 e5             	mov    %rsp,%rbp
    115c:	48 83 ec 18          	sub    $0x18,%rsp
    1160:	89 7d fc             	mov    %edi,-0x4(%rbp)
    1163:	48 89 75 f0          	mov    %rsi,-0x10(%rbp)
    1167:	48 89 55 e8          	mov    %rdx,-0x18(%rbp)
    116b:	8b 45 fc             	mov    -0x4(%rbp),%eax
    116e:	89 c1                	mov    %eax,%ecx
    1170:	48 8b 45 f0          	mov    -0x10(%rbp),%rax
    1174:	89 c2                	mov    %eax,%edx
    1176:	89 c8                	mov    %ecx,%eax
    1178:	0f af c2             	imul   %edx,%eax
    117b:	89 c2                	mov    %eax,%edx
    117d:	48 8b 45 e8          	mov    -0x18(%rbp),%rax
    1181:	88 10                	mov    %dl,(%rax)
    1183:	b8 00 00 00 00       	mov    $0x0,%eax
    1188:	e8 bc ff ff ff       	call   1149 <func1>
    118d:	48 8b 45 f0          	mov    -0x10(%rbp),%rax
    1191:	89 c2                	mov    %eax,%edx
    1193:	8b 45 fc             	mov    -0x4(%rbp),%eax
    1196:	0f af c2             	imul   %edx,%eax
    1199:	c9                   	leave  
    119a:	c3                   	ret    

000000000000119b <main>:
    119b:	f3 0f 1e fa          	endbr64 
    119f:	55                   	push   %rbp
    11a0:	48 89 e5             	mov    %rsp,%rbp
    11a3:	48 83 ec 10          	sub    $0x10,%rsp
    11a7:	64 48 8b 04 25 28 00 	mov    %fs:0x28,%rax
    11ae:	00 00 
    11b0:	48 89 45 f8          	mov    %rax,-0x8(%rbp)
    11b4:	31 c0                	xor    %eax,%eax
    11b6:	48 8d 45 f3          	lea    -0xd(%rbp),%rax
    11ba:	48 89 c2             	mov    %rax,%rdx
    11bd:	be 02 00 00 00       	mov    $0x2,%esi
    11c2:	bf 01 00 00 00       	mov    $0x1,%edi
    11c7:	e8 88 ff ff ff       	call   1154 <func2>
    11cc:	89 45 f4             	mov    %eax,-0xc(%rbp)
    11cf:	b8 00 00 00 00       	mov    $0x0,%eax
    11d4:	48 8b 55 f8          	mov    -0x8(%rbp),%rdx
    11d8:	64 48 2b 14 25 28 00 	sub    %fs:0x28,%rdx
    11df:	00 00 
    11e1:	74 05                	je     11e8 <main+0x4d>
    11e3:	e8 68 fe ff ff       	call   1050 <__stack_chk_fail@plt>
    11e8:	c9                   	leave  
    11e9:	c3                   	ret  
......
2.2.1 func1

func1 函数比较简单,由 func1 先讲起。 func1 主要关注函数调用关系流程。

func2 中的 call 指令意为:
将 func2 () 中下一条语句的地址(也就是「mov -0x10(%rbp),%rax」这句的地址)压入栈中并修改 rip 的值为 func1() 的地址

  1188:	e8 bc ff ff ff       	call   1149 <func1>

在这里插入图片描述

再看函数 func1,

0000000000001149 <func1>:
    1149:	f3 0f 1e fa          	endbr64 
    114d:	55                   	push   %rbp         # 将 rbp 压栈
    114e:	48 89 e5             	mov    %rsp,%rbp	# 将 rsp -> rbp 
    1151:	90                   	nop
    1152:	5d                   	pop    %rbp			# 将 rbp 弹出
    1153:	c3                   	ret    				
  • 通过执行push %rbp,使用将 rbp 压栈的方式,保存 rbp
  • 通过执行 mov %rsp,%rbp,原来的栈底(rbp 指向的位置)成为了新的栈顶(rsp 指向的位置)

到这里,函数栈情况如下图
在这里插入图片描述

  • 通过执行pop %rbp,将 rbp 弹出,恢复调用函数的栈基址寄存器 rbp
  • 通过执行ret,根据 rsp 指向的位置从栈中弹出返回位置(也就是上面 call 指令中压栈的值),并通过修改 rip 的值为返回地址。到这里,函数 func1 执行结束,转而返回到 func2 中
2.2.2 func2

我们再看看 func2,func2 则主要关注函数传参以及局部变量的存储。

0000000000001154 <func2>:
    1154:	f3 0f 1e fa          	endbr64 
    1158:	55                   	push   %rbp					 # rbp 入栈保存值
    1159:	48 89 e5             	mov    %rsp,%rbp			 # 修改 rbp 指向地址为 rsp
    115c:	48 83 ec 18          	sub    $0x18,%rsp			 # 为 func2 函数的入参分配空间
    1160:	89 7d fc             	mov    %edi,-0x4(%rbp)		 # edi -> (rbp - 4)
    1163:	48 89 75 f0          	mov    %rsi,-0x10(%rbp)		 # rsi -> (rbp - 16)
    1167:	48 89 55 e8          	mov    %rdx,-0x18(%rbp)		 # rdx -> (rbp - 24)
    116b:	8b 45 fc             	mov    -0x4(%rbp),%eax       # (rbp - 4) -> eax
    116e:	89 c1                	mov    %eax,%ecx			 # eax -> ecx
    1170:	48 8b 45 f0          	mov    -0x10(%rbp),%rax      # (rbp - 16)-> rax
    1174:	89 c2                	mov    %eax,%edx			 # eax -> edx
    1176:	89 c8                	mov    %ecx,%eax			 # ecx -> eax
    1178:	0f af c2             	imul   %edx,%eax			 # edx * eax -> eax
    117b:	89 c2                	mov    %eax,%edx		     # eax -> edx
    117d:	48 8b 45 e8          	mov    -0x18(%rbp),%rax		 # (rbp - 18)-> rax
    1181:	88 10                	mov    %dl,(%rax)			 # dl -> (rax)
    1183:	b8 00 00 00 00       	mov    $0x0,%eax
    1188:	e8 bc ff ff ff       	call   1149 <func1>
    118d:	48 8b 45 f0          	mov    -0x10(%rbp),%rax		 # (rbp - 16)-> rax
    1191:	89 c2                	mov    %eax,%edx			 # eax -> edx
    1193:	8b 45 fc             	mov    -0x4(%rbp),%eax		 # (rbp - 4)-> eax
    1196:	0f af c2             	imul   %edx,%eax			 # edx * eax -> eax
    1199:	c9                   	leave  
    119a:	c3                   	ret   

在这里插入图片描述

  • 通过以下片段,对 func2 入参进行压栈。这里需要注意的是,为了栈对齐,参数 b 放到了 rbp - 16 的位置,而不是 rbp - 12
    115c:	48 83 ec 18          	sub    $0x18,%rsp			 # 为 func2 函数的入参分配空间
    1160:	89 7d fc             	mov    %edi,-0x4(%rbp)		 # edi -> (rbp - 4)
    1163:	48 89 75 f0          	mov    %rsi,-0x10(%rbp)		 # rsi -> (rbp - 16)
    1167:	48 89 55 e8          	mov    %rdx,-0x18(%rbp)		 # rdx -> (rbp - 24)
  • 通过以下片段,完成 C 语句:*c = a * b; 的功能
    116b:	8b 45 fc             	mov    -0x4(%rbp),%eax       # (rbp - 4) -> eax
    116e:	89 c1                	mov    %eax,%ecx			 # eax -> ecx
    1170:	48 8b 45 f0          	mov    -0x10(%rbp),%rax      # (rbp - 16)-> rax
    1174:	89 c2                	mov    %eax,%edx			 # eax -> edx
    1176:	89 c8                	mov    %ecx,%eax			 # ecx -> eax
    1178:	0f af c2             	imul   %edx,%eax			 # edx * eax -> eax
    117b:	89 c2                	mov    %eax,%edx		     # eax -> edx
    117d:	48 8b 45 e8          	mov    -0x18(%rbp),%rax		 # (rbp - 18)-> rax
    1181:	88 10                	mov    %dl,(%rax)			 # dl -> (rax)
  • 通过以下片段,完成 C 语句:a * b; 的功能,结果存储在 eax 寄存中,作为 func2 函数的返回值
    118d:	48 8b 45 f0          	mov    -0x10(%rbp),%rax		 # (rbp - 16)-> rax
    1191:	89 c2                	mov    %eax,%edx			 # eax -> edx
    1193:	8b 45 fc             	mov    -0x4(%rbp),%eax		 # (rbp - 4)-> eax
    1196:	0f af c2             	imul   %edx,%eax			 # edx * eax -> eax
  • 执行 leave 指令相当于执行如下两条指令:
mov %rbp, %rsp
pop %rbp
  • 通过 ret指令,根据 rsp 指向的位置从栈中弹出返回位置(也就是上面 call 指令中压栈的值),并通过修改 rip 的值为返回地址。到这里,函数 func2 执行结束,转而返回到 main 中

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值