Inline Assembly

来自osdev
跳到导航 跳到搜索

“内联汇编”背后的思想是在除使用汇编语言之外别无选择的情况下,使用asm关键字在C/C++代码中嵌入汇编指令。

概述

有时,即使C/C++是你选择的语言,你也“需要”在操作系统中使用一些汇编代码。 无论是因为极端的优化需求,还是因为你正在实现的代码是高度特定于硬件的(比如说,通过端口输出数据),结果都是一样的:没有办法绕过它。 你必须要使用汇编语言。

你可以选择编写一个汇编函数并调用它,但是有时甚至“调用”开销对你来说都太大了。 在这种情况下,你需要的是内联汇编技术,这意味着使用asm()关键字插入代码中间的任意汇编片段。 这个关键字的工作方式是特定于编译器的。 本文描述了它在GCC中的工作方式,因为它是操作系统世界中使用最多的编译器。

语法

这是在C/C++代码中使用asm()关键字的语法:

asm ( assembler template
    : output operands                   (optional)
    : input operands                    (optional)
    : clobbered registers list          (optional)
    );

汇编程序模板基本上是GAS兼容的代码,除非有限制,在这种情况下,寄存器名必须以%%而不是%开头。 这意味着以下两行都是将eax寄存器的内容移动到ebx的代码:

asm ("movl %eax, %ebx");
asm ("movl %%eax, %%ebx" : );

现在,你可能想知道为什么这%%会出现。 这是因为内联汇编的一个有趣特性:你可以在汇编代码中使用一些C变量。 为了简化该机制的实现,GCC在汇编代码中命名这些变量%0、%1等等,从输入/输出operand部分中提到的第一个变量开始。 你需要使用此%%语法来帮助GCC区分寄存器和参数。

该操作的具体工作原理将在后面的章节中详细解释。 现在,下面这个例子可以先提供一个演示:

int a=10, b;
asm ("movl %1, %%eax; 
      movl %%eax, %0;"
     :"=r"(b)        /*输出*/
     :"r"(a)         /*输入*/
     :"%eax"         /* 被破坏的寄存器(clobbered register) */
     );

这里你已经设法使用汇编代码将“a”的值复制到“b”中,在汇编代码中有效地使用了一些C变量。

最后一个“clobbered register”部分用于告诉GCC你的代码正在使用处理器的一些寄存器,并且在执行asm代码段之前,它应该将正在运行的程序中的所有活动数据移出该寄存器。 在上面的例子中,我们在第一条指令中将a移动到eax,有效地擦除了它的内容,因此我们需要要求GCC在操作之前清除该寄存器,所以要保存寄存器数据。

汇编程序模板

汇编器模板定义要内联的汇编器指令。 默认情况下,此处使用AT&T语法。 如果要使用Intel语法,应将-masm=Intel指定为命令行选项。

例如,要停止CPU,只需使用以下命令:

asm( "hlt" );

输出Operands

本节“输出Operands”部分用于告诉编译器/汇编程序如何处理存储ASM代码输出的C变量。 输出Operands是一组各自成对,每个Operands由一个字符串文字组成,称为“constraint”,说明C变量应该映射到哪里(到寄存器通常用于最佳性能),以及要映射到哪里的C变量(在括号中)。

假设你正在为IA32体系结构编码,在constraint中,“a”表示EAX,“b”表示EBX,“c”表示ECX,“d”表示EDX,“S”表示ESI,“d”表示EDI(请阅读GCC手册中的完整列表)。 等式符号表示汇编代码不关心映射变量的初始值(这允许一些优化)。 考虑到所有这些,现在下面的代码将EAX设置为0就很清楚了。

int EAX;
asm( "movl $0, %0"
   : "=a" (EAX)
    );

请注意,编译器将枚举以%0开头的operand,如果将寄存器用于存储输出operand,则无需将其添加到已删除的寄存器列表中。 GCC足够聪明,能够自己决定做什么。

从GCC 3.1开始,你可以使用更可读的标签,而不是容易出错的枚举:

int current_task;
asm( "str %[output]"
   : [output] "=r" (current_task)
    );

这些标签位于它们自己的名称空间中,不会与任何C标识符冲突。 对于输入Operands也可以这样做。

输入Operands

虽然输出Operands通常只用于输出,输入Operands还允许参数化ASM代码; 将只读参数从C代码传递到ASM块。 同样,字符串文字用于指定详细信息。

如果你想将某个值移动到EAX,可以通过以下方式执行(即使这样做,而不是直接将该值映射到EAX肯定是毫无用处的):

int randomness = 4;
asm( "movl %0, %%eax"
   :
   : "b" (randomness)
   : "eax"
    );

请注意,GCC将始终假定输入Operands是只读的(不更改的)。 写入输入Operands时,正确的做法是将它们列为输出,但不使用等式符号,因为这一次它们的原始值很重要。 下面是一个简单的例子:

asm("mov %%eax,%%ebx": : "a" (amount));// 没用但用它说明问题

Eax将包含“amount”,并移动到ebx中。

删除寄存器列表

请记住一件事很重要:“C/C++编译器对汇编器一无所知”。 对于编译器来说,asm语句是不透明的,如果你没有指定任何输出,它甚至可能得出结论,认为这是一个不可操作的语句,并对其进行优化。 一些第三方文档指出,使用asm volatile将导致不被移动的关键字。 然而,根据GCC文档,“volatile关键字表示指令有重要的副作用。 如果可以访问易失性asm,GCC将不会删除该asm。,这只表示它不会被删除 (也就是说,它是否仍然可以移动是一个尚未回答的问题)。 一种可行的方法是使用asm(volatile)并将“内存”放入缓冲寄存器中,如下所示:

__asm__("cli": : :"memory"); // 将导致语句不移动,但可能会将其优化。
__asm__ __volatile__("cli": : :"memory"); // 将导致语句不会被移动或优化。

由于编译器使用CPU寄存器对C/C++变量进行内部优化,并且不知道ASM操作码, 你必须警告它,任何寄存器可能会因为副作用而被破坏, 因此,编译器可以在进行ASM调用之前保存它们的内容。

Clobbered Registers列表是一个以逗号分隔的寄存器名列表,以字符串文本形式显示。

通配符(Wildcards):如何让编译器选择

你不需要告诉编译器在每个操作中应该使用哪个特定寄存器,一般来说 除非你有充分的理由特别喜欢一个寄存器, 你最好让编译器替你决定。

例如,强制在任何其他寄存器上使用EAX可能会迫使编译器代码报错,将以前在EAX中的内容保存在其他寄存器中,或者可能会在操作之间引入不必要的依赖关系(管道优化被破坏)

通配符constraints允许你在输入/输出映射方面给予GCC更多自由:

The "g" constraint :
"movl $0, %0" : "=g" (x)
x可以是编译器喜欢的任何东西:寄存器、内存引用。在另一个上下文中,它甚至可以是一个字面常量。
The "r" constraint :
"movl %%es, %0" : "=r" (x)
你希望x通过寄存器。 如果x没有作为寄存器进行优化,编译器会将其移动到应该的位置。 这意味着"movl %0, %%es" : : "r" (0x38)足以加载段寄存器。
The "N" constraint :
"outl %0, %1" : : "a" (0xFE), "N" (0x21)
说明值 '0x21' 可以在out或in操作中用作常数,范围从0到255

当然,你可以对operand选择施加更多constraints,无论是否依赖于机器,这些constraints都列在GCC的手册中 (参考[1], [2], [3], 和[4])。

使用C99

当使用gcc-std=c99时,asm不是关键字。 只需使用gcc -std=gnu99即可使用带有GNU扩展的C99。 或者,你可以使用 __asm__ 作为备用关键字,即使编译器严格遵守标准,该关键字也可以使用。

分配标签

可以将所谓的ASM标签指定给C/C++关键字。 你可以通过对变量定义使用asm命令来执行此操作,如本例所示:

int some_obscure_name asm("param") = 5; //“param”将可在内联汇编中访问。

void foo()
{
asm("mov param, %%eax");
}

下面是一个示例,说明如果不显式声明名称,如何访问这些变量:

int some_obscure_name = 5;

void foo()
{
asm("mov some_obscure_name, %%eax");
}

请注意,根据你的链接选项,你可能还必须使用_some_obscure_name(带前导下划线)。

汇编中的GOTO

在GCC4.5之前,不支持跨内联汇编跳转。编译器无法跟踪正在发生的事情, 因此,只能生成错误的代码。
你可能会被告知“gotos是邪恶的”。 如果你相信这是真的,那么asm gotos就是你最可怕的噩梦。 然而,它们确实提供了一些有趣的代码优化选项。

asm goto没有很好的文档记录,但其语法如下:

 asm goto( "jmp %l[labelname]" : /* no outputs */ : /* inputs */ : "memory" /* clobbers */ : labelname /* any labels used */ );

CMPXCHG指令就是一个很有用的例子(参见Compare and Swap),Linux内核源代码对其定义如下:

/* TODO: You should use modern GCC atomic instruction builtins instead of this. */
#include <stdint.h>
#define cmpxchg( ptr, _old, _new ) { \
  volatile uint32_t *__ptr = (volatile uint32_t *)(ptr);   \
  uint32_t __ret;                                     \
  asm volatile( "lock; cmpxchgl %2,%1"           \
    : "=a" (__ret), "+m" (*__ptr)                \
    : "r" (_new), "0" (_old)                     \
    : "memory");				 \
  );                                             \
  __ret;                                         \
}

除了返回EAX中的当前值,CMPXCHG在成功时设置零标志(Z)。 如果没有ASM GOTOS,你的代码将必须检查返回值;可以按如下方式避免此CMP指令:

/* TODO: You should use modern GCC atomic instruction builtins instead of this. */
// Works for both 32 and 64 bit
#include <stdint.h>
#define cmpxchg( ptr, _old, _new, fail_label ) { \
  volatile uint32_t *__ptr = (volatile uint32_t *)(ptr);   \
  asm goto( "lock; cmpxchg %1,%0 \t\n"           \
    "jnz %l[" #fail_label "] \t\n"               \
    : /* empty */                                \
    : "m" (*__ptr), "r" (_new), "a" (_old)       \
    : "memory", "cc"                             \
    : fail_label );                              \
}

然后可以按如下方式使用此新宏:

struct Item {
volatile struct Item* next;
};

volatile struct Item *head;

void addItem( struct Item *i ) {
volatile struct Item *oldHead;
again:
oldHead = head;
i->next = oldHead;
cmpxchg( &head, oldHead, i, again );
}

Intel语法

通过在内联汇编中启用选项,你可以让GCC使用Intel语法,如下所示:

asm(".intel_syntax noprefix");
asm("mov eax, ebx");

类似地,你可以使用以下代码段切换回AT&T语法:

asm(".att_syntax prefix");
asm("mov %ebx, %eax");

通过这种方式,你可以将Intel语法和AT&T语法内联汇编结合起来。 请注意,一旦触发其中一种语法类型,源文件中命令下面的所有内容都将使用此语法进行汇编, 因此,在必要时不要忘记切换回来,否则可能会出现大量编译错误!

还有一个命令行选项-masm=intel,用于全局触发Intel语法。

另见

文章

论坛主题

外部链接

de:Inline-Assembler_mit_GCC