GCC提供的基本汇编语法形式如下:
__asm__(AssemblerTemplate);
其中,__asm__是内嵌汇编命令的关键字,用来声明内嵌汇编表达式。AssemblerTemplate则是一组插入到C/C++代码中的汇编指令。
例如,下面的代码用于在C语言中插入一条mov寄存器的指令:
__asm__("mov %edx, %eax");
内嵌汇编指令的书写方式与直接在汇编文件中写汇编指令没有区别。基本内嵌汇编支持汇编器的所有指令形式,包括汇编中的伪指令。
在基本内嵌汇编中,我们可以插入一段汇编指令,但是无法让汇编指令与我们原本的C/C++程序代码产生关联。例如,修改或读取C/C++中的变量等。因此,除了支持基本内嵌汇编指令,GCC还支持通过扩展内嵌汇编的方式让汇编指令与C/C++代码进行互操作。
GCC的内嵌汇编语法形式如代码清单1-2所示。
1 __asm__asm-qualifiers(
2 AssemblerTemplate
3 :OutputOperands /* 可选 */
4 :InputOperands /* 可选 */
5 :Clobbers) /* 可选 */
从以上语法形式可以看出,GCC的内嵌汇编主要分为6个部分,下面依次进行解释。
1) __asm__: 同基本内嵌汇编一样,扩展内嵌汇编同样使用__asm__作为关键字,GCC可以识别__asm__或者asm关键字。该标识符标识了内嵌汇编表达式的开始。
2) asm-qualifiers: 该位置可选,一般常用的修饰符是volatile。GCC在优化过程中可能会对内嵌汇编进行修改或者消除。例如,当优化器发现内嵌汇编中某些指令对最后的输出没有影响时,优化器会消除掉这些指令,又或者优化器会对循环中的一些不变量进行外提操作。在某些情况下,编译器的这些优化并不是程序员所期望的行为,因此可以通过volatile关键字来禁止编译器对内嵌汇编的类似优化。
3) AssemblerTemplate: 这个位置是内嵌汇编的主体部分,由一组包含汇编指令的字符串组成。GCC编译器识别其中的占位符,替换为对应的输出操作数、输入操作数等内容,最后将替换好的汇编指令作为汇编器的输入。每条指令最好以\n\t结尾,这样GCC产生的汇编文件的格式比较好看。例如下面的例子:
__asm__ __volatile__("mov %%edx,% %eax":);
该例子同基本内嵌汇编中的例子的内容是一样的,但这里采用的是扩展内嵌汇编的方式,因此有两个不同的地方:一是因为该例子不涉及任何与C/C++交互的地方,所以例子中输出操作数、输入操作数以及破坏描述部分都为空,需要在最后以一个冒号结尾;二是在扩展内嵌汇编中,引用寄存器时,需要在寄存器名称前添加“%%”,这是为了与操作数占位符的“%”进行区分。
4) OutputOperands: 输出操作数,由逗号分隔,可以为空。每个内嵌汇编表达式都可以有0个或多个输出操作数,用来标识在汇编中被修改的C/C++程序变量。
输出操作数的形式如下:
[[asmSymbolicName]]constraint(cvariablename)
要理解asmSymbolicName的含义,需要先理解扩展内嵌汇编中操作数占位符的作用。在扩展内嵌汇编指令中,汇编指令的操作数可以由占位符进行引用,占位符代表了输出操作数以及输入操作数的位置。例如总共有5个操作数(2个输出操作数,3个输入操作数),则占位符%0~%4分别代表了这5个操作数,具体的实现如代码清单1-3所示。
1 int out1,out2;
2 int in1=1,in2=2,in3=3;
3 __asm__ __volatile__(
4 "add %3,%4\n\t"
5 "add %2,%3\n\t"
6 "mov %4,%1\n\t"
7 "mov %3,%0\n\t"
8 :"=r"(out1),"=r"(out2)
9 :"r"(in1),"r"(in2),"r"(in3)
10 :
11);
例子中占位符%0~%4分别指向C代码中out1、out2、in1、in2、in3这5个变量。
虽然数字类型的占位符比较方便,但是如果输出/输入操作数太多,则容易使得数字类型占位符过于混乱。因此,asmSymbolicName提供了一种别名的方式,允许在扩展内嵌汇编中使用别名来操作占位符。上面例子也可以修改为别名的形式,具体实现如代码清单1-4所示。
1 int out1,out2;
2 int in1=1,in2=2,in3=3;
3 __asm__ __volatile__(
4 "add %[in2],%[in3]\n\t"
5 "add %[in1],%[in2]\n\t"
6 "mov %[in3],%[out2]\n\t"
7 "mov %[in2],%[out1]\n\t"
8 :[out1]"=r"(out1),[out2]"=r"(out2)
9 :[in1]"r"(in1),[in2]"r"(in2),[in3]"r"(in3)
10:
11);
constraint表明操作数的约束,即上面例子中out1和out2的“=r”。对输出操作数而言,约束必须以“=”(意思是对当前变量进行写操作)或“+”(意思是对当前变量进行读和写操作)开头。在前缀之后,必须有一个或多个附加约束来描述值所在的位置。常见的约束包括代表寄存器的“r”和代表内存的“m”。上述例子中“=r(out1)”的约束含义是:内嵌汇编指令将会对out1变量进行写操作,并且会将out1与一个寄存器进行关联。GCC内嵌汇编中的约束符还有很多,详细列表可以查看GCC官方手册,此处不再赘述。
(cvariablename)表示该输出操作符所绑定的C/C++程序的变量,这个比较好理解。
最后再看一下来自Linux 0.11中的具体例子,如代码清单1-5所示。
1 inline unsigned long get_fs(){
2 unsigned short_v;
3 __asm__("mov %%fs,%%ax":"=a"(_v):);
4 return_v;
5 }
这个函数的功能是获取当前fs寄存器的值并返回。在函数get_fs()中,输出操作数为变量_v,其形式为“=a(_v)”。这里约束“=a”表明输出操作符与寄存器%ax绑定,因此内嵌汇编的作用就是将寄存器%fs的值存储到变量_v中。
5) InputOperands: 输入操作数,由逗号分隔,可以为空。输入操作数集合标识了哪些C/C++变量是需要在汇编代码中读取使用的。
输入操作数的形式如下:
[[asmSymbolicName]]constraint(cvariablename)
同输出操作数语法形式一致。这里需要单独对输入操作数的constraint进行说明,与输出操作数不同,输入约束字符串不能以“=”或“+”开头,另外,输入约束也可以是数字。这表明指定的输入变量必须与输出约束列表中(从零开始的)索引处的输出变量指向同一个变量。
例如以下例子:
1 __asm__ __volatile__(
2 "add %2,%0"
3 :"=r"(a)
4 :"0"(a),"r"(b)
5 :
6);
在这个例子中,变量a对应的寄存器既要作为输入变量,也要作为输出变量。这里通过“0”约束将输入操作数与输出操作数绑定。
我们最后再看一下Linux 0.11中的具体例子:
1 inline void set_fs(unsigned long val){
2 __asm__("mov %0,%%fs"::"a"((unsigned short)val));
3 }
该函数的作用是将变量val的值存到%fs寄存器中。在对应的内嵌汇编中,输出操作数为空,而输入操作数则为val变量,在汇编指令里通过%0占位符来表示。
6) Clobbers: 破坏描述部分。该位置需要列出除了输出操作数列表中会被修改的值之外,其他会被内嵌汇编修改的寄存器值。破坏描述部分的列表内容是寄存器的名称,要通过引号引起来,如果需要多个寄存器的话,则需要使用逗号进行分隔。这里的作用是通知编译器,说明在内嵌汇编中有哪些寄存器的值会被修改,使得编译器在内嵌汇编语句之前保存对应的寄存器值。
例如以下例子:
1 __asm__ __volatile__(
2 "mov %0, %eax"
3 ::"a"(a):" %eax"
4);
例子中将变量a的值写到 %eax寄存器中,这里 %eax寄存器既非输出操作数,又非输入操作数,因此需要在破坏描述部分进行声明。
除了通用寄存器,clobbers list还有两个特殊的参数有着不同的含义:一个是“cc”,它用来表示内嵌汇编修改了标志寄存器(flags register);另一个是“memory”,它用于通知编译器汇编代码对列表中的项目执行内存读取或写入(例如,访问由输入参数指向的内存)。为了确保内存包含正确的值,GCC可能需要在执行内嵌汇编之前将特定的寄存器值保存到内存中。此外,编译器不会假设在内嵌汇编之前从内存读取的值保持不变,它会根据需要重新加载这些值。“memory clobber”的作用等同于为编译器添加了一个读写内存屏障。