This wiki is being migrated to


This is an old revision of the document!

z88dk logo z88dk is a collection of software development tools that targets z80 machines. It consists of a C compiler, a set of libraries implementing the C standard library, an assembler / linker and a variety of utilities for profiling and generating executables in a number of formats. Development in C, assembly language or a mixture of the two is directly supported.

The name z88dk originates from the time when the project was founded and targetted only the Cambridge z88 portable. Today z88dk directly supports more than forty z80 targets with the level of library support for each target varying with interest shown by users. It is possible to add new targets with relative ease.

z88dk is known to run on a wide variety of platforms. Binary releases are available for Win32 and MacOS X and a source tarball is available to build the tools for other platforms.

There are a few things that make z88dk unique:

About the z88dk logo

Mandelbrot Twitter Client File Transfer
Click on the link on the right for the video YouTube
Ninjajar! 3D Globe Forest Raider Cherry


Information can be found here:


Benchmarks can found on the new wiki:


Program size is often more important than performance in the small 64k space available to standard z80 programs.

A selection of programs from z88dk's examples directory were compiled to compare binary sizes using the widely supported CP/M target.

backgammon.c (703 lines) clisp.c (1279 lines) eliza.c (352 lines) startrek.c (2153 lines)
Relative Bytes Relative Bytes Relative Bytes Relative Bytes
Hitech-C CPM v3.09 * * 1.68 12699 * *
Hitech-C Z80 v7.50 1.19 32186 2.00 15193 1.28 41038*
Z88DK/SCCZ80_CLASSIC 1.00 27039 1.00 7578
Z88DK/SCCZ80_NEW 1.07 28888 1.13 8601
Z88DK/SDCC 1.00 30392 1.12 30155 1.10 8324 1.00 32053


  • BACKGAMMON.C (Z88DK/SCCZ80_NEW, Z88DK/SDCC Pragmas to minimize crt, only %c%d enabled for printf, error strings reduced to minimum, –opt-code-size for sdcc)
  • CLISP.C (Hitech-C CPM v3.09 Does not allow switch on longs) (Hitech-C Z80 v7.50 Remove seed loop counter, enable long printf) (Z88DK/SCCZ80_CLASSIC Pragmas to minimize size, removed seed loop counter, added special functions for line editing) (Z88DK/SCCZ80_NEW, Z88DK/SDCC Pragmas to minimize crt, only %s%ld enabled for printf, error strings reduced to minimum, –opt-code-size for sdcc)
  • ELIZA.C (Hitech-C CPM v3.09 Simple strlwr() strstr() provided, "//" comments removed, “const” removed, EXIT_FAILURE defined, Constant type specifiers removed) (Hitech-C Z80 v7.50 Simple strlwr() provided) (Z88DK/SCCZ80_CLASSIC Pragmas to minimize size and USING_amalloc pragma for automatic malloc) (Z88DK/SCCZ80_NEW, Z88DK/SDCC Pragmas to eliminate exit stack and stdio heap, only %s enabled for printf, –opt-code-size for sdcc, automatic malloc)
  • STARTREK.C (Hitech-C CPM v3.09 Too many symbols for assembler) (Hitech-C Z80 v7.50 Program hangs may need lower optimization level, Typedef double_t, Remove seed loop counter) (Z88DK/SCCZ80_NEW, Z88DK/SDCC Pragmas to minimize crt, only %sdf enabled for printf, error strings reduced to minimum, –opt-code-size for sdcc)


Installation information can be found on the new wiki:

The Tools

An overview of the tools can be found on the new wiki:

Quickstart Guide

These brief command line examples will get you compiling programs right away.

When compiling there is a choice between using the classic C library or the new C library.

Supported Targets

A compile line must specify a target. This allows the compiler to automatically select the correct libraries and drivers to use for the targeted machine. The set of supported targets differs between the two C libraries. If your machine is not directly supported, the generic target can be used to generate code. With the classic C library, ”+test” is the generic target and with the new C library the generic target is ”+embedded”.

List of Supported Targets (Classic C Library)

List of Supported Targets (New C Library)

  • cpm CPM 2.2 currently without disk i/o (the classic lib does implement disk i/o)
  • embedded Embedded z80 systems or generic z80 target
  • zx Sinclair ZX Spectrum

Compiling with SCCZ80

SCCZ80 is z88dk's native C compiler.

The Classic C Library

Information on the classic library can be found on the new wiki:

The New C Library

An introduction to the new library can found on the new wiki:

The New C Library

The new C library aims to comply with a large subset of C11. It features an object oriented stdio, windowed terminals, proportional fonts and the ability to generate standalone & ROMable software.

zcc +zx -vn -SO3 -clib=sdcc_ix --max-allocs-per-node200000 --reserve-regs-iy test.c -o test
  • +zx Select the target machine (zx spectrum in this example). This causes the appropriate crt and libraries to be linked. ”+embedded” is the generic target.
  • -vn Verbose messages off. Omitting this option will cause zcc to detail each step taken during compilation.
  • -SO3 Use the aggressive peephole rules supplied with z88dk. This can improve sdcc's generated code significantly.
  • -clib=sdcc_ix, -clib=sdcc_iy Selects the new C library and sdcc as compiler. The modified version of sdcc must be in the path. The new C library uses one index register and selection of “sdcc_ix” chooses the library version using ix. “sdcc_iy” is also available and has the library using iy. Because sdcc uses the ix register as a frame pointer, use of the “sdcc_iy” library can lead to smaller executables as the library will not have to preserve the ix register in calls. The choice is available because some targets do not allow the use of one index register or the other. Note that sdcc expects that iy is not modified by user-code so selection of the “sdcc_iy” library automatically causes ”–reserve-regs-iy” to be added to the compile line.
  • –max-allocs-per-nodeNNNNN (optional) Selects sdcc's optimization level. The default value is 3000; large values can increase compile time considerably with correspondingly better code generation. NNNNN=200000 is a reasonable upper bound.
  • –reserve-regs-iy (optional) sdcc is not allowed to use iy. sdcc usually generates better code with this option enabled so it can be worthwhile to try but primarily this option is available for targets that cannot easily allow use of more than one index register.
  • -o filename Output filename.
  • -lm (optional) Link against z88dk's math48 floating point library. Native floating point libraries are not yet available from the new clib.

The list of source files can contain any combination of C source (*.c), asm source (*.asm,*.opt), object files (*.o) or (nightly build) list files. List files are identified by a leading ”@” in their names and contain a list of source files, one file per line. zcc will read the contents of a list file and add listed files to the compile.

There must be exactly one main() function defined if an executable is to be produced.

Because the new C library is section aware, the generated output will be one or more raw binaries. The ”-create-app” option can no longer be used as there is insufficient information available to generate a default executable suitable for the target from multiple output binaries. Instead, APPMAKE can be invoked directly on the output binaries to generate a suitable output file.

Selected Command Line Options

  • -a Translate C source to assembler.
  • -c Generate an object file rather than an executable.
  • –c-code-in-asm (nightly build) Place C code as comments in output assembler.
  • -Ca.. Pass an option to the assembler. For example, "-Ca-D__SDCC"
  • -Ca-IXIY Instruct the assembler to swap IX and IY in assembled code.
  • -Cl.. Pass an option to the linker. For example, ”-Cl–split-bin”
  • -Cl–split-bin Causes z80asm to generate one output binary per section defined in the source. A directory listing afterward can tell you at a glance what is taking up the space in the final executable.
  • -D__SDCC_DISABLE_BUILTIN Disable sdcc's builtin functions. sdcc will sometimes inline a handful of common string functions by default (see the end of string.h).
  • –fsigned-char (zsdcc updated after v1.99) “char” means “signed char”. New versions of sdcc treat “char” as “unsigned char”.
  • –fverbose-asm When translating C to asm, includes code generator and peephole comments in the output asm source. Comments in function headers will tell if the compiler's optimization level (–max-allocs-per-node) was high enough to achieve optimal register assignment.
  • -g Generate a global defines file that lists global labels and their assigned values as a sequence of defc constants.
  • -h Help.
  • -lname Link against the “name” library.
  • -Lpath Search path for libraries when linking.
  • –list (nightly build) Generate list files for the crt and each C source file compiled.
  • -m Generate a map file listing all labels and their values twice with the first half in alphabetical order and the second half in address order.
  • –max-allocs-per-nodeNNNNN Selects code generation optimization level for sdcc. NNNNN is 3000 by default and 200000 is a good upper bound. Note there is no space between the option text and NNNNN.
  • -notemp Intermediate files are left in the compile directory. Can be helpful for debugging.
  • -o outfile Change the output filename root.
  • -On Selects z88dk's peephole optimization level. Under sdcc the purpose of the optimization rules is different. Level 0 will leave sdcc's output in its native asz80 format. z88dk cannot assemble this format. Level 1 translates sdcc's native format to more standard Zilog syntax. Level 2 changes sdcc's calls to its primitives to use a calling convention that saves extra bytes. The default is level 2.
  • –opt-code-size sdcc will call a subroutine to set up the frame pointer on entry to a function. (nightly build) programs using 64-bit integers will see significant code size reduction.
  • -preserve Prevent the list of active pragmas stored in “zcc_opt.def” from being deleted prior to compiling. This is important when generating object files in a makefile. If your compile is a single line invocation of zcc this option is unnecessary.
  • –reserve-regs-iy Prevent sdcc from using the iy register pair.
  • -SOn Selects the peephole optimization level for sdcc. Level 0 does not perform any code substitutions. Level 1 uses only the rules supplied by sdcc. Level 2 uses the sdcc rules and some rules that fix some of sdcc's code generation bugs. Level 3 is the highest level and in addition to level two, it introduces many new code substitution rules that can reduce code size and improve speed. Level 2 is the default.
  • -startup=n Quite often targets have several different crts available that can be selected by number. Differences between the crts can include different devices attached to stdin,stdout,stderr and different memory models.

In general any sdcc option can be appended to the compile line.

Translating C to Assembler

zcc +zx -vn -a -SO3 -clib=sdcc_ix --max-allocs-per-node200000 --reserve-regs-iy test.c
zcc +zx -vn -a -SO3 -clib=sdcc_iy --max-allocs-per-node200000 test.c
zcc +zx -vn -a -SO3 -clib=sdcc_iy --max-allocs-per-node200000 test.c --c-code-in-asm

“test.c” will be translated to assembler in output file “test.opt” or “test.asm” depending on the -O optimization level.

Selection of the target machine ensures the include path is set, will enable headers specific to the target architecture, chooses default optimization (”-O2 -SO2” for sdcc) but otherwise it has no effect on the code generated. If you just want to see the generated code for a standard C program, any target selection will do and the most generic in the new C library is the ”+embedded” target.

Information on what occupies space in the final binary can be learned by adding ”-Cl–split-bin” to a compile line that generates a binary. The output will be one binary per defined section whose sizes will reveal what is occupying space.


ZCC is the toolchain front-end. It takes as input a list of files consisting of any combination of C source (*.c), assembly language (*.asm,*.opt), object files (*.o) or (nightly build) list files, and compiles them into a single object file or into one or more binary executables. The steps taken to compile or assemble each input file is determined by each file's extension. A mandatory target selector (”+target”) allows zcc to read a configuration file that specifies which libraries to link against, which crt to use, what the include path is and various other defaults. In short, zcc acts like a magic bullet that will do what is needed to generate final binaries given any sort of input. The tools (compilers, assemblers, linkers) can also be invoked directly if that is preferable.

Z88DK Contains Two Independent C Libraries

The “classic” library is the one that has always shipped with z88dk. It is large and, in particular, contains a lot of extension libraries for things like graphics and sound. Its stdio model supports both input and output via character i/o functions supplied by the target. One disk device, one terminal and one network device can be supported in a single compile. stdio is compact and is easy to re-target. The classic library is not section aware yet so it generates code and data into a single binary blob that is intended to be run from RAM, with a few exceptions.

The “new” library is aiming for compliance with a large subset of C11. It is still under development but is already extensive, containing 700 functions and 30,000 lines of assembly code. It too contains extension libraries for things like sound, proportional fonts, data compression, container types, etc, but it does not yet contain many of the extension libraries found in the “classic” library. The stdio model is object-oriented and supports any number of attached devices in a single compile. The library supplies object-oriented driver base classes that can be derived from to write target-specific drivers. Currently the library contains terminal drivers that support line editing and terminal emulation using fixed width or proportional fonts. Writing new device drivers (and therefore targeting a new machine) involves writing code that responds to a small set of stdio messages and this can be done with or without the help of library code. The library is section aware and can produce code and data that is placed arbitrarily in memory or in multiple memory banks.

Over time features will homogenize between the two C libraries but they will remain independent. The simpler feature set of the “classic” C library can mean a smaller code footprint in many circumstances.

The selection of which C library to use is made on the compile line. Both libraries' functions are documented below.

Z88DK Supports Two C Compilers

sccz80 is z88dk's native C compiler. It makes optimization decisions locally. The peephole optimizer stage can further reduce code size by up to a third by performing simple text transformations on the assembly output of the compiler. The main feature of sccz80 is that it tries to generate small code by implementing most compiler actions through subroutine calls. The resulting code will be slower than some other compilers but it should also be smaller. This strategy pairs well with a library written in assembly language which provides a speed and size advantage compared to other z80 compilers. sccz80 does not reserve any registers for itself and accesses the stack frame through offsets from the stack pointer register.

sdcc is an open source optimizing compiler than can target the z80. It is capable of global optimizations and contains a peephole optimizer stage that can perform simple code analysis while performing code substitutions. sdcc's compiler actions tend to be inlined which generates faster code but may also lead to larger code. Aside from its optimization capability, it is one of the few, if only, z80 C compilers aiming for comprehensive standards compliance with C89, C99 and some C11. sdcc implements a stack frame using the IX register as frame pointer whose value must be preserved. IY must also be preserved but sdcc can be instructed not to use IY with a compiler switch. The current sdcc has to be patched in order to be made compatible with z88dk.

The selection of which C compiler to use is made on the compile line with either the ”-clib=” (new library) or ”-compiler=” (classic library) options.

Z88DK Enables Integrated C and Assembly Language Development

C and assembly language are treated equally within z88dk. Projects are free to be written purely in C, purely in assembly language or any mixture of the two. Assembly language can be inlined within C code but it can also interact with C as a collection of standalone assembler subroutines. Indeed the C libraries are written in assembly language and supply C interface code so that they can be called directly from either C or asm.

C and Asm global variables can be directly accessed from both C and assembly language. C calls to assembly subroutines can use one of the three linkages supported by both C compilers (standard, fastcall and callee) with parameters passed in the fashion specified, either via stack or via register.

More details can be found here.

C Compiler Characteristics

Known issues can be found on the new wiki:

Data Types

Information on datatypes can be found on the new wiki:

Function Call Linkage

Function call linkage can be found on the new wiki:

This is a technical subject probably only of interest to assembly language programmers and can safely be skipped.

Function call linkage refers to how the compiler communicates function parameters to- and return values from- a called function.

Parameter Passing

Calls through function pointers always use standard linkage.

All functions in the new C library make use of the quicker & smaller fastcall or callee linkage as do most functions in the classic C library. When C library functions are assigned to a function pointer, the function pointer is assigned an entry point that uses standard linkage.

Fastcall and callee linkage were originally added to sccz80 to efficiently call library functions written in assembler. sdcc added these calling conventions for compatibility with z88dk's libraries. sccz80 is capable of generating callee code. Both sccz80 and sdcc are capable of generating fastcall C code. So if your C code takes just one parameter, sdcc will compile better linkage if the C function has the ”__z88dk_fastcall” decoration added to the end of the function signature. There is a noticeable improvement in the code generated by sdcc for fastcall functions particularly for small functions. Keep in mind that sdcc may have trouble calling fastcall functions by function pointer – it can only generate code for this if it is allowed to use the iy register. The C library has a different method for resolving this that allows all library functions, standard / fastcall / callee, to be called seemlessly via function pointer.

More details can be found in the Mixing C and Assembly topic.


sdcc tends to generate faster code while sccz80 tends to generate smaller code particularly when dealing with longs, floats and statics and after code has been made small-uP friendly. For this reason we are working on making sdcc and sccz80 object code compatible so that portions of a C project can be compiled with both compilers and the result linked together. Although both C compilers are using the same library code this is not currently possible because of differences in the order that parameters are pushed on the stack for vararg functions and the incompatible float types that prevent floats from being communicated between sdcc- and sccz80- compiled functions.

So for the time being a project must be completely compiled with either sdcc or sccz80.


sccz80 produces binaries very quickly so the most convenient way to generate an executable is to simply list all the source files in a single zcc invocation and have it produce an executable from scratch every time.

sdcc, however, can take a long time to generate binaries when its optimization level is turned up (–max-allocs-per-node). It can save a great deal of time to have a makefile that generates object files from separate source files and then combines the lot into an executable. This way only C source that changes is re-compiled in each build step.

Makefiles are also a good way to automate the generation of the final output, which may not be limited to a single output binary. So even though sccz80 can generate binaries quickly, it can make sense to use a makefile with sccz80 to automate generation of the final output.

Object files can be generated using zcc with the ”-c” option added to the compile line. The generation of an executable can then be done by invoking zcc with a list of object files. This should not be new to anyone familiar with makefiles.


What is different in z88dk is that pragmas, generated by the compiler and the user, are used to select options in the crt.

An example of a compiler-generated pragma with the classic C library is sccz80's scan of printf format strings. sccz80 will keep track of what format specifiers are used and determine whether the compile can use a simple printf, medium printf or large printf implementation. This way the compiler can automatically reduce the size of the output binary by selecting the smallest printf implementation allowable.

An example of a user pragma that sets a crt option with the new C library is ”#pragma output CLIB_MALLOC_HEAP_SIZE = 2048”. This pragma will be seen by the crt which will reserve space for a 2k heap and automatically initialize it before main() is executed.

These pragmas are written to a file “zcc_opts.def” as assembler define directives during compilation. When an executable is generated, the crt is assembled as part of the last step of the compile and after “zcc_opts.def” is complete. The crt includes the “zcc_opts.def” file and uses the options specified to perform whatever initialization is necessary to implement the options.

Each time zcc is invoked, this “zcc_opts.def” file is deleted so that there is a clean compile on each invocation. However, when using a makefile the project is normally split into many source files that are individually compiled to object files. When each individual source file is compiled to an object file, the first step taken by zcc is to erase “zcc_opts.def” but this is not what you want to happen – the “zcc_opts.def” file should accumulate all options generated by all the source files. To allow that to happen, the ”-preserve” option should be specified on the zcc compile line to prevent “zcc_opts.def” from being deleted. When ”-preserve” is active any options encountered will be appended to “zcc_opts.def”.

Makefile Example

how to ensure zcc_opts.def does not grow indefinitely and is erased at an appropriate time.

Optimization Level

Both compilers support user-selected optimization levels.


Peephole Optimizer

sccz80's output is passed through a peephole optimizer step that performs text substitutions on the compiler's output. There are three rule sets provided by z88dk and they are cumulative (ie applied one after the other). Which rule sets are applied is determined by the optimization level chosen on the compile line ”-On”.

  • -O0. Skip peephole optimizer.
  • -O1. Level 1 optimizations.
  • -O2. Level 2 optimizations.
  • -O3. Level 3 optimization that attempts to reduce code size at the expense of speed.

Example compile line:

zcc +zx -vn -O3 test.c -o test -lndos
zcc +zx -vn -O3 -clib=new test.c -o test

With ”-O3” selected, rule set #1 followed by rule set #2 followed by rule set #3 will be applied.

The default for all targets is ”-O2”.


Code Generation

sdcc applies an optimization level during code generation that is supplied with the ”–max-allocs-per-node” flag. Larger numbers permit sdcc to perform deeper code analysis but this will also increase compile time considerably. The default is 3000 but a reasonable upper bound is probably 200000.

Another flag ”–opt-code-size” is intended to indicate to sdcc that small code is preferred. In the unpatched version of sdcc, this currently has little effect except to use a subroutine to set up stack frames inside functions and to prefer small code to clear up the stack after function calls. The impact on code size is small but present. In z88dk's version of sdcc, ”–opt-code-size” will also significantly reduce the size of code generated for handling 64-bit integers, sometimes by up to 50%. Recent changes also attempt to reduce code size of programs making heavy use of longs and floats where a 10% code size reduction is seen.

Example compile line:

zcc +cpm -vn -SO3 -clib=sdcc_iy --opt-code-size --max-allocs-per-node200000 test.c -o test

Peephole Optimizer

sdcc's output is passed through its own peephole optimizer step that performs text substitutions on the compiler's output. sdcc comes with some rules of its own but under z88dk we provide three different rule sets selected with ”-SOn” on the compile line.

  • -SO0. Skip peephole optimizer.
  • -SO1. Apply the rules provided by sdcc.
  • -SO2. Apply the rules provided by sdcc along with rules that fix some code generation bugs.
  • -SO3. Apply the rules provided by sdcc, the bugfix rules and a large set of aggressive rules provided by z88dk.

The default for all targets is ”-SO2”.

The SO3 rules have a significant impact on code size and speed and are regularly expanded as we check code generation for different programs. There are many hundreds of rules in the SO3 set so it is possible that errors may be present. If a program fails to run with SO3 enabled, try a compile at SO2 level to rule out the aggressive rules. A bug report in the z88dk forums would be appreciated if the SO3 rules are found to be at fault.

Example compile line:

zcc +zx -vn -SO3 -clib=sdcc_ix --reserve-regs-iy --max-allocs-per-node200000 test.c -o test

Z88dk Peephole Step

The output from the two steps above is passed through z88dk's peephole optimizer using sdcc-specific rules. There are several levels of rules which can be selected with ”-On” on the compile line. These rules are cumulative, meaning they are applied in sequence. With ”-O2” selected, the -O1 rules will be applied and then the -O2 rules afterward. The purpose of these rules is not to improve the code but instead to further process it.

  • -O0. Skip this step. In particular, sdcc's output will retain asz80's assembler syntax. This can be helpful when creating new peephole rules as sdcc's peephole optimizer operates on asz80 syntax input.
  • -O1. Translate assembler to (nearly) Zilog syntax. Replace sdcc areas with z88dk sections. Remove the redundant INITIALIZER area. Bring sdcc primitives into scope.
  • -O2. Change sdcc calls to its primitives to use callee linkage.

The default is ”-O2”. The minimum level must be ”-O1” for a program to be compilable by z88dk. The toolchain will accept source files ending in .s as using asz80 syntax and will perform the translation automatically during the compile.

Example compile line:

zcc +zx -vn -a -SO3 -O0 -clib=sdcc_ix --max-allocs-per-node200000 --reserve-regs-iy test.c

This line will translate the C to assembler and leave it in asz80 syntax.

Compile Sequence

zcc takes a list of files (*.c, *.o, *.asm, *.opt) and generates a single object file or binary executable as output. For each file, it determines from the file extension what steps need to be taken to generate an object file. When all files have been assembled into object files, they are collected together and either linked to form an executable or merged to form a single object file.

Seeing the steps taken for compiling C source to object file can take some of the mystery out of things.


Example compile:

zcc +zx -vn -clib=new test.c -o test

“test.c” is taken through the following steps:

  • zcc reads configuration information from the target config file, in this case {z88dk}/lib/config/zx.cfg. This file contains various default flag settings such as crt to use, include path, library to use, default optimization level and so on. ”-clib=new” also causes the options under “CLIB new” to be read.
  • zcpp is run to process all c pre-processor directives. The output is a new C source file without pre-processor directives.
  • zpragma is run to process and remove any pragmas. The output is a C source file stripped of these pragmas and the file “zcc_opts.def” which contains information that needs to be communicated to the crt.
  • sccz80 is run to translate the C source to assembler. The output is an asm file containing the translated C source.
  • if the opt level is at least 1 (-O1) copt is run on the asm source using the level 1 peephole rules.
  • if the opt level is at least 2 (-O2) copt is run on the result using the level 2 peephole rules. (-O2 is the default)
  • if the opt level is at least 3 (-O3) copt is run on the result using the level 3 peephole rules.
  • z80asm is run to generate an object file from the asm source.

At this point if any other source files are included on the compile line, they are taken to an object file using the same or similar steps.

  • z80asm is invoked as linker with the crt listed as first file (in asm form) and a list of the object files following. The target library ”-lzx” and any libraries indicated by the user are added to the compile line. One or more binaries are output, one per section with independent ORG.

The crt includes the “zcc_opts.def” file which contains defined constants that indicate various options. The crt tests these defines to insert any asm code necessary to implement the options. Being listed first among the linker's list of files guarantees it is processed first and this also allows the crt to create the memory map.


Example compile:

zcc +zx -vn -clib=sdcc_ix -SO3 --max-allocs-per-node200000 --reserve-regs-iy test.c -o test

“test.c” is taken through the following steps:

  • zcc reads configuration information from the target config file, in this case {z88dk}/lib/config/zx.cfg. This file contains various default flag settings such as crt to use, include path, library to use, default optimization level and so on. ”-clib=sdcc_ix” also causes the options under “CLIB sdcc_ix” to be read.
  • zsdcpp is run to process all c pre-processor directives. The output is a new C source file without pre-processor directives.
  • zpragma is run to process and remove any pragmas. The output is a C source file stripped of these pragmas and the file “zcc_opts.def” which contains information that needs to be communicated to the crt.
  • zsdcc is invoked with options not understood by zcc (”–max-allocs-per-node200000 –reserve-regs-iy”) to translate the C source to assembler and to run sdcc's peephole optimizer step. The peephole optimizer uses one of three rulesets. If “SO=0” no peephole rules are applied, if “SO=1” the rules that come with sdcc are applied, if “SO=2” the rules that come with sdcc are applied along with a few rules that fix code generation bugs, if “SO=3” the rules with sdcc are run along with the bugfix rules and a new set of rules that aim to improve code size and speed. The default is SO=2 but the compile line above selects “SO=3”. The output is an asm file in asz80 format.
  • if the O level is at least 1 (”-O1”) copt is run using sdcc rule set 1. This translates the asz80 syntax to (more or less) standard Zilog syntax, translates sdcc AREA designations into z80asm SECTIONs, and eliminates the INITIALIZER area created by sdcc. The output is an asm file (*.opt) in z80asm format.
  • if the O level is at least 2 (”-O2”) copt is run using sdcc rule set 2. This replaces all sdcc calls to its primitives with callee linkage. The output is an asm file. (-O2 is the default)
  • z80asm is run to generate an object file from the asm source.

At this point if any other source files are included on the compile line, they are taken to an object file using the same or similar steps.

  • z80asm is invoked as linker with the crt listed as first file (in asm form) and a list of the object files following. The target library ”-lzx” and any libraries indicated by the user are added to the compile line. One or more binaries are output, one per section with independent ORG.

The crt includes the “zcc_opts.def” file which contains defined constants that indicate various options. The crt tests these defines to insert any asm code necessary to implement the options. Being listed first among the linker's list of files guarantees it is processed first and this also allows the crt to create the memory map.

Note that sdcc is only used to translate C to assembler and its backend is not used to generate libraries, object files or executables. Instead sdcc's output is translated to standard Zilog syntax for consumption by z88dk's back end. This means inline assembler is assembled by z80asm and must use standard Zilog syntax. Inlined assembly can be enclosed in ”#asm” / ”#endasm” or "__asm" / "__endasm;" blocks.

Reducing Binary Size

This information can be found on the new wiki:

C Library Reference (Classic)

C Library Reference (New)


The introduction to newlib can found on the new wiki:

Library Configuration

Documentation detailing the configuration for newlib can be found on the new wiki:


Documentation detailing the newlib CRT can now found on the new wiki:

Assembly Language

The entire library is accessible from assembly language. The assembly entry points for functions are prefixed with “asm_” and the register interface for each function is documented in the source code rooted in {z88dk}/libsrc/_DEVELOPMENT. Here is a brief example that makes use of stdlib's dtoa() function to convert a double to ascii text:

; assumes math48 is the math library linked

SECTION code_user

EXTERN asm_dtoa

ld bc,$490F      ; AC' = pi
ld de,$DAA2      ; math48 uses BCDEHL to hold a double
ld hl,$2182

ld c,0           ; no flags
ld de,-1         ; max precision
ld hl,buffer     ; destination buffer

call asm_dtoa    ; write double in ddd.ddd format to buffer


SECTION bss_user

buffer:   defs 32

asm_dtoa is located in {z88dk}/libsrc/_DEVELOPMENT/stdlib/z80. The comments detail input parameters, output parameters and registers modified. The C documentation can also be consulted for more details.

Vararg functions such as printf expect their parameters to be pushed onto the stack. In these sorts of cases, the asm caller must use standard C linkage to call the function. Here is a brief example that uses printf:

; assumes sccz80 is the compiler
; L->R parameter order, varargs require A to be loaded with num words pushed onto stack

SECTION code_user

EXTERN asm_printf

ld hl,fmt       ; format string
push hl
ld hl,100       ; 100 dollars (16-bit integer)
push hl
ld a,2          ; sccz80 only, number of words pushed
call asm_printf
pop af
pop af          ; clear stack


SECTION rodata_user

fmt:  defm "You win %d dollars.\n"
      defb 0

The equivalent C is:

printf("You win %d dollars.\n", 100);

If this is a project compiled with sccz80 or if this is an assembly language project linked against the sccz80 library, then printf is expecting its parameters to be pushed in left-to-right order and the 'A' register must be loaded with the number of 16-bit words pushed.

On the other hand if the project is compiled with sdcc or if this is an assembly language project linked against the sdcc library, then printf is expecting its parameters to be pushed in right-to-left order and nothing needs to be loaded into 'A'.

More details can be found in the Mixing C and Assembly topic. Most library functions are not vararg and asm parameters will be passed via register rather than stack.

You must also be aware that the new c library employs sections. Sections are destination containers that hold code and/or data and can have an ORG address associated with them. All assembly language written should be assigned to a section so that the linker can know where to place it in memory.

The crts previously discussed create three basic sections: CODE, DATA and BSS. These large sections hold many small ones, including some sections designated for user code:

  • code_user assign executable code to this section
  • rodata_user assign read-only data to this section
  • smc_user assign self-modifying code to this section
  • data_user assign non-zero initial data to this section
  • bss_user assign zero initial data to this section

By assigning your assembly code to the correct sections, the linker will be able to create ROMable software with your code.

You can also create your own sections. There's no magic incantation, simply start using it and assign a name as in:

SECTION my_section


    ld hl,2

Since this section is not in the crts' memory map, all data or code assigned to it will be output as a separate binary when the project is assembled. The name of the binary will be “outputname_my_section.bin”. If no ORG is assigned to the section anywhere in your project, it is assigned an ORG of 0. (Note that un-ORGed sections may be appended to previously defined sections in the same source file; this is how memory maps are built with z80asm).

These details are best described in the Mixing C and Assembly topic.

Header Listing

Details on the header files available in newlib is now located here:

Function Listing

Library In Depth

Creating a Target

This information has been moved to the new wiki:



These example programs are intended to help familiarize you with some of the library's features. Some of the programs have been made more complicated than necessary to illustrate various points.

  • BEEPFX is a sound effect generator for targets with 1-bit sound devices. The program plays all available sound effects and then accepts user input to play effects by number. A windows program is available to make new effects and it is explained how they can be imported into your project.
  • CHESS is one of the shortest chess programs in the world. Straightforward modifications are made to allow a program written for a 32-bit machine to run on a z80.
  • CLISP is a lisp interpreter written in C. The output binary is destined for a 16k ROM which presents challenges as the binary is larger than 16k. The solution chosen is to run the program from RAM but have a compressed copy stored in ROM that is decompressed in RAM at power up. Details on the ROM model and data compression are discussed.
  • PASSWORD QUERY. Stdin has the username stuffed into its input buffer so that a following scanf primes the input with the last username entered. Password mode is set so that asterisks are printed while the password is entered.
  • PI computes pi to 800 digits. The library is configured to speed up execution almost four times.
  • SP1_EX1 sp1 software sprite engine example for the zx spectrum target. Mixes C and external asm data, applies crt and library configurations to minimize code size, customizes sp1 for a different display area, moves the library's malloc heap to a known location in memory. A bonus program adds collision detection and a few sound effects.
  • SUDOKU SOLVER solves sudoku puzzles. The more sophisticated version uses p_list_t (a doubly-linked list type from the library), makes use of obstack for memory allocation, and creates a new SECTION to hold data that is assembled as a separate binary file.

Mixing C and Assembly Language

The compilers translate C code into assembler. These translated assembler files are treated no differently from hand-written assembly code consumed by the assembler. In particular, just like any other assembly language input, assembly code can interact with variables and functions defined in the translated C.

This section gives a compact overview of the assembly language environment supported by z80asm and then describes how assembly language code and C code can interact with each other.


The basic translation unit is the file. All symbols within the same file are implicitly local, meaning they are not visible to asm code in other files. To make symbols publicly visible, they must be declared PUBLIC. To reference a symbol defined in another file, it must be declared EXTERN. Extern symbol references are resolved at link time.

FILE 1: “asm_strcpy.asm”

PUBLIC asm_strcpy


   push de
   xor a


   cp (hl)
   jr nz, loop
   pop hl
   dec de

FILE 2: “Hello.asm”


EXTERN asm_strcpy


   ; enter : de = destination string pointer

   ld hl,hello_s
   jp asm_strcpy


   defm "Hello"
   defb 0

FILE 3: “World.asm”



   ; de = destination string pointer

   ld hl,world_s
   jp asm_strcpy


   defm "World"
   defb 0

FILE #1 exports the symbol “asm_strcpy”. The other symbol “loop” is local and cannot be seen outside the file.

FILE #2 exports the symbol “Hello”. It declares an external reference to “asm_strcpy”. “hello_s” is a local symbol. The code jumps to “asm_strcpy” which is declared EXTERN, so the assembler will resolve this reference at link time when it has access to all PUBLIC symbols.

FILE #3 exports the symbol “World”. The symbol “world_s” is local. The assembler will complain about an unresolved reference in “asm_strcpy”. Since it's (mistakenly) not declared EXTERN, the assembler expects the label to be defined locally.

To assemble these files they can be listed in the same z80asm invocation:

z80asm -b -o=program asm_strcpy.asm Hello.asm World.asm

or they can be assembled into object files individually and then linked together:

z80asm -Mo -o=asm_strcpy asm_strcpy.asm
z80asm -Mo -o=Hello Hello.asm
z80asm -Mo -o=World World.asm
z80asm -b -o=program asm_strcpy.o Hello.o World.o

z80asm also supplies the GLOBAL scope operator for compatibility with sdcc. GLOBAL indicates that the symbol may be declared locally or it may be declared externally. If the symbol is declared locally, it is exported so that GLOBAL behaves like PUBLIC. If the symbol is not declared locally, GLOBAL behaves like EXTERN so that it is assumed to be declared externally.

Code and Data Placement in Memory: SECTIONs

A section is a named container with an optional ORG address that can hold assembly code and data. The programmer can direct code and data into specific sections with suitable section directives embedded in the assembly source. On assembly, the output will be composed of one binary for each section with its own ORG address. Sections without ORG addresses append themselves to the last section defined in text order.

The best way to understand sections is to see some examples.

org 0


EXTERN display, input


   call initialize


   call display
   call input
   jr loop

org 32768





PUBLIC workspace, _score

workspace:  defs 128
_score   :  defs 2


PUBLIC calculate

EXTERN l_mult


    ld hl,(_score)
    ld de,10
    call l_mult

In the above source, three sections are defined: “code”, “data” and “bss”. As can be seen sections are open, meaning they can be added to from anywhere. The “calculate” function at the end of the example is placed in the same “code” section as the “main” code at the start of the example. In fact “calculate” will immediately follow “main” in memory. This also applies to source code spread across multiple files.

The “code” section has been assigned an ORG of 0 and the “data” section has been assigned an ORG of 32768. However “bss” has not been assigned an ORG. It will be appended to the previously defined section in the file (“data” in this example). If there were no such previous section, it would have behaved as if assigned an ORG of 0.

On assembly, two binary files will be generated: “name_code.bin” and “name_data.bin” where “name” is the name of the output file. The “bss” section, not having an ORG, will be part of “name_data.bin”. The “code” section had an ORG of 0 so “name_code.bin” should be loaded at address 0 in memory. Similarly “data” had an ORG of 32768 so “name_data.bin” should be loaded at address 32768.

“ORG -1” has special meaning. It indicates that the section will follow the previous one in address order but it will be output as a separate binary.

Notice that because sections can append to previously defined ones if not given an ORG address, the order that they are encountered during assembly determines where the linker will place them in memory. A sane project will define a memory map, which is simply a listing of sections in the order that they should appear in memory; by necessity this memory map must be present in the first file seen by the linker. Within z88dk this memory map is defined in the crt with the (unassembled) crt.asm file appearing first in the list of object files being linked to form the executable. An example of the typical memory map defined in crts appears in the next section.

Sections give programmers the power to place code and data at specific places in memory. These are some applications:

  • Programs destined for ROM must have their read-only (code) portion stored in ROM but their variables must be stored in RAM. The read-only portion of asm code can be assigned to a CODE section and the variables used by the asm code can be assigned to a DATA section. ORGing the CODE section to ROM and the data section to the RAM area solves the problem.
  • Programs using bankswitched memory must assign code and data to specific memory banks that are often fixed in size. There is normally address overlap as well with different banks occupying the same address range. A section can be created for each bank and these sections can have any ORG, including overlapping values. z80asm does not enforce size constraints on sections so it is up to the programmer to verify that sections do not exceed their maximum size when assembled.
  • Sometimes data must be aligned on certain byte boundaries. A section can be created with an ORG address set to a byte-aligned boundary and any data placed in it will have the desired alignment.

Crt Memory Map

The new C library makes extensive use of sections. Sections are defined at a fine-grain level, normally one per logical module. There is a section for string functions, a section for stdlib, a section for byte arrays, and so on. This fine-grained section assignment gives the programmer more control over where library functions are located.

When compiling C programs, the library supplies crts that are responsible for initializing the C environment before starting main(). They are also guaranteed to be the first file listed in the final linking step when an executable is made. This allows them to define the memory map for the compile.

The memory map is simply a list of sections that informs the linker what order to place code and data in memory. Most crts divide memory into three main sections: CODE, DATA and BSS.

The memory map is defined right at the beginning of the crt before any code or data is seen. A typical crt contains a prologue similar to this:

;;                zx spectrum if2 cartridge                  ;;
;;     generated by target/zx/startup/zx_crt_if2.m4          ;;
;;                                                           ;;
;;   16k ROM in 0-16k area, ram placement per pragmas        ;;

;; CRT AND CLIB CONFIGURATION ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

define CONFIG_ZX_IF2

include "../"
include ""
include "../"

;; SET UP MEMORY MODEL ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

include ""

and the memory model looks like this:



org __crt_org_code

section code_crt_init
section code_crt_main
section code_crt_exit
section code_crt_return
section code_crt_common

include "../../"
include "../../"

section code_lib
section rodata_lib

section code_compiler
section rodata_compiler

section code_user
section rodata_user




IF __crt_org_data

   org __crt_org_data


   IF __crt_model
      "DATA section address must be specified for rom models"


defb 0

include "../../"
section smc_compiler
section smc_lib
section smc_user

include "../../"
section data_compiler
section data_lib
section data_user




IF __crt_org_bss

   org __crt_org_bss


   IF __crt_model

      org -1



defb 0

include "../../"
section bss_compiler
section bss_lib
section bss_user


;; end memory model ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

The includes are aggregates of the fine-grained sections defined by the new C library. They can be examined in {z88dk}/libsrc/_DEVELOPMENT.

There are three main sections defined: “CODE”, “DATA” and “BSS”. Each of these may have an independent ORG address. If “DATA” does not have an independent ORG address, it will be appended to “CODE”. Similarly if “BSS” does not have an independent ORG address, it will be appended to “DATA”. Recall that any section with an independent address will be output in a separate binary file on assembly.

The smaller sections that follow “CODE”, “DATA” and “BSS” never have an independent ORG so they will always be absorbed as part of “CODE”, “DATA” or “BSS”. This is what it means to define a memory map – all sections are sequenced and their placement in memory is defined.

Some sections have been set aside to hold user code and data (code_user, rodata_user, smc_user, data_user, bss_user). Properly placing any user assembly code into these sections allows the compiler to generate binaries destined for ROMs. User assembly code is not confined to using these defined sections; by simply declaring new sections with their own ORG, a compile will output those sections as independent binary files. This has many applications as well.

What Happens If There Aren't Any Sections?

A no-name section is active when an asm file is opened. If no sections are defined at all in asm source files, all asm code and data will be placed into this no-name section and the project will assemble as expected into a single binary file. If the project imports code that has been assigned to sections, those sections, assuming no ORG address, will simply append to the no-name section.

In short, the asm code will assemble as if the assembler knew nothing about sections.

Note that the classic C library is not yet section aware so its code and data is defined in the no-name section.

C Translation to Assembly

The compilers translate C code to assembly language and at this point no distinction is made among any assembly input to z80asm. This means any assembly code in a project can interact with the translated C as if it were any ordinary assembly code.

Global Names (Variables and Functions)

The same scoping rules apply to the translated C code as to the ordinary assembly code: labels are local to a file unless they are declared PUBLIC and any external labels are only visible inside the file if they are declared EXTERN. This maps directly to C where global variables and functions are declared PUBLIC by the compiler inside the translated C and all other labels within the translated C are invisible outside the asm file. The C code can access external variables by using the standard C “extern” keyword which causes the compiler to declare those variables as EXTERN in the translated asm.

The only piece of information missing is how C mangles function and variable names. It's simple: all names are preceded with a leading underscore when the C is translated to assembler. There is one exception under sccz80 where functions can be given the “LIB” attribute in declarations and this prevents the leading underscore from being added but let's ignore that for now and see an example to solidify these ideas.

This example contains a small C program and a separate assembly subroutine that interact through global variables and functions. We've gone out of our way to avoid dealing with parameters passed to functions which is discussed in the next topic.

File: “main.c”

#include <stdio.h>
int a[5] = {0,1,2,3,4};
void negate_odd_a(void)
   int i;
   for (i=0; i!=5; ++i)
      if (a[i] & 0x01) a=-a;          // negate odd numbers in array a[]
extern void neg_and_triple_a(void);   // implemented elsewhere
   int i;
   neg_and_triple_a();                // call to asm subroutine
   for (i=0; i!=5; ++i)
      printf("a[%d]=%d\n", i, a[i]);
   return 0;

File: “nata.asm”

SECTION code_user

PUBLIC _neg_and_triple_a  ; export C name "neg_and_triple_a"

EXTERN _a                 ; access global C variable "a"
EXTERN _negate_odd_a      ; access global C function "negate_odd_a"


   call _negate_odd_a     ; call C function "negate_odd_a()"

   ; triple contents of array a[]

   ld b,5               ; array a[] has five members
   ld hl,_a             ; hl = &a[0]


   push hl              ; save address in array a[]

   ld e,(hl)
   inc hl
   ld d,(hl)            ; de = int member of a[]

   ld l,e
   ld h,d
   add hl,hl
   add hl,de
   ex de,hl             ; de = int * 3

   pop hl               ; hl = address in array a[] of int read

   ld (hl),e
   inc hl
   ld (hl),d            ; replace int with its value tripled
   inc hl               ; hl points to next array entry

   djnz loop            ; do it for all five members of array a[]

Both files are listed in the compile line (or they are individually assembled to object files and linked in a final linking step):

zcc +zx -vn -clib=new main.c nata.asm -o main

The C code contains three global names that will be exported as PUBLIC when the C is translated to asm:

  • int a[5] is declared “PUBLIC _a”
  • void negate_odd_a(void) is declared “PUBLIC _negate_odd_a”
  • main() is declared “PUBLIC _main”

The C code imports one name by declaring it “extern”:

  • extern void neg_and_triple_a(void) is declared “EXTERN _neg_and_triple_a”

The asm code exports one global name:

  • PUBLIC _neg_and_triple_a

The asm code imports two global names:

  • EXTERN _a which is the address of C's “int a[5]”
  • EXTERN _negate_odd_a which is the address of C's “void negate_odd_a(void)”

Tracing execution from main(), C calls function “neg_and_triple_a()” which has been declared extern. This means the linker must find it outside the translated C asm file. And it does find it in “_neg_and_triple_a” exported from file “nata.asm”. So the C function calls the assembly subroutine “_neg_and_triple_a”.

The assembly subroutine “_neg_and_triple_a” calls function “_negate_odd_a” which has been declared EXTERN in “nata.asm”. This means the linker must find it outside the file “nata.asm”. And it does – the name “_negate_odd_a” is exported from the C program by “void negate_odd_a(void){}” so the asm subroutine calls the C function “negate_odd_a()”.

The C function “negate_odd_a()” negates all odd members of global array a[].

On return, the asm subroutine “_neg_and_triple_a” then triples all members of the array a[]. It gets the address of the first element of “int a[5]” by declaring the name “_a” EXTERN. The C programs exports the address of the array a[] when it is translated to asm by declaring it “PUBLIC _a”.

The asm subroutine then returns to main(). main() prints the elements of a[] to screen.

The result:


As you can see, global names are very simply shared between C and asm.

In C, apparently global names can be declared “static”. The static keyword used in these cases actually means do not make the associated name global. Here are a few examples:

static int a[5];
static void negate_odd_a(void)

These names will not be made PUBLIC in the translated C and they will not be visible outside the C file where they are defined.

Local variables in functions can also be declared static. In these cases the static keyword means something entirely different – the variable is assigned permanent storage in memory rather than temporary storage on the stack. This is discussed in the next section.


The address of a function is indicated by its associated label. That label is shared by declaring it PUBLIC and accessed from another file by declaring it EXTERN. This has already been discussed in the previous section.

What has not been discussed is how parameters and return values are communicated with functions. An overview was given earlier in the documentation which should be reviewed before proceeding as that information will not be rehashed here.

Return Value

The return value from functions is communicated via a subset of registers DEHL. If the result is char (8-bit), it is returned in the L register. If the result is int or pointer (16-bit), it is returned in HL. If the result is long (32-bit), it is returned in DEHL.

Neither compiler supports returning structs.

In sdcc, floats/doubles are 32-bit and are communicated by DEHL as usual. In sccz80, floats/doubles are 48-bit and are treated differently. In the new C library, floats/doubles are returned in BCDEHL in the exx set. In the classic C library, floats/doubles are returned via the “primary floating point accumulator” which is 6-bytes of static memory at address “fa”. Because the classic C library uses static memory to communicate float values, float computations are not reentrant with the classic C library.


Both C compilers support parameter passing using three different linkages:

  • Standard. The parameter values are pushed onto the stack, the function is called, and then the stack is repaired by popping the values off. The function called collects parameter values from the stack without disturbing the stack pointer on return.
  • Fastcall. One parameter is passed to the function via register. The rules are exactly the same as for the return value already discussed. A subset of DEHL holds the parameter value (L for an 8-bit value, HL for a 16-bit value, DEHL for a 32-bit value). Neither compiler supports passing structs as a parameter. Floats/doubles under sdcc are 32-bit and are communicated through DEHL. sccz80 uses a 48-bit float/double type which is treated differently. Under the new C library, the float/double value is communicated by BCDEHL in the exx set. Under the classic C library, the float/double value is communicated through the “primary floating point accumulator” which is six bytes of memory at address “fa”. Because of this, floating point computations are not reentrant under the classic C library.
  • Callee. The parameter values are pushed onto the stack and the function is called. The callee is expected to repair the stack. This is especially convenient for assembly subroutines as the act of repairing the stack (removing the pushed parameters) is the same as popping the parameters into appropriate registers as required by the subroutine. Because the caller no longer repairs the stack, callee linkage can save hundreds of bytes in long programs.

Whether the C compiler generates standard, fastcall, or callee linkage for a particular function depends on the function's prototype. The prototype supplies attributes identifying which linkage to use.

Some examples:

// SCCZ80
int    __FASTCALL__ abs(int i);                                 // sccz80 fastcall linkage
double __CALLEE__   strtod(const char *nptr, char **endptr);    // sccz80 callee linkage
int                 printf(const char *format, ...);            // sccz80 standard linkage (note: vararg! see below)
int    abs(int i) __z88dk_fastcall;                             // sdcc fastcall linkage
double strtod(const char *nptr, char **endptr) __z88dk_callee;  // sdcc callee linkage
int    printf(const char *format, ...);                         // sdcc standard linkage

Note: Under sccz80, vararg functions use standard linkage but the caller must also set the A register equal to the number of 16-bit words pushed onto the stack.

With no decorations added, both compilers use standard linkage to call a function. However sccz80 and sdcc differ on the attribute decorations used to indicate fastcall and callee linkage.

That's not where the differences end. sdcc pushes its parameters on the stack in right-to-left order whereas sccz80 pushes in left-to-right order. Right-to-left order is most appropriate for C compilers because accessing the first parameter in a vararg parameter list is trivial. That's not the case for left-to-right order where it must be known how many parameters are pushed on the stack to calculate where the first vararg parameter is located (see note above). This error was made 35 years ago with Ron Cain's original small-C compiler and since then it has infected dozens of small-C derived compilers since.

These complications are taken care of automatically by the C compilers when C code is compiled and the library makes these details invisible. C code, if accepted by both compilers, can be compiled by either compiler without any changes to the source.

What this does affect is assembly code. An assembly subroutine can be called by the C compiler if it is given an extern function prototype. The extern prototype specifies what linkage to use and the C compilers will generate appropriate calls using the linkage specified. The assembly subroutine can then collect parameters according to the linkage contract.

There are three difficulties with this:

  • 1. Linkage Attributes are Different for the Two Compilers. This can be solved by supplying different prototypes depending on which compiler is doing the compiling.
#ifdef __SDCC
extern char *itoa(int num, char *buf, int radix) __z88dk_callee;
#ifdef __SCCZ80
extern char __CALLEE__ *itoa(int num, char *buf, int radix);
  • 2. Calls Through Function Pointers Must Use Standard Linkage. Under z88dk all calls through function pointers use standard linkage. This means if a function is implemented using another linkage and function pointer calls are to be allowed, the implementation must also offer a second entry point for standard linkage. This is resolved with a little macro magic.
#ifdef __SDCC
extern char *itoa(int num, char *buf, int radix);
extern char *itoa_callee(int num, char *buf, int radix) __z88dk_callee;
#define itoa(a,b,c) itoa_callee(a,b,c)
#ifdef __SCCZ80
extern char            *itoa(int num, char *buf, int radix);
extern char __CALLEE__ *itoa_callee(int num, char *buf, int radix);
#define itoa(a,b,c) itoa_callee(a,b,c)

Two functions are declared, one with standard linkage and another with callee linkage with name augmented with “_callee”. The implementation must provide both entry points. The magic is in the following #define. If the function is invoked with parameters, it is substituted with a call to the _callee entry point. When function pointers are assigned, they are assigned the name of the function only without parameters so that the substitution is not done and the function pointer will be assigned the address of the plain standard linkage function.

p = itoa(10000, p, 16);
f = itoa;
p = (f)(20000, p, 10);
p = itoa_callee(10000, p, 16);   // normal invocation uses callee linkage
f = itoa;                        // function pointer assigned standard linkage
p = (f)(20000, p, 10);           // function pointer calls use standard linkage
  • 3. Callee and Standard Linkage Have Parameters Pushed in Different Order for the Two Compilers. This can be solved by supplying different asm code to collect parameters depending on which compiler is active.
; char *itoa(int num, char *buf, int radix)
; (callee linkage from sdcc or sccz80)
SECTION code_user
PUBLIC _itoa, _itoa_callee, asm_itoa
  ; standard linkage entry point
   pop af      ; return address
   pop hl      ; hl = int num
   pop de      ; de = char *buf
   pop bc      ; bc = int radix
   ; restore stack
   push bc
   push de
   push hl
   push af     ; return address
   pop af      ; return address
   pop bc      ; bc = int radix
   pop de      ; de = char *buf
   pop hl      ; hl = int num
   ; restore stack
   push hl
   push de
   push bc
   push af     ; return address
   jr asm_itoa
   ; callee entry point
   pop af      ; return address
   pop hl      ; hl = int num
   pop de      ; de = char *buf
   pop bc      ; bc = int radix
   push af     ; return address
   pop hl      ; return address
   pop bc      ; bc = int radix
   pop de      ; de = char *buf
   ex (sp),hl  ; hl = int num
   ; params have been removed from the stack per callee contract
   ; assembly entry point with register API
   ; bc = radix
   ; de = buf
   ; hl = num
   ; stack = return address
   ;; implementation here
   ;; return char* in HL here

This code snippet, in combination with the C function prototypes in point 2, supplies an entry point for function pointers (_itoa), an entry point for callee linkage (_itoa_callee) and an assembly language entry point with parameters in registers (asm_itoa). All that and it supports both C compilers equally.

The above example is nearly how the new C library is written to accommodate both C compilers. When libraries are discussed later it will be revealed that the basic unit of code is the file so that if a certain library function is required by the program, the entire file's contents that it is found in will be pulled out of the library and attached to the program. For this reason, it is desirable to compose libraries of many minimally sized files normally one subroutine per file. So in the example above, the new C library would separate the asm implementation (asm_itoa), the standard linkage (_itoa with a jump to asm_itoa) and the callee linkage (_itoa_callee with a jump to asm_itoa) into three separate files. The new C library also has two different header files, one for sdcc and one for sccz80, generated from a single prototype header file using macros and m4 to automatically build them.

Reserved Registers

sccz80 does not reserve any registers for itself. Assembly language subroutines can use whatever registers they want that are allowed by the target.

sdcc, however, expects the two index registers ix and iy to be unchanged. Assembly language subroutines must preserve those registers if they are modified. If the C code is compiled with ”–reserve-regs-iy” then sdcc will not use iy and iy is safe to modify. Take note of this known bug associated with ”–reserve-regs-iy”.

The Stack Frame

Inside a C function, a stack frame is constructed. The stack frame is an area on the stack that contains the parameters passed to the function and the local non-static variables declared inside the function. sccz80 and sdcc differ in how the stack frame is formatted and how it is addressed.

Until this point the concern has been on how C code can call assembly language subroutines and how the assembly language subroutines gather passed parameters and return a value. With this section is going to examine how to go the other way with assembly language calling a C subroutine.


Inlined Assembly

Assembly language can be inlined in C code if it is surrounded by ”#asm” and ”#endasm” tags. Note that sdcc's back end is not used by z88dk so all assembly language instructions, inlined assembly included, is processed by z80asm and therefore use standard Zilog mnemonics.

The C compilers ignore inlined assembly code and simply generate code around it. Inlined assembly can access any variables and functions defined in the local file and can make use of PUBLIC and EXTERN to export and import names as discussed before. They can also access local variables declared on the stack as described in the last section.

Inlined assembly is often misused in the z80 community. It's meant to interject a small amount of assembly code into an otherwise C function. But quite often C functions are declared with their entire bodies written in assembly language using inlined assembler. The preferred way to implement assembly subroutines called from C was described earlier: prototypes are supplied in a header file to tell the C compiler how to call the asm functions and then a separate asm file contains the asm implementation.

  • C prologue can change over time or across compilers
  • cannot place code in arbitrary section
  • separation of C and asm is cleaner organizationally and increases portability
  • separation makes it easy to create libraries of asm routines later
  • can easily supply an asm interface
  • compilers may have moved data into a register


Magazine Articles


temp/front.1587415249.txt.gz · Last modified: 2020/04/20 21:40 by dom
Recent changes RSS feed Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki