HOW TO WRITE A HARD DRIVE INSTALLER FOR STRIDER (MFM)

Written by Codetapper/Action on 20/08/2004!

INTRODUCTION

This tutorial has been written to explain how to write a hard drive
installer for the Amiga game Strider which is in a non-standard disk
format. The game uses a custom MFM format and loading system and we will
reverse engineer the disk format to save the game to a single file, and
write a WHDLoad slave to allow it to run from a hard drive.

To follow this tutorial you must have a working knowledge of 680×0
assembly language as well as the Amiga hardware. I have not explained how
to use tools like Resource – you must know how to use them yourself or read
the documentation that comes with the program.

You can also use tools like the Action Replay cartridge to disassemble
code but once you turn off your Amiga you have lost everything. With a
disassembler you can save your work and reload it later. Hence there is no
real support for Action Replay like monitors in this document.

TOOLS REQUIRED

In this tutorial I have used the following tools:

        Resource – Disassembler
WWarp – Processing the disk image
Grab – Saves part of a file
RawDIC – Ripping the disk

You should not need any of the tools to follow the tutorial, but it is a
good exercise to try them out yourself:

WWarp and RawDIC are available in the WHDLoad developer package at
http://www.whdload.de

The Grab utility is on Aminet or the Action website at http://zap.to/action

Resource is a commercial product and you will have to obtain this by some
other means. Please do not ask me for it!

WRITING THE GAME BACK TO A FLOPPY DISK

If you have the file Strider.wwp you can write the game back to a floppy
disk and try it out. To do this, execute the following commands:

        1:> WWarp Strider.wwp W
writing track 0, format dosf
writing track 1, format tiertex
testing drive speed at track 1, 3142.7 3143.2 3143.0 3142.1
3142.3, using writelen 3154.
writing track 2, format tiertex
writing track 3, format tiertex

writing track 152, format tiertex
writing track 153, format tiertex

WWarp happens to know the disk layout for this game and can therefore
produce a working copy of the disk minus any special longtrack copy
protection the game might have.

PART 1: THE BOOTBLOCK

First we need to grab the bootblock of the game. There are several tools
you can use for this, but since we have Strider as a WWarp file let’s use
WWarp:

        1:> WWarp Strider.wwp S 0
save file track.000.dosf

This means save track 0 of strider. The output file is track.000.dosf. Now
we only really care about the bootblock, so grab the first 1024 bytes of it
into a new file:

        1:> Grab track.000.dosf boot FIRST 1024
Grab v0.01
? 1998 Codetapper ([email protected])

Reading from track.000.dosf to boot from 0 to 1024

You now have the bootblock as a single file called “boot” which is 1024
bytes in length. You can now move onto the next phase.

PART 2: DISASSEMBLING THE BOOTBLOCK

Start off by disassembling the bootblock by loading it into your
disassembler. It will look like this:

                neg.w   ????
subq.b #1,d0
move.w -(a5),-(sp)
chk.w ????,d4
ori.b #0,d0
lea ( DFF000).l,a0
move.w # 7FFF,( 9A,a0)
move.w # 7FFF,( 96,a0)
move.w # F00,( 180,a0)
lea (START+ 2E,pc),a1
move.l a1,( 20).l
move.w # 2000,sr
lea ( 80000).l,sp
lea (START+ 50,pc),a1
lea ( 80).l,a2
movea.l a2,a3
move.w # 1FF,d0
move.l (a1)+,(a2)+
dbra d0,START+ 48
jmp (a3)

The first 3 longwords are data – first off is the type of disk it is, the
string “DOS” followed by a number. Then comes the checksum for the
bootblock. Following that is a magic number that is ignored so can be
anything. A lot of crackers put their name or handle here! Change the
first 3 longwords to display as data, and set the next part to code:

                dc.l    (‘DOS’<<8)              ;Type of disk
                dc.l     3F25498E               ;Checksum
                dc.l    0                       ;Magic number (ignored)

                lea     ( DFF000).l,a0
                move.w  # 7FFF,( 9A,a0)
                move.w  # 7FFF,( 96,a0)
                move.w  # F00,( 180,a0)
                lea     (START+ 2E,pc),a1
                move.l  a1,( 20).l
                move.w  # 2000,sr
                lea     ( 80000).l,sp
                lea     (START+ 50,pc),a1
                lea     ( 80).l,a2
                movea.l a2,a3
                move.w  # 1FF,d0
                move.l  (a1)+,(a2)+
                dbra    d0,START+ 48
                jmp     (a3)

Now disassemble the bootblock:

                dc.l    (‘DOS’<<8)              ;Type of disk
                dc.l     3F25498E               ;Checksum
                dc.l    0                       ;Magic number (ignored)
        
                lea     ( DFF000).l,a0
                move.w  # 7FFF,( 9A,a0)
                move.w  # 7FFF,( 96,a0)
                move.w  # F00,( 180,a0)
                lea     (lbC00002E,pc),a1
                move.l  a1,( 20).l
lbC00002E       move.w  # 2000,sr
                lea     ( 80000).l,sp
                lea     (lbC000050,pc),a1
                lea     ( 80).l,a2
                movea.l a2,a3
                move.w  # 1FF,d0
lbC000048       move.l  (a1)+,(a2)+
                dbra    d0,lbC000048
                jmp     (a3)

Now let’s work out what the bootblock is doing:

                lea     ( DFF000).l,a0          ;a0 = Custom registers
move.w # 7FFF,(intena,a0) ;Disable interrupts
move.w # 7FFF,(dmacon,a0) ;Disable DMA
move.w # F00,(color,a0) ;Set background to red

The background turns red because we wrote f00 which is an RGB value. The
red component is f, green is 0 and blue is 0. Hence the screen will
turn red.

All standard stuff so far – killing the operating system. Now the game
wants to go into supervisor mode:

                lea     (lbC00002E,pc),a1
move.l a1,( 20).l
lbC00002E move.w # 2000,sr
lea ( 80000).l,sp

The game sets up the routine lbC00002E at 20 so if a violation occurs, the
routine will be run. The game sets the status register to 2000 which
causes a violation to occur, so the 68000 follows the vector at 20 which
points to the routine lbC00002E. Let’s comment this:

                lea     (_Supervisor,pc),a1     ;Go into supervisor mode
move.l a1,( 20).l
_Supervisor move.w # 2000,sr
lea ( 80000).l,sp ;Supervisor stack = 80000

The Amiga is now taken over and the stack has been moved to 80000. Chances
are the game requires 512k of chip memory (512k = 80000). Let’s continue:

                lea     (lbC000050,pc),a1
lea ( 80).l,a2
movea.l a2,a3
move.w # 1FF,d0
lbC000048 move.l (a1)+,(a2)+
dbra d0,lbC000048
jmp (a3)

lbC000050 move.w # 7CF,d7

The game wants to copy the code to 80 because currently the routine could
be located anywhere in memory:

                lea     (_Hex80,pc),a1          ;Code we want to copy
lea ( 80).l,a2 ;Setup destination
movea.l a2,a3 ;a3 = Destination
move.w # 1FF,d0
_CopyTo80 move.l (a1)+,(a2)+ ;Copy 200-1 longs from
dbra d0,_CopyTo80 ;a1 to a2
jmp (a3) ;Jump to 80

_Hex80 move.w # 7CF,d7

Now the game is clearing some memory at 70000:

_Hex80          move.w  # 7CF,d7                ;Clear  7d0 longwords at
lea ( 70000).l,a2 ; 70000 (which is 1f40
_Clear70000 clr.l (a2)+ ;bytes, which is 8000
dbra d7,_Clear70000 ;bytes in decimal)

The next piece of code is rather stupid and pointless:

                lea     (lbW00031C,pc),a1
lea ( 70000).l,a2
move.w # 1F,d7
lbC00006E move.w #2,d6
lbC000072 dbra d6,lbC000072
adda.w # 22,a2
dbra d7,lbC00006E

The d6 and d7 dbra loops are run but there is nothing in between the d6
loop (as it just decrements d6 until d6 is false = ffff) and then it adds
on 22 to a2 for d7 loops. It is most likely setting up some kind of
copperlist, but we don’t need to worry about it now. The next part is more
interesting as it is setting up a screen and a copperlist:

                move.w  #0,( 180,a0)
move.w # 200,( 96,a0)
move.w # 1200,( 100,a0)
clr.w ( 102,a0)
clr.w ( 108,a0)
move.w # 38,( 92,a0)
move.w # D0,( 94,a0)
move.w # 2C81,( 8E,a0)
move.w # F4C1,( 90,a0)
lea (lbW000290,pc),a1
move.l a1,( 80,a0)
move.w ( 88,a0),d0
move.w # 8380,( 96,a0)

lbW000290 dc.w E0
dc.w 7
dc.w E2
dc.w 0
dc.w 182
dc.w A00
dc.w 3201
dc.w FFFE
dc.w 182
dc.w A10
dc.w 3401
dc.w FFFE

dc.w 182
dc.w EC0
dc.w 4E01
dc.w FFFE
dc.w FFFF
dc.w FFFE

The first part is referencing dff000 (stored in a0 remember!) which is
custom hardware references, so let’s fill in some comments:

                move.w  #0,(color,a0)           ;Background to black
move.w # 200,(dmacon,a0) ;Master DMA switch off
move.w # 1200,(bplcon0,a0) ;Setup 1 bitplane screen
clr.w (bplcon1,a0) ;No playfield scroll
clr.w (bpl1mod,a0) ;No bitplane modulo
move.w # 38,(ddfstrt,a0) ;Setup data display fetch
move.w # D0,(ddfstop,a0) ;start and stop values
move.w # 2C81,(diwstrt,a0) ;Setup display window
move.w # F4C1,(diwstop,a0) ;start and stop values
lea (_Copperlist,pc),a1 ;a1 = Copperlist
move.l a1,(cop1lc,a0) ;Move into copperlist
move.w (copjmp1,a0),d0 ;counter 1 and strobe
move.w # 8380,(dmacon,a0) ;Enable DMA, bitplane
;and copper

Now we come to the fun part, the custom disk accessing! Here is the
routine before commenting – see if you can spot the loading parameters:

                lea     ( BFE001).l,a1
lea ( BFD000).l,a2
lea (lbL000314,pc),a6
move.b # 7F,( 100,a2)
andi.b # F7,( 100,a2)
ori.b #2,( 100,a2)
lbC0000E4 andi.b # FE,( 100,a2)
ori.b #1,( 100,a2)
bsr.w lbC000158
btst #4,(0,a1)
bne.b lbC0000E4
clr.w (2,a6)
move.w #1,(0,a6)
lea ( 800).l,a5
move.l # 2BF20,d5
move.l d5,d7
addi.l # 17FF,d7
divu.w # 1800,d7
subq.w #1,d7
bsr.w lbC000162
ori.b # 88,( 100,a2)
andi.b # F7,( 100,a2)
move.b # FF,( 100,a2)
move.w (2,a6),d1
move.w (0,a6),d0
jmp ( 81C).l

The important registers for disk accessing are bfe001 and bfd100. The
CIA register bfe001 tells you when a disk has been removed, is write
protected, has it’s heads on track 0, and is ready to receive commands.

The CIA register bfd100 controls moving the heads, setting the direction
to move, selecting the side of the disk, selecting which floppy drive is
under control and turning on the disk motors.

                lea     ( BFE001).l,a1
lea ( BFD000).l,a2
lea (_DiskParams,pc),a6
move.b # 7F,( 100,a2) ;Motor on
andi.b # F7,( 100,a2) ;Select DF0:
ori.b #2,( 100,a2) ;Set direction outwards

To step the disk drive heads you must set bit 0 of bfd100 to 1, then 0 and
then 1 again followed by a 3 millisecond delay. If the delay is not at
least 3ms, the heads will not have finished moving, so you might not have
the heads over the correct cylinder.

_GoToTrack0     andi.b  # FE,( 100,a2)          ;Prepare to step heads
ori.b #1,( 100,a2)
bsr.w _Delay
btst #4,(0,a1)
bne.b _GoToTrack0

_Delay move.w # 10C8,d0
_EmptyLoop dbra d0,_EmptyLoop
rts

The first thing to notice is the stupid programmer has made a nasty CPU
dependant loop for the delay. It counts down from 10c8 to -1 and then
returns. On a fast CPU with a cache, the loop may be almost instant, so
the heads probably will not have moved in time. Hence this game is
unlikely to work on most grunty Amigas without degrading.

Because the game has just loaded the bootblock, the heads should be on
track 0 but if they weren’t, the loop would repeat by setting bit 0 to 0,
then to 1, delaying for a while and then checking if the disk is on track
0. If it isn’t on track 0 (because bit 4 is not 0) it goes back in a loop:

_GoToTrack0     andi.b  # FE,( 100,a2)          ;Set bit 0 to 0
ori.b #1,( 100,a2) ;Set bit 0 to 1
bsr.w _Delay ;Wait 3ms (yeah right!)
btst #4,(0,a1) ;Test if we are on track
bne.b _GoToTrack0 ;0 and loop until we are

_Delay move.w # 10C8,d0 ;Lame 3ms delay
_EmptyLoop dbra d0,_EmptyLoop
rts

The Amiga hardware has no idea where the disk drive heads are. The only
thing it can tell you is if they are on track 0. So to go and load data on
track 10, you have to go to track 0, then keep a counter yourself of how
many tracks you have stepped over.

We know the disk drive heads for DF0: are on track 0 so let’s continue.
Usually after moving the heads you do some kind of load and jmp to start
the next part, so keep that in mind when analysing the next part of the
code:

                clr.w   (2,a6)                  ;Track heads are over
move.w #1,(0,a6) ;Track to load perhaps?
lea ( 800).l,a5 ;Load address perhaps?
move.l # 2BF20,d5 ;Length perhaps?
move.l d5,d7
addi.l # 17FF,d7
divu.w # 1800,d7 ;Divide length by 1800
subq.w #1,d7 ;and subtract one
bsr.w _Loader ;Load data
ori.b # 88,( 100,a2) ;Motor off, deselect DF0:
andi.b # F7,( 100,a2) ;Deselect DF0:
move.b # FF,( 100,a2)
move.w (2,a6),d1 ;Setup parameters
move.w (0,a6),d0
jmp ( 81C).l ;Start game

I usually find it easier to guess what the parameters are for a loader, and
then change the labels later if I guessed incorrectly. Giveaways for
offset or lengths include division. A normal AmigaDos disk has 1600 bytes
per track of data. If you wanted to start reading the 5000th byte of a
disk, you need to work out which track that would be on. To do that you
divide 5000 by 1600 and take just the integer part. 5000 / 1600 = 3
remainder e00. So you would load track 3 and start copying data from
position e00.

                addi.l  # 17FF,d7
divu.w # 1800,d7 ;Divide length by 1800
subq.w #1,d7 ;and subtract one

In Striders case, it is adding on 17ff, dividing by 1800 and subtracting
one. If you put some numbers into the formula, you can see what is
happening:

             0 +  17ff =   17ff /  1800 =  0 – 1 = -1
200 + 17ff = 19ff / 1800 = 1 – 1 = 0
1800 + 17ff = 2fff / 1800 = 1 – 1 = 0
2bf20 + 17ff = 2d71f / 1800 = 30 – 1 = 29

If there is nothing to load, d7 is -1. If there is one track to load, d7
is 0. In our case, we have d7 equal to 29. Whenever you see a minus one
after a calculation you can be pretty sure the game uses a DBF loop in it’s
code to count how many loops to perform. DBF loops count down until they
are false which means they do one extra loop. Hence programmers subtract
one from calculations so the DBF loop will be the correct number of loops.

The division is the length of a track, so in this case Strider expects
1800 bytes of data per track. This is more than AmigaDos can handle, so
this is a custom MFM format.

Whenever you disassemble a routine you should try and think about
everything you would do yourself to write a safe generic routine. If you
do this, it will often help you work out what code is doing. This is part
of the art of Zen cracking – identifying code just by a quick glance.

Back to Strider – if I had to write a routine to load a big chunk of data,
I would start by moving to the correct track. On the Amiga a disk has 2
sides, so either you load all data from one side of the disk, then switch
to the other, or you alternate between the disk sides. I would setup a
retry counter so if the read failed, it would retry a few times. If the
read was successful, move to the next track, otherwise just reload that
track. If a track could not be read a number of times, flash the screen or
print up an error message. If the data appeared to load OK, check if the
data matches what we expect. Keep loading until we have read enough data
correctly. Keep all this in the back of your mind as it will help
analysing loaders! Not all games will have retry counts, checksums etc but
if you try and consider everything that could exist, it will help you guess
what each part is doing.

Here is the rest of the bootblock commented:

_FatalError     move.w  # F00,(color,a0)        ;Red background
bsr.w _WaitAWhile
move.w #0,( 180,a0)
bra.w _ChecksumOK

_Loader move.w #8,(4,a6) ;Retry count perhaps?
move.w (0,a6),d0
btst #0,d0 ;Test for odd number
beq.b _MoveTracks ;If it’s even, skip ahead
andi.b # FB,( 100,a2) ;Select top of disk
bra.b _WaitDiskReady

_MoveTracks ori.b #4,( 100,a2) ;Select bottom of disk
andi.b # FD,( 100,a2) ;Direction inwards, bit 0
andi.b # FE,( 100,a2) ;to 1, then 0, then 1
ori.b #1,( 100,a2) ;(step a track)
bsr.w _Delay ;3ms delay
_WaitDiskReady btst #5,(0,a1) ;Wait for the disk drive
bne.b _WaitDiskReady ;to be ready
clr.w (dsklen,a0) ;Track length = 0
move.w # 8210,(dmacon,a0) ;Enable disk DMA
move.w # 8400,(adkcon,a0) ;Word sync
move.w # A245,(dsksync,a0) ;Sync = a245
move.l # 7C000,(dskpt,a0) ;Destination for MFM
move.w # 9C00,(dsklen,a0) ;Write to disk length
move.w # 9C00,(dsklen,a0) ;twice to start disk DMA
_WaitDiskInt move.w ( 1E,a0),d0 ;Wait for the disk
andi.w #2,d0 ;interrupt to show data
beq.b _WaitDiskInt ;has finished loading
move.w d0,( 9C,a0) ;Acknowledge interrupt
clr.w (dsklen,a0) ;Clear track length
lea ( 7C002).l,a3 ;a3 = Start of data (one
movea.l a5,a4 ;word past first sync)
move.l # 55555555,d3 ;MFM Mask
move.w (a3)+,d2 ;Merge 2 MFM words of
move.w (a3)+,d1 ;data into one word
and.w d3,d2
and.w d3,d1
add.w d2,d2
or.w d2,d1
cmp.w (0,a6),d1 ;Check track number is
beq.b _TrackNumberOK ;what we expected
move.w # FF,(color,a0) ;Cyan background to show
bsr.w _WaitAWhile ;error and wait a bit
_TrackNumberOK moveq #0,d4
move.w # 5FF,d6 ;Decode 600-1 longs
_DecodeLoop move.l ( 1804,a3),d1 ;Read even longword
and.l d3,d1 ;Mask out clock bits
move.l (a3)+,d2 ;Read odd longword
and.l d3,d2 ;Mask out clock bits
add.l d2,d2 ;Rotate odd bits left
or.l d2,d1 ;Merge odd and even bits
add.l d1,d4 ;Add to checksum
move.l d1,(a4)+ ;Store decoded longword
dbra d6,_DecodeLoop ;Decode rest of track
move.l ( 1804,a3),d1 ;Read even longword
and.l d3,d1 ;Mask out clock bits
move.l (a3)+,d2 ;Read odd longword
and.l d3,d2 ;Mask out clock bits
add.l d2,d2 ;Rotate odd bits left
or.l d2,d1 ;Merge odd and even bits
not.l d4
cmp.l d4,d1 ;Compare checksum from
beq.b _ChecksumOK ;data with expected value
subq.w #1,(4,a6) ;Checksum did not match so
beq.w _FatalError ;decrement retry count
move.w #15,(color,a0) ;Blue background
bra.w _WaitDiskReady

_ChecksumOK movea.l a4,a5 ;Move destination address
move.w (0,a6),d1 ;Read previous track
addq.w #1,d1 ;number, add one and
move.w d1,(0,a6) ;store this new value
lsr.w #1,d1 ;Divide track number by 2
move.w d1,(2,a6) ;to get cylinder and store
dbra d7,_Loader ;Loop around loading
rts

_WaitAWhile move.l d0,-(sp)
_WaitButtonDown btst #6,( BFE001).l
bne.w _WaitButtonDown
move.w # FFFF,d0
_SmallDelay nop
dbra d0,_SmallDelay
_WaitButtonUp btst #6,( BFE001).l
beq.w _WaitButtonUp
move.w # FFFF,d0
_AnotherDelay nop
dbra d0,_AnotherDelay
move.l (sp)+,d0
rts

To start disk access on the Amiga you must write to the disklen register
twice. This is a safety mechanism so that rogue programs that have crashed
are not as likely to destroy data when they have access faults.

MFM data has a clock bit between each real bit of data. The clock bit is
set to 0 or 1 depending on the data bit and this is done so that long runs
of 0’s or 1’s do not occur. It is much easier for a disk controller to
synchronise when the data changes between 0 and 1 regularly.

Once you have identified the loading routine, you need to write a program
to rip all the data from the disk. Some crackers will try and play through
the game with a cartridge and save the files as they load. This is
obviously dodgy when the game isn’t sequential or has lots of hidden
sections that might be missed. Others will use the games own loader and
call the routine with the parameters to get at the data. This can also
cause problems because sometimes you do not have a table of everything the
game loads. The safest method is to rip every track of the disk and store
it in one big chunk. Then you alter the game loader so instead of using
the built in loader (which works in chunks of 1800 bytes/track) it calls
your loader.

We will use the great program RawDIC to write a “slave” which knows the
layout of the disk and will include enough code to decode a track of MFM
data into a track of useful data.

Before we proceed you should understand the 3 vital disk hardware
registers. Here is some background information:

 DFF020 DSKPT
Disk Pointer
Status: Write-Only.
Chip: Agnus

This is where you store the starting address of your disk data prior to
activating disk DMA. DSKPT actually consists of two separate hardware
registers – DSKPTH (H for the High bits of the address) and DSKPTL (L for
the Low bits of the address). Since they’re mapped as two consecutive
memory locations, it’s easiest to treat them as one 32-bit register.

DFF024 DSKLEN
Disk Data Length
Status: Write-Only.
Chip: Paula

Bits 0-13: LENGTH: Number of words to read or write
Bit 14: WRITE: 1 = Activate write mode; 0 = Activate read mode
Bit 15: DMAENA: 1 = Activate disk DMA; 0 = Deactivate disk DMA

DFF07E DSKSYNC
Disk Sync Pattern
Status: Write-Only.
Chip: Agnus

Before reading data from the disk, it’s often necessary to syncronize the
drive’s head on a particular bit pattern. This register allows you to do
just that.
When the WORDSYNC bit (bit 10) in the ADKCON register ( DFF09E) is set, the
disk controller’s DMA is enabled and the controller prepares to search the
disk for the sync pattern found in this register. The disk controller
doesn’t start searching until this register is written to. When the sync
pattern is found, subsequent data is read into RAM. Bit 12 of the DSKBYTR
register ( DFF01A) is set to 1 for two or four microseconds (depending on
the setting of ADKCON’s bit 8) as soon as the sync pattern is located. This
event can also be used to trigger a level 6 interrupt.
In MFM format (the disk format used by AmigaDOS), the sync pattern should
be a value that is impossible to create using MFM data coding. This way it
can’t be confused with actual data.

Now let’s work through the loader in stages and identify the main parts:

                move.w  # A245,(dsksync,a0)     ;Sync =  a245
move.l # 7C000,(dskpt,a0) ;Destination for MFM
move.w # 9C00,(dsklen,a0) ;Write to disk length
move.w # 9C00,(dsklen,a0) ;twice to start disk DMA

A normal AmigaDOS disk uses the sync 4489. Strider uses a non-standard
sync of a245.

This game uses the address 7c000 as it’s MFM buffer. This is where the
data will appear once it has been read from the disk.

Bits 0-13 of DSKLEN ( dff024) store the length, so mask out the 2 high bits
of the length register – ie. 9c00 AND 3fff = 1c00. Strider therefore
is reading 1c00 words of MFM data.

Now the game waits for the disk interrupt to happen:

_WaitDiskInt    move.w  ( 1E,a0),d0             ;Wait for the disk
andi.w #2,d0 ;interrupt to show data
beq.b _WaitDiskInt ;has finished loading
move.w d0,( 9C,a0) ;Acknowledge interrupt
clr.w (dsklen,a0) ;Clear track length

Now we need to work out how to decode this MFM data into normal data. This
is because the data at 7c000 has 2 bits of data for every one useful bit,
so we must skip the useless stuff.

                lea     ( 7C002).l,a3           ;a3 = Start of data (one
movea.l a5,a4 ;word past first sync)
move.l # 55555555,d3 ;MFM Mask
move.w (a3)+,d2 ;Merge 2 MFM words of
move.w (a3)+,d1 ;data into one word
and.w d3,d2
and.w d3,d1
add.w d2,d2
or.w d2,d1
cmp.w (0,a6),d1 ;Check track number is
beq.b _TrackNumberOK ;what we expected

Track 1 as MFM data looks like this:

                 a245  4489  2aaa  aaa9  12a5  2aa9…

The sync a245 was read by the disk controller and as soon as it is found
it starts copying the data into the DSKPT register ( 7c000). The first
word of data will therefore be the 4489 at address 7c000. The register
a3 points at 7c002 which is 2 bytes past the sync. Hence a3 is pointing
to the 2aaa part. Now tracing through the code:

                lea     ( 7C002).l,a3           ;a3 =  7c002
movea.l a5,a4 ;a4 = 800
move.l # 55555555,d3 ;d3 = 55555555

Now it starts processing the data, starting with the 2aaa:

                move.w  (a3)+,d2                ;d2 =  2aaa
move.w (a3)+,d1 ;d1 = aaa9
and.w d3,d2 ;d2 = 0000
and.w d3,d1 ;d3 = 0001
add.w d2,d2 ;d2 = 0000
or.w d2,d1 ;d1 = 0001

Now after reading 2 words of MFM data, stripping out the clock bits and
adjusting for odd bits, we have one word of real data. The register d1 is
now equal to 1, which funnily enough matches the track number we loaded:

                cmp.w   (0,a6),d1               ;Check track number is
beq.b _TrackNumberOK ;what we expected

So far so good! We read track 1 from the disk, found the sync we wanted,
and read the next useful word of data which is the track number. If the
track number didn’t match (because of a bad disk or a faulty read) then the
following code would turn the screen cyan and wait a while:

                move.w  # FF,(color,a0)         ;Cyan background to show
bsr.w _WaitAWhile ;error and wait a bit

Continuing on:

_TrackNumberOK  moveq   #0,d4
move.w # 5FF,d6 ;Decode 600-1 longs
_DecodeLoop move.l ( 1804,a3),d1 ;Read even longword
and.l d3,d1 ;Mask out clock bits
move.l (a3)+,d2 ;Read odd longword
and.l d3,d2 ;Mask out clock bits
add.l d2,d2 ;Rotate odd bits left
or.l d2,d1 ;Merge odd and even bits
add.l d1,d4 ;Add to checksum
move.l d1,(a4)+ ;Store decoded longword
dbra d6,_DecodeLoop ;Decode rest of track

The game now processes 600 longwords. d6 is set to 5ff and because of the dbra
loop it does one extra loop which is 600. 600 loops processing one
longword of data means 600 x 4 = 1800 bytes. So this is the guts of the
loader, as we decode the MFM buffer into 1800 bytes of real data.

Looking at the label _DecodeLoop, you can see that it reads an even
longword of data from 1804 bytes past the current position of a3, and then
reads the odd longword from the present position of a3 and then increments
it. It then loops around 600 times. Note that at the top of the routine it
sets d4 to 0, and everytime it decodes a longword, it adds the value onto
d4. This is a dead giveaway for a checksum! Continuing on:

                move.l  ( 1804,a3),d1           ;Read even longword
and.l d3,d1 ;Mask out clock bits
move.l (a3)+,d2 ;Read odd longword
and.l d3,d2 ;Mask out clock bits
add.l d2,d2 ;Rotate odd bits left
or.l d2,d1 ;Merge odd and even bits

We have read the sync, decoded the track number, decoded the main data on
the track and now are decoding another longword. At the end of the routine,
d1 contains the longword read from the disk.

                not.l   d4
cmp.l d4,d1 ;Compare checksum from
beq.b _ChecksumOK ;data with expected value

Now d4 is inverted with the not.l operation, and the value d1 is compared
to d4. If the values match, it branches to the label _ChecksumOK. If the
value did not match, the following code is run which decrements a retry
count and sets the background colour to blue:

                subq.w  #1,(4,a6)               ;Checksum did not match so
beq.w _FatalError ;decrement retry count
move.w #15,(color,a0) ;Blue background
bra.w _WaitDiskReady

If the checksum was OK it adjusts the destination address, increments the
track number and loops around loading until d7 is ffff:

_ChecksumOK     movea.l a4,a5                   ;Move destination address
move.w (0,a6),d1 ;Read previous track
addq.w #1,d1 ;number, add one and
move.w d1,(0,a6) ;store this new value
lsr.w #1,d1 ;Divide track number by 2
move.w d1,(2,a6) ;to get cylinder and store
dbra d7,_Loader ;Loop around loading
rts

After the data has been loaded, the disk drive is turned off, and a few
parameters are passed into the game via d0 and d1, and then jmp 81c starts
the action:

                ori.b   # 88,( 100,a2)          ;Motor off, deselect DF0:
andi.b # F7,( 100,a2) ;Deselect DF0:
move.b # FF,( 100,a2)
move.w (2,a6),d1 ;Setup parameters
move.w (0,a6),d0
jmp ( 81C).l ;Start game

We now have enough information to rip track 1 from the game. And 99% of
games use the same disk loader for the entire game, so once you can rip
track 1 you can generally rip the entire disk.

PART 3: RIPPING THE DISK WITH RAWDIC

RawDIC is the tool of choice for cleanly ripping a disk in a system
friendly way. It consists of a series of lists that describe the format of
the disk, as well as routines to decode a track of data. All we know from
the game so far is that track 0 is standard AmigaDOS format and track 1 is
a custom format with sync a245 which has 1800 bytes/track. We can assume
the rest of the disk is also this format and change it later if this isn’t
the case.

We will start off by creating the routine to decode a track of data.
RawDIC passes the MFM data from the disk in the register a0, and expects
you to fill the buffer a1 with the decoded data. RawDIC passes the track
number into your routine in the register d0, and expects you to return a
success or error code in d0 depending on your results.

Once you have worked out the loading routine you can lift the entire
routine and drop it into your source. Then you usually just need to change
a few addresses to make it work:

                move.l  # 7C000,(dskpt,a0)      ;Destination for MFM
lea ( 7C002).l,a3 ;a3 = Start of data (one
movea.l a5,a4 ;word past first sync)

These absolute values of 7c000 and 7c002 were used in Strider but you
cannot use these in the RawDIC imager as the data could be loaded anywhere.
RawDIC takes care of all the disk seeking and reading so all you need to
worry about is converting the data. All registers can be destroyed so you
do not need to worry about preserving anything.

Strider uses a3 as the source data and a5 (which was moved into a4) as the
destination for the data. So in the RawDIC imager, you can put set the
RawDIC parameters to what Strider wanted:

_RipTrack       lea     (2,a0),a3               ;lea     ( 7C002).l,a3
movea.l a1,a4 ;movea.l a5,a4

Now we copy in the code from Strider:

                move.l  # 55555555,d3           ;MFM Mask
move.w (a3)+,d2 ;Merge 2 MFM words of
move.w (a3)+,d1 ;data into one word
and.w d3,d2
and.w d3,d1
add.w d2,d2
or.w d2,d1

At this poing the game checks if the track number matched. Strider had a
small structure in register a6 which stored a few parameters such as the
track number, so we must change this:

                cmp.w   (0,a6),d1               ;Check track number is
beq.b _TrackNumberOK ;what we expected

RawDIC passed the actual track number in register d0, so rather than
comparing (0,a6) with d1, we can just compare d0 with d1:

                cmp.w   d0,d1

If the track number does not match, we need to bail out with an error code,
so rather than branching if the track number matched, let’s abort if it
didn’t. There are 3 main error codes in RawDIC:

        IERR_OK       = Sucess!
IERR_CHECKSUM = Checksum error
IERR_NOSECTOR = No Sector error

We’ll make RawDIC abort with the no sector error if it failed, so the code
becomes:

                cmp.w   d0,d1
bne _NoSector

_NoSector moveq #IERR_NOSECTOR,d0
rts

Now we can keep copying Striders loader because it just processes data and
will safely work no matter where in memory it is running from:

                moveq   #0,d4
move.w # 5FF,d6 ;Decode 600-1 longs
_DecodeLoop move.l ( 1804,a3),d1 ;Read even longword
and.l d3,d1 ;Mask out clock bits
move.l (a3)+,d2 ;Read odd longword
and.l d3,d2 ;Mask out clock bits
add.l d2,d2 ;Rotate odd bits left
or.l d2,d1 ;Merge odd and even bits
add.l d1,d4 ;Add to checksum
move.l d1,(a4)+ ;Store decoded longword
dbra d6,_DecodeLoop ;Decode rest of track
move.l ( 1804,a3),d1 ;Read even longword
and.l d3,d1 ;Mask out clock bits
move.l (a3)+,d2 ;Read odd longword
and.l d3,d2 ;Mask out clock bits
add.l d2,d2 ;Rotate odd bits left
or.l d2,d1 ;Merge odd and even bits
not.l d4

The checksum part needs a small change aswell:

                cmp.l   d4,d1                   ;Compare checksum from
beq.b _ChecksumOK ;data with expected value
subq.w #1,(4,a6) ;Checksum did not match so
beq.w _FatalError ;decrement retry count
move.w #15,(color,a0) ;Blue background
bra.w _WaitDiskReady

_ChecksumOK movea.l a4,a5 ;Move destination address

If the checksum matched, it means we have successfully decoded a track of
data, so we are done. There is no need to step the heads and alter
destination addresses because RawDIC takes care of all of this. Just tell
RawDIC we have succeeded if the checksum matched, and if it failed then
return the error code IERR_CHECKSUM. RawDIC will then re-read the track a
few times in case it just had a weak read. The code above therefore
becomes:

                cmp.l   d4,d1                   ;Compare checksum from
beq.b _OK ;data with expected value
bra _Checksum

_OK moveq #IERR_OK,d0
rts

_Checksum moveq #IERR_CHECKSUM,d0
rts

And that is basically it! RawDIC now just needs to know which tracks to
decode on the disk by setting up a tracklist:

TL_1            TLENTRY 000,000, 1600,SYNC_STD,DMFM_STD
TLENTRY 001,159, 1800, a245,_RipTrack
TLEND
EVEN

This means for the first tracklist entry we are reading from track 000 to
000, storing 1600 bytes/track, using a standard sync ( 4489) and using the
built in standard decoder.

For tracks 001 to 159, we are storing 1800 bytes/track and the sync
pattern is a245. Everytime it decodes a track it calls the routine
_RipTrack.

Here is the RawDIC imager source so far:

                ; Strider Imager by Codetapper/Action!

incdir include:
include RawDIC.i

OUTPUT “Strider.islave”

;=====================================================================

SLAVE_HEADER
dc.b 1 ;Slave version
dc.b 0 ;Slave flags
dc.l DSK_1 ;Pointer to the first disk structure
dc.l Text ;Pointer to the text displayed in the imager window

dc.b ” VER:”
Text dc.b “Strider imager V1.0”,10
dc.b “by Codetapper/Action (20.06.2002)”
dc.b 0
cnop 0,4

;=====================================================================

DSK_1 dc.l 0 ;Pointer to next disk structure
dc.w 1 ;Disk structure version
dc.w DFLG_NORESTRICTIONS ;Disk flags
dc.l TL_1 ;List of tracks which contain data
dc.l 0 ;UNUSED, ALWAYS SET TO 0!
dc.l FL_DISKIMAGE ;List of files to be saved
dc.l 0 ;Table of certain tracks with CRC values
dc.l 0 ;Alternative disk structure, if CRC failed
dc.l 0 ;Called before a disk is read
dc.l 0 ;Called after a disk has been read

TL_1 TLENTRY 000,000, 1600,SYNC_STD,DMFM_STD
TLENTRY 001,159, 1800, a245,_RipTrack
TLEND
EVEN

;=====================================================================

_RipTrack lea (2,a0),a3 ;lea ( 7C002).l,a3
movea.l a1,a4 ;movea.l a5,a4
move.l # 55555555,d3 ;MFM Mask
move.w (a3)+,d2 ;Merge 2 MFM words of
move.w (a3)+,d1 ;data into one word
and.w d3,d2
and.w d3,d1
add.w d2,d2
or.w d2,d1
cmp.w d0,d1 ;Check track number is
bne _NoSector ;what we expected
moveq #0,d4
move.w # 5FF,d6 ;Decode 600-1 longs
_DecodeLoop move.l ( 1804,a3),d1 ;Read even longword
and.l d3,d1 ;Mask out clock bits
move.l (a3)+,d2 ;Read odd longword
and.l d3,d2 ;Mask out clock bits
add.l d2,d2 ;Rotate odd bits left
or.l d2,d1 ;Merge odd and even bits
add.l d1,d4 ;Add to checksum
move.l d1,(a4)+ ;Store decoded longword
dbra d6,_DecodeLoop ;Decode rest of track
move.l ( 1804,a3),d1 ;Read even longword
and.l d3,d1 ;Mask out clock bits
move.l (a3)+,d2 ;Read odd longword
and.l d3,d2 ;Mask out clock bits
add.l d2,d2 ;Rotate odd bits left
or.l d2,d1 ;Merge odd and even bits
not.l d4
cmp.l d4,d1 ;Compare checksum from
beq.b _OK ;data with expected value
bra _Checksum

;=====================================================================

_OK moveq #IERR_OK,d0
rts

_Checksum moveq #IERR_CHECKSUM,d0
rts

_NoSector moveq #IERR_NOSECTOR,d0
rts

Now to test it! Assemble the code and call RawDIC with the name of the slave
as the parameter:

        1:> RawDIC SLAVE=Strider.slave

If you have the original Strider disk (or have copied the game back onto a
disk) you can just insert the disk and let RawDIC run. If you don’t have
the original disk, it used to be a problem! However you can now use
MFMWarp or WWarp files and “insert” these into RawDIC so instead of reading
from the floppy it reads from the files. This is also much quicker than a
physical floppy disk! You can also use a CAPS image inserted into the disk
drive and run the imager in WinUAE.

After letting RawDIC run, it comes up with “sector missing on track 154”.
This is most likely because the game only has data on tracks 1-153, with
154 onwards being unformatted or some kind of copy protection. Just skip
the track and let RawDIC continue, noting down all tracks that have errors.
In Striders case, tracks 154-159 all say “sector missing”. For now we have
no idea what format these other tracks are so let’s change the tracklist so
we don’t bother imaging them:

TL_1            TLENTRY 000,000, 1600,SYNC_STD,DMFM_STD
TLENTRY 001,153, 1800, a245,_RipTrack
TLEND
EVEN

Now RawDIC will image track 0 as an AmigaDOS track, and all other tracks as
the format used on Strider. There is just one other change to make now to
make the install more elegant. Strider has 1800 bytes/track, but for
track 0 we are imaging it as a standard AmigaDOS track with only 1600
bytes. It is nicer to pad this track out so it is also 1800 bytes. That
way we can work out all offsets just by dividing by 1800 rather than
having to take into account track 0. There are other techniques you could
do this but this is the easiest for a WHDLoad install:

TL_1            TLENTRY 000,000, 1800,SYNC_STD,DMFM_STD
TLENTRY 001,153, 1800, a245,_RipTrack
TLEND
EVEN

Run the imager again. It completes successfully and you have a file called
Disk.1 which is 946176 bytes in length. Have a quick look at the file and
notice that at f700 is a whole bunch of text from the game. This is a
quick way to tell you have ripped it correctly!

I’ve added a couple of comments to the top and the completed RawDIC imager
source is now:

                ; Strider Imager by Codetapper/Action!
;
; Tracks 000-000: Dos
; Tracks 001-153: MFM (Sync a245, 1800 bytes)

incdir include:
include RawDIC.i

OUTPUT “Strider.islave”

;=====================================================================

SLAVE_HEADER
dc.b 1 ;Slave version
dc.b 0 ;Slave flags
dc.l DSK_1 ;Pointer to the first disk structure
dc.l Text ;Pointer to the text displayed in the imager window

dc.b ” VER:”
Text dc.b “Strider imager V1.0”,10
dc.b “by Codetapper/Action (20.06.2002)”
dc.b 0
cnop 0,4

;=====================================================================

DSK_1 dc.l 0 ;Pointer to next disk structure
dc.w 1 ;Disk structure version
dc.w DFLG_NORESTRICTIONS ;Disk flags
dc.l TL_1 ;List of tracks which contain data
dc.l 0 ;UNUSED, ALWAYS SET TO 0!
dc.l FL_DISKIMAGE ;List of files to be saved
dc.l 0 ;Table of certain tracks with CRC values
dc.l 0 ;Alternative disk structure, if CRC failed
dc.l 0 ;Called before a disk is read
dc.l 0 ;Called after a disk has been read

TL_1 TLENTRY 000,000, 1800,SYNC_STD,DMFM_STD
TLENTRY 001,153, 1800, a245,_RipTrack
TLEND
EVEN

;=====================================================================

_RipTrack lea (2,a0),a3 ;lea ( 7C002).l,a3
movea.l a1,a4 ;movea.l a5,a4
move.l # 55555555,d3 ;MFM Mask
move.w (a3)+,d2 ;Merge 2 MFM words of
move.w (a3)+,d1 ;data into one word
and.w d3,d2
and.w d3,d1
add.w d2,d2
or.w d2,d1
cmp.w d0,d1 ;Check track number is
bne _NoSector ;what we expected
moveq #0,d4
move.w # 5FF,d6 ;Decode 600-1 longs
_DecodeLoop move.l ( 1804,a3),d1 ;Read even longword
and.l d3,d1 ;Mask out clock bits
move.l (a3)+,d2 ;Read odd longword
and.l d3,d2 ;Mask out clock bits
add.l d2,d2 ;Rotate odd bits left
or.l d2,d1 ;Merge odd and even bits
add.l d1,d4 ;Add to checksum
move.l d1,(a4)+ ;Store decoded longword
dbra d6,_DecodeLoop ;Decode rest of track
move.l ( 1804,a3),d1 ;Read even longword
and.l d3,d1 ;Mask out clock bits
move.l (a3)+,d2 ;Read odd longword
and.l d3,d2 ;Mask out clock bits
add.l d2,d2 ;Rotate odd bits left
or.l d2,d1 ;Merge odd and even bits
not.l d4
cmp.l d4,d1 ;Compare checksum from
beq.b _OK ;data with expected value
bra _Checksum

;=====================================================================

_OK moveq #IERR_OK,d0
rts

_Checksum moveq #IERR_CHECKSUM,d0
rts

_NoSector moveq #IERR_NOSECTOR,d0
rts

0

Publication author

offline 20 years

Codetapper

0
Comments: 29Publics: 4Registration: 07-08-2004

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments
morpa
7 years ago

Just a note that the link for the download doesn’t seem to work anymore.

0
plagueis
9 years ago

Indeed. I just learned a lot more about loaders.

0
WayneK
19 years ago

Good stuff, explains the workings of the loader in detail – lessons which can be applied to many loaders in future 🙂

0
Authorization
*
*

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

Registration
*
*
*

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

Password generation

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

0
Would love your thoughts, please comment.x
()
x