这一部分,我们将基于之前创建好的抽象语法树为源代码生成具体的汇编语言代码。在这之前,我们先来看看下面这段源代码对应生成的汇编代码的内容:
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 函数定义
对于main
和foo
这样的函数,在定义的过程中,它们对应的节点函数如下:
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)