wiki:Internals/StackFrames

Stack frames on x86 ports

When all of a function's arguments fit into argument registers, building a stack frame is very simple (cf. vinsn save-lisp-context-no-stack-args):

(push (% ebp))
(mov (% esp) (% ebp))

After this, the stack looks like:

4(%ebp) tagged return address
0(%ebp) saved ebp
-4(%ebp) locals
.
.
.
0(%esp) top of stack

When some arguments must be pushed, it's a little more complicated. The caller first needs to reserve space for a stack frame, and then push any args. Say there's a function FOO that takes five args. In FOO's caller, we see something like:

(push ($ reserved-frame-marker))   ;could be NIL or 0 or some other GC-safe thing
(push ($ reserved-frame-marker))
(push <1st arg>)
(push <2nd arg>)
(push <3rd arg>)
(movl <4th arg> (% arg_y))
(movl <5th arg> (% arg_z))
(set-nargs 5)
(movl (@ 'FOO (% fn)) (% temp0))
(:talign 5)  ;align return address
(call (@ symbol.fcell (% temp0)))

Thus, on entry to FOO, the stack looks like this:

+ 20 reserved-frame-marker
+ 16 reserved-frame-marker
+ 12 arg1
+ 8 arg2
+ 4 arg3
0(%esp) tagged return address

We now have to do a bit of shuffling to put the saved frame pointer and the return address into the reserved places on the stack. The vinsn save-lisp-context-variable-arg-count does that with code like this:

(movl (% ebp) (@ 16 (% esp))  ;save old frame pointer
(leal (@ 16 (% esp)) (% ebp)) ;set new frame pointer
(popl (@ 4 (% ebp))           ;relocate return address

Now, the stack looks like this:

4(%ebp) return address
0(%ebp) saved ebp
-4(%ebp) 1st stack arg
-8(%ebp 2nd stack arg
-12(%ebp) 3rd stack arg

There are some cases where a function might or might not have received any args on the stack and has to decide at runtime how to build its stack frame.

Returning a single value is a matter of leave/ret, regardless of how the frame was constructed.

Tail calling

When tail calling a function that gets some args on the stack, we copy the stack args to the location just below the current function's saved frame pointer, push the current funtion's return address, and set the frame pointer to the saved frame pointer. We then jump to the new function (which can then build a stack frame as described above).

Stack layout in caller, before sliding args:

return address
0(%ebp) saved ebp
local junk on the stack
reserved-frame-marker
reserved-frame-marker
arg1
arg2
0(%esp) arg3

After sliding:

return address
saved ebp
arg1
arg2
arg3
0(%esp) return address

At this point, %ebp contains the saved %ebp.

The return address and saved ebp above the stack args are in the place where the reserved-frame-marker would be expected if we weren't recycling the stack frame for a tail call. These locations will be overwritten when the called function builds a stack frame (via the save-lisp-context-variable-args vinsn).

When there aren't any args being passed on the stack (that is, all the args fit in registers), we don't have to do any copying; we just unlink the frame pointer with LEAVE, and jump to the new function.