3.3.2 嵌入汇编(摘自<linux内核完全剖析>)
内核C语言程序嵌入式汇编代码又叫内联汇编,具有输入和输出参数的嵌入汇编语句的基本格式为:
**************************************************
asm("汇编语句"
: 输出寄存器
: 输入寄存器
: 会被修改的寄存器);
**************************************************
除第一行外,后面带冒号的行若不使用就可以省略。其中,"asm"是内联汇编语句关键词;"汇编语句"是写汇编指令的地方;“输出寄存器”表示当这段嵌入式汇编执行完成后,哪些寄存器用于存放输出数据。这些寄存器会分别对应一个C语言表达式的值或一个内存地址;“输入寄存器”表示在开始执行汇编代码时,这里指定的寄存器中应存放输入值,他们也分别对应着一C语言变量或常数值。“会被修改的寄存器”表示你已经对其中列出的寄存器额值进行了改动,gcc编译器不能再依赖于它原先对这些寄存器加载的值。如果必要的话,gcc需要重新加载这些寄存器。因此我们要把那些没有在输入/输出寄存器部分列出,但是在汇编语句中明确使用到或隐含使用到的寄存器名列在这个部分中。
例子:
asm("cld\n\t" "rep\n\t" "stol" /*没有输出寄存器*/ "c"(count-1), "a"(fill_value), "D"(dest) "%eax", "%edi");
1-3行汇编语句用于清零方向位,重复保存值。第4行说明这段嵌入式汇编程序没有用到输出寄存器。第5行的含义是:将count-1的值加载到ecx寄存器中(加载代码是"c"),fill_value加载到eax中,dest放到edi中。为什么要让gcc编译程序去做这些寄存器值的加载,而不让我们自己做呢?因为gcc在它进行寄存器分配时可以进行某些优化工作,例如fill_value值可能已经在eax中,这样就可以在每次循环中少用一个movl语句。最后一行是告诉gcc这些寄存器的值已经改变了。在gcc知道你拿这些寄存器做些什么后,能够对gcc的优化操作有所帮助。
常用寄存器加载代码说明
a - 使用寄存器eax b - 使用寄存器ebx c - 使用寄存器ecx d - 使用寄存器edx S - 使用esi D - 使用edi
q - 使用动态分配字节可寻址寄存器(eax,ebx,ecx,edx) r - 使用任意动态分配的寄存器 g - 使用通用有效的地址即可(eax,ebx,ecx,edx或内存变量)
A - 使用eax与edx联合(64位) + - 表示操作数可读可写 m - 使用内存地址 o - 使用内存地址并可以加偏移值
I - 使用常数0~31 J - 使用常数0~63 K - 使用常数0~255 L - 使用常数0~65535 M - 使用常数0~3 N - 使用1字节常数(0~255)
O - 使用常数0~31 = - 输出操作数,输出值将替换前值 & - 早起会变的(earlyclobber)操作数。表示在使用完操作数之前,内容会被修改。
下面这个例子不是让你自己制定哪个变量使用哪个寄存器,而是让gcc为你选择。
asm("leal (%1, %1, 4), %0" : "=r"(y) : "0"(x);
注意,在执行代码时,如果不希望汇编语句被gcc优化而修改,就需要在asm符号后面添加关键词volatile:
asm volatile(.....); 或者更详细的说明为: _asm_ _volatile_(.....);
这两种声明的区别在于程序兼容性方面。建议使用后一种声明方式。
关键词volatile也可以放在函数名前来修饰函数,用来通知gcc编译器该函数不会返回。这样就可以让gcc产生更好一些的代码。另外,对于不会返回的函数,这个关键词也可以用来避免gcc产生假的警告信息。
下面这个例子是从include/string.h文件中摘取的,是strncmp()字符串比较函数的一种实现。其中每行中的"\n\t"是用于gcc预处理程序输出好看而设置的,含义与C语言中相同。
extern inline int strncmp(const char *cs,const char *ct,int count) { register int _res; _asm_("cld\n" "1:\tdecl %3\n\t" "js 2f\n\t" "lodsb \n\t" "scasb\n\t" "jne 3f\n\t" "testb %%al, %%al\n\t" "jne 1b\n" "2:\txorl %%eax,%%eax\n\t" "jmp 4f\n" "3:\tmovl $1, %%eax\n\t" "jl 4f\n\t" "negl %%eax\n" "4:" :"=a"(_res)
:"D"(cs),"S"(ct),"c"(count) :"si","di","cx"); return _res; }
下面对这段代码简单注释:
extern关键字将这个函数定义为外部函数,这个是C语言中的关键字,以便于其他文件中的代码来调用这个函数。inline关键字将这个函数定义为内联函数,当其他函数调用这个函数的时候,gcc会把该函数的代码集成到调用函数的代码中去。
函数体开始部分首先声明一个寄存器整型变量_res,接下来是嵌入式汇编部分,最后把这个_res做为返回值返回。汇编代码先从最后三行解释,倒数第三行"=a"表示将eax寄存器做为输出寄存器,并将eax中的值赋值给_res这个函数返回值,倒数第二行定义三个输入寄存器,分别将cs,ct这两个字符串指针赋值给edi和esi寄存器,然后将比较字符的数量count赋值给ecx寄存器。最后一行表示程序运行过程中除了输入输出寄存器以外,还会改变esi,edi和ecx的值,这三个寄存器虽然都已经定义为输入寄存器,但是在会被改变的寄存器列表中重复说明是为了让gcc编译器更容易进行优化。
接下来从汇编代码开始说明:
第一行,cld命令用来清理标志寄存器的方向位DF
第二行,定义了标号1,执行decl(decrement--减)命令,后缀 l 定义这个命令的操作数为long型(32位),%3代表输入输出寄存器列表里面的第三个寄存器,嵌入式汇编规定把输入输出寄存器统一按顺序编号,顺序是从输出寄存器序列从左到右,从上到下以%0开始,分别记为%0、%1、....%9。因此%3就表示%ecx寄存器,第二行代码的意思就是把ecx寄存器的值减一,结果会影响标志位,如果结果小于0就会把符号标志位SF设置为1,表示结果为负。
第三行,js指令根据SF符号标志位的值来决定要不要跳转,如果ecx的值为负就跳转到标号2处执行。
第四行,如果ecx的值不小于0就执行lodsb指令,该指令将ds:[esi]处的一个字节(也就是一个字符的长度)赋值给al寄存器,并将esi+1。
第五行,scabs是串扫描指令,将al寄存器内的字符和es:[edi]处的值进行比较(es:[edi] - al),并将edi+1,比较结果影响zf和sf标志位。
第六行,根据上一句代码的比较结果来决定要不要执行跳转,如果扫描结果是不相等(即zf的值为0),就跳转到标号3处执行进一步判断是大于还是小于。
第七行,testb指令将%al寄存器的值和自己相与,然后根据结果更新zf,sf标志位的值,就可以根据标志位的值判断al是否为NULL字符,也就是字符串结束字符。
第八行,如果标志位zf不为1,就表示al不是结束字符,而且ds:[esi]处的值和es:[edi]地址处的值相等,就跳转到标号1继续比较。
第九行,标号2,如果上一行判断al是空字符就执行xorl指令将eax和自己进行异或运算,结果就是把eax的值归零。在第二行如果count的值减一后小于0,也会跳转到这一行。
第十行,直接跳转到标号4,返回eax中的值,也就是0,然后结束程序。
第十一行,标号3,第五行扫描结果如果不相等,就执行movl将eax的值设置为1
第十二行,jl是如果小于才跳转,判断第五行扫描结果如果将符号标志位sf设置为1,就表示字符串1的值大于al中的字符串2的值,跳转至标号4,返回1。
第十三行,如果结果是字符串1小于字符串2(es:[edi] - al < 0),上一行jl就不会跳转,就会执行本行的negl,将eax中的1进行求补运算,就得到-1,并返回-1,结束程序。