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, and linker 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:

       ORG    0100H

        ; 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.

We need to set the program's overall starting point in an assembly file that begins with the definition ORG 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:


        global puts, newline

        ; Print the string whose address is in HL

The label puts: is turned into a global (exported) symbol using the global keyword. Macro80 has a shorthand notation for this, using two colons:


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 "". 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.