简单的c编译器(c语⾔实现),实现简易的C语⾔编译器
(part12)
挑椅子
这⼀部分,我们将基于之前创建好的抽象语法树为源代码⽣成具体的汇编语⾔代码。在这之前,我们先来看看下⾯这段源代码对应⽣成的汇编代码的内容:
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__(lf):
lf.curr_str = ""
def output(lf, op, left_reg, left_type, right_reg=None, right_type=None):
_str = f"\_str(left_type, right_type)} {_str(left_type)}"
if right_reg is not None:
if right_type is not None:
_str += f", {_str(right_type)}"
el:
_str += f", {_str(left_type)}"
lf.curr_str += _str + "\n"
这⾥的curr_str⽤来存储遍历过程中⽣成的汇编语⾔代码。只要获得当前操作对应的操作指令和操作数,就可以利⽤output函数⽣成具体的汇编代码。由于操作指令的后缀对应着操作数的⼤⼩,即操作指令具体的变种类型,就需要通过操作数具体的类型进⾏综合判断。例如,数据传送指令就有4个变种:传送字节的movb,传送字的movw和传送双字的 movl以及传送4字的 movq。
剩下的,就是仿照前⾯语义分析所⽤到的⽅法:定义节点函数。
服字组词8.1 函数定义
对于main和foo这样的函数,在定义的过程中,它们对应的节点函数如下:
def visit_FunctionDefn(lf, node):
lf.stack = RegistersManager(lf)
# head
葡萄柚精油
lf.output(Push, Rbp, PointerType())
lf.output(Mov, Rsp, PointerType(), Rbp)
# parameters and local variables
stack_frame_size = lf.calc_function_var_addrs(node.scope_symbol, -8)
lf.stack.t_ba_fp(stack_frame_size)
previous_str = lf.curr_str
lf.curr_str = ""
oppo铃声node.body.visit(lf)
function_str = lf.curr_str
lf.curr_str = previous_str
left_reg = ImmediateFreme(f"${-_max_fp()}")
lf.output(Sub, left_reg, PointerType(), Rsp)
# callee saves registers
lf.stack.save_callee_saves()
lf.curr_str += function_str
store_callee_saves()
if not _max_fp() == 0:
lf.output(Mov, Rbp, PointerType(), Rsp)
lf.output(Pop, Rbp, PointerType())
def calc_function_var_addrs(lf, scope_symbol, last_fp_loc):边城游侠
lf.calc_function_arg_addrs(scope_symbol)
return lf.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(lf, 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
el:
ve_free_pile_loc)
arg_index += 1
这⾥,我们⽤到了在语义分析中声明检查时得到的产物:作⽤域和变量表。具体地,对函数的前六个参数,我们使⽤对应的寄存器进⾏保存,避免了先转换为存储器,实际运算时可能再转换为寄存器的⿇烦,使⽣成的代码更加简洁。剩下的函数参数,我们则向上(栈帧地址增⼤的⽅向)开辟出具体的空间来存储它们。值得注意的是,这⾥我们并没有关⼼参数的具体类型,统⼀⽤4字来存储。
8.1.2 局部变量
线上小游戏
接着,我们处理函数体中的局部变量:
def calc_local_var_addrs(lf, scope_symbol, last_fp_loc):
"""calculate local variable address in function body"""
for symbol in scope_symbol._symbols.values():
if not symbol.is_ud:
continue
next_fp_loc = last_fp_loc - lf.calc_var_pe)
last_fp_loc = lf.calc_var_align(lf.calc_var_pe), next_fp_loc)
max_last_fp = last_fp_loc
# recursive calculate local variables inside the scope
for kid in scope_symbol.children:
curr_last_fp = lf.calc_local_var_addrs(kid, last_fp_loc)
if curr_last_fp < max_last_fp:
max_last_fp = curr_last_fp
max_last_fp = lf.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(lf, _type):
type_str = __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)
el:
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(lf, node):
lf.stack.save_caller_saves()
ver()
arg_num = len(des)
for arg in des:
arg_reg = lf.visit_and_pop(arg)
if arg_num > 6:
offt = (8 - arg_num) * FrameManager.WORD_SIZE
if not offt == 0:
right_reg = ImmediateFreme(f"{-offt}(%rsp)")
el:
right_reg = ImmediateFreme(f"(%rsp)")
el:
right_reg = Arg_regs[arg_num-1]
lf.output(Mov, arg_reg, pe, right_reg)
arg_num -= 1
ver()
lf.stack.done()
result_reg = lf.stack.push(preferred_reg=Rax)
if not result_reg == Rax:
lf.output(Mov, Rax, pe, result_reg)