本节是第一次在内核源程序中接触到C语言中的嵌入式汇编代码。由于我们在通常的C语言程序的编制过程中一般是不会使用嵌入式汇编程序的,因此这里有必要对其基本格式进行简单的描述,详细的说明可参见GNU gcc手册中[5]第4章的内容(Extensions to the C Language Family),或见参考文献[20](Using Inline Assembly with gcc)。
具有输入和输出参数的嵌入汇编的基本格式为:
asm(“汇编语句”
: 输出寄存器
: 输入寄存器
: 会被修改的寄存器 );
其中,“汇编语句”是你写汇编指令的地方;“输出寄存器”表示当这段嵌入汇编执行完之后,哪些寄存器用于存放输出数据。此地,这些寄存器会分别对应一C 语言表达式或一个内存地址;“输入寄存器”表示在开始执行汇编代码时,这里指定的一些寄存器中应存放的输入值,它们也分别对应着一C变量或常数值。下面我们用例子来说明嵌入汇编语句的使用方法。
我们在下面列出了前面代码中第22行开始的一段代码作为例子来详细解说,为了能看清楚我们将这段代码进行了重新编排和编号。
01 #define get_seg_byte(seg,addr) \
02 ({ \
03 register char __res; \
04 __asm__("push %%fs; \
05 mov %%ax,%%fs; \
06 movb %%fs:%2,%%al; \
07 pop %%fs" \
08 :"=a" (__res) \
09 :"" (seg),"m" (*(addr))); \
10 __res;})
这段10行代码定义了一个嵌入汇编语言宏函数。因为是宏语句,需要在一行上定义,因此这里使用反斜杠'\'将这些语句连成一行。这条宏定义将被替换到宏名称在程序中被引用的地方。第1行定义了宏的名称,也即是宏函数名称get_seg_byte(seg,addr)。第3行定义了一个寄存器变量 __res。第4行上的__asm__表示嵌入汇编语句的开始。从第4行到第7行的4条AT&T格式的汇编语句。
第8行即是输出寄存器,这句的含义是在这段代码运行结束后将eax所代表的寄存器的值放入__res变量中,作为本函数的输出值,"=a"中的"a"称为加载代码, "="表示这是输出寄存器。第9行表示在这段代码开始运行时将seg放到eax寄存器中,""表示使用与上面同个位置的输出相同的寄存器。而(* (addr))表示一个内存偏移地址值。为了在上面汇编语句中使用该地址值,嵌入汇编程序规定把输出和输入寄存器统一按顺序编号,顺序是从输出寄存器序列从左到右从上到下以"%0"开始,分别记为%0、%1、…%9。因此,输出寄存器的编号是%0(这里只有一个输出寄存器),输入寄存器前一部分("" (seg))的编号是%1,而后部分的编号是%2。上面第6行上的%2即代表(*(addr))这个内存偏移量。
现在我们来研究4— 7行上的代码的作用。第一句将fs段寄存器的内容入栈;第二句将eax中的段值赋给fs段寄存器;第三句是把fs:(*(addr))所指定的字节放入 al寄存器中。当执行完汇编语句后,输出寄存器eax的值将被放入__res,作为该宏函数的返回值。很简单,不是吗?
通过上面分析,我们知道,宏名称中的seg代表一指定的内存段值,而addr表示一内存偏移地址量。到现在为止,我们应该很清楚这段程序的功能了吧!该宏函数的功能是从指定段和偏移值的内存地址处取一个字节。
在看下一个例子。
01 asm("cld\n\t"
02 "rep\n\t"
03 "stol"
04 : /* 没有输出寄存器 */
05 : "c"(count-1), "a"(fill_value), "D"(dest)
06 : "%ecx", "%edi");
1-3行这三句是通常的汇编语句,用以清方向位,重复保存值。第4行说明这段嵌入汇编程序没有用到输出寄存器。第5行的含义是:将count-1的值加载到ecx寄存器中(加载代码是"c"),fill_value加载到eax中,dest放到edi中。为什么要让gcc编译程序去做这样的寄存器值的加载,而不让我们自己做呢?因为gcc在它进行寄存器分配时可以进行某些优化工作。例如fill_value值可能已经在eax中。如果是在一个循环语句中的话,gcc就可能在整个循环操作中保留eax,这样就可以在每次循环中少用一个movl语句。
最后一行的作用是告诉gcc这些寄存器中的值已经改变了。很古怪吧?不过在gcc知道你拿这些寄存器做些什么后,这确实能够对gcc的优化操作有所帮助。
下面列表中,是一些你可能会用到的寄存器加载代码及其具体的含义。
表4.1 常用寄存器加载代码说明
代码说明代码说明
-----------------------------------------------
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位)
下面的例子不是让你自己指定哪个变量使用哪个寄存器,而是让gcc为你选择。
01asm("leal (%1, %1, 4), %0"
02: "=r"(y)
03: "0"(x));
第一句汇编语句leal (r1, r2,4), r3语句表示 r1+r2*4 r3。这个例子可以非常快地将x乘5。其中"%0","%1"是指gcc自动分配的寄存器。这里"%1"代表输入值x要放入的寄存器,"%0"表示输出值寄存器。输出寄存器代码前一定要加等于号。如果输入寄存器的代码是0或为空时,则说明使用与相应输出一样的寄存器。所以,如果gcc将r指定为eax的话,那么上面汇编语句的含义即为:
"leal (eax,eax,4), eax"
注意:在执行代码时,如果不希望汇编语句被gcc优化而挪动地方,就需要在asm符号后面添加volatile关键词:
asm volatile (……);
或者更详细的说明为:
__asm__ __volatile__ (……);
下面在具一个较长的例子,如果能看得懂,那就说明嵌入汇编代码对你来说基本没问题了。这段代码是从include/string.h文件中摘取的,是 strncmp()字符串比较函数的一种实现。需要注意的是,其中每行中的"\n\t"是用于gcc预处理程序输出列表好看而设置的,含义与C语言中相同。
//// 字符串1与字符串2的前count个字符进行比较。
// 参数:cs - 字符串1,ct - 字符串2,count - 比较的字符数。
// %0 - eax(__res)返回值,%1 - edi(cs)串1指针,%2 - esi(ct)串2指针,%3 - ecx(count)。
// 返回:如果串1 > 串2,则返回1;串1 = 串2,则返回0;串1 < 串2,则返回-1。
107 extern inline int strncmp(const char * cs,const char * ct,int count)
108 {
109 register int __res __asm__("ax"); // __res是寄存器变量(eax)。
110 __asm__("cld\n" // 清方向位。
111 "1:\tdecl %3\n\t" // count--。
112 "js 2f\n\t" // 如果count<0,则向前跳转到标号2。
113 "lodsb\n\t" // 取串2的字符ds:[esi]al,并且esi++。
114 "scasb\n\t" // 比较al与串1的字符es:[edi],并且edi++。
115 "jne 3f\n\t" // 如果不相等,则向前跳转到标号3。
116 "testb %%al,%%al\n\t" // 该字符是NULL字符吗?
117 "jne 1b\n" // 不是,则向后跳转到标号1,继续比较。
118 "2:\txorl %%eax,%%eax\n\t" // 是NULL字符,则eax清零(返回值)。
119 "jmp 4f\n" // 向前跳转到标号4,结束。
120 "3:\tmovl $1,%%eax\n\t" // eax中置1。
121 "jl 4f\n\t" // 如果前面比较中串2字符<串2字符,则返回1,结束。
122 "negl %%eax\n" // 否则eax = -eax,返回负值,结束。
123 "4:"
124 :"=a" (__res):"D" (cs),"S" (ct),"c" (count):"si","di","cx");
125 return __res; // 返回比较结果。
126 }