IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    AT&T 汇编和 GCC 内联汇编简介

    dutor发表于 2013-11-25 15:03:25
    love 0

      对一个应用程序员来讲,了解汇编不是必需的,更少有手写纯汇编的需求。但是如果能了解些基本的汇编知识,对程序调试和一些语言特性的理解是大有裨益的。本文介绍 AT&T 语法的汇编的要点以及 GCC 使用的内联汇编(inline assembly)的使用。

    AT&T 汇编

      AT&T 汇编是 GCC 所采用的语法,要点:

    • 寄存器名以 ‘%’ 为前缀:%eax;
    • 立即数以 ‘$’ 为前缀:$0x80;
    • 指令格式 instrunction src, dest,分别为指令名,源操作数,目的操作数,例如 mov $0, %rax;
    • 操作数的宽度以指令名后缀指名,或者由操作数宽度隐式推出:单字节 b,双字节 w,四子节 l,八字节 q。例如 movb $0, (rax);
    • 相对寻址/寄存器寻址/索引寻址均由 seg:off(base, index, scale) 标识。seg 为段寄存器;off 为偏移量;base 为基址寄存器;index 为索引寄存器;scale 为索引的偏移粒度。seg/off/index/scale 均可省略:seg 默认由操作数的属性决定,数据寻址为 ds,代码寻址为 cs;off 默认为 0;index 默认为 0;scale 默认为 1。比如,有个 Message 的结构体数组,该结构体 大小为 16 字节,len 成员的偏移量为 8,数组起始地址保存在 %rbx,元素索引保存在 %rcx,那么,movq 8(rax, rcx, 16), %rdx 将数组的第 %rcx 个元素的 len 成员 load 到 %rdx 中。

      通用寄存器(x86_64):

    • rax, eax, ax, ah, al;
    • rbx, ebx, bx, bh, bl;
    • rcx, ecx, cx, ch, cl;
    • rdx, edx, dx, dh, dl;
    • rsi, esi, si;
    • rdi, edi, di;
    • rbp, ebp;
    • rsp, esp;
    • r8-r15;
    • xmm0-xmm7;
    • st0-st7;
    • fs;

      X86_64 下 ABI 调用约定:

    • 整型参数(包括整数、指针等),由左至右,分别使用 rdi, rsi, rdx, rcx, r8, r9 传递参数,超过 6 六个参数时,多余参数压栈传递;
    • 浮点型参数使用 xmm0-xmm7 传递,多余参数压栈传递;
    • 整型返回值使用 rax:rdx,浮点型返回值使用 xmm0:xmm1,long double 使用 st0:st1;
    • 结构体参数的传递较为复杂,可能由寄存器或者压栈传递,参考 AMD64 ABI 文档;
    • 结构体返回值,由调用方提供栈空间,并将起始地址通过 rdi 传入;
    • rbp 『一般』为栈帧(stack frame)基址;
    • rsp 为栈顶地址;
    • Linux 中使用 fs 实现 TLS(Thread Local Storage);
    • rbx, rbp, rsp, r12-r15 为 callee-saved registers,即调用其他函数不会改变此类寄存器内容。其他寄存器为 caller-saved,如有必要,函数调用方需要自行保存;

    GCC 内联汇编

      内联汇编允许在 C/C++ 代码中嵌入汇编代码,以优化关键代码或者使用架构特有的指令。内联汇编的基本格式如下:

    1
    2
    3
    4
    5
    
    asm [volatile] ( <assembler template>
        : ["constraints"(var)] [,"constraints"(var)]  /* output operands */
        : ["constraints"(var)] [,"constraints"(var)]  /* input operands */
        : ["register"] [,"register"] [,"memory"]      /* clobbered registers */
        );

      中括号中为可选部分,尖括号为必选部分。圆括号内由 ‘:’ 分割为四个部分:

    • asm 为 GCC 扩展关键字,为防止和代码标识符冲突,可使用 __asm__ 代替;
    • volatile 告诉编译器,不要试图优化圆括号中的汇编代码;
    • 『assembler template』内为指令模板,其中的操作数可以使用 %n 样式的占位符(placeholder),n 为 0-9 的数字,编译器会使用后面输入/输出部分代入。如果代码中直接使用寄存器,需要使用两个 ‘%%’, 例如 ‘%%eax’;
    • 第二和第三部分分别为输出/输入操作数说明;输入/输出部分是 C/C++ 代码和汇编代码交互的界面,用来指名汇编代码中可以使用哪些变量以及汇编代码的计算结果保存到哪些变量。变量可以为多个,以逗号分割,按照出现的顺序分别编号,汇编代码中使用该编号来引用这个变量,比如 %0 为第一个变量。每个变量的指示格式为 “contraints”(var),其中 constraints 限定了汇编代码中变量 var 可以使用的寄存器(输入变量)或者将哪个寄存器保存到变量 var 中(输出)。constraints 中可以指名多个寄存器,编译器按照实际情况任意分配其中一个。
    • 第四部分为修改说明(clobbered list)。clobbered list 中可以列举寄存器名,这些寄存器在代码中是显式使用的,而不是由编译器自动分配或者在输入/输出指名的。特殊地,“memory” 告诉编译器,汇编代码中显式使用内存地址/全局变量访问了内存,执行该段汇编之后,所有之前的寄存器需要重新加载。

      常用的 constraints 为:

    • r, 分别下面子列表中寄存器的任意一个来保存 var 变量,相当于 abcdSD:
      • a, %rax, %eax, %ax, %al
      • b, %rbx, %ebx, %bx, %bl
      • c, %rcx, %ecx, %cx, %cl
      • d, %rdx, %edx, %dx, %dl
      • S, %rsi, %esi, %si
      • D, %rdi, %edi, %di
    • q, 相当于 abcd
    • m, 内存操作数
    • digit, 使用和第 #digit 个相同的寄存器
    • f, 使用一个浮点寄存器

      输出 constraints 中需要下面至少一个『修饰符』(constraints modifier)作为前缀:

      =, 此操作数仅作为输出,之前的内容可以抛弃;
      +, 此操作数同时作为输入和输出。

      下面看几个示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    asm ("":::); //~ nothing
    asm ("incl %%eax\n\t":::"eax"); //~ access register directly
    asm ("movq $1, %0\n\t" : "=m"(var)); //~ write 1 to var
    asm ("mov %0, %%eax\n\t" : : "m"(var)); //~ read from var to eax
    //~ read a to eax, read b to either ebx|ecx|edx|edi|esi, add it to eax, write back eax to a
    asm ("addl %1, %0\n\t" : "+a"(a) : "r"(b));
    asm ("incq global_var\n\t" :::"memory"); //~ access global_var directly
    asm ("incl %0\n\t" : "+q"(var)); //~ read var to either eax|ebx|ecx|edx, increase it, write it back to var
    asm ("incl %0\n\t" : "=q"(var) : "0"(var)) //~ the same as above, constraint 0 means using the same register
    asm ("incl %[__var__]\n\t" : [__var__]"+q"(var)); //~ use user-defined placeholder

    最后一个示例使用了用户自定义的占位符,通常在输入输入变量较多的情况下使用,省得逐个地对应。
      在汇编中调用 printf:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    #include <stdio.h>
    int
    main()
    {
      char *fmt = "Hello, %s\n";
      char *s = "World";
      int ret = 0;
      asm (" callq printf\n\t"
          : "=a"(ret)
          : "D"(fmt), "S"(s));
      printf("ret: %d\n", ret);
      return 0;
    }

      在汇编中进行系统调用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    int
    sys_write(int fd, const char *buf, size_t n)
    {
      int ret;
      asm (
          "syscall\n\t"
          : "=a"(ret)
          : "0"(1), "D"(fd), "S"(buf), "d"(n)
          );
      return ret;
    }
     
    int
    main()
    {
      char *s = "Hello, World\n";
      printf("%d\n", sys_write(fileno(stdout), s, strlen(s)));
      return 0;
    }

    参考资料

    • Professional Assembly Language, Richard Blum. 貌似是唯一一本以 AT&T 语法讲解汇编语言的了。
    • Programming From The Ground Up, Jonathan Bartlett, 如果上一本是以编程讲汇编的,这一本就是以汇编讲编程的了。
    • System V Application Binary Interface, AMD64 架构下的 System V ABI, 也是 Linux 使用的 ABI.


沪ICP备19023445号-2号
友情链接