1.2.1 GCC内联汇编
KVM模块中切入Guest模式的代码使用GCC的内联汇编编写,为了理解这段代码,我们需要简要地介绍一下这段内联汇编涉及的语法,其基本语法模板如下:
asm volatile ( assembler template : output operands /* optional */ : input operands /* optional */ : list of clobbered registers /* optional */ );
(1)关键字asm和volatile
asm为GCC关键字,表示接下来要嵌入汇编代码,如果asm与程序中其他命名冲突,可以使用__asm__。
volatile为可选关键字,表示不需要GCC对下面的汇编代码做任何优化,类似的,GCC也支持__volatile__。
(2)汇编指令(assembler template)
这部分即要嵌入的汇编指令,由于是在C语言中内联汇编代码,因此须用双引号将命令括起来。如果内嵌多行汇编指令,则每条指令占用1行,每行指令使用双引号括起来,以后缀\n\t结尾,其中\n为newline的缩写,\t为tab的缩写。由于GCC将每条指令以字符串的形式传递给汇编器AS,所以我们使用\n\t分隔符来分隔每一条指令,示例代码如下:
__asm__ ("movl %eax, %ebx \n\t" "movl $56, %esi \n\t" "movl %ecx, $label(%edx,%ebx,$4) \n\t" "movb %ah, (%ebx) \n\t");
当使用扩展模式,即包含output、input和clobber list部分时,汇编指令中需要使用两个“%”来引用寄存器,比如%%rax;使用一个“%”来引用输入、输出操作数,比如%1,以便帮助GCC区分寄存器和由C语言提供的操作数。
(3)输出操作数(output operands)
内联汇编有零个或多个输出操作数,用来指示内联汇编指令修改了C代码中的变量。如果有多个输出参数,则需要对每个输出参数进行分隔。每个输出操作数的格式为:
[[asmSymbolicName]] constraint (cvariablename)
我们可以为输出操作数指定一个名字asmSymbolicName,汇编指令中可以使用这个名字引用输出操作数。
除了使用名字引用操作数外,还可以使用序号引用操作数。比如输出操作数有两个,那么可以用%0引用第1个输出操作数,%1引用第2个操作数,以此类推。
输出操作数的约束部分必须以“=”或者“+”作为前缀,“=”表示只写,“+”表示读写。在前缀之后,就可以是各种约束了,比如“=a”表示先将结果输出至rax/eax寄存器,然后再由rax/eax寄存器更新相应的输出变量。
cvariablename为代码中的C变量名字,需要使用括号括起来。
(4)输入操作数(input operands)
内联汇编可以有零个或多个输入操作数,输入操作数来自C代码中的变量或者表达式,作为汇编指令的输入,每个输入操作数的格式如下:
[[asmSymbolicName]] constraint (cexpression)
同输出操作数相同,也可以为每个输入操作数指定名字asmSymbolicName,汇编指令中可以使用这个名字引用输入操作数。
除了使用名字引用输入操作数外,还可以使用序号引用输入操作数。输入操作数的序号以最后一个输出操作数的序号加1开始,比如输出操作数有两个,输入操作数有3个,那么需要使用%2引用第1个输入操作数,%3引用第2个输入操作数,以此类推。
除了不必以“=”或者“+”前缀开头外,输入操作数的前缀与输出操作数基本相同。除了寄存器约束外,在后面的代码中我们还会看到“i”这个约束,表示这个输入操作数是个立即数(immediate integer)。
cexpression为代码中的C变量或者表达式,需要使用括号括起来。
(5)clobber list
某些汇编指令执行后会有一些副作用,可能会隐性地影响某些寄存器或者内存的值,如果被影响的寄存器或者内存并没有在输入、输出操作数中列出来,那么需要将这些寄存器或者内存列入clobber list。通过这种方式,内联汇编告知GCC,需要GCC“照顾”好这些被影响的寄存器或者内存,比如必要时需要在执行内联汇编指令前保存好寄存器,而在执行内联汇编指令后恢复寄存器的值。
接下来我们来看一个具体的例子。这个例子是一个加法运算,一个加数是val,值为100,另外一个加数是一个立即数400,计算结果保存到变量sum中:
01 int val = 100, sum = 0; 02 03 asm ("movl %1, %%rax; \n\t" 04 "movl %c[addend], %%rbx; \n\t" 05 "addl %%rbx, %%rax; \n\t" 06 “movl %%rax, %0; \n\t” 07 08 : “=”(sum) 09 : (c)(val), [addend]”i”(400) 10 : “rbx” 11 );
我们先来看第3行的汇编指令。因为存在寄存器引用和通过序号引用的操作数,所以使用两个“%”引用寄存器。%1引用的是输入操作数val,其中c表示使用rcx寄存器保存val,也就是说在执行这条汇编指令前,首先将val的值赋值到rcx寄存器中,然后汇编指令再将rcx寄存器的值赋值到rax寄存器中。
第4行的汇编指令引用的addend是第2个输入操作数的符号名字,因为这是一个立即数,所以这个变量前面使用了c修饰符。这是GCC的一个语法,表示后面是个立即数。
第5条指令求rbx寄存器和rax寄存器的和,并将结果保存到rax寄存器中。
第6条指令中的%0引用的是输出操作数sum,这是C代码中的变量,因为sum是只写的输出操作数,所以使用约束“=”。所以第6行的汇编指令是将计算的结果存储到变量sum中。
从这段代码中我们看到,在汇编代码中使用了rbx寄存器,而rbx寄存器没有出现在输出、输入操作数中,所以内联汇编需要把rbx寄存器列入clobber list中,见第10行代码,告诉GCC汇编指令污染了rbx寄存器,如果有必要,则需要在执行内联汇编指令前自行保存rbx寄存器,执行内联汇编指令后再自行恢复rbx寄存器。