作者Bharata B Rao
将各个部分组合起来
如果您是 Linux 内核的开发人员您会发现自己经常要对与体系结构高度相关的功能进行编码或优化代码路径您很可能是通过将汇编语言指令插入到 C 语句的中间(又称为内联汇编的一种方法)来执行这些任务的让我们看一下 Linux 中内联汇编的特定用法(我们将讨论限制在 IA 汇编)
GNU 汇编程序简述
让我们首先看一下 Linux 中使用的基本汇编程序语法GCC(用于 Linux 的 GNU C 编译器)使用 AT&T 汇编语法下面列出了这种语法的一些基本规则(该列表肯定不完整只包括了与内联汇编相关的那些规则)
寄存器命名
寄存器名称有 % 前缀即如果必须使用 eax它应该用作 %eax
源操作数和目的操作数的顺序
在所有指令中先是源操作数然后才是目的操作数这与将源操作数放在目的操作数之后的 Intel 语法不同
mov %eax %ebx transfers the contents of eax to ebx
操作数大小
根据操作数是字节 (byte)字 (word) 还是长型 (long)指令的后缀可以是 bw 或 l这并不是强制性的GCC 会尝试通过读取操作数来提供相应的后缀但手工指定后缀可以改善代码的可读性并可以消除编译器猜测不正确的可能性
movb %al %bl Byte move
movw %ax %bx Word move
movl %eax %ebx Longword move
立即操作数
通过使用 $ 指定直接操作数
movl $xffff %eax will move the value of xffff into eax register
间接内存引用
任何对内存的间接引用都是通过使用 ( ) 来完成的
movb (%esi) %al will transfer the byte in the memory
pointed by esi into al
register
内联汇编
GCC 为内联汇编提供特殊结构它具有以下格式
GCG 的 asm 结构
asm ( assembler template
: output operands (optional)
: input operands (optional)
: list of clobbered registers
(optional)
);
本例中汇编程序模板由汇编指令组成输入操作数是充当指令输入操作数使用的 C 表达式输出操作数是将对其执行汇编指令输出的 C 表达式
内联汇编的重要性体现在它能够灵活操作而且可以使其输出通过 C 变量显示出来因为它具有这种能力所以 asm 可以用作汇编指令和包含它的 C 程序之间的接口
一个非常基本但很重要的区别在于简单内联汇编只包括指令而扩展内联汇编包括操作数要说明这一点考虑以下示例
内联汇编的基本要素
{
int a= b;
asm (movl % %%eax;
movl %%eax %;
:=r(b) /* output */
:r(a) /* input */
:%eax); /* clobbered register */
}
在上例中我们使用汇编指令使 b 的值等于 a请注意以下几点
b 是输出操作数由 % 引用a 是输入操作数由 % 引用
r 是操作数的约束它指定将变量 a 和 b 存储在寄存器中请注意输出操作数约束应该带有一个约束修饰符 =指定它是输出操作数
要在 asm 内使用寄存器 %eax%eax 的前面应该再加一个 %换句话说就是 %%eax因为 asm 使用 %% 等来标识变量任何带有一个 % 的数都看作是输入/输出操作数而不认为是寄存器
第三个冒号后的修饰寄存器 %eax 告诉将在 asm 中修改 GCC %eax 的值这样 GCC 就不使用该寄存器存储任何其它的值
movl % %%eax 将 a 的值移到 %eax 中movl %%eax % 将 %eax 的内容移到 b 中
因为 b 被指定成输出操作数因此当 asm 的执行完成后它将反映出更新的值换句话说对 asm 内 b 所做的更改将在 asm 外反映出来
现在让我们更详细的了解每一项的含义
汇编程序模板
汇编程序模板是一组插入到 C 程序中的汇编指令(可以是单个指令也可以是一组指令)每条指令都应该由双引号括起或者整组指令应该由双引号括起每条指令还应该用一个定界符结尾有效的定界符为新行 (\n) 和分号 (;) \n 后可以跟一个 tab(\t) 作为格式化符号增加 GCC 在汇编文件中生成的指令的可读性 指令通过数 %% 等来引用 C 表达式(指定为操作数)
如果希望确保编译器不会在 asm 内部优化指令可以在 asm 后使用关键字 volatile如果程序必须与 ANSI C 兼容则应该使用 __asm__ 和 __volatile__而不是 asm 和 volatile
操作数
C 表达式用作 asm 内的汇编指令操作数在汇编指令通过对 C 程序的 C 表达式进行操作来执行有意义的作业的情况下操作数是内联汇编的主要特性
每个操作数都由操作数约束字符串指定后面跟用括弧括起的 C 表达式例如constraint (C expression)操作数约束的主要功能是确定操作数的寻址方式
可以在输入和输出部分中同时使用多个操作数每个操作数由逗号分隔开
在汇编程序模板内部操作数由数字引用如果总共有 n 个操作数(包括输入和输出)那么第一个输出操作数的编号为 逐项递增最后那个输入操作数的编号为 n总操作数的数目限制在 如果机器描述中任何指令模式中的最大操作数数目大于 则使用后者作为限制
修饰寄存器列表
如果 asm 中的指令指的是硬件寄存器可以告诉 GCC 我们将自己使用和修改它们这样GCC 就不会假设它装入到这些寄存器中的值是有效值通常不需要将输入和输出寄存器列为 clobbered因为 GCC 知道 asm 使用它们(因为它们被明确指定为约束)不过如果指令使用任何其它的寄存器无论是明确的还是隐含的(寄存器不在输入约束列表中出现也不在输出约束列表中出现)寄存器都必须被指定为修饰列表修饰寄存器列在第三个冒号之后其名称被指定为字符串
至于关键字如果指令以某些不可预知且不明确的方式修改了内存则可能将 memory 关键字添加到修饰寄存器列表中这样就告诉 GCC 不要在不同指令之间将内存值高速缓存在寄存器中
操作数约束
前面提到过asm 中的每个操作数都应该由操作数约束字符串描述后面跟用括弧括起的 C 表达式操作数约束主要是确定指令中操作数的寻址方式约束也可以指定
是否允许操作数位于寄存器中以及它可以包括在哪些种类的寄存器中
操作数是否可以是内存引用以及在这种情况下使用哪些种类的地址
操作数是否可以是立即数
约束还要求两个操作数匹配
常用约束
在可用的操作数约束中只有一小部分是常用的下面列出了这些约束以及简要描述有关操作数约束的完整列表请参考 GCC 和 GAS 手册
寄存器操作数约束 (r)
使用这种约束指定操作数时它们存储在通用寄存器中请看下例
asm (movl %%cr %\n :=r(crval));
这里变量 crval 保存在寄存器中%cr 的值复制到寄存器上crval 的值从该寄存器更新到内存中指定 r 约束时GCC 可以将变量 crval 保存在任何可用的 GPR 中要指定寄存器必须通过使用特定的寄存器约束直接指定寄存器名
a %eax
b %ebx
c %ecx
d %edx
S %esi
D %edi
内存操作数约束 (m)
当操作数位于内存中时任何对它们执行的操作都将在内存位置中直接发生这与寄存器约束正好相反后者先将值存储在要修改的寄存器中然后将它写回内存位置中但寄存器约束通常只在对于指令来说它们是绝对必需的或者它们可以大大提高进程速度时使用当需要在 asm 内部更新 C 变量而您又确实不希望使用寄存器来保存其值时使用内存约束最为有效例如idtr 的值存储在内存位置 loc 中
(sidt %\n : :m(loc));
匹配(数字)约束
在某些情况下一个变量既要充当输入操作数也要充当输出操作数可以通过使用匹配约束在 asm 中指定这种情况
asm (incl % :=a(var):(var));
在匹配约束的示例中寄存器 %eax 既用作输入变量也用作输出变量将 var 输入读取到 %eax增加后将更新的 %eax 再次存储在 var 中这里的 指定第 个输出变量相同的约束即它指定 var 的输出实例只应该存储在 %eax 中该约束可以用于以下情况
输入从变量中读取或者变量被修改后修改写回到同一变量中
不需要将输入操作数和输出操作数的实例分开
使用匹配约束最重要的意义在于它们可以导致有效地使用可用寄存器
一般内联汇编用法示例
以下示例通过各种不同的操作数约束说明了用法有如此多的约束以至于无法将它们一一列出这里只列出了最经常使用的那些约束类型
asm 和寄存器约束 r
让我们先看一下使用寄存器约束 r 的 asm我们的示例显示了 GCC 如何分配寄存器以及它如何更