Properties of the call conventions
On the PIC24, the function call conventions described in the MPLAB XC16 C Compiler User’s Guide are followed for all function calls generated by the C compiler.
Register usage
- Register
W15
is used as a stack pointer. This choice that is reflected in the various pop and push machine instructions. - Registers
W0
toW7
are caller saved. They are also used for parameters and return values as explained below. - Registers
W8
toW14
are callee saved. UsuallyW14
will be used as a frame pointer.
Parameter passing
If possible, function arguments are passed in registers
W0
to W7
.
8-bit and 16-bit parameters are passed in a single register,
32-bit parameters are passed in two registers,
and 64-bit parameters are passed in four registers.
The parameters are placed, in order, in the registers
W0
to W7
. If a parameter cannot fit
within the remaining registers, it is placed on the stack
in right-to-left order.
It is possible for the first and third arguments to be
passed in registers, while the second argument is
passed on the stack.
In C, an array is passed as a pointer to its first element. If space permits, a structure is passed in sequential registers, even if the structure contains arrays. Otherwise the address of the structure is passed.
C also permits variadic functions, such as
printf
, which have a variable number of arguments.
Effectively these functions receive a list of parameters
as its last argument (written as ...
in the function
header). This variably-size parameter list, which
always includes the last parameter before the ...
,
is never placed in registers.
An example
struct sillyStruct { int8_t buff[80] } ; int16_t f(int8_t a, int32_t b, struct sillyStruct c, int32_t d[80], int16_t e) ;
parameter | location |
---|---|
a |
W0 |
b |
W2:W1 |
c |
on stack (offset -86) |
d |
W3 (as address) |
e |
W4 |
Return value
The simpler return values, such as numbers, are returned in
registers W0
to W3
as needed.
A 16-bit value, such as an uint16_t
, will be returned
in W0
. A 64-bit value, such as a double
,
will use all four registers.
When an aggregate value, such as a struct
or an union
, is returned from a function;
W0
will contain the address of the returned value.
Stack frame
When programs are compiled under the XC16 compiler, the default
action for function invocation is to allocate a stack frame.
The frame pointer, the
address of the base of the stack frame, is contained in W14
.
Since W14
is a callee saved, there is no problem with
calling routines that don’t allocate a stack frame.
Here is the order for storing information on the stack.
purpose | size in bytes |
---|---|
Parameters that couldn’t fit in registers | varies, but typically 0 |
saved PC , return address |
4 |
saved R14 , dynamic link |
2 |
local variables, saved registers, arguments to other procedures | varies |
By definition, the stack frame begins at the address contained in
the frame pointer. On the PIC24 with the XC compilers, this is the location
of the first local variable.
This means that local variables and saved registers are typically addressed
as positive offsets from R14
, such as [R14+2]
.
If any parameters have been passed on the stack, they will
be addressed as negative offsets, such as such as [R14-8]
.
For a small gain in efficiency, it is possible to avoid allocating a stack frame. In this case variables are accessed at negative offsets from the stack pointer.
Actions of the called function
Function prologue
On entry to the function, all of its arguments are either contained in registers or stored near the top of the stack. At the very top of the stack is the two-word return address.
When a stack frame is being used, the first instruction of a
function is
LNK #n
where n
is the amount of storage needed for local variables and saved registers.
The LNK #n
instruction performs the following actions:
- Pushes
W14
, the present frame pointer, onto the stack. This now becomes the dynamic link. - Sets
W14
to the address of the top of the stack. This now becomes the bottom of the new stack frame. - Increases the stack pointer,
W15
, byn
. This creates space for local variables and saved registers within the stack frame.
The other action usually performed during the function prologue
is saving registers. If the function modifies any callee-saved registers,
W8
to W13
, they must be saved.
Also, the function usually needs to save
any caller-saved registers used
to pass parameters in its own function calls.
Otherwise, these parameters will be lost.
Inside the function
All local variables stored in the stack frame
will be accessed by positive offsets of W14
, the frame pointer.
Most parameters will be accessed in the registers in which they were
passed, but those that are stored on the stack will be accessed
by negative offsets of the frame pointer.
Function epilogue
Before the function exits, any modified callee-saved registers must be restored and return values must be placed in appropriate registers.
Then the function calls the ULNK
instruction
which does the following:
- Copies the frame pointer,
W14
, to the stack pointer,W15
. This deallocates the present stack frame. - Pops the top of the stack, which now contains the dynamic link, into the frame pointer. This restores the stack frame of the caller function.
The only remaining action of the epilogue is to call the
RETURN
instruction which pops the two words containing
the return address into the program counter. This causes
control to transfer back to the calling function.
Actions of the calling function
Function invocation
Before the function is called, all its arguments must be evaluated
and placed in the appropriate registers or pushed on the stack.
Now a call instruction, typically RCALL
, will be made.
The call instruction will push the present program counter, the address
of the next instruction, onto the stack and then set the program counter
to its argument.
The function will also need to save copies of any caller-saved registers it needs after the call is completed.
Function return
If any parameters were passed on the stack, the calling function should adjust the stack pointer to “remove” them. Then, the calling function may also copy the return value to its own local storage. Finally, the calling function may need to restore any caller-saved registers it was using.
An example
Let’s look at an example, an inefficient recursive function for squaring a positive number.
int square(int n) { int r = 0 ; if (n != 0) { r = square(n-1) + n + n - 1 ; } return r ; }
Implemented on the PIC
Here’s the code generated by the XC16 compiler at optimization level 0, with a few comments added by me.
LNK #0x4 MOV W0, [W14+2] ;; n is saved is [W14+2] CLR W0 MOV W0, [W14] ;; r is saved in [W14] MOV [W14+2], W0 ;; testing if n == 0 SUB W0, #0x0, [W15] BRA Z, 1f MOV [W14+2], W0 DEC W0, W0 RCALL square ;; calling square with n-1 MOV [W14+2], W1 ADD W0, W1, W1 MOV [W14+2], W0 ADD W1, W0, W0 DEC W0, [W14] 1: MOV [W14], W0 ULNK RETURN
This version is a shorter.
;; start of prologue LNK #2 MOV W0, [W14] ;; n is saved is [W14] ;; end of prologue CLR W7 ;; r is saved in W7 CP0 W0 ;; testing if n == 0 BRA Z, 1f ;; start of invocation DEC W0, W0 ;; Setting first parameter to n-1 RCALL square ;; calling square with n-1 ;; end of invocation ;; On return, W0 is square(n-1) MOV [W14], W6 ;; must restore the old n (as W6) ADD W0, W6, W7 ;; W7 == square(n-1) + n ADD W7, W6, W7 ;; W7 == square(n-1) + n + n DEC W7, W7 ;; W7 == square(n-1) + n + n - 1 ;; start of epilogue 1: MOV W7, W0 ULNK RETURN ;; end of epilogue
Implemented in Java
If you put the keyword static
in front of the
square
function header, you get a Java static method.
Here is the
JVM bytecode
for that method.
When this method is running,
local variable
0 is parameter n
and local variable 1 is
r
.
The operand
stack is shown to the right of the JVM instructions.
Also, square
is static method 2 of the class.
static int square(int); Code: 0: iconst_0 // [[ 0 1: istore_1 // [[ 2: iload_0 // [[ n 3: ifeq 19 // [[ 6: iload_0 // [[ n 7: iconst_1 // [[ n 1 8: isub // [[ n-1 9: invokestatic #2 // [[ sq(n-1) 12: iload_0 // [[ sq(n-1) n 13: iadd // [[ sq(n-1)+n 14: iload_0 // [[ sq(n-1)+n n 15: iadd // [[ sq(n-1)+n+n 16: iconst_1 // [[ sq(n-1)+n+n 1 17: isub // [[ sq(n-1)+n+n-1 18: istore_1 // [[ 19: iload_1 // [[ r 20: ireturn
Chapter 6 of the JVM specification contains a detailed description of the Java Virtual Machine instruction set.
Calling main
We do need some assembly language code to get a C program running.
Typical C startup with crt0
on a multi-user system
- Initialize the stack
- Initialize global variables
- Initialized variables are read from the executable
- Uninitialized global variables are cleared as required by the C standard
- Create links to shared libraries
- May be done by the operating system
- May be delayed until libraries are used
- Get ready to call
main
- Set up memory management for the heap
- Allocate space for the stack
- Create the initial stack frame
- Set up the parameters
- Call
main
- Return to operating system after the call
Implemented on the IBM/360
Mark Smotherman of Clemson University is an expert on the history of computer architecture. His page on IBM S/360 Subroutines describes a calling convention that does not use a stack. This dates from a time when recursive subroutines were considered just a tad too radical.
Startup for calling main
on the PIC
If the PIC assembly language programmer does not provide a
__reset
entry point, the following actions will
be performed to call main
:
- The stack pointer,
W15
, and stack limit file register,SPLIM
are initialized. - Read-only data, contained in program memory, is mapped into data memory.
- Unitialized data is cleared.
- Initialized data is copied from program memory into data memory.
main
is called.- If
main
returns, the processor is reset.
This list is a summary of a more detailed description in the MPLAB® XC16 Assember, Linker and Utilities User’s Guide.