关于参数传递的问题:
x86-64中有6个寄存器用于保存传入子函数的参数:
寄存器 | 参数 |
---|---|
%rdi | 第一个参数 |
%rsi | 第二个参数 |
%rdx | 第三个参数 |
%rcx | 第四个参数 |
%r8 | 第五个参数 |
%r9 | 第六个参数 |
超过6个以上的参数,需要被保存在调用者的函数栈帧中,通过%ebp的偏移值去获取。
x86-64架构函数栈帧图如下图所示:
long func(long a, long b, long c, long d,
long e, long f, long g, long h){
long sum;
sum = (a + b + c + d + e + f + g + h);
return sum;
}
int main(){
long sum;
sum = func(1, 2, 3, 4, 5, 6, 7, 8);
return 0;
}
生成的汇编代码:
.file "tests.cpp"
.text
.globl func
.type func @function
func:
pushq %rbp
movq %rsp, %rbp
movq %rdi, -24(%rbp) #这里从寄存值中取出传入的参数
movq %rsi, -32(%rbp)
movq %rdx, -40(%rbp)
movq %rcx, -48(%rbp)
movq %r8, -56(%rbp)
movq %r9, -64(%rbp)
movq -32(%rbp), %rax
movq -24(%rbp), %rdx #移进又移出,这不是多此一举吗?请看下面分解
addq %rax, %rdx
movq -40(%rbp), %rax
addq %rax, %rdx
movq -48(%rbp), %rax
addq %rax, %rdx
movq -56(%rbp), %rax
addq %rax, %rdx
movq -64(%rbp), %rax
addq %rax, %rdx
movq 16(%rbp), %rax
addq %rax, %rdx
movq 24(%rbp), %rax
addq %rdx, %rax
movq %rax, -8(%rbp)
movq -8(%rbp), %rax
popq %rbp
ret
main:
pushq %rbp
movq %rsp, %rbp
subq $32, %rsp
movq $8, 8(%rsp) #第8个参数保存在调用者函数栈中
movq $7, (%rsp) #第7个参数保存在调用者函数栈中
movl $6, %r9d #第1-6个参数保存在寄存器中
movl $5, %r8d
movl $4, %ecx
movl $3, %edx
movl $2, %esi
movl $1, %edi
call func
movq %rax, -8(%rbp)
movl $0, %eax
leave
ret
在上面的汇编代码中可以看出,6个以内的函数参数是通过寄存器传递的,超过6个的参数是通过栈传递的。这里给了我们平时编程时的一些启示:尽量使用少于6个的形式参数,并且应该尽量使用指针或引用的方式。
看了上面的代码,可能有同学会有一些困惑:为什么在子函数中要先将保存在寄存器中的参数保存到栈中,再从栈取出到寄存器中进行运算呢?直接使用寄存器中的值进行运算不行吗?当然不行了,道理很简单,就打个比方,要是在子函数中需要使用指针指向某个参数,总不能让指针指向寄存器吧(寄存器是没有内存地址的哦)。
可能有同学又会问如果参数的大小超过64(例如一个结构体或者一个类),而寄存器大小只有64位,这怎么办呢?下面我们通过实验来看看是怎么回事。
class A{
public:
long a;
long b;
};
int func(A a){
return 1;
}
int main(){
int a ,b, c;
a = 1;
b = 2;
A aa;
aa.a = 3;
aa.b = 4;
c = func(aa);
return 0;
}
汇编代码:
.file "tests.cpp"
.text
.globl _func
.type _func, @function
func:
pushq %rbp
movq %rsp, %rbp
movq %rdi, %rax
movq %rsi, %rcx
movq %rcx, %rdx
movq %rax, -16(%rbp)
movq %rdx, -8(%rbp)
movl $1, %eax
popq %rbp
ret
.size _Z4func1A, .-_Z4func1A
.globl main
.type main, @function
main:
pushq %rbp
movq %rsp, %rbp
subq $32, %rsp
movl $1, -4(%rbp)
movl $2, -8(%rbp)
movq $3, -32(%rbp)
movq $4, -24(%rbp)
movq -32(%rbp), %rdx #从下面四行汇编可以看到,将对象A分别保存在rdi和rsi传递参数
movq -24(%rbp), %rax
movq %rdx, %rdi
movq %rax, %rsi
call func
movl %eax, -12(%rbp)
movl $0, %eax
leave
ret
继续增加A类的大小:
class A{
public:
long a;
long b;
long c;
};
int func(A a){
return 1;
}
int main(){
int a ,b, c;
a = 1;
b = 2;
A aa;
aa.a = 3;
aa.b = 4;
aa.c = 5;
c = func(aa);
return 0;
}
生成的汇编代码如下:
.file "tests.cpp"
.text
.globl func
.type func, @function
func:
pushq %rbp
movq %rsp, %rbp
movl $1, %eax
popq %rbp
ret
.size func, .-func
.globl main
.type main, @function
main:
pushq %rbp
movq %rsp, %rbp
subq $72, %rsp
movl $1, -4(%rbp)
movl $2, -8(%rbp)
movq $3, -48(%rbp)
movq $4, -40(%rbp)
movq $5, -32(%rbp)
movq -48(%rbp), %rax #编译器将对象A
压入栈中传递参数
movq %rax, (%rsp)
movq -40(%rbp), %rax
movq %rax, 8(%rsp)
movq -32(%rbp), %rax
movq %rax, 16(%rsp)
call func
movl %eax, -12(%rbp)
movl $0, %eax
leave
ret
以上,当传递的参数大小较大时,可能会拆分到多个寄存器中或者直接压栈传递参数。
将函数形参改为指针或引用又是什么情况呢??
class A{
public:
long a;
long b;
long c;
};
int func(A *a){
return 1;
}
int main(){
int a ,b, c;
a = 1;
b = 2;
A aa;
aa.a = 3;
aa.b = 4;
aa.c = 5;
c = func(&aa);
return 0;
}
生成的汇编代码:
.file "tests.cpp"
.text
.globl _Z4funcP1A
.type _Z4funcP1A, @function
_Z4funcP1A:
pushq %rbp
movq %rsp, %rbp
movq %rdi, -8(%rbp)
movl $1, %eax
popq %rbp
ret
.size _Z4funcP1A, .-_Z4funcP1A
.globl main
.type main, @function
main:
pushq %rbp
movq %rsp, %rbp
subq $48, %rsp
movl $1, -4(%rbp)
movl $2, -8(%rbp)
movq $3, -48(%rbp)
movq $4, -40(%rbp)
movq $5, -32(%rbp)
leaq -48(%rbp), %rax #leaq将-48(%rbp)即对象aa的地址赋给rdi
movq %rax, %rdi
call _Z4funcP1A
movl %eax, -12(%rbp)
movl $0, %eax
leave
ret
可以看出,直接以指针的方式传递参数,效率更高(免去了很多压栈的操作,尤其是传递的参数大小较大时)。
传引用:
class A{
public:
long a;
long b;
long c;
};
int func(A &a){
return 1;
}
int main(){
int a ,b, c;
a = 1;
b = 2;
A aa;
aa.a = 3;
aa.b = 4;
aa.c = 5;
c = func(aa);
return 0;
}
生成的汇编代码:
.file "tests.cpp"
.text
.globl _Z4funcR1A
.type _Z4funcR1A, @function
_Z4funcR1A:
pushq %rbp
movq %rsp, %rbp
movq %rdi, -8(%rbp)
movl $1, %eax
popq %rbp
ret
.size _Z4funcR1A, .-_Z4funcR1A
.globl main
.type main, @function
main:
pushq %rbp
movq %rsp, %rbp
subq $48, %rsp
movl $1, -4(%rbp)
movl $2, -8(%rbp)
movq $3, -48(%rbp)
movq $4, -40(%rbp)
movq $5, -32(%rbp)
leaq -48(%rbp), %rax #leaq将-48(%rbp)即对象aa的地址赋给rdi
movq %rax, %rdi
call _Z4funcR1A
movl %eax, -12(%rbp)
movl $0, %eax
leave
ret
从汇编代码的角度来看,传递指针和传递引用没有什么分别,都是要取地址。实际上,大家都知道指针是一个地址,它可以指向其它的对象,而引用只是一个别名,只能初始化绑定对象。
稍微具体一点说:
指针参数传递本质上是值传递,它所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成了实参的一个副本(替身)。值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变)。
引用参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。
小结:
- 参数会通过寄存器和压栈的形式传递。
- 参数数量尽量少于6个,毕竟读取寄存器的速度要比读取内存要快很多。
- 尽量使用指针或引用的形式来传递参数,尤其是参数大小较大时。
还有一个小细节值得注意:
就是在汇编代码中,每一个函数的模板几乎都是:
func:
pushq %rbp
movq %rsp, %rbp
subq %N, %rsp #预留栈空间,rsp指向下一个栈帧的起始地址
...
leave
ret
而如果在一个函数中没有再调用其它的子函数,则其汇编模板是这样的:
func:
pushq %rbp
movq %rsp, %rbp
...
popq %rbp
ret
可以看到少了subq和leave两条指令。这里试着做一个合理的解释,我的理解是rsp实际上指向下一个调用的函数栈帧的起点,如果没有下一个调用的函数自然就不需要减rsp,同理函数结尾也不要用leave指令,直接popq %rbp后再加个ret指令即可。