October 29, 2024

Sun 3/80 -- m68k assembler

This is a short course on the m68k architecture. In particular I will talk about the mc68030, but this applies almost exactly to the mc68020, and largely to the mc68000 and mc68010.

These are 32 bit processors with 8 "D" registers and 8 "A" registers. It annoyes some people that there are two kinds of registers. I agree that it would be nicer to just have 16 "registers" and be done with it, but we forget the limitations of the world back in 1979 when the 68000 was released with a mere 68,000 transistors. It was a miracle in its time with twice as many transistors as the Intel 8086.

Instructions are variable length (this is a CISC processor after all). Some instructions are only 16 bits, others can be 32, 48, or even 80 bits depending on the address mode being used.

In the following I will try to progress from simple things to more complex. Also I will only discuss the assembler syntax generated by Gnu objdump. I am well aware that there are several other assemblers with different syntax out in the wild. I considered mentioning these in an attempt to be comprehensive, but decided against it. It would only generate confusion. I will stay focused and describe only the syntax used by the gcc toolset. My interest currently is in analyzing the disassembly of the sun 3/80 bootrom.

Basic instructions

To zero a register you could do either of the following.. The first is a 16 bit instruction (and would be prefered), the second uses 48 bits because it must contain the 32 bit constant:

		clrl %d6
		movel #0,%d6
Notice that "motion" with the assembler syntax I am describing is from left to right.

To move a constant into a register, you use:

		moveq #3,%d1
		movel #0xfef72166,%d0
		lea 0xfef60c00,%a0
There are several things to notice here. First is that you don't use "move" to put stuff into an "A" register. Move only will target a D regsiter. It is actually much more general and will move from any EA to any EA (where EA is "effective address). The moveq instruction is short (16 bits) and can only work with small values (8 bit values that are sign extended).

Now consider these instructions. Note that there is no "#" in front of the 32 bit constant. These fetch data from the address given and place it in the "D" or "A" register specified.

		movel 0xfef72166,%d0
		movea 0xfef60c00,%a0
Now consider that many of the instructions above end in "l" indicating that a 32 bit "long" is being moved around. We could have ended the instruction with "w" or "b" to indicate that 16 or 8 bits should be moved.

Addressing modes

The manual likes to talk about an EA (effective address) and there are a myriad of ways to compose one. The manual has fancy names for each of them. There are 18 addressing modes and 9 basic types!

A warning! If you are a veteran C programmer there is some notation here that may mislead you. Unlike C, the location of the + or - mean nothing -- they aren't like ++ and --. With the m68K the "+" is always postincrement, and it always happens after the address is used. The "-" is always predecrement, and it always happens before the address is used, even though the "-" follows the register.

Here we go, with examples of each whereever possible.

Register direct -- very simple, data comes from or goes to a register. Here are examples that use it twice, moving data from one register to another. This is so common you usually don't think of it as an addressing mode.

		movel %d0,%d2
		movel %a0,%d0

Immediate -- the instruction itself holds a constant. The clue is the "#" before the constant. Note that this can only be a "source" (on the left side of a comma) Again, this is so common that it isn't often thought of as an addressing mode.

		moveal #0,%a0
		moveb #-3,%d1
		movel #0x80d07660,%d7
Absolute -- the instruction holds a 32 bit address, and data is fetched from (or written to) that address. Note that this could be used on both sides of the comma to do a memory to memory move, though this is not often seen. Such an instruction would be 80 bits long!
		moveb #0,0xfef0b400
		movel 0xfef72166,%d0
		movew 0x61000000,%d0
Absolute short -- this is in the book, but I have never seen it. It is denoted by ".w" after that address. The idea is that a 16 bit value is sign extended to 16 bits, then used.

Address register indirect - here the address register holds an address and we go to that address and fetch (or store) to memory. In other words we use the address register as a pointer. My assember put a "@" after a register to indicate this mode.

		movew %d0,%a0@
		clrl %a0@
		movel %a5@,%d5

Address register indirect with displacement - Here the instruction supplies an offset that is added to the value in the address register. Think of the address register supplying the base address of a C structure and the displacement being the offsets to get different fields in the structure.

		clrw %a0@(6)

A mystery -- what can the following mean with empty parenthesis? These are simply address register indirect with displacement with the displacement value of 0. If the disassembler had generated (0) this would not be so confusing. Of course the programmer could have used the more compact and simple address indirect encoding, saving us some confusion. But he didn't. I include a "tstb" instruction that is clearly plain old address indirect, which yields a 16 bit instruction.

4a28 0000       tstb %a0@()
c028 0000       andb %a0@(),%d0
4a10            tstb %a0@

Address register indirect with postincrement - this is the same as the above, but after referencing memory, the value in the address register is incremented by 1, 2, or 4, according to whether "b", "w", or "l" was used for the transfer. At the risk of getting ahead of myself, it is worth noting that if the address register is the stack pointer and we are fetching from it, this can be a POP from the stack. There is no literal "pop" instruction.

		movew %a0@+,%a2@+
		moveb %a5@+,%d5
		clrw %a5@+

Address register indirect with predecrement - Here we subtract the item size first from the value in the address register, then use that value to reference memory. If we are using the stack pointer and writing to memory, this is a stack push. There is no literal "push" instruction.

		movel %a2,%sp@-
		movew %d0,%sp@-
		clrl %sp@-

Address register indirect with index register and offset - I cannot find any examples of this in the bootrom code. The idea is that we get two registers involved. We have the address register giving a base address. We can add in an 8 bit (sign extended) offset, and we also add in a value from an index register. An added trick is that the index register value can be "scaled", presumably by 1, 2, or 4 -- the manual is vague.

This may be one of those cases where CISC instruction set designers got carried away designing instructions that neither compilers nor programmers ever used.

Address register indirect with index register and displacement - This takes the above yet further. Once again, I find no examples in the bootrom code. Here instead of the 8 bit offset, we have a 32 bit offset.

Memory indirect postindexed mode -
Memory indirect preindexed mode -
These are two other modes I don't see getting used. We have a value in an address register, and index register, and several displacements. You can get the manual and look them up if you think there is any point in it.

Program counter indirect with displacement - We add the value of the program counter to an 8 bit sign extended displacement to get a value. A questionable example from the bootrom code follows. Notice that the disassembler has generated the resulting address for us. Also note that the PC value is not the value of the instruction, but apparently is that value plus 2 (the PC gets incremented after the fetch of the first part of the instruction but before fetching the offset, or so it seems.

fefe7620:   4dfa 0008       lea %pc@(xfefe762a),%fp

Program counter indirect with index - Here we add in the value from another register, as well as a sign extended 8 bit displacement. Notice that this and the above will fetch from instruction space, so a likely use (as in the examples below) is to work with a jump table set up in the code. The could perhaps also be used to reference immutable data structures included with the code. pea 0x6

fefe0172:   4efb 0002       jmp %pc@(xfefe0176,%d0:w)
fefe01ae:   4efb 0002       jmp %pc@(xfefe01b2,%d0:w)
fefe01f0:   4efb 0002       jmp %pc@(xfefe01f4,%d0:w)

Program counter indirect with index and displacement -
Program counter memory indirect postindexed -
Program counter memory indirect preindexed -
These are 3 modes that I don't recognize in the bootrom code. They are analogies to fancy address register indirect modes that nobody uses, as mentioned above.

I am getting tired of describing these wacky modes that nobody uses. The existence of these was part of the justification in moving to RISC. CISC machines like the m68k were wasting silicon and microcode supporting silly modes that programmers and especially compilers never use.

LEA and PEA instructions

Here we don't use the EA to reference memory, but we generate the 32 bit address and stick it someplace. In the case of "lea", we stick it in the specified register. In the case of "pea", we push it onto the stack.
		lea 0xfef60c00,%sp
		lea %pc@(xfefe04ee),%a0
		lea %a4@(1),%a5
		pea 0x6
pea 0xfeff3c8c
		pea %a1@
		pea 0x6
Notice that we can use these instructions to simply put values into an address register or on the stack. The "lea" instruction will only load address registers. It looks to me like the instruction "pea %a1@" simply pushes the value in A1 onto the stack.

The stack

The a7 register is also the stack pointer "sp". You will rarely if ever see it referenced as "a7". The stack grows towards lower addresses and always points to the valid item on the top of the stack. When the stack is empty (i.e. when first initialized) it will point the the address immediately after the memory allocated for the stack.

Subroutines

The "jsr" instruction calls a subroutine, pushing the return address onto the stack. The call address can be any EA, but is usually just a 32 bit address (Absolute addressing).
		jsr 0xfefe0ab2
		jsr %pc@(xfefe065c)
		jsr %a0@
To return from a subroutine, use the "rts" instruction.

Many subroutines also use the "fp" register which is "a6". You will pretty much never see "a6" used explicitly. The "fp" is for "frame pointer" and gets used along with the linkw and unlk instructions.

		linkw %fp,#0
		linkw %fp,#-36
This instruction first pushes the contents of the fp register onto the stack. Then it copies the value of sp into fp. Then it adds the displacement to the sp. Why linkw rather than linkl you ask? The "l" versus "w" indicates whether the offset is 16 or 32 bits. Nobody every needs a 32 bit offset for local storage (or has a stack large enough to allow it), so you always see linkw.

The idea here is to allocate a block of stack memory for local variables. Having a displacement of 0 seems kind of silly, but keeps things consistent, especially for compiler generated code. Once this is done, the FP can be used to reference subroutine arguments which were previously pushed onto the stack (the first will be at fp+8). The offset of 8 skips the saved fp and the return address. Then sp is used to access the local variables. Here is an example:

		linkw %fp,#-4
		movel %d7,%sp@		; save d7
		movel %fp@(8),%d0	; fetch first argument
		moveq #32,%d7
		cmpl %d7,%d0
		....
		movel %fp@(-4),%d7	; restore d7
		unlk %fp
		rts
Strangely in the above, the sp is used to save d7 and fp is used with an offset to restore it.

The unlk instruction reverses the effect of link. It first copies the value in "fp" back into "sp", then it pops the value on top of the stack into fp.

movem

This is move multiple. A list of registers gets moved. This is often part of the entry and exit to a subroutine. It saves whatever register are given in a list. The usual scheme uses the linkw instruction to allocate enough memory to accomodate the registers (plus whatever local storage is needed), then a typical subroutine looks like this:
		linkw %fp,#-8
		moveml %a4-%a5,%sp@
		....
		moveml %fp@(-8),%a4-%a5
		unlk %fp
		rts
Here we are saving two registers (a4 and a5) so we need 8 bytes. The linkw is only saving the 8 bytes we need for those registers. The register list can be more complex, such as:
		moveml %d5-%d7/%a5,%sp@
		moveml %d2/%d4/%d6-%d7/%a4-%a5,%sp@

Here the EA for the movem is simply address register indirect, telling it to stick them where the sp is pointing, but any sort of EA can be used with a variety of effects that you may need to think long and hard about.

Branching

We have both "bra" and "jmp". They both do the same thing. You see bra used much more often. The difference is that "bra" uses a PC relative displacement, whereas "jmp" uses any EA you might like to use.
		bras 0xfefe5d38
		braw 0xfefe6224
		jmp %pc@(xfefe0176,%d0:w)
Sometimes jmp must be used to jump to a fixed address because it is too far away and a 16 bit offset is not enough to get there.

Everything else

I haven't discussed the usual add, sub, shift, compare and such. I need to leave you something to figure out.

Interesting things

fefe0578:   7020            moveq #32,%d0
fefe057a:   51c8 fffe       dbf %d0,0xfefe057a
The above uses the "dbf" instruction to implement a delay loop. I find this a cute piece of code. The "db" instructions are "decrement and branch". They decrement the given register until it gets to -1 unless the condition changes. Here the condition is "f" (false) and nothing can change the condition codes, so only the count will terminate the loop. Other conditions are possible, allowing more complex things to be achieved, perhaps using the count as a timeout.