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.
