Multi-source Z80 assembly programming for CP/M
This is undeniably a niche subject, even among retrocomputing enthusiasts. Not many people are still running CP/M machines, and even fewer are writing new software for them. Of the people who are, I suspect that few are writing programs of sufficient complexity that they need to be built from multiple Z80 assembly-language modules.
For the vanishingly small number of people who need to do this, I describe a way to build multi-source assembly programs using the Microsoft Macro80 and Link80 tools. These tools are actually well-documented as individual utilities; it's using them together that is a bit of a challenge.
There are other assembly tools for CP/M, but I've not found any that
actually run efficiently on a CP/M machine,
have full macro support, and can handle multiple
source modules. I do not know what the copyright status of these
old utilities is; they are widely available, but (I think) not yet
in the public domain. You can get
the assembler m80.com
, and linker l80.com
from RetroArchive, among other places.
In this article I only provide skeleton source code; full source is available in my GitHub repository.
The problem
The assembly source file main.asm
contains this code:
.Z80 ORG 0100H main: ; Print the message LD HL, message CALL puts CALL newline ...
Here puts
is a subroutine that prints the text in the
address indicated by the HL register, and newline
is a
subroutine that prints a newline.
These routines, puts
and newline
are defined
in another assembly source file called conio.asm
.
This source contains other routines for controlling the console, but
they won't be used in this example.
The problem is to construct a CP/M .COM
file from
these two source modules,
in such a way that the addresses of the subroutines are properly
resolved.
Note:.
We need to set the program's overall starting point in an assembly file that begins with the definitionORG 0100H
. This is the address at which all CP/M programs are loaded, and where the command processor will start execution
Exposing the global symbols
The first step is to tell the assembler which symbols are to be
exported from the source, and thus made available to other modules.
In conio.asm
we have:
.Z80 global puts, newline puts: ; Print the string whose address is in HL ... RET
The label puts:
is turned into a global (exported)
symbol using the global
keyword. Macro80 has a
shorthand notation for this, using two colons:
puts::
This shorthand defines the label and exports it. This is slightly more convenient, but I don't use it because other assemblers don't support it.
Importing the global symbols
The source module main.asm
will call puts
and
newline
, but these addresses are not defined in the same source,
but in conio.asm
. Along with exporting the symbols from
the file where they're defined, we must import them where they're used.
So in main.asm
we have:
external puts external newline
I prefer to put these external
definitions into a separate
file, and use include
to insert this file into the main
assembler source. This provides a single place where all the public
subroutines in a module can be listed and documented (as we
do wither header files in C).
Compiling the modules
We compile the two source modules completely separately. The syntax for this with Macro80 is as follows:
A>m80 =main.asm No Fatal error(s) A>m80 =conio.asm No Fatal error(s)
If successful, this operation will produce the two relocatable
object files main.rel
and conio.rel
.
We need to combine these into a single .COM
file.
Linking the modules
The command-line syntax for Link80 is a little peculiar, by contemporary standards. In this case we need:
A>> l80 main,conio,test/n/e Link-80 3.44 09-Dec-81 Copyright (c) 1981 Microsoft Data 0103 02A8 < 421> 44628 Bytes Free [0000 02A8 2]
The linker takes as its argument a list of files separated by commas,
along with some
other options marked with switches. In this case, main,conio
indicates the two .rel
files, and test/n/e
sets the name of the output file "test.com". The "/e" switch just means
"exit"; without this we remain at the linker's command prompt.
Note that the modules are loaded in the order specified on the
command line, and main
must be first. This is because
main.asm
contains the program's entry point, at address
100h.
There's a lot more involved in creating complex assembly-language programs, of course; but hopefully this is enough to get started.