前言
本篇文章介绍通用寄存器的使用规则,x86-64的通用寄存器就那么十几个,寄存器的说明参考文章x86-64汇编指令支持的通用目的寄存器,而我们的函数调用却可以嵌套很多层,也可以处理很复杂的逻辑,所以,每个寄存器都有他的作用和使用规则
写在前面
- 函数的调用者和被调用者:
函数A中调用函数B,我们说A是调用者,B是被调用者 - 栈帧
栈帧是在函数调用过程中函数负责维护的自己执行过程中使用到的栈空间
,在函数A调用函数B的时候,有下列几种情况需要在运行时栈上分配空间保存数据:- 函数调用过程中,如果寄存器无法全部保存参数数据,就会在运行时栈上分配空间来保存参数数据
- 如果函数存在非静态局部变量,并且无法用寄存器保存,或者使用到了变量的地址,就会在运行时栈上分配空间
- 如果函数存在数组或者结构联合的局部变量,需要分配栈空间保存数据
%rax寄存器
该寄存器最常用的就是作为函数调用的返回值
,一个函数如果有返回值,该返回值一定是通过rax寄存器或者他的低位寄存器返回的
,当然,在函数执行过程中也可以存储别的数据。
%rdi %rsi %rdx %rcx %r8 %r9
这六个寄存器是作为函数传递参数使用的,并且使用规则很简单,如果一个参数能够保存到寄存器,一定是按照标题寄存器的顺序列表依此保存的
,当然参数可能无法使用寄存器保存,比如参数超过了寄存器能保存的数量
看下面的例子:
int add_scale(int a1,int* a2,int a3,int a4,int a5,int a6,int a7,int a8);
int main()
{
int a2 = 2;
add_scale(1,&a2,3,4,5,6,7,8);
return 0;
}
int add_scale(int a1,int* a2,int a3,int a4,int a5,int a6,int a7,int a8)
{
int s = 2.0;
return a1 + (*a2) * s + a3 + a4 + a5 + a6 +a7 + a8;
}
在x86-64机器上生成的main函数的汇编代码如下:
main:
pushq %rbp
movq %rsp, %rbp
subq $32, %rsp
movl $2, -4(%rbp)
leaq -4(%rbp), %rax
movl $8, 8(%rsp) // 第八参数保存到栈
movl $55, (%rsp) // 第七参数保存到栈顶,第七个参数占用八个字节
movl $6, %r9d // 第六参数保存到%r9的低位
movl $5, %r8d // 第五个参数保存到%r8的低位
movl $4, %ecx // 第四个参数保存到%rcx的低位
movl $3, %edx // 第三个参数保存到%rdx的低位
movq %rax, %rsi // 第二个参数的地址保存到%rsi
movl $1, %edi // 第一个参数保存到%rdi的低位
call add_scale
可以看到
- 参数保存的顺序就是按照标题列出的顺序依次保存的
- 超出6个参数后保存到栈里,并且从栈顶依次往后保存
- 通过栈传递参数时,所有的数据大小都向8的倍数对齐
%rbx %rbp %r12 %r13 %r14 %r15
这六个寄存器被称为被调用者保存寄存器
,意思就是被调用函数必须保证调用该函数之前和从该函数返回之前这几个寄存器的值是不变的
。
一般情况下,如果函数中用到哪个寄存器的话,会在函数开始的时候先把该寄存器的值入栈,函数返回之前在把寄存器出栈,这样,函数返回后就能保证寄存器的值没有发生变化。
%rsp
运行时栈顶指针
,与其说rsp保存的是运行时栈顶的位置还不如说,运行时栈顶的位置随着rsp寄存器的值的改变而改变
%r10 %r11
这两个寄存器是被称为调用者保存的寄存器
,意思就是比如函数A调用函数B,函数A中使用了这两个寄存器,但是函数B可以随便修改这两个寄存器的值而不用考虑任何问题,函数A调用函数B之后,需要确保这两个寄存器的值依然可用。