在Linux内核代码中有一部分是使用汇编语言编写的,尤其是与特定体系结构相关的代码和一些对性能影响很大的代码都是使用汇编语言进行编写的,那么GCC为了可以在C语言中来编写汇编代码,提供了内联汇编的功能,可以在C代码中直接内嵌汇编语言,大大方便了程序设计。
__asm__ [__volatiate__](
汇编语句模板
:输出部分
:输入部分
:破坏描述部分)
主要分为几个部分,gcc内核汇编前缀,汇编语句模板,输出部分,输入部分,破坏描述部分,每一个部分是用”:”分割开汇编语句模板必不可少,其他三部分可选,如果使用了后面的部分,而前面部分为空,也需要用”:”格开,相应部分内容为空。
gcc内核汇编前缀: 这个部分有一个固定前缀asm 还有可选的前缀volatiate用于控制生成的汇编代码
汇编语句模板: 这就是我们要执行的汇编指令,可能会略有一点点不同,后面会详细介绍
输出部分: 内联汇编输出的内容 asm(“movl %%cr0,%0”:”=a”(cr0)); “=a”(cr0)就是输出部分,其含义是把cr0寄存器的值赋值给cr0变量
输入部分: 内联汇编输入的内容 asm volatile(“movq %0,%%rbx”::”a”(var)); “a”(var)); “a”(var)是输入部分,其含义是把var变量的值赋值给rbx寄存器
破坏描述部分: 这个部分需要列出汇编语句模板中被修改的寄存器列表,当我们在汇编语句模板中对寄存器做了修改需要在这里列出asm volatile(“movq %0,%%rbx”::”a”(var):”bx”); “bx”是破坏描述部分,因为汇编语句模板中对rbx寄存器进行了修改。具体细节后面还会再介绍
注意: 对于输入和输出部分中的”=a”,”a”这些前缀来说不影响最终的结果,这些前缀改变了中间的过程,文章的下面会逐一介绍其含义。
一个简单的例子:
void swap()
{
int a = 4;
int b = 5;
__asm__(
"movl %0,%%eax \n\t"
"movl %%eax,%1"
:"=m" (a)
:"m" (b)
);
printf("a=%d,b=%d\n",a,b);
}
一个简单的内联汇编的例子,从中你会发现有很多元素都是我们不熟知的,寄存器使用了%%前缀,还有%0(不是立即数,立即数仍然是使用$开头)%1,还有”=m”之类的,总而言之内联汇编的语法还是很晦涩难懂的。本文试图根据自己的理解想把内联汇编的语法解释清楚,下面从这个列子着手一步步分析。
要使用gcc内联汇编需要使用asm作为前缀,其后可以接一个volatite,这个关键字是用来告诉gcc编译器不要对汇编语句模板部分的指令做变动进行优化,因为这样可能会带来一些问题。不加volatile的情况下,汇编语句模板部分是什么指令就是什么指令。所以这个部分没有什么难度的,比较固定没有可讨论的地方。
这个部分就是我们通常意义上的汇编指令部分,在这里你可以使用任何汇编指令,语法遵循AT&T的语法格式,但是需要注意几点第一就是所有寄存器都使用%%作为前缀,第二在这个部分新增了%0~%9的占位符来表示用户填充的数据。那么占位符,占的是什么位呢,谁来填充,怎么填充?%0~%9的占位符会用输出部分和输入部分指定的寄存器或变量按照出现的顺序依次填充。如果不够填充则会出现编译错误的情况。
invalid 'asm': operand number out of range
例如下面的汇编代码:
__asm__(
"movl %0,%%eax \n\t"
"movl %%eax,%1"
:"=m" (a)
:"m" (b)
);
其中%0会被变量a的内存地址替换掉,%1会被变量b的内存地址替换掉。除了这两点外,还需要注意一点的就是多条指令如何分割,在这里需要使用\n\t进行分割,每条语句使用冒号包裹起来。有的文档提到还可以使用分号来分割指令但是经过我测试发现其实并不可以,使用分号分割指令编译是可以通过的,但是生成的汇编代码中会保留分号导致分号后面的代码都被注释了(汇编语言中分号代表注释)linux内核中使用内联汇编也是使用\n\t来进行分割的,所以这里我只推荐\n\t来进行分割多条指令。
输出部分是用于控制汇编语句中的输出,将寄存器的值输出到C语言中的变量中去。
__asm__("movq %%rcx,%0":"=a"(cr0));
这句汇编代码的含义则是把寄存器rcx的值赋给C语言中的变量cr0,那么”=a”的前缀是啥意思呢,a代表的就是rax/eax/ax/ah这类寄存器,会根据目标变量的大小选择对应的寄存器,这里我是64位的平台,并且rcx寄存器也是64位的,所以默认是将rax的值赋值给cr0。那么=号是啥意思呢? 这是输出部分必须要加的前缀,表示这个部分是输出,对应的变量值是只写的。那么在这里首先movq %%rcx,%0 会将rcx寄存器的值赋值给rax寄存器,然后再将rax寄存器的值赋值给C语言中的cr0变量,如果把=a换成=m,则意思变为将rcx寄存器的值赋值给c变量中的cr0变量,最终的结果都是一样,只不过中间过程不同而已,像这样的前缀类型还有很多,详细的见附录A,如果不清楚前缀的作用可以试图使用gcc -S 进行编译,然后查看编译后的汇编代码做了哪些改动,从而可以确定前缀的含义。对于+号这一前缀来说也有一些其它的前缀,这些前缀有的只能用于输出部分,有的只能用于输入部分,例如+号就只能用于输出部分,详细的见附录B。这里再解释一个+号的含义,+号表示目标是可读也可写的,如果把=a换成+a那么含义就变成了先把C中变量cr0的值赋值给rax寄存器,然后再把rcx寄存器的值赋值给rax寄存器,最后再把rax寄存器的值赋值给变量cr0。这里多出了一步,先把cr0变量的值赋值给rax寄存器。那么这就是+号的含义,显然在这段汇编中使用+号是没有意义的,因为rax寄存器紧跟着就被rcx寄存器的值覆盖了。
输入部分是用于将C中的变量值作为汇编语句中的输入,例如下面这个例子
__asm__ __volatile__("movq %0,%%rbx"::"a"(var));
这里是将C语言中变量var的值赋值给rbx寄存器,至于前缀”a”则表示,会先将变量var的值赋值给rax寄存器,然后再把rax寄存器的值赋值给rbx寄存器,如果把这里换成m则表示直接将var内存地址处的值赋值给rbx寄存器。前缀类型请参考附录A。
这个部分我在前面也简单描述了一下,这个部分需要将修改的寄存器列表放在这里,但是你也可以不放,但是这样可能会造成意想不到的结果,因为寄存器是全局唯一的,某一时刻一个进程使用了寄存器ecx,并存入10,然后你执行你的程序修改了ecx寄存器的值,这将导致其他的程序出现问题,那么如果你将ecx加入破坏描述部分,gcc会在使用ecx寄存器前先push入栈,等使用完ecx后再pop回去。这就保证了ecx寄存器在使用过程中没有被修改。
对于内联汇编来说还有很多我没有介绍到的东西,这里仅仅列出一些常用的,助于我们阅读别人的内联汇编代码,要想深入理解内联汇编,请不要只看文章试图使用gcc -S生成汇编代码测试不同前缀,不同的写法,产生的汇编代码有何异同。这样才是学习内联汇编的王道。
修饰符 I/O 意义
* = O 表示此Output操作表达式是只写的
* + O 表示此Output操作表达式是可读可写的
* & O 表示此Output操作表达式独占为其指定的寄存器
* % I 表示此Input操作表达式中的C/C++表达式可以与下一个Input操作表达式中的C/C++表达式互换
注意:I/O 表示用于输入部分还是输出部分,O表示只能用户输出,I表示只能用于用户输入