Calling convention notes

From SpecNext official Wiki
Jump to: navigation, search

Note: these notes by AA were originally posted on the specnext discord

There are three calling conventions in z88dk: standard C (stack based), fastcall (single parameter by register) and callee. Return values are by register unless it's 64-bit (a different method is used) or struct (this will be suboptimal). Values passed by register are via a subset of DEHL. L if 8-bit, HL if 16-bit, DEHL if 32-bit.

A. Standard C

int foo(int a, int b)

CALLER 1. push values onto the stack in right to left order 2. call function 3. clean up stack 4. return value in subset of DEHL

CALLEE 1. Stack above ret address must not be modified because the caller cleans up 2. Access via (ix+n) stack frame or pop/push sequence into registers 3. Place return value in subset of DEHL

For asm subroutines, fastcall or callee is always better. Standard stack based is required for variable number of arguments like printf and function pointers.

B. Fastcall

CALLER 1. Load subset of DEHL with single argument (L if 8-bit, HL if 16-bit, DEHL if 32-bit) 2. call function 3. return value is in a subset of DEHL

CALLEE 1. Argument is in subset of DEHL 2. Place return value in a subset of DEHL

C. Callee

CALLER 1. push values onto stack in right to left order 2. call function 3. return value is in a subset of DEHL

CALLEE 1. Pop values off stack into registers, restore return address, only return address remains on stack 2. Place return value in a subset of DEHL

For asm functions specifically, it is far better not to inline code and rather just supply a prototype to tell the C compiler how to call the asm function and provide the implementation in a separate asm file. Inlined asm affects a few things: you lose control over section placement (stuff will be put in the c compiler's destination sections), if c/asm is mixed it interferes with the optimizer and if c/asm is mixed you can't always know if values are being temporarily held in registers or the stack. z88dk provides special intrinsic functions in intrinsic.h to inline some common and simple asm like di/ei or endian conversion that don't interfere with the optimizer.

examples:

extern unsigned char foo(unsigned int a, unsigned int b);

(standard c only use for c functions or asm calling convention for variable arguments / function pointer)

extern unsigned char foo(unsigned char a) preserves_regs(b,c,d,e,h,l) z88dk_fastcall;

(fastcall function. Parameter "a" is passed in register L, return value is expected in L. Preserves spec tells the compiler it can store values in the listed registers across a call. Remember if you return a value those registers are not preserved. The c compiler can also generate z88dk_fastcall functions for c code without the preserves spec but it will only be good if the function is very short. The implementation in a separate asm file would be:

SECTION code_user    ;; where in the memory map it goes, this will be in the main binary
             ;; other sections like PAGE_14 or BANK_7 can be specified too
PUBLIC _foo    ;; c names have a leading underscore applied by the compiler when translating to asm
_foo:
 ; L = parameter a
  ld l,0   ; return value
  ret
extern unsigned char foo(unsigned int a, unsigned int b) preserves_regs(b,c,d,e,h,l) z88dk_callee;

(callee linkage, incoming parameters are on the stack, same sort of preserves spec)

SECTION code_user
PUBLIC _foo
_foo:
  pop hl   ; hl = return address
  pop de  ; de  = 'b'
  ex (sp),hl   ; hl = 'a'
  ld l,0   ;; return value
  ret

A typical compile with files listed in the compile line:

zcc +zxn -v -clib=sdcc_iy -SO3 --max-allocs-per-node200000 main.c main.asm -o main -subtype=nex -Cz"--clean" -create-app

The entire c library is written in asm in z88dk using fastcall and callee linkage. The newlib implementation is rooted here:

https://github.com/z88dk/z88dk/tree/master/libsrc/_DEVELOPMENT

The implementation is normally split between an assembly implementation which uses registers as input and a c interface which does the fastcall / callee parameter collection and then jumps into the asm implementation. Normally the fastcall c interface is an alias for the asm entry point.

long atol_fastcall(const char *buf)

c fastcall interface: https://github.com/z88dk/z88dk/blob/master/libsrc/_DEVELOPMENT/stdlib/c/sdcc_iy/atol_fastcall.asm (it's an alias for the asm entry point)

c standard stack interface: https://github.com/z88dk/z88dk/blob/master/libsrc/_DEVELOPMENT/stdlib/c/sdcc_iy/atol.asm (used only for function pointers due to macro defined in header stdlib.h)

asm implementation: https://github.com/z88dk/z88dk/blob/master/libsrc/_DEVELOPMENT/stdlib/z80/asm_atol.asm

The same pattern is followed for the entire library. c/sdcc_iy/ holds the c interface. z80/ holds the asm implementation. The separation is made because the library is intended for asm programmers as well so calling directly there will avoid including c interface code. For c code, the linker will normally place the c interface just before the asm implementation in memory so there is a "jp foo; foo:" bit of code. The linker is meant to be modified later to eliminate these sorts of superfluous jumps.