§ 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 jmp
s to f
s 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 B
and vice versa.