Open tyalie opened 6 months ago
[!WARNING] This might be incomplete, if there are inconsistencies, falsehoods or other, please comment below
In order to keep compatibility (and keep the already high mental load on us to a minimum), we will implement the SDCC calling convention for STM8 version 1. (see SDCC 4.4.1 manual - Section 4.5.1.1). It will be described here in more detail and with examples whenever possible.
[!NOTE] First and foremost, SDCC strictly only considers the C calling convention, this is in opposition to LLVM which also supports calling conventions with multiple return values. As such we will not support the latter case for now.
The only registers (out of the three) that are often used are A
and X
. Y
is used to communicate return parameters or temporary values, but that's about it. Your guess is as good as mine, but I assume it's due to the fact that most mnemonics operating on Y have the 0x90
opcode-prefix and are as such a byte longer.
As STM8 has less than a handful of registers, this is a very easy answer. As far as we know, the caller is always responsible for saving their registers, whilst there are no callee saved register.
It is decided on whether arguments are passed on the stack, or using registers using the following flow chart
SDCC is very strictly following that graph
C Code | SDCC Assembly |
---|---|
void fn1(uint8_t a) { volatile uint8_t z = a; } |
; only register a is used _fn1: push a ld (0x01, sp), a pop a ret |
void fn2(uint8_t a, uint16_t b) { volatile uint8_t z = a + b; } |
; register a and x are used _fn2: push a pushw x add a, (2, sp) popw x ld (0x01, sp), a pop a ret |
void fn3(uint8_t a, uint8_t b) { volatile uint8_t z = a + b; } |
; register a is used for a, but b is on the stack _fn3: push a add a, (0x04, sp) ld (0x01, sp), a pop a popw x pop a jp (x) |
void fn4(uint32_t a) { volatile uint8_t z = a; } |
; everything is on the stack _fn4: push a ld a, (0x07, sp) ld (0x01, sp), a ldw x, (2, sp) addw sp, #7 jp (x) |
There is also the special case of function with variable arguments, these will have all the function arguments on the stack.
The SDCC manual states:
Return values are in a (8 bit), x (16 bit), xyl (24 bit), xy (32 bit) or use a hidden extra pointer parameter pointing to the location (anything wider than 32 bit, and all struct /union)
The hidden pointer parameter will always be transferred on the stack.
In the case that the return parameters are on the stack, it will look like this
C code | SDCC ASM |
---|---|
uint64_t fn() { return 0; } void caller() { fn(); } |
_fn: ldw x, (0x03, sp) ; get the pointer to the location ; where the return should be clrw y ldw (#6, x), y ldw (#4, x), y ldw (#2, x), y ldw (x), y ret _caller: sub sp, #8 ldw x, sp addw x, #1 ; SP points to the next free location, ; so SP + 1 is our data pushw x ; storing the pointer to the data on ; the stack [SP -= 2] call _fn addw sp, #10 ret |
Who does the stack cleanup for the parameters depends on how the callee looks like. In general the caller is responsible, except in the following cases:
C code | SDCC ASM code |
---|---|
void fn(uint32_t a) {} void caller() { fn(0); } |
_fn: ldw x, (1, sp) addw sp, #6 jp (x) _caller: clrw x pushw x clrw x pushw x call _fn ret |
uint8_t fn(uint32_t a) {} void caller() { fn(0); } |
_fn: ldw x, (1, sp) addw sp, #6 jp (x) _caller: clrw x pushw x clrw x pushw x call _fn ret |
uint32_t fn(uint32_t a) {} void caller() { fn(0); } |
_fn: ret _caller: clrw x pushw x clrw x pushw x call _fn addw sp, #4 ret |
float fn(float a) {} void caller() { fn(0); } |
_fn: pushw x ldw x, (3, sp) ldw (7, sp), x popw x addw sp, #4 ret _caller: clrw x pushw x clrw x pushw x call _fn ret |
Okay. At least the following functions need to be implemented according to LLVMs TargetLowering:
LowerReturn
LowerFormalArguments
LowerCall
Description from the source