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.
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,%d6Notice 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,%a0There 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,%a0Now 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.
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,%d7Absolute -- 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,%d0Absolute 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 0xfef60c00,%sp lea %pc@(xfefe04ee),%a0 lea %a4@(1),%a5 pea 0x6 pea 0xfeff3c8c pea %a1@ pea 0x6Notice 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.
jsr 0xfefe0ab2 jsr %pc@(xfefe065c) jsr %a0@There is also a "bsr" instruction which uses a PC relative address to do the same thing -- but the routine must be close enough to reach with a small displacement. Usually just the bsrs form is used, which has an 8 bit displacement. I see both the bsrs and bsrw used in the sun bootrom
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,#-36This 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 ever 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 rtsStrangely 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.
linkw %fp,#-8 moveml %a4-%a5,%sp@ .... moveml %fp@(-8),%a4-%a5 unlk %fp rtsHere 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.
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.
fefe0578: 7020 moveq #32,%d0 fefe057a: 51c8 fffe dbf %d0,0xfefe057aThe 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.