汇编语言
x86内联汇编

关于阅读本篇内容的一些汇编预备知识,可以参考:

x86汇编指南之寄存器与内存寻址模式 (opens in a new tab)

GCC汇编语法与Intel汇编语法的几个差异点 (opens in a new tab)

通过使用asm关键字,允许在C代码中嵌入汇编指令。

GCC提供了两种形式的内联asm语句:

  • 一个是基本内联汇编,不支持操作数;
  • 另一个是扩展内联汇编,可以包含一到多个操作数。

使用扩展内联汇编,可以在代码中混合使用C代码和汇编语句。

GCC的内联汇编,采用了AT&T的汇编语法。

基本内联汇编

首先,来看看最简单的内联汇编格式:

asm("汇编指令");

格式说明:先是关键词asm,然后是一对括号,括号内是用双引号括起来的汇编指令。

例如:

asm("movl %ecx,%eax"); /* 将寄存器ecx的值传入寄存器eax */

另一个等价的写法是:

__asm__("movl %ecx,%eax");

可以看到,我们在asm的前面和后面分别添加了两个下划线'_'。

当关键字asm在程序中可能出现冲突时,可以使用

__asm__

来代替。

上面的示例,是对于单条汇编指令的用法。

如果有多条汇编指令,需要每条指令作为一个字符串(被双引号包含)并独立写成一行,然后在指令的末尾添加\n\t(最后一条指令可以不用添加)。 这样,GCC才能够正确的解析并将指令发送给汇编器执行。

例如:

asm ("pushl %eax\n\t"
     "movl $0, %eax\n\t"
     "popl %eax");

扩展内联汇编

基本的内联汇编,仅支持简单的指令代码。

如果想支持更复杂的应用场景,如支持输入和输出参数,那么可以使用扩展内联汇编。

扩展内联汇编的基本格式为:

asm [ volatile ] ( /* 在这里编写汇编指令 */
	[:    /* 输出操作数列表 - 以英文逗号分隔的0到多个输出操作数 */
	[:    /* 输入操作数列表 - 以英文逗号分隔的0到多个输入操作数 */
	[:    /* clobbered寄存器列表,多个寄存器以英文逗号分隔 */
	]]]);

其中,中括号[ ]表示里面的内容是可选的。

最简单的格式就是基本内联汇编的写法,即括号内只有汇编指令代码。

简化一下格式。

asm ( 汇编指令
        : 输出操作数列表(可选)
        : 输入操作数列表(可选)
        : clobbered寄存器列表(可选)
   );

再把括号内的参数写成一行来看看。

asm (汇编指令 : 输出操作数列表 : 输入操作数列表 : clobbered寄存器列表)

括号内的参数,使用英文冒号':'分隔为四个部分。

第1部分是汇编指令,和本文最开始介绍的相同。采用AT&T汇编语法,每条指令写成一行。

第2部分是输出操作数列表,为可选参数,多个输出操作数以英文逗号','分隔。

第3部分是输入操作数列表,为可选参数,多个输出操作数以英文逗号','分隔。

第4部分是clobbered寄存器列表,为可选参数,多个寄存器以英文逗号','分隔。

除了汇编指令之外,其他各部分都是可选的。

对于输入和输出操作数,其格式都是:"约束符"(C表达式)

如果没有输出操作数,只有输入操作数,需要在汇编指令和输入操作数之间连续写2个冒号':'。

也就是说,输出操作数对应的冒号':'不能省略。例如:

asm (汇编指令 : : 输入操作数列表 : clobbered寄存器列表)

实例

使用汇编指令,将b的值设置为a对应的值。

int a=10, b;
asm ("movl %1, %%eax; 
	  movl %%eax, %0;"
	 :"=r"(b)        /* 输出操作数 */
	 :"r"(a)         /* 输入操作数 */
	 :"%eax"         /* clobbered寄存器 */
	 );   

其中,b作为输出操作数,在汇编指令中使用%0来引用。a作为输入操作数,在汇编指令中使用%1来引用。

r用来约束操作数,告诉GCC可以使用任意寄存器来存储被约束的操作数。

对于输出操作数,双引号内的约束符还需要添加一个修饰符'=',这个等号用来表示被修饰的是输出操作数,并且是只写的。

对于寄存器,使用两个百分号'%'作为前缀(不是一个);而对于普通操作数,则使用一个百分号'%'作为前缀。这样便于GCC区分解析寄存器和普通操作数。

最后一个参数%eax,用于通知GCC,寄存器eax将会在汇编指令中被使用和修改,让GCC不要使用该寄存器来存储任何值。

第1条指令,表示将输入操作数a传入寄存器eax。

第2条指令,表示将寄存器eax的值传入输出操作数b中。

下面具体说明一下asm括号内各个部分的含义及用法。

汇编指令

第1部分是汇编指令,可以是一条或多条指令。

如果是多条指令,可以使用英文分号';'分隔,或者使用\n\t进行分隔。可以是每条指令使用一个双引号,或者多条指令使用一个双引号。

在汇编指令中,对输出操作数和输入操作数的引用,使用百分号加数字编号的格式,编号从0开始。

先对输出操作数进行编号,比如有2个输出操作数,则分别表示为%0、%1。 再对输入操作数进行编号,比如也有2个输入操作数,则继续编号为%2、%3。 以此类推。

输入和输出操作数

第2部分和第3部分,分别是输出操作数列表和输入操作数列表,两者之间以英文冒号':'分隔。

输入和输出操作数列表在许多方面是相同的:

  • 操作数的格式都是:"约束符"(C表达式)
  • 多个操作数之间都以英文逗号','分隔
  • 两者均为可选参数

对于输出操作数,双引号内的约束符还需要添加一个修饰符'=',这个等号用来表示被修饰的是输出操作数,并且是只写的。

如果没有输出操作数,只有输入操作数,需要在汇编指令和输入操作数之间连续写2个冒号':'。 也就是说,输出操作数对应的冒号':'不能省略。

下面是一些例子:

1)输入操作数和输出操作数使用任意寄存器来存储。

asm ("leal (%1,%1,4), %0"
     : "=r" (x_times_5)
     : "r" (x) );

2)输入操作数和输出操作数使用同一个寄存器来存储。将输入操作数的约束设置为0,表示和位置0(第1个)的输出操作数使用相同的寄存器。

asm ("leal (%0,%0,4), %0"
     : "=r" (x)
     : "0" (x) );

3)在约束符中,a、b、c、d分别表示寄存器eax、ebx、ecx、edx。下面输入操作数和输出操作数使用同一个ebx寄存器。

asm ("leal (%%ebx,%%ebx,4), %%ebx"
     : "=b" (x)
     : "b" (x) );

clobber寄存器列表

第4部分是clobber寄存器列表。用于通知GCC,指定的寄存器将会在汇编指令中被使用和修改,让GCC不要使用该寄存器来存储任何值。

在输入操作数和输出操作数中使用的寄存器,不需要在clobber列表中列出。因为这个是显式指定的约束,GCC知道在汇编指令中会使用到它们。

多个clobber寄存器之间以英文逗号','分隔。

volatile

volatile关键字,用于让GCC优化器禁用特定的优化。

例如:当优化器发现输出操作数没有用时,可能会丢弃这些语句。当优化器判定被执行的代码总是返回相同的结果时,则会将代码从循环逻辑中移出来。

如果使用了volatile关键字,那么优化器将会禁用上面的优化。

换句话说,使用volatile可以让汇编指令在我们所编写的代码位置中执行,而不会被删除、移动或者执行其他优化。

为了避免造成冲突,volatile同样可以写成

__volatile__

常用约束符

寄存器操作数约束

输入和输出操作数,比较常见的是使用寄存器来进行约束。

寄存器约束有很多,常见的约束符及其含义如下。

r:任意一个寄存器(动态分配)
a:EAX寄存器
b:EBX寄存器
c:ECX寄存器
d:EDX寄存器
f:浮点寄存器
D:EDI寄存器
S:ESI寄存器

在指令执行前,会先将输入或输出操作数的值存入寄存器中,然后再使用相应的寄存器来完成指令操作。

对于输出操作数,在指令执行完成后,会将寄存器的值回写到输出操作数对应的变量中。

内存操作数约束

当操作数在内存中时,对其进行任何操作将直接反映到内存中。

使用约束符m,来代替寄存器约束,可以实现直接对内存中的值进行修改。

#include <stdio.h>
int main (void) {
  int x = 3;
  asm volatile("incl %0" : : "m"(x));
  printf("%d", x);
}

程序执行后输出结果:

4

如果使用寄存器约束,那么内存值会先存储到寄存器中,在指令执行结束后再将寄存器的值回写到内存。

匹配约束

当一个变量既作为输入操作数,也作为输出操作数时,可以使用匹配约束,使得输入和输出操作数使用同一个寄存器。

例如:

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

其中,输入操作数的约束为0,表示该输入操作数和第0个输出操作数使用相同的约束。

在该例子中,输出操作数的约束为a,因此输入操作数和输出操作数都使用寄存器eax来存储。

更多实例

1)实例1

使用汇编指令add,实现将两个数值相加。

#include <stdio.h>
int main(void)
{
        int foo = 10, bar = 15;
        __asm__ __volatile__("addl  %%ebx,%%eax" // 汇编指令
                             :"=a"(foo)  // 输出操作数:a表示eax,用于存放计算结果,最后保存到foo
                             :"a"(foo), "b"(bar)  // 输入操作数:第一个参数是foo,用eax来存放;第二个参数是bar,用ebx来存放
                             );
        printf("foo+bar=%d\n", foo);
        return 0;
}

执行程序后,foo的值为25。

程序执行后输出结果:

foo+bar=25

2)实例2

#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <time.h>
 
int f( int );
 
int main (void) {
  int x; 
  asm volatile("movl $3,%0" : "=g"(x) : : "memory"); // x = 3;
  printf("%d  ->  %d\n", x, f(x));
}
 
int f( int x ) {
	asm volatile ("movl %0,%%eax;" // eax=3
                 "imull $3, %%eax;" // eax=eax*3=9
                 "addl $4,%%eax;" // eax=eax+4=13
         :
         :"a" (x)
         : "memory"
        ); // 返回 (3*x + 4);
}

第一段指令代码没有输入操作数,指令的作用是将常量值3传入x。

第二段指令代码没有输出操作数,其中

  • 第1条指令表示将x的值传入eax;
  • 第2条指令表示将eax和3相乘,并将结果存入eax;
  • 第3条指令表示将eax和4相加,并将结果存入eax。

函数执行完成后,将eax的值作为结果返回。

程序执行后输出结果:

3  ->  13

参考

GCC-Inline-Assembly-HOWTO (opens in a new tab)

Brennan's Guide to Inline Assembly

Using-Assembly-Language-with-C (opens in a new tab)