What I learned from doing the OpenRISC GCC port, defining the stack frame
This is a continuation on my notes of things I learned while working on the OpenRISC GCC backend port. The stack frame layout is very important to get right when implementing an architecture’s calling conventions. If not we may have a compiler that works fine with code it compiles but cannot interoperate with libraries produced by another compiler.
For me I figured this would be most difficult as I am horrible with off by one bugs. However, after learning the whole picture I was able to get it working.
In this post we will go over the two main stack frame concepts:
- Registers - The GCC internal and hard register numbers pointing into the stack
- Stack Layout - How memory layout of the stack is defined
Registers
Stack registers are cpu registers dedicated to point to different locations in
the stack. The content of these registers is updated during function epilogue
and prologue. In the above diagram we can see the pointed out as AP
, HFP
,
FP
and SP
.
Virtual Registers
GCC’s first glimpse of the stack.
These are created during the expand
and eliminated during vreg
pass.
By looking at these we cat understand the whole picture: Offsets, outgoing
arguments, incoming arguments etc.
The virtual registers are GCC’s canonical view of the stack frame. During the vregs
pass they will be replaced with architecture specific registers. See details on
this in my discussion on GCC important passes.
Macro | GCC | OpenRISC |
---|---|---|
VIRTUAL_INCOMING_ARGS_REGNUM |
Points to incoming arguments. ARG_POINTER_REGNUM + FIRST_PARM_OFFSET . |
default |
VIRTUAL_STACK_VARS_REGNUM |
Points to local variables. FRAME_POINTER_REGNUM + TARGET_STARTING_FRAME_OFFSET . |
default |
VIRTUAL_STACK_DYNAMIC_REGNUM |
STACK_POINTER_REGNUM + STACK_DYNAMIC_OFFSET . |
default |
VIRTUAL_OUTGOING_ARGS_REGNUM |
Points to outgoing arguments. STACK_POINTER_REGNUM + STACK_POINTER_OFFSET . |
default |
Real Registers (Sometimes)
The stack pointer will pretty much always be a real register that shows up in the final assembly. Other registers will be like virtuals and eliminated during some pass.
Macro | GCC | OpenRISC |
---|---|---|
STACK_POINTER_REGNUM |
The hard stack pointer register, not defined where it should point | Points to the last data on the current stack frame. i.e. 0(r1) points next function arg[0] |
FRAME_POINTER_REGNUM (FP) |
Points to automatic/local variable storage | Points to the first local variable. i.e. 0(FP) points to local variable[0]. |
HARD_FRAME_POINTER_REGNUM |
The hard frame pointer, not defined where it should point | Points to the same location as the previous functions SP. i.e. 0(r2) points to current function arg[0] |
ARG_POINTER_REGNUM |
Points to current function incoming arguments | For OpenRISC this is the same as HARD_FRAME_POINTER_REGNUM . |
Stack Layout
Stack layout defines how the stack frame is placed in memory.
Eliminations
Eliminations provide the rules for which registers can be eliminated by
replacing them with another register and a calculated offset. The offset is
calculated by looking at data collected by the TARGET_COMPUTE_FRAME_LAYOUT
macro function.
On OpenRISC we have defined these below. We allow the frame pointer and argument pointer to be eliminated. They will be replaced with either the stack pointer register or the hard frame pointer. In OpenRISC there is no argument pointer so it will always need to be eliminated. Also, the frame pointer is a placeholder, when elimination is done it will be eliminated.
Note GCC knows that at some optimization levels the hard frame pointer will be
omitted. In these cases HARD_FRAME_POINTER_REGNUM
will not selected as the
elimination target register. We don’t need to define any hard frame pointer
eliminations.
Macro | GCC | OpenRISC |
---|---|---|
ELIMINABLE_REGS |
Sets of registers from, to which we can eliminate by calculating the difference between them. | We eliminate Argument Pointer and Frame Pointer. |
INITIAL_ELIMINATION_OFFSET |
Function to compute the difference between eliminable registers. | See implementation below |
Example
or1k.h
#define ELIMINABLE_REGS \
{ { FRAME_POINTER_REGNUM, STACK_POINTER_REGNUM }, \
{ FRAME_POINTER_REGNUM, HARD_FRAME_POINTER_REGNUM }, \
{ ARG_POINTER_REGNUM, STACK_POINTER_REGNUM }, \
{ ARG_POINTER_REGNUM, HARD_FRAME_POINTER_REGNUM } }
#define INITIAL_ELIMINATION_OFFSET(FROM, TO, OFFSET) \
do { \
(OFFSET) = or1k_initial_elimination_offset ((FROM), (TO)); \
} while (0)
or1k.c
Stack Section Growth
Some sections of the stack frame may contain multiple variables, for example we may have multiple outgoing arguments or local variables. The order in which these are stored in memory is defined by these macros.
Note On OpenRISC the local variables definition changed during implementation from upwards to downwards. These are local only to the current function so does not impact calling conventions.
For a new port is recommended to define FRAME_GROWS_DOWNWARD
as 1
as it is
usually not critical to the target calling conventions and defining it also
enables the Stack Protector
feature. The stack protector can be turned on in gcc using -fstack-protector
,
during build ensure to --enable-libssp
which is enabled by default.
Macro | GCC | OpenRISC |
---|---|---|
STACK_GROWS_DOWNWARD |
Define true if new stack frames decrease towards memory address 0x0. | 1 |
FRAME_GROWS_DOWNWARD |
Define true if increasing local variables are at negative offset from FP. Define this to enable the GCC stack protector feature. | 1 |
ARGS_GROW_DOWNWARD |
Define true if increasing function arguments are at negative offset from AP for incoming args and SP for outgoing args. | 0 (default) |
Stack Section Offsets
Offsets may be required if an architecture has extra offsets between the different register pointers and the actual variable data. In OpenRISC we have no such offsets.
Macro | GCC | OpenRISC |
---|---|---|
STACK_POINTER_OFFSET |
See VIRTUAL_OUTGOING_ARGS_REGNUM |
0 |
FIRST_PARM_OFFSET |
See VIRTUAL_INCOMING_ARGS_REGNUM |
0 |
STACK_DYNAMIC_OFFSET |
See VIRTUAL_STACK_DYNAMIC_REGNUM |
0 |
TARGET_STARTING_FRAME_OFFSET |
See VIRTUAL_OUTGOING_ARGS_REGNUM |
0 |
Outgoing Arguments
When a function calls another function sometimes the arguments to that function will need to be stored to the stack before making the function call. For OpenRISC this is when we have more arguments than fit in argument registers or when we have variadic arguments. The outgoing arguments for all child functions need to be accounted for and the space will be allocated on the stack.
On some architectures outgoing arguments are pushed onto and popped off the stack. For OpenRISC we do not do this we simply, allocate the required memory in the prologue.
Macro | GCC | OpenRISC |
---|---|---|
ACCUMULATE_OUTGOING_ARGS |
If defined, don’t push args just store in crtl->outgoing_args_size . Our prologue should allocate this space relative to the SP (as per ARGS_GROW_DOWNWARD ). |
1 |
CUMULATIVE_ARGS |
A C type used for tracking args in the TARGET_FUNCTION_ARG_* macros. |
int |
INIT_CUMULATIVE_ARGS |
Initializes a newly created CUMULALTIVE_ARGS type. |
Sets the int variable to 0 |
TARGET_FUNCTION_ARG |
Return a reg RTX or Zero to indicate when to start to pass outgoing args on the stack. | See implementation |
FUNCTION_ARG_REGNO_P |
Returns true of the given register number is used for passing outgoing function arguments. | r3 to r8 are OK for arguments |
TARGET_FUNCTION_ARG_ADVANCE |
This is called during iterating through outgoing function args to account for the next function arg size. | See implementation |
Further Reading
These references were very helpful in getting our calling conventions right:
- Stack frame layout on x86-64 - some great diagrams and examples
- 14.8 Registers and Memory - gcc internals, stack layout virtual registers
- 18.9.5 Eliminating Frame Pointer and Arg Pointer - gcc internals
- 18.9.6 Passing Function Arguments on the Stack - gcc internals
- 18.9.7 Passing Arguments in Registers - gcc internals
- OpenRISC Frame Handling - embecosm, but I think the diagram has link register and previous FP in the wrong place