2017-02-24 63 views
2

我有一个抽象语法树,我需要将它转换为虚拟机的程序集。我不知道如何最好地做到这一点,所以我开始使用一串字符串模板。我的意思伪代码示例,说需要编译一个简单的if语句有一个条件:编译一个AST到程序集

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

”然后最后一个字符串通过汇编程序运行。“ - 它可能是也可能不是“大多数编译器”所做的,但这是一个完全有效的方法。尽管如此,我没有看到这与C++有什么关系,所以我删除了标签。 –

+0

你在问什么? – harold

+0

模板字符串 – Accumulator

回答

5

声明:我只有X86机器代码的经验。其他指令集可能具有不同的寻址能力,因此我的建议的部分内容可能不适用。对不起,我目前没有时间研究指令集。


那么首先,大多数编译器不会生成汇编文本,因为它有点低效的序列化的代码转换成汇编只有把它解析马上由汇编程序,你可能已经实现。它合理有单独的汇编汇编阶段,但不是必需的。

在编译阶段,这两种策略,我会考虑是:

  • 的(a)生成组件指令的树/对象数组可以象征性地指到彼此。在汇编阶段,这些需要被序列化为字节码/机器码。我推荐这种方法,即使它让你的编译器的架构更复杂一点。

  • (b)将程序集生成为机器码/字节码到缓冲区中,并带有一些辅助函数;在这种情况下,你实际上并没有单独的组装阶段。 我个人尝试过这种方法,并且在单个函数的范围内它并不坏,但是可能会因为在组装前不知道函数的大小而导致一些额外的困难。

我猜想的(a)是通过优化像GCC编译器所使用的方法,同时(b)中是通过高速的编译器等TCC使用的方法。


让我们再次通过检查现有的编译器生成一个简单的if/else分支代码考虑if例如:

source -> disassembly

注意重叠在拆卸跳跃 - 一个跳过“采取'块和一个跳过'未采取'块。

这些是相对跳转,所以为了组装它们,我们需要知道在跳转指令和目标之间有多少字节的指令。

这里是什么样的编辑功能,可以像使用策略(一)一个例子:

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

祝你好运。 “