C内联汇编

[toc]


GCC-Inline-Assembly-HOWTO

GCC 汇编语法

GCC采用AT&T的汇编语法,这和intel汇编语法有一些不同:

  • 操作数的顺序。intel语法通常是op dst src,在AT&T语法中则是op src dst
  • 寄存器命名时需要加上%作为前缀
  • 立即数的表示。AT&T立即数以$作为前缀,对于字符串常量而言也需要加上该前缀。intel语法中使用h后缀表示一个十六进制数,AT&T使用前缀0x表示。因而当表示一个十六进制的立即数时,则写为$0x123的格式
  • 操作符的大小。AT&T中使用操作符后缀来确定该被作用的操作数的长度,’b’, ‘w’, ‘l’分别表示byte(8-bit), word(16-bit), long(32-bit);在intel汇编中则是通过ptr作用在操作数上确定对应的长度
  • 地址操作数。在间接寻址时,intel使用[base + index*scale + disp]的语法,在AT&T中则使用disp(base, index, scale)的语法。需要注意,当disp/scale中出现常量时,不需要$前缀。

基础语法

1
asm("assembly code");

如果asm关键字与代码中其余部分冲突,也可以使用__asm__指令,二者是等价的。

如果希望输入多条汇编语句,则应该每条一条语句都使用双引号括起来,语句和语句之间不需要添加额外的逗号等标点,C语言会自动将多个字符串合并起来,因而每条语句结尾应该加上\n\t

如果在代码中修改了寄存器的内容且在退出时没有恢复这些值,那么可能会产生一些错误结果。编译器可能对之前的代码做了一些优化并将其结果存放在某些寄存器中,但我们的内联汇编修改了部分值且没有通知GCC。。为了解决这个问题我们有扩展ASM方法。

扩展asm (Extended Asm)

在拓展asm中,我们不仅可以给出操作符,还可以给定输入、输出寄存器,并告诉GCC我们更改了哪些寄存器的值。基本的格式如下:

1
2
3
4
5
asm ( assembler template 
: output operands /* optional */
: input operands /* optional */
: list of clobbered registers /* optional */
);

Assembler Template

assembler template中包含了若干条汇编指令,这些汇编指令要么就用一整个双引号包括,要么就像之前提到的基础用法那样使用多个双引号并用\n\t结尾。对应的C表达式操作数使用%0, %1, ...进行表达。

Operands

操作数的一般格式如下:

1
"限定符"(c表达式),...

如果某个输出表达式不能被地址直接寻址,那么限定符必须允许某个寄存器被使用,GCC会使用寄存器保存最终结果,再将寄存器的值传送到输出。

考虑一些例子

1
2
3
4
asm ("leal (%1,%1,4), %0"
: "=r" (five_times_x)
: "r" (x)
);

lea指令取地址并传送;这里做的事情其实是将5x的值作为一个地址去查找对应的值,对这个值做lea指令当然就能还原出其地址,即5x了。

1
2
3
4
asm ("leal (%0,%0,4), %0"
: "=r" (five_times_x)
: "0" (x)
);

0限定符表示和第0个限定符相匹配,使用同样的寄存器。GCC扩展内联汇编关于寄存器的一点问题

1
2
3
4
asm ("leal (%%ecx,%%ecx,4), %%ecx"
: "=c" (x)
: "c" (x)
);

r限定符会让GCC自己选一个合适的寄存器,也可以通过一些特定的限定符规定选对应的某些寄存器。

这些例子都没有通知GCC任何已改写的寄存器信息。在前两个例子中,使用了r限定符,因而GCC直到哪些寄存器被使用;在第三个例子中c限定符指明了使用ecx寄存器,因而GCC通过限定符也知道这一事实,所以同样不需要通知ecx被改写的信息。

Clobber List

给出一个例子,from和to拥有g限定符,允许使用任何非常规寄存器、内存或立即数,eax和ecx作为参数传递给foo子程序,因而我们有必要告知GCC这两个寄存器可能会受到影响。

1
2
3
4
5
6
7
asm ("movl %0,%%eax;
movl %1,%%ecx;
call _foo"
: /* no outputs */
: "g" (from), "g" (to)
: "eax", "ecx"
);

如果指令可能修改状态寄存器(condition code register),则应该添加cc到该列表中;如果指令有可能修改内存,那么应该添加memory到该列表中。

Volatile

在asm后添加volatile关键字可以保证该段内联汇编语句不会被编译器优化、修改,当我们希望汇编语句不被移动、修改地执行时,可以加上该关键字;而如果执行的汇编语句只是一些简单的计算等操作,那么可以不使用该关键字,允许编译器对其进行优化。

The difference between asm, asm volatile and clobbering memory

限定符

通用限定符

寄存器操作数限定符

1
2
3
4
5
6
7
8
9
10
+---+--------------------+
| r | Register(s) |
+---+--------------------+
| a | %eax, %ax, %al |
| b | %ebx, %bx, %bl |
| c | %ecx, %cx, %cl |
| d | %edx, %dx, %dl |
| S | %esi, %si |
| D | %edi, %di |
+---+--------------------+

内存操作数限定符

当操作数存在于内存中时,对这些操作数的操作会直接在内存中进行;与之相对应的是寄存器限定符方式,此时GCC会先将值存放在寄存器中以便快速的修改,之后才将其写回到内存。

当C变量需在asm中修改且无需寄存器保持其值时,内存限制符可最大化性能。例如将idtr的值存储于loc的内存位置中:

1
__asm__("sidt %0\n" : :"m"(loc));

匹配限定符

通过使用匹配限定符,可以指定一个变量既是输入也是输出操作符。

1
asm ("incl %0" :"=a"(var):"0"(var));

通常,在下述情况可以使用匹配限定符:

  • 当输入从某个变量读入,且输出又写回到该变量
  • 分离输入输出为两个不同的操作数没有必要,可以节省可用寄存器的数量。

下面给出一些其余的限定符:

  • m: 允许给出一个内存操作数,这个地址可以是机器支持的任何地址。
  • o: 允许给出一个可偏移的内存操作数,在添加一个小的偏移量后该地址仍然合法。
  • V: 允许给出一个不可偏移的内存操作数,也就是所有满足m限定符但不满足o限定符的地址。
  • i: 允许给出一个立即数操作数,这个立即数也包括在编译期间可见的符号常量。
  • n: 允许给出一个数字值的立即数操作数。许多系统不支持编译期间小于一个字(word)宽的常量符号作为操作数,此时应该使用n限定符而不是i
  • g: 允许给出任何寄存器、内存或立即数,但是寄存器不能是通用寄存器。

还有一些是x86架构下专有的限定符,这里不予赘述了。。。

限定符修饰符

  • =: 表明该操作数只写,之前的值会被舍弃,替换为输出的数据。
  • &: (没搞懂。。附上原文) Means that this operand is an earlyclobber operand, which is modified before the instruction is finished using the input operands. Therefore, this operand may not lie in a register that is used as an input operand or as part of any memory address. An input operand can be tied to an earlyclobber operand if its only use as an input occurs before the early result is written.
  • +: 可读可写操作数,只能用来修饰输出操作数

  • 默认下操作数是只读的。