----------------Eggs-It---------------- A 4am crack 2019-04-14 -------------------. updated 2020-06-24 |___________________ Name: Eggs-It Genre: action Year: 1982 Credits: Nasir Gebelli Publisher: Gebelli Software Protection: Roland Gustafsson Platform: Apple ][ (48K) Media: 5.25-inch disk Sides: 1 OS: custom ~ Chapter 0 In Which Various Automated Tools Fail In Interesting Ways COPYA immediate disk read error Locksmith Fast Disk Backup unable to read any track EDD 4 bit copy (no sync, no count) read errors on tracks $0C+ copy loads title page then hangs when it tries to load the rest Copy ][+ nibble editor appears to be 4-4 encoded data no standard sectors or structure Why didn't COPYA work? not a 16-sector disk Why didn't Locksmith FDB work? not a 16-sector disk Why didn't my EDD copy work? I don't know. Half tracks, maybe? The disk sounds like it's seeking to successive tracks very quickly, so maybe it's doing a spiral-y kind of thing. Hard to tell just on sound alone though. This is decidedly not a single-load game. There is a classic crack that is a single binary, but it cuts out the initial animated title page. Combined with the early indications of a custom bootloader and 4-4 encoded sectors, this is not going to be a straightforward crack by any definition of "straight" or "forward." Let's start at the beginning. ~ Chapter 1 In Which We Brag About Our Humble Beginnings I have two floppy drives, one in slot 6 and the other in slot 5. My "work disk" (in slot 5) runs Diversi-DOS 64K, which is compatible with Apple DOS 3.3 but relocates most of DOS to the language card on boot. This frees up most of main memory (only using a single page at $BF00..$BFFF), which is useful for loading large files or examining code that lives in areas typically reserved for DOS. [S6,D1=original disk] [S5,D1=my work disk] The floppy drive firmware code at $C600 is responsible for aligning the drive head and reading sector 0 of track 0 into main memory at $0800. Because the drive can be connected to any slot, the firmware code can't assume it's loaded at $C600. If the floppy drive card were removed from slot 6 and reinstalled in slot 5, the firmware code would load at $C500 instead. To accommodate this, the firmware does some fancy stack manipulation to detect where it is in memory (which is a neat trick, since the 6502 program counter is not generally accessible). However, due to space constraints, the detection code only cares about the lower 4 bits of the high byte of its own address. $C600 (or $C500, or anywhere in $Cx00) is read-only memory. I can't change it, which means I can't stop it from transferring control to the boot sector of the disk once it's in memory. BUT! The disk firmware code works unmodified at any address. Any address that ends with $x600 will boot slot 6, including $B600, $A600, $9600, &c. ; copy drive firmware to $9600 *9600 branch 21F0- F0 09 BEQ $21FB ; failure -> reboot 21F2- 6C F2 03 JMP ($03F2) ; (subroutine that reads 1 nibble) 21F5- BD 8C C0 LDA $C08C,X 21F8- 10 FB BPL $21F5 21FA- 60 RTS ; execution continues here from the ; BEQ at $01F0 -- set the stack pointer ; again 21FB- AE 65 05 LDX $0565 21FE- 9A TXS ; by the time we get here, this has ; been replaced by #$60 (set at $0133), ; which is an RTS 21FF- 00 BRK Amusing side note: the JSR $0190 never returns to the caller. If the read fails, it just reboots (at $01F2); if it succeeds, we branch to $01FB (from $01F0) and exit via RTS (at $01FF, set at $0133) after setting stack pointer based on code we just read. Under no circumstances do we execute this code at $018B: 218B- A9 04 LDA #$04 218D- 48 PHA 218E- 48 PHA 218F- 60 RTS Roland just put that in there because he had 5 extra bytes and wanted to f--- with people trying to break his code. Hi Roland. There is enough room at $01FB to change the LDX to a JMP and capture the code that's being stored on the text page. *9600 pairs from disk and sets them, just like the one we saw earlier at $0500. It even has the same exit condition: reading an #$EA value. Hi ho, hi ho, it's back to trace we go. *9600 $E8 (used later to turn ; off the drive motor) 0BDC- 29 F8 AND #$F8 0BDE- 8D F9 05 STA $05F9 ; munge $E8 -> $E9 (used later to turn ; on the drive motor) 0BE1- 09 01 ORA #$01 0BE3- 8D 79 03 STA $0379 0BE6- 8D 17 02 STA $0217 ; munge $E9 -> $E0 (used later to move ; the drive head via the stepper motor) 0BE9- 49 09 EOR #$09 0BEB- 8D 57 03 STA $0357 ; munge $E0 -> $60 (boot slot x16, used ; by the write routine) 0BEE- 29 70 AND #$70 0BF0- 8D 47 02 STA $0247 0BF3- 8D 79 02 STA $0279 0BF6- 8D 8F 02 STA $028F 0BF9- 8D BD 02 STA $02BD 0BFC- 60 RTS ~ Chapter 16 6 + 2 Before I dive into the next chunk of code, I get to pause and explain a little bit of theory. As you probably know if you're the sort of person who's read this far already, Apple II floppy disks do not contain the actual data that ends up being loaded into memory. Due to hardware limitations of the original Disk II drive, data on disk is stored in an intermediate format called "nibbles." Bytes in memory are encoded into nibbles before writing to disk, and nibbles that you read from the disk must be decoded back into bytes. The round trip is lossless but requires some bit wrangling. Decoding nibbles-on-disk into bytes-in- memory is a multi-step process. In "6-and-2 encoding" (used by DOS 3.3, ProDOS, and all ".dsk" image files), there are 64 possible values that you may find in the data field (in the range $96..$FF, but not all of those, because some of them have bit patterns that trip up the drive firmware). We'll call these "raw nibbles." Step 1: read $156 raw nibbles from the data field. These values will range from $96 to $FF, but as mentioned earlier, not all values in that range will appear on disk. Now we have $156 raw nibbles. Step 2: decode each of the raw nibbles into a 6-bit byte between 0 and 63 (%00000000 and %00111111 in binary). $96 is the lowest valid raw nibble, so it gets decoded to 0. $97 is the next valid raw nibble, so it's decoded to 1. $98 and $99 are invalid, so we skip them, and $9A gets decoded to 2. And so on, up to $FF (the highest valid raw nibble), which gets decoded to 63. Now we have $156 6-bit bytes. Step 3: split up each of the first $56 6-bit bytes into pairs of bits. In other words, each 6-bit byte becomes three 2-bit bytes. These 2-bit bytes are merged with the next $100 6-bit bytes to create $100 8-bit bytes. Hence the name, "6-and-2" encoding. The exact process of how the bits are split and merged is... complicated. The first $56 6-bit bytes get split up into 2-bit bytes, but those two bits get swapped (so %01 becomes %10 and vice- versa). The other $100 6-bit bytes each get multiplied by 4 (a.k.a. bit-shifted two places left). This leaves a hole in the lower two bits, which is filled by one of the 2-bit bytes from the first group. A diagram might help. "a" through "x" each represent one bit. ------------- 1 decoded 3 decoded nibble in + nibbles in = 3 bytes first $56 other $100 00abcdef 00ghijkl 00mnopqr | 00stuvwx | split | & shifted swapped left x2 | | V V 000000fe + ghijkl00 = ghijklfe 000000dc + mnopqr00 = mnopqrdc 000000ba + stuvwx00 = stuvwxba ------------- Tada! Four 6-bit bytes 00abcdef 00ghijkl 00mnopqr 00stuvwx become three 8-bit bytes ghijklfe mnopqrdc stuvwxba When DOS 3.3 reads a sector, it reads the first $56 raw nibbles, decoded them into 6-bit bytes, and stashes them in a temporary buffer (at $BC00). Then it reads the other $100 raw nibbles, decodes them into 6-bit bytes, and puts them in another temporary buffer (at $BB00). Only then does DOS 3.3 start combining the bits from each group to create the full 8-bit bytes that will end up in the target page in memory. This is why DOS 3.3 "misses" sectors when it's reading, because it's busy twiddling bits while the disk is still spinning. EggBoot also uses "6-and-2" encoding. The first $56 nibbles in the data field are still split into pairs of bits that will be merged with nibbles that won't come until later. But instead of waiting for all $156 raw nibbles to be read from disk, it "interleaves" the nibble reads with the bit twiddling required to merge the first $56 6-bit bytes and the $100 that follow. By the time EggBoot gets to the data field checksum, it has already stored all $100 8-bit bytes in their final resting place in memory. This means that we can read all 16 sectors on a track in one revolution of the disk. That's what makes it crazy fast. To make it possible to twiddle the bits and not miss nibbles as the disk spins(*), we do some of the work in advance. We multiply each of the 64 possible decoded values by 4 and store those values. (Since this is done by bit shifting and we're doing it before we start reading the disk, this is called the "pre-shift" table.) We also store all possible 2-bit values in a repeating pattern that will make it easy to look them up later. Then, as we're reading from disk (and timing is tight), we can simulate bit math with a series of table lookups. There is just enough time to convert each raw nibble into its final 8-bit byte before reading the next nibble. (*) The disk spins independently of the CPU, and we only have a limited time to read a nibble and do what we're going to do with it before WHOOPS HERE COMES ANOTHER ONE. So time is of the essence. Also, "As The Disk Spins" would make a great name for a retrocomputing-themed soap opera. The first table, at $0600..$06FF, is three columns wide and 64 rows deep. Astute readers will notice that 3 x 64 is not 256. Only three of the columns are used; the fourth (unused) column exists because multiplying by 3 is hard but multiplying by 4 is easy (in base 2 anyway). The three columns correspond to the three pairs of 2-bit values in those first $56 6-bit bytes. Since the values are only 2 bits wide, each column holds one of four different values (%00, %01, %10, or %11). The second table, at $0796..$07FF, is the "pre-shift" table. This contains all the possible 6-bit bytes, in order, each multiplied by 4 (a.k.a. shifted to the left two places, so the 6 bits that started in columns 0-5 are now in columns 2-7, and columns 0 and 1 are zeroes). Like this: 00ghijkl --> ghijkl00 Astute readers will notice that there are only 64 possible 6-bit bytes, but this second table is larger than 64 bytes. To make lookups easier, the table has empty slots for each of the invalid raw nibbles. In other words, we don't do any math to decode raw nibbles into 6-bit bytes; we just look them up in this table (offset by $96, since that's the lowest valid raw nibble) and get the required bit shifting for free. addr | raw | decoded 6-bit | pre-shift ------+-----+---------------+---------- $0796 | $96 | 0 = %00000000 | %00000000 $0797 | $97 | 1 = %00000001 | %00000100 $0798 | $98 [invalid raw nibble] $0799 | $99 [invalid raw nibble] $079A | $9A | 2 = %00000010 | %00001000 $079B | $9B | 3 = %00000011 | %00001100 $079C | $9C [invalid raw nibble] $079D | $9D | 4 = %00000100 | %00010000 . . . $07FE | $FE | 62 = %00111110 | %11111000 $07FF | $FF | 63 = %00111111 | %11111100 Each value in this "pre-shift" table also serves as an index into the first table (with all the 2-bit bytes). This wasn't an accident; I mean, that sort of magic doesn't just happen. But the table of 2-bit bytes is arranged in such a way that we can take one of the raw nibbles to be decoded and split apart (from the first $56 raw nibbles in the data field), use each raw nibble as an index into the pre-shift table, then use that pre-shifted value as an index into the first table to get the 2-bit value at exactly the right time. ~ Chapter 17 Back to EggBoot Continuing from $0818... This is the loop that creates the pre-shift table at $0796. As a special bonus, it also creates the inverse table that is used during disk write operations (converting in the other direction). 0818- A2 3F LDX #$3F 081A- 86 FF STX $FF 081C- E8 INX 081D- A0 7F LDY #$7F 081F- 84 FE STY $FE 0821- 98 TYA 0822- 0A ASL 0823- 24 FE BIT $FE 0825- F0 18 BEQ $083F 0827- 05 FE ORA $FE 0829- 49 FF EOR #$FF 082B- 29 7E AND #$7E 082D- B0 10 BCS $083F 082F- 4A LSR 0830- D0 FB BNE $082D 0832- CA DEX 0833- 8A TXA 0834- 0A ASL 0835- 0A ASL 0836- 99 80 07 STA $0780,Y 0839- 98 TYA 083A- 09 80 ORA #$80 083C- 9D 8A 03 STA $038A,X 083F- 88 DEY 0840- D0 DD BNE $081F And this is the result (".." means the address is uninitialized and unused): 0790- 00 04 0798- .. .. 08 0C .. 10 14 18 07A0- .. .. .. .. .. .. 1C 20 07A8- .. .. .. 24 28 2C 30 34 07B0- .. .. 38 3C 40 44 48 4C 07B8- .. 50 54 58 5C 60 64 68 07C0- .. .. .. .. .. .. .. .. 07C8- .. .. .. 6C .. 70 74 78 07D0- .. .. .. 7C .. .. 80 84 07D8- .. 88 8C 90 94 98 9C A0 07E0- .. .. .. .. .. A4 A8 AC 07E8- .. B0 B4 B8 BC C0 C4 C8 07F0- .. .. CC D0 D4 D8 DC E0 07F8- .. E4 E8 EC F0 F4 F8 FC Next up: a loop to create the table of 2-bit values at $0600, magically arranged to enable easy lookups later. 0842- 84 FD STY $FD 0844- 46 FF LSR $FF 0846- 46 FF LSR $FF 0848- BD D8 05 LDA $05D8,X 084B- 99 00 06 STA $0600,Y 084E- E6 FD INC $FD 0850- A5 FD LDA $FD 0852- 25 FF AND $FF 0854- D0 05 BNE $085B 0856- E8 INX 0857- 8A TXA 0858- 29 03 AND #$03 085A- AA TAX 085B- C8 INY 085C- C8 INY 085D- C8 INY 085E- C8 INY 085F- C0 03 CPY #$03 0861- B0 E5 BCS $0848 0863- C8 INY 0864- C0 03 CPY #$03 0866- 90 DC BCC $0844 And this is the result: 0600- 00 00 00 .. 00 00 02 .. 0608- 00 00 01 .. 00 00 03 .. 0610- 00 02 00 .. 00 02 02 .. 0618- 00 02 01 .. 00 02 03 .. 0620- 00 01 00 .. 00 01 02 .. 0628- 00 01 01 .. 00 01 03 .. 0630- 00 03 00 .. 00 03 02 .. 0638- 00 03 01 .. 00 03 03 .. 0640- 02 00 00 .. 02 00 02 .. 0648- 02 00 01 .. 02 00 03 .. 0650- 02 02 00 .. 02 02 02 .. 0658- 02 02 01 .. 02 02 03 .. 0660- 02 01 00 .. 02 01 02 .. 0668- 02 01 01 .. 02 01 03 .. 0670- 02 03 00 .. 02 03 02 .. 0678- 02 03 01 .. 02 03 03 .. 0680- 01 00 00 .. 01 00 02 .. 0688- 01 00 01 .. 01 00 03 .. 0690- 01 02 00 .. 01 02 02 .. 0698- 01 02 01 .. 01 02 03 .. 06A0- 01 01 00 .. 01 01 02 .. 06A8- 01 01 01 .. 01 01 03 .. 06B0- 01 03 00 .. 01 03 02 .. 06B8- 01 03 01 .. 01 03 03 .. 06C0- 03 00 00 .. 03 00 02 .. 06C8- 03 00 01 .. 03 00 03 .. 06D0- 03 02 00 .. 03 02 02 .. 06D8- 03 02 01 .. 03 02 03 .. 06E0- 03 01 00 .. 03 01 02 .. 06E8- 03 01 01 .. 03 01 03 .. 06F0- 03 03 00 .. 03 03 02 .. 06F8- 03 03 01 .. 03 03 03 .. And with that, EggBoot is fully armed and operational. ; Set up an initial read of $40 sectors ; from track $01 into $2000..$5FFF. ; Also set the stack pointer lower, ; because the read routine uses the top ; $56 bytes of the stack page as a ; temporary buffer. (Hey, I told you ; memory was tight.) 0868- A9 01 LDA #$01 086A- A2 40 LDX #$40 086C- 9A TXS 086D- A0 20 LDY #$20 ; Read the title page and animation ; routine from tracks $01-$05 086F- 20 00 05 JSR $0500 ; Set up the "return" address at $0141/ ; $0142 to regain control after the ; title page code is done. 0872- A9 FF LDA #$FF 0874- 8D 41 01 STA $0141 0877- A9 06 LDA #$06 0879- 8D 42 01 STA $0142 ; jump to the title page entry point 087C- 4C 00 40 JMP $4000 Execution will continue at $0700 (which is $06FF+1) when the title page routine returns. But first, I get to finish showing you how the disk read routine works. ~ Chapter 18 Read & Go Seek In a standard DOS 3.3 RWTS, the softswitch to read the data latch is "LDA $C08C,X", where X is the boot slot times 16 (to allow disks to boot from any slot). EggBoot also supports booting and reading from any slot, but instead of using an index, most fetch instructions are set up in advance based on the boot slot. Not only does this free up the X register, it lets us juggle all the registers and put the raw nibble value in whichever one is convenient at the time. (We take full advantage of this freedom.) I've marked each pre-set softswitch with "o_O". There are several other instances of addresses and constants that get modified while EggBoot is executing. I've left these with a bogus value $D1 and marked them with "o_O". EggBoot's source code should be available from the same place you found this write-up. If you're looking to modify this code for your own purposes, I suggest you "use the source, Luke." ; A = the track number to seek to. We ; multiply it by 2 to convert it to a ; phase, then store it inside the seek ; routine which we will call shortly. 0500- 0A ASL 0501- 8D 13 03 STA $0313 ; X = the number of sectors to read 0504- 8E C8 05 STX $05C8 ; Y = the starting address in memory 0507- 8C 21 05 STY $0521 ; turn on the drive motor and wait for ; it to spin up (not shown) 050A- 20 78 03 JSR $0378 ; are we reading this entire track? 050D- A9 10 LDA #$10 050F- CD C8 05 CMP $05C8 ; yes -> branch 0512- B0 01 BCS $0515 ; no -> store the number of sectors we ; want to read 0514- AA TAX 0515- 8E D8 05 STX $05D8 ; seek to the track we want to read ; (not shown) 0518- 20 07 03 JSR $0307 ; Initialize an array of which sectors ; we've read from the current track. ; The array is in physical sector ; order, thus the RWTS assumes data is ; stored in physical sector order on ; each track. (This saves 18 bytes: 16 ; for the table and 2 for the lookup ; command!) Values are the actual pages ; in memory where that sector should ; go, and they get zeroed once the ; sector is read (so we don't waste ; time decoding the same sector twice). 051B- AE D8 05 LDX $05D8 051E- A0 00 LDY #$00 0520- A9 D1 LDA #$D1 o_O 0522- 99 D9 05 STA $05D9,Y 0525- EE 21 05 INC $0521 0528- C8 INY 0529- CA DEX 052A- D0 F4 BNE $0520 052C- 20 E6 02 JSR $02E6 ; This routine reads nibbles from disk ; until it finds the sequence "D5 AA", ; then it reads one more nibble and ; returns it in the accumulator. We ; reuse this routine to find both the ; address and data field prologues. 02E6- 20 F5 02 JSR $02F5 02E9- C9 D5 CMP #$D5 02EB- D0 F9 BNE $02E6 02ED- 20 F5 02 JSR $02F5 02F0- C9 AA CMP #$AA 02F2- D0 F5 BNE $02E9 02F4- A8 TAY 02F5- AD EC C0 LDA $C0EC o_O 02F8- 10 FB BPL $02F5 02FA- 60 RTS Continuing from $052F... ; If that third nibble is not #$AD, we ; assume it's the end of the address ; prologue. (#$96 would be the third ; nibble of a standard address ; prologue, but we don't actually ; check.) We fall through and start ; decoding the 4-4 encoded values in ; the address field. 052F- 49 AD EOR #$AD 0531- F0 1A BEQ $054D 0533- 20 D3 02 JSR $02D3 ; This routine parses the 4-4-encoded ; values in the address field. The ; first time through this loop, we'll ; read the disk volume number. The ; second time, we'll read the track ; number. The third time, we'll read ; the physical sector number. We don't ; actually care about the disk volume ; or the track number, and once we get ; the sector number, we don't verify ; the address field checksum. 02D3- A0 03 LDY #$03 02D5- 20 F5 02 JSR $02F5 02D8- 2A ROL 02D9- 8D B9 05 STA $05B9 02DC- 20 F5 02 JSR $02F5 02DF- 2D B9 05 AND $05B9 02E2- 88 DEY 02E3- D0 F0 BNE $02D5 ; On exit, the accumulator contains the ; physical sector number. 02E5- 60 RTS Continuing from $0536... ; use physical sector number as an ; index into the sector address array 0536- A8 TAY ; get the target page (where we want to ; store this sector in memory) 0537- BE D9 05 LDX $05D9,Y ; if the target page is #$00, it means ; we've already read this sector, so ; loop back to find the next address ; prologue 053A- F0 F0 BEQ $052C ; store the physical sector number ; later in this routine 053C- 8D B9 05 STA $05B9 ; store the target page in several ; places throughout this routine 053F- 8E A6 05 STX $05A6 0542- CA DEX 0543- 8E 76 05 STX $0576 0546- 8E 8E 05 STX $058E 0549- A0 00 LDY #$00 ; this is an unconditional branch 054B- B0 DF BCS $052C ; execution continues here (from $0531) ; after matching the data prologue 054D- E0 00 CPX #$00 ; If X is still #$00, it means we found ; a data prologue before we found an ; address prologue. In that case, we ; skip this sector, because we don't ; know which sector it is and we ; wouldn't know where to put it. Sad! 054F- F0 DB BEQ $052C Nibble loop #1 reads nibbles $00..$55, looks up the corresponding offset in the preshift table at $0796, and stores that offset in a temporary buffer on the stack page. ; initialize rolling checksum to #$00, ; or update it with the results from ; the calculations below 0551- 8D 60 05 STA $0560 ; read one nibble from disk 0554- AE EC C0 LDX $C0EC o_O 0557- 10 FB BPL $0554 ; The nibble value is in the X register ; now. The lowest possible nibble value ; is $96 and the highest is $FF. To ; look up the offset in the table at ; $0796, we index off $0700 + X. Math! 0559- BD 00 07 LDA $0700,X ; Now the accumulator has the offset ; into the table of individual 2-bit ; combinations ($0600..$06FF). Store ; that offset in a temporary buffer ; on the stack page. 055C- 99 00 01 STA $0100,Y ; The EOR value is set at $0551 ; each time through loop #1. 055F- 49 D1 EOR #$D1 o_O ; The Y register started at #$AA ; (set by the "TAY" instruction ; at $0536), so this loop reads ; a total of #$56 nibbles. 0561- C8 INY 0562- D0 ED BNE $0551 Here endeth nibble loop #1. Nibble loop #2 reads nibbles $56..$AB, combines them with bits 0-1 of the appropriate nibble from the first $56, and stores them in bytes $00..$55 of the target page in memory. 0564- A0 AA LDY #$AA 0566- AE EC C0 LDX $C0EC o_O 0569- 10 FB BPL $0566 056B- 5D 00 07 EOR $0700,X 056E- BE 00 01 LDX $0100,Y 0571- 5D 02 06 EOR $0602,X ; This address was set at $0543 ; based on the target page (minus 1 ; so we can add Y from #$AA..#$FF). 0574- 99 56 D1 STA $D156,Y o_O 0577- C8 INY 0578- D0 EC BNE $0566 Here endeth nibble loop #2. Nibble loop #3 reads nibbles $AC..$101, combines them with bits 2-3 of the appropriate nibble from the first $56, and stores them in bytes $56..$AB of the target page in memory. 057A- 29 FC AND #$FC 057C- A0 AA LDY #$AA 057E- AE EC C0 LDX $C0EC o_O 0581- 10 FB BPL $057E 0583- 5D 00 07 EOR $0700,X 0586- BE 00 01 LDX $0100,Y 0589- 5D 01 06 EOR $0601,X ; This address was set at $0546 ; based on the target page (minus 1 ; so we can add Y from #$AA..#$FF). 058C- 99 AC D1 STA $D1AC,Y o_O 058F- C8 INY 0590- D0 EC BNE $057E Here endeth nibble loop #3. Loop #4 reads nibbles $102..$155, combines them with bits 4-5 of the appropriate nibble from the first $56, and stores them in bytes $AC..$FF of the target page in memory. 0592- 29 FC AND #$FC 0594- A2 AC LDX #$AC 0596- AC EC C0 LDY $C0EC o_O 0599- 10 FB BPL $0596 059B- 59 00 07 EOR $0700,Y 059E- BC FE 00 LDY $00FE,X 05A1- 59 00 06 EOR $0600,Y ; This address was set at $053F ; based on the target page. 05A4- 9D 00 D1 STA $D100,X 05A7- E8 INX 05A8- D0 EC BNE $0596 ; Finally, get the last nibble and ; convert it to a byte. This should ; equal all the previous bytes XOR'd ; together. (This is the standard ; checksum algorithm shared by all ; 16-sector disks.) 05AA- 29 FC AND #$FC 05AC- AC EC C0 LDY $C0EC o_O 05AF- 10 FB BPL $05AC 05B1- 59 00 07 EOR $0700,Y ; if data checksum failed, start over 05B4- C9 01 CMP #$01 05B6- B0 93 BCS $054B ; This was set to the physical ; sector number (at $053C), so ; this is a index into the 16- ; byte array at $05D9. 05B8- A0 D1 LDY #$D1 o_O 05BA- 8A TXA ; store #$00 at this location in ; the sector array to indicate ; that we've read this sector 05BB- 99 D9 05 STA $05D9,Y ; decrement sector count 05BE- CE C8 05 DEC $05C8 05C1- CE D8 05 DEC $05D8 05C4- 38 SEC ; If the sectors-left-in-this-track ; count (in $05D8) isn't zero yet, ; loop back to read more sectors. 05C5- D0 EF BNE $05B6 ; If the total sector count (in ; $05C8, set at $0504 and decremented ; at $05BE) is zero, we're done ; so we can skip the rest of ; the track. (This lets us have ; sector counts that are not ; multiples of 16, i.e. reading ; just a few sectors from the ; last track of a multi-track ; read.) 05C7- A2 D1 LDX #$D1 o_O 05C9- F0 09 BEQ $05D4 ; increment track (twice because ; it's store as the phase, which ; is half a track) 05CB- EE 13 03 INC $0313 05CE- EE 13 03 INC $0313 ; jump back to seek and read ; from the next track 05D1- 4C 0D 05 JMP $050D ; Execution continues here (from ; $05C9). We're all done, so ; turn off drive motor and exit. 05D4- AD E8 C0 LDA $C0E8 o_O 05D7- 60 RTS And that's all she wrote^H^H^H^Hread. ~ Chapter 19 Omelette You Finish, But Roland Had One Of The Best Bootloaders Of All Time! When we left our custom bootloader, we had jumped to $4000 to let the title page do its thing. The title page sets the stack pointer to #$40 and returns, so we had set $0141/$0142 to point to the final phase of EggBoot at $0700. This code was originally in our boot sector and loaded at $087F, but it was soon copied to $0700 by the subroutine at $0B8A. The stack pointer is already low enough that the read routine's temporary buffer at $01AA won't clobber actual return addresses, so we've got that going for us, which is nice. ; seek to track $05, read $80 sectors ; into $4000..$BFFF 0700- A9 05 LDA #$05 0702- A2 80 LDX #$80 0704- A0 40 LDY #$40 0706- 20 00 05 JSR $0500 ; seek to track $0D, read $18 sectors ; into $0800..$1FFF 0709- A9 0D LDA #$0D 070B- A2 18 LDX #$18 070D- A0 08 LDY #$08 070F- 20 00 05 JSR $0500 That fills main memory, $0800..$BFFF, minus hi-res page 1 which is still showing the title page. ; seek to track $22, read 1 sector ; into $0400 0712- A9 22 LDA #$22 0714- A2 01 LDX #$01 0716- A0 04 LDY #$04 0718- 20 00 05 JSR $0500 ; set game-specific zero page values ; (these were originally read from disk ; and set by the self-modifying loop at ; $06BF) 071B- A9 80 LDA #$80 071D- 85 20 STA $20 071F- A9 9F LDA #$9F 0721- 85 21 STA $21 0723- A9 00 LDA #$00 0725- 85 3A STA $3A 0727- 85 4E STA $4E 0729- A9 1F LDA #$1F 072B- 85 4F STA $4F 072D- A9 0B LDA #$0B 072F- 85 36 STA $36 0731- A9 1F LDA #$1F 0733- 85 37 STA $37 0735- A9 D0 LDA #$D0 0737- 85 38 STA $38 0739- A9 51 LDA #$51 073B- 85 39 STA $39 073D- A9 59 LDA #$59 073F- 85 3B STA $3B ; jump to game entry point 0741- 6C 20 00 JMP ($0020) The game uses the text page to display high scores (and enter them, if you're so talented), so none of this code will survive. But that's okay, because we're done reading the disk. Once the game is in memory, the only disk-related activity is writing the high scores to track $22. That code (not shown here) is identical to DOS 3.3's write routine except starting at $0207. That's the entry point the game expects, so no changes to the game code are required. Quod erat liberandum. ~ Changelog 2020-06-24 - typo in the 6-and-2 encoding diagram [thanks Andrew R.] 2019-04-14 - initial release --------------------------------------- A 4am crack No. 2000 ------------------EOF------------------