compiler-construction – 为8051实现函数调用

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

假设您要编译RPN表达式2 3. 8051具有原生的推送和弹出指令,因此您可以编写

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’ ,比如说),并且第三次为你有一些额外空间的临时工具(将它们的位置’顶部’分配给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

你几乎得到了合理的代码.
(那很有趣).

点赞