9

I'm writing a very basic OS for my homebrew Z80 computer. As an absolute assembly language beginner, I managed to obtain a working "os plus memory monitor" that can show memory content and load bytes to RAM. In doing so, I wrote some "system routines" to interface some I/O devices. For example, I have a "Printc" routine that reads a byte and draws the corresponding ASCII character on the screen.

This is working with the code built by the assembler because the assembler decides where to put the routine's first byte and uses that address when encounters a jp command with the same label.

Now, I'd like to call the Printc routine from a dynamically loaded program. I'm able to tell where the assembler placed the routine's first byte in the ROM thanks to the -l flag, that produces output containing:

...
Print:    equ $043a
Printc:    equ $043e
Readc:    equ $0442
Readline:    equ $0446
...

I can now write a program like this:

ld a, 0x50     ; ASCII code for P
call 0x043e    ; Calls Printc

This program prints the letter P successfully: I called my Printc routine using its memory address.

This is fine as long as I don't change any assembly code preceding the Printc declaration in my "os". If I do, the Printc label will be assigned to another address and my existing program will stop working.

What is the canonical solution for this type of problem? The only one that comes to my mind is to create a "jump table" at the beginning of my assembly code, before any import, with the list of system calls, hoping that they will obtain every time the same address. Something like:

...
; System routines
Sys_Print:
call Print
ret
Sys_Printc:
call Printc
ret
.... and so on

But this seems pretty hackish... Is it possible to instruct the z80asm assembler to place the routine's first instruction at a memory address decided by me?

4
  • 1
    When you get a bit further you may want to look into getting CP/M up and running. This will give you access to a very large software library. Dec 29 '20 at 20:24
  • 1
    The "canonical solution" is surely to use a software interrupt (UUO, trap, …) to transfer control to a dispatcher, so that the calling code does not need to know the address of the called routine. Dec 29 '20 at 21:22
  • 2
    The software interrupts are called "restarts" on the Z80, and have fixed locations: rst #. Dec 30 '20 at 12:43
  • Where is z80asm run? On Linux? Dec 30 '20 at 13:33
10

What is the canonical solution for this type of problem?

There isn't any canonical solution, but many variants, all to be found usable.

The only one that comes to my mind is to create a "jump table" at the beginning

Which is a perfect good one. Except, usually one would use jumps instead of calls to reduce code length, speed up execution, and reduce stack load.


JUMP_TABLE:
PRINT    JP  _I_PRINT    ; First Function
READC    JP  _I_READC    ; Second Function
...

But this seems pretty hackish...

No, many 8080 and Z80 systems work like that.

The main step ahead is that all entry points are at a single defined location and sequence.

Is it possible to instruct the z80asm assembler to place the routine's first instruction at a memory address decided by me?

Sure, use an ORG to put it at whatever address you want it (*1). But that would be hackish or at least not very forward looking. Having such a jump table at a defined address is a great start. Of course it eats up some space. Three bytes per entry, but only two being the address. Wouldn't it be better to just make an address table? Like:

SYS_TABLE:
         DW    _I_PRINT    ; First Function
         DW    _I_READC    ; Second Function

Calling a function would be like

         LD    HL, (SYS_TABLE+0)   ; Load Address of First Function - PRINT
         JP    (HL)                ; Do it

This can easy be combined with kind of a function selector:

SYS_ENTRY:
         PUSH  HL
         LD    H,0
         LD    L,A
         ADD   HL,HL
         ADD   HL,SYS_TABLE
         JP    (HL)

Now even the jump table can be moved around in ROM (or RAM) as needed.

Calling it would be by using a function number - like many OS have - simply put the function number in A and call the default system entry point (SYS_ENTRY).

         LD    A,0   ; Print
         CALL  SYS_ENTRY

Of course it gets more readable if the OS provides a set of equates for the function numbers :)

So far the program loaded does still need to know either the table address (SYS_TABLE) or the entry point for the selector (SYS_ENTRY). The next level of abstraction would move their address into a defined location, like 0100h, best maybe in form of a JP, so any user program always calls that fixed address (0100h) no matter if your OS is in ROM or RAM or wherever.

And yes, if this seems familiar, it is, as it's the same way CP/M handles system calls, or MS-DOS does.

Speaking of MS-DOS, it provides an additional (and more common known way) to call an OS function, so called software-interrupts, like the well known INT 21h. And there's something quite similar the Z80 (and 8080 before) offers: A set of eight distinct ReSTart vectors (0/8/16/...). Restart 0 is reserved for reset, all others can be used. So why not using the second (RST 8h) for your OS? Function calls then would look like:

         LD    A,0   ; Print
         RST   8h

Now user program code is as much separated from OS structure and memory layout as possible - without the need of any relocator or whatsoever. The best part is, with a little fiddling, the whole selector fits into the 8 bytes available, making it optimal coding.


A little suggestion:

If you go for any of these models, make sure that the first function (0) of your OS will be a call providing information about the OS, so programs can check for compatibility. At least two basic values should be returned:

  • ABI release number
  • Maximum supported function number.

The ABI release number may or may not be the same as a version number, but does not have to. It must be increased with every API change. Together with the maximum supported function number this information can be used by a user program to quit graceful in case of incompatibility - instead of crashing midway. For luxury the function may as well return a pointer to a

  • Structure containing further information about OS like
    • readable name/version
    • Addresses of various sources
    • 'special' entry points
    • Machine information like RAM size
    • available interfaces, etc.

Just saying...


*1 - And no, other than some may assume, ORG should never ever add padding or alike on its own. Assemblers doing so are a bad choice. Org should only change the address level, not define what is in any area 'jumped over'. Doing so adds may levels of potential errors - at least as soon as some advanced ORG usage is done - believe me, ORG is a very versatile tool when doing complex structures.

In addition filling 'void' areas with some padding will result in this padding being part of the program instead of untouched memory, taking away a main tool for later patches: uninitialized EPROM space. By simply not defining and not loading these areas, they will stay in whatever the cleared state is (all ones in case of EPROM) and can be later programmed - for example to hold some code during debugging, or to apply a hot fix without the need of programming new devices.

So undefined memory should be just that, undefined. And that's why even the earliest assembler output/loader formats (think Motorola SREC or Intel HEX) used for program delivery to anything from ROM fabrication all the way to user programs supported a way to leave out areas.

Long story short: If one want's to have it filled, it has to be done expcit. z80asm does it right.

9
  • What is "ABI" in this context? Application binary interface? Dec 30 '20 at 13:32
  • @PeterMortensen Exact. The way the OS call is coded in a program binary.
    – Raffzahn
    Dec 30 '20 at 13:40
  • 3 bytes per entry, 2 being the address, then you save the non-address byte, but how much bigger do you make the calls?
    – user253751
    Dec 30 '20 at 14:05
  • @user253751 Not really sure what you're asking for? The main point in creating an ABI is to make binary code include as least information that needs fixup during load as possible - at best zero.
    – Raffzahn
    Dec 30 '20 at 14:45
  • 1
    Thank you, @Raffzahn. Definitely the most complete answer, I accepted it. Thank you very much.
    – penguin86
    Jan 2 at 7:00
12

The problem with Z80ASM specifically is that it takes the assembly input and spits out a static binary file. This is good and bad.

In "normal" systems, address assignment is, inevitably, the responsibility of the linker, not the assembler. But assemblers are simple enough that many skip that aspect of the build cycle.

Since Z80ASM spits out literal binary images, rather than "object" files, it does not need a linker. But it also won't let you necessarily do what you want to do.

Consider the ubiquitous ORG directive.

ORG tells the assembler what the starting (origin -- thus ORG) address is for the upcoming assembly code.

This means if you do this:

    ORG 0x100
L1: jp L1

The assembler will assemble the JP instruction to JUMP to the address of 0x100 (L1).

BUT, when it spits out he binary file, the file will be just 3 bytes. The jump instruction, followed by 0x100 in the binary format. There's nothing in this file that tells, well, anything, that it must be loaded at 0x100 to "work". That information is missing.

If you do:

    ORG 0x100
L1: jp L2

    ORG 0x200
L2: jp L1

This is going to produce a file that is 6 bytes long. It's going put those two JP instructions right after each other. The only thing the ORG statement is doing is telling what the labels should be. This is not what you would expect.

So, simply adding a ORG to your file will not do what you want to do, unless you have an alternate method to load the code at the specific place that you want your code to be.

The only way to do that with Z80ASM out of the box is to pad your output file with blocks of bytes, empty space, that will fill the binary up to put your code at the right place.

Normally, this is what the linker does for you. The linker's job is to take your disparate pieces of code, and create a resulting binary image. It does all this for you.

On my assembler, which did not use a linker, it produced an Intel HEX file format which includes the actual address for each block of data.

So, for the previous example, it would have created two records. One destined for 0x100, the other for 0x200, and then the hex loading program would put things in the right place. This is another alternative, but Z80ASM doesn't seem to support that either.

So.

Z80ASM is great if you're making ROM images starting at, say, arbitrarily, 0x1000. You would ORG that, get a resulting binary, and download the entire file burned in to an EPROM. It's perfect for that.

But for what you want to do, you'll need to pad you code to move your routines to the right places, or come up with some other loader scheme to manifest this for you.

1
  • Thank you very much for your answer: it contains a precious information that is not present in any of the other answers (the fact that the assembler doesn't take care of adding the padding needed to match the real memory positions).
    – penguin86
    Dec 30 '20 at 18:02
5

The org directive should do specifically what you ask. However, z80asm is a little simplistic in its output format. Instead you can use ds to place routines at particular addresses:

        ds     0x1000
printc:
        ...
        ret

        ds     0x1100-$
readc:
        ...
        ret

This will always put printc at 0x1000 and readc at 0x1100. There are many disadvantages. Should printc grow larger than 0x100 the program won't assemble and you'll need to break printc apart in some fashion and put the extra code somewhere else. For that and other reasons a jump table at a fixed location in memory is easier to manage and more flexible:

           ds    0x100
v_printc:  jp    printc
v_readc:   jp    readc
           ...

Another technique is to use a single entry point and choose the function using a value in A register. This will at least be a bit slower but does mean that only a single entry point needs to be maintained as the operating system changes.

And instead of doing a CALL to the entry point place it at one of the special RST locations (0, 8, 0x10, 0x18, 0x20, 0x28, 0x30, 0x38) where you can use RST 0x18 as a single byte call to memory location 0x18. Usually RST 0 and RST 0x38 are avoided since one they are the pwoer-on entry point and interrupt model 1 handler locations respectively.

3
  • 2
    I don't think you're right about org; this will not insert the necessary padding to put the routine at the right location.
    – OmarL
    Dec 30 '20 at 3:12
  • 2
    Good catch; z80asm is not clever about padding. Changed the first part to use ds instead. Dec 30 '20 at 3:58
  • 1
    Very interesting! I checked out ds and is in fact adding the required padding. Thank you very much!
    – penguin86
    Jan 2 at 7:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.