Compiler construction 8051函数调用的实现

Compiler construction 8051函数调用的实现,compiler-construction,compilation,8051,forth,Compiler Construction,Compilation,8051,Forth,假设您有一个没有外部RAM的8051微控制器。内部RAM为128字节,可用字节约为80字节。您需要为堆栈语言编写编译器 假设您要编译一个RPN表达式23+。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 resul

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

假设您要编译一个RPN表达式
23+
。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
将返回地址推到堆栈上并跳转到标签,而
ret
返回到堆栈顶部指定的地址。但是,这些操作会干扰堆栈,因此如果我们执行
lcall
跳转到
+
的实现,第一条指令
pop A
将弹出返回地址,而不是我们要操作的值

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

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


以下是8051指令集说明:

这是一台非常有限的机器

好的,最大的问题是您想要使用“堆栈”来保存操作数,但它也保存返回地址。因此,解决方法是:当回信地址遇到阻碍时,将其移开,完成后将其放回原处

你的例子是:

    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
我猜“保存返回地址”, “还原返回地址”将非常常见。我不知道如何对“save return address”进行空间优化,但您可以使大多数子例程的结尾变得通用:

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风格)

我们可以分配(使用寄存器分配算法)

并生成代码

 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
你几乎可以得到合理的代码。 (那很有趣)

 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