“Inline Assembly”的版本间差异
(创建页面,内容为““内联汇编”背后的思想是在除了使用Assembly语言之外别无选择的情况下,使用<tt>asm</tt>关键字在C/C++代码中嵌入汇编指令。 == 概述 == 有时,即使C/C++是您选择的语言,您“需要”在操作系统中使用一些汇编代码。 无论是因为极端的优化需求,还是因为您正在实现的代码是高度特定于硬件的(比如说,通过端口输出数据),结果都是一样的:没有…”) |
|||
第1行: | 第1行: | ||
“内联汇编”背后的思想是在除使用[[Assembly|汇编]]语言之外别无选择的情况下,使用<tt>asm</tt>关键字在C/C++代码中嵌入汇编指令。 | |||
== 概述 == | == 概述 == | ||
有时,即使C/C++ | 有时,即使C/C++是您选择的语言,您也“需要”在操作系统中使用一些汇编代码。 无论是因为极端的优化需求,还是因为您正在实现的代码是高度特定于硬件的(比如说,通过端口输出数据),结果都是一样的:没有办法绕过它。 你必须要使用汇编语言。 | ||
您可以选择编写一个汇编函数并调用它,但是有时甚至“调用”开销对您来说都太大了。 | 您可以选择编写一个汇编函数并调用它,但是有时甚至“调用”开销对您来说都太大了。 在这种情况下,您需要的是内联汇编技术,这意味着使用<tt>asm()</tt>关键字插入代码中间的任意程序集片段。 这个关键字的工作方式是特定于编译器的。 本文描述了它在GCC中的工作方式,因为它是操作系统世界中使用最多的编译器。 | ||
== 语法 == | == 语法 == | ||
第17行: | 第17行: | ||
</source> | </source> | ||
汇编程序模板基本上是[[GAS]]兼容的代码,除非有限制,在这种情况下,寄存器名必须以%%而不是%开头。 | 汇编程序模板基本上是[[GAS]]兼容的代码,除非有限制,在这种情况下,寄存器名必须以%%而不是%开头。 这意味着以下两行都是将<tt>eax</tt>寄存器的内容移动到<tt>ebx</tt>的代码: | ||
<source lang="c"> | <source lang="c"> | ||
第24行: | 第24行: | ||
</source> | </source> | ||
现在,您可能想知道为什么这%%会出现。 | 现在,您可能想知道为什么这%%会出现。 这是因为内联汇编的一个有趣特性:您可以在汇编代码中使用一些C变量。 因为,为了简化该机制的实现,GCC在汇编代码中命名这些变量%0、%1等等,从输入/输出操作数部分中提到的第一个变量开始。 您需要使用此%%语法来帮助GCC区分寄存器和参数。 | ||
该操作的具体工作原理将在后面的章节中详细解释。 现在,下面这个例子可以先提供一个演示: | |||
<source lang="c"> | <source lang="c"> | ||
第38行: | 第38行: | ||
</source> | </source> | ||
这里您已经设法使用汇编代码将“a”的值复制到“b”中,在汇编代码中有效地使用了一些C变量。 | |||
最后一个“clobbered register”部分用于告诉GCC您的代码正在使用处理器的一些寄存器,并且在执行asm代码段之前,它应该将正在运行的程序中的所有活动数据移出该寄存器。 在上面的例子中,我们在第一条指令中将<tt>a</tt>移动到eax,有效地擦除了它的内容,因此我们需要要求GCC在操作之前从未保存的数据中清除该寄存器。 | 最后一个“clobbered register”部分用于告诉GCC您的代码正在使用处理器的一些寄存器,并且在执行asm代码段之前,它应该将正在运行的程序中的所有活动数据移出该寄存器。 在上面的例子中,我们在第一条指令中将<tt>a</tt>移动到eax,有效地擦除了它的内容,因此我们需要要求GCC在操作之前从未保存的数据中清除该寄存器。 | ||
=== 汇编程序模板 === | === 汇编程序模板 === | ||
汇编器模板定义要内联的汇编器指令。 默认情况下,此处使用AT&T语法。 | 汇编器模板定义要内联的汇编器指令。 默认情况下,此处使用AT&T语法。 如果要使用Intel语法,应将-masm=Intel</tt>指定为命令行选项。 | ||
例如,要停止CPU,只需使用以下命令: | 例如,要停止CPU,只需使用以下命令: | ||
第51行: | 第51行: | ||
</source> | </source> | ||
=== | === 输出Operands === | ||
本节“输出Operands”部分用于告诉编译器/汇编程序如何处理存储ASM代码输出的C变量。 输出Operands是一组Pairs,每个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"> | ||
第74行: | 第74行: | ||
</source> | </source> | ||
这些标签位于它们自己的名称空间中,不会与任何C标识符冲突。 | 这些标签位于它们自己的名称空间中,不会与任何C标识符冲突。 对于输入Operands也可以这样做。 | ||
=== | === 输入Operands === | ||
虽然输出Operands通常只用于输出,输入操作数还允许参数化ASM代码; 将只读参数从C代码传递到ASM块。 同样,字符串文字用于指定详细信息。 | |||
如果您想将某个值移动到EAX,可以通过以下方式执行(即使这样做,而不是直接将该值映射到EAX肯定是毫无用处的): | |||
<source lang="c"> | <source lang="c"> | ||
第90行: | 第90行: | ||
</source> | </source> | ||
请注意,GCC将始终假定输入Operands是只读的(不更改的)。 写入输入Operands时,正确的做法是将它们列为输出,但不使用等式符号,因为这一次它们的原始值很重要。 下面是一个简单的例子: | |||
<source lang="c"> | <source lang="c"> | ||
asm("mov %%eax,%%ebx": : "a" (amount));//useless but it gets the idea | asm("mov %%eax,%%ebx": : "a" (amount));//useless but it gets the idea | ||
</source> | </source> | ||
Eax将包含“amount”,并移动到ebx中。 | |||
=== 删除寄存器列表 === | === 删除寄存器列表 === | ||
请记住一件事很重要:“C/C++编译器对汇编器一无所知”。 对于编译器来说,asm语句是不透明的,如果您没有指定任何输出,它甚至可能得出结论,认为这是一个不可操作的语句,并对其进行优化。 一些第三方文档指出,使用asm volatile将导致关键字不被移动。 然而,根据GCC文档,“volatile关键字表示指令有重要的副作用。 如果可以访问易失性asm,GCC将不会删除该asm。'',这只表示它不会被删除 (也就是说,它是否仍然可以移动是一个尚未回答的问题)。 一种可行的方法是使用asm(volatile)并将“内存”放入缓冲寄存器中,如下所示: | |||
<source lang="c"> | <source lang="c"> | ||
__asm__("cli": : :"memory"); // | __asm__("cli": : :"memory"); // 将导致语句不移动,但可能会将其优化。 | ||
__asm__ __volatile__("cli": : :"memory"); // | __asm__ __volatile__("cli": : :"memory"); // 将导致语句不会被移动或优化。 | ||
</source> | </source> | ||
第109行: | 第109行: | ||
=== 通配符:如何让编译器选择 === | === 通配符:如何让编译器选择 === | ||
您不需要告诉编译器在每个操作中应该使用哪个特定寄存器,一般来说 除非你有充分的理由特别喜欢一个寄存器, 你最好让编译器替你决定。 | |||
例如,强制在任何其他寄存器上使用EAX可能会迫使编译器代码报错,将以前在EAX中的内容保存在其他寄存器中,或者可能会在操作之间引入不必要的依赖关系(管道优化被破坏) | |||
通配符constraints允许您在输入/输出映射方面给予GCC更多自由: | |||
{| {{wikitable}} | {| {{wikitable}} | ||
|- | |- | ||
第125行: | 第125行: | ||
| tells the value '0x21' can be used as a constant in the out or in operation if ranging from 0 to 255 | | tells the value '0x21' can be used as a constant in the out or in operation if ranging from 0 to 255 | ||
|} | |} | ||
当然,您可以对operand选择施加更多约束,无论是否依赖于机器,这些constraints都列在GCC的手册中 (see [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], and [http://gcc.gnu.org/onlinedocs/gcc-4.4.4/gcc/Machine-Constraints.html#Machine-Constraints]). | |||
== 使用C99 == | == 使用C99 == | ||
<tt> | 当使用<tt>gcc-std=c99</tt>时,<tt>asm</tt>不是关键字。 只需使用<tt>gcc -std=gnu99</tt>即可使用带有GNU扩展的C99。 或者,您可以使用 <tt >__asm__</tt> 作为备用关键字,即使编译器严格遵守标准,该关键字也可以使用。 | ||
== 分配标签 == | == 分配标签 == | ||
第135行: | 第135行: | ||
<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'''(带前导下划线)。 | |||
== asm goto == | == asm goto == | ||
在GCC4. | 在GCC4.5之前,不支持跨内联汇编跳转。编译器无法跟踪正在发生的事情, | ||
因此,只能生成错误的代码。 | |||
<br>你可能会被告知“gotos是邪恶的”。 如果你相信这是真的,那么asm gotos就是你最可怕的噩梦。 | <br>你可能会被告知“gotos是邪恶的”。 如果你相信这是真的,那么asm gotos就是你最可怕的噩梦。 | ||
然而,它们确实提供了一些有趣的代码优化选项。 | 然而,它们确实提供了一些有趣的代码优化选项。 | ||
第167行: | 第167行: | ||
</source> | </source> | ||
CMPXCHG指令就是一个很有用的例子(参见[http://en.wikipedia.org/wiki/Compare-and- | 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)。 | 除了返回EAX中的当前值,CMPXCHG在成功时设置零标志(Z)。 如果没有ASM GOTOS,您的代码将必须检查返回值;可以按如下方式避免此CMP指令: | ||
<source lang="c"> | <source lang="c"> | ||
第220行: | 第219行: | ||
</source> | </source> | ||
== | == Intel语法 == | ||
通过在内联汇编中启用选项,您可以让GCC使用Intel语法,如下所示: | |||
<source lang="c"> | <source lang="c"> | ||
第235行: | 第234行: | ||
</source> | </source> | ||
通过这种方式,您可以将Intel语法和AT&T语法内联汇编结合起来。 | 通过这种方式,您可以将Intel语法和AT&T语法内联汇编结合起来。 请注意,一旦触发其中一种语法类型,源文件中命令下面的所有内容都将使用此语法进行汇编, 因此,在必要时不要忘记切换回来,否则可能会出现大量编译错误! | ||
还有一个命令行选项<tt>-masm=intel</tt> | 还有一个命令行选项<tt>-masm=intel</tt>,用于全局触发Intel语法。 | ||
== 另见 == | == 另见 == | ||
=== | ===文章=== | ||
* [[Inline Assembly/Examples]] - | * [[Inline Assembly/Examples]] - 有用且常用的函数 | ||
=== | === 论坛主题 === | ||
* [http://forum.osdev.org/viewtopic.php?f=11&t=24168&p=196655&hilit=asm+volatile+moved asm volatile being moved] | * [http://forum.osdev.org/viewtopic.php?f=11&t=24168&p=196655&hilit=asm+volatile+moved asm volatile being moved] | ||
=== | === 外部链接 === | ||
* [http://gcc.gnu.org/onlinedocs/ GCC Manuals] | * [http://gcc.gnu.org/onlinedocs/ GCC Manuals] | ||
* [http://web.archive.org/web/20041210030000/http://www-106.ibm.com/developerworks/library/l-ia.html Inline assembly for x86 in Linux (by IBM)] | * [http://web.archive.org/web/20041210030000/http://www-106.ibm.com/developerworks/library/l-ia.html Inline assembly for x86 in Linux (by IBM)] |
2022年2月22日 (二) 02:23的版本
“内联汇编”背后的思想是在除使用汇编语言之外别无选择的情况下,使用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等等,从输入/输出操作数部分中提到的第一个变量开始。 您需要使用此%%语法来帮助GCC区分寄存器和参数。
该操作的具体工作原理将在后面的章节中详细解释。 现在,下面这个例子可以先提供一个演示:
int a=10, b;
asm ("movl %1, %%eax;
movl %%eax, %0;"
:"=r"(b) /* output */
:"r"(a) /* input */
:"%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是一组Pairs,每个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开头的操作数,如果将寄存器用于存储输出操作数,则无需将其添加到已删除的寄存器列表中。 GCC足够聪明,能够自己决定做什么。
从GCC 3.1开始,您可以使用更可读的标签,而不是容易出错的枚举:
int current_task;
asm( "str %[output]"
: [output] "=r" (current_task)
);
这些标签位于它们自己的名称空间中,不会与任何C标识符冲突。 对于输入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));//useless but it gets the idea
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列表是一个以逗号分隔的寄存器名列表,以字符串文本形式显示。
通配符:如何让编译器选择
您不需要告诉编译器在每个操作中应该使用哪个特定寄存器,一般来说 除非你有充分的理由特别喜欢一个寄存器, 你最好让编译器替你决定。
例如,强制在任何其他寄存器上使用EAX可能会迫使编译器代码报错,将以前在EAX中的内容保存在其他寄存器中,或者可能会在操作之间引入不必要的依赖关系(管道优化被破坏)
通配符constraints允许您在输入/输出映射方面给予GCC更多自由:
The "g" constraint : "movl $0, %0" : "=g" (x)
|
x can be whatever the compiler prefers: a register, a memory reference. It could even be a literal constant in another context. |
The "r" constraint : "movl %%es, %0" : "=r" (x)
|
you want x to go through a register. If x wasn't optimized as a register, the compiler will then move it to the place it should be. This means that "movl %0, %%es" : : "r" (0x38) is enough to load a segment register.
|
The "N" constraint : "outl %0, %1" : : "a" (0xFE), "N" (0x21)
|
tells the value '0x21' can be used as a constant in the out or in operation if ranging from 0 to 255 |
当然,您可以对operand选择施加更多约束,无论是否依赖于机器,这些constraints都列在GCC的手册中 (see [1], [2], [3], and [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(带前导下划线)。
asm goto
在GCC4.5之前,不支持跨内联汇编跳转。编译器无法跟踪正在发生的事情,
因此,只能生成错误的代码。
你可能会被告知“gotos是邪恶的”。 如果你相信这是真的,那么asm gotos就是你最可怕的噩梦。
然而,它们确实提供了一些有趣的代码优化选项。
asm goto没有很好的文档记录,但其语法如下:
asm goto( "jmp %l[labelname]" : /* no outputs */ : /* inputs */ : "memory" /* clobbers */ : labelname /* any labels used */ );
CMPXCHG指令就是一个很有用的例子(参见[http://en.wikipedia.org/wiki/Compare-and-swap 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 - 有用且常用的函数