在 Linux/C 环境中横行几年后,越发体会到汇编对于 GDB 的重要。在开始前,先来看一段 sample:

#include<stdlib.h>
#include<stdio.h>

/*这个函数没有任何地方调用过 */
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 的生成与销毁

#include <stdio.h>
 
int 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  //恢复指令指针寄存器