C/C++ 函数原理传参示例详解

x84-64的寄存器

本文所用gccx86-64 gcc 10.1

wiki.cdot.senecacollege.ca/wiki/X86_64…

rax - register a extended

rbx - register b extended

rcx - register c extended

rdx - register d extended

rbp - register base pointer (start of stack)

rsp - register stack pointer (current location in stack, growing downwards)

rsi - register source index (source for data copies)

rdi - register destination index (destination for data copies)

其他寄存器: r8 r9 r10 r11 r12 r13 r14 r15

函数是个什么东西?

一个简单的函数

int func(){}
int main() {
 int x = 2;
 func();
}
main:
 pushq %rbp
 movq %rsp, %rbp
 subq $16, %rsp
 movl $2, -4(%rbp)
 call func()
 movl $0, %eax
 leave
 ret

分配空间动作如下所示:

这里加了个函数调用是因为在有些时候,没有函数调用,就不会使用subq $16, %rsp 这一条指令,我的猜想是既然你都是栈顶的,并且不会再有rbp的变化,那么栈顶以上的元素我都可以随便用。
并且我们观察可以得知,分配栈空间时,他是分配的16个字节,也就是说,有对齐
返回时,弹出栈顶,就可以恢复到上一个栈帧的状态了。

传参姿势

入栈规则

c/c++ 中规定的函数压栈顺序是从右到左,当然,如果你是 Visual C/C++的话,它们有更多的玩法 比如:

template<typename T>
T val(T t) {
 cout << t << endl;
 return t;
}
signed main() {
 printf("%d%d%d", val(1), val(2), val(3));
 return 0;
}

结果

3
2
1
123

看看汇编

int func(int x, int y, int z) {
 return 0;
}
int main() {
 func(1, 2, 3);
}

生成的汇编

func(int, int, int):
 pushq %rbp
 movq %rsp, %rbp
 movl %edi, -4(%rbp)
 movl %esi, -8(%rbp)
 movl %edx, -12(%rbp)
 movl $0, %eax
 popq %rbp
 ret
main:
 pushq %rbp
 movq %rsp, %rbp
 movl $3, %edx
 movl $2, %esi
 movl $1, %edi
 call func(int, int, int)
 movl $0, %eax
 popq %rbp
 ret

上文中可以看出,也证实了我们所观察到的,首先把3传给了edx,2传给了esi,1传给了edi

全都存寄存器吗?

寄存器毕竟少,当然,还可以存在栈上嘛

int fun() {return 0;}
int func(int x, int y, int z, int a, int b, int c, int d, int e, int f){
 fun();
 return e;
}
int main() {
 func(1, 2, 3, 4, 5, 6, 7, 8, 9);
 return 0;
}
fun():
 pushq %rbp
 movq %rsp, %rbp
 movl $0, %eax
 popq %rbp
 ret
func(int, int, int, int, int, int, int, int, int):
 pushq %rbp
 movq %rsp, %rbp
 subq $24, %rsp
 movl %edi, -4(%rbp)
 movl %esi, -8(%rbp)
 movl %edx, -12(%rbp)
 movl %ecx, -16(%rbp)
 movl %r8d, -20(%rbp)
 movl %r9d, -24(%rbp)
 call fun()
 movl 24(%rbp), %eax
 leave
 ret
main:
 pushq %rbp
 movq %rsp, %rbp
 pushq $9 // 24+%rbp
 pushq $8 // 16+%rbp
 pushq $7 // 8 +%rbp
 movl $6, %r9d
 movl $5, %r8d
 movl $4, %ecx
 movl $3, %edx
 movl $2, %esi
 movl $1, %edi
 call func(int, int, int, int, int, int, int, int, int)
 addq $24, %rsp
 movl $0, %eax
 leave
 ret

主函数中的这三条语句

pushq $9
pushq $8
pushq $7

说明了,当函数入栈放寄存器放不下时,会放在栈上,放在栈顶之上,等函数调用执行完成后,rbp取出回到当前位置之后,再去addq $24, %rsp 把栈弹出这些元素。

并且func函数中的movl 24(%rbp), %eax也证明了,传的参数是在栈顶的上面(自上向下增长) 24 + %rbp 刚好是 $9, 也就是局部变量f的位置

传对象呢?

在这里,暂且不谈内存布局,把一个对象看成一块内存对于的位置
这里用一个结构体做示例

struct E {int x, y, z;};
E func(E e){
 e.x = 2;
 return e;
}
int main() {
 E e = {.x = 1, .y = 2, .z = 3};
 e = func(e);
 return 0;
}
func(E):
 pushq %rbp
 movq %rsp, %rbp
 // 将rdi 和 esi 取出来 放到 rdx 和 eax 中
 movq %rdi, %rdx
 movl %esi, %eax
 // 存放到开辟好的空间中 {x = rbp - 32, y = rbp - 28, z = rbp - 24}
 movq %rdx, -32(%rbp)
 movl %eax, -24(%rbp)
 // 更改 x
 movl $2, -32(%rbp)
 // 将值移动到寄存器上,从返回寄存器上移动到局部返回出去的变量
 movq -32(%rbp), %rax
 movq %rax, -12(%rbp)
 movl -24(%rbp), %eax
 movl %eax, -4(%rbp)
 // 将返回值值移动到寄存器上 rax rdx 上
 movq -12(%rbp), %rax
 movl -4(%rbp), %ecx
 movq %rcx, %rdx
 popq %rbp
 ret
main:
 // 压栈保存现场 没什么好说的
 pushq %rbp
 movq %rsp, %rbp
 subq $16, %rsp
 // 内存布局
 rbp
 | z rbp - 4 
 | y rbp - 8
 | x rbp - 12
 movl $1, -12(%rbp)
 movl $2, -8(%rbp)
 movl $3, -4(%rbp)
 // 移动 x 和 y 到 rdx 寄存器中
 movq -12(%rbp), %rdx
 // 移动 z 到 eax中
 movl -4(%rbp), %eax
 // 再将 rdx 和 eax 分别移动到rdi 和 esi中
 movq %rdx, %rdi
 movl %eax, %esi
 call func(E)
 // 从rax 中取出x y
 movq %rax, -12(%rbp)
 // 从rdx中取出z
 movl -4(%rbp), %eax
 andl $0, %eax
 orl %edx, %eax //
 movl %eax, -4(%rbp)
 movl $0, %eax
 leave
 ret
作者:amjieker

%s 个评论

要回复文章请先登录注册