实现简易的C语言编译器(part 12)

        这一部分,我们将基于之前创建好的抽象语法树为源代码生成具体的汇编语言代码。在这之前,我们先来看看下面这段源代码对应生成的汇编代码的内容:

int foo(
                             _foo:
                                  push   %rbp                                     
                                  mov    %rsp,%rbp  
        int a,                    mov    %edi,-0x4(%rbp)  
        int b                     mov    %esi,-0x8(%rbp) 
)     
{                        
                                  mov    -0x4(%rbp),%edx                          
                                  mov    -0x8(%rbp),%eax                          
    return a + b;                 add    %edx,%eax   
                                  pop    %rbp
                                  retq
}                                             
 

int main()
{
                            _main:
                                  push   %rbp                                     
                                  mov    %rsp,%rbp                                     
    int a;                        sub    $0x10,%rsp 
    a = 1;                        movl   $0x1,-0x4(%rbp)
                                  mov    -0x4(%rbp),%eax 
    return foo(  
        a,                        mov    %eax,%edi 
        2                         mov    %0x2,%esi
    );                            callq  _foo
                                  leaveq                                          
                                  retq
}

这里使用的是OnlineGDB在线编译器,然后将源代码和汇编语言代码一一对应起来。每一行汇编代码的意义,大家可以参考汇编语言指令的详细介绍。接下来,将以左边的源代码代码为例,开始实现一个汇编语言生成器,生成右边的汇编代码。
        我们还是需要遍历这棵抽象语法树。为此,首先定义一个这样的类:

class CodeGenVisitor(NodeVisitor):
    def __init__(self):
        self.curr_str = ""

    def output(self, op, left_reg, left_type, right_reg=None, right_type=None):
        _str = f"\t{op.to_str(left_type, right_type)} {left_reg.to_str(left_type)}"
        if right_reg is not None:
            if right_type is not None:
                _str += f", {right_reg.to_str(right_type)}"
            else:
                _str += f", {right_reg.to_str(left_type)}"

        self.curr_str += _str + "\n"

这里的curr_str用来存储遍历过程中生成的汇编语言代码。只要获得当前操作对应的操作指令和操作数,就可以利用output函数生成具体的汇编代码。由于操作指令的后缀对应着操作数的大小,即操作指令具体的变种类型,就需要通过操作数具体的类型进行综合判断。例如,数据传送指令就有4个变种:传送字节的movb,传送字的movw和传送双字的 movl以及传送4字的 movq
        剩下的,就是仿照前面语义分析所用到的方法:定义节点函数。

8.1 函数定义

        对于mainfoo这样的函数,在定义的过程中,它们对应的节点函数如下:

    def visit_FunctionDefn(self, node):
        self.stack = RegistersManager(self)
        # head
        self.output(Push, Rbp, PointerType())
        self.output(Mov, Rsp, PointerType(), Rbp)
        # parameters and local variables
        stack_frame_size = self.calc_function_var_addrs(node.scope_symbol, -8)
        self.stack.set_base_fp(stack_frame_size)

        previous_str = self.curr_str
        self.curr_str = ""

        node.body.visit(self)

        function_str = self.curr_str
        self.curr_str = previous_str

        left_reg = ImmediateFreme(f"${-self.stack.get_max_fp()}")
        self.output(Sub, left_reg, PointerType(), Rsp)
        # callee saves registers
        self.stack.save_callee_saves()
        self.curr_str += function_str
        self.stack.restore_callee_saves()
        
        if not self.stack.get_max_fp() == 0:
            self.output(Mov, Rbp, PointerType(), Rsp)
        self.output(Pop, Rbp, PointerType())

    def calc_function_var_addrs(self, scope_symbol, last_fp_loc):
        self.calc_function_arg_addrs(scope_symbol)
        return self.calc_local_var_addrs(scope_symbol.children[0], last_fp_loc)

在函数体内部,我们首先初始化了一个操作数管理器。接下来的两句和最后的输出可以对应汇编语言代码中处理函数的“标准”格式,在进入函数和退出函数的时候,一般都是:

push   %rbp                                     
mov    %rsp,%rbp  
...
pop    %rbp

8.1.1 函数参数

        定义函数入口之后,我们首先处理函数的参数:

    def calc_function_arg_addrs(self, scope_symbol):
        Arg_regs = [Rdi, Rsi, Rdx, Rcx, R8, R9]

        arg_index = 0
        arg_size = 0
        for symbol in scope_symbol._symbols.values():
            if arg_index > 5:
                arg_size += FrameManager.WORD_SIZE
                symbol.compile_loc = MemoryFrame(f"(%rbp)", arg_size)
            else:
                symbol.compile_loc = Arg_regs[symbol.parms_index]
                self.stack.remove_free_reg(symbol.compile_loc)

            arg_index += 1

这里,我们用到了在语义分析中声明检查时得到的产物:作用域和变量表。具体地,对函数的前六个参数,我们使用对应的寄存器进行保存,避免了先转换为存储器,实际运算时可能再转换为寄存器的麻烦,使生成的代码更加简洁。剩下的函数参数,我们则向上(栈帧地址增大的方向)开辟出具体的空间来存储它们。值得注意的是,这里我们并没有关心参数的具体类型,统一用4字来存储。

8.1.2 局部变量

        接着,我们处理函数体中的局部变量:

    def calc_local_var_addrs(self, scope_symbol, last_fp_loc):
        """calculate local variable address in function body"""
        for symbol in scope_symbol._symbols.values():
            if not symbol.is_used:
                continue
          
            next_fp_loc = last_fp_loc - self.calc_var_size(symbol.type)
            last_fp_loc = self.calc_var_align(self.calc_var_size(symbol.type), next_fp_loc)
            symbol.compile_loc = MemoryFrame(f"(%rbp)", last_fp_loc + self.calc_var_size(symbol.type))

        max_last_fp = last_fp_loc
        # recursive calculate local variables inside the scope
        for kid in scope_symbol.children:
            curr_last_fp = self.calc_local_var_addrs(kid, last_fp_loc)
            if curr_last_fp < max_last_fp:
                max_last_fp = curr_last_fp

        max_last_fp = self.calc_var_align(FrameManager.WORD_SIZE, max_last_fp)

        return max_last_fp

同样地,函数体内部的变量我们都存储在了变量表中,并且保存在作用域中。在声明检查中,我们提到过,由于大括号对会引入新的作用域,因此,还需要遍历子一层作用域进行类似的操作。为了节约使用寄存器,我们会为每个使用过的变量向下(栈帧地址减小的方向)开辟出新的地址来存储它们。

8.1.2.1 数据对齐

        许多计算机系统都要求某种类型对象的地址必须是某个值(通常是2,4或8)的倍数。因此,我们需要使用last_fp_loc来动态跟踪最小地址的位置。这里,用到了几个辅助函数:

    @staticmethod
    def calc_var_size(self, _type):
        type_str = _type.get_string()
        if type_str == 'char':
            return FrameManager.CHAR_SIZE
        elif type_str == 'int':
            return FrameManager.INT_SIZE
        elif type_str == 'pointer':
            return FrameManager.WORD_SIZE

    @staticmethod
    def calc_var_align(align, next_fp_loc):
        bytes_overboard = (-next_fp_loc) % align
        if not bytes_overboard == 0:
            last_fp_loc = next_fp_loc - (align - bytes_overboard)
        else:
            last_fp_loc = next_fp_loc

        return last_fp_loc

calc_var_size用来计算变量对应的类型大小,决定分配多大的地址。而calc_var_align则用来进行数据对齐。这方面的知识大家可以查阅其它资料进行获得完全的认识,这里就忽略了。

8.1.3 其它

        得到的last_fp_loc指出了存储数据地址的范围。那么,在操作数管理器中,临时变量的区域我们就选择开辟在这之外,即向下(栈帧地址减小的方向)开辟存储区域。并将栈指针放置到当前位置。
        接下来,我们会访问函数体内部的其它节点,我们会在后面详细定义。在外部看来,当前函数就是被调用函数,因此,我们需要保存对应的被调用保存寄存器。这样,就完成了生成函数定义的汇编语言的实现。整个过程,其实就是按照上一部分,对栈帧结构实现的过程。

8.2 函数调用

        说完了函数定义的过程,我们看看函数调用如何处理:

    def visit_FunctionOp(self, node):
        self.stack.save_caller_saves()

        node.args.nodes.reverse()
        arg_num = len(node.args.nodes)
        for arg in node.args.nodes:
            arg_reg = self.visit_and_pop(arg)
            if arg_num > 6:
                offset = (8 - arg_num) * FrameManager.WORD_SIZE
                if not offset == 0:
                    right_reg = ImmediateFreme(f"{-offset}(%rsp)")
                else:
                    right_reg = ImmediateFreme(f"(%rsp)")
            else:
                right_reg = Arg_regs[arg_num-1]

            self.output(Mov, arg_reg, arg.type, right_reg)

            arg_num -= 1

        node.args.nodes.reverse()
        self.comment(f"callq {node.expr.symbol.compile_loc}")

        self.stack.done()
        result_reg = self.stack.push(preferred_reg=Rax)
        if not result_reg == Rax:
            self.output(Mov, Rax, node.type, result_reg)

        arg_stack_size = ((len(node.args.nodes) - 6) * WORD_SIZE)
        if arg_stack_size > 0:
            left_reg = ImmediateFreme(f"${arg_stack_size}")
            self.output(Add, left_reg, PointerType(), Rsp)

正如在栈帧结构中所说的那样,在调用函数时,调用者需要保存对应的寄存器,然后,再处理函数参数传递相关的工作。这里需要注意的是,由于函数参数是按照向上(往栈帧地址增大的方向)顺序排布的,为了方便起见,我们先将函数参数顺序翻转再进行操作。此外,按照规定,结果寄存器%rax用来存储返回值。因此,如果得到的最终寄存器不是%rax,则需要进行转换。函数调用结束之后,就需要移动栈指针,回收函数参数分配的地址空间。

8.3 四则运算

        函数体内的节点包含着具体的语句,我们首先从最基本的四则运算过程说起。定义赋值操作节点函数如下:

    def visit_BinOp(self, node):
        if node.op == '=':
            self.binop_assign(node)

    def binop_assign(self, node):
        node.left.visit(self)
        right_reg = self.visit_and_pop(node.right)
        left_reg = self.stack.pop()

        self.output(Mov, right_reg, node.type, left_reg)
        self.stack.done()

    def visit_and_pop(self, node):
        if node.is_const():
            return ImmediateFreme(f"${node.expr}")
        else:
            node.visit(self)
            return self.stack.pop()

在赋值运算操作过程中:

  • 首先访问赋值运算符左边节点,将对应的操作数存储在堆栈中。
  • 其次,访问赋值运算符右边节点。如果是立即数,则直接返回;否则访问节点得到具体的操作数。
  • 再从堆栈中得到存放的赋值运算符左边节点对应的操作数。
  • 输出具体的操作指令代码,释放所有当前使用的寄存器。

        看起来,似乎可以采用直接访问节点得到操作数的方法,完全不用堆栈先将左边节点存储起来,等右边节点访问结束才弹出节点对应的操作数。但是,不要忘了,我们这里定义的节点是广义的节点,一个节点本身又可能是一个双目操作符,采用堆栈结构可以严格保证操作数的对应关系,这是由遍历抽象语法树的过程所决定的。那么,左右两边的节点到底去访问什么呢?

    def visit_Id(self, node):
        if self.stack.last_is_memory():
            reg = self.stack.push()
            self.output(Mov, node.symbol.compile_loc, node.type, reg)
            return

        self.stack.push(node.symbol.compile_loc, is_reg=False)

通过访问最终的标志符,就得到了具体的操作数,因为我们在函数定义中已经给这些变量都分配了存储空间,或者说都是存储器引用。这里,我们使用了一点技巧来简化汇编代码:

class FrameManager:
    ...
    def last_is_memory(self):
        if self.is_empty():
            return False

        return self.stack[-1].is_memory()

由于在进行一个完整操作指令代码生成之前,会有很多节点访问到此,都会先存储在操作数管理器的堆栈中。如果当前堆栈里面已经存放了一个存储器,由于汇编语言规定,操作指令中的两个操作数不能同时为存储器,必须将额外的存储器先转换成寄存器再进行操作。因此,在这里,我们先进行这样的转换。
        有了赋值运算的操作流程,那么加减乘除也可以对应地实现:

    def visit_BinOp(self, node):
        ...
        if node.op in ('+', '-', '*', '/'):
            self.binop_arith(node)

    def binop_arith(self, node):
        binop_arith_instrs = {'+': Add, '-': Sub, '*': Mul, '/': Div}

        node.left.visit(self)
        right_reg = self.visit_and_pop(node.right)
        left_reg = self.stack.pop()

        self.output(binop_arith_instrs[node.op], right_reg, node.type, left_reg)
        self.stack.done()

        self.stack.push(left_reg)

唯一不同的是,需要将运算结果放到堆栈中,留作后面的运算使用。这样,由于进行了转换,这个结果就是寄存器,而不是存储器引用,方便了后面运算的调用,而不用每次运算的时候都进行一次存储器到寄存器的转换。

        将这之前我们实现的代码整理一下,便可以对开头那段源代码生成对应的汇编语言代码,结果如下:

    pushq %rbp
    movq %rsp, %rbp
    subq $8, %rsp
    addl %edi, %esi
    movl %esi, %eax
    movq %rbp, %rsp
    popq %rbp
                        
    pushq %rbp
    movq %rsp, %rbp
    subq $16, %rsp
    movl $1, -8(%rbp)
    movl $2, %esi
    movl -8(%rbp), %edi
    callq _foo
    movq %rbp, %rsp
    popq %rbp

看着还是比较清晰和简洁,但还有一些细节没有处理。此外,还有语句的实现,我们都会在下一部分继续研究。

实现简易的C语言编译器(part 0)
实现简易的C语言编译器(part 1)
实现简易的C语言编译器(part 2)
实现简易的C语言编译器(part 3)
实现简易的C语言编译器(part 4)
实现简易的C语言编译器(part 5)
实现简易的C语言编译器(part 6)
实现简易的C语言编译器(part 7)
实现简易的C语言编译器(part 8)
实现简易的C语言编译器(part 9)
实现简易的C语言编译器(part 10)
实现简易的C语言编译器(part 11)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容