相关
《Linux函数调用栈的实现原理(X86)》
总结
rsp向低地址生长(栈顶),rbp记录旧值(栈底)。
- intel x86测试,六个和六个以内的参数用寄存器传递。
- 8个参数场景,6个用寄存器传递,2个用栈帧传递:
- 调用者caller使用rsp指针准备参数,先让rsp向低地址生长一段空间,然后用rsp+偏移的方式准备调用。
- 被调用者callee使用rbp指针接受参数,rsp先生长到callee的frame,然后rbp指向rsp,然后rbp-偏移的方式将参数写入callee的栈帧。注意这里写入的只有从寄存器里面读出来的参数。
- 被调用者callee如果需要使用寄存器传递的参数,使用rbp-偏移量的方式寻找local变量。
- 被调用者callee如果需要使用栈帧传递的变量,使用rbp+偏移量,寻找调用者放在栈帧中的变量。
前言
C语言传参使用寄存器+压栈的方式,这里对传参的细节流程做一些分析记录。
《Modern Compiler Implementation in C》中提到:
分析过程
示例代码:gcc -O0 t.c -o t -g
void foo(int a, int b, long c, long d, int e, int f, int g, long h) {
int bar[4];
}
int main(void) {
foo(1, 2, 3, 4, 5, 6, 7, 8);
}
main函数disassemble
main
(gdb) disassemble /m main
Dump of assembler code for function main:
5 int main(void) {
0x0000000000400509 <+0>: push %rbp
0x000000000040050a <+1>: mov %rsp,%rbp
0x000000000040050d <+4>: sub $0x10,%rsp
6 foo(1, 2, 3, 4, 5, 6, 7, 8);
0x0000000000400511 <+8>: movq $0x8,0x8(%rsp) // movq 8字节
0x000000000040051a <+17>: movl $0x7,(%rsp) // movl 4四字节
0x0000000000400521 <+24>: mov $0x6,%r9d
0x0000000000400527 <+30>: mov $0x5,%r8d
0x000000000040052d <+36>: mov $0x4,%ecx
0x0000000000400532 <+41>: mov $0x3,%edx
0x0000000000400537 <+46>: mov $0x2,%esi
0x000000000040053c <+51>: mov $0x1,%edi
0x0000000000400541 <+56>: callq 0x4004ed <foo>
7 }
0x0000000000400546 <+61>: leaveq
0x0000000000400547 <+62>: retq
End of assembler dump.
stack frame初始状态
high address
-------------------- <- rbp
frame (main)
-------------------- <- rsp
low address
旧的rbp保存起来,进入新函数main。
push %rbp
mov %rsp,%rbp
high address
-------------------- <-
frame (main)
-------------------- <- rsp / rbp
low address
注意这里rsp向下继续生长了16字节,用来放调用foo函数的入参。
sub $0x10,%rsp
high address
-------------------- <-
frame (main)
-------------------- <- rbp
分配16字节,准备放参数
-------------------- <- rsp
low address
8个字节放参数8
4个字节放参数7
movq $0x8,0x8(%rsp)
movl $0x7,(%rsp)
high address
-------------------- <-
frame (main)
-------------------- <- rbp
8 Byte << 写入第8个参数
4 Byte << 写入第7个参数
-------------------- <- rsp
low address
mov $0x6,%r9d
mov $0x5,%r8d
mov $0x4,%ecx
mov $0x3,%edx
mov $0x2,%esi
mov $0x1,%edi
写入1-6参数到寄存器
注意e开头的寄存器是32位的,因为参数是int四字节够用。是拿64位寄存器用了一半。
注意r开头的寄存器是64位的,因为参数是long八字节,用完整的寄存器。
callq 0x4004ed
0x4004ed是低地址存放代码段的
callq的过程,会把rsp向下继续生长一段空间,
high address
-------------------- <-
frame (main)
-------------------- <-
8 Byte << 写入第8个参数
4 Byte << 写入第7个参数
-------------------- <- rbp
callq后rsp向低地址移动
-------------------- <- rsp
low address
foo函数disassemble
(gdb) disassemble /m foo
Dump of assembler code for function foo:
1 void foo(int a, int b, long c, long d, int e, int f, int g, long h) {
0x00000000004004ed <+0>: push %rbp
0x00000000004004ee <+1>: mov %rsp,%rbp
0x00000000004004f1 <+4>: mov %edi,-0x14(%rbp)
0x00000000004004f4 <+7>: mov %esi,-0x18(%rbp)
0x00000000004004f7 <+10>: mov %rdx,-0x20(%rbp)
0x00000000004004fb <+14>: mov %rcx,-0x28(%rbp)
0x00000000004004ff <+18>: mov %r8d,-0x2c(%rbp)
0x0000000000400503 <+22>: mov %r9d,-0x30(%rbp)
2 int bar[4];
3 }
0x0000000000400507 <+26>: pop %rbp
0x0000000000400508 <+27>: retq
End of assembler dump.
push %rbp
mov %rsp,%rbp
旧的rbp保存起来,进入新函数main,rbp指向新的栈顶(低地址)。
注意这里只存了6个参数,但是函数有8个入参,原因是有两个入参main函数已经放好了。
mov %edi,-0x14(%rbp)
mov %esi,-0x18(%rbp)
mov %rdx,-0x20(%rbp)
mov %rcx,-0x28(%rbp)
mov %r8d,-0x2c(%rbp)
mov %r9d,-0x30(%rbp)
high address
-------------------- <-
frame (main)
-------------------- <-
8 Byte << 写入第8个参数
4 Byte << 写入第7个参数
-------------------- <-
callq后rsp向低地址移动
-------------------- <- rsp / rbp
写入参数1
写入参数2
写入参数3
写入参数4
写入参数5
写入参数6
--------------------
low address