看看C语言函数返回值背后的故事

背景

在学习C语言路途中,在各大书籍中,应该都会见到“函数返回值先拷贝到临时寄存器中,再将临时寄存器拷贝到调用函数变量中”。是不是先来个问号三连,为什么这样做呢?有什么好处?为什么不直接拷贝到调用函数变量中,减少拷贝呢?接下来从汇编角度一探究竟。

一,C代码转汇编样例讲解

在x86汇编中,函数调用时,返回值会先存入临时寄存器(如EAX、EBX等),然后再拷贝到调用函数的变量中。由于EAX、EBX  size均为4 字节,即需要根据函数返回值size进行分类分析。

1,函数返回值size<=4

C代码:

#include 

int add(int a, int b)
{
	int c = a + b;
    return c;
}

int main()
{
    int result = add(2, 3);
    printf("The result is: %d\n", result);
    return 0;
}

执行指令

gcc -S -o main.S main.c

生成汇编代码(x86):

	.file	"main.c"
	.text
	.globl	add
	.type	add, @function
add: ;开始定义add函数。
.LFB0: ;标签,表示函数体的开始。
	.cfi_startproc ;表示结束处理当前函数的调用帧信息。
	pushq	%rbp ;将基址指针(rbp)压入栈中。
	.cfi_def_cfa_offset 16 ;设置当前函数的调用帧偏移量为16字节。
	.cfi_offset 6, -16 ;设置寄存器6的偏移量为-16字节。
	movq	%rsp, %rbp ;将栈顶指针(rsp)的值赋给基址指针(rbp)。
	.cfi_def_cfa_register 6 ;设置当前函数的调用帧寄存器为6。
	movl	%edi, -20(%rbp) ;将第一个参数(edi)的值存储到基址指针(rbp)的前20个字节处
	movl	%esi, -24(%rbp) ;将第二个参数(esi)的值存储到基址指针(rbp)的前24个字节处
	movl	-20(%rbp), %edx ;将基址指针(rbp)的前20个字节处的值加载到寄存器edx中
	movl	-24(%rbp), %eax ;将基址指针(rbp)的前24个字节处的值加载到寄存器eax中
	addl	%edx, %eax ;将寄存器edx和eax中的值相加,并将结果存储到寄存器eax中
	movl	%eax, -4(%rbp) ;将寄存器eax中的值存储到基址指针(rbp)的前4个字节处
	movl	-4(%rbp), %eax ;将基址指针(rbp)的前4个字节处的值加载到寄存器eax中
	popq	%rbp ;从栈顶弹出一个值,并将其赋给基址指针(rbp)。
	.cfi_def_cfa 7, 8 ;设置当前函数的调用帧格式为7,偏移量为8字节。
	ret ;返回到调用者。
	.cfi_endproc ;表示结束处理当前函数的调用帧信息。
.LFE0:
	.size	add, .-add
	.section	.rodata
.LC0:
	.string	"The result is: %d\n"
	.text
	.globl	main
	.type	main, @function
main:
.LFB1:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp ;将栈顶指针(rsp)减去16字节,以保留足够的空间来存储局部变量。
	movl	$3, %esi ;将3赋值给寄存器esi
	movl	$2, %edi ;将2赋值给寄存器edi
	call	add ;调用add函数,并将返回值存储在寄存器eax中
	movl	%eax, -4(%rbp) ;将寄存器eax中的值存储到基址指针(rbp)的前4个字节处
	movl	-4(%rbp), %eax ;将基址指针(rbp)的前4个字节处的值加载到寄存器eax中
	movl	%eax, %esi ;将寄存器eax中的值赋给寄存器esi
	leaq	.LC0(%rip), %rdi
	movl	$0, %eax ;将0赋值给寄存器eax
	call	printf@PLT ;调用printf函数,将字符串常量和寄存器esi中的值作为参数
	movl	$0, %eax ;将0赋值给寄存器eax
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE1:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
	.section	.note.GNU-stack,"",@progbits

C用例转换为汇编,很清楚看到 函数返回值size<=4赋值给调用函数变量 过程:

1)先将局部变量值拷贝到寄存器eax;

2)再将寄存器eax值拷贝到调用函数变量。

2,函数返回值4

C代码: 

#include 

struct coord_struct {
	int x;
	int y;
};

struct coord_struct get_coord(int x, int y)
{
	struct coord_struct ret;
	ret.x = x;
	ret.y = y;
    return ret;
}

int main()
{
    struct coord_struct result = get_coord(2, 3);
    return 0;
}

 生成汇编代码(x86):

	.file	"main.c"
	.text
	.globl	get_coord
	.type	get_coord, @function
get_coord:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	%edi, -20(%rbp)
	movl	%esi, -24(%rbp)
	movl	-20(%rbp), %eax ;将基指针寄存器rbp的-20字节处的值加载到eax寄存器。
	movl	%eax, -8(%rbp) ;将eax寄存器的值存储到基指针寄存器rbp的-8字节处
	movl	-24(%rbp), %eax ;将基指针寄存器rbp的-24字节处的值加载到eax寄存器。
	movl	%eax, -4(%rbp) ;将eax寄存器的值存储到基指针寄存器rbp的-4字节处。
	movq	-8(%rbp), %rax ;将基指针寄存器rbp的-8字节处的值加载到rax寄存器。
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	get_coord, .-get_coord
	.globl	main
	.type	main, @function
main:
.LFB1:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp
	movl	$3, %esi
	movl	$2, %edi
	call	get_coord
	movq	%rax, -8(%rbp)
	movl	$0, %eax
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE1:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
	.section	.note.GNU-stack,"",@progbits

rax寄存器:在x86-64架构中,rax寄存器是一个64位(8字节)的寄存器。

函数返回值4赋值给调用函数变量 过程同size<=4,只是将size为4字节的寄存器eax换成

8字节rax寄存器。

3,函数返回值size巨大

C代码: 

#include 

struct large_struct {
	int data[128];
};

struct large_struct get_data()
{
	struct large_struct ret;
    return ret;
}

int main()
{
    struct large_struct result = get_data();
    return 0;
}

 生成汇编代码(x86):

	.file	"main.c"
	.text
	.globl	get_data
	.type	get_data, @function
get_data:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$544, %rsp ;栈指针减去544字节
	movq	%rdi, -536(%rbp) ;将参数`%rdi`的值存储在基址指针(Base Pointer)-536字节处
	movq	%fs:40, %rax ;从FS段的40字节处获取值,并将其存储在寄存器RAX中
	movq	%rax, -8(%rbp) ;将寄存器RAX的值存储在基址指针(Base Pointer)-8字节处
	xorl	%eax, %eax ;将寄存器EAX的值清零
	movq	-536(%rbp), %rax ;将基址指针(Base Pointer)-536字节处的值加载到寄存器RAX中
	movq	%rax, %rdx ;将寄存器RAX的值复制到寄存器RDX中
	leaq	-528(%rbp), %rax ;将基址指针(Base Pointer)-528字节处的值加载到寄存器RAX中
	movl	$512, %ecx ;将常量512加载到寄存器ECX中
	movq	(%rax), %rsi ;
	movq	%rsi, (%rdx)
	movl	%ecx, %esi ;将寄存器ECX的值复制到寄存器ESI中
	addq	%rdx, %rsi ;将寄存器RDX的值加到寄存器RSI上
	leaq	8(%rsi), %rdi ;将寄存器RSI加8后的值加载到寄存器RDI中
	movl	%ecx, %esi
	addq	%rax, %rsi ;将寄存器RAX的值加到寄存器RSI上
	addq	$8, %rsi ;将寄存器RSI的值加8
	movq	-16(%rsi), %rsi
	movq	%rsi, -16(%rdi)
	leaq	8(%rdx), %rdi
	andq	$-8, %rdi
	subq	%rdi, %rdx
	subq	%rdx, %rax
	addl	%edx, %ecx
	andl	$-8, %ecx
	shrl	$3, %ecx
	movl	%ecx, %edx
	movl	%edx, %edx
	movq	%rax, %rsi
	movq	%rdx, %rcx
	rep movsq ;重复执行`movsq`指令,将寄存器RSI和RDX中的值复制到寄存器RAX和RDX指向的位置
	movq	-536(%rbp), %rax ;将基址指针(Base Pointer)-536字节处的值加载到寄存器RAX中
	movq	-8(%rbp), %rdi ;将基址指针(Base Pointer)-8字节处的值加载到寄存器RDI中
	xorq	%fs:40, %rdi ;将FS段的40字节处的值与寄存器RDI进行异或运算
	je	.L3
	call	__stack_chk_fail@PLT
.L3:
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	get_data, .-get_data
	.globl	main
	.type	main, @function
main:
.LFB1:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$528, %rsp ;将栈指针减去528字节,以便为局部变量分配空间
	movq	%fs:40, %rax ;从FS段寄存器(File System Global Offset Table)的第40个条目获取地址,并将其存储在RAX寄存器中
	movq	%rax, -8(%rbp) ;将RAX寄存器中的地址复制到基址寄存器(Base Register)指向的位置,即-8字节处
	xorl	%eax, %eax ;将EAX寄存器中的值清零
	leaq	-528(%rbp), %rax ;计算-528字节处的地址,并将其存储在RAX寄存器中
	movq	%rax, %rdi ;将RAX寄存器中的地址复制到RDI寄存器中
	movl	$0, %eax
	call	get_data
	movl	$0, %eax
	movq	-8(%rbp), %rdx
	xorq	%fs:40, %rdx
	je	.L6
	call	__stack_chk_fail@PLT
.L6:
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE1:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
	.section	.note.GNU-stack,"",@progbits

很明显当函数返回值内存巨大时时,计算将变得极为复制。可以简化理解:

struct large_struct result = get_data();实际是转变成

struct large_struct result;  get_data(&result);

此时又产生一个问题,当main()直接调用get_data(),未创建变量result,又该如何处理?

实际同上一样处理,可自行编写代码验证。

二,总结

三个样例分析,可以回答开始的三个问题:

1,寄存器相当于共享区域,两个函数很容易访问;

2,当返回值内存小时,使用临时寄存器eax或rax,原因是访问速度远比内存快,提升效率;

3,当返回值内存巨大时,为减少内存拷贝,相当于直接传递调用函数变量地址,被调函数返回值直接存入该变量内存中。

古人云,只根只底,不枝不蔓,万法本无间,有心须识见。

咱对待程序也应该如此,只根只底,用起来才放心。

你可能感兴趣的:(C,c语言,开发语言)