Amiga Machine Code Letter X - Trackdisk

Amiga Machine Code - Letter X

Before we dive in, a little warning is in place. This is the hardest piece of machine code, that I yet had to decipher, and there is no doubt, that we are almost at the limits of readability with regards to 68K assembly. The code would have been far more readable if written in C. 😃

We need to start softly, with an explaination of the physical disk, and then continue with a brief introduction to the AmigaOS and it’s Trackdisk device. Later, we loook at the mc1006 program from Disk1 that reads and writes to disk.

Btw, we have reached Letter X of the Amiga Machine Code course. As always, make sure to read the letter, since I won’t go through all the details… Well, in this case I’ll make an exception, more about that later. 😉

The Physical Disk

The Amiga disk consists of 80 cylinders, where cylinder 0 is on the outermost ring, and cylinder 79 is on the innermost. Each cylinder has two tracks, one on the upper side, and the lower side. The tracks on the upper sider have even numbers, while the tracks on the lower side has uneven numbers. The track is divided into 11 sectors, that can store 512 bytes. A simple calculation reveals the storage space of an Amiga disk.

$Storage = 80 cylinders * 2 tracks * 11 sectors * 512 bytes = 880 Kb$

When communicating with the disk drive, we will use block numbers to designate the locations on the disk. This is much easier than to specify track and sektor. The block number is calculated like this:

$Block = 2 * 11 * Cylinder + 11 * side + sector$

Cylinder Side Sector Block
0 0 0 0
0 0 1 1
0 1 0 11
0 1 1 12
79 0 0 1.738
79 0 1 1.739
79 1 0 1.749
79 1 1 1.750

There’s a good description of Amiga floppy drives in the book Amiga Disk Drives Inside and Out. Also take a look at Gary Browns walktrough of How Floppy Disk Drives Work. Great stuff 🚀.

The mechanics of the disk drive is such, that it has a motor for spinning the drive, and a stepper motor for moving the read and write heads accross the cylinders. The read and write heads, for the lower and upper side of the disk, are physically attached to the same mechanics. E.g. when track 0 on the upper side is read, the disk drive can also read track 1 on the lower side, without moving the heads. This is an enginious design, since data is often stored sequentially.

AmigaOS and the Trackdisk Device

The AmigaOS is a mikrokernel operating system, where the kernel takes care of things such as memory, loading auxillary libraries and providing a message system. The Kernel of the AmigaOS is the Exec library and it was written by Carl Sassenrath around 1985. On his hompage he writes the following:

Introduced multitasking to the world of personal computers in 1985 with the Amiga Operating System Executive.

The AmigaOS was a dramatic departure from previous home computer systems, in that it allowed for multitasking. This was something quite new for home computers at that time.

As mentioned in the book The Future Was Here (chapter 6), Carl Sassenrath were given free hands to develop an OS for the Amiga. He choose the mikrokernel architecture, because it was better suited for the limited hardware of the first Amiga. E.g. the AmigaOS could load libraries, devices, and ressources, when they were needed, and this flexibility allowed the AmigaOS to keep it’s footprint low.

However, since we are dealing with a mikrokernel architecture, we can’t just communicate with the disk, because the Kernel doesn’t know how to do that. Instead, we have to open a device driver called the Trackdisk, that will take care of disk operations.

The AmigaOS communicates with devices through messages, that a routed through the message system in the Exec library. A message can be sent to messageports in other tasks, which allows of a request/responce communication flow between taks. The message system also allows for extending the communication with extra data.

The AmigaOS is a really facinating construction, and you should read up on it yourself. There is a really good description of how the AmigaOS works in part II of the book The Kickstart Guide to the Amiga, over at Archive.org. It covers the AmigaOS as it was, when Kickstart 1.2 ruled the day. It’s a read well worth your time.

The Trackdisk Program

In this section, we’ll take a closer look at the mc1006 program. The program contains a subroutine called sector, that uses the Trackdisk device to read and write to disk. The code can also be found on Disk1.

The sector subroutine, is meant to be used in your own programs, but the subroutine itself is very hard to read, without understanding the AmigaOS - especially the Exec library. Here’s the documentation for the sector subroutine.

sector
Description: reads and writes to disk
Syntax:      sector(buffer, diskStation, block, length, mode)
ML:          d0 = mc1006(a0, d0, d1, d2, d3)
Arguments:   buffer = pointer to input or output buffer
             diskStation = the disk station to read from or to write to
             block = the start block
             length = the length in number of blocks
             mode = The mode. 1 = READ, 2 = WRITE, 3=UPDATE
Result:      no result is given

Before we look at the code, it’s important to get aquainted with some of the methods from the Exec library, that the code uses. The subroutine starts by calling AllocSignal to get a signal number. This signal number will be used when we communicate with the trackdisk device.

AllocSignal
Library:     exec.library
Offset:      -$14A (-330)
Description: allocates a signal bit
Syntax:      signalNum = AllocSignal(signalNum)
ML:          d0 = AllocSignal(d0)
Arguments:   signalNum = the desired signal number; 0-31, or -1 for no preference
Result:      signalNum = the signal bit number allocated; 0-31, or -1 if no signals are available

Next, we need to call FindTask, that will return a pointer to our task. This pointer will be used to designate the task that needs to be signaled.

FindTask
Library:     exec.library
Offset:      -$126 (-294)
Description: finds a task by name, og finds oneself
Syntax:      task = FindTask(name)
ML:          d0 = FindTask(a1)
Arguments:   name = name of the task to find; 0 to find oneself
Result:      task = the task (or process) matching the name; zero if unsuccessful

We also need to call OpenDevice. This is the way the AmigaOS opens a device, which in our case, will be the trackdisk device.

OpenDevice
Library:     exec.library
Offset:      -$1BC (-444)
Description: gains access to a device
Syntax:      error = OpenDevice(devName, unitNumber, iORequest, flags)
ML:          d0 = OpenDevice(a0, d0, a1, d1)
Arguments:   devName = name of the device requested
             unitNumber = number of unit to be accessed (0 to 3)
             iORequest = The IORequst structure
             flags = set to zero for opening
Result:      error = zero if successful

When we have opened the device, we need to communicate with it. This is done by calling DoIO. Note that this method waits for an I/O request to be fully complete ie. it blocks.

DoIO
Library:     exec.library
Offset:      -$1C8 (-456)
Description: executes an I/O command and waits for its completion
Syntax:      error = DoIO(iORequest)
ML:          d0 = DoIO(a1)
Arguments:   iORequest = an IORequest initialized by OpenDevice()
Result:      error = a sign-extended copy of the io_Error field of the IORequest
             Most device commands require that the error return be checked.

Here’s the source for mc1006, with my comments added. The program uses sector to read from the internal disk drive df0, where the read starts from block 0 for a length of 195 blocks, which corresponds to 99.840 bytes.

lea.l	buffer,a0  ; move buffer address into a0
move.l	#0,d0      ; move 0 into d0 (diskStation = internal drive)
move.l	#0,d1      ; move 0 into d1 (block = block 0)
move.l	#195,d2    ; move 195 into d2 (length = 195)
move.l	#1,d3      ; move 1 into d3 (mode = READ)

bsr	sector
rts                 ; return from subroutine

sector:                   ; sector(a0,d0,d1,d2,d3)
movem.l	d0-d7/a0-a6,-(a7) ; push register values onto the stack
lsl.l	#8,d1             ; left shift d1 8 bit. convert to offset in bytes
add.l	d1,d1             ; add d1 to d1. convert to offset in bytes
lsl.l	#8,d2             ; left shift d2 8 bit. convert to length in bytes
add.l	d2,d2             ; add d2 to d2. convert to length in bytes
move.l	d1,-(a7)          ; push d1 onto the stack (block)
move.l	d2,-(a7)          ; push d2 onto the stack (length)
move.l	a0,-(a7)          ; push a0 onto the stack (buffer)
move.l	d0,-(a7)          ; push d0 onto the stack (diskStation)
move.l	$4,a6             ; move base of exec.library into a6
lea.l	ws_diskport,a2    ; move ws_diskport address into a2
moveq	#-1,d0            ; move -1 into d0 (no preference for signal number)
jsr	-330(a6)          ; call AllocSignal. d0 = AllocSignal(d0)
moveq	#-1,d1            ; move -1 into d1
move.b	d0,15(a2)         ; move d0 (signal number) into address a2+15
clr.b	14(a2)            ; clear byte at address a2+14
move.b	#4,8(a2)          ; move 4 into address 8+a2
move.b	#120,9(a2)        ; move 120 into address 9+a2
sub.l	a1,a1             ; set a1 to 0 (find oneself)
jsr	-294(a6)          ; call FindTask. d0 = FindTask(a1)
move.l	d0,16(a2)         ; move task into address 16+a2
lea.l	20(a2),a0         ; move value in address 20+a2 into a0
move.l	a0,(a0)           ; move a0 into address a0
addq.l	#4,(a0)           ; add 4 to value in address a0
clr.l	4(a0)             ; clear long in address 4+a0
move.l	a0,8(a0)          ; move a0 into address 8+a0
lea.l	ws_diskreq,a1     ; move ws_diskreq address into a1 (IOStdReq)
move.b	#$05,8(a1)        ; move 5 into address 8+a1. NT_MESSAGE indicates message currently pending
move.l	a2,14(a1)         ; move a2 into address 14+a1. Pointer to MsgPort
lea.l	ws_devicename,a0  ; move ws_devicename address into a0 (devName)
move.l	(a7)+,d0          ; pop stack into register d0 (diskStation)
clr.l	d1                ; clear d1 (flags, 0 for opening)
jsr	-444(a6)          ; call OpenDevice. d0 = OpenDevice(a0,d0,a1,d1)
move.l	(a7)+,40(a1)      ; pop stack into address 40+a1 (buffer)
andi.l	#3,d3             ; preserve first 3 bits in d3. Map mode to command
addq.w	#1,d3             ; add 1 to d3. Map mode to command
move.w	d3,28(a1)         ; move d3 into address 28+a1. Set the command
move.l	(a7)+,36(a1)      ; pop stack into address 36+a1 (length)
move.l	(a7)+,44(a1)      ; pop stack into address 44+a1 (block)
jsr	-456(a6)          ; call DoIO. d0 = DoIO(a1)
move.l	d0,d7             ; move d0 (error) into d7
move.l	#0,36(a1)         ; move 0 into address 36+a1
move.w	#$9,28(a1)        ; move 9 into address 28+a1
jsr	-456(a6)          ; call DoIO. d0 = DoIO(a1)
movem.l	(a7)+,d0-d7/a0-a6 ; pop values from the stack into the registers
rts                       ; return from subroutine
ws_diskport:
blk.l	100,0
ws_diskreq:
blk.l	15,0
ws_devicename:
dc.b	"trackdisk.device",0,0


buffer:
blk.w	49920,0           ; allocate buffer for 195 blocks

Letter X continues with a test of the program. Let’s follow that test here.

First, we need to format a disk. In WinUAE this can be done by creating a standard disk, by choosing Floppy drives in the tree view.

create standard disk

Take the new disk and insert it into df0:, and change the mc1006 program, by using the code below.

lea.l	buffer,a0  ; move buffer address into a0
move.l	#0,d0      ; move 0 into d0 (diskStation = internal drive)
move.l	#100,d1    ; move 100 into d1 (block = block 100)
move.l	#1,d2      ; move 1 into d2 (length = 1)
move.l	#2,d3      ; move 2 into d3 (mode = WRITE)

bsr	sector
move.l	#3,d3      ; move 3 into d3 (mode = UPDATE)
bsr	sector
rts                ; return from subroutine

...

buffer:
dc.b	"This is a test"
blk.b 512,0

What happens here, is that we first tell the sector subroutine to start writing at block 100 for a length of 1 block (512 bytes). The data to be written is “This is a test”. After writting, we need to issue an update, to commit data to disk. Compile the program and run it from Seka. You will notice the disk spinning.

Next, we’ll change the mc1006 program yet again, by using the following code:

lea.l	buffer,a0  ; move buffer address into a0
move.l	#0,d0      ; move 0 into d0 (diskStation = internal drive)
move.l	#100,d1    ; move 100 into d1 (block = block 100)
move.l	#1,d2      ; move 1 into d2 (length = 1)
move.l	#1,d3      ; move 1 into d3 (mode = READ)

bsr	sector
rts                ; return from subroutine

...

buffer:
blk.b 512,0

The program has been changed, so that we use the sector subroutine to read the data back from disk.

Compile and run the program from Seka. You will notice that there is no disk spinning. This is because the AmigaOS caches the disk content. Try to eject the disk and then insert it again, after which you compile and run the program. Now, the disk will spin 😃.

The authors of Letter X writes, that we should not be burdened with the details of this program. It’s not a trivial program and details will follow in later letters. However, if you are like me, and you can’t wait for an explaination, then read on 😃

Trackdisk deep dive

To understand what goes on in the sector subroutine, we must consult the autodocs and includes for the AmigaOS. It’s also a really good idea to read part II of the book The Kickstart Guide to the Amiga, to get aquainted with how the AmigaOS works.

The sector subroutine is littered with magic numbers, that makes the code hard to read. We the first clues to what goes on, in the call to OpenDevice, which takes an IORequest as an input.

When I first started to write this text, I looked at the C definitions for the structs like e.g. IORequest, because I know C. But, since I’m writting a walktrough of a machine code course, I much rather look at the assembly representation of the structures. The trick to understanding the assembly structures, is to realize that they use assembly macros extensively.

The STRUCTURE Macro

There is a good explaination of the STRUCTURE macro in the online document at github called Total AMIGA Assembler.

Lets first take a look at the following C definition of IORequest. I have also added the byte offsets, because they will reveal the meaning of the magic numbers in the code.

struct IORequest
{                               /* Offsets                        */
  struct Message  io_message;   /*  0  $00                        */
  struct Device  *io_Device;    /* 20  $14  device node pointer   */
  struct Unit    *io_Unit;      /* 24  $18  unit (driver private) */
         UWORD    io_Command;   /* 28  $1C  device command        */
         UBYTE    io_Flags;     /* 30  $1E                        */
         BYTE     io_Error;     /* 31  $1F error or warning num   */
};

Now, lets look at the corresponding assembly definition of IORequest.

STRUCTURE  IO,MN_SIZE
    APTR    IO_DEVICE     ; device node pointer
    APTR    IO_UNIT       ; unit (driver private)
    UWORD   IO_COMMAND    ; device command
    UBYTE   IO_FLAGS      ; special flags
    BYTE    IO_ERROR      ; error or warning code
    LABEL   IO_SIZE

On the surface, the assembly structure looks much like the C structure, but it makes heavy use of macros. The STRUCTURE, APTR, UWORD, UBYTE, BYTE, and LABEL are all macros.

There’s a neat logic behind the assembly structure that deals with offsets. When we write STRUCTURE, we give it two inputs IO and MN_SIZE. The first is just a name, and the second is a size offset. In this case, it’s the size of a message. I must admit that the C definition is a bit more explicit here.

The definition continues with types like APTR, UWORD, UBYTE, BYTE which all increment the offset.

At the end of the structure definition, we have the LABEL macro, which assigns the sum of all offsets, i.e the size of the structure to IO_SIZE. We can use IO_SIZE when we e.g. allocate memory, and thereby avoid using a magic number. In fact, if we used the assembly structures, we could completely avoid magic numbers! ❤️

Here’s an example of some code from the mc1006 program.

move.w	d3,28(a1)         ; move d3 into address 28+a1. Set the command

And now the same code cleaned up, using the offset from the assembly structure.

move.w	d3,IO_COMMAND(a1) ; move d3 into address 28+a1. Set the command

OMG - no magic numbers! So much more readable 😱. Why was this approach not used with mc1006? My guess is that it was to keep things simple, and not introduce students to linking of assembly files. Well, perhaps a good call, but the code is really unreadable as a result.

UPDATE: Another reason might be that the K-Seka assembler is not compliant with the macros used in the include files. 😢

Ok, lets continue exploring mc1006.

Further Exploration

We have to digg a little deeper, to make head or tails of the code. Let’s start with what we know; the IORequest structure.

; IO Request Structures     Offsets
STRUCTURE  IO,MN_SIZE     ;  0  $00  
    APTR    IO_DEVICE     ; 20  $14  device node pointer
    APTR    IO_UNIT       ; 24  $18  unit (driver private)
    UWORD   IO_COMMAND    ; 28  $1C  device command
    UBYTE   IO_FLAGS      ; 30  $1E  special flags
    BYTE    IO_ERROR      ; 31  $1F  error or warning code
    LABEL   IO_SIZE       ; 32  $20

    ;------ Standard IO request extension:

    ULONG   IO_ACTUAL     ; 32  $20  actual # of bytes transfered
    ULONG   IO_LENGTH     ; 36  $24  requested # of bytes transfered
    APTR    IO_DATA       ; 40  $28  pointer to data area
    ULONG   IO_OFFSET     ; 44  $2C  offset for seeking devices
    LABEL   IOSTD_SIZE    ; 48  $30

Notice the IO request extension. According to the book Amiga Disk Drives Inside and Out.

The normal IORequest is not usable for the Trackdisk device and for this reason an extended version exists.

This extended version is the IOStdReq structure, which is also used in mc1006.

Let’s expand undtil we have all the types defined. MN_SIZE referes to the Message structure. This is not easy to see from the assembly definition, but it’s quite clear from the C definition of the IOStdReq structure.

; Message Structure         Offsets
STRUCTURE  MN,LN_SIZE     ;  0  $00
    APTR    MN_REPLYPORT  ; 14  $0E  message reply port
    UWORD   MN_LENGTH     ; 18  $12  total message length in bytes (include MN_SIZE in the length)
    LABEL   MN_SIZE       ; 20  $14

The LN_SIZE refers to the Node.

; Node Structrue    Offsets
STRUCTURE	LN,0    ;   0  $00  List Node
	  APTR	LN_SUCC ;   0  $00  Pointer to next (successor)
	  APTR	LN_PRED ;   4  $04  Pointer to previous (predecessor)
	  UBYTE	LN_TYPE ;   8  $08
	  BYTE	LN_PRI  ;   9  $09  Priority, for sorting
	  APTR	LN_NAME ;  10  $0A  ID string, null terminated
	  LABEL	LN_SIZE ;  14  $0E  Note: word aligned

We can now asses how mc1006 populates the IOStdReq structure, which is shown in the diagram below.

IORequest

As can be seen from the diagram above, it’s a fairly complex datastructure that we are populating. No wonder that the code is hard to read!

Let’s turn our attention to the reply port in the message structure. When a message is sent, the reciever will use the reply port to answer back. The reply port pointer is a MsgPort structure.

; Message Port Structure           Offsets
STRUCTURE  MP,LN_SIZE            ;  0  $00
    UBYTE   MP_FLAGS             ; 14  $0E
    UBYTE   MP_SIGBIT            ; 15  $0F  signal bit number
    APTR    MP_SIGTASK           ; 16  $10  object to be signalled
    STRUCT  MP_MSGLIST,LH_SIZE   ; 20  $14  message linked list
    LABEL   MP_SIZE              ; 34  $22

The STRUCT macro defines a sub-structure, which allows for an additonal input that increments the offset. In this case LH_SIZE, that refers to the List structure.

; List Structure         Offsets  
STRUCTURE	LH,0         ;  0  $00
    APTR	LH_HEAD      ;  0  $00
    APTR	LH_TAIL      ;  4  $04
    APTR	LH_TAILPRED  ;  8  $08
    UBYTE	LH_TYPE      ; 12  $0C
    UBYTE	LH_pad       ; 13  $0D padding
    LABEL	LH_SIZE      ; 14  $0E word aligned

The following diagram shows how the program sets the MsgPort structure.

MsgPort

Now, we have more or less explained the magic numbers in mc1006. But it still remains to be seen, how much more readable the code will be with offset variables inserted. Let’s try it! 😃

start:
;----- Library Vector Offsets
LVOAllocSignal=-330
LVOFindTask=-294
LVOOpenDevice=-444
LVODoIO=-456
;----- Node structure offsets
LN_TYPE=8
LN_PRI=9
;----- Message type for LN_TYPE
NT_MSGPORT=4
NT_MESSAGE=$05
;----- List structure offsets
LH_HEAD=0
LH_TAIL=4
LH_TAILPRED=8
;----- MsgPort structure offsets
MP_FLAGS=14
MP_SIGBIT=15
MP_SIGTASK=16
MP_MSGLIST=20
;----- Message structure offset
MN_REPLYPORT=14
;----- IOStdRequest structure offsets
IO_COMMAND=28
IO_LENGTH=36
IO_DATA=40
IO_OFFSET=44
;----- Command type for IO_COMMAND
CMD_FLUSH=$9

;----- Begin program
lea.l	buffer,a0  ; move buffer address into a0
move.l	#0,d0      ; move 0 into d0 (diskStation = internal drive)
move.l	#0,d1      ; move 0 into d1 (block = block 0)
move.l	#195,d2    ; move 195 into d2 (length = 195)
move.l	#1,d3      ; move 1 into d3 (mode = READ)

bsr	sector
rts                ; return from subroutine
  
  
sector:                          ; sector(a0=buffer,d0=diskStation,d1=block,d2=length,d3=mode)
movem.l	d0-d7/a0-a6,-(a7)        ; push register values onto the stack
lsl.l	#8,d1                    ; convert d1=block from blocks to offset in bytes
add.l	d1,d1                    ; convert d1=block from blocks to offset in bytes
lsl.l	#8,d2                    ; convert d2=length from blocks to bytes
add.l	d2,d2                    ; convert d2=length from blocks to bytes
move.l	d1,-(a7)                 ; push d1=block onto the stack
move.l	d2,-(a7)                 ; push d2=length onto the stack
move.l	a0,-(a7)                 ; push a0=buffer onto the stack
move.l	d0,-(a7)                 ; push d0=diskStation onto the stack
move.l	$4,a6                    ; move base of exec.library into a6
lea.l	ws_diskport,a2           ; move ws_diskport address into a2 (MsgPort)
moveq	#-1,d0                   ; move -1 into d0 (no preference for signal number)
jsr	LVOAllocSignal(a6)       ; call AllocSignal. d0 = AllocSignal(d0)
moveq	#-1,d1                   ; move -1 into d1
move.b	d0,MP_SIGBIT(a2)         ; set signal number in MsgPort
clr.b	MP_FLAGS(a2)             ; clear flags in MsgPort
move.b	NT_MSGPORT,LN_TYPE(a2)   ; set message type in MsgPort.Node
move.b	#120,LN_PRI(a2)          ; set priority in MsgPort.Node
sub.l	a1,a1                    ; set a1 to 0 (find oneself)
jsr	LVOFindTask(a6)          ; call FindTask. d0 = FindTask(a1)
move.l	d0,MP_SIGTASK(a2)        ; set object to be signaled in MsgPort to result of FindTask
lea.l	MP_MSGLIST(a2),a0        ; Initialize MsgPort.List
move.l	a0,LH_HEAD(a0)           ; Initialize MsgPort.List
addq.l	#LH_TAIL,(a0)            ; Initialize MsgPort.List
clr.l	LH_TAIL(a0)              ; Initialize MsgPort.List
move.l	a0,LH_TAILPRED(a0)       ; Initialize MsgPort.List
lea.l	ws_diskreq,a1            ; move ws_diskreq address into a1 (IOStdReq)
move.b	#NT_MESSAGE,LN_TYPE(a1)  ; set node type in IOStdReq.Message.Node
move.l	a2,MN_REPLYPORT(a1)      ; set reply port a2 in IOStdReq.Message
lea.l	ws_devicename,a0         ; set a0=devName
move.l	(a7)+,d0                 ; set d0=diskStation by popping stack
clr.l	d1                       ; set d1=flags (0 for opening)
jsr	LVOOpenDevice(a6)        ; call OpenDevice. (d0=returnCode) = OpenDevice(a0=devName,d0=unitNumber,a1=IORequest,d1=flags)
move.l	(a7)+,IO_DATA(a1)        ; set data in IOStdReq.Data to buffer by popping stack
andi.l	#3,d3                    ; convert subroutine input mode to command 
addq.w	#1,d3                    ; convert subroutine input mode to command 
move.w	d3,IO_COMMAND(a1)        ; set IOStdReq.Command to d3
move.l	(a7)+,IO_LENGTH(a1)      ; set IOStdReq.Length to length by popping stack
move.l	(a7)+,IO_OFFSET(a1)      ; set IOStdReq.Offset to block by popping stack
jsr	LVODoIO(a6)              ; call DoIO. (d0=returnCode) = DoIO(a1=IORequest)
move.l	d0,d7                    ; move d0=returnCode into d7
move.l	#0,IO_LENGTH(a1)         ; set IOStdReq.Length to 0
move.w	#CMD_FLUSH,IO_COMMAND(a1); set IOStdReq.Command to CMD_FLUSH
jsr	LVODoIO(a6)              ; call DoIO. (d0=returnCode) = DoIO(a1)
movem.l	(a7)+,d0-d7/a0-a6        ; pop values from the stack into the registers
rts                              ; return from subroutine
ws_diskport:  
blk.l	100,0  
ws_diskreq:
blk.l	15,0
ws_devicename:
dc.b	"trackdisk.device",0,0


buffer:
blk.w	49920,0           ; allocate buffer for 1.560 blocks

The above code is identical with mc1006, with the difference that I have replaced all magic number with variables. I have followed the naming convention in the include files.

The include files also contains a lot of macros, that should make it a breeze to auto-generate the variables. Let’s investigate this further.

Library Vector Offsets

Notice how I in the above improved version of mc1006 have replaced all the library vector offsets (LVO) with variables, e.g. like this:

jsr	-330(a6)            ; library offset vector
jsr	LVOAllocSignal(a6)  ; the same but with a variable

This can be done with some macro trickery. It’s explained in part on this german board or at The Digtial Cat (meow 🐱 ).

Since I use the K-Seka assembler, things will work differently. Normally you would put a prefix “_LVO” on the variables, but K-Seka does not like leading underscores. Another thing is that K-Seka does not seem to be compliant with the pseudo-ops and macro syntax used in the include files, which is a real bummer. 😢

The K-Seka assmbler is documented in the Amiga Machine Language online document.

Since K-Seka is so different, I just handwrote the variables from the following procedure.

Take a look at the file exec_lib.i, it contains a series of FUNCDEF macros

FUNCDEF	Supervisor
FUNCDEF	ExitIntr
FUNCDEF	Schedule
...
FUNCDEF	OpenDevice
FUNCDEF	CloseDevice
FUNCDEF	DoIO
...

The FUNCDEF macro is not defined by the AmigaOS, you have to do that yourself. Fortunatly it’s an easy thing to do (but not in K-Seka). Let’s first look at the result of running the macro

LVOSupervisor:equ -30
LVOExitIntr:equ -42
LVOSchedule:equ -48
...
LVOOpenDevice:equ -444
LVOCloseDevice:equ -450
LVODoIO:equ -456
...

The first offset -30 is also called a bias. Those can be found in the system references, in the file Function.offs. This file also shows all the library vector offsets. Notice how all functions are 6 bytes apart.

We can now define the FUNCDEF macro. There’s an example of the FUNCDEF macro in libraries.i but again it will not work in K-Seka.

FUNC_CNT  EQU  -30
FUNCDEF MACRO
 _LVO\1    EQU  FUNC_CNT
 FUNC_CNT  SET  FUNC_CNT-6	* Standard offset-6 bytes each
ENDM

Discussion

There is no doubt, that this code looked tricky. Digging a bit deeper was fun and revealed a beautiful multi-tasking OS, where devices can be loaded when needed and communication is done by sending messages around.

The authors did warn, that this was an advanced topic that would not be covered in letter X, so sprinkling the code with magic numbers are from that perspective ok. It’s just so much less you have to explain, by doing it that way.

Also I have found the K-Seka to be a bit frustrating to work with, especially with regards to macros and pseudo-ops. It does not seem compliant with the style used in the include files. K-Seka is the recommended assembler for this machine code course, but if I could choose freely, I would try out Asm-One, AsmTwo, or Asm-Pro, which are more recent.

To prepare for this post, I read part II of The Kickstart Guide To Amiga. I can warmly recommend this book if you are interested in the AmigaOS from the Kickstart 1.2 / 1.3 era.

After reading part II, it also dawned upon me that the mc1006 has some serious flaws. Let’s go through them.

In the code we allocate space for the IOStdReq and MsgPort structures directly, without using AllocMem. This is not recommended style, because those structures will be shared between tasks. On an Amiga that supports memory partitioning, this would not work since the structures resides in the memory for our program, and that memory space would not be accessible to other tasks. Data structures shared between tasks should be AllocMem’d with MEMF_PUBLIC.

It’s also recommended style, in fact it’s written in the docs, that when we call OpenDevice we should match it with a call to CloseDevice. This is not done in mc1006 with the result that we are leaking.

The same issue is found with AllocSignal, where we do not make a matching call to FreeSignal.

The Exec library version 36 comes with the function CreateMsgPort which could make it simpler to set up the MsgPort structure. However that function was first available with Kickstart 2.0.

Another odd thing is that we do not set the length of the message when we prepare the IOStdReq. I would have set it to 48 bytes, but the code seems to work fine without it.

There’s a neat closing remark from The Kickstart Guide To Amiga p.37, where they comment on sending messages between a main and child task.

There is a final subtlety to this business which is well worth noting. This is taht very little actually gets moved about in memory when a message is sent; the message data actually stays in the same place, but gets attached to the child’s message port by cunning use of pointers. For this reason, hte main task must be very careful not to touch the message data, or de-allocate the message memory etc, until the child task has replied the message. Another way of looking at this is to say that by sending the message, the main task grants the child task a temporary licence to mess about with a bit of main task’s memory; by replying the message, the child taks returns this memory to the main task.

I really like this alternative view of sending messages as granting a temporary license to the memory to the reciever. The memory still have to be MEMF_PUBLIC, but there is an implicit ownership e.g. it’s the main task that deallocates the memory for the message.


Amiga Machine Code Course

Previous post: Amiga Machine Code Letter X - More CLI.

Next post: Amiga Machine Code Letter XI - The Mouse.

Mark Wrobel
Mark Wrobel
Team Lead, developer and mortgage expert