关于C和C++函数调用的参数传递问题(从x86-64汇编角度分析)

关于参数传递的问题:

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指令即可。

猜你喜欢

转载自blog.csdn.net/qq_38600065/article/details/108565120