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:
Mandelbrot | Twitter Client | File Transfer |
---|---|---|
![]() | ![]() | ![]() |
Ninjajar! | 3D Globe | Forest Raider Cherry |
![]() | ![]() | ![]() |
Information can be found here: https://github.com/z88dk/z88dk/wiki/license
Benchmarks can found on the new wiki: https://github.com/z88dk/z88dk/wiki/Benchmarks
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* | ||
SDCC | ||||||||
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 |
Notes:
Installation information can be found on the new wiki: https://github.com/z88dk/z88dk/wiki/installation
An overview of the tools can be found on the new wiki: https://github.com/z88dk/z88dk/wiki#tools
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.
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)
SCCZ80 is z88dk's native C compiler.
Information on the classic library can be found on the new wiki: https://github.com/z88dk/z88dk/wiki/Classic-Overview
An introduction to the new library can found on the new wiki: https://github.com/z88dk/z88dk/wiki/Introduction
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
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.
In general any sdcc option can be appended to the compile line.
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.
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.
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.
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.
Known issues can be found on the new wiki: https://github.com/z88dk/z88dk/wiki/Suite-deficiencies
Information on datatypes can be found on the new wiki: https://github.com/z88dk/z88dk/wiki/Datatypes
Function call linkage can be found on the new wiki: https://github.com/z88dk/z88dk/wiki/CallingConventions
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”.
how to ensure zcc_opts.def does not grow indefinitely and is erased at an appropriate time.
Both compilers support user-selected optimization levels.
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”.
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”.
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
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.
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
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.
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.
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:
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.
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:
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.
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.
This information can be found on the new wiki: https://github.com/z88dk/z88dk/wiki/WritingOptimalCode
The introduction to newlib can found on the new wiki: https://github.com/z88dk/z88dk/wiki/Introduction
Documentation detailing the configuration for newlib can be found on the new wiki: https://github.com/z88dk/z88dk/wiki/Configuration
Documentation detailing the newlib CRT can now found on the new wiki: https://github.com/z88dk/z88dk/wiki/CRT
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 exx ld bc,$490F ; AC' = pi ld de,$DAA2 ; math48 uses BCDEHL to hold a double ld hl,$2182 exx 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:
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 start: ld hl,2 .... ret
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.
Details on the header files available in newlib is now located here: https://github.com/z88dk/z88dk/wiki/Header-Files
This information has been moved to the new wiki: https://github.com/z88dk/z88dk/wiki/Library-in-Depth
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.
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 asm_strcpy: push de xor a loop: cp (hl) ldi jr nz, loop pop hl dec de ret
FILE 2: “Hello.asm”
PUBLIC Hello EXTERN asm_strcpy Hello: ; enter : de = destination string pointer ld hl,hello_s jp asm_strcpy hello_s: defm "Hello" defb 0
FILE 3: “World.asm”
PUBLIC World World: ; de = destination string pointer ld hl,world_s jp asm_strcpy world_s: 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.
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.
SECTION code org 0 PUBLIC main EXTERN display, input main: call initialize loop: call display call input jr loop SECTION data org 32768 PUBLIC maze maze: defm "XOXOXXXXXOXXXO" defm "XOXOOOOOOOOOOO" defm "XOXXXXXOXXXXXO" ... SECTION bss PUBLIC workspace, _score workspace: defs 128 _score : defs 2 SECTION code PUBLIC calculate EXTERN l_mult calculate: ld hl,(_score) ld de,10 call l_mult ret
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:
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 "../crt_defaults.inc" include "crt_target_defaults.inc" include "../crt_rules.inc" ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; SET UP MEMORY MODEL ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; include "memory_model.inc" ...
and the memory model looks like this:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; SECTION CODE 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 "../../clib_code.inc" include "../../clib_rodata.inc" section code_lib section rodata_lib section code_compiler section rodata_compiler section code_user section rodata_user SECTION CODE_END ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; SECTION DATA IF __crt_org_data org __crt_org_data ELSE IF __crt_model "DATA section address must be specified for rom models" ENDIF ENDIF defb 0 include "../../clib_smc.inc" section smc_compiler section smc_lib section smc_user include "../../clib_data.inc" section data_compiler section data_lib section data_user SECTION DATA_END ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; SECTION BSS IF __crt_org_bss org __crt_org_bss ELSE IF __crt_model org -1 ENDIF ENDIF defb 0 section BSS_UNINITIALIZED include "../../clib_bss.inc" section bss_compiler section bss_lib section bss_user SECTION BSS_END ;; 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.
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.
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.
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[] return; } extern void neg_and_triple_a(void); // implemented elsewhere main() { 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" _neg_and_triple_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] loop: 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[] ret
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:
The C code imports one name by declaring it “extern”:
The asm code exports one global name:
The asm code imports two global names:
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:
a[0]=0 a[1]=-3 a[2]=6 a[3]=-9 a[4]=12
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.
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:
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) // SDCC 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:
#ifdef __SDCC extern char *itoa(int num, char *buf, int radix) __z88dk_callee; #endif #ifdef __SCCZ80 extern char __CALLEE__ *itoa(int num, char *buf, int radix); #endif
#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) #endif #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) #endif
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.
// BEFORE MACRO SUBSTITUTION BY ZCPP p = itoa(10000, p, 16); f = itoa; p = (f)(20000, p, 10); // AFTER MACRO SUBSTITUTION BY ZCPP 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
; char *itoa(int num, char *buf, int radix) ; (callee linkage from sdcc or sccz80) SECTION code_user PUBLIC _itoa, _itoa_callee, asm_itoa _itoa: ; standard linkage entry point IFDEF __SDCC 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 ENDIF IFDEF __SCCZ80 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 ENDIF jr asm_itoa _itoa_callee: ; callee entry point IFDEF __SDCC pop af ; return address pop hl ; hl = int num pop de ; de = char *buf pop bc ; bc = int radix push af ; return address ENDIF IFDEF __SCCZ80 pop hl ; return address pop bc ; bc = int radix pop de ; de = char *buf ex (sp),hl ; hl = int num ENDIF ; params have been removed from the stack per callee contract asm_itoa: ; assembly entry point with register API ; bc = radix ; de = buf ; hl = num ; stack = return address ;; implementation here ;; return char* in HL here ret
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.
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”.
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.
……
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.