假设您有一个没有外部RAM的8051微控制器。内部RAM是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
答案 0 :(得分:2)
这是一台相当有限的机器。
好吧,最大的问题是你想使用“堆栈”来保存操作数,但它也包含返回地址。所以治愈方法:将返回地址移开挡路,并在完成后将其放回原位。
你的例子:
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',比如说),并且第三次为临时工作你有一些额外的空间(将它们的位置'顶部'分配给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
你几乎可以得到合理的代码。 (这很有趣)。