购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

1.4.1 内嵌汇编

GCC提供的基本汇编语法形式如下:

__asm__(AssemblerTemplate);

其中,__asm__是内嵌汇编命令的关键字,用来声明内嵌汇编表达式。AssemblerTemplate则是一组插入到C/C++代码中的汇编指令。

例如,下面的代码用于在C语言中插入一条mov寄存器的指令:

__asm__("mov %edx, %eax");

内嵌汇编指令的书写方式与直接在汇编文件中写汇编指令没有区别。基本内嵌汇编支持汇编器的所有指令形式,包括汇编中的伪指令。

在基本内嵌汇编中,我们可以插入一段汇编指令,但是无法让汇编指令与我们原本的C/C++程序代码产生关联。例如,修改或读取C/C++中的变量等。因此,除了支持基本内嵌汇编指令,GCC还支持通过扩展内嵌汇编的方式让汇编指令与C/C++代码进行互操作。

GCC的内嵌汇编语法形式如代码清单1-2所示。

代码清单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-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-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-5 Linux 0.11中的真实示例

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”的作用等同于为编译器添加了一个读写内存屏障。 8jN+LkXJYyfitLFSsYhA6hbULOR9CyLvVQMntdB+iSKspDRAX4rT4QYAOQ2xatYz

点击中间区域
呼出菜单
上一章
目录
下一章
×