前言

前段时间和在同学院里研究嵌入式方向的一位同学介绍了一下 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 总览

IntelGCC
mov eax, 1movl $1, %eax
mov ebx, 0A2hmovl $0xA2, %ebx
int 3int $3
mov ecx, edxmovl %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 eaxincl 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

咱来学习了哦

发表评论

电子邮件地址不会被公开。 必填项已用*标注

 

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据