用一个简单的例子解释C++函数调用的过程,备忘。
实验环境
以下是本次实验的环境配置
1* 操作系统: Ubuntu 14.04 x86_64 2* 编译器: gcc-4.8.2
开始之前
阅读资料
开始之前,建议先阅读如下几篇文章,对call stack和asm多少有点了解,下文会涉及到很多这方面的东西。
- Understanding Memory
- Wikipedia:call stack
- A Readers Guide to x86 Assembly
- Wikipedia:function prologue
预备知识
下面是一些在后面的解释中会用到的知识,以下说明均基于x86-64平台。
-
栈(call stack, runtime stack或stack): 在Linux上我们编译程序后一般生成的可执行文件是ELF格式的,下图是一个简化版的ELF文件结构[1]
1 +----------------------+ 2 | ... ... | 3 +----------------------+ 4 | .text | <- 程序的可执行指令 5 +----------------------+ 6 | ... ... | 7 +----------------------+ 8 | .data | <- 初始化的全局和静态变量 9 +----------------------+ 10 | .bss | <- 未初始化的全局和静态变量 11 +----------------------+ 12 | ... ... | 13 +----------------------+可执行文件将会在运行时被载入内存中,下面是一个简化的进程虚拟地址空间图。
1 +----------------------+ <- 高地址处 2 | ... ... | 3 +----------------------+ 4 | stack | <- 本文的主角,call stack,向低地址处生长 5 +----------------------+ 6 | ... ... | 7 +----------------------+ 8 | heap | <- 动态分配的内存,向高地址处生长 9 +----------------------+ 10 | uninitialized data | <- 未初始化的全局变量和静态变量 11 +----------------------+ 12 | initialzed data | <- 初始化的全局和静态变量,从ELF的.data区载入 13 +----------------------+ 14 | code | <- 程序代码,从ELF的.text区载入 15 +----------------------+ 16 | ... ... | 17 +----------------------+ <- 低地址处 -
寄存器:
- %rax: 通常用于返回第一个整数。
- %rbp: base pointer,指向当前frame的底部附近(caller的%rbp)。
- %rsp: stack pointer,用于保存最新分配的frame的顶部,也就是stack顶部。
-
调用者(caller)和被调用者(callee): 在函数foo的上下文中,调用者(caller)就是main函数,被调用者(callee)就是foo函数。
-
stack frame: stack是由一个个的stack frmae组成的,每次函数调用都会在栈上分配一个新的frame,该frame内保存了当前函数调用的上下文信息,包括请求参数(可能直接保存在寄存器中[2]),返回地址和局部变量等内容。
-
返回地址: callq指令会在调用函数的时候将下一条指令的地址push到stack上,当本次调用结束后,retq指令会跳转到被保存的返回地址处使程序继续执行。
-
stack上的数据是字节对齐的。
-
stack由高地址处向低地址处生长,在下图中
16(%rbp)表示地址16 + value of register[%rbp]1 +----------------------+ <- 高地址处 2 | ... ... | 3 +----------------------+ 4 | parameters | <- 没有保存到寄存器里的callee的函数参数(如果有的话) 5 +----------------------+ <- 16(%rbp) + n*8 n=0,1,2,3 ... 6 | return address | <- callee的返回地址,callee的stack frame开始处 7 +----------------------+ <- 8(%rbp) 8 | caller's %rbp | <- 保存的是caller的%rbp 9 +----------------------+ <- (%rbp) 10 | saved registers | <- 最开始是在函数调用中会被占用的寄存器(如果有的话),调用结束后恢复 11 +----------------------+ 12 | callee's locals etc. | <- 之后是函数内定义的局部变量和临时变量(保存函数参数寄存器等) 13 +----------------------+ 14 | ... ... | 15 +----------------------+ 16 | parameters | <- callee的callee的函数参数(如果有的话) 17 +----------------------+ <- n*8(%rsp) n=0,1,2,3 ... 18 | ... ... | <- 栈顶,calleee的stack frame结束处 19 +----------------------+ <- (%rsp) 20 | ... ... | 21 +----------------------+ <- 低地址处当callee的函数参数太多(或存在不能存储到寄存器上的参数类型)时,多余的函数参数会按照从右向左的顺序依次入栈,占用caller的stack frame的空间。[3]
-
汇编指令:
简单的例子
下面是一个非常简单的例子。
cpp源代码
1// call_stack_example.cpp 2int foo2(int a, long b, int c, int d, int e, int f, int g, int i) { 3 return a + b; 4} 5int foo(int &a, long b) { 6 int m = 1; 7 int o[3] = {0x1, 0x2, 0x3}; 8 return foo2(0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x9); 9} 10int main() { 11 int z = 0xa; 12 int r = foo(z, 0xb); 13 return r; 14}
使用如下命令编译call_stack_example.cpp
g++ -g -O0 -fno-stack-protector call_stack_example.cpp -o a.out
为了让汇编代码更简单,我们在编译选项里使用-fno-stack-protector,这可以禁止gcc默认启用的Stack Protection[6],关于Stack Protection的详细信息可参考Buffer overflow protection和StackGuard: Simple Stack Smash Protection for GCC。
汇编代码和注释
然后使用objdump输出汇编代码
objdump -dS a.out -j .text
下面是call_stack_example.cpp内三个函数的汇编代码和注释,gcc默认使用的是AT&T汇编语法。
1int foo2(int a, long b, int c, int d, int e, int f, int g, int i) { 2 push %rbp // 将caller的%rbp入栈 3 mov %rsp,%rbp // 初始化callee的%rbp 4 mov %edi,-0x4(%rbp) // a: mem[R[rbp]-0x4] = R[edi] 5 // 暂存寄存器的值,使其可以被重用,使用-O选项可以优化掉这部分代码 6 mov %rsi,-0x10(%rbp) // b 7 mov %edx,-0x8(%rbp) // c 8 mov %ecx,-0x14(%rbp) // d 9 mov %r8d,-0x18(%rbp) // e 10 mov %r9d,-0x1c(%rbp) // f 11 12return g + i; 13 mov 0x18(%rbp),%eax // g在caller的stack frame的底部 14 mov 0x10(%rbp),%edx // i在caller的stack frame的底部 15 add %edx,%eax // R[eax] += R[edx] 16} 17 pop %rbp // 将caller的rbp出栈(恢复%rbp) 18 retq // 将返回地址出栈,跳转到该地址处 19 20int foo(int &a, long b) { 21 push %rbp // 将caller的%rbp入栈 22 mov %rsp,%rbp // 初始化callee的%rbp 23 sub $0x30,%rsp // 为当前stack frame分配0x30字节的空间 24 mov %rdi,-0x18(%rbp) // mem[R[rbp]-0x18] = R[rdi] 25 mov %rsi,-0x20(%rbp) // mem[R[rbp]-0x20] = R[rsi] 26 27int m = 1; 28 movl $0x1,-0x4(%rbp) // mem[R[rbp]-0x4] = 0x1 29 30int o[3] = {0x1, 0x2, 0x3}; 31 movl $0x1,-0x10(%rbp) // mem[R[rbp]-0x10] = 0x1 32 movl $0x2,-0xc(%rbp) // mem[R[rbp]-0xc] = 0x1 33 movl $0x3,-0x8(%rbp) // mem[R[rbp]-0x10] = 0x1 34 35return foo2(0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x9); 36 movl $0x9,0x8(%rsp) // mem[R[rsp]+0x8] = 0x9; 参数太多,无法用寄存器传递参数0x9 37 movl $0x7,(%rsp) // mem[$[rsp]] = 0x7; 参数太多,无法用寄存器传递参数0x7 38 mov $0x6,%r9d // R[r9d] = 0x6 使用寄存器传递函数参数,下同 39 mov $0x5,%r8d // R[r8d] = 0x5 40 mov $0x4,%ecx // R[ecx] = 0x4 41 mov $0x3,%edx // R[edx] = 0x3 42 mov $0x2,%esi // R[esi] = 0x2 43 mov $0x1,%edi // R[edi] = 0x1 44 callq 4004ed <_Z4foo2iliiiiii> // 调用foo2 45} 46 leaveq // 将caller的rbp出栈(恢复%rbp),将已保存的局部变量和临时变量出栈 47 retq // 将返回地址出栈,跳转到该地址处 48 49int main() { 50 push %rbp // 将caller的%rbp入栈 51 mov %rsp,%rbp // 初始化callee的%rbp 52 sub $0x10,%rsp // 为当前stack frame分配0x10字节的空间 53 54int z = 0xa; 55 movl $0xa,-0x8(%rbp) // mem[R[%rbp]-0x8] = 0xa 56 57int r = foo(z, 0xb); 58 lea -0x8(%rbp),%rax // R[rax] = &z 59 mov $0xb,%esi // R[esi] = 0xb 60 mov %rax,%rdi // R[rdi] = R[rax] 61 callq 400513 <_Z3fooRil> // 调用foo 62 mov %eax,-0x4(%rbp) // mem[R[rbp]-0x4] = R[eax] (%eax保存了之前foo的返回值) 63 64return r; 65 mov -0x4(%rbp),%eax // R[eax] = mem[R[rbp]-0x4] 66} 67 leaveq // 将caller的rbp出栈(恢复%rbp),将已保存的局部变量和临时变量出栈 68 retq // 将返回地址出栈,跳转到该地址处 69 nopl (%rax)
其他例子
函数参数传递
下面的例子取自System V Application Binary Interface的图3.5 Parameter Passing Example和图3.6 Register Allocation Example
假设有以下结构体和函数
1typedef struct { 2 int a, b; 3 double d; 4} structparm; 5 6extern void func( 7 int e, 8 int f, 9 structparm s, 10 int g, 11 int h, 12 long double ld, 13 double m, 14 __m256 y, 15 double n, 16 int i, 17 int j, 18 int k);
然后如下调用函数func:
1structparm s; 2int e, f, g, h, i, j, k; 3long double ld; 4double m, n; 5__m256 y; 6 7func (e, f, s, g, h, ld, m, y, n, i, j, k);
则函数参数的传递方式如下表
General Purpose Registers
Floating Point Registers
Stack Frame Offset
%rdi: e
%xmm0: s.d
0: ld
%rsi: f
%xmm1: m
16: j
%rdx: s.a, s.b
%ymm2: y[7]
24: k[8]
%rcx: g
%xmm3: n
%r8: h
%r9: i
额外阅读资料
-
关于参数是否通过寄存器传递的具体细则,请参考System V Application Binary Interface的
3.2.3 Parameter Passing部分。 ↩ -
参考System V Application Binary Interface的3.2 Function Calling Sequence,引用于2014-05-07。 ↩
-
Wikipedia:function prologue, 引用于2014-05-04。 ↩
-
Call vs Jmp: The Stack Connection,引用于2014-05-05。 ↩ ↩
-
参考
man gcc中关于-fstack-protector选项的说明。 ↩ -
%ymm0-%ymm15是256位的浮点数寄存器,其低128位对应%xmm0-%xmm15 ↩
-
栈上的函数参数要按8字节对齐 ↩