前言
当我们调用函数的时候,参数是怎么传递给被调用方的,有想过这个问题吗?传递不同大小的参数对调用方式有影响吗?本文将带你探究这些问题,阅读本文需要对函数栈帧有一定的理解,并了解基本的汇编指令。
文章汇编代码:采用 GCC 8.3.1,对 C 代码使用 -Og 优化级别生成的可执行程序,再用 objdump -d 反汇编的结果。
下表为文章使用到的一些基本汇编操作:
函数名 | 参数 | 操作 |
---|---|---|
mov | Src,Dest | Src -> Dest(寄存器之间) |
mov | Src,(Dest) | Src -> Dest 存储的内存地址处 |
addq | Src,Dest | Dest = Dest + Src |
subq | Src,Dest | Dest = Dest - Src |
imulq | Src,Dest | Dest = Dest * Src |
x86-64 中,4 字节操作后缀为 l,8 字节操作后缀为 q。
寄存器保存
如果参数比较小(4 or 8 bytes),在寄存器中可以放得下,那么前 6 个参数将被放在寄存器中(第一个在 %rdi,第二个在 %rsi,…),多的参数将被放在栈中保存。返回值存放在 %rax。
下图寄存器只能存放整数数据和指针,浮点数使用另外一组单独的寄存器。
来看一段简单的代码:
// 一段简单让两数相乘的代码,写成这种形式,主要是尽量减少编译器优化
long mult2(long a, long b) {
long t = a * b;
return t;
}
void mult_store(long x, long y, long* dest) {
long t = mult2(x, y);
*dest = t;
}
00000000004004d2 <mult2>:
# a in %rdi,b in %rsi
4004d2: mov %rdi,%rax # 把 a 移动到 %rax
4004d5: imul %rsi,%rax # a * b
4004d9: retq
00000000004004da <mult_store>:
# x in %rdi,y in %rsi,dest in %rdx
4004da: push %rbx
4004db: mov %rdx,%rbx # 保存 dest,下文后讲为什么要这么做
4004de: callq 4004d2 <mult2> # 调用 mult2
4004e3: mov %rax,(%rbx) # 将 %rax 里面的返回值
# 移动到 %rbx 保存的 dest 指针指向的内存处
4004e6: pop %rbx
4004e7: retq
保存参数
先看一段代码:
long incr(long* p, long val) {
long x = *p;
long y = x + val;
*p = y;
return x;
}
long call_incr(long x) {
long v1 = 2048;
long v2 = incr(&v1, 1024);
return x + v2;
}
看了上面的代码,你可能会有这样的疑惑:函数的第一个参数保存在 %rdi,call_incr
的 x 存储在 %rdi 中,在调用 incr
时,该寄存器的值已经被修改了,后面又会使用到 x,那该怎么办呢?
编译器有两种策略:
一种是调用方保存,后续我还会使用的参数都会保存在特定寄存器中,不管被调用的函数是否会修改。
一种是被调用方保存,使用时先将保存参数的寄存器的值存储到特定的寄存器,返回前修改回原状态。
下图为保存参数的特定寄存器:
上图寄存器分类只是一种约定,编译器并不一定遵守,编译器可能有自己的使用分类。
00000000004004d2 <incr>:
# p in %rdi,val in %rsi
4004d2: mov (%rdi),%rax
4004d5: add %rax,%rsi
4004d8: mov %rsi,(%rdi)
4004db: retq
00000000004004dc <call_incr>:
# x in %rdi
4004dc: push %rbx
4004dd: sub $0x10,%rsp # %rsp 为栈顶指针,减小意味着为 call_incr 开辟 16 字节栈帧
4004e1: mov %rdi,%rbx # 保存参数 x 到 %rbx
4004e4: movq $0x800,0x8(%rsp) # 将 2024 存储到 %rsp + 8 处
4004ed: mov $0x400,%esi # 将 1024 传到 %esi,即 %rsi 的低 32 位
# movq 指令比 mov 指令占用的字节多
4004f2: lea 0x8(%rsp),%rdi # 把 v1 的地址传到 %rdi,可以看到传参顺序是从右到左
4004f7: callq 4004d2 <incr> # 调用 incr,此时 %rax 保存的值为 v2
4004fc: add %rbx,%rax # 将 x + v2
4004ff: add $0x10,%rsp # 销毁栈帧
400503: pop %rbx
400504: retq
编译器优化
上面讨论的都是比较小的内置类型,那假如对象很大,寄存器放不下该怎么办?
下面介绍一种 C++ 对参数的优化方式,实际编译器并不一定会使用该方式。
class qgw {
// 有默认构造函数、拷贝构造函数、析构函数等
long a1;
long a2;
long a3;
} qgw;
void fun(qgw num);
int main() {
qgw tmp;
...
fun(tmp);
return 0;
}
实际上,编译器可能创建一个临时变量,并修改函数的参数。转化结果可能为:
void fun(qgw& num); // 修改参数为引用
int main() {
qgw tmp;
...
qgw __temp0; // 创建临时对象
__temp0.qgw::qgw(tmp); // 调用拷贝构造
fun(__temp0);
__temp.qgw::~qgw(); // 销毁该临时对象
return 0;
};
经过这样的转化,实际传递的参数变成了对象的地址,就可以保存到寄存器中了。
转载:https://blog.csdn.net/qq_40080842/article/details/128757629