Register
首先简单介绍下寄存器与栈帧,这个是理解subroutine call的基础。来看一下几个与本节内容相关的x86-64 arch的寄存器
Register |
Purpose |
%rax |
temp register; return value |
%rbx |
callee-saved |
%rcx |
used to pass 4th argument to functions |
%rdx |
used to pass 3rd argument to functions |
%rsp |
stack pointer |
%rbp |
callee-saved; base pointer |
%rsi |
used to pass 2nd argument to functions |
%rdi |
used to pass 1st argument to functions |
%r8 |
used to pass 5th argument to functions |
%r9 |
used to pass 6th argument to functions |
%r10-r11 |
temporary |
%r12-r15 |
callee-saved registers |
另外还有一个重要的寄存器 %rip(instruction pointer register),指向下一条将要执行的指令。%rip不能被程序直接访问。 在gdb中,%rip被叫做 pc.
Stack Frame
x86-64的memory layout如下图, 栈由高地址向低地址增长。
每一个procedure call都将会导致一个stack frame的创建, 栈帧的组成如下图,可以看到栈帧范围内的内存寻址是通过寄存器 %rbp的偏移位置来完成,也就是说在一个栈帧中%rbp的值是固定的,所以可以拿%rbp来做基准参考,通过相对与%rbp的偏移在栈上进行寻址。 %rsp也拥有%rbp类似的寻址功能。
哪些指令可以修改%rsp寄存器呐?
PUSH,POP,CALL,RET,IRET,INT instruction implicitly use the stack pointer.
从上图可以看到每个栈帧中,首先被push了 “return address” 返回地址,也就是子程序ret后将要执行的原父函数的指令地址; 接着是保存了上一栈帧的%rbp值; 剩下的就是子函数的本地/临时变量。
关于函数入参,看到argument7 到 argument n是保存在 previous 栈帧上,它们的入栈顺序是从右到左。 前六个argument是保存在寄存器中,从register 表中可以找到相关的信息。
call & ret
与函数调用最密切的两个指令: call & ret。一个问题是栈帧中的“return address”是 如何&何时 push到栈上的?
是call指令。执行call指令是,它做了两件事:
1.push %rip值到寄存器,前面说了%rip指向下一条执行指令,对于子函数来说就是返回地址了;
2.然后跳转到target地址。
ret 正好与call指令对应, pop 当前栈帧的“return address” 到 %rip寄存器,开始执行%rip指向的指令。
Toy example
来看一个栗子????吧,
#include
#include
int trampoline_test(void);
int fun_a(int i, char *pchar) {
if (i == 5) goto next;
printf("pchar is:%s\n", pchar); return 0;
next: printf("j is %d\n", i);
return 1; }
int main() { char test[] = "Hellow"; int i = 0;
i = trampoline_test(); i = 200; i += 8;
printf("i==%d\n",i); fun_a(1, (char *)&test);
return 0; } |
0000000100003e60 <_fun_a>: // 在 entry fun_a之前,返回地址已经push到stack了 100003e60: 55 pushq %rbp // 保存前一个栈帧的%rbp值; 100003e61: 48 89 e5 movq %rsp, %rbp // 为当前栈帧 更新%rbp 100003e64: 48 83 ec 10 subq $16, %rsp // 为本地变量预留栈空间 100003e68: 89 7d f8 movl %edi, -8(%rbp) 100003e6b: 48 89 75 f0 movq %rsi, -16(%rbp) 100003e6f: 83 7d f8 05 cmpl $5, -8(%rbp) 100003e73: 0f 85 05 00 00 00 jne 0x100003e7e <_fun_a+0x1e> 100003e79: e9 1e 00 00 00 jmp 0x100003e9c <_fun_a+0x3c> 100003e7e: 48 8b 75 f0 movq -16(%rbp), %rsi
100003e82: 48 8d 3d f5 00 00 00 leaq 245(%rip), %rdi # 100003f7e 100003e89: b0 00 movb $0, %al
100003e8b: e8 cc 00 00 00 callq 0x100003f5c 100003e90: c7 45 fc 00 00 00 00 movl $0, -4(%rbp) 100003e97: e9 18 00 00 00 jmp 0x100003eb4 <_fun_a+0x54> 100003e9c: 8b 75 f8 movl -8(%rbp), %esi
100003e9f: 48 8d 3d e5 00 00 00 leaq 229(%rip), %rdi # 100003f8b 100003ea6: b0 00 movb $0, %al
100003ea8: e8 af 00 00 00 callq 0x100003f5c 100003ead: c7 45 fc 01 00 00 00 movl $1, -4(%rbp) 100003eb4: 8b 45 fc movl -4(%rbp), %eax 100003eb7: 48 83 c4 10 addq $16, %rsp // 回收栈空间 100003ebb: 5d popq %rbp // 恢复previous 栈帧 %rbp值 100003ebc: c3 retq // “return address”出栈,更新%rip 100003ebd: 0f 1f 00 nopl (%rax) |
理解了函数如何返回到previous后,我们来定义一个函数trampoline_test(),修改该函数栈帧上返回地址的内容,使得从trampoline_test()返回后,跳过“i = 200”这条语句。
代码如下:
cat stack_frame.S #include #include #include .text .global _trampoline_test _trampoline_test: pushq %rbp movq %rsp, %rbp subq $8, %rsp addq $8, %rsp popq %rbp // pop %rbp后,此时 %rsp指向了“return address” addq $13, (%rsp) // 等价于: %rsp += 13,跳过13个字节的指令 movl $2, %eax // 返回值为2 retq |
跳过的13个字节就是 下面高亮部分;
100003ef3: e8 4c 00 00 00 callq 0x100003f44 <_trampoline_test> 100003ef8: 89 45 f0 movl %eax, -16(%rbp) 100003efb: c7 45 f0 c8 00 00 00 movl $200, -16(%rbp) 100003f02: 8b 45 f0 movl -16(%rbp), %eax 100003f05: 83 c0 08 addl $8, %eax // trampoline_test返回值为2,2+8=10 100003f08: 89 45 f0 movl %eax, -16(%rbp) 100003f0b: 8b 75 f0 movl -16(%rbp), %esi
100003f0e: 48 8d 3d 86 00 00 00 leaq 134(%rip), %rdi # 100003f9b 100003f15: b0 00 movb $0, %al
100003f17: e8 40 00 00 00 callq 0x100003f5c 100003f1c: 48 8d 7d f5 leaq -11(%rbp), %rdi |
代码实际执行后的结果是:
$ as -o stack_frame.o stack_frame.S $ gcc -c -o jump_test.o jump_test.c
$ gcc jump_test.o stack_frame.o -o jump_test |