0
votes

Suppose function f calls function g(a0, a1, a2, a3, a4, a5).

a0, a2, a4 are escape variables(they are pushed on frame so they can be accessed by inner function), and a1, a3, a5 are not(so they can be passed to MIPS' input registers.

In Appel's compiler book, a frame will be created whenever a function is called. Since there are three escape variables, the new stack will have three word-size cells above the frame pointer. But how am I suppose to know which input parameters should go to which cell in code generation phase?


My workaround:

Pass first four parameter in input register, and the rest of them on stack.

And when frame g is created, do a view shift: copy all input registers and stack memory to local variables. But I think function arguments should be at known offset from frame pointer. So i am not sure if this is even a workaround.


When f calls g, f has no idea about g's frame layout. So how can I implement function call in code generation phase?

UPDATE: I think my question should be rephrased to this: the caller passes a0, a1, a2, a3 to the input registers, and the rest on stack. The callee however, decided at compile time that a0, a2, a4 should be pushed on stack since they escape. So how can callee do a proper view shift before actually execute function body?

In type checking phase of my compiler, whenever it sees a function declaration. It will try to decide if any formal parameters escape or not (whether they will be accessed by any inner function). If it escape, it will be on stack, or it will be in a register. Sample code in OCaml:

  type access = 
    (* whether it will be in frame or in a register *)
    | In_frame of offset
    | In_reg of Temp.temp

When a new frame needs to be created, the function name and a list of boolean variables will be passed into new_frame function:

  let gen_offset () = 
    let res = !loc * word_size in
    loc := !loc + 1; res


  let new_frame name escapes  = 
    loc := 0 ;
    let fs = List.map escapes
      ~f:(fun t -> if t then (In_frame (gen_offset ())) else In_reg (Temp.new_temp ()))
    in
    let l = List.length fs in
    {
      name    = name;
      length  = l;
      formals = fs;
      locals  = [];
    }
1

1 Answers

1
votes

There are two stack areas in this process and they are distinct. It seems [to me] that you're conflating the two into one, and that may be the source of the confusion.

(1) The area where a caller places arguments. This is where you pass arguments and it has nothing to do with the callee's stack frame (e.g. This is what f sets up before calling g)

(2) The stack frame of the callee. This is where the callee (e.g. g) sets up space for its function scoped, automatic variables. Caller does not touch this area. In other words, f has no knowledge of the layout, nor is it allowed to have any. See below.

A callee doesn't even have to set up a stack frame if it's a tail function, so there may not even be one. Callee may be a pure register function that only uses a0-a3, v0/v1, t0-t9 and, so, has no need of a stack frame at all.

While some functions, set up the $fp reg, many compilers can optimize use of $fp away and use offsets to $sp only. Setting up $fp is only really required if a function has a VLA, such as int dim = foo(); int arr[dim]; or uses alloca

Once the function is called, it "owns" both the argument area and its stack frame. It is free to do whatever it wants with either as long as boundaries aren't exceeded. More on this below.


Let's ignore passing arguments in registers for the moment and, for brevity, pretend we have push and pop instructions (e.g. macros)

The caller will do:

push    a5
push    a4
push    a3
push    a2
push    a1
push    a0

jal     g

addiu   $sp,$sp,24

The pushes set up the argument area. That is not part of the callee's (i.e. g's) stack frame.

Note that after the call, the caller removes the argument area with the addiu. It assumes nothing about any values remaining or not.

That's what I meant by the callee "owning" it. Callee can stuff whatever data it wants there (e.g. after using a5, callee is free to overwrite the cell in the argument area with something else if it so desires).


The callee will set its stack frame up on its own. Note, I may have an off-by-one error on the offsets here, but you'll get the idea:

push    $fp
move    $fp,$sp
subiu   $sp,$sp,8       # establish space for callee vars: c0 and c1

lw      $t0,4($fp)      # get a0
lw      $t1,8($fp)      # get a1
# ...

lw      $t6,-0($fp)     # get c0
lw      $t7,-4($fp)     # get c1

# do stuff ...

move    $sp,$fp
pop     $fp
jr      $ra

The caller does not stuff anything into callee's stack frame [as it hasn't been set up yet]. It puts the arguments in the argument area, which is above the callee stack frame.


Now, for the real mips ABI, the first four arguments get passed in registers $a0-$a3 and arguments a4 and a5 get pushed on the stack. This is the first part of your "workaround" but not the "view shift" stuff:

push    a5
push    a4
move    $a3,a3
move    $a2,a2
move    $a1,a1
move    $a0,a0

jal     g

addiu   $sp,$sp,8

The callee code for this gets adjusted similarly.