内联汇编简介
内联汇编用于在C程序中直接嵌入汇编代码,可用于编写使用C/C++语句无法实现的功能(当然,最好先去看一下 GCC 文档,尤其是__builtin类函数)
GCC 默认使用 AT&T 语法。AT&T 语法有以下不同。
- 立即数:所有立即数前需要加上
$
,如1h
在 AT&T 语法中写作$0x1
。 - 寄存器:寄存器名称前需要加上
%
,如寄存器eax
需要写作%eax
。 - 操作数顺序:与 Intel 语法相反,AT&T 语法中,源操作数在前,目标操作数在后,与 Intel 语法相反。如
MOV EBX,EAX
在 AT&T 语法中应为mov %eax,%ebx
。 - 内存寻址:AT&T 语法为
%seg:disp(base,index,scale)
,其中,非实模式下不使用seg
,并且立即数用于 disp 和 scale 时,不加$
号。例:(%eax)
表示访问eax
寄存器中存储的内存地址。 - 操作数长度:Intel 语法中,操作数长度在内存操作数中指定。而 AT&T 语法中,操作数长度在指令中指定,并与 Intel 语法使用的符号不同。
基本语法
内联汇编分为基本内联汇编和扩展内联汇编两种。
基本内联汇编:asm [限定符](汇编语句);
扩展内联汇编:`asm 限定符;
限定符常用于内联汇编。volatile
指示编译器不要自动优化(经常在内联汇编上出问题)这段汇编。
基本内联汇编将汇编语句直接插入程序中(可能会被自动“优化”掉),而编译器会处理扩展内联汇编,自动分配使用的寄存器,将汇编的输入与输出和C语言变量绑定,避免汇编语句干扰变量和程序正常执行,并且可以生成更短、性能更好的代码。因此,除非编写裸函数(编译器不生成函数对应的调用与返回序列)等特殊用途,大部分情况下建议使用扩展内联汇编。
而扩展内联汇编中,汇编语句扩展为汇编模板,此时,编译器就需要分配寄存器,并在汇编语句两侧生成可能需要的额外指令,然后填上汇编模板中的格式串。
如果需要在汇编语句或模板中使用多个汇编指令,可以使用 \n
或者 ;
分隔。
约束模板
扩展内联汇编可指定输入与输出,而将汇编语句与C变量绑定,就需要使用约束模板。
约束模板格式:"[约束]"(值)
常用约束如下:
- 内存:
m
- 通用寄存器:
r
- 立即数:
i
- 编号:十进制数字,用于重复约束,即将一个已有的操作数需要用于其它处(通常为输入和输出),常用于CISC指令集。
- 赋值:
=
- 读取且赋值:
+
赋值与读取赋值操作符用来修饰一个已有的约束。
操作数的大小需要与所编写的汇编指令所需的大小相符,否则会导致编译失败。
此外,在不同架构上有不同的约束符以指定特定寄存器,可将值放入指定位置,或者从指定位置提取值。对于x86与amd64,下列约束可指定特定寄存器:
a
,b
,c
,d
:相应地指定a
,b
,c
,d
寄存器S
:si
寄存器D
:di
寄存器A
: 64位的结果分别储存在a
,d
寄存器中。
在汇编模板中,为了能正确地与C程序互操作,应避免直接使用寄存器名与已定义的符号(如变量名)。
修饰列表(Clobber)
当汇编模板中的汇编语句修改了输入输出约束以外的寄存器或内存,则需要在clobbered
列表中指定,以告知编译器此段指令完成后,相应寄存器、内存值已被改写,不可使用。如果指定多个目标,则使用逗号分隔各个字符串。
什么时候需要clobber,clobber什么?
凡是被修改(无论显式或隐式)的寄存器,并且未出现于输入输出约束中,则需要添加此寄存器。
如果输入输出约束中的寄存器出现在 clobber 列表中,则编译器会报告“无法满足约束”的错误。
如果此段汇编完成后以某种形式修改了输入输出约束以外的内存,则需要添加"memory"
至此列表中。如果改变了标志位寄存器,则为 cc
。
汇编模板
在基本汇编基础上,汇编模板通过将操作数模板化,并在编译时替换,增加了灵活性,有利于C语言与汇编的正确互操作。
常用模板:
%[n]
:引用第n个约束(C操作数)%%
:字面的%
,直接指定寄存器时常用。
例子
in指令
asm volatile("inb %1,%0":"=a"(ret):"Nd"(port):)
其中,N
指示操作数为无符号8位整数。in
与out
指令要求指定的端口为立即数或dx
寄存器。
交换两个值(xchg
指令)
asm volatile("xchg %1,%0":
"=r"(a):
"m"(b),"0"(a):
"memory"
);
注:x86与x86-64的所有指令均不支持两个操作数均为内存。
这里使用了引用约束,"0"(a[i])
中,寄存器与前面的"=r"(a[i])
使用的相同。
报错
gcc 对于有错误的内联汇编的报错能力相比于 C 语言本身差了很多,例如不能准确报告不能满足约束的原因。如果错误发生在汇编器阶段,那么汇编器多半只会给出一个含义难以理解、模棱两可的错误信息,需要通过仔细检查汇编(内联汇编多半不会很长)来确定可能的错误原因,包括操作数大小的匹配、操作数类型是否合法、约束的正确性等。