C语言内联汇编

通过扩展asm语法,能够读写通过汇编器读写C语言变量及从汇编代码跳转到C标签。扩展asm语法使用”:”在汇编器模板后分割操作数,有两种语法结构:

1
2
3
4
asm asm-qualifiers  ( AssemblerTemplate 
: OutputOperands
[ : InputOperands
[ : Clobbers ] ])
1
2
3
4
5
asm asm-qualifiers ( AssemblerTemplate 
:
: InputOperands
: Clobbers
: GotoLabels)

后者包含goto(前者不包含),asm是GNU扩展属性。当使用-asni和-std选项编译时,需要使用asm替换asm。

修饰语

  1. volatile
    扩展asm语句的典型用法,用于操作输入值产生输出值。不过该修饰语存在副作用,volatile会禁用某些优化。
  2. inline
    使用inline修饰语后,asm语句所占空间会尽量减小到最小值。
  3. goto
    这个修饰语告知编译器,asm语句可能跳转到GotoLabels中的某一个标签。

    参数

  4. AssemblerTemplate
    汇编模板是固定文本及代表输入、输出和goto参数的组合
  5. OutputOperands
    由’,’分割的C语言变量列表,这些变量由汇编模板操作修改,可以为空
  6. InputOperands
    由“,”分割的C语言表达式列表,这些表达式用于汇编模板读取,可以为空
  7. Clobbers
    由“,”分割的寄存器或者其他由汇编模板改变的值,这些寄存器或者值在outputs所列之外。
  8. GotoLabels
    当使用goto形式的汇编语句时,这个段包含所有从汇编模板中跳转的C标签。asm语句不会跳转到其他汇编语句,只会跳转到GotoLabels,gcc的优化器也不感知其他跳转,因此它在优化时也无法考虑到其他跳转。

input+output+goto操作数总量限制在30个以内。

注意!!!

asm语句允许在C代码中直接使用汇编指令。这能够帮助人们最大化优化时间敏感性代码及使用C程序无法使用的汇编指令。不过需注意扩展asm语句必须在一个函数中。只有基础asm可以在函数之外。使用nake属性申明的函数也需要基础asm。使用asm的场景很多且各不一样,总而言之可以认为asm语句是一系列底层指令,它用于将输入参数转换为输出参数。例如:

1
2
3
4
5
6
7
8
9
int src = 1;
int dst;

asm("mov %1, %0\n\t"
"add $1,%0"
: "=r" (dst)
: "r"(src));

printf("%d\n", dst);

该代码将src拷贝到dst,然后将dst加1

volatile

gcc优化器会对内联汇编代码做优化:1. 当它认为无须输出变量时会丢弃;2.如果它该段代码一直返回相同的结果,它会将代码移动到loops外面。使用volatile修饰符能够禁用这类优化。无输出操作数,包括asm goto语句,为缺省volatile的。

以下i386代码展示一种无须volatile修饰的例子。若其执行断言检查,这段代码使用asm语句来执行确认。否则dwRes不被任何代码引用。结果是,优化器会丢弃这段asm代码,最终导致整个DoCheck程序被删除。在没有必要的情况下通过省略volitile修饰符,可以允许优化器尽可能产生最有效的代码。

1
2
3
4
5
6
7
8
9
10
11
12
void DoCheck(uint32_t dwSomeValue)
{
uint32_t dwRes;

// Assumes dwSomeValue is not zero.
asm ("bsfl %1,%0"
: "=r" (dwRes)
: "r" (dwSomeValue)
: "cc");

assert(dwRes > 3);
}

下面这个例子是优化器认为函数在执行过程中,输入永远不会发生变化,如此可以将asm移动到loop外面,以取得更有效的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void do_print(uint32_t dwSomeValue)
{
uint32_t dwRes;

for (uint32_t x=0; x < 5; x++)
{
// Assumes dwSomeValue is not zero.
asm ("bsfl %1,%0"
: "=r" (dwRes)
: "r" (dwSomeValue)
: "cc");

printf("%u: %u %u\n", x, dwSomeValue, dwRes);
}
}

以下例子展现一种需要使用volatile修饰符的例子。当使用x86 rdtsc指令时,若不使用volatile修饰符,优化器将认为两个asm块放回相同的内容,从而第二个调用被优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
uint64_t msr;

asm volatile ( "rdtsc\n\t" // Returns the time in EDX:EAX.
"shl $32, %%rdx\n\t" // Shift the upper bits left.
"or %%rdx, %0" // 'Or' in the lower bits.
: "=a" (msr)
:
: "rdx");

printf("msr: %llx\n", msr);

// Do other work...

// Reprint the timestamp
asm volatile ( "rdtsc\n\t" // Returns the time in EDX:EAX.
"shl $32, %%rdx\n\t" // Shift the upper bits left.
"or %%rdx, %0" // 'Or' in the lower bits.
: "=a" (msr)
:
: "rdx");

printf("msr: %llx\n", msr);

gcc优化器不会像对待上一个例子的非volatile代码那样。它不会认为前一个调用仍然有效从而忽略或者移除到loop外。

注意到即使在加了volatile的情况下编译器也可能移动代码,包括跨越跳转指令。比如,对于很多目标系统存在一个系统寄存器用于控制浮点运算的舍入模式。将其设置为volatile asm语句,比如如下的PowerPC上的代码例子,可能工作不正常。

1
2
asm volatile("mtfsf 255, %0" : : "f" (fpenv));
sum = x + y;

编译器可能会将后面的代码优化到前面去。为让其正确工作,必须在后面人工添加依赖关系,如下所示。

1
2
asm volatile ("mtfsf 255,%1" : "=X" (sum) : "f" (fpenv));
sum = x + y;

在多种场景下,gcc在优化代码时可能会复制一份代码。如果汇编代码定义了符号或者标签,可能会导致编译错误。对于这种场景,使用”%=”能避免这种问题。

汇编模板

一个汇编模板是包含汇编指令的一组字符串。编译器会将模板中的标记替换为所代表的输入、输出和goto标签,然后输出到汇编器的结果代码中。这些字符串包含任意能被汇编器识别的指令,包括指示。gcc自身并不解析这些汇编代码,也不知道它的真实含义以及作为输入是否有效,它只会简单地计算其size。

汇编模板也支持在一个模板中包含多句汇编语句,这些汇编语句也是由正常汇编语句的相应字符分割。一个组合一般都是通过一个新行来开始,加上一个制表符来到达新的指令(‘\n\t’)。某些汇编器也支持使用”;”来分行。不过也要注意到某些汇编器使用”;”来开启一个评论。

不要期望一段汇编代码在编译后也能完美保序,即使使用了volatile修饰符。如果多个指令需要在输出保序,最好将它们放在单一的多指令asm语句中。

不使用输入/输出操作数而从C程序访问数据(比如直接在汇编模板中使用全局符号)可能无法如预期工作。类似的,直接在汇编模板中调用函数需要深入理解目标汇编器和ABI。

由于GCC不会解析汇编器模板,所引用的任意符号也对其不可见。这会导致GCC丢弃这些被认为没有被引用的符号,除非他们列作输入、输出或goto操作数。

特殊格式化字符串

  1. ‘%%’ 在汇编代码中输出单个‘%’
  2. ‘%=’ 在整个编译中,为每个汇编语句的实例都输出一个唯一数。这一选项在创建本地标签以及在一个模板中多次引用它们时能够生成多个汇编指令。
  3. ‘%{‘ ‘%}’ ‘%|’ 在汇编代码中输出’{‘ ‘}’ ‘|’ 字符。当不可避免时,这些字符串对于表示多个汇编语言时,有特殊含义。

在asm模板中有多个汇编语言

对于某些系统比如说x86,GCC支持多种汇编语言。-masm选项用于控制gcc使用它的默认内联汇编器。需要重点理解的是,在使用某一种语法编译的可能正确工作,但切换到其他语法可能失败。

如果汇编代码需要支持多种汇编语义(比如,如果写需要支持多种编译选项的公共头文件),使用如下形式的结构:

1
{dialect0 | dialect1 | dialect2 ...}

例如,如果x86编译器支持两种语法(‘att’, ‘intel’),汇编模板书写如下:

1
“bt{1 %[Offset],%[Base] | %[Base],%[Offset]}; jc %12

等同于

1
2
"btl %[Offset],%[Base] ; jc %l2"   /* att dialect */
"bt %[Base],%[Offset]; jc %l2" /* intel dialect */

使用相同的编译器,这种代码

1
"xchg{l}\t{%%}ebx, %1"

管理两者中任意一个

1
2
"xchgl\t%%ebx, %1"                 /* att dialect */
"xchg\tebx, %1" /* intel dialect */

但不支持嵌套语义选项。

输出操作数

asm语义可以有0个或者多个指示为C语言变量的输出操作数,这些操作数被汇编代码修改。

在如下的i386例子中, old(由被模板串中的%0表示)和*Base(作为%1)是输出,Offset(%2)是输入:

1
2
3
4
5
6
7
8
9
bool old;

__asm__ ("btsl %2,%1\n\t"
"sbb %0,%0"
: "=r"(old), "+rm"(*Base)
: "Ir"(Offset)
: "cc");

return old;

操作数由”,”分割,每个操作数如下

1
[[asmSymbolicName]] constraint (cvariablename)

asmSymbolicName

指定操作数的符号名称。由汇编模板引用的名称被方括号包含。名称的范围为包含这个定义的汇编语句。任何有效的C变量名称都能被接受,包括已经在已由周边代码定义的名称。同一汇编语句中不能有两个使用相同符号名称的操作数。

当不使用asmSymbolicName时,汇编模板中的操作数列表使用0位置操作数。比如有3个输出操作数,使用“%0”表示第1个,”%1”表示第2个,”%2”表示第3个。

constraint

为表示操作数位置的限定。

输入限定可能不会起始于”=”或者”+”。当你列出多个可能的位置时(如”irm”),编译器会基于上下文选择最有效的一个。如果你必须使用指定的寄存器,但是机器限定并不提供这种选择特定寄存器的完全控制,本地寄存器时一个解决方案。

输入限定也可以是数字(比如说, “0”)。这指示在输出的限定列表中输入和输出位于相同的索引。对于输出操作数,当使用asmSymbolicName语法时,可能要使用名称来替代数字。

cexpression

这作为输入传递给汇编语句的C变量和表达式。这种括号是语法表虚的一部分。当编译器选择寄存器来代表输入操作数时,它并不使用任何的修饰寄存器。

如果没有输出操作数但由输入操作数,在输出操作数直接放上两个”:”即可。

1
2
3
__asm__ ("some instructions"
: /* No outputs. */
: "r" (Offset / 8));

警告: 不要修改只有输入操作数的类容(除非输入也是输出)。编译器假定在asm语句退出时这些操作数的值同它之前执行的相同。不能通过修饰来告知编译器这些输入已经被改变了。一个普遍的work-around方案是将输入变量改变为永远不会使用的输出变量。然而,如果这些代码紧跟着无用的输出操作数,GCC优化器可能认为这些是无用的因而丢弃这些asm语句。

asm支持在操作数上的修改(‘%k2’代替’%2’)。一般来说这些修饰符是硬件独立的。

在此示例中,使用虚构的组合指令,输入操作数1的约束“0”表示它必须占用与输出操作数0相同的位置。只有输入操作数可以在约束中使用数字,并且它们必须各自引用输出操作数。 约束中只有一个数字(或符号汇编程序名称)可以保证一个操作数与另一个操作数位于同一位置。 foo是两个操作数的值这一事实并不足以保证它们在生成的汇编代码中处于同一位置。

1
2
3
4
5
6
7
8
9

asm ("combine %2, %0"
: "=r" (foo)
: "0" (foo), "g" (bar));
Here is an example using symbolic names.

asm ("cmoveq %1, %2, %[result]"
: [result] "=r"(result)
: "r" (test), "r" (new), "[result]" (old));

标志输出操作数

某些汇编代码会对标志寄存器产生影响。通常,该寄存器的类容要么不被asm语句修改,要么asm语句被认为破坏了其中类容。

输入操作数

重写和擦除寄存器

虽然编译器意识到输出操作数条目的修改,但内联汇编代码不仅仅修改输出。比如,计算可能需要额外寄存器,或者作为某些特定汇编指令的副作用,处理器会重写寄存器。为了通知编译器这些改变,在重写列表中列出它们。重写列表项可以使寄存器名称,也可以是特殊clobbers。每个重写列表项是一个用逗号分隔的由双引号括起来的字符串常量。

重写描述不能同输入输出操作数重叠。比如,不能使用操作数描述一个在重写列表中列出的寄存器类。声明在特定寄存器的变量及用作汇编语句的输入输出操作数不能是重写描述语句的一部分。特别地,我们无法指定输入操作数被修改同时它也是输出操作数。

当编译器选择寄存器来代表输入和输出操作数时,它代表不使用任何重写寄存器。结果是,重写寄存器可用于任何汇编代码。

另一个限制是clobber列表不应该包含堆栈指针寄存器。这是因为编译器要求堆栈指针的值在asm语句之后与在语句入口时相同。但是,以前版本的GCC没有强制执行这个规则,允许堆栈指针出现在列表中,语义不清楚。这种行为是不赞成的,在GCC的未来版本中,列出堆栈指针可能会成为一个错误。

如下是VAX上的一个真实的重写寄存器用例:

1
2
3
4
asm volatile ("movc3 %0, %1, %2"
: /* No outputs. */
: "g" (from), "g" (to), "g" (count)
: "r0", "r1", "r2", "r3", "r4", "r5", "memory");

也有两个特殊的重写参数:

“cc”

cc重写表示汇编代码可能修改标志寄存器。在某些机器上,gcc将条件代码表述为特定硬件寄存器,”cc”用于这个寄存器的命名。对于其他机器,条件代码处理不相同,指定”cc”无作用。但是这在所有目标机器上都是有效的。

“memory”

“memory”重写标记会告知编译器汇编代码会执行除输入输出操作数之外的内存读写(例如,通过输入参数访问所指向内存)。为保证内存包含正确的变量,GCC可能需要在执行汇编语句前刷写特定的寄存器到内存中。进一步,编译器不会假定任何在asm之前从内存中读取的值在asm语句之后都不会被改变;它会按需重新加载。使用”memory”重写标记有效告知编译器做一个读写内存barrier。

注意到clobber不会阻止处理器在asm语句之后做预测读

操作符和修饰符

操作符 语义
r 通用寄存器
m 有效的内存地址
I 数据处理指令中的立即数
x 被修饰的操作数只能作为输出
修饰符 语义
被修饰的操作符是只读的
= 被修饰的操作符只写
+ 被修饰的操作符可读可写
& 被修饰的操作符只能作为输出