前言
前段时间和在同学院里研究嵌入式方向的一位同学介绍了一下 x86 汇编,另外还告诉他能在 C 语言代码里直接嵌入内联汇编代码,但是一上手就遭遇了尴尬。(−_−#)
我还在用 Windows 做开发的时候常用的是 Visual Studio,Visual Studio 有着微软自家的一套 C 语言编译器 VC Compiler,尽管微软在里面加了一堆无厘头设计(比如 sprintf 函数莫名其妙要加个 _s 才算『安全』)。在这里可以直接使用 asm 或者 __asm 关键字嵌入内联汇编,比如下面的代码。
int counter = 0;
__asm {
mov eax, counter
inc eax
mov counter, eax
}
我将一段内联汇编代码直接复制到另一个开发环境 CLion 下,发现编译失败。刚还和同学信誓旦旦地说能在 C 语言里面写汇编的,这下尴尬的……
过一段时间,查了一些资料,恶补一波汇编知识,也算是开了眼界。
CLion 默认使用的是 GCC 编译器,GCC 编译器同样支持内联汇编,但是语法与 VC 编译器并不一样。
1. GCC 的汇编语法
VC 编译器使用的汇编语法是 Intel 语法,与 VC 编译器不同,GCC 编译器使用的汇编语法是 AT&T/UNIX 语法,毕竟 GCC 是 UNIX 环境下的 C 语言编译器。
至于为什么 VC 编译器会使用 Intel 汇编语法,可能和当年微软和英特尔的商业合作有关,有兴趣的读者可以去了解下 Wintel Dynasty (Wintel 朝代)的历史。
那么这个 AT&T 又是何方神圣呢?它是美国的一家很知名的通信公司,旗下包含另外几个有名子公司比如 AOL (American Online,美国在线,就是那个做了 AIM 软件的公司)。
1.1 源-目标的寻址顺序
- Intel:Op-code dst src (如 mov eax, ecx,将 ecx 赋值给 eax)
- GCC:Op-code src dst (如 movl %ecx, %eax,将 ecx 赋值给 eax)
这同时也是 Intel 语法与 AT&T 语法最大的不同,寻址顺序是相反的,在实际迁移过程中经常会在这里犯错误。
1.2 寄存器命名方式
- Intel:eax
- GCC:%eax
注意寄存器要以 % 作为前缀。
1.3 立即寻址型操作数
- Intel:十进制常数可直接用数值表示。十六进制常数一般用 h 作为后缀,以表明这是个十六进制常数,有时也可以 0x 为前缀表示,比如 11h 和 0x11。
- GCC:任何立即数都以 $ 开头,十六进制数还要在美元符号后边加上 0x 表示它是个十六进制常数。
注意,对于 GCC 环境,这里所适用的情况是在立即寻址时表示立即数,当涉及到变址寻址表示偏移量操作数时,不再需要加上 $ 作为前缀。
1.4 操作数大小
- Intel:byte ptr,word ptr,dword ptr,qword ptr。分别表示字节、双字节、四字节、十六字节。
- GCC:更为简化,使用 b,w,l,q 在指令的后缀加上即可。比如:mov al, byte ptr counter => movb counter, %al
1.5 基地址
- Intel:基地址放在 [ 、 ] 之间。
- GCC:基地址放在 ( 、 ) 之间。
1.6 变址寻址操作数
- Intel:[base + index*size + disp],如 mov eax, [ebx+esi*4-20h]
- GCC:disp(base, index, size),如 movl -0x20(%ebx, %esi, 4), %eax
偏移量和大小数值都不需要 $ 前缀。
1.7 总览
Intel | GCC |
mov eax, 1 | movl $1, %eax |
mov ebx, 0A2h | movl $0xA2, %ebx |
int 3 | int $3 |
mov ecx, edx | movl %edx, %ecx |
mov eax, [ebx] | movl (%ebx), (%eax) |
mov ebp, [esp+8] | movl 8(%esp), %ebp |
mov rax, [rdx+10h] | movq 0x10(%rdx), %rax |
add eax, [ebx+ecx*4h] | addl (%ebx, %ecx, 0x4), %eax |
lea edx,[eax+ebx*4h-20h] | leal -0x20(%eax, %ebx, 0x4), %edx |
inc eax | incl eax |
2. 在 C 语言中使用 GCC 内联汇编
与 VC 编译器相似,在 GCC 中使用内联汇编一样要用到 asm 关键字,当然为了避免关键字混淆,你也可以使用 __asm__ 关键字。
asm("你的汇编代码")
例如
asm("movl %ebx, %eax")
__asm__("movl $0x20, %edi")
当要在同一条 __asm__ 调用里嵌入多行汇编代码时,应使用 \n\t 分割每一行。
__asm__("movl $0xb, %ebx\n\t"
"movl $0x20, %edi\n\t"
"nop\n\t"
"incl %eax");
3. GCC 扩展汇编
3.1 扩展汇编
GCC 里的汇编指令并不是一成不变的一串字符串,你可以进行一些指定或者限定。
asm ("汇编指令"
: 输出 /* 可选 */
: 输入 /* 可选 */
: 涉及到的寄存器 /* 可选 */
用一段代码来说明扩展汇编。
int a=10, b;
asm ("movl %1, %%eax; movl %%eax, %0;"
: "=r"(b)
: "r"(a)
: "%eax"
);
上面的代码将变量 a 的值赋值给变量 b。
第一行冒号指定输出操作数,在汇编代码里用 %0 表示,这里指的是变量 b。
第二行冒号指定输入操作数,在汇编代码里可用 %1、%2、%3 …… 表示,这里的 %1 指的是第一个输入的参数:变量 a。
“r” 用于限制操作数,这里的 r 告诉 GCC 编译器,可以使用任意的寄存器来存取输入和输出的操作数。输出操作数的限制符前面需要一个 = 号,说明这个输出操作数是只写的。
第三行冒号告诉这次内联汇编哪些寄存器受到了影响,这一行一般可以省略掉,编译器会自动处理。
※ 与通常的内联汇编不同,在使用了扩展汇编后,汇编代码里的各个寄存器的前缀还要另外加上一个 % 符号,以免和一些变量混淆,如 %eax => %%eax。
3.2 “不稳定的”声明
在 asm 关键字后边可以加入 volatile (也可以用 __volatile__),防止你的汇编代码因为编译器自身的代码优化而被移动、删除或其他任何改变。
3.3 限制符
3.3.1 寄存器限制符
前面提到可以用 r 表示使用任意的寄存器,其实我们还可以自己指定要使用到的寄存器。
r | 任何寄存器 |
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 |
3.3.2 内存操作数限制符
尽管我们有寄存器可以作为外部输出输入的中转,但代价有时是破坏已有的寄存器内容,为解决这个问题,可以使用内存操作数限制符,直接通过内存进行寻址,最大化寻址性能,而不是使用寄存器(尽管寄存器寻址更快)。该限制符是“m”。
asm("sidt %0\n" : :"m"(loc));
上述代码通过内存操作数寻址把 idtr 寄存器的内容储存到变量 loc。
3.3.3 数字限制符
如 “0” 表示在汇编代码中,当前输入或输出参数用 %0 表示。
3.3.4 其他限制符
除了上述的限制符外还有其他的限制符。
限制符 | 功能 |
m | 内存操作数 |
o | 内存操作数,仅用于偏移量 |
V | 内存操作数,用于非偏移量 |
i | 立即整型操作数 |
n | 立即整型操作数,允许已知的数值 |
g | 任何寄存器,但寄存器不是常规的寄存器 |
4. 结论
本文主要介绍 GCC 内联汇编基础,并将其与 Intel 内联汇编语法进行对比,为以后在不同平台进行迁移提供帮助。GCC 内联汇编还有很多的内容在本文还没有涉及到,更多内容可查阅 GNU 文档。
GCC 内联汇编与 Intel 内联汇编有很多相同之处,但又有各自不同的特性。特别要注意在64位编译环境下,VC 编译器不再支持在 C 语言代码里插入内联汇编,而是使用了 intrinsics 的函数式指令来代替64位汇编指令,详情可查阅 Intel 手册学习相关函数。
1 条评论
AFAF · 2019年10月13日 下午11:06
咱来学习了哦