§ Self modifying code for function calls: Look ma, I don't need a stack!
If one does not have recursive calls, one can eliminate the need to push
return addresses on a call stack by writing self-modifying code ---
I leant of this from TAOCP, volume 1.
Knuth shows this off once he introduces MIXAL, his fantasy
aseembly language in which TAOCP programs are written.
I'll explain the usual way one performs call-return, then explain the nifty
self-modifying-code way. I think this is the cleanest, most accessible
example of self-modifying-code that I know.
§ The traditional solution for call/ret
We wish to have function f call function g. For g to be able to
return control to f, f pushes a return address into the call stack,
that g pops and jmps to fs body. In code:
START_F:
...
L0 push addr(L0); location of instr to be run after call.
jmp START_G
L1:
=========
g's body
START_G:
...
retloc = pop ; pop location to jump to
RETG: jmp retloc
Rather than push ing and pop ing, we can rewrite the code of g, to
change retloc before a call to g. In made-up-pseudocode, here's what that
would look like:
§ The jump based solution
* f's body
START_F:
...
L0 store loc(RETG) assemble(jmp addr(L1))
jmp START_G
L1:
=========
* g's body
START_G:
...
RETG: <###-to-be-filled-dummy-###>
instead of having a call stack, before f calls g, f modify g's code at location RETG
into a jmp instruction by store ing the instruction jmp addr(L1).
This effectively creates a 'custom' g that knows how to return
control flow into f.
* g's body (after execution of L0)
START_G:
...
RETG: jump addr(L1)
This way, we have obviated the need for a push/pop sequence, by directly
modifying g's code. This is really neat --- we replace the overhead of
a push/pop with a single store.
§ Why recursion breaks.
We don't actually need a call stack, as long as we don't want to write recursive functions.
We can't have recursion, or more generally "re-entrance": consider a call chain of the form:
-
A -> B -> C -> B. - during
A -> B, A will scribble a into B. - during
B -> C, B will scribble a into C. - during
C -> B, C will scribble into B, destroying the previous . - This creates a cycle, where
C will attempt to return to Band vice versa.