Strider

Hits: 3046

MFM Tutorial part 1 (understanding MFM)

More
Tags
Category: TutorialsAmigaCrackingMFM
Author: CodeTapper
Submitted by: Codetapper
Date: 2004-08-23 01:21
No tags

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 680x0 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 (codetapper@hotmail.com)

        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

FileDownload: Strider
Filesize: 0KB, downloaded 149 times
Powered by the best online Amiga mod player: FLOD


Some more you may like:

None


Comments

Leave a Comment!

Name:
: Use this calculator
Your comment will be available for editing for 10 minutes
2004-08-25 21:53

1. WayneK writes

Good stuff, explains the workings of the loader in detail - lessons which can be applied to many loaders in future :)
reply
2014-09-25 15:42

2. plagueis writes

Indeed. I just learned a lot more about loaders.
reply
2016-12-13 15:38
Avatar

3. morpa writes

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