Table of Contents

Amiga, Assembler and System

Most of assembler programming courses for Amiga ignore its operating system and ways of using it. Authors pay attention to using the CPU and chipset directly via their hardware registers, bypassing the OS. It is often unavoidable when writing games or scene demos, because it is the fastest way. On the other hand the operating system can save a lot of work when writing application programs. Some of OS features may be also useful in a game or demo.

System Libraries

Operating system functions are grouped by purpose into libraries. Most of basic libraries reside in Amiga ROM memory, called Kickstart. The rest of them is placed on the harddrive or floppy disk, depending on system boot device. For the start we will use the two most important system libraries: exec.library and dos.library. The first is the system kernel. It controls multitasking, launching of processes, interprocess communication, memory management and so on. The second one is responsible for input/output operations, access to files and devices.

Library Base

In every programming language a system library is accessed via memory pointer to a data structure called library base. This pointer is dynamic, it means it is different in different Amiga models, operating system versions and even may be different for consecutive starts of the same Amiga. Address of the library base is obtained by opening the library. So any program wanting to use a library has to open it first. Library is opened with OpenLibrary() function from exec.library. Value returned by this function is just a library base.

There is one exception of the above rule, which is exec.library itself. As it is used to open other libraries, it has to be opened automatically by the system at boot time. Then applications can call OpenLibrary() and other Exec functions. Address of the base of exec.library is always stored at fixed memory location $00000004.

Library base spreads in memory in both directions from the base address. Below the base (towards higher memory addresses) there are library data. It can be either public, to be used by applications, or private. Above the base (towards lower memory addresses) there is library functions jump table. The table contains real addresses of functions code. The jump table allows for compatibility between different versions of operating system and allows for dynamic loading of system libraries from mass storage devices. While addresses of function entries are variable, applications always use only jump table offsets, which are constant.

Calling Library Functions

Before calling a function, its arguments have to be placed in processor registers. Registers assignment for each function is described in its documentation (autodocs). Usually numeric arguments are placed in data registers starting from D0. Pointers are placed in address registers starting from A0. Then the library base pointer is placed in A6. It has to be A6 register, as most of functions use the library base internally. Finally function is called via jump table by executing JSR jump with offset relative to A6 register. Here is an example which calls OpenLibrary() function:

           MOVEQ   #34,D0
           LEA     DosName,A1
           MOVEA.L $00000004,A6
           JSR     -552(A6)

           ...

DosName:   DC.B    "dos.library",0

According to the docs, OpenLibrary() takes two arguments. Minimum requested version of the library is a numeric argument and should be placed in D0. Pointer to library name is placed in A1. The name is an usual zero-terminated string of ASCII characters, it is placed in the code with DC.B assembler directive. Then, as OpenLibrary() function is in exec.library, the library base is loaded from fixed location $00000004. The last step is to jump into the jump table. Exactly 552 bytes before the ExecBase, there is JMP instruction, which then directs the processor to the real code of the function called. Then the whole sequence of system function call may be illustrated as below:

JSR -552(A6) JMP $xxxxxxxx library base application Kickstart function code RTS –552

Figure 1: System function call sequence.

Defining Offsets

Code looking like "JSR -552(A6)" is not easily readable. An obvious step is to define offsets between the library base and jump entries of functions (named shortly just "function offsets") as named constants, like this:

           SysBase     = 4
           OpenLibrary = -552

           MOVEA.L SysBase,A6
           JSR     OpenLibrary(A6)

Declaring these offsets and other constants by hand is easy in small programs using a few functions. Larger projects may use system header files for assembler (with *.i filename extension). These files contain definitions of function offsets, offsets of fields of system structures and other system constants.

Preserving Registers Contents

A convention used throughout the whole AmigaOS treats CPU registers D0, D1, A0 and A1 as scratch registers. Every OS function is allowed to change them and leave at unspecified values (D0 register usually contains the function result). Other registers must not be changed. Then if a system function uses them, it has to save them on the processor stack and restore at exit.

"Hello World"

Let's start with classic "Hello World" program, writing a text in a CLI window. We will use PutStr() call, which sends a text to program standard output. This function is located in dos.library, so we'll have to open it and then close. Offsets of functions used:

           SysBase         = 4
           OpenLibrary     = -552
           CloseLibrary    = -414
           PutStr          = -948

The whole program is listed below. It contains 14 processor instructions:

           LEA     DosName,A1
           MOVEQ   #36,D0
           MOVEA.L SysBase,A6
           JSR     OpenLibrary(A6)

           TST.L   D0
           BEQ.S   NoDos

           MOVE.L  #Hello,D1
           MOVEA.L D0,A6
           JSR     PutStr(A6)

           MOVEA.L A6,A1
           MOVEA.L SysBase,A6
           JSR     CloseLibrary(A6)
 
NoDos:     CLR.L   D0
           RTS
 
DosName    DC.B    "dos.library",0
Hello      DC.B    "Hello World!",10,0

The first four instructions open dos.library. Then TST.L tests for success. Zero in D0 means fail, so program jumps straight to exit. I set 36 as the minimum accepted version of dos.library, because PutStr() call has been added to the library in this version (Kickstart 2.0). When someone runs this code on Amigas with older Kickstart, it will just exit immediately after jumping to "NoDos" label.

The next block is PutStr() call. Dos.library is a bit unusual, as it expects all, even non-numeric arguments (like pointers to strings) in data registers. The base address of dos.library is just moved from D0 to A6.

The third block is freeing allocated resources, it means closing dos.library in our case. Exec.library needs not (and should not) to be closed, as it is opened permanently. The last step is clearing D0 register. Its value at program exit is relayed to the shell process and considered as program execution result. Zero value means execution without errors.

As we have used standard output for text printing, our application is compatible, for example, with output redirection. If we call it like this:

hello >RAM:pff.txt
the text "Hello World!" will be saved into file RAM:pff.txt.

AmigaOS uses (mostly) regular C language strings, terminated with $00 byte. However in assembler terminating zero is not added implicitly, so it has to be added in DC.B defining the string. Newline character (ASCII 10) has been also added to force a new line in the console.

Final remark

The program discussed here will execute properly when launched from a console window. However if one tries to create an icon for it and to run it from Workbench by icon doubleclick, it will crash. It simply lacks proper startup code, which includes handling of Workbench launch. I've skipped it for simplicity, as startup code is discussed in another article.

May 19, 2018