编译器设计中的内联汇编

3 compiler-construction parsing language-design x86-64 inline-assembly

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

int main() {
   __asm {
       mov rcx, rsp
       call func
   }
}
Run Code Online (Sandbox Code Playgroud)

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

Pet*_*des 5

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

但是,如果您没有在您关心的所有目标 ISA 中掌握未来的指令集扩展,那将不会很好地工作。


我真的建议使用GNU C 的扩展内联 asm 语法,其中操作数约束向编译器描述 asm 模板字符串。编译器本身并没有去了解它的一切,刚刚替补串到它像printf寻找%conversion。(请参阅“asm”、“__asm”和“__asm__”之间的区别是什么?

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

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

这也使您可以编写诸如asm("blsi %1, %0" : "=r"(dst) : "r"(src) )编译器选择实际使用哪些寄存器之类的东西。(仅输出寄存器操作数,仅输入寄存器操作数)。这让编译器尽可能高效地围绕黑盒(asm 语句)进行寄存器分配。它可以为输入和输出选择相同的寄存器,或者不方便,因为源没有使用“早期破坏”("=&r"),因此它可以假设在写入任何输出之前读取所有输入。

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


您展示的 MSVC 样式语法必须解析块以检测损坏的寄存器,并提及 var 名称。那要困难得多。

现代 clang 确实支持asm{}带有命令行选项的块,但它很难有效地使用(就像在 MSVC 中一样);它们不能用寄存器代替变量名,因此输入/输出必须通过内存反弹。

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

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