具有多个操作数的表达式的编译器设计代码生成

时间:2011-02-25 11:59:54

标签: c compiler-construction code-generation cpu-registers

我正在编写C语言编译器。

我已经通过了语法和语义检查,我开始了代码生成阶段。

我必须生成的最终代码必须是3地址加载/存储架构。我被允许假设一组无限的寄存器和一个32 M的内存空间用于堆栈和系统内存。

现在,当我开始生成代码时,我首先假设一个非常大的int数组int R[2000000]来表示寄存器集。当我遇到变量声明时(通过解析器/语义分析器),我为该特定变量分配了一个寄存器。

现在在程序的过程中,当我再次遇到这个变量时,我从该寄存器号中取回它。我保存了符号表中每个变量的寄存器号。

我现在的问题是:假设我们有这样的陈述 -

a := b + c + e / f *h;

我已经分别在R1,R2,R3,R4,R5,R6中保存了a,b,c,e,f,h,最终生成的代码将是(假设下一个可用的regs从R7开始.. 。)

R9 = R5 * R6;
R8 = R4 / R9;
R7 = R3 + R8;
R1 = R2 + R7;

“记住”之前的注册和操作是什么方法?

如果这不是正确的方法,有人可以给我一些关于如何实现它的指示吗?

任何建议都会很棒。感谢

4 个答案:

答案 0 :(得分:3)

假设未绑定的寄存器设置感觉像“作弊”。在这种情况下,我猜你可以继续前进并选择下一个可用的寄存器。

在实际情况中,寄存器分配通常使用“图形着色”来完成。在简单的情况下,图形由表示变量(包括临时值)的节点和表示变量之间冲突的弧组成。目标是为每个变量分配一个颜色(表示寄存器编号),这样两个冲突的寄存器就不会有相同的颜色。 (从历史上看,这个问题类似于绘制世界各国地图的问题,因此使用了“颜色”一词。)

如果算法无法找到合适的颜色,则必须“溢出”某些变量,例如将它们放在堆栈上。

寄存器分配可以在代码生成之前或之后完成,每个都会产生不同的有趣问题。

您可能需要考虑的其他事情是调用约定,其中函数的参数和返回值必须放在特定的寄存器中。

答案 1 :(得分:1)

我建议你可以简单地枚举操作并使用它作为保存结果值的寄存器的数量。 如果使用树表示,则注册执行操作的数字是子节点数。 此外,您可以使用stack machine进行中间代码表示。

答案 2 :(得分:0)

这种表达通常用树来表示。每个节点对应一个操作,每个节点包含保存结果的寄存器的索引。

答案 3 :(得分:0)

注册绑定应该是最后一步。

要为上面的表达式生成代码,应先将其转换为反向波兰语符号,如下所示:

a b c + e f / h * + :=

现在要生成代码,我们将经历此过程,每当遇到一个操作时,我们都会在生成代码的同时保持一堆中间结果。因此,让我们开始吧。进行直到第一个操作:

a b c +

因此,我们应该添加两个变量并存储立即结果。在x86中,只能在寄存器之间或寄存器与内存之间添加。因此,我们必须将b加载到寄存器中并向其中添加c

MOV R1, [&b]
ADD R1, [&c]

这时我们不知道要使用哪个寄存器,所以我用R1标记了它。 现在我们的堆栈是:

a R1

继续并添加接下来的几个元素,直到进行下一个操作为止。

a R1 e f /

在x86除法中比较棘手,将红利存储在EDX:EAX 64位对中,商将在EAX中。假设您正在使用带符号的32位整数,我们必须将e加载到EAX中并将其符号扩展到EDX,然后执行除法。因此,需要生成以下代码:

MOV EAX, [&e]
CDQ
IDIV [&f]

幸运的是,EDX和EAX都是免费的,因此我们可以使用它。如果它们已经被使用,则需要将它们的值保存在另一个寄存器中以释放它们,我们现在不需要这样做。

结果以EAX表示。现在我们的堆栈:

a R1 EAX

继续前进:

a R1 EAX h *

第一个操作数是一个寄存器,因此我们不需要加载任何内容,只需立即进行乘法运算即可:

IMUL EAX, [&h]

结果在EAX中,因此现在是堆栈:

a R1 EAX

继续:

a R1 EAX +

重新获得寄存器来注册操作,只需一条指令即可完成

ADD R1, EAX

结果在R1中。现在的堆栈:

a R1

最后是作业:

MOV [&a], R1

因此,几乎最终的代码将是:

MOV R1, [&b]
ADD R1, [&c]
MOV EAX, [&e]
CDQ
IDIV [&f]
IMUL EAX, [&h]
ADD R1, EAX
MOV [&a], R1

现在该替换R1了。可以是哪个寄存器? 主要规则是寄存器寿命不得重叠。 寄存器的生命周期从第一个完整覆盖(如MOV)开始,到下一个完整覆盖(如果有)之前的最后一次使用结束。

MOV R1, [&b]  ; R1 lifetime starts
ADD R1, [&c]
MOV EAX, [&e] ; EAX lifetime starts.
CDQ           ; EDX lifetime starts
IDIV [&f]     ; EDX lifetime ends.
IMUL EAX, [&h]
ADD R1, EAX   ; EAX lifetime ends.
MOV [&a], R1  ; R1 lifetime ends

在我们的情况下,R1的寿命与EAX和EDX重叠。因此我们不能将它们用于R1,但​​是EBX尚未使用,因此我们可以使用它:

MOV EBX, [&b]
ADD EBX, [&c]
MOV EAX, [&e] 
CDQ           
IDIV [&f]     
IMUL EAX, [&h] 
ADD EBX, EAX  
MOV [&a], EBX

然后所有寄存器都释放了。

这将是您表达的代码。可以看出,每个变量仅加载一次。因此,在此特定示例中,将它们存储在寄存器中并没有太大好处。但是,如果您的函数有多个语句(当然也可以),则在生成整个函数的代码后仍然可以找到空闲的变量时,应将最常用的变量加载到寄存器中。

得出的结论是,您应该为每个可能的方案制定一个计划,在这里我们看到了变量到变量的方案,但是有时您会遇到诸如4 * a之类的常量,您可以在其中加载a,即向左移动而不是乘以< / p>