Category:68000 Assembly: Difference between revisions
Puppydrum64 (talk | contribs) No edit summary |
Puppydrum64 (talk | contribs) m →Alignment: Provided an example of a "dummy read" |
||
Line 173: | Line 173: | ||
DC.W $1000,$2000,$3000,$4000</lang> |
DC.W $1000,$2000,$3000,$4000</lang> |
||
Another way is to pad the data with an extra byte, so that there is an even number of entries in the table. |
Another way is to pad the data with an extra byte, so that there is an even number of entries in the table. This becomes impractical with large data tables, so the <code>EVEN</code> directive can be placed after a series of bytes. If the byte count is odd, <code>EVEN</code> will pad the data with an extra byte. If it's already even, the <code>EVEN</code> command is ignored. This saves you the trouble of having to count a long series of bytes without worrying about wasting space. |
||
<lang 68000devpac>MyString: DC.B "HELLO WORLD 12345678900000",0 |
<lang 68000devpac>MyString: DC.B "HELLO WORLD 12345678900000",0 |
||
EVEN ;some assemblers require this to be on its own line</lang> |
EVEN ;some assemblers require this to be on its own line</lang> |
||
A third way is to perform a "dummy read." This is when a value is read from an address using pre-decrement or post-increment, with the sole purpose of moving the pointer, and the value being read is of zero interest. This method lets you work with mixed data types in the same table, but it requires the programmer to know in advance where the byte-length data begins and ends. |
|||
<lang 68000devpac>TestData: |
|||
DC.B $02,$03,$04 |
|||
DC.W $0345 |
|||
LEA TestData,A0 ;load effective address of TestData into A0. |
|||
MOVE.B (A0)+,(A1)+ ;copy $02 to a new memory location |
|||
MOVE.B (A0)+,(A1)+ ;copy $03 to a new memory location |
|||
MOVE.B (A0)+,(A1)+ ;copy $04 to a new memory location |
|||
;if we did MOVE.W (A0)+,(A1)+ now we'd crash. First we need to adjust the pointers. |
|||
MOVE.B (A0)+,D7 ;dummy read to D7. Now A0 is word aligned. |
|||
MOVE.B (A1)+,D7 ;dummy read to D7. Now A0 is word aligned. |
|||
MOVE.W (A0)+,(A1)+ ;copy $0345 to a new memory location</lang> |
|||
Using <code>ADDA.L #1,A0</code> and <code>ADDA.L #1,A1</code> would have worked also, instead of the dummy read. The 68000 gives the programmer a lot of different ways to do a task. |
|||
==Subroutines== |
==Subroutines== |
||
Subroutines work exactly the same as they do in [[6502 Assembly]]. Even the commands are the same; <code>JSR</code> and <code>RTS</code>. The only difference is that return spoofing doesn't require the return address to be decremented by 1. The 68000 also adds <code>BSR</code> for nearby subroutines. These still need to end in an <code>RTS</code> just the same, but saves CPU cycles compared to a <code>JSR</code>. |
Subroutines work exactly the same as they do in [[6502 Assembly]]. Even the commands are the same; <code>JSR</code> and <code>RTS</code>. The only difference is that return spoofing doesn't require the return address to be decremented by 1. The 68000 also adds <code>BSR</code> for nearby subroutines. These still need to end in an <code>RTS</code> just the same, but saves CPU cycles compared to a <code>JSR</code>. |
Revision as of 16:24, 17 September 2021
![Language](http://static.miraheze.org/rosettacodewiki/thumb/2/27/Rcode-button-language-crushed.png/64px-Rcode-button-language-crushed.png)
This programming language may be used to instruct a computer to perform a task.
See Also: |
|
---|
68000 assembly is the assembly language used for the Motorola 68000, or commonly known as the 68K. It should not be confused with the 6800 (which predates it). The Motorola 68000 is a big-endian processor with full 32-bit capabilities (despite most systems that use it being considered 16-bit.) It was used in many computers such as the Amiga or the Canon Cat, as well as game consoles such as the Sega Genesis and Neo Geo.
Architecture Overview
Big-Endian
The 68000, unlike most processors of its era, is big-endian. This means that bytes are stored from left to right. The example below illustrates this concept: <lang 68000devpac>MOVE.L #$12345678,$100000 ;store the hexadecimal numeral #$12345678 at memory address $100000
- hexdump of $100000
- $100000 = $12
- $100001 = $34
- $100002 = $56
- $100003 = $78</lang>
On a little-endian processor such as in x86 Assembly, the order of the bytes would be reversed, i.e.: <lang asm>;hexdump of $100000
- $100000 = $78
- $100001 = $56
- $100002 = $34
- $100003 = $12</lang>
This difference isn't usually relevant in the majority of situations, so don't concern yourself too much. It's much more important when doing 6502 Assembly where registers are smaller than the address space.
Data Registers
There are eight 32-bit data registers on the 68000, numbered D0-D7. As the name implies, these are designed to hold data. Much like in ARM Assembly, each one is identical in terms of which commands it can use. A command that can be used for D0 can be used for any other D-register.
<lang 68000devpac>MOVE.B #$FF,D0 ;move the hexadecimal value 0xFF into the bottom byte of D0. ADD.W #$8000,D4 ;add hexadecimal 0x8000 to the value stored in D4.</lang>
Address Registers
There are eight of these as well, numbered A0-A7. A7 is reserved as the stack pointer, and is commonly referenced as SP in assemblers. The others are free to use for any purpose. Although these registers are 32-bit, the 68000's address space is 24-bit (ranges from 0x000000 to 0xFFFFFF), so the leftmost byte is ignored. You can do simple math involving these registers but more complicated commands like multiply or divide can only be used with data registers. Address registers are used to contain addresses and extract the values stored within.
Loading From Memory
<lang 68000devpac>MOVEA.L #$200000,A2 ;usually these are loaded from a label.
- The hex dump of address $200000
- 44 55 66 77
MOVE.L #$00000000,D0 MOVE.B (A2),D0 ;load the byte stored at $200000 into D0. D0 = #$00000044 MOVE.W (A2),D0 ;load the word stored at $200000 into D0. D0 = #$00004455 MOVE.L (A2),D0 ;load the long stored at $200000 into D0. D0 = #$44556677 MOVE.L D2,(A5) ;store the contents of D2 into the memory address pointed to by A5.</lang>
Post-Increment
The post-increment mode is specified by adding a + to the end of parentheses. This means that after the command is done, the address stored in the address register (not the value stored at that address) is increased by the byte length of the command (1 for .B
, 2 for .W
, 4 for .L
).
<lang 68000devpac>MOVEA.L #$00240000,A4 ;load the address $240000 into A4
MOVE.W (A4)+,D0 ;move the word stored at $240000 into D0, then increment to #$240002
MOVE.L (A4)+,D1 ;move the long stored at $240000 into D1, then increment to #$240006
MOVE.L (SP)+,D3 ;pop the top value of the stack into D3</lang>
Pre-Decrement
The pre-decrement mode is specified by typing a - before the parentheses. This means that before the command is done, the address stored in the address register is decreased by the byte length of the command. <lang 68000devpac>MOVEA.L #$0024000A,A4 ;load the address $24000A into A4 MOVE.W -(A4),D0 ;move the word stored at $240008 into D0 MOVE.L -(A4),D1 ;move the long stored at $240004 into D1 MOVE.L D2,-(SP) ;push the contents of D2 onto the stack</lang>
Address Offsets
A memory address can be offset by a data register, an immediate value, or both. If a data register is used, only the bottom 2 bytes are considered. In either case, the contents of the data register and/or the immediate value are added to the value stored in the address register, and the value is read from that address at the specified length. The offsets are applied during the calculation only; the actual contents in the address register after the move are unchanged. Using a post-increment or pre-decrement with this addressing mode will only update the address by the specified length, not by the offsets.
<lang 68000devpac>MOVE.B (4,A0,D0),D1 ;The byte at A0+D0+4 is loaded into D1.</lang> It's possible to use the same data register as the offset and the destination. This does not cause any problems whatsoever, as the data register offset is "locked in" before the move, and is only updated after the command fully executes. Using the same command again immediately afterwards will offset based on the new value of that register.
<lang 68000devpac>MOVE.W (6,A0,D0),D0 ;The word at A0+D0+6 is read, then loaded into D0.</lang>
A very important note is that when using this method with words and longs, the resulting address must be even! Otherwise the CPU will crash. For MOVE.B
it doesn't matter.
Effective Address
A calculated offset can be saved to an address register with the LEA
command, which stands for "Load Effective Address." x86 Assembly also has this command, and it serves the same purpose. The syntax for it can be a bit misleading depending on your assembler.
<lang 68000devpac>LEA myData,A0 ;load the effective address of myData into A0 LEA (4,A0),A1 ;load into A1 the effective address A0+4. This looks like a dereference operation but it is not! MOVE.W (A1),D1 ;dereference A1, loading the value it points to into D1.</lang>
This can get confusing, especially if you have tables of pointers. Just remember that LEA
cannot dereference an address.
If you don't want to store the effective address in an address register, you can use PEA
(push effective address) to put it onto the stack instead.
<lang 68000devpac>LEA myData,A0 ;load the effective address of myData into A0
PEA (4,A0) ;store the effective address of A0+4 onto the stack.</lang>
The Stack
The 68000's stack is commonly referred to as SP
but it is also address register A7
. This register is handled differently than the other address registers when pushing bytes onto the stack. A byte value pushed onto the stack will be padded to the right with zeroes. The stack needs to pad byte-length data so that it can stay word-aligned at all times. Otherwise the CPU would crash as soon as you tried to use the stack for anything other than a byte!
<lang 68000devpac>MOVE.B #$FF,-(SP) ;push #$FF then #$00 onto the stack, in that order. MOVE.B (SP)+,D0 ;The values are popped in the order #$00 #$FF.</lang>
You can abuse this property of the stack to quickly swap bytes around. Suppose you had a number like #$11223344
stored in D0
and you wanted to change it to #$11224433
:
<lang 68000devpac>MOVE.W D0,-(SP) ;push #$3344 onto the stack MOVE.B (SP)+,D0 ;pop them in the order #$44 #$33.</lang>
Length
The 68000 can work with 8-bit, 16-bit, or 32-bit values. Some commands only work with specific lengths but most work with all three. To specify which length you are using, the command ends in a different suffix:
.B
for byte length (8 bit).W
for word length (16 bit).L
for long length (32 bit)
If you don't specify a length with your command, it usually defaults to word length, but ultimately it depends on the command you are using. (Some commands cannot be used at word length.)
Bytes and words moved into a register are always stored on the right-hand side. For example: <lang 68000devpac>MOVE.L #$FFFFFFFF,D7 MOVE.B #$00,D7 ;D7 contains #$FFFFFF00 MOVE.W #$2222,D7 ;D7 contains #$FFFF2222</lang>
As you can see, the rest of the register is unchanged. (On the ARM, it would turn to zeroes.) This is very important to remember. If your code is doing something unexpected it might be due to the "old" value of the register corrupting another function.
If the given constant is smaller than the length provided, the value is padded to the left with zeroes. <lang 68000devpac>MOVE.W #$FF,D3 ;D3 = #$xxxx00FF, where x is the previous value of D3. MOVE.L #0,D3 ;D3 = #$00000000</lang>
Loading immediate values into address registers is different. You can only move words or longer into address registers, and if you move a word, the value is sign-extended. This means that if the top nibble of the word is 8 or greater, the value gets padded to the left with Fs, and is padded with zeroes if the top nibble is 7 or less. It's best to always move longs into address registers. That way you know what you're getting.
<lang 68000devpac>MOVEA.W #$8000,A4 ;A4 = #$FFFF8000. Remember the top byte is ignored so this is the same as #$00FF8000. MOVEA.W #$7FFF,A3 ;A3 = #$00007FFF</lang>
Traps
Traps are the equivalent of INT
in 8086 Assembly/x86 Assembly or SVC
/SWI
in ARM Assembly. The 68000 only supports 17 of these in total. A trap is very similar to a subroutine call, except it can also occur automatically if certain conditions are met, such as dividing by zero. This saves the trouble of having to do the following:
<lang 68000devpac>;this is an example of how NOT to do it!
CMP.L #0,D0 BEQ DivideByZeroError DIVU D0,D1 RTS DivideByZeroError:
- put your error handler here</lang>
This is completely unnecessary, as the DIVU
and DIVS
use a trap to handle a divide by zero automatically. If the CPU would attempt to divide by zero, the processor automatically calls the relevant trap (Trap 5 in this case). The return address and processor flags are saved, and execution jumps to the address specified in the trap list (a table of pre-defined memory addresses, stored at $000080 and going up. The standard 16 traps are stored here, with a 17th stored at $00001C.
Hardware-Defined Traps
Certain traps have specific meanings as defined by the 68000 itself:
- Trap 4 occurs if the
ILLEGAL
command is executed. This is similar toBRK
on 6502 Assembly. If you're programming on a system or emulator with no built-in debugger, it's a handy way of seeing if execution is arriving at a certain point. If Trap 4 is pointed to a system reset or a hexdump routine you created, you'll know in an instant if the code before it is bugged. This (admittedly contrived) example will show the basic concept.
<lang 68000devpac>;trying to see if this routine works properly TestRoutine: CMP.L D0,D1 ;programmer expects these to always be equal BEQ weShouldGoHere ILLEGAL weShouldGoHere:
- rest of routine which depends on D0 and D1 being equal.</lang>
- Trap 5 occurs if a
DIVU
orDIVS
instruction attempts to divide by zero.
- Trap 6 detects if a value is out of bounds for a desired range. The
CHK
command takes a numeral, data register, or memory address (either explicit or pointed to by an address register) and compares it to a data register containing the upper bound. If the first operand is greater than the upper bound, trap 6 will be called. Otherwise execution will resume as normal.
<lang 68000devpac>MOVE.L #500,D3 ;our maximum range CHK D0,D3 ;if D0 > D3 then Trap 6 will occur.</lang>
- The 17th trap is called the "Overflow Trap" and can only be called with the
TRAPV
instruction, which calls it if the overflow flag is set.
Kernel-Defined Traps
Depending on the hardware, certain traps are built-in to perform certain tasks, such as reading a keyboard or mouse, or are user-defined. To create your own trap routine, you'll need to first write the routine, then store its address in the corresponding trap number. Trap 0 is stored at $000080, Trap 1 at $000084, and so on. The overflow trap is stored at address $00001C.
Alignment
The 68000 can only read or write words and longs at even addresses. Doing so at an odd address will result in the CPU crashing. (Note that reading byte data will not cause a crash regardless of whether it's located at an odd or even address.) This isn't usually a problem, but it can be if the programmer is not careful with the way their data is organized. Consider the following example: <lang 68000devpac>TestData: DC.B $02 DC.W $0345
LEA TestData,A0 ;load effective address of TestData into A0. MOVE.B (A0)+,D0 ;load $02 into D0, increment A0 by 1 MOVE.W (A0)+,D1 ;this crashes the CPU since A0 is now odd</lang>
How was it known that the address was odd at the second instruction? Simple. All instructions take an even number of bytes to encode. So there are only a few ways improper alignment can occur:
- An odd value is loaded into an address register.
- An addition or subtraction done to the value in an address register resulted in an odd memory address.
- An indirect read at byte length was performed using pre-decrement or post-increment, at an even address. (e.g.
MOVE.B (A0)+,D0
orMOVE.B -(A0),D0
where A0 contained an even memory address.)
If the programmer is smart with the way they encode byte-length data they can avoid this problem entirely with little effort.
One way is to separate byte-length data into its own table. <lang 68000devpac>ByteData: DC.B $20,$40,$60,$80
WordData: DC.W $1000,$2000,$3000,$4000</lang>
Another way is to pad the data with an extra byte, so that there is an even number of entries in the table. This becomes impractical with large data tables, so the EVEN
directive can be placed after a series of bytes. If the byte count is odd, EVEN
will pad the data with an extra byte. If it's already even, the EVEN
command is ignored. This saves you the trouble of having to count a long series of bytes without worrying about wasting space.
<lang 68000devpac>MyString: DC.B "HELLO WORLD 12345678900000",0
EVEN ;some assemblers require this to be on its own line</lang>
A third way is to perform a "dummy read." This is when a value is read from an address using pre-decrement or post-increment, with the sole purpose of moving the pointer, and the value being read is of zero interest. This method lets you work with mixed data types in the same table, but it requires the programmer to know in advance where the byte-length data begins and ends. <lang 68000devpac>TestData: DC.B $02,$03,$04 DC.W $0345
LEA TestData,A0 ;load effective address of TestData into A0. MOVE.B (A0)+,(A1)+ ;copy $02 to a new memory location MOVE.B (A0)+,(A1)+ ;copy $03 to a new memory location MOVE.B (A0)+,(A1)+ ;copy $04 to a new memory location
- if we did MOVE.W (A0)+,(A1)+ now we'd crash. First we need to adjust the pointers.
MOVE.B (A0)+,D7 ;dummy read to D7. Now A0 is word aligned. MOVE.B (A1)+,D7 ;dummy read to D7. Now A0 is word aligned. MOVE.W (A0)+,(A1)+ ;copy $0345 to a new memory location</lang>
Using ADDA.L #1,A0
and ADDA.L #1,A1
would have worked also, instead of the dummy read. The 68000 gives the programmer a lot of different ways to do a task.
Subroutines
Subroutines work exactly the same as they do in 6502 Assembly. Even the commands are the same; JSR
and RTS
. The only difference is that return spoofing doesn't require the return address to be decremented by 1. The 68000 also adds BSR
for nearby subroutines. These still need to end in an RTS
just the same, but saves CPU cycles compared to a JSR
.
Citations
- 'Akuyou', Keith. Learn Multiplatform Assembly Programming with Chibiakumas! Las Vegas, NV. Self-published, 05 April 2021.
- [ChibiAkumas Motorola 68000 Tutorial]
- [68000 Tricks and Traps]
Subcategories
This category has the following 2 subcategories, out of 2 total.
@
- 68000 Assembly User (9 P)
Pages in category "68000 Assembly"
The following 106 pages are in this category, out of 106 total.