2.6.3 Gcc嵌入式汇编

Linux的源代码中,有很多C语言的函数中嵌入一段汇编语言程序段,这就是gcc提供的“asm”功能,例如在include/asm-i386/system.h中定义的,读控制寄存器CR0的一个宏read_cr0()


#define read_cr0() ({ \

unsigned int __dummy; \

__asm__( \

"movl %%cr0,%0\n\t" \

:"=r" (__dummy)); \

__dummy; \

})


这种形式看起来比较陌生,这是因为这不是标准C所定义的形式,而是gcc C语言的扩充。其中__dummyC函数所定义的变量;关键词__asm__表示汇编代码的开始。括弧中第一个引号中为汇编指令movl,紧接着有一个冒号,这种形式阅读起来比较复杂。

一般而言,嵌入式汇编语言片段比单纯的汇编语言代码要复杂得多,因为这里存在怎样分配和使用寄存器,以及把C代码中的变量应该存放在哪个寄存器中。为了达到这个目的,就必须对一般的C语言进行扩充,增加对编译器的指导作用,因此,嵌入式汇编看起来晦涩而难以读懂。

1. 嵌入式汇编的一般形式:


__asm__ __volatile__ ("" : output : input : modify);


其中,__asm__表示汇编代码的开始,其后可以跟__volatile__(这是可选项),其含义是避免“asm”指令被删除、移动或组合;然后就是小括弧,括弧中的内容是我们介绍的重点:

·""为汇编指令部分,例如,"movl %%cr0,%0\n\t"。数字前加前缀“%“,如%1,%2等表示使用寄存器的样板操作数。可以使用的操作数总数取决于具体CPU中通用寄存器的数量,如Intel可以有8个。指令中有几个操作数,就说明有几个变量需要与寄存器结合,由gcc在编译时根据后面输出部分和输入部分的约束条件进行相应的处理。由于这些样板操作数的前缀使用了”%“,因此,在用到具体的寄存器时就在前面加两个“%”,如%%cr0

·输出部分(output),用以规定对输出变量(目标操作数)如何与寄存器结合的约束(constraint,输出部分可以有多个约束,互相以逗号分开。每个约束以“=”开头,接着用一个字母来表示操作数的类型,然后是关于变量结合的约束。例如,上例中:

:"=r" (__dummy)

“=r”表示相应的目标操作数(指令部分的%0)可以使用任何一个通用寄存器,并且变量__dummy 存放在这个寄存器中,但如果是:

:“=m”__dummy

“=m”就表示相应的目标操作数是存放在内存单元__dummy中。

表示约束条件的字母很多,表 25 给出几个主要的约束字母及其含义:

2.5 主要的约束字母及其含义

字母

含义

m, v,o

表示内存单元

R

表示任何通用寄存器

Q

表示寄存器eax, ebx, ecx,edx之一

I, h

表示直接操作数

E, F

表示浮点数

G

表示“任意”

a, b.c d

表示要求使用寄存器eax/ax/al, ebx/bx/bl, ecx/cx/cledx/dx/dl

S, D

表示要求使用寄存器esiedi

I

表示常数(031

·输入部分(Input):输入部分与输出部分相似,但没有“=”。如果输入部分一个操作数所要求使用的寄存器,与前面输出部分某个约束所要求的是同一个寄存器,那就把对应操作数的编号(如“1”,“2”等)放在约束条件中,在后面的例子中,我们会看到这种情况。

·修改部分(modify:这部分常常以“memory”为约束条件,以表示操作完成后内存中的内容已有改变,如果原来某个寄存器的内容来自内存,那么现在内存中这个单元的内容已经改变。


注意,指令部分为必选项,而输入部分、输出部分及修改部分为可选项,当输入部分存在,而输出部分不存在时,分号“:“要保留,当“memory”存在时,三个分号都要保留,例如system.h中的宏定义__cli()

#define __cli() __asm__ __volatile__("cli": : :"memory")

2. Linux源代码中嵌入式汇编举例

Linux源代码中,在arch目录下的.h.c文件中,很多文件都涉及嵌入式汇编,下面以system.h中的C函数为例,说明嵌入式汇编的应用。

1)简单应用

#define __save_flags(x) __asm__ __volatile__("pushfl ; popl %0":"=g" (x): /* no input */)

#define __restore_flags(x) __asm__ __volatile__("pushl %0 ; popfl": /* no output */

:"g" (x):"memory", "cc")

第一个宏是保存标志寄存器的值,第二个宏是恢复标志寄存器的值。第一个宏中的pushfl指令是把标志寄存器的值压栈。而popl是把栈顶的值(刚压入栈的flags)弹出到x变量中,这个变量可以存放在一个寄存器或内存中。这样,你可以很容易地读懂第二个宏。

(2) 较复杂应用

static inline unsigned long get_limit(unsigned long segment)

{

unsigned long __limit;

__asm__("lsll %1,%0"

:"=r" (__limit):"r" (segment));

return __limit+1;

}

这是一个设置段界限的函数,汇编代码段中的输出参数为__limit(即%0),输入参数为segment(即%1)。Lsll是加载段界限的指令,即把segment段描述符中的段界限字段装入某个寄存器(这个寄存器与__limit结合),函数返回__limit1,即段长。

3)复杂应用

Linux内核代码中,有关字符串操作的函数都是通过嵌入式汇编完成的,因为内核及用户程序对字符串函数的调用非常频繁,因此,用汇编代码实现主要是为了提高效率(当然是以牺牲可读性和可维护性为代价的)。在此,我们仅列举一个字符串比较函数strcmp,其代码在arch/i386string.h中。

static inline int strcmp(const char * cs,const char * ct)

{

int d0, d1;

register int __res;

__asm__ __volatile__(

"1:\tlodsb\n\t"

"scasb\n\t"

"jne 2f\n\t"

"testb %%al,%%al\n\t"

"jne 1b\n\t"

"xorl %%eax,%%eax\n\t"

"jmp 3f\n"

"2:\tsbbl %%eax,%%eax\n\t"

"orb $1,%%al\n"

"3:"

:"=a" (__res), "=&S" (d0), "=&D" (d1)

:"1" (cs),"2" (ct));

return __res;

}

其中的“\n”是换行符,“\t”tab符,在每条命令的结束加这两个符号,是为了让gcc把嵌入式汇编代码翻译成一般的汇编代码时能够保证换行和留有一定的空格。例如,上面的嵌入式汇编会被翻译成:

1 lodsb //装入串操作数,即从[esi]传送到al寄存器,然后esi指向串中下一个元素

scasb //扫描串操作数,即从al中减去es:[edi],不保留结果,只改变标志

jne2f //如果两个字符不相等,则转到标号2

testb %al %al

jne 1b

xorl %eax %eax

jmp3f

2: sbbl %eax %eax

orb $1 %al

3:

这段代码看起来非常熟悉,读起来也不困难。其中1f表示往前(forword)找到第一个标号为1的那一行,相应地,1b表示往后找。其中嵌入式汇编代码中输出和输入部分的结合情况为:

·返回值__res,放在al寄存器中,与%0相结合;

·局部变量d0,与%1相结合,也与输入部分的cs参数相对应,也存放在寄存器ESI中,即ESI中存放源字符串的起始地址。

·局部变量d1与%2相结合,也与输入部分的ct参数相对应,也存放在寄存器EDI中,即EDI中存放目的字符串的起始地址。

通过对这段代码的分析我们应当体会到,万变不利其本,嵌入式汇编与一般汇编的区别仅仅是形式,本质依然不变。因此,全面掌握Intel 386 汇编指令乃突破阅读低层代码之根本。


Ref: http://oss.org.cn/kernel-book/ch02/2.6.3.htm