如何在C代码中嵌入汇编代码?

16

我看过一些用于Arduino和其他硬件的代码,其中C语言中嵌入了汇编语句,大致如下:

asm("movl %ecx %eax"); /* moves the contents of ecx to eax */
__asm__("movb %bh (%eax)"); /*moves the byte from bh to the memory pointed by eax */

这实际上是如何运作的?我知道每个编译器都不同,但通常为什么要这样做,以及有哪些方法可以利用它呢?

4个回答

10
内联汇编代码直接嵌入完整的汇编代码中,且不经修改成为一个整体。当您需要绝对控制您的指令序列,或者不能让优化器对您的代码进行任何干预时,就需要这样做。也许您需要每个时钟周期。也许您需要让代码的每个分支使用相同数量的时钟周期,并填充NOP来实现这一点。
无论如何,有很多原因可能导致某人想要这样做,但是您真的需要知道您在做什么。这些代码块对您的编译器来说可能会非常难以理解,如果您正在进行错误操作,可能不会得到任何警告。

2
跟进“你真的需要知道你在做什么”的建议。提醒一下,你想要内联的确切汇编行并不总是被编译器完全采用。我曾经遇到过编译器进行微小更改的情况。编译后始终要仔细查看反汇编结果,确保它正在按照意图执行。 - BabaBooey
1
我相信如果你也添加了 volatile 说明符,例如 __asm__ volatile("instrs");,他们会感到非常荣幸。 - slugonamission
您还需要知道您的代码将在哪种处理器(系列)上运行,有时还需要了解内存模型。 ANSI-C 可以编译成几乎任何处理器和操作系统,但汇编指令是不可移植的 - Steve Barnes

6
通常,编译器会直接将汇编指令插入到生成的汇编输出中,而且不考虑后果。 例如,在这段代码中,优化器执行了复制传播操作,它看到y=x,然后z=y。所以它用z=x替换了z=y,希望这样可以进行更进一步的优化。然而,它没有注意到我在此期间改变了x的值。
char x=6;
char y,z;

y=x;                 // y becomes 6

_asm                    
    rrncf x, 1       // x becomes 3. Optimiser doesn't see this happen!
_endasm  

z=y;                 // z should become 6, but actually gets
                     // the value of x, which is 3

为了避免这种情况,你可以告诉优化器不要对该变量执行此优化。
volatile char x=6;   // Tell the compiler that this variable could change
                     // all by itself, and any time, and therefore don't
                     // optimise with it.
char y,z;

y=x;                 // y becomes 6

_asm                    
    rrncf x, 1       // x becomes 3. Optimiser doesn't see this happen!
_endasm  

z=y;                 // z correctly gets the value of y, which is 6

4
“volatile char y,z;”能否防止优化错误? - Scott Seidman
@ScottSeidman:是的,具体来说是 volatile char y。编译器会确保分配实际的 y 而不是优化后的“最近写入 x 然后写入 y 的值”。仅将 z 设为 volatile 是没有帮助的。 - SF.

5

历史上,C编译器生成汇编代码,然后由汇编器将其转换为机器码。内联汇编作为一项简单的功能而出现,在中间汇编代码中,在该点,注入一些用户选择的代码。某些编译器直接生成机器码,在这种情况下,它们包含一个汇编器或调用外部汇编器来生成内联汇编片段的机器码。

汇编代码的最常见用途是使用专门的处理器指令,而编译器无法生成这些指令。例如,对于关键部分禁用中断,控制处理器功能(缓存、MMU、MPU、电源管理、查询CPU能力等),访问协处理器和硬件外设(例如,在x86上使用inb/outb指令等)。很少会找到asm("movl %ecx %eax")这样的代码,因为它会影响C代码周围也在使用的通用寄存器,但像asm("mcr p15, 0, 0, c7, c10, 5")这样的代码有其用途(在ARM上的数据内存屏障)。 OSDev wiki网站有几个示例,其中包含代码片段。

汇编语言也有助于实现打破C流程控制模型的功能。一个常见的例子是在线程之间进行上下文切换(无论是协作式还是抢占式,无论是否在同一地址空间中),需要使用汇编代码保存和恢复寄存器值。
汇编语言还有助于手动优化内存或速度的小代码片段。随着编译器变得更加智能,这在应用程序级别上很少有意义,但在嵌入式领域仍然非常重要。
将汇编与C结合的方法有两种:使用内联汇编或通过链接汇编模块和C模块。链接可能更清晰,但并不总是适用:有时您需要在函数中间插入一条指令(例如,在上下文切换时保存寄存器,函数调用会覆盖寄存器),或者您不想支付函数调用的成本。
大多数C编译器都支持内联汇编,但语法有所不同。通常由关键字asm_asm__asm__asm__引入。除了汇编代码本身外,内联汇编结构还可以包含其他代码,允许您在汇编和C之间传递值(例如,请求将局部变量的值复制到进入时的寄存器),或声明汇编代码破坏或保留某些寄存器。

3

asm("")__asm__都是有效用法。基本上,如果关键字asm在您的程序中与某些内容冲突,可以使用__asm__。如果您有多个指令,则可以在双引号中每行编写一个指令,并在指令后缀一个'\n''\t'。这是因为gcc将每条指令作为字符串发送给as(GAS),通过使用换行符/制表符,您可以将正确格式的行发送到汇编器。您问题中的代码片段是基本内联

基本内联汇编中,只有指令。在扩展汇编中,您还可以指定操作数。它允许您指定输入寄存器、输出寄存器和一组被破坏的寄存器。不必指定要使用的寄存器,您可以将其留给GCC,并且那可能更适合于GCC的优化方案。扩展开头示例是:

__asm__ ("movl %eax, %ebx\n\t"
           "movl $56, %esi\n\t"
           "movl %ecx, $label(%edx,%ebx,$4)\n\t"
           "movb %ah, (%ebx)");

请注意,除了最后一行外,每行末尾都有'\n\t',并且每行都用引号括起来。这是因为gcc将每个指令作为字符串发送给as,就像我之前提到的那样。换行符/制表符组合是必需的,以便按正确格式将行馈送到as。

这与gcc有多大关系? - Scott Seidman
这是一个示例“as”汇编器,GNU C编译器用作后端。而且这个汇编器使用AT&T语法。 - gbudan
几个更正:1)如果您正在编译到ISO标准,则还需要使用__asm__而不是asm。2)虽然'\t'可能对某些汇编器来说是必需的,但对于其他汇编器来说,它只是使您的汇编输出更美观。3)您关于基本和扩展的描述是正确的,但是您在gcc中发布的示例是“基本”而不是“扩展”。请参阅https://gcc.gnu.org/onlinedocs/gcc/Using-Assembly-Language-with-C.html上的基本和扩展文档。 - David Wohlferd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接