Next: , Previous: Immediate values, Up: Standard macros


3.4.5 Implementing the ABI

Implementing the underlying architecture's abi is done in the macros that handle function prologs and epilogs and argument passing.

Let's look at the prologs and epilogs first. These are usually pretty simple and, what's more important, with constant content—that is, they always generate exactly the same instruction sequence. Here is an example:

               SPARC                        x86
               save %sp, -96, %sp           push %ebp
                                            push %ebx
                                            push %esi
                                            push %edi
                                            movl %esp, %ebp
               ...                          ...
               ret                          popl %edi
               restore                      popl %esi
                                            popl %ebx
                                            popl %ebp
                                            ret

The registers that are saved (%ebx, %esi, %edi) are mapped to the V0 through V2 registers in the gnu lightning instruction set.

Argument passing is more tricky. There are basically three cases1:

Register windows
Output registers are different from input registers—the prolog takes care of moving the caller's output registers to the callee's input registers. This is the case with the SPARC.
Passing parameters via registers
In this case, output registers are the same as input registers. The program must take care of saving input parameters somewhere (on the stack, or in non-argument registers). This is the case with the PowerPC.
All the parameters are passed on the stack
This case is by far the simplest and is the most common in CISC architectures, like the x86 and Motorola 68000.

In all cases, the port-specific header file will define two variable for private use—one to be used by the caller during the prepare/pusharg/finish sequence, one to be used by the callee, specifically in the jit_prolog and jit_arg macros.

Let's look again, this time with more detail, at each of the cases.

Register windows
jit_finish is the same as jit_calli, and is defined in core-common.h (see Common features supported by core-common.h).
          #define jit_prepare_i(numargs)  (_jitl.pusharg = _Ro(numargs))
          #define jit_pusharg_i(rs)       (--_jitl.pusharg,         \
                                           MOVrr((rs), _jitl.pusharg))
     

Remember that arguments pushing takes place in reverse order, thus giving a pre-decrement (rather than post-increment) in jit_pusharg_i.

Here is what happens on the callee's side:

          #define jit_arg_c()           (_jitl.getarg++)
          #define jit_getarg_c(rd, ofs) jit_extr_c_i  ((rd), (ofs))
          #define jit_prolog(numargs)   (SAVErir(JIT_SP, -96, JIT_SP), \
                                         _jitl.getarg = _Ri(0))
     

The jit_arg macros return nothing more than a register index, which is then used by the jit_getarg macros. jit_prolog resets the counter used by jit_arg to zero; the numargs parameter is not used. It is sufficient for jit_leaf to be a synonym for jit_prolog.

Passing parameter via registers
The code is almost the same as that for the register windows case, but with an additional complexity—jit_arg will transfer the argument from the input register to a non-argument register so that function calls will not clobber it. The prolog and epilog code can then become unbearably long, up to 20 instructions on the PPC; a common solution in this case is that of trampolines.

The prolog does nothing more than put the function's actual address in a caller-preserved register and then call the trampoline:

                 mflr    r0                 ! grab return address
                 movei   r10, trampo_2args  ! jump to trampoline
                 mtlr    r10
                 blrl
          here:  mflr    r31                ! r31 = address of epilog
                 ...actual code...
                 mtlr    r31                ! return to the trampoline
                 blr
     

In this case, jit_prolog does use its argument containing the number of parameters to pick the appropriate trampoline. Here, trampo_2args is the address of a trampoline designed for 2-argument functions.

The trampoline executes the prolog code, jumps to the contents of r10, and upon return from the subroutine it executes the epilog code.

All the parameters are passed on the stack
jit_pusharg uses a hardware push operation, which is commonly available on CISC machines (where this approach is most likely followed). Since the stack has to be cleaned up after the call, jit_prepare_i remembers how many parameters have been put there, and jit_finish adjusts the stack pointer after the call.
          #define jit_prepare_i(numargs) (_jitl.args += (numargs))
          #define jit_pusharg_i(rs)      PUSHLr(rs)
          #define jit_finish(sub)        (jit_calli((sub)),              \
                                         ADDLir(4 * _jitl.args, JIT_SP), \
                                         _jitl.numargs = 0)
     

Note the usage of += in jit_prepare_i. This is done so that one can defer the popping of the arguments that were saved on the stack (stack pollution). To do so, it is sufficient to use jit_calli instead of jit_finish in all but the last call.

On the caller's side, arg returns an offset relative to the frame pointer, and getarg loads the argument from the stack:

          #define jit_getarg_c(rd, ofs) jit_ldxi_c((rd), _EBP, (ofs));
          #define jit_arg_c()           ((_jitl.frame += sizeof(int) \
                                                      -  sizeof(int))
     

The _jitl.frame variable is initialized by jit_prolog with the displacement between the value of the frame pointer (%ebp) and the address of the first parameter.

These schemes are the most used, so core-common.h provides a way to employ them automatically. If you do not define the jit_getarg_c macro and its companions, core-common.h will presume that you intend to pass parameters through either the registers or the stack.

If you define JIT_FP, stack-based parameter passing will be employed and the jit_getarg macros will be defined like this:

     #define jit_getarg_c(reg, ofs)  jit_ldxi_c((reg), JIT_FP, (ofs));

In other words, the jit_arg macros (which are still to be defined by the platform-specific back-end) shall return an offset into the stack frame. On the other hand, if you don't define JIT_FP, register-based parameter passing will be employed and the jit_arg macros shall return a register number; in this case, jit_getarg will be implemented in terms of jit_extr and jit_movr operations:

     #define jit_getarg_c(reg, ofs)		jit_extr_c_i  ((reg), (ofs))
     #define jit_getarg_i(reg, ofs)		jit_movr_i    ((reg), (ofs))

Footnotes

[1] For speed and ease of implementation, gnu lightning does not currently support passing some of the parameters on the stack and some in registers.