在学习C语言路途中,在各大书籍中,应该都会见到“函数返回值先拷贝到临时寄存器中,再将临时寄存器拷贝到调用函数变量中”。是不是先来个问号三连,为什么这样做呢?有什么好处?为什么不直接拷贝到调用函数变量中,减少拷贝呢?接下来从汇编角度一探究竟。
在x86汇编中,函数调用时,返回值会先存入临时寄存器(如EAX、EBX等),然后再拷贝到调用函数的变量中。由于EAX、EBX size均为4 字节,即需要根据函数返回值size进行分类分析。
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值拷贝到调用函数变量。
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
8字节rax寄存器。
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,当返回值内存巨大时,为减少内存拷贝,相当于直接传递调用函数变量地址,被调函数返回值直接存入该变量内存中。
古人云,只根只底,不枝不蔓,万法本无间,有心须识见。
咱对待程序也应该如此,只根只底,用起来才放心。