对一个应用程序员来讲,了解汇编不是必需的,更少有手写纯汇编的需求。但是如果能了解些基本的汇编知识,对程序调试和一些语言特性的理解是大有裨益的。本文介绍 AT&T 语法的汇编的要点以及 GCC 使用的内联汇编(inline assembly)的使用。
AT&T 汇编
AT&T 汇编是 GCC 所采用的语法,要点:
- 寄存器名以 ‘%’ 为前缀:%eax;
- 立即数以 ‘$’ 为前缀:$0×80;
- 指令格式 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++ 代码中嵌入汇编代码,以优化关键代码或者使用架构特有的指令。内联汇编的基本格式如下:
- 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)作为前缀:
- =, 此操作数仅作为输出,之前的内容可以抛弃;
+, 此操作数同时作为输入和输出。
下面看几个示例:
- 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:
- #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;
- }
在汇编中进行系统调用:
- 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.