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 | 
