在 Linux/C 环境中横行几年后,越发体会到汇编对于 GDB 的重要。在开始前,先来看一段 sample:
#include#include /*这个函数没有任何地方调用过 */void why_here(void) { printf("why u here ?!\n"); exit(0);}int main(int argc,char * argv[]) { long long buff[1]; buff[3]=(long long)why_here; return 0;}
不带 -O 编译后发现,why_here 居然被调用了!
很神奇是吧?想要知道为什么,就得开始了解 x64 寄存器、汇编、Frame 和 stack 等,Let's start!
一. 通用寄存器
x86-64 与 x86 并不是同一个概念,且实际上变化挺大的,包括寄存器个数、传参方式等。鉴于 x86-64 已经是主流,最起码在阿狸的服务器上是主流(呃,alipay 不思进取,不包括在内),因此,x86 的相关实现就不列举了。毕竟,对过去了解得越多,除了证明你已经老了,并不能体现你现在有多牛B。
1. 寄存器数目
新增加寄存器 %r8 到 %r15,加上 x86 的原有 8 个,一共 16 个寄存器,分别是:%rax, %rbx, %rcx, %rdx, %rsi, %rdi, %rbp, %rsp, %r8, %r9, %r10, %r11, %r12, %r13, %r14, %r15。
2. 寄存器长度
x86-64 中的寄存器都是 64 位的,相对于 x86 来说,标识符发生了变化,比如:从原来的 %ebp 变成了 %rbp。为了向后兼容性,%ebp 依然可以使用,不过指向了 %rbp 的低 32 位。
3. 寄存器使用方法
所谓的通用,意味着在使用上没有限制,接下来提到的规则,仅仅是 GCC 遵循的规则。这些内容必须记住!
%rax 作为函数返回值使用
%rsp 栈指针寄存器,指向栈顶 (stack pointer)
%rbp 栈指针寄存器,指向栈底 (bottom pointer)
%rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数。。。
%rbx,%rbp,%r12,%r13,%14,%15 用作数据存储,遵循被调用者使用规则,简单说就是随便用,调用子函数之前要备份它,以防它被修改
%r10,%r11 用作数据存储,遵循调用者使用规则,简单说就是使用之前要先保存原值
%rip 下一条待执行的指令 (instruction pointer)
二. Frame
C 语言是面向过程的语言,一个程序最终会分解成若干过程(函数),而 GCC 则将过程转换成 Frame。联系到 GDB 调用栈中的 frame 指令就好理解了。接下来,就开始深入了解 Frame。
1. Frame 的起始
如前所述,使用寄存器 %rbp 和 %rsp 分别指向 Frame 底部和 Frame 顶部。
2. Frame 生长方向
与常例不同,Frame 是从高地址向低地址生长,即 %rbp 保存的地址要大于 %rsp。但这其实并不是固定的,只不过大多数操作系统都选择了这种方式。如果你闲得蛋疼想扭转,Intel 也支持通过调整属性的方式来完成。
3. Frame 的生成与销毁
#includeint test(int x) { int array[] = {1,3,5}; return array[x];} int main(int argc, char *argv[]) { int i = 1; int j = foo(i); printf("i=%d,j=%d\r\n", i, j); return 0;}
使用 GCC 生成汇编语言:
Shell > gcc –S –o test.s test.c
Main 函数第 40 行的指令 Call test 其实干了两件事情:
Pushl %rip //保存下一条指令(第 41 行的代码地址)的地址,用于函数返回继续执行
Jmp foo //跳转到函数 foo
Foo 函数第 19 行的指令 ret 相当于:
popl %rip //恢复指令指针寄存器