编译器设计中的内联汇编

Inline assembly in compiler design

我正在为自己的类 C 语言 (x86-64) 创建自己的编译器。但是我很困惑如何编译另一种语言的片段,即 x86-64 程序集,例如:

int main() {
   __asm {
       mov rcx, rsp
       call func
   }
}

一旦遇到 __asm,它必须以某种方式将标记更改为汇编标记,例如,如果我在 __asm 块之外有一个名为 rcx 的变量怎么办?将其合并到类似 C 的编译器设计中的好方法是什么?你将如何标记它并以一种将它与类 C 代码分开的方式解析它? __asm 块将首先在解析器级别上被识别,但如果不对其进行标记化,您将无法达到该级别....

一个选择是做现代 MSVC 所做的事情,并为每条指令提供内在函数,包括像 invlpg 这样的特权指令。 (因为 MSVC 不支持 32 位 x86 以外的目标的内联汇编)。这就是 MS 仍然能够使用它来开发 Windows 内核的原因。

不过,如果您不掌握您关心的所有目标 ISA 中未来的指令集扩展,那将无法正常工作。


我真的推荐使用 GNU C's Extended inline asm syntax where operand constraints describe the asm template string to the compiler. The compiler itself doesn't have to understand it at all, just substitute strings into it like printf looking for %conversion. (See What is the difference between 'asm', '__asm' and '__asm__'?)

正在访问的 C var 名称是使用不依赖于 asm 语法的固定语法指定的。此外,asm 在 "" 中作为 C 语法级别的字符串文字 ,因此像 ARM push {r4, lr} 这样的东西对于块作用域解析是不可见的.有关 GNU C 内联 asm 工作原理的更多文档/指南,请参阅 https://whosebug.com/tags/inline-assembly/info。另请注意,它的模板/操作数约束语法(几乎?)与 GCC 在其机器定义文件中内部使用的语法相同,这些文件教编译器针对不同目标提供可用指令。

这将问题抛给了编写所有 clobber 声明的程序员,以告诉编译器关于 call 到任意函数可以修改的每个寄存器,假设它遵循标准调用约定。

这还可以让您编写类似 asm("blsi %1, %0" : "=r"(dst) : "r"(src) ) 的内容,其中编译器选择实际使用的寄存器。 (仅输出寄存器操作数,仅输入寄存器操作数)。这让编译器尽可能高效地围绕黑盒(asm 语句)进行寄存器分配。它可以为输入和输出选择相同的寄存器,也可以不选择相同的寄存器,因为源没有使用 "early clobber" ("=&r"),所以它可以假设在任何输出之前读取所有输入写了。

它非常适合包装单条指令,但也可用于包装多条指令和访问指向内存,例如通过 "memory" 破坏。


您展示的 MSVC 风格的语法必须解析该块以检测被破坏的寄存器和 var 名称的提及。那更难了。

Modern clang 确实支持 asm{} 带有命令行选项的块,但使用效率很差(就像在 MSVC 中一样);它们无法用寄存器替换变量名,因此输入/输出必须通过内存来回弹。

MSVC 不支持除 32 位 x86 以外的目标的 asm 块,可能是因为它们用于处理 asm{} 的编译器内部结构如此混乱以至于对于具有寄存器 args 的函数来说是不安全的。这使得它无法用于现代调用约定。那不是语法问题,只是编译器技术债务问题。

但是在将数据输入/输出 asm{} 块时不可避免的低效率是一个语法/设计问题。不要犯与 MSVC 相同的错误。 或者如果您只想让用户提及 var 名称,请在您的文档中明确说明它们可以被寄存器或内存替换,如果你认为你可以让它在你的优化后端工作。