内嵌汇编
参考1、AT&T汇编语言与GCC内嵌汇编简介
2、Professional.Assembly.Language十三章
ARM GCC 内嵌(inline)汇编手册
内嵌汇编语法如下:
__asm__ __volatile__ (
汇编语句模板:
输出部分:
输入部分:
破坏描述部分
);
汇编语句模板由汇编语句序列组成,语句之间使用“;”、“\n”或“\n\t”分开。指令中的操作数可以使用占位符引用C语言变量,操作数占位符最多10个,名称如下:%0,%1…,%9。指令中使用占位符表示的操作数,总被视为long型(4个字节),但对其施加的操作根据指令可以是字或者字节,当把操作数当作字或者字节使用时,默认为低字或者低字节。对字节操作可以显式的指明是低字节还是次字节。方法是在%和序号之间插入一个字母,“b”代表低字节,“h”代表高字节,例如:%h1。
“__asm__” 表示后面的代码为内嵌汇编,“asm”是“__asm__”的别名。
“__volatile__” 表示编译器不要优化代码,后面的指令保留原样,“volatile”是它的别名。括号里面是汇编指令
C语言关键字volatile(注意它是用来修饰变量而不是上面介绍的__volatile__)表明某个变量的值可能在外部被改变,因此对这些变量的存取不能缓存到寄存器,每次使用时需要重新存取。该关键字在多线程环境下经常使用,因为在编写多线程的程序时,同一个变量可能被多个线程修改,而程序通过该变量同步各个线程。
破坏描述部分:内嵌的汇编代码可以直接使用寄存器,而编译器在转换的时候并不去检查内嵌的汇编代码使用了哪些寄存器,因此需要一种机制通知编译器我们使用了哪些寄存器),否则对这些寄存器的使用就有可能导致错误,修改描述部分可以起到这种作用。当然内嵌汇编的输入输出部分指明的寄存器或者指定为“r”,“g”型由编译器去分配的寄存器就不需要在破坏描述部分去描述,因为编译器已经知道了。
破坏描述部分中的memory描述符告知GCC:
1)不要将该段内嵌汇编指令与前面的指令重新排序;也就是在执行内嵌汇编代码之前,它前面的指令都执行完毕
2)不要将变量缓存到寄存器,因为这段代码可能会用到内存变量,而这些内存变量会以不可预知的方式发生改变,因此GCC插入必要的代码先将缓存到寄存器的变量值写回内存,如果后面又访问这些变量,需要重新访问内存。如果汇编指令修改了内存,但是GCC 本身却察觉不到,因为在输出部分没有描述,此时就需要在修改描述部分增加“memory”,告诉GCC 内存已经被修改,GCC 得知这个信息后,就会在这段指令之前,插入必要的指令将前面因为优化Cache 到寄存器中的变量值先写回内存,如果以后又要使用这些变量再重新读取。第一条保证不会因为指令的重新排序将临界区内的代码调到临界区之外(如果临界区内的指令被重排序放到临界区之外,What will happen?),第二条保证在临界区访问的变量的值,肯定是最新的值,而不是缓存在寄存器中的值,否则就会导致奇怪的错误。
1. 使用“r”限制的输入变量,GCC 先分配一个寄存器,然后将值读入寄存器,最后用该寄存器替换占位符;
2. 使用“r”限制的输出变量,GCC会分配一个寄存器,然后用该寄存器替换占位符,但是在使用该寄存器之前并不将变量值先读入寄存器,GCC认为所有输出变量以前的值都没有用处,不读入寄存器(可能是因为AT&T汇编源于CISC架构处理器的汇编语言,在CISC处理器中大部分指令的输入输出明显分开,而不像RISC那样一个操作数既做输入又做输出,例如add r0,r1,r2,r0 和r1 是输入,r2 是输出,输入和输出分开,没有使用输入输出型操作数,这样我们就可以认为r2 对应的操作数原来的值没有用处,也就没有必要先将操作数的值读入r2,因为这是浪费处理器的CPU周期),最后GCC插入代码,将寄存器的值写回变量;
3. 输入变量使用的寄存器在最后一处使用它的指令之后,就可以挪做其他用处,因为已经不再使用。例如上例中的%edx。在执行完addl之后就作为与result对应的寄存器。
在内嵌的汇编指令中可能会直接引用某些寄存器,我们已经知道AT&T 格式的汇编语言中,寄存器名以“%”作为前缀,为了在生成的汇编程序中保留这个“%”号,在asm语句中对寄存器的引用必须用“%%”作为寄存器名称的前缀。原因是“%”在asm 内嵌汇编语句中的作用与“\”在C语言中的作用相同,因此“%%”转换后代表“%”。例(没有使用修改描述符):
int main(void)
{
int input, output,temp;
input = 1;
__asm__ __volatile__ ("movl $0, %%eax;\n\t
movl %%eax, %1;\n\t
movl %2, %%eax;\n\t
movl %%eax, %0;\n\t"
:"=m"(output),"=m"(temp) /* output */
:"r"(input) /* input */
);
return 0;
}
这段代码使用%eax作为临时寄存器,功能相当于C代码:“temp = 0;output=input”,
对应的汇编代码如下:
movl $1,-4(%ebp)
movl -4(%ebp),%eax
#APP
movl $0, %eax;
movl %eax, -12(%ebp);
movl %eax, %eax;
movl %eax, -8(%ebp);
#NO_APP
显然GCC给input分配的寄存器也是%eax,发生了冲突,output的值始终为0,而不是input。
使用破坏描述后的代码:
int main(void)
{
int input, output,temp;
input = 1;
__asm__ __volatile__ ("movl $0, %%eax;\n\t
movl %%eax, %1;\n\t
movl %2, %%eax;\n\t
movl %%eax, %0;\n\t"
:"=m"(output),"=m"(temp) /* output */
:"r"(input) /* input */
:"%eax"); /* 描述符 */
return 0;
}
对应的汇编代码:
movl $1,-4(%ebp)
movl -4(%ebp),%edx
#APP
movl $0, %eax;
movl %eax, -12(%ebp);
movl %edx, %eax;
movl %eax, -8(%ebp);
#NO_APP
通过破坏描述部分,GCC得知%eax 已被使用,因此给input分配了%edx。在使用内嵌汇编时请记住一点:尽量告诉GCC尽可能多的信息,以防出错。
“&”限制符
限制符“&”在内核中使用的比较多,它表示输入和输出操作数不能使用相同的寄存器,这样可以避免很多错误。举一个例子,下面代码的作用是将函数foo的返回值存入变量ret中:
__asm__ ( “call foo;movl %%edx,%1”, :”=a”(ret) : ”r”(bar) );
我们知道函数的int型返回值存放在%eax中,但是gcc编译的结果是输入和输出同时使用了寄存器%eax,如下:
movl bar, %eax
#APP
call foo
movl %edx,%eax
#NO_APP
movl %eax, ret
结果显然不对,原因是GCC并不知道%eax中的值是我们所要的。避免这种情况的方法是使用“&”限定符,这样bar就不会再使用%eax寄存器,因为已被ret指定使用。
__asm__ ( “call foo;movl %%edx,%1”,:”=&a”(ret) : ”r”(bar) );