本文总结内联汇编的基本用法和应用举例。

1 内联汇编语法格式

1
2
3
4
5
6
asm [volatile] (
    "汇编指令"
    : 输出操作数列表
    : 输入操作数列表
    : 被破坏的寄存器列表
);

1.1 内联汇编参数说明

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>                                                                                                                                                                                                                 

int add(int a, int b)
{
    int res;
    asm volatile(
        "addl %1, %2 \n\t"
        "movl %2, %0 \n\t"
        : "=r"(res)
        : "r"(a), "r"(b));
    return res;
}

int main(void)
{
    int result = add(1, 2);
    printf("result: %d\n", result);

    return 0;
}

代码说明:该代码为GCC内联汇编,汇编格式为AT&T。

  1. 内联汇编代码说明
  • 引号包含的内容为汇编语言。此处第7,8行为汇编代码。其中%0, %1, %2为占位符,占位符将在后续说明。
  • 内联汇编中,若有多行代码,一般使用\n\t分隔(也可以写在同一行,使用分好分隔);
  • 此处%0表示的是第一个输出操作数。%1为第一个输入操作数,%2为第二个输入操作数。
  1. 输出操作数列表
  • 汇编代码结束后,紧挨着的一个冒号(:)后就是输出操作数列表;
  • 输出操作数可以为空,但是即使输出操作数列表为空,冒号却不能省略;
  • 若有多个输出操作数,多个输出操作数之间使用逗号(,)分隔。
  1. 输入操作数列表
  • 输出操作数列表后紧挨着的一个冒号(:)后就是输入操作数列表;
  • 输入操作数可以为空,但是即使输出操作数列表为空,冒号却不能省略;
  • 若有多个输入操作数,多个操作数之间使用逗号(,)分隔。
  1. 被破坏的寄存器列表
  • 若存在被破坏的寄存器列表,放在输入操作列表后。
  • 若被破坏的寄存器列表为空,则冒号可以省略;

1.2 内联汇编中符号的功能

1.2.1 占位符

在内联汇编中,%0、%1、%2 等是占位符(placeholders),代表操作数列表中各个操作数的引用。它们按照操作数在列表中的顺序进行编号。

编号规则:

  • 从0开始编号
  • 先从输出操作数开始,依次编号。
  • 编号顺序为:第一个输出操作数为%0,第二个输出操作数为%1,然后依次编号输入操作数。输出操作数编号完毕后,紧接着编号输入操作数。

1.2.2 修饰符

修饰符用于控制输入操作数列表,输出操作数列表和被破坏寄存器列表,可以多个修饰符共同使用。

修饰符 功能说明 示例
= 表示当前变量为只写变量,一般用用于修饰输出操作数 “=r”(res)
+ 表示当前变量为可读写变量 “+r”(a)
& 表示修饰的变量不能与其他变量公用寄存器(或内存) “=&r”(result1)
r 表示当前操作数使用通用寄存器存放 “r”(sum)
m 表示当前操作数使用内存存放 “m”(res)
0-9 使用指定操作数保存变量 “0”(var)

2 内联汇编实例

2.1 x86内联汇编实例

1
2
3
4
5
6
7
8
9
static inline int a_cas(volatile int *p, int t, int s)
{
    __asm__ __volatile__ (
        "lock ; cmpxchg %3, %1"
        : "=a"(t), "=m"(*p) 
        : "a"(t), "r"(s) 
        : "memory" );
    return t;
}

代码含义说明:

  • %0表示输出参数t,%1表示输出参数*p,%2表示输入参数t,%3表示输入参数s。
  • %0 和 %2 都关联到同一个 C 变量 t,但含义不同:%2 是输入,提供期望值;%0 是输出,将操作后的实际值写回 t。"=a"(t)表示将通用寄存器EAX的值写入到输出参数t中。“a”(t)表示将输入参数t的保存到通用寄存器EAX中。

  • %1 被声明为 =m(输出),同时又被隐含地作为 CMPXCHG 的目的操作数,会被修改。虽然未在输入列表中显式出现,但 "memory" 破坏描述符同样确保了编译器不会在此前后缓存该内存的值。

  • *(void *volatile *)p 这个写法:先将 p 强制转换为 void *volatile * 类型(即指向 volatile void* 的指针),再解引用,从而操作一个指针大小的内存单元,并告知编译器该内存是易变的。

  • 函数功能说明:原子的比较 *p(即地址 p 处存放的指针值)是否等于 t,如果相等则将其替换为 s;*无论成功与否,最终都返回 p 在操作前的实际值

  • 返回值的行为是标准 CAS 的一种形式:函数返回旧值,调用方通过判断 返回值 == t 来得知是否交换成功。