为什么要用内联汇编
首先,对于那些频繁调用的函数,为了提高执行效率,直接用汇编写比较好。
其次,有些功能只能用汇编实现,比如开中断和关中断:
#define sti() __asm__ ("sti"::)
#define cli() __asm__ ("cli"::)
内联汇编的重要性体现在它能够灵活操作,而且可以使其输出通过 C 变量显示出来。因为它具有这种能力,所以 “asm” 可以用作汇编指令和包含它的 C 程序之间的接口
内联汇编的基本要素
{
int a=10, b;
asm ("movl %1, %%eax;
movl %%eax, %0;"
:"=r"(b) /* output */
:"r"(a) /* input */
:"%eax"); /* clobbered register */
}
在上例中,我们使用汇编指令使 “b” 的值等于 “a”。请注意以下几点:
- “b” 是输出操作数,由 %0 引用,“a” 是输入操作数,由 %1 引用。
- “r” 是操作数的约束,它指定将变量 “a” 和 “b” 存储在寄存器中。请注意,输出操作数约束应该带有一个约束修饰符 “=”,指定它是输出操作数。
- 要在 “asm” 内使用寄存器 %eax,%eax 的前面应该再加一个 %,换句话说就是 %%eax,因为 “asm” 使用 %0、%1 等来标识变量。任何带有一个 % 的数都看作是输入/输出操作数,而不认为是寄存器。
- 第三个冒号后的修饰寄存器 %eax 告诉将在 “asm” 中修改 GCC %eax 的值,这样 GCC 就不使用该寄存器存储任何其它的值。
movl %1, %%eax
将 “a” 的值移到 %eax 中,movl %%eax, %0
将 %eax 的内容移到 “b” 中。- 因为 “b” 被指定成输出操作数,因此当 “asm” 的执行完成后,它将反映出更新的值。换句话说,对 “asm” 内 “b” 所做的更改将在 “asm” 外反映出来。
语法
__asm__(汇编语句模板: 输出部分: 输入部分: 破坏描述部分)
共四个部分:汇编语句模板,输出部分,输入部分,破坏描述部分。
各部分使用冒号格开,汇编语句模板必不可少,其他三部分可选。
如果使用了后面的部分,而前面部分为空,也需要用冒号格开,例如:
__asm__ __volatile__("cli": : :"memory")
其中,“asm” 是内联汇编语句关键词;
汇编语句模板:就是我们写汇编指令的地方;
输出部分:表示当这段嵌入汇编执行完之后,哪些寄存器用于存放输出数据。同时,这些寄存器会分别对应一 C 语言表达式值或一个内存地址;
输入部分:表示在开始执行汇编代码时,这里指定的一些寄存器中应存放的输入值,它们也分别对应着一 C 变量或常数值。
破坏描述部分:表示你已对其中列出的寄存器中的值进行了改动,GCC 编译器不能再依赖于它原先对这些寄存器加载的值。如果必要的话,GCC 需要重新加载这些寄存器。因此我们需要把那些没有在输出或输入寄存器部分列出,但是在汇编语句中明确使用到或隐含使用到的寄存器名列在这个部分中。
汇编语句模板
汇编程序模板是一组插入到 C 程序中的汇编指令(可以是单个指令,也可以是一组指令)。每条指令都应该由双引号括起,或者整组指令应该由双引号括起。每条指令还应该用一个定界符结尾。有效的定界符为新行 (\n) 和分号。 ‘\n’ 后可以跟一个 tab(\t) 作为格式化符号,增加 GCC 在汇编文件中生成的指令的可读性。 指令通过数 %0、%1 等来引用 C 表达式(指定为操作数)。
如果希望确保编译器不会在 “asm” 内部优化指令,可以在 “asm” 后使用关键字 “volatile”。如果程序必须与 ANSI C 兼容,则应该使用 __asm__
和 __volatile__
,而不是 asm 和 volatile。
操作数
C 表达式用作 “asm” 内的汇编指令操作数。在汇编指令通过对 C 程序的 C 表达式进行操作来执行有意义的作业的情况下,操作数是内联汇编的主要特性。
每个操作数都由操作数约束字符串指定,后面跟用括弧括起的 C 表达式,例如:“constraint” (C expression)。操作数约束的主要功能是确定操作数的寻址方式。
可以在输入和输出部分中同时使用多个操作数。每个操作数由逗号分隔开。
在汇编语句模板内部,操作数由数字引用。如果总共有 n 个操作数(包括输入和输出),那么第一个输出操作数的编号为 0,逐项递增,最后那个输入操作数的编号为 n -1。
举例
static __inline__ void atomic_add(int i, atomic_t *v)
{
__asm__ __volatile__(
LOCK "addl %1,%0"
:"=m" (v->counter)
:"ir" (i), "m" (v->counter));
}
上面的例子,%0 就代表 v->counter,%1 就代表 i,%2 也代表 v->counter(代码中没有用 2%)
输出部分和输入部分
每个输出操作数的限定字符串必须包含“=”,表示他是一个输出操作数。
下面这个例子有助于理解输入和输出操作数,也展示了寄存器约束 “r” 的用法。
int main(void)
{
int x = 10, y;
asm ("movl %1, %%eax;
"movl %%eax, %0;"
:"=r"(y) /* y is output operand */
:"r"(x) /* x is input operand */
:"%eax"); /* %eax is clobbered register */
}
在该例中,x 的值复制为 “asm” 中的 y。x 和 y 都通过存储在寄存器中传递给 “asm”。为该例生成的汇编代码如下:
main:
pushl %ebp
movl %esp,%ebp
subl $8,%esp
movl $10,-4(%ebp)
movl -4(%ebp),%edx /* x=10 is stored in %edx */
#APP /* asm starts here */
movl %edx, %eax /* x is moved to %eax */
movl %eax, %edx /* y is allocated in edx and updated */
#NO_APP /* asm ends here */
movl %edx,-8(%ebp) /* value of y in stack is
updated with the value in %edx */
当使用 “r” 约束时,GCC 在这里可以自由分配任何寄存器。在我们的示例中,它选择 %edx 来存储 x。在读取了 %edx 中 x 的值后,它为 y 也分配了相同的寄存器。
注意第 11 行,因为 y 是在输出操作数部分中指定的,所以 %edx 中更新的值存储在 -8(%ebp),堆栈上 y 的位置中。如果 y 是在输入部分中指定的,那么即使它在 y 的临时寄存器存储值 (%edx) 中被更新,堆栈上 y 的值也不会更新。
因为 %eax 是在修饰列表中指定的,GCC 不在任何其它地方使用它来存储数据。
在这个例子中,GCC 为输入的 x 和作为输出的 y 分配了同一个寄存器 %edx。
要确保输入和输出分配到不同的寄存器中,可以指定 & 约束修饰符。下面是添加了约束修饰符的示例。
int main(void)
{
int x = 10, y;
asm ("movl %1, %%eax;
"movl %%eax, %0;"
:"=&r"(y) /* y is output operand, note the
& constraint modifier. */
:"r"(x) /* x is input operand */
:"%eax"); /* %eax is clobbered register */
}
以下是为该示例生成的汇编代码,从中可以明显地看出 x 和 y 存储在 “asm” 中不同的寄存器中。
main:
pushl %ebp
movl %esp,%ebp
subl $8,%esp
movl $10,-4(%ebp)
movl -4(%ebp),%ecx /* x, the input is in %ecx */
#APP
movl %ecx, %eax
movl %eax, %edx /* y, the output is in %edx */
#NO_APP
movl %edx,-8(%ebp)
为 x 分配了 %ecx,为 y 分配了 %edx
操作数约束
前面提到过,“asm” 中的每个操作数都应该由操作数约束字符串描述,后面跟用括弧括起的 C 表达式。操作数约束主要是确定指令中操作数的寻址方式。约束也可以指定:
- 是否允许操作数位于寄存器中,以及它可以包括在哪些种类的寄存器中
- 操作数是否可以是内存引用,以及在这种情况下使用哪些种类的地址
- 操作数是否可以是立即数
约束还要求两个操作数匹配。
常用约束
在可用的操作数约束中,只有一小部分是常用的;下面列出了这些约束以及简要描述。有关操作数约束的完整列表,请参考 GCC 和 GAS 手册。
寄存器操作数约束
要指定寄存器,必须通过使用特定的寄存器约束直接指定寄存器名。
对应关系是
a %eax
b %ebx
c %ecx
d %edx
S %esi
D %edi
内存操作数约束 (m)
当操作数位于内存中时,任何对它们执行的操作都将在内存位置中直接发生,这与寄存器约束正好相反,后者先将值存储在要修改的寄存器中,然后将它写回内存位置中。
当需要在 “asm” 内部更新 C 变量,而您又确实不希望使用寄存器来保存其值时,使用内存约束最为有效。例如,idtr 的值存储在内存位置 loc 中:
SIDT 指令:Store Interrupt Descriptor Table Register
("sidt %0\n" : :"m"(loc));
匹配(数字)约束
在某些情况下,一个变量既要充当输入操作数,也要充当输出操作数。可以通过使用匹配约束。
asm ("incl %0" :"=a"(var):"0"(var));
在匹配约束的示例中,寄存器 %eax 既用作输入变量,也用作输出变量。将 var 输入读取到 %eax,增加后将更新的 %eax 再次存储在 var 中。这里的 “0” 指定和第 0 个输出变量有相同的约束。
- 输入从变量中读取,或者变量被修改后,修改写回到同一变量中
- 不需要将输入操作数和输出操作数的实例分开
(这是一个简陋的版本,后面再扩充)