“Inline Assembly”的版本间差异
(未显示同一用户的1个中间版本) | |||
第2行: | 第2行: | ||
== 概述 == | == 概述 == | ||
有时,即使C/C++ | 有时,即使C/C++是你选择的语言,你也“需要”在操作系统中使用一些汇编代码。 无论是因为极端的优化需求,还是因为你正在实现的代码是高度特定于硬件的(比如说,通过端口输出数据),结果都是一样的:没有办法绕过它。 你必须要使用汇编语言。 | ||
你可以选择编写一个汇编函数并调用它,但是有时甚至“调用”开销对你来说都太大了。 在这种情况下,你需要的是内联汇编技术,这意味着使用<tt>asm()</tt>关键字插入代码中间的任意汇编片段。 这个关键字的工作方式是特定于编译器的。 本文描述了它在GCC中的工作方式,因为它是操作系统世界中使用最多的编译器。 | |||
== 语法 == | == 语法 == | ||
第24行: | 第24行: | ||
</source> | </source> | ||
现在,你可能想知道为什么这%%会出现。 这是因为内联汇编的一个有趣特性:你可以在汇编代码中使用一些C变量。 为了简化该机制的实现,GCC在汇编代码中命名这些变量%0、%1等等,从输入/输出operand部分中提到的第一个变量开始。 你需要使用此%%语法来帮助GCC区分寄存器和参数。 | |||
该操作的具体工作原理将在后面的章节中详细解释。 现在,下面这个例子可以先提供一个演示: | 该操作的具体工作原理将在后面的章节中详细解释。 现在,下面这个例子可以先提供一个演示: | ||
第32行: | 第32行: | ||
asm ("movl %1, %%eax; | asm ("movl %1, %%eax; | ||
movl %%eax, %0;" | movl %%eax, %0;" | ||
:"=r"(b) /* | :"=r"(b) /*输出*/ | ||
:"r"(a) /* | :"r"(a) /*输入*/ | ||
:"%eax" /* clobbered register */ | :"%eax" /* 被破坏的寄存器(clobbered register) */ | ||
); | ); | ||
</source> | </source> | ||
这里你已经设法使用汇编代码将“a”的值复制到“b”中,在汇编代码中有效地使用了一些C变量。 | |||
最后一个“clobbered | 最后一个“clobbered register”部分用于告诉GCC你的代码正在使用处理器的一些寄存器,并且在执行asm代码段之前,它应该将正在运行的程序中的所有活动数据移出该寄存器。 在上面的例子中,我们在第一条指令中将<tt>a</tt>移动到eax,有效地擦除了它的内容,因此我们需要要求GCC在操作之前清除该寄存器,所以要保存寄存器数据。 | ||
=== 汇编程序模板 === | === 汇编程序模板 === | ||
第52行: | 第52行: | ||
=== 输出Operands === | === 输出Operands === | ||
本节“输出Operands”部分用于告诉编译器/汇编程序如何处理存储ASM代码输出的C变量。 | 本节“输出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就很清楚了。 | |||
<source lang="c"> | <source lang="c"> | ||
第63行: | 第63行: | ||
</source> | </source> | ||
请注意,编译器将枚举以% | 请注意,编译器将枚举以%0开头的operand,如果将寄存器用于存储输出operand,则无需将其添加到已删除的寄存器列表中。 GCC足够聪明,能够自己决定做什么。 | ||
从GCC 3. | 从GCC 3.1开始,你可以使用更可读的标签,而不是容易出错的枚举: | ||
<source lang="c"> | <source lang="c"> | ||
第77行: | 第77行: | ||
=== 输入Operands === | === 输入Operands === | ||
虽然输出Operands通常只用于输出,输入Operands还允许参数化ASM代码; 将只读参数从C代码传递到ASM块。 同样,字符串文字用于指定详细信息。 | |||
如果你想将某个值移动到EAX,可以通过以下方式执行(即使这样做,而不是直接将该值映射到EAX肯定是毫无用处的): | |||
<source lang="c"> | <source lang="c"> | ||
第92行: | 第92行: | ||
请注意,GCC将始终假定输入Operands是只读的(不更改的)。 写入输入Operands时,正确的做法是将它们列为输出,但不使用等式符号,因为这一次它们的原始值很重要。 下面是一个简单的例子: | 请注意,GCC将始终假定输入Operands是只读的(不更改的)。 写入输入Operands时,正确的做法是将它们列为输出,但不使用等式符号,因为这一次它们的原始值很重要。 下面是一个简单的例子: | ||
<source lang="c"> | <source lang="c"> | ||
asm("mov %%eax,%%ebx": : "a" (amount));// | asm("mov %%eax,%%ebx": : "a" (amount));// 没用但用它说明问题 | ||
</source> | </source> | ||
Eax将包含“amount”,并移动到ebx中。 | Eax将包含“amount”,并移动到ebx中。 | ||
=== 删除寄存器列表 === | === 删除寄存器列表 === | ||
请记住一件事很重要:“C/C++编译器对汇编器一无所知”。 | 请记住一件事很重要:“C/C++编译器对汇编器一无所知”。 对于编译器来说,asm语句是不透明的,如果你没有指定任何输出,它甚至可能得出结论,认为这是一个不可操作的语句,并对其进行优化。 一些第三方文档指出,使用asm volatile将导致不被移动的关键字。 然而,根据GCC文档,“volatile关键字表示指令有重要的副作用。 如果可以访问易失性asm,GCC将不会删除该asm。'',这只表示它不会被删除 (也就是说,它是否仍然可以移动是一个尚未回答的问题)。 一种可行的方法是使用asm(volatile)并将“内存”放入缓冲寄存器中,如下所示: | ||
<source lang="c"> | <source lang="c"> | ||
第104行: | 第104行: | ||
</source> | </source> | ||
由于编译器使用CPU寄存器对C/C++变量进行内部优化,并且不知道ASM操作码, | 由于编译器使用CPU寄存器对C/C++变量进行内部优化,并且不知道ASM操作码, 你必须警告它,任何寄存器可能会因为副作用而被破坏, 因此,编译器可以在进行ASM调用之前保存它们的内容。 | ||
Clobbered Registers列表是一个以逗号分隔的寄存器名列表,以字符串文本形式显示。 | Clobbered Registers列表是一个以逗号分隔的寄存器名列表,以字符串文本形式显示。 | ||
=== | === 通配符(Wildcards):如何让编译器选择 === | ||
你不需要告诉编译器在每个操作中应该使用哪个特定寄存器,一般来说 除非你有充分的理由特别喜欢一个寄存器, 你最好让编译器替你决定。 | |||
例如,强制在任何其他寄存器上使用EAX可能会迫使编译器代码报错,将以前在EAX中的内容保存在其他寄存器中,或者可能会在操作之间引入不必要的依赖关系(管道优化被破坏) | 例如,强制在任何其他寄存器上使用EAX可能会迫使编译器代码报错,将以前在EAX中的内容保存在其他寄存器中,或者可能会在操作之间引入不必要的依赖关系(管道优化被破坏) | ||
通配符constraints允许你在输入/输出映射方面给予GCC更多自由: | |||
{| {{wikitable}} | {| {{wikitable}} | ||
|- | |- | ||
| The "g" constraint : <source lang="c">"movl $0, %0" : "=g" (x)</source> | | The "g" constraint : <source lang="c">"movl $0, %0" : "=g" (x)</source> | ||
| | | x可以是编译器喜欢的任何东西:寄存器、内存引用。在另一个上下文中,它甚至可以是一个字面常量。 | ||
|- | |- | ||
| The "r" constraint : <source lang="c">"movl %%es, %0" : "=r" (x)</source> | | The "r" constraint : <source lang="c">"movl %%es, %0" : "=r" (x)</source> | ||
| | | 你希望x通过寄存器。 如果x没有作为寄存器进行优化,编译器会将其移动到应该的位置。 这意味着<code>"movl %0, %%es" : : "r" (0x38)</code>足以加载段寄存器。 | ||
|- | |- | ||
| The "N" constraint : <source lang="c">"outl %0, %1" : : "a" (0xFE), "N" (0x21)</source> | | The "N" constraint : <source lang="c">"outl %0, %1" : : "a" (0xFE), "N" (0x21)</source> | ||
| | | 说明值 '0x21' 可以在out或in操作中用作常数,范围从0到255 | ||
|} | |} | ||
当然,你可以对operand选择施加更多constraints,无论是否依赖于机器,这些constraints都列在GCC的手册中 (参考[http://gcc.gnu.org/onlinedocs/gcc-4.4.4/gcc/Simple-Constraints.html#Simple-Constraints], [http://gcc.gnu.org/onlinedocs/gcc-4.4.4/gcc/Modifiers.html#Modifiers], [http://gcc.gnu.org/onlinedocs/gcc-4.4.4/gcc/Multi_002dAlternative.html#Multi_002dAlternative], 和[http://gcc.gnu.org/onlinedocs/gcc-4.4.4/gcc/Machine-Constraints.html#Machine-Constraints])。 | |||
== 使用C99 == | == 使用C99 == | ||
当使用<tt>gcc-std=c99</tt>时,<tt>asm</tt>不是关键字。 只需使用<tt>gcc -std=gnu99</tt>即可使用带有GNU扩展的C99。 | 当使用<tt>gcc-std=c99</tt>时,<tt>asm</tt>不是关键字。 只需使用<tt>gcc -std=gnu99</tt>即可使用带有GNU扩展的C99。 或者,你可以使用 <tt >__asm__</tt> 作为备用关键字,即使编译器严格遵守标准,该关键字也可以使用。 | ||
== 分配标签 == | == 分配标签 == | ||
可以将所谓的ASM标签指定给C/C++关键字。 | 可以将所谓的ASM标签指定给C/C++关键字。 你可以通过对变量定义使用<tt>asm</tt>命令来执行此操作,如本例所示: | ||
<source lang="c"> | <source lang="c"> | ||
int some_obscure_name asm("param") = 5; // | int some_obscure_name asm("param") = 5; //“param”将可在内联汇编中访问。 | ||
void foo() | void foo() | ||
第154行: | 第154行: | ||
</source> | </source> | ||
请注意,根据你的链接选项,你可能还必须使用'''_some_obscure_name'''(带前导下划线)。 | |||
== | ==汇编中的GOTO== | ||
在GCC4.5之前,不支持跨内联汇编跳转。编译器无法跟踪正在发生的事情, | 在GCC4.5之前,不支持跨内联汇编跳转。编译器无法跟踪正在发生的事情, | ||
因此,只能生成错误的代码。 | 因此,只能生成错误的代码。 | ||
第167行: | 第167行: | ||
</source> | </source> | ||
CMPXCHG指令就是一个很有用的例子(参见[http://en.wikipedia.org/wiki/Compare-and-swap Compare and Swap),Linux内核源代码对其定义如下: | CMPXCHG指令就是一个很有用的例子(参见[http://en.wikipedia.org/wiki/Compare-and-swap Compare and Swap]),Linux内核源代码对其定义如下: | ||
<source lang="c"> | <source lang="c"> | ||
/* TODO: You should use modern GCC atomic instruction builtins instead of this. */ | /* TODO: You should use modern GCC atomic instruction builtins instead of this. */ | ||
第183行: | 第183行: | ||
</source> | </source> | ||
除了返回EAX中的当前值,CMPXCHG在成功时设置零标志(Z)。 如果没有ASM | 除了返回EAX中的当前值,CMPXCHG在成功时设置零标志(Z)。 如果没有ASM GOTOS,你的代码将必须检查返回值;可以按如下方式避免此CMP指令: | ||
<source lang="c"> | <source lang="c"> | ||
第220行: | 第220行: | ||
== Intel语法 == | == Intel语法 == | ||
通过在内联汇编中启用选项,你可以让GCC使用Intel语法,如下所示: | |||
<source lang="c"> | <source lang="c"> | ||
第227行: | 第227行: | ||
</source> | </source> | ||
类似地,你可以使用以下代码段切换回AT&T语法: | |||
<source lang="c"> | <source lang="c"> | ||
第234行: | 第234行: | ||
</source> | </source> | ||
通过这种方式,你可以将Intel语法和AT&T语法内联汇编结合起来。 请注意,一旦触发其中一种语法类型,源文件中命令下面的所有内容都将使用此语法进行汇编, 因此,在必要时不要忘记切换回来,否则可能会出现大量编译错误! | |||
还有一个命令行选项<tt>-masm=intel</tt>,用于全局触发Intel语法。 | 还有一个命令行选项<tt>-masm=intel</tt>,用于全局触发Intel语法。 |
2022年2月22日 (二) 15:49的最新版本
“内联汇编”背后的思想是在除使用汇编语言之外别无选择的情况下,使用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语法。
另见
文章
- Inline Assembly/Examples - 有用且常用的函数