4

假设您有一个没有外部 RAM 的 8051 微控制器。内部 RAM 为 128 字节,大约有 80 字节可用。并且您想为堆栈语言编写编译器。

假设您要编译一个 RPN 表达式2 3 +。8051有nativepushpop指令,所以可以写

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说明retlcall LABEL将返回地址压入堆栈并跳转到 LABEL,同时ret返回到堆栈顶部指定的地址。但是,这些操作会干扰我们的堆栈,因此如果我们lcall跳转到+第一条指令的实现,pop A则会弹出返回地址,而不是我们想要操作的值。

在我们预先知道每个函数的参数数量的语言中,我们可以重新排列堆栈顶部的少数值并将参数放在堆栈顶部,并将返回地址进一步向下推。但是对于基于堆栈的语言,我们不知道每个函数将接受多少个参数。

那么,在这些情况下可以采取哪些方法来实现函数调用呢?

这是 8051 指令集的说明:http ://sites.fas.harvard.edu/~phys123/8051_refs/8051_instruc_set_ref.pdf

4

1 回答 1

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-address code] 和操作在你的代码中。其次,应用某种寄存器分配(查找寄存器着色以获得一个很好的示例)以确定哪些操作数将在 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

你几乎可以得到合理的代码。(那很有趣)。

于 2014-08-13T00:56:39.750 回答