Chinaunix首页 | 论坛 | 博客
  • 博客访问: 27148
  • 博文数量: 7
  • 博客积分: 244
  • 博客等级: 入伍新兵
  • 技术积分: 80
  • 用 户 组: 普通用户
  • 注册时间: 2010-04-29 06:53
文章分类
文章存档

2010年(7)

最近访客

分类:

2010-05-08 18:46:45

触到的嵌入式汇编(内联汇编)语句。由于在编制C程序过程中一般很少用到嵌入式汇编代码,因此这里有必要对其基本格式和使用方法进行说明。具有输入和输出参数的嵌入汇编语句的基本格式为:
asm("汇编语句"
: 输出寄存器
: 输入寄存器
: 会被修改的寄存器);

除第1行以外,后面带冒号的行若不使用就都可以省略。其中,"asm"是内联汇编语句关键词;"汇编语句"是你写汇编指令的地方;"输出寄存器"表示当这段嵌入汇编执行完之后,哪些寄存器用于存放输出数据。这些寄存器会分别对应一C语言表达式值或一个内存地址;"输入寄存器"表示在开始执行汇编代码时,这里指定的一些寄存器中应存放的输入值,它们也分别对应着一C变量或常数值。"会被修改的寄存器"表示你已对其中列出的寄存器中的值进行了改动,gcc编译器不能再依赖于它原先对这些寄存器加载的值。如果必要的话,gcc需要重新加载这些寄存器。因此我们需要把那些没有在输出/输入寄存器中的部分列出,但是在汇编语句中明确使用到或隐含使用到的寄存器名列在这个部分中。

下面我们用例子来说明嵌入汇编语句的使用方法。这里列出了kernel/traps.c文件中第22行开始的一段代码作为例子来详细解说。为了能看得更清楚一些,我们对这段代码进行了重新排列和编号。

01  #define get_seg_byte(seg,addr) \
02  ({ \
03  register char __res; \   // 定义了一个寄存器变量__res。
04  __asm__("push %%fs;  \    // 首先保存fs寄存器原值(段选择符)。
05  mov %%ax,%%fs;  \  // 然后用seg设置fs。
06  movb %%fs:%2,%%al;  \    // 取seg:addr处1字节内容到al寄存器中。
07  pop %%fs"  \    // 恢复fs寄存器原内容。
08  :"=a" (__res)  \  // 输出寄存器列表。
09  :"0" (seg),"m" (*(addr))); \    // 输入寄存器列表。
10  __res;})

这段代码定义了一个嵌入汇编语言宏函数。通常使用汇编语句最方便的方式是把它们放在一个宏内。用圆括号括住的组合语句(花括号中的语句)"({})"可以作为表达式使用,其中最后一行(第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  常用寄存器加载代码说明

代码

说明

代码

说明

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

使用动态分配字节可寻址寄存器(eaxebxecxedx

M

使用常数0-3

r

使用任意动态分配的寄存器

N

使用1字节常数(0-255

g

使用通用有效的地址即可(eaxebxecxedx或内存变量)

O

使用常数0-31

A

使用eaxedx联合(64)

=

输出操作数。输出值将替换前值

+

表示操作数可读可写

&

早期会变的(earlyclobber)操作数。表示在使用完操作数之前,内容会被修改

现在我们来研究第4~7行代码的作用。第一句将fs段寄存器的内容入栈;第二句将eax中的段值赋给fs段寄存器;第三句是把fs:(*(addr))所指定的字节放入al寄存器中。当执行完汇编语句后,输出寄存器eax的值将被放入__res,作为该宏函数(块结构表达式)的返回值。

通过上面的分析知道,宏名称中的seg代表一指定的内存段值,而addr表示一内存偏移地址量。到现在为止,应该很清楚这段程序的功能了吧!该宏函数的功能是从指定段和偏移值的内存地址处取1字节。再看下一个例子。

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行是通常的汇编语句,用来清方向位,重复保存值。其中头两行中的字符"\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"
02  : "=r"(y)
03  : "0"(x));
指令"leal"用于计算有效地址,但这里用它来进行一些简单计算。第1条汇编语句"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__ (……);
关键词volatile也可以放在函数名前来修饰函数,用来通知gcc编译器该函数不会返回。这样就可以让gcc产生更好一些的代码。另外,对于不会返回的函数,这个关键词也可以用来避免gcc产生假警告信息。例如mm/memory.c中的如下语句说明函数do_exit()和oom()不会再返回到调用者代码中:
31 volatile void do_exit(long code);
32
33 static inline volatile void oom(void)
34 {
35printk("out of memory\n\r");
36do_exit(SIGSEGV);
37 }
下面再举一个较长的例子,读者如果能看懂,那就说明嵌入汇编代码对你来说基本没问题了。这段代码是从include/string.h文件中摘取的,是strncmp()字符串比较函数的一种实现。同样,其中每行中的"\n\t"是用于gcc预处理程序输出列表的美观而设置的。
//// 字符串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。
extern inline int strncmp(const char * cs,const char * ct,int count)
{
register int __res ;    // __res是寄存器变量。
__asm__("cld\n"   // 清方向位。
"1:\tdecl %3\n\t"   // count--。
"js 2f\n\t"    // 如果count<0,则向前跳转到标号2。
"lodsb\n\t"    // 取串2的字符ds:[esi]→al,并且esi++。
"scasb\n\t"  // 比较al与串1的字符es:[edi],并且edi++。
"jne 3f\n\t"    // 如果不相等,则向前跳转到标号3。
"testb %%al,%%al\n\t"  // 该字符是NULL字符吗?
"jne 1b\n"    // 不是,则向后跳转到标号1,继续比较。
"2:\txorl %%eax,%%eax\n\t" // 是NULL字符,则eax清零(返回值)。
"jmp 4f\n"   // 向前跳转到标号4,结束。
"3:\tmovl $1,%%eax\n\t" // eax中置1。
"jl 4f\n\t"  // 如果前面比较中串2字符<串1字符,则返回1,结束。
"negl %%eax\n" // 否则eax = -eax,返回负值,结束。
"4:"
:"=a" (__res):"D" (cs),"S" (ct),"c" (count):"si","di","cx");
return __res;  // 返回比较结果。
}
阅读(1573) | 评论(0) | 转发(0) |
0

上一篇:硬盘源代码

下一篇:嵌入式汇编

给主人留下些什么吧!~~