今晨的日出
早晨学习操作系统,看到系统调用实现时,遇到了C内嵌汇编,所以补了下相关知识。
__asm__
补充资料:https://dirtysalt.github.io/html/gcc-asm.html
在内嵌汇编中,可以将C语言表达式指定为汇编指令的操作数,而且不用去管如何将C语言表达式的值读入哪个寄存器,以及如何将计算结果写回C 变量,你只要告诉程序中C语言表达式与汇编指令操作数之间的对应关系即可,GCC会自动插入代码完成必要的操作。
完整的内嵌汇编格式:__asm__ __volatile__("Instruction List" : Output : Input : Clobber/Modify);
__asm__ 是GCC关键字asm的宏定义,asm用于声明这行代码是一个内嵌汇编表达式,任何内嵌的汇编表达式都以此关键字作为开头。
一个最简单的内嵌汇编:__asm__("nop");
括号里的是汇编指令,表示运行空指令,一个机器周期。Instruction List 是汇编指令序列,它可以是空的。可以有多条汇编指令,需要将所有指令放在多对引号中,两条指令必须用换行或分号分开。
1 | int printk(const char *fmt, ...) |
可以看上面printk()
的例子,首先push %fs
保存这个指向用户段的寄存器,在最后pop %fs
将其恢复,printk()
的核心是调用tty_write()
。
Output Input 用来指定当前内联汇编语句的输出与输入,格式为形如“constraint”(variable)的列表(逗号分隔),constraint是限制字符,下面表中列出几个常用限制字符作用,更多用法见:GCC内嵌汇编一些限制字符串。
限制字符 | 作用 |
---|---|
a/b/c/d/s/d | 将输入变量放入eax/ebx/ecx/edx/esi/edi |
q | 将输入变量放入eax,ebx,ecx,edx中的一个 |
r | 输入变量放入通用寄存器,也就是eax,ebx,ecx,edx,esi,edi中的一个 |
=/+ | 操作数在指令中是只写/读写类型的(输出操作数) |
__volatile__
__volatile__ 是GCC关键字volatile的宏定义,在内联汇编中,它是可选的。使用它会向GCC声明不允许对该内联汇编优化,否则当使用了优化选项(-O)进行编译时,GCC将会根据自己的判断决定是否将这个内联汇编表达式中的指令优化掉。
编译器优化常用的方法有:将内存变量缓存到寄存器;调整指令顺序充分利用CPU指令流水线,常见的是重新排序读写指令。对常规内存进行优化的时候,这些优化是透明的,而且效率很好。由编译器优化或者硬件重新排序引起的问题的解决办法是在从硬件(或者其他处理器)的角度看必须以特定顺序执行的操作之间设置内存屏障(memory barrier),linux 提供了一个宏,barrier()
,解决编译器的执行顺序问题,但对硬件无效。
有一种硬件级别的优化:内存访问速度远不及CPU处理速度,为提高机器整体性能,在硬件上引入硬件高速缓存Cache,加速对内存的访问。在现代CPU中指令的执行并不一定严格按照顺序执行,没有相关性的指令可以乱序执行,以充分利用CPU的指令流水线,提高执行速度。
Clobber/Modify
有时候,你想通知GCC当前内联汇编语句可能会对某些寄存器或内存进行修改,希望GCC在编译时能够将这一点考虑进去。那么你就可以在Clobber/Modify域声明这些寄存器或内存。这种情况一般发生在一个寄存器出现在”Instruction List”,但却不是由Input/Output操作表达式所指定的,也不是在一些Input/Output操作表达式使用”r”约束时由GCC 为其选择的,同时此寄存器被”Instruction List”中的指令修改,而这个寄存器只是供当前内联汇编临时使用的情况。
例如:__asm__ ("mov R0, #0x34" : : : "R0");
寄存器R0出现在”Instruction List中”,并且被mov指令修改,但却未被任何Input/Output操作表达式指定,所以你需要在Clobber/Modify域指定”R0”,以让GCC知道这一点。
因为你在Input/Output操作表达式所指定的寄存器,或当你为一些Input/Output操作表达式使用”r”约束,让GCC为你选择一个寄存器时,GCC对这些寄存器是非常清楚的——它知道这些寄存器是被修改的,你根本不需要在Clobber/Modify域再声明它们。但除此之外, GCC对剩下的寄存器中哪些会被当前的内联汇编修改一无所知。所以如果你真的在当前内联汇编指令中修改了它们,那么就最好在Clobber/Modify 中声明它们,让GCC针对这些寄存器做相应的处理。否则有可能会造成寄存器的不一致,从而造成程序执行错误。
如果一个内联汇编语句的Clobber/Modify域存在”memory”,那么GCC会保证在此内联汇编之前,如果某个内存的内容被装入了寄存器,那么在这个内联汇编之后,如果需要使用这个内存处的内容,就会直接到这个内存处重新读取,而不是使用被存放在寄存器中的拷贝。因为这个 时候寄存器中的拷贝已经很可能和内存处的内容不一致了。
这只是使用”memory”时,GCC会保证做到的一点,但这并不是全部。因为使用”memory”是向GCC声明内存发生了变化,而内存发生变化带来的影响并不止这一点。
1 | int main(int __argc, char* __argv[]) |
上面例子中,如果没有那条内联汇编语句,那个if语句的判断条件就完全是一句废话。GCC在优化时会意识到这一点,而直接只生成return 5的汇编代码,而不会再生成if语句的相关代码,而不会生成return (*__p)的相关代码。但你加上了这条内联汇编语句,它除了声明内存变化之外,什么都没有做。但GCC此时就不能简单的认为它不需要判断都知道 (*__p)一定与9999相等,它只有老老实实生成这条if语句的汇编代码,一起相关的两个return语句相关代码。
linux内核中内存屏障也是基于它实现,#define barrier() _asm__volatile_("" : : : "memory")
,主要是保证程序的执行遵循顺序一致性。