我有一个抽象语法树,我需要将其转换为虚拟机的程序集。我不知道如何最好地这样做,所以我开始使用一串字符串模板。我所说的伪代码示例,比如需要编译一个带有一个条件的简单if语句:
std::string compile_if(Node* n) {
std::string str = "";
curLabel = nLabels++;
str += compile_comparison(n->getChild(0));
str += ".true"+curLabel+":";
str += compile_block(n->getChild(1));
str += ".false"+curLabel+":";
return str;
}
其中每个compile_ *基于当前/下一个AST节点生成汇编字符串。然后最后一个字符串通过汇编程序运行。这看起来很草率,难以维护,这肯定不是大多数编译器所做的。这是一个坏主意,我应该改变吗?大多数其他编译器如何生成虚拟汇编代码/机器代码?
答案 0 :(得分:9)
免责声明:我只有X86机器代码的经验。其他指令集可能具有不同的寻址功能,因此部分建议可能不适用。对不起,我现在没有时间研究教学用语。
首先,大多数编译器都不会将汇编文本生成为文本,因为将代码序列化为汇编只是为了让汇编程序直接解析它是有点低效的,正如您可能已经意识到的那样。 是合理的,具有单独的编译和汇编阶段,但不是必需的。
在编制阶段,我要考虑的两个策略是:
(a)将程序集生成为指令对象的树/数组,它们可以象征性地相互引用。在汇编阶段,这些需要序列化为字节码/机器码。我推荐这种方法,即使它使你的编译器架构稍微复杂一点。
(b)将程序集作为机器码/字节码生成到缓冲区中,并带有一些辅助函数;在这种情况下,你真的没有单独的装配阶段。 我亲自尝试过这种方法,并且在一个功能的范围内它并不坏,但是可能会因为不知道功能在它组装之前会有多大而引起一些额外的困难。
我猜想(a)是优化编译器(如GCC)所使用的方法,而(b)是高速编译器(如TCC)使用的方法。
让我们再次考虑if
示例,检查现有编译器为简单if/else
分支生成的代码:
请注意反汇编中的重叠跳跃 - 跳过“'”阻止和跳过“未被采取”的阻止。块。
这些是相对跳转,因此为了组装它们,我们需要知道跳转指令和目标之间有多少字节的指令。
以下是使用策略编译功能的示例(a):
Instruction[] compile_if(IfNode n) {
Instruction[] code;
code ~= compile_condition(n.condition);
Instruction skip_taken = new JumpInstruction(`jz`);
code ~= skip_taken;
code ~= compile_block(n.taken_block);
Instruction skip_nottaken = new JumpInstruction(`jmp`);
code ~= skip_nottaken;
Instruction[] nottaken_code = compile_block(n.nottaken_block);
skip_taken.destination = nottaken_code[0];
code ~= nottaken_code;
Instruction end = new NopInstruction();
skip_nottaken.destination = end;
code ~= end;
return code;
};
这应该是不言自明的。
请注意指令如何以符号方式(skip_taken.destination = nottaken_code[0]
)相互引用,而不是像序列化机器码中的字节偏移。我们将这些偏移计算留给汇编程序。
另请注意我们如何设置JumpInstruction
的目的地,只有在它们可用时才会设置。
最后的NopInstruction
只是为skip_nottaken
跳转提供参考内容。
现在,我们如何将这些跳转组装成真正的机器码/字节码?这是一种可能性(一个非常基本的例子):
byte[2] assemble_jz(Instruction[] code, int idx) {
// assemble the jz instruction at code[idx]
JumpInstruction jump = code[idx];
++idx;
byte jump_offset = 0;
while (code[idx] != jump.destination) {
jump_offset += size_of_instruction(code[idx]);
++idx;
};
byte[2] machinecode = [
0x74, // jz short
jump_offset
];
return machinecode;
};
因为汇编程序可以访问所有指令对象,所以它可以通过向前扫描直到找到目标指令来计算相对跳转的实际偏移量。
我希望这个简短的介绍可以帮助您开始设计自己的编译器后端。显然,我并不是建议您编写与我的示例完全相同的编译器,但它应该为您提供一些如何处理编译和汇编非线性指令块的一般问题的想法。
您可能还想查看一些现有的汇编程序API,例如https://github.com/asmjit/asmjit。
祝你好运。