x86_64汇编之五:System V AMD64调用约定下的函数调用

在阅读本文之前,请先阅读 x86_64汇编之四:函数调用、调用约定

之前提到了,System V AMD64调用约定是x86_64 Linux系统上使用最广泛的调用约定,gcc/g++等编译器都默认使用该调用约定。

凡是遵循System V AMD64调用约定的汇编代码,都有一定的固定模式,比如函数开头做哪些处理,函数末尾做哪些处理等等。本文将会分析caller和callee进行函数调用时的通用模板。

请结合https://godbolt.org/z/x8rIGD中的汇编代码阅读本文,效果更佳。

Caller的操作模板

caller在调用callee之前,通常有以下操作:

步骤1:设置参数到寄存器/栈中

	mov xxx, %rdi # 第一个参数
	mov xxx, %rsi # 第二个参数
	mov xxx, %rdx # 第三个参数
	...

步骤2:调用函数

call func # 调用函数 (当前指令寄存器地址入栈 + 执行权调转到func函数的起始地址)

该步骤要特别留意call指令将当前指令寄存器的地址压入栈中了。该步骤执行完后栈指针的状态如下:

在这里插入图片描述

步骤3:callee返回,获取返回值

mov %rax, %rcx # 整型返回值存在%rax中
inc %rcx
... 

从相应的寄存器中取返回值并使用

Callee的操作模板

步骤1:保存caller的%rbp, 设置%rbp指向%rsp

	pushq	%rbp
	movq	%rsp, %rbp

保存caller的%rbp是为了在callee返回之后可以恢复caller的栈帧状态。

执行pushq %rbp后栈指针的状态如下:

在这里插入图片描述

然后执行movq指令将%rbp设置为当前的%rsp对应的位置(如上图所示)。

步骤2:减小%rsp的值,为参数和局部变量腾出空间,然后设置参数和局部变量

# 缩减%rsp, 为参数和局部变量腾出空间
subq $40, %rsp
# 下面将寄存器中的参数放入栈中
mov %rdi, -24(%rbp) # 第一个参数,占8字节
mov %rsi, -32(%rbp) # 第二个参数,占8字节
...

将寄存器中的函数参数放入栈的过程中,前面的参数放在高地址,后面的参数放在低地址。因此从低地址开始看,参数是从右到左入栈的。

注意:如果callee是调用链中最后一个函数,那么它可以不修改%rsp指针,也能实现正确的函数调用,结合步骤3理解。

步骤1和步骤2修改了%rbp, %rsp的内容,回顾一下上一篇博客,%rbp, %rsp是由callee保存的寄存器,也就是由callee负责保存和恢复。前面两个步骤我们对它们作出了修改,后面势必要对恢复它俩原来的值,这也是下面步骤4的内容。

步骤3:保存返回值到%rax/%xmm0寄存器

步骤4:恢复%rbp, %rsp的值

恢复它俩的值分两种情况,callee是调用链中的最后一个函数 以及 callee不是调用链中的最后一个函数

(1) 如果callee是调用链中的最后一个函数 (也就是它没再调用其他函数了),那么首先它不会也没必要修改%rsp指针。正因为它没修改%rsp指针,所以其恢复%rbp, %rsp的代码如下:

popq %rbp
ret

因为它没修改%rsp指针,所以%rsp%rbp指向同一个地址,该地址存放的是caller的%rbp,所以popq就是将之前栈中保存的caller的%rbp再次设置到%rbp中,并且%rsp减去8个字节。减去8个字节后的%rsp指向的地址存放的是返回地址,因此调用ret指令可以返回到caller函数,并且%rsp再减去8。(结合上面的图理解)

(2) 如果callee不是调用链中的最后一个函数,那么它恢复%rbp, %rsp的代码如下:

leave 
retq

leave指令是步骤1的逆操作,它将%rbp的值复制到%rsp,然后恢caller的%rbp的值到%rbp中,同时%rsp减去8个字节。然后,ret指令的操作和(1)一样。(结合上面的图理解)

总结

相信经过上面的分析,相信已经可以对System V AMD64调用约定下的函数调用过程有了一个详细的认识,可以自己编写一段C代码然后用gcc -S命令进行编译获取对应的汇编代码并进行分析。

猜你喜欢

转载自blog.csdn.net/qq_29328443/article/details/107235138