Inside SDCC: Part 1 – Mastering the New Z80 ABI
For the Benefit of Mr. Kite
As of SDCC 4.2.0+, the Z80 ABI (Application Binary Interface) has changed. The new calling convention improves performance by passing the first few function arguments in registers rather than on the stack, significantly reducing overhead in small or frequently called functions.
I discovered this the hard way when I recompiled my old Iskra Delta Partner code with SDCC 4.2.0+, only to find that everything broke — silently. The new ABI is now the default, and it’s not backward compatible with the previous stack-only model. I was both impressed by the optimization and frustrated by the fallout. My entire codebase — Z80 assembly wrappers, graphics drivers, and system libraries — suddenly needed revision.
I want to fully adopt the optimized ABI, so I started reviewing how arguments are passed by reading the documentation and compiling test cases. This article summarizes that information in a clearer, example-driven form — for the benefit of all fellow SDCC ABI martyrs now facing the same migration pain.
ABI overview
In the new Z80 calling convention, SDCC passes up to two arguments in registers. This table explains the basic rules of how SDCC passes function arguments under the default ABI introduced in SDCC 4.2.0 and later.
Argument Type(s) | Register(s) Used | Notes |
---|---|---|
First uint8_t |
A or L |
A is used if all args are 8-bit; otherwise L is used |
Second uint8_t |
L or E |
L if first was in A ; E if first arg was 16-bit (HL ) |
Third and later uint8_t |
Stack | No register usage; pushed right-to-left via push |
First uint16_t |
HL |
Full 16-bit register pair |
Second uint16_t |
DE |
Full 16-bit register pair |
Third and later uint16_t |
Stack | Pushed via push |
Mixed uint8_t , uint16_t |
L , DE |
A is skipped; byte goes into L , word into DE |
Mixed uint16_t , uint8_t |
HL , E |
Word in HL , byte in low byte of DE (E ) |
Notes:
- For 8-bit arguments, only the low byte of the 16-bit register is used (
A
,L
). - Additional arguments are passed right to left on the stack.
- Structs and pointers follow the same rules based on size.
Function argument examples
The following table shows how SDCC passes function arguments under the new default ABI. Function names are abbreviated:
b
= 8-bit (uint8_t
)w
= 16-bit (uint16_t
)fn2b1w
means: function takes 2 bytes, then 1 word
Each function is named according to its signature:
fn1b
— function accepts 1 byte (8 bit) argumentfn2w
— function accepts 2 word (16 bit) argumentsfn1w1b
— function accepts 1 word followed by 1 byte arguments
Here is the mapping table:
Function Signature | Name Meaning | Register Assignment | Stack Usage | Notes |
---|---|---|---|---|
void fn1b(uint8_t a) |
1 byte | A |
— | Byte in A |
void fn1w(uint16_t a) |
1 word | HL |
— | Word in HL |
void fn2b(uint8_t a, uint8_t b) |
2 bytes | A , L |
— | First in A , second in L |
void fn2w(uint16_t a, uint16_t b) |
2 words | HL , DE |
— | First in HL , second in DE |
void fn1b1w(uint8_t a, uint16_t b) |
1 byte, 1 word | L , DE |
— | A is skipped, a in L , b in DE |
void fn1w1b(uint16_t a, uint8_t b) |
1 word, 1 byte | HL , E |
— | a in HL , b in low byte of DE |
void fn3b(uint8_t a, uint8_t b, uint8_t c) |
3 bytes | A , L ; third on stack |
Yes | Only first two in registers |
void fn2b1w(uint8_t a, uint8_t b, uint16_t c) |
2 bytes + 1 word | A , L ; word on stack |
Yes | Word c on stack |
void fn3w(uint16_t a, uint16_t b, uint16_t c) |
3 words | HL , DE ; third on stack |
Yes | Third word always on stack |
void fn5b(uint8_t a, b, c, d, e) |
5 bytes | A , L ; rest on stack |
Yes | Only two 8-bit args in registers |
fn1b
void fn1b(uint8_t a);
Passing:
a
→A
fn1w
void fn1w(uint16_t a);
Passing:
a
→HL
fn2b
void fn2b(uint8_t a, uint8_t b);
Passing:
a
→A
b
→L
fn2w
void fn2w(uint16_t a, uint16_t b);
Passing:
a
→HL
b
→DE
fn1b1w
void fn1b1w(uint8_t a, uint16_t b);
Passing:
a
→L
b
→DE
Note: SDCC avoids using
A
when mixing 8-bit and 16-bit args. It usesL
instead.
fn1w1b
void fn1w1b(uint16_t a, uint8_t b);
Passing:
a
→HL
b
→E
fn3b
void fn3b(uint8_t a, uint8_t b, uint8_t c);
Passing:
a
→A
b
→L
c
→ stack
fn2b1w
void fn2b1w(uint8_t a, uint8_t b, uint16_t c);
Passing:
a
→A
b
→L
c
→ stack (pushed right-to-left)
fn3w
void fn3w(uint16_t a, uint16_t b, uint16_t c);
Passing:
a
→HL
b
→DE
c
→ stack
fn5b
void fn5b(uint8_t a, uint8_t b, uint8_t c, uint8_t d, uint8_t e);
Passing:
a
→A
b
→L
c
,d
,e
→ stack
SDCC uses
push af
+inc sp
to simulatepush a
(Z80 has nopush a
).
Return Values
Unlike the calling convention for function arguments, the ABI for return values has not changed in SDCC 4.2.0+. Return values have always been returned using registers, and the convention remains consistent:
- 8-bit values are returned in the
L
register (low byte ofHL
) - 16-bit values are returned in the
HL
register - 32-bit values (e.g.
long
,float
) are returned acrossHL:DE
withHL
holding the high word andDE
the low word
This applies uniformly to both the old and the new ABI.
Return Type | Return Method | Registers Used | Notes |
---|---|---|---|
uint8_t / char |
Register | L |
Returned in low byte of HL Only L is significant |
uint16_t / int |
Register | HL |
Full 16-bit result in HL |
uint32_t / long |
Register pair | DE (low), HL (high) |
32-bit value returned in HL:DE Big endian across registers |
float |
Register pair | DE (low), HL (high) |
IEEE 754 32-bit float Returned same as long |
Notes on push af
trick
SDCC can push 8 bit value on the stack. But since Z80 has no push a
, SDCC may emit:
push af ; pushes A and F
inc sp ; discard F, simulate push A
This ensures consistent behavior when passing extra 8-bit arguments on the stack.
Conclusion
The SDCC Z80 calling convention is now more efficient and modern:
- Arguments passed via registers (
A/L/E
orHL/DE
) - Reduced stack usage
- Compatible with inlined or
__naked
functions (if ABI respected) - Stack is used only when arguments exceed available registers