3.3.2 嵌入汇编
本节介绍内核C语言程序中接触到的嵌入式汇编(内联汇编)语句。由于在编制C程序过程中一般很少用到嵌入式汇编代码,因此这里有必要对其基本格式和使用方法进行说明。具有输入和输出参数的嵌入汇编语句的基本格式为:
asm("汇编语句" |
除第1行以外,后面带冒号的行若不使用就都可以省略。其中,"asm"是内联汇编语句关键词;"汇编语句"是你写汇编指令的地方;"输出寄存器"表示当这段嵌入汇编执行完之后,哪些寄存器用于存放输出数据。这些寄存器会分别对应一C语言表达式值或一个内存地址;"输入寄存器"表示在开始执行汇编代码时,这里指定的一些寄存器中应存放的输入值,它们也分别对应着一C变量或常数值。"会被修改的寄存器"表示你已对其中列出的寄存器中的值进行了改动,gcc编译器不能再依赖于它原先对这些寄存器加载的值。如果必要的话,gcc需要重新加载这些寄存器。因此我们需要把那些没有在输出/输入寄存器中的部分列出,但是在汇编语句中明确使用到或隐含使用到的寄存器名列在这个部分中。
下面我们用例子来说明嵌入汇编语句的使用方法。这里列出了kernel/traps.c文件中第22行开始的一段代码作为例子来详细解说。为了能看得更清楚一些,我们对这段代码进行了重新排列和编号。
01 #define get_seg_byte(seg,addr) \ |
这段代码定义了一个嵌入汇编语言宏函数。通常使用汇编语句最方便的方式是把它们放在一个宏内。用圆括号括住的组合语句(花括号中的语句)"({})"可以作为表达式使用,其中最后一行(第10行)的变量__res是该表达式的输出值,见下一节说明。
因为宏语句需要定义在一行上,因此这里使用反斜杠"\"将这些语句连成一行。这条宏定义将被替换到程序中引用该宏名称的地方。第1行定义了宏的名称,即宏函数名称get_seg_byte (seg,addr)。第3行定义了一个寄存器变量__res。该变量将被保存在一个寄存器中,以便快速访问和操作。如果想指定寄存器(例如eax),那么我们可以把该句写成"register char __res asm ("ax");",其中asm也可以写成__asm__。第4行上的__asm__表示嵌入汇编语句的开始。第4~7行的4条语句是AT&T格式的汇编语句。另外,为了让gcc编译产生的汇编语言程序中寄存器名称前有一个百分号"%",在嵌入汇编语句寄存器名称前就必须写上两个百分号"%%"。
第8行即输出寄存器,该语句的含义是在这段代码运行结束后将eax所代表的寄存器的值放入__res变量中,作为本函数的输出值,"=a"中的"a"称为加载代码,"="表示这是输出寄存器,并且其中的值将被输出值替代。加载代码是CPU寄存器、内存地址以及一些数值的简写字母代号。表3-4中是一些常用的寄存器加载代码及其具体的含义。第9行表示在这段代码开始运行时将seg放到eax寄存器中,"0"表示使用与上面相同位置上的输出寄存器。而(*(addr))表示一个内存偏移地址值。为了在上面汇编语句中使用该地址值,嵌入汇编程序规定把输出和输入寄存器统一按顺序编号,顺序是从输出寄存器序列从左到右从上到下以"%0"开始,分别记为%0、%1、…%9。因此,输出寄存器的编号是%0(这里只有一个输出寄存器),输入寄存器前一部分("0" (seg))的编号是%1,而后部分的编号是%2。上面第6行上的%2即代表(*(addr))这个内存偏移量。
表3-4 常用寄存器加载代码说明
|
代码< xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" /> |
< xml:namespace prefix = v ns = "urn:schemas-microsoft-com:vml" /> |
代码 |
说明 |
|
a |
使用寄存器eax |
m |
使用内存地址 |
|
b |
使用寄存器ebx |
o |
使用内存地址并可以加偏移值 |
|
c |
使用寄存器ecx |
I |
使用常数0-31 |
|
d |
使用寄存器edx |
J |
使用常数0-63 |
|
S |
使用esi |
K |
使用常数0-255 |
|
D |
使用edi |
L |
使用常数0-65535 |
|
q |
使用动态分配字节可寻址寄存器(eax、ebx、ecx或edx) |
M |
使用常数0-3 |
|
r |
使用任意动态分配的寄存器 |
N |
使用1字节常数(0-255) |
|
g |
使用通用有效的地址即可(eax、ebx、ecx、edx或内存变量) |
O |
使用常数0-31 |
|
A |
使用eax与edx联合(64位) |
= |
输出操作数。输出值将替换前值 |
|
+ |
表示操作数可读可写 |
& |
早期会变的(earlyclobber)操作数。表示在使用完操作数之前,内容会被修改 |
现在我们来研究第4~7行代码的作用。第一句将fs段寄存器的内容入栈;第二句将eax中的段值赋给fs段寄存器;第三句是把fs:(*(addr))所指定的字节放入al寄存器中。当执行完汇编语句后,输出寄存器eax的值将被放入__res,作为该宏函数(块结构表达式)的返回值。
通过上面的分析知道,宏名称中的seg代表一指定的内存段值,而addr表示一内存偏移地址量。到现在为止,应该很清楚这段程序的功能了吧!该宏函数的功能是从指定段和偏移值的内存地址处取1字节。再看下一个例子。
01 asm("cld\n\t" |
1~3行是通常的汇编语句,用来清方向位,重复保存值。其中头两行中的字符"\n\t"是用于gcc预处理程序输出程序列表时能排得整齐而设置的,字符的含义与C语言中的相同。即gcc的运作方式是先产生与C程序对应的汇编程序,然后调用汇编器对其进行编译产生目标代码。如果在写程序和调试程序时想看看C对应的汇编程序,那么就需要得到预处理程序输出的汇编程序结果(这是编写和调试高效的代码时常用的做法)。为了预处理输出的汇编程序格式整齐,就可以使用"\n"、"\t"这两个格式符号。
第4行说明这段嵌入汇编程序没有用到输出寄存器。第5行的含义是:将count-1的值加载到ecx寄存器中(加载代码是"c"),fill_value加载到eax中,dest放到edi中。为什么要让gcc编译程序去做这样的寄存器值的加载,而不是程序员自己做呢?因为gcc在它进行寄存器分配时可以进行某些优化工作。例如fill_value值可能已经在eax中。如果是在一个循环语句中,则gcc就可能在整个循环操作中保留eax,这样就可以在每次循环中少用一个movl语句。
最后一行的作用是告诉gcc这些寄存器中的值已经改变了。在gcc知道你拿这些寄存器做些什么后,能够对gcc的优化操作有所帮助。下面的例子不是让你自己指定哪个变量使用哪个寄存器,而是让gcc为你选择。
01 asm("leal (%1, %1, 4), %0" |
"leal (eax,eax,4), eax" |
asm volatile (……); |
__asm__ __volatile__ (……); |
31 volatile void do_exit(long code); |
//// 字符串1与字符串2的前count个字符进行比较。 |