假设您有一个没有外部RAM的8051微控制器。内部内存是128字节,你有大约80字节可用。你想为堆栈语言写一个编译器
假设你想编译一个RPN表达式2 3 +
。8051有原生的push
和pop
指令,所以你可以写
push #2
push #3
则可以将+
实现为:
pop A ; pop 2 into register A
pop B ; pop 3 into register B
add A, B ; A = A + B
push A ; push the result on the stack
简单,对吧?但是在这种情况下,+
是作为内联程序集实现的。如果您想重用这些代码,并将其放入子例程中,该怎么办?幸运的是,8051有lcall
和ret
指令。lcall LABEL
将返回地址压入堆栈并跳转到LABEL,而ret
返回到堆栈顶部指定的地址。然而,这些操作干扰了我们的堆栈,所以如果我们让lcall
跳转到+
的实现,pop A
的第一条指令将弹出返回地址,而不是我们想要操作的值。
在我们事先知道每个函数的实参数量的语言中,我们可以重新排列堆栈顶部的几个值,并将实参放在堆栈顶部,并将返回地址进一步下压。但是对于基于堆栈的语言,我们不知道每个函数将接受多少个参数。
那么,在这种情况下可以采取哪些方法来实现函数调用呢?
下面是8051指令集的描述:http://sites.fas.harvard.edu/~phys123/8051_refs/8051_instruc_set_ref.pdf
这是一台相当有限的机器。
好的,最大的问题是你想使用"堆栈"来保存操作数,但它也保存返回地址。因此,解决方法是:当返回地址碍事时,把它移开,完成后再放回去。
你的例子:
push #2
push #3
lcall my_add
...
myadd:
pop r6 ; save the return address
pop r7
pop a
pop b
add a, b
push a
push r7
push r8
ret
我猜"保存返回地址","恢复返回地址"会很常见。我不知道如何对"保存返回地址"进行空间优化,但是您可以使大多数子例程的尾部通用:
myadd:
pop r6 ; save the return address
pop r7
pop a
pop b
add a, b
jmp push_a_return
...
; compiler library of commonly used code:
push_ab_return: ; used by subroutines that return answer in AB
push b
push_a_return: ; used by subroutines that return answer in A
push a
return: ; used by subroutines that don't produce a result in register
push r7
push r6
ret
push_b_return: ; used by subroutines that compute answer in B
push b
jmpshort return
然而,大部分问题似乎是坚持要将操作数压入堆栈。那你就找不到回信地址了。你的编译器当然可以处理这个问题,但你遇到麻烦的事实建议你应该做其他事情,例如,如果你可以帮助它,不要把操作数放在堆栈上。
相反,编译器也可以生成面向寄存器的代码,尽可能地将操作数保留在寄存器中。毕竟,你有8个(我认为)R0..R7和A和B很容易访问。
所以你应该做的是,首先找出所有的操作数(由原始程序员命名的,以及编译器需要的临时数[比如3地址代码])和操作都在你的代码中。其次,应用某种寄存器分配(查找寄存器着色的一个很好的例子)来确定哪些操作数将在R0中。R7,应用相同的技术将未分配给寄存器的命名变量分配到您的直接可寻址(将它们分配到位置8-'top'),并且第三次为您有一些额外空间的临时变量(将它们的位置'top'分配到64)。这将迫使其余的元素在生成时进入堆栈,其位置为65到127。(坦率地说,除非你的程序对8051来说太大,否则我怀疑你最终会在堆栈中使用这种方案。)
一旦每个操作数都有一个指定的位置,代码生成就很容易了。如果在寄存器中分配了一个操作数,要么使用a、B和适当的算术运算来计算它,要么按照三个地址指令的指示用MOV来填充或存储它。
如果操作数位于栈顶,则将其放入A或B;如果它"深"嵌套在堆栈中,您可能需要做一些花哨的寻址才能到达它的实际位置。如果生成的代码在被调用的子例程中,并且操作数在堆栈上,则使用返回地址保存技巧;如果R6和R7忙,将返回地址保存到另一个寄存器库。每个子例程可能最多只保存一次返回值。
如果堆栈由交错的返回地址和变量组成,编译器实际上可以计算所需变量的位置,并从堆栈指针使用复杂索引来访问它。这只会发生在你跨越多个嵌套函数调用时;大多数C实现不允许这样做(GCC允许)。所以你可以宣布这个案子为非法,或者决定处理它取决于你的野心。
对于程序(C风格)
byte X=2;
byte Y=3;
{ word Q=X*Y;
call W()
}
byte S;
W()
{ S=Q; }
(使用寄存器分配算法)
X to R1
Y to location 17
Q to the stack
S to R3
和生成代码
MOV R1,2
MOV A, 3
MOV 17, A
MOV A, 17
MOV B, A
MOV A, R1
MUL
PUSH A ; Q lives on the stack
PUSH B
CALL W
POP A ; Q no longer needed
POP B
...
W:
POP R6
POP R7
POP A
POP B
MOV R3, B
JMP PUSH_AB_RETURN
你几乎可以得到合理的代码。