生成机器代码的“正确”/标准方法是使用优化编译器,该编译器通过内部表示(通常是SSA形式)进行转换,并且看起来很难进行各种优化。
解释器更容易编写,如果编写得好可以提供比低效/天真生成的 asm 更好的性能,因此没有标准的“简单”方式来生成 asm,因为没有人想要那样。(我猜,除了作为一个自学编译器的业余爱好项目。)
自己编写一个好的编译器需要几十年的工作。请参阅为什么 C 编译器这么少?,尤其是 Basile Starynkevitch 的回答。即使对于行为不如现代 x86-64 复杂的“简单”CPU 也是如此;优化多余的工作,决定何时内联函数等等,并不容易。
但是针对现代 x86-64 的优化范围从简单(乱序执行不太关心指令顺序)到晦涩难懂(例如,inc eax
与或部分标志失速)。或者,与英特尔 Sandybridge 系列 CPU 上的 2 个单独的 LEA / ADD 指令相比,3 组件 LEA 具有更高的延迟(但可能具有更好的吞吐量)。 另请参阅x86 标签 wiki 中的Agner Fog 优化指南和其他性能优化链接。 如果您要尝试进行优化,则只值得担心这一点。高效地做大量多余的工作并没有那么有用。add eax,1
要为一门新语言制作编译器,您只需编写一个生成 LLVM-IR 的前端,并将其提供给 LLVM 库,以供其优化和生成 asm 或机器代码。(你可以对 GIMPLE 做同样的事情,使用 gcc 的优化中间/后端而不是 LLVM)。作为奖励,您的编译器有望在 LLVM 或 gcc 支持的大多数 CPU 架构上工作,而不仅仅是 x86。
例如,请参阅使用 LLVM 实现语言教程。
天真地将每个表达式的每个部分分别音译成 asm 指令会产生缓慢而臃肿的 asm。clang -O0
可能与您10 + x + 4
从x + 14
. clang -O0
还增加了在每条语句之后将所有内容溢出到内存的负担,因此您可以在任何断点处使用调试器修改内存中的 C 变量。(这-O0
意味着:保证一致的调试,以及以最少的优化工作量快速编译。)
一个不关心这一点的天真的编译器可能会跟踪哪些值存在于哪个寄存器中,并在需要新寄存器时溢出旧值。如果您不提前考虑很快需要哪些值,而宁愿将这些值保存在寄存器中,这很容易变得很糟糕。
如果您不关心生成的 asm 的质量,那么当然,做任何方便的事情。
TinyCC 是一个一次性的 C 编译器。当它发出一个函数序言时,它还没有决定它需要保留多少字节的堆栈空间。(它会在到达函数末尾时返回并填充它。)请参阅Tiny C 编译器生成的代码会发出额外的(不必要的?)NOP 和 JMP,以了解其有趣的结果:anop
填充其函数序言的一个版本。
IDK 它在内部做了什么,但是大概当它遇到新的变量声明时,它会将它们附加到它将保留的堆栈帧的末尾(因此不会将偏移量更改rbp
为任何现有变量,因为它可能已经发出使用他们)。
TCC 是开源的,写得小/简单(编译速度快),不是为了创建好的 asm,所以你可能想看看它的源代码,看看它做了什么。