问题
Say you have an 8051 microcontroller with no external RAM. Internal RAM is 128 bytes, and you have around 80 bytes available. And you want to write a compiler for a stack language.
Say you want to compile an RPN expression 2 3 +
. 8051 has native push
and pop
instructions, so you can write
push #2
push #3
Then you can implement +
as:
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
Simple, right? But in this case +
is implemented as an inline assembly. What if you want to reuse this code, and put it into a subroutine? Fortunately, 8051 has lcall
and ret
instructions. lcall LABEL
pushes the return address onto the stack and jumps to the LABEL, while ret
returns to the address specified at the top of the stack. However, these operations interfere with our stack, so if we do lcall
to jump to our implementation of +
the first instruction pop A
will pop the return address, instead of the value that we want to operate on.
In a language where we knew the number of arguments for each function in advance we could rearrange the few values on top of the stack and put the arguments on top of the stack, and push the return address further down. But for a stack-based language we don't know how many arguments each function will take.
So, what are the approaches one can take to implement function calls in these circumstances?
Here's the 8051 instruction set description: http://sites.fas.harvard.edu/~phys123/8051_refs/8051_instruc_set_ref.pdf
回答1:
This is a pretty limited machine.
OK, the biggest problem is that you want to use the "stack" to hold operands, but it also holds return addresses. So the cure: move the return address out of the way when it is in the way, and put it back when done.
Your example:
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
My guess is that the "save return address" , "restore return address" are going to be pretty common. I don't know how to space-optimize the "save return address", but you could make the tail end of most subroutines common:
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
However, much of your trouble seems to be the insistence that you are going to push operands onto the stack. Then you have trouble with return addresses. Your compiler can certainly handle that, but the fact that you are having trouble suggests you should do something else, e.g., don't put the operands on the stack if you can help it.
Instead, your compiler could also generate register-oriented code, trying to keep operands in registers whenever possible. After all, you have 8 (I think) R0..R7 and A and B as easily accessible.
So what you should do, is to first figure out what all the operands (both named by the original programmer, and temporaries the compiler needs [say for 3-address code] and operations are in your code. Second, apply some kind of register allocation (look up register coloring for a nice example) to determine which operands will be in R0..R7, apply the same technique to allocate the named variables not assigned to registers to your directly addressable (assign them to locations 8-'top', say), and a third time for temporaries for which you have some additional space (assign them locations 'top' to 64). THis forces the the rest into the stack, as they are generated, having locations 65 to 127. (Frankly, I doubt you'll end up with many in the stack with this scheme unless your program is just too big for the 8051).
Once every operand has an assigned location, code generation is then easy. If an operand has been allocated in a register, either compute it using A, B and arithmetic as appropriate, or a MOV to fill or store it as the three address instruction indicates.
If the operand is on the stack, pop it into A or B if on top; you might to do some fancy addressing to reach its actual location if it is nested "deeply" in the stack. If the generated code is in the called subroutine and an operand is on the stack, use the return address saving trick; if R6 and R7 are busy, save the return address in another register bank. You likely only have to save the return at most once per subroutine.
If the stack consists of interleaved return addresses and variables, the compiler can actually compute where the desired variable is, and use complex indexing from the stack pointer to get to it. That will only happen if you address across multiple nested function calls; most C implementations don't allow this (GCC does). So you can outlaw this case, or decide to handle it depending on your ambition.
So for the program (C style)
byte X=2;
byte Y=3;
{ word Q=X*Y;
call W()
}
byte S;
W()
{ S=Q; }
we might assign (using the register allocation algorithm)
X to R1
Y to location 17
Q to the stack
S to R3
and generate code
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
You almost get reasonable code with this. (That was fun).
来源:https://stackoverflow.com/questions/25274153/implementing-function-calls-for-8051