Electronic Arts C64 Fat Track loader This one is a monster… even more so when you take a look at the possibilities for games to check if they were loaded by the proper, unmodified loader. It's pretty much all here: self-modifying code, P-code, encryption, checksumming, memory fills, everything. This particular loader was taken from Skyfox, so there could be differences between the games. It all starts out with the autoloader. Unlike most of them, it actually checks to see if it loaded the main loader successfully, printing an error if it fails, and retrying until it gets a good load. Also, the vectors altered by the autoloader are left in place. Thus, if the main loader exits for any reason (such as when it overwrites itself after a protection failure), when BASIC is re-entered, the autoloader will kick everything off again. Also, several key bytes of the autoloader are checked by the P-code. It checks to ensure that the autostart vectors are not tampered with. It checks that the garbage character in the main loader filename is not tampered with. Games might also use this as a decryption key. (Skyfox is loaded with overlapping decryptions). -- Autoloader -- .C:02b8 A9 08 LDA #$08 .C:02ba AA TAX .C:02bb A0 01 LDY #$01 .C:02bd 20 BA FF JSR $FFBA ; Set the LFN to 8, 8, 1 .C:02c0 A9 04 LDA #$04 .C:02c2 A2 ED LDX #$ED .C:02c4 A0 02 LDY #$02 .C:02c6 20 BD FF JSR $FFBD ; Set the filename to 4 characters at $02ED (EA"0x9D) .C:02c9 A9 00 LDA #$00 .C:02cb 85 9D STA $9D ; Disable system messages .C:02cd 20 D5 FF JSR $FFD5 ; And load the main loader .C:02d0 B0 03 BCS $02D5 ; If the carry is set (error occurred), jump to the handler .C:02d2 4C 00 C0 JMP $C000 ; Start the main loader .C:02d5 20 CC FF JSR $FFCC ; Clear channels .C:02d8 A2 06 LDX #$06 ; Set up for the error message .C:02da BD E4 02 LDA $02E4,X .C:02dd 20 D2 FF JSR $FFD2 ; Print ERROR to the user .C:02e0 CA DEX .C:02e1 D0 F7 BNE $02DA ; Keep printing if there are more letters .C:02e3 F0 D3 BEQ $02B8 ; And jump back to the beginning to try again >C:02e5 0d 52 4f 52 52 45 53 48 45 41 22 9d dd dd dd dd .RORRESHEA"..... >C:02f5 dd dd dd dd dd dd dd dd dd dd dd b8 02 b8 02 b8 ................ >C:0305 02 b8 02 b8 02 b8 02 The main loader is, of course, far more interesting. The theory is simple. The loader has the start page, and the end page of the protected program stored in a header at the beginning. The protected program is stored as a user file, starting at track 1, sector 0, and proceeds on up through each available track. Sector 20 of each track seems to be skipped to make it easier to roll the sectors. Since tracks 1-17 could hold just about any reasonable program, there's no reason to worry about the shorter tracks. While these sectors are being loaded, the ECA logo is shown, with the colors slowly scrolling down as each byte is read. Once the load is finished, the protected program is in memory, but encrypted. The next step is to check the fat track. So, the head is moved out to track 34, and every sector is checked. There is no data read, just the error channel. Then, a memory-write command is sent to the drive to step the head in by one half-track. The read is repeated again. If it's still successful, the head is stepped another half-track, and the read repeated. Finally, back to track 34.5, and then back to track 34. After each read, a memory-read is used to check the motor control register to ensure that the head did not step as part of any error handling routine. If this passes, the decryption routine is slightly altered, and then finally called. The decryption routine uses multiple checksums of the loader code to come up with the decryption key. Finally, memory from $0800 to $7EFF is decrypted. Then a checksum is run over the resulting code, and compared to a value in the header. If it is good, then the game's start address is decrypted and the game is started up. The more sensitive the game is to the exact state of the machine at this point, the harder it is going to be to save off the game. I/O registers are subtly tampered with. Key values are written under the kernel. Since there is not much memory available in the machine (the decryption will corrupt $0800-$7EFF, the game may well load in at the higher end of memory, the loader either uses or fills memory from $C000-$CFFF, and the logo-screen uses the memory under the kernel), it would be a tough nut to crack as well, as there is no place left to store the 'good' copy. Some EA games are easily saved from memory. Skyfox is painfully sensitive to the state of the system. Altering a single bit of memory in certain places causes decryption operations to go wildly awry. Now, where is all of this decryption and fat track code hidden? Inside a P-code interpreter. The loader interleaves 6502 code with P-code, making it difficult to get a dump of either. I ended up writing a disassembler for the P-code, and figuring out by hand which bytes were P-code. It is a pretty linear flow, however. After making copies of the loader to places far and wide, the P-code is first started up at $C02F. The P-code always starts 4 bytes after the startup JSR, giving some space for some garbage instructions to try and slow down reversing. The first block of P-code checks the state of the system, ensuring that key values in the auto-loader are intact, sets up the logo screen, and then jumps to another block of code at $C0C2. That block of code does some more initialization, then initializes the drive, and sends the first U1 command. After it sends the read command, it jumps to ANOTHER block of P-code at $C149 which sends the B-P command to retrieve the data. That code leaves the P-machine entirely and jumps to a block of 6502 code that reads the sector into memory. When that code finishes, it re-invokes the P-machine and starts executing at $C233. That code checks to see if the last sector of the protected file has been loaded. If it has not, it's back off to $C118 to read more sectors. If the load is done, the fat-track check and decryptor at $C309 is called. Since the fat-track check is done entirely in P-code, and P-code is encrypted, it's not possible to find the M-W or M-R commands by looking at the loader. If the fat-track is present, control returns to the $C233 routine at $C244. The files are closed, and the checksum is checked. If it's all good, the value of the ending page, and the checksum are subtracted from the value at C2EF. Finally, the P-code jumps to a JMP instruction at $C2EE to start up the game using the newly decrypted start address. I have not discussed any specific cracks in here (other than to say, there's a lot of checksums) as my interest was in understanding the functionality of the loader itself, however the loader is so inefficient, that it's far better to find a way to save off the game than it is to try and crack the loader. P-machine locations: $22 - $23 - Work pointer $26 - $27 - Program Counter $28 - Register - 8 bits wide $C6C1 - 0xC6C2 - 16-bit Register. Supports constant loads only P-code instructions: PJMP Jump to pseudocode, Absolute mode, 3 byte instruction Opcode $00 Handler $C547 ANDR Logical AND of register, Immediate mode, 2 byte instruction Opcode $01 Handler $C583 PCAL Call P-code, Absolute mode, 3 byte instruction Opcode $02 Handler $C5D9 CALL Call 6502 code (see note), Absolute mode, 3 byte instruction Opcode $03 Handler $C586 LDR Load Register, Immediate mode, 2 byte instruction Opcode $04 Handler $C5AE LDR Load Register, Absolute mode, 3 byte instruction Opcode $05 Handler $C5BC BEQL Branch if equal, Absolute mode, 3 byte instruction Opcode $06 Handler $C5A4 STR Store Register, Absolute mode, 3 byte instruction Opcode $07 Handler $C5FC SUB Subtract from Register, Immediate mode, 2 byte instruction Opcode $08 Handler $C60D JUMP Jump to 6502 code (see note), Absolute mode, 3 byte instruction Opcode $09 Handler $C59E PRET Return to P-code call, Implied mode, 1 byte instruction Opcode $0A Handler $C5F1 LDR Load Register, Indexed mode (R is index), 3 byte instruction Opcode $0B Handler $C5C8 SHL Shift R Left, Implied mode, 1 byte instruction Opcode $0C Handler $C608 INC Increment memory, Absolute mode, 3 byte instruction Opcode $0D Handler $C622 ADD Add to R, Absolute mode, 3 byte instruction Opcode $0E Handler $C625 DECR Decrypt two bytes (see note), Implied mode, 1 byte instruction Opcode $0F Handler $C628 BNEQ Branch if not equal, Absolute mode, 3 byte instruction Opcode $10 Handler $C62B SUB Subtract from R, Absolute mode, 3 byte instruction Opcode $11 Handler $C62E BPLU Branch on plus, Absolute mode, 3 byte instruction Opcode $12 Handler $C631 LD16 Load 16-bit Register, Immediate mode, 3 byte instruction Opcode $13 Handler $C634 P-machine instruction notes: All Immediate mode instructions except for LD16 have the data operand encrypted with $6B. All Absolute mode instructions and LD16 have the data operand encrypted with $292B. All 16-bit values are stored low-byte, high-byte just like the 6502. When making a CALL to 6502 code, Register is placed into the 6502 accumulator, the low-byte of the 16-bit register is placed into the X index register, and the high-byte of the 16-bit register is placed into the Y index register. This is compatible with kernel calling conventions. Upon return, the 6502 accumulator is placed back into the P-machine Register. A simple 6502 RTS instruction will return to the P-machine. When making a JUMP to 6502 code, no register communication is done. The P-machine can be easily restarted at a new location, but an RTS would not give desired results. The DECR instruction uses the following algorithm. EOR two bytes pointed to by $2C-$2D with the Register. Set the Register to the value in $2D EOR with $7F. --- Loader Disassembly --- .C:c000 4C 12 C0 JMP $C012 ; Jump over the game specific header .C:c003 46 ; The first page of the user file .C:c004 C0 ; The last page of the user file + 1. In this case, load up to address $BFFF .C:c005 65 ; The checksum of the decrypted user file .C:c006 00 .C:c007 00 .C:c008 11 C0 .C:c00a 14 C0 .C:c00c C3 C2 .C:c00e CD 38 30 .C:c011 40 ; Loader initialization .C:c012 B0 FE BCS $C012 ; Check if the carry flag is clear from the kernel LOAD call in the autoloader. ; I would guess this would detect a monitor, as it's very tough to write much code without changing a flag or two .C:c014 A2 FF LDX #$FF .C:c016 9A TXS ; Normal stack reset, take control of the machine .C:c017 E8 INX .C:c018 BD 08 C0 LDA $C008,X .C:c01b 9D 00 80 STA $8000,X .C:c01e 9D 00 B0 STA $B000,X .C:c021 9D 00 20 STA $2000,X .C:c024 9D 00 40 STA $4000,X .C:c027 9D E3 C9 STA $C9E3,X .C:c02a CA DEX .C:c02b D0 EB BNE $C018 ; Make copies of the code to multiple locations. Possibly to support loading games at any address, but the loader is not ; particularly relocatable .C:c02d 20 08 C5 JSR $C508 ; Start the P-machine. The P-machine always starts executing 4 bytes after this JSR, which never returns as it's return address ; is used to get the start address of the P-code .C:c030 CA DEX .C:c031 10 E2 BPL $C015 ; Dummy instructions that never get executed. Would lead to a crash by jumping to the $FF above ; == P-Code == ; Loader initialization .C033 04 69 LDR #$02 .C035 03 EE D6 CALL $FFC3 ; Close file 2 .C038 04 64 LDR #$0F .C03A 03 EE D6 CALL $FFC3 ; Close file 15 ; Now check the machine environment to see if anything has been tampered with up to this point .C03D 05 26 2A LDR $030B ; Pull the pointer to the BASIC token evaluation routine, which should have been pointed down to the auto-loader .C040 08 69 SUB #$02 ; Subtract $02 from the pointer (the high-byte of the auto-loader entry point) .C042 10 7D ED BNEQ $C450 ; And if it's not equal to 0, branch to a crash routine .C045 05 DD 2B LDR $02F0 ; Grab the control character buried in our expected filename from the autoloader (make sure our name is garbaged up in the dir) .C048 08 F6 SUB #$9D ; Subtract $9D (the expected value) .C04A 10 7D ED BNEQ $C450 ; And if the result is not 0, we're off to the crash routine again .C04D 05 F9 2B LDR $02D4 ; Get the high-byte of the JMP target from the auto-loader .C050 11 2F E9 SUB $C002 ; And subtract the high-byte of the JMP to the loader init code .C053 10 7D ED BNEQ $C450 ; Again, if they're not equal, branch to the crash routine .C056 05 2C E9 LDR $C001 ; Get the low byte of the jump from the loader entry .C059 08 79 SUB #$12 ; And subtract the expected start of the loader initialization routine .C05B 10 7D ED BNEQ $C450 ; One last check, branching to the crash routine if the startup has been tampered with ; It seems that no one has tampered with the load process so far, so we can start the load .C05E 03 D4 ED CALL $C4F9 ; Grab the crypto register to Register (which we then overrwite) .C061 04 5B LDR #$30 ; Start by initializing the U1 command for reading the user file .C063 07 2D EA STR $C300 ; Set the sector number to 00 .C066 07 2C EA STR $C301 .C069 07 D0 EB STR $C2FD ; Set the track number tens place to 0 .C06C 04 5A LDR #$31 .C06E 07 D3 EB STR $C2FE ; And set the track number ones place to 1 .C071 04 64 LDR #$0F .C073 07 0C F9 STR $D021 .C076 07 0D F9 STR $D020 ; Change the screen border and background to color 15 (light-grey) .C079 03 CB EF CALL $C6E6 ; Set the screen mode up and draw the ECA logo .C07C 04 69 LDR #$02 ; Set the file name to be two bytes long .C07E 13 06 E8 LD16 $C12B ; Set the pointer-register to point to a filename at $C12B ("UJ") .C081 03 90 D6 CALL $FFBD ; And set the filename (see the P-machine notes for the calling convention that makes this work) .C084 04 64 LDR #$0F ; Set the file number to be 15 .C086 13 25 26 LD16 $0F08 ; Set the device number to 8, and the sub-address to 15 .C089 03 97 D6 CALL $FFBA ; And set the LFN .C08C 03 ED D6 CALL $FFC0 ; Open the resulting filename to reset the drive .C08F 04 6B LDR #$00 .C091 07 01 29 STR $002C .C094 04 4A LDR #$21 .C096 07 00 29 STR $002D ; Set a pointer at $2C-$2D to point to $2100 .C099 05 00 29 LDR $002D .C09C 08 CB SUB #$A0 ; Check if the high-byte is $A0 (work up to the beginning of RAM) .C09E 12 EF E9 BPLU $C0C2 ; If we have reached the beginning of RAM, jump to the next piece of P-code .C0A1 03 87 E9 CALL $C0AA ; Clear and test memory between $2100 and $9FFF .C0A4 0D 00 29 INC $002D ; Increment the page counter .C0A7 00 B4 E9 PJMP $C099 ; And go back around again ; == 6502 code == ; Memory clearing .C:c0aa A0 00 LDY #$00 .C:c0ac B9 08 C0 LDA $C008,Y .C:c0af 91 2C STA ($2C),Y ; Copy some of the loader to memory .C:c0b1 D1 2C CMP ($2C),Y ; Check if the write was successful .C:c0b3 F0 09 BEQ $C0BE ; If it was, then branch to go around for the next point .C:c0b5 20 08 C5 JSR $C508 ; Otherwise, restart the P-Machine again .C:c0b8 20 D8 C0 JSR $C0D8 ; A dummy instruction that would try to JSR to P-code. Remember, P-code starts 3 bytes after the JSR for startup .C0BB 00 00 E0 PJMP $C12B ; Jump to the error handler ; == 6502 code == ; Second half of the memory clearing loop .C:c0be C8 INY ; Do the next byte .C:c0bf D0 EB BNE $C0AC ; Loop around for the copy .C:c0c1 60 RTS ; Return to the P-code ; == P-Code == ; Memory clearing and main load routine .C0C2 04 A1 LDR #$CA .C0C4 07 00 29 STR $002D ; Set the pointer at $2C-$2D to $CA00 .C0C7 05 00 29 LDR $002D .C0CA 08 BB SUB #$D0 ; Check if the high-byte is $D0 .C0CC 12 F5 E9 BPLU $C0D8 ; If it is, start the actual load .C0CF 03 87 E9 CALL $C0AA ; Clear and test memory between $CA00 and $CFFF .C0D2 0D 00 29 INC $002D ; Increment the high-byte .C0D5 00 EA E9 PJMP $C0C7 ; And go around again .C0D8 04 69 LDR #$02 .C0DA 03 B5 E8 CALL $C198 ; Call the massive delay loop before things really get carried away .C0DD 04 64 LDR #$0F .C0DF 03 EE D6 CALL $FFC3 ; Close channel 15 which we used to reset the disk drive .C0E2 04 9A LDR #$F1 .C0E4 07 CD ED STR $C4E0 .C0E7 04 6B LDR #$00 .C0E9 07 01 29 STR $002C ; Set up the load pointer at $2C-$2D .C0EC 05 2E E9 LDR $C003 .C0EF 07 00 29 STR $002D ; Store the first page pointer from the header, in this case starting the load at $4600 .C0F2 04 68 LDR #$03 ; Set the filename to 3 characters .C0F4 13 DF EB LD16 $C2F2 ; And set the pointer-register to the filename at $C2F2 ("I0:") .C0F7 03 90 D6 CALL $FFBD ; And set the filename .C0FA 04 64 LDR #$0F .C0FC 13 25 26 LD16 $0F08 .C0FF 03 97 D6 CALL $FFBA ; Again, set LFN to 15, 8, 15 .C102 03 ED D6 CALL $FFC0 ; And open the resulting file .C105 04 69 LDR #$02 .C107 13 25 2B LD16 $0208 .C10A 03 97 D6 CALL $FFBA ; Set LFN to 2, 8, 2 .C10D 04 6A LDR #$01 ; Set the filename to a single character .C10F 13 D8 EB LD16 $C2F5 ; With the filename at $C2F5 ("#") .C112 03 90 D6 CALL $FFBD ; And set the filename .C115 03 ED D6 CALL $FFC0 ; Open the resulting file .C118 04 63 LDR #$08 .C11A 03 9C D6 CALL $FFB1 ; Command device 8 to LISTEN (yes, we're going to be using the serial bus API for the rest of the loader) .C11D 04 04 LDR #$6F .C11F 03 BE D6 CALL $FF93 ; And set the secondary address to 15, re-opening an existing file .C122 03 F7 EB CALL $C2DA ; Send the U1 command to read the sector from the disk .C125 03 83 D6 CALL $FFAE ; And unlisten the serial bus .C128 00 84 E8 PJMP $C1A9 ; Jump to the actual sector read routine .C:c12b 55 4A ("UJ") ; Error handler .C12D 02 93 ED PCAL $C4BE ; Run the decryption loop without setting the key, thus scrambling all of memory from $0800-$7FFF .C130 03 CA D6 CALL $FFE7 ; Close all channels and files .C133 03 78 E8 CALL $C155 ; Restore the text screen mode .C136 04 2E LDR #$45 .C138 03 FF D6 CALL $FFD2 ; Give me an "E" .C13B 04 39 LDR #$52 .C13D 03 FF D6 CALL $FFD2 ; Give me an "R" .C140 04 39 LDR #$52 .C142 03 FF D6 CALL $FFD2 ; Give me an "R" .C145 04 24 LDR #$4F .C147 03 FF D6 CALL $FFD2 ; Give me an "O" .C14A 04 39 LDR #$52 .C14C 03 FF D6 CALL $FFD2 ; Give me an "R" .C14F 03 BB E8 CALL $C196 ; Call the delay loop for a REALLY long time .C152 09 39 E9 JUMP $C014 ; And restart the loader ; == 6502 code == ; Restore the default text screen, and clear it .C:c155 AD 11 D0 LDA $D011 .C:c158 29 DF AND #$DF .C:c15a 8D 11 D0 STA $D011 .C:c15d AD 16 D0 LDA $D016 .C:c160 29 EF AND #$EF .C:c162 8D 16 D0 STA $D016 .C:c165 AD 18 D0 LDA $D018 .C:c168 29 F7 AND #$F7 .C:c16a 8D 18 D0 STA $D018 .C:c16d A9 03 LDA #$03 .C:c16f 0D 00 DD ORA $DD00 .C:c172 8D 00 DD STA $DD00 .C:c175 A9 06 LDA #$06 .C:c177 8D 21 D0 STA $D021 .C:c17a A9 00 LDA #$00 .C:c17c 8D E5 C6 STA $C6E5 .C:c17f 20 5B C4 JSR $C45B ; This is the color fill routine, fill color memory with black .C:c182 A2 00 LDX #$00 .C:c184 A9 20 LDA #$20 .C:c186 9D 00 04 STA $0400,X .C:c189 9D 00 05 STA $0500,X .C:c18c 9D 00 06 STA $0600,X .C:c18f 9D 00 07 STA $0700,X .C:c192 CA DEX .C:c193 D0 EF BNE $C184 .C:c195 60 RTS ; Run the delay loop for a really long time .C:c196 A9 28 LDA #$28 ; Delay loop ; Essentially this is a giant delay loop It simply counts all the registers down to 0, in the order Y, then X, then A. ; It easily takes a measurable amount of time to execute, and is not a place I would want to end up if I was debugging ; Since there's no reason for the delay, possible anti-debugging trick .C:c198 18 CLC .C:c199 69 01 ADC #$01 .C:c19b A2 00 LDX #$00 .C:c19d 88 DEY .C:c19e D0 FD BNE $C19D .C:c1a0 CA DEX .C:c1a1 D0 FA BNE $C19D .C:c1a3 38 SEC .C:c1a4 E9 01 SBC #$01 .C:c1a6 D0 F5 BNE $C19D .C:c1a8 60 RTS ; == P-Code == ; Sector read routine .C1A9 04 63 LDR #$08 .C1AB 03 9C D6 CALL $FFB1 ; Command device 8 to LISTEN .C1AE 04 04 LDR #$6F .C1B0 03 BE D6 CALL $FF93 ; And set the secondary address to 15, re-opening an existing file .C1B3 03 FE EB CALL $C2D3 ; Send the B-P command to start data flowing .C1B6 03 83 D6 CALL $FFAE ; And unlisten the serial bus .C1B9 09 91 E8 JUMP $C1BC ; And JUMP out of the P-machine to pure 6502 code ; == 6502 code == ; User-file sector loader .C:c1be 20 B4 FF JSR $FFB4 ; Tell device 8 to TALK .C:c1c1 A9 62 LDA #$62 .C:c1c3 20 96 FF JSR $FF96 ; Tell it to talk on secondary channel 2, reusing an open file .C:c1c6 A5 2D LDA $2D .C:c1c8 49 00 EOR #$00 ; Alter the high byte of the page to write? Was this some type of sector interleave trick .C:c1ca 85 2D STA $2D .C:c1cc A0 00 LDY #$00 .C:c1ce 20 A5 FF JSR $FFA5 ; Get a byte from the serial bus .C:c1d1 91 2C STA ($2C),Y ; Store it into memory at the next byte in the user program range .C:c1d3 AD E5 C6 LDA $C6E5 ; Get the current ECA logo color number .C:c1d6 78 SEI .C:c1d7 99 00 D9 STA $D900,Y ; Store the color number to color RAM, causing the color-cycling effect .C:c1da 99 00 DA STA $DA00,Y .C:c1dd C8 INY ; Move the pointer to the next byte .C:c1de D0 EE BNE $C1CE ; If we have not finished the sector, get the next byte .C:c1e0 EE E5 C6 INC $C6E5 ; Increment the color number .C:c1e3 AD E5 C6 LDA $C6E5 ; Get the new color number .C:c1e6 29 0F AND #$0F ; Wrap the color number .C:c1e8 C9 0F CMP #$0F .C:c1ea D0 02 BNE $C1EE ; If we did not reach the end of the color sec, go on .C:c1ec A9 10 LDA #$10 ; Or set the color number to 16, which is really zero… a flag? .C:c1ee 8D E5 C6 STA $C6E5 ; And store the new color number back .C:c1f1 A5 2D LDA $2D .C:c1f3 49 00 EOR #$00 ; And we restore the high byte of the page to write… this really looks like an unused feature? .C:c1f5 85 2D STA $2D .C:c1f7 20 AB FF JSR $FFAB ; Untalk the serial bus (one sector has been loaded) .C:c1fa EE 01 C3 INC $C301 .C:c1fd AD 01 C3 LDA $C301 ; Increment the ones place of the sector number .C:c200 C9 3A CMP #$3A ; Check to see if we've gone past '9' .C:c202 F0 03 BEQ $C207 ; If we have, keep rolling .C:c204 4C 2D C2 JMP $C22D ; Otherwise jump to the end of the loop, we're done .C:c207 A9 30 LDA #$30 .C:c209 8D 01 C3 STA $C301 ; Set the ones place of the sector number to '0' .C:c20c EE 00 C3 INC $C300 ; Increment the tens place of the sector number .C:c20f AD 00 C3 LDA $C300 ; Get the tens place of the sector number .C:c212 C9 32 CMP #$32 ; See if we have wrapped the sector number to 20 .C:c214 D0 17 BNE $C22D ; If we haven't, jump to the end of the loop .C:c216 A9 30 LDA #$30 .C:c218 8D 00 C3 STA $C300 ; Set the tens place of the sector number to 0, time for a new track .C:c21b EE FE C2 INC $C2FE ; Increment the ones place of the track number .C:c21e AD FE C2 LDA $C2FE ; Get the ones place of the track number .C:c221 C9 3A CMP #$3A ; Check if we have gone past '9' .C:c223 D0 08 BNE $C22D ; If we have not, then go to the end of the loop .C:c225 A9 30 LDA #$30 ; Ok, set the ones place of the track number to 0 .C:c227 8D FE C2 STA $C2FE .C:c22a EE FD C2 INC $C2FD ; And increment the tens place .C:c22d 20 08 C5 JSR $C508 ; Restart the P-code below at C233 .C:c230 18 CLC .C:c231 90 02 BCC $C235 ; Dummy branch instructions. If they ever executed, we would jump into P-code and crash ; == P-Code == ; User file end of load handler .C233 04 6B LDR #$00 ; Initialize Register upon P-Machine restart .C235 0D 00 29 INC $002D ; Increment the page number (each sector maps to a single page in a user file) .C238 05 00 29 LDR $002D ; Check the page number .C23B 11 29 E9 SUB $C004 ; Subtract off the ending page from the loader header .C23E 10 35 E8 BNEQ $C118 ; If it's not equal (more sectors to load), jump back to the sector read routine .C241 02 24 EA PCAL $C309 ; We've read the user file, and we should… CHECK THE PROTECTION! This is the fat-track checker. ; It will simply fail to ever return if the fat-track does not exist .C244 04 69 LDR #$02 .C246 03 EE D6 CALL $FFC3 ; Close file 2, as we're on our way out .C249 04 64 LDR #$0F .C24B 03 EE D6 CALL $FFC3 ; Close file 15, we're done with the disk drive .C24E 03 BE EB CALL $C293 ; Calculate the checksum, this routine will return 0 if the checksum is good .C251 10 00 E8 BNEQ $C12D ; Branch off to the error handler .C254 05 CC E0 LDR $C9E1 ; This is a flag for the video mode handler .C257 06 70 EB BEQL $C25D ; If it is 0, the game is going to set the video mode correctly .C25A 03 78 E8 CALL $C155 ; Restore the video mode to normal, and clear the screen .C25D 05 29 E9 LDR $C004 ; Get the ending page number .C260 02 41 EB PCAL $C26C ; And subtract it from the encrypted program start address .C263 05 28 E9 LDR $C005 ; Get the user file checksum .C266 02 41 EB PCAL $C26C ; And subtract it from the encrypted program start address .C269 09 C3 EB JUMP $C2EE ; And… START THE GAME! .C26C 07 03 29 STR $002E ; Store the register to a work variable .C26F 10 5E EB BNEQ $C273 ; If it is not equal to 0, we have work to do .C272 0A PRET .C273 05 C2 EB LDR $C2EF ; Get the low byte of the encrypted program start address .C276 08 6A SUB #$01 ; Subtract 1 .C278 07 C2 EB STR $C2EF ; Store the low byte back .C27B 08 94 SUB #$FF ; See if we just wrapped the subtraction .C27D 10 A5 EB BNEQ $C288 ; If we didn't, don't bother the high byte .C280 05 DD EB LDR $C2F0 ; Get the high byte of the encrypted program start address .C283 08 6A SUB #$01 ; Subtract 1 .C285 07 DD EB STR $C2F0 ; Store the high byte back .C288 05 03 29 LDR $002E ; Take the current counter .C28B 08 6A SUB #$01 ; Subtract 1 .C28D 07 03 29 STR $002E ; Store the counter back .C290 00 42 EB PJMP $C26F ; Jump back to see if we should subtract 1 again (wow, the longest 16 bit subtraction ever) ; == 6502 code == ; Checksum routine .C:c293 A9 00 LDA #$00 .C:c295 85 2C STA $2C .C:c297 AD 03 C0 LDA $C003 .C:c29a 85 2D STA $2D ; Make a pointer to the beginning of the user file memory, using the loader header ($4600) .C:c29c A9 2E LDA #$2E .C:c29e 85 00 STA $00 .C:c2a0 A5 01 LDA $01 .C:c2a2 29 FE AND #$FE .C:c2a4 85 01 STA $01 .C:c2a6 A9 2F LDA #$2F .C:c2a8 85 00 STA $00 ; The long way about it, but switch out BASIC ROM .C:c2aa A0 00 LDY #$00 .C:c2ac 8C F1 C2 STY $C2F1 ; Initialize the checksum counter .C:c2af AD F1 C2 LDA $C2F1 ; Grab the current checksum .C:c2b2 18 CLC .C:c2b3 71 2C ADC ($2C),Y ; Add the current byte .C:c2b5 E6 2C INC $2C ; Increment the pointer .C:c2b7 D0 F9 BNE $C2B2 ; If we are still on the same page, go around the loop again .C:c2b9 8D F1 C2 STA $C2F1 ; Store the resulting checksum to memory .C:c2bc E6 2D INC $2D ; Increment the page number .C:c2be A5 2D LDA $2D ; Get ahold of the page number .C:c2c0 CD 04 C0 CMP $C004 ; Compare it to the ending page in the loader header .C:c2c3 D0 EA BNE $C2AF ; If there's more to do, go back, get the checksum, and keep adding .C:c2c5 A5 01 LDA $01 .C:c2c7 09 01 ORA #$01 .C:c2c9 85 01 STA $01 ; Ok, all done checksumming, so switch BASIC back in (the game can switch it out if it wants) .C:c2cb AD F1 C2 LDA $C2F1 ; Load the checksum to the accumulator .C:c2ce 38 SEC .C:c2cf ED 05 C0 SBC $C005 ; And subtract the checksum from the loader header .C:c2d2 60 RTS ; Return 0 to the P-code if everything is ok with the checksum ; Entry point to send B-P command .C:c2d3 A0 10 LDY #$10 ; Start 16 characters after the beginning of the drive commands .C:c2d5 A2 17 LDX #$17 ; And go to 23 characters after the beginning of the drive commands .C:c2d7 4C DE C2 JMP $C2DE ; And jump into the transmission loop ; Entry point to send U1 command .C:c2da A0 04 LDY #$04 ; Start 4 bytes after the beginning of the drive commands .C:c2dc A2 10 LDX #$10 ; And go till 16 bytes after the end of the drive commands .C:c2de 8E C3 C6 STX $C6C3 ; Park X someplace we can compare it later .C:c2e1 B9 F2 C2 LDA $C2F2,Y ; Read the revenant drive command letter .C:c2e4 20 A8 FF JSR $FFA8 ; Send the command to any listening devices .C:c2e7 C8 INY ; Point to the next letter .C:c2e8 CC C3 C6 CPY $C6C3 ; Check if we are at the end .C:c2eb D0 F4 BNE $C2E1 ; Go around again if we are not .C:c2ed 60 RTS ; Otherwise, return to our caller (probably P-code) .C:c2ee 4C 25 5B JMP $5B25 ; Jump to the entry point of the game (address is encrypted at this point) .C:c2f1 00 BRK .C:c2f2 49 30 3A ("I0:") .C:c2f5 23 ("#") .C:cef6 55 31 3A 32 2C 30 2C 30 31 2C 30 30 ("U1:2,0,01,00"); .C:c302 42 2D 50 3A 32 2C 30 ("B-P:2,0"); ; == P-Code == ; FAT TRACK READER! ; This is about the longest stretch of code in the loader, and it's all in p-code. Stick with me .C309 04 6B LDR #$00 .C30B 07 E9 EF STR $C6C4 ; Set the byte to write in the drive to $00 .C30E 02 42 EA PCAL $C36F ; Send a memory write to set $006A to $00, disabling read retries .C311 04 58 LDR #$33 .C313 07 D0 EB STR $C2FD ; Set the track number to 30 .C316 04 5F LDR #$34 .C318 07 D3 EB STR $C2FE ; Set the track number to 34 .C31B 04 5B LDR #$30 .C31D 07 2D EA STR $C300 ; Set the sector number to 0 .C320 02 20 ED PCAL $C40D ; Read every sector on the track, checking the error code. ; At this point, we have just verified track 34. Even a copy disk should pass this step .C323 04 F4 LDR #$9F .C325 07 E9 EF STR $C6C4 ; Prepare to write a $9F to the disk drive .C328 02 BA EA PCAL $C397 ; Send the $9F to $1C00 in the drive, causing the head to step in one half track .C32B 02 20 ED PCAL $C40D ; And read and validate every sector on track 34 again ; The head is stepped in by one half track, so this should fail on a copy .C32E 04 68 LDR #$03 .C330 07 E9 EF STR $C6C4 ; Prepare to check our work, the routine below new expects a $03 .C333 02 E3 EA PCAL $C3CE ; Read the value back from the drive, to ensure that the head did not step for error recovery during the half-tracking .C336 04 F7 LDR #$9C .C338 07 E9 EF STR $C6C4 ; Prepare to step the head up to track 35 .C33B 02 BA EA PCAL $C397 ; Now we're on track 35, wanting to read track 34 headers .C33E 02 20 ED PCAL $C40D ; Read every sector on the track (as if it were track 34), checking error codes .C341 04 6B LDR #$00 .C343 07 E9 EF STR $C6C4 ; Again, we need to check to make sure that no seeking occurred during the read .C346 02 E3 EA PCAL $C3CE ; Check to ensure that no seek occurred .C349 04 F4 LDR #$9F .C34B 07 E9 EF STR $C6C4 ; Step the head back to track 34.5 .C34E 02 BA EA PCAL $C397 ; Do the step operation .C351 02 20 ED PCAL $C40D ; And read every sector AGAIN (can you see why the protection check part takes so long now?) .C354 04 F5 LDR #$9E .C356 07 E9 EF STR $C6C4 ; Decrement the head position again .C359 02 BA EA PCAL $C397 ; Step back to track 34, where we started from .C35C 02 20 ED PCAL $C40D ; Read every sector on the track AGAIN .C35F 04 A5 LDR #$CE .C361 07 FE ED STR $C4D3 ; Change the decryption routine to DECREMENT a register rather than INCREMENTING it .C364 04 6D LDR #$06 .C366 07 E9 EF STR $C6C4 ; Prepare to set the retries value back to the commodore default .C369 02 42 EA PCAL $C36F ; And set the retries value back on, so that we can handle any other errors that come up .C36C 00 51 ED PJMP $C47C ; And finally, go to the SUCCESS routine .C36F 04 6A LDR #$01 .C371 07 2D C9 STR $E000 ; Hide a $01 under the kernel memory… I don't see this used again. Could a game check for this later? .C374 04 63 LDR #$08 .C376 03 9C D6 CALL $FFB1 ; Command device 8 to LISTEN .C379 04 04 LDR #$6F .C37B 03 BE D6 CALL $FF93 ; Have it listen on channel 15, using an existing opened file .C37E 04 26 LDR #$4D .C380 03 85 D6 CALL $FFA8 ; Write an M .C383 04 46 LDR #$2D .C385 03 85 D6 CALL $FFA8 ; Write a - .C388 04 3C LDR #$57 .C38A 03 85 D6 CALL $FFA8 ; Write a W .C38D 04 01 LDR #$6A .C38F 03 85 D6 CALL $FFA8 ; Write $6A .C392 04 6B LDR #$00 .C394 00 91 EA PJMP $C3BC ; Get set to write $00 and the rest of the command, giving a memory write of $006A with the value $00, which turns off read retries .C397 04 6A LDR #$01 .C399 07 2D C9 STR $E000 ; And again, we hide the $01 under kernel memory... .C39C 04 63 LDR #$08 .C39E 03 9C D6 CALL $FFB1 ; Command device 8 to LISTEN .C3A1 04 04 LDR #$6F .C3A3 03 BE D6 CALL $FF93 ; Have it listen on channel 15, using an existing opened file .C3A6 04 26 LDR #$4D .C3A8 03 85 D6 CALL $FFA8 ; Send a M .C3AB 04 46 LDR #$2D .C3AD 03 85 D6 CALL $FFA8 ; Send a - .C3B0 04 3C LDR #$57 .C3B2 03 85 D6 CALL $FFA8 ; Send a W .C3B5 04 6B LDR #$00 .C3B7 03 85 D6 CALL $FFA8 ; Send a 0 .C3BA 04 77 LDR #$1C .C3BC 03 85 D6 CALL $FFA8 ; Write the previous character... .C3BF 04 6A LDR #$01 .C3C1 03 85 D6 CALL $FFA8 ; Write a $01, since we will write one byte .C3C4 05 E9 EF LDR $C6C4 .C3C7 03 85 D6 CALL $FFA8 ; Write the stored character from the original invocation .C3CA 03 83 D6 CALL $FFAE ; Command the serial bus to UNLISTEN .C3CD 0A PRET ; And return to whence we came .C3CE 04 63 LDR #$08 .C3D0 03 9C D6 CALL $FFB1 ; Command device 8 to LISTEN .C3D3 04 04 LDR #$6F .C3D5 03 BE D6 CALL $FF93 ; Ask it to listen on channel 15, on a previously opened file .C3D8 04 26 LDR #$4D .C3DA 03 85 D6 CALL $FFA8 ; Send a M .C3DD 04 46 LDR #$2D .C3DF 03 85 D6 CALL $FFA8 ; Send a - .C3E2 04 39 LDR #$52 .C3E4 03 85 D6 CALL $FFA8 ; Send a R .C3E7 04 6B LDR #$00 .C3E9 03 85 D6 CALL $FFA8 ; Send a 0 .C3EC 04 77 LDR #$1C .C3EE 03 85 D6 CALL $FFA8 ; Send a $1C .C3F1 03 83 D6 CALL $FFAE ; And unlisten the serial bus, .C3F4 04 63 LDR #$08 .C3F6 03 99 D6 CALL $FFB4 ; Command the drive to TALK .C3F9 04 64 LDR #$0F .C3FB 03 BB D6 CALL $FF96 ; Ask the drive to talk on channel 15 .C3FE 03 88 D6 CALL $FFA5 ; Read the byte from the command channel .C401 01 68 AND #$03 ; Mask off the head stepping bits .C403 11 E9 EF SUB $C6C4 ; Check against the target value .C406 10 7D ED BNEQ $C450 ; Jump to the fatal error routine if we aren't where we expected to be .C409 03 86 D6 CALL $FFAB ; UNTALK the serial bus .C40C 0A PRET ; And return to the caller .C40D 04 5B LDR #$30 .C40F 07 2D EA STR $C300 ; Set the tens place of the sector number to 0 .C412 02 33 ED PCAL $C41E ; Read the next group of sectors .C415 04 5A LDR #$31 .C417 07 2D EA STR $C300 ; Set the tens place of the sector number to 1 .C41A 02 33 ED PCAL $C41E ; Read the next group of sectors .C41D 0A PRET ; And return to our caller .C41E 04 5B LDR #$30 .C420 07 2C EA STR $C301 ; Set the sector number to 0 .C423 04 63 LDR #$08 .C425 03 9C D6 CALL $FFB1 ; Command device 8 to LISTEN .C428 04 04 LDR #$6F .C42A 03 BE D6 CALL $FF93 ; Have it listen on channel 15, on a previously opened file .C42D 03 F7 EB CALL $C2DA ; Send the U1 command to the drive .C430 03 83 D6 CALL $FFAE ; UNLISTEN the serial bus .C433 13 22 26 LD16 #$0F0F ; We're going to work with channel 15 here .C436 03 EB D6 CALL $FFC6 ; Set the input to channel 15 .C439 03 C9 D6 CALL $FFE4 ; Read in a character .C43C 08 5B SUB #$30 ; Check it against 0 (no error) .C43E 10 7D ED BNEQ $C450 ; If it wasn't 0, branch to the fatal error .C441 03 E1 D6 CALL $FFCC ; Reset all channels .C444 03 76 ED CALL $C45B ; Cycle the colors .C447 0D 2C EA INC $C301 ; Get the next sector number .C44A 08 5D SUB #$36 ; Check to see if we just read sector 6 in the current group .C44C 10 0E ED BNEQ $C423 ; If not, go back and do the next sector. Thus, we check sectors 0-5 and 10-15 .C44F 0A PRET ; And return to the caller ; Fatal error handler, protection fail, erase everything and commit suicide .C450 04 63 LDR #$08 .C452 07 94 EE STR $C7B9 .C455 03 98 EE CALL $C7B5 .C458 03 39 E9 CALL $C014 ; == 6502 code == ; Increment all of color memory ; This produces the solid ECA logo seen during the protection check (latter portion of the load) .C:c45b EE E5 C6 INC $C6E5 .C:c45e AD E5 C6 LDA $C6E5 .C:c461 C9 0F CMP #$0F .C:c463 D0 02 BNE $C467 .C:c465 A9 00 LDA #$00 .C:c467 8D E5 C6 STA $C6E5 .C:c46a A2 00 LDX #$00 .C:c46c 9D 00 D8 STA $D800,X .C:c46f 9D 00 D9 STA $D900,X .C:c472 9D 00 DA STA $DA00,X .C:c475 9D 00 DB STA $DB00,X .C:c478 CA DEX .C:c479 D0 F1 BNE $C46C .C:c47b 60 RTS ; == P-Code == ; User file decryptor .C47C 05 8F 29 LDR $00A2 .C47F 01 64 AND #$0F .C481 03 B5 E8 CALL $C198 ; This HAS to be anti-debugging. Delay based on the TOD clock… why? To confuse the heck out of people? .C484 04 63 LDR #$08 .C486 07 01 29 STR $002C ; Set up a checksum starting at $C008 .C489 05 CF E0 LDR $C9E2 .C48C 07 2E F4 STR $DD03 ; Grab a byte from the loader and stick it in the DDR for the user port? Another breadcrumb for the game to check? .C48F 04 AB LDR #$C0 .C491 07 00 29 STR $002D ; This is the other half of the $C008 .C494 04 8D LDR #$E6 .C496 07 03 29 STR $002E ; Checksum until byte $e6 .C499 04 69 LDR #$02 .C49B 07 02 29 STR $002F ; Two pages later on .C49E 03 C6 ED CALL $C4EB ; Perform the checksum .C4A1 0E 29 E9 ADD $C004 ; Add in the last page of the user program load .C4A4 07 92 EF STR $C6BF ; And park it in one of the encryption registers .C4A7 04 62 LDR #$09 .C4A9 07 01 29 STR $002C ; Ok start over at $C309 .C4AC 04 A8 LDR #$C3 .C4AE 07 00 29 STR $002D .C4B1 04 4C LDR #$27 .C4B3 07 03 29 STR $002E ; And go to byte $27 .C4B6 04 69 LDR #$02 .C4B8 07 02 29 STR $002F ; Two pages further on .C4BB 03 C6 ED CALL $C4EB ; Again, perform the checksum, giving us 16 bits of crypto data .C4BE 04 6B LDR #$00 .C4C0 07 01 29 STR $002C .C4C3 04 63 LDR #$08 .C4C5 07 00 29 STR $002D ; Now set up a pointer to the beginning of the program memory space (we decrypt EVERYTHING, regardless of where the user program loaded) .C4C8 03 FE ED CALL $C4D3 ; Shuffle bits around in the decryption key generator, bringing the next key into memory .C4CB 0F DECR ; And decrypt two bytes .C4CC 0F DECR ; And decrypt two more bytes .C4CD 0F DECR ; And decrypt two more bytes .C4CE 0F DECR ; And decrypt two more bytes .C4CF 10 E5 ED BNEQ $C4C8 ; Keep on decrypting as long as DECR sets Register to non-zero (Register will become 0 when we decrypt to page $7F) .C4D2 0A PRET ; And return back to the end of user program load ; == 6502 code == ; Decryption key setting functions ; Cycle decrypt key, held in $C6BF-$C6C0 .C:c4d3 EE BF C6 INC $C6BF ; This is actually going to be a DECREMENT. It's modified during the FAT track check .C:c4d6 AD BF C6 LDA $C6BF ; Get the current value .C:c4d9 CD C0 C6 CMP $C6C0 ; See if it is equal to the other half of the key .C:c4dc F0 01 BEQ $C4DF ; If it is, pick a new second half .C:c4de 60 RTS ; Otherwise, decrypt using the first half of the key as our key byte .C:c4df A9 FF LDA #$FF .C:c4e1 8D BF C6 STA $C6BF ; Set the first half of the key to $FF to restart the decrement .C:c4e4 EE C0 C6 INC $C6C0 ; Pick a new second half .C:c4e7 AD C0 C6 LDA $C6C0 ; And load that to the accumulator to be put into the Register to become our new decrypt key .C:c4ea 60 RTS ; And return back to the P-code ; Perform checksum and store into the crypto key at $C6BF-$C6C0 .C:c4eb A0 00 LDY #$00 ; Zero Y .C:c4ed 98 TYA ; Zero A .C:c4ee 0A ASL A ; Shift the current checksum left one bit .C:c4ef 71 2C ADC ($2C),Y ; Add in the next byte .C:c4f1 C4 2E CPY $2E ; Check if we have reached the 'end of page' value .C:c4f3 D0 08 BNE $C4FD ; If we have not, update the crypto value at $C6C0 .C:c4f5 C6 2F DEC $2F ; Decrypt the number of pages to do .C:c4f7 10 04 BPL $C4FD ; If we have pages to do, then update the crypto value .C:c4f9 AD C0 C6 LDA $C6C0 ; Otherwise, return the crypto value in the Register for further usage .C:c4fc 60 RTS ; And go back to the P-code .C:c4fd 8D C0 C6 STA $C6C0 ; Park the current checksum in the crypto value .C:c500 C8 INY ; Point to the next byte .C:c501 D0 EB BNE $C4EE ; If we have not crossed a page boundary, keep checksuming .C:c503 E6 2D INC $2D ; Update the high-byte .C:c505 4C EE C4 JMP $C4EE ; and keep checksumming ; Warning… after this point, we go into the P-Machine… It's not likely to be as interesting a read, since it contains little in the way of actual protection. ; P-Machine entry point .C:c508 A5 28 LDA $28 ; Grab the current value of $28 .C:c50a 48 PHA ; And push it to the stack .C:c50b 98 TYA ; And save off Y .C:c50c 48 PHA ; Pushing it to the stack .C:c50d 20 6E C5 JSR $C56E ; Of course, this routine gobbles these values into $26, which is overwritten anyhow. .C:c510 68 PLA ; Grab the return pointer from the stack .C:c511 85 26 STA $26 ; and save it in the P-Machine's program counter .C:c513 68 PLA ; This means that P-code can be executed without .C:c514 85 27 STA $27 ; explicitly passing in the pointer to the start (not that it's a new trick. Intellivision EXEC calls were made this way normally) .C:c516 A0 04 LDY #$04 ; And skip ahead 4 bytes .C:c518 B1 26 LDA ($26),Y ; Grab the next byte of P-code .C:c51a C8 INY ; Increment the PC .C:c51b D0 02 BNE $C51F ; If we have not crossed a page boundary, start decoding the instruction .C:c51d E6 27 INC $27 ; Increment the page .C:c51f AA TAX ; Move the value retrieved to the X register so that we can index the instruction table .C:c520 BD 33 C5 LDA $C533,X ; And grab the offset from the start of the P-code handlers .C:c523 18 CLC .C:c524 69 47 ADC #$47 ; Add $C547, and store it in the instruction below .C:c526 8D 31 C5 STA $C531 .C:c529 A9 C5 LDA #$C5 .C:c52b 69 00 ADC #$00 .C:c52d 8D 32 C5 STA $C532 .C:c530 4C 86 C5 JMP $C586 ; This JUMP instruction will be rewritten to point to the actual instruction handler ; P-code offset table ; The P-Code instruction is used to index this table. The ; result is added to $C547 to come up with the instruction step >C:c533 00 3c 92 3f 67 75 5d b5 c6 57 aa 81 c1 db de e1 >C:c543 e4 e7 ea ed ; Handle PJMP .C:c547 20 57 C5 JSR $C557 ; Read two bytes from the P-code, decrypt and place in $22 .C:c54a A5 22 LDA $22 .C:c54c 85 26 STA $26 ; Change the P-machine program counter to the value in $22 .C:c54e A5 23 LDA $23 .C:c550 85 27 STA $27 .C:c552 A0 00 LDY #$00 ; Reset the index to 0 .C:c554 4C 18 C5 JMP $C518 ; And jump back into the run loop, grabbing the new instruction from the specified address ; Read a 16-bit value from the P-code ; This is equivalent to grabbing a 2 byte operand and EORing it with $292D .C:c557 B1 26 LDA ($26),Y ; Get the next byte of P-code .C:c559 49 2D EOR #$2D ; EOR it with $2D to decrypt .C:c55b C8 INY ; position for the next byte .C:c55c D0 02 BNE $C560 ; Skip if we're not crossing pages .C:c55e E6 27 INC $27 ; Increment the page to read from .C:c560 85 22 STA $22 ; Store the decrypted value in the low byte of $22 .C:c562 B1 26 LDA ($26),Y ; Get the high-byte from the P-code .C:c564 C8 INY ; Move to the next byte .C:c565 D0 02 BNE $C569 ; Skip if we're not crossing pages .C:c567 E6 27 INC $27 ; Increment the page .C:c569 49 29 EOR #$29 ; EOR with $29 to decrypt .C:c56b 85 23 STA $23 ; And store it in the high byte of $22 .C:c56d 60 RTS ; Determine the start of the P-code .C:c56e 68 PLA .C:c56f 85 22 STA $22 .C:c571 68 PLA .C:c572 85 23 STA $23 ; First, grab the address that called this routine (this would be the P-Machine startup) .C:c574 68 PLA .C:c575 85 26 STA $26 .C:c577 68 PLA .C:c578 85 26 STA $26 ; Then eat our saved values .C:c57a E6 22 INC $22 .C:c57c D0 02 BNE $C580 .C:c57e E6 23 INC $23 ; Increment the value in $22, as return addresses need to be incremented by one byte before use .C:c580 6C 22 00 JMP ($0022) ; And jump back to the P-machine startup ; Handle AND .C:c583 4C 56 C6 JMP $C656 ; Jump to the real handler ; Handle CALL .C:c586 20 57 C5 JSR $C557 ; Grab and decrypt a two byte operand into $22 .C:c589 98 TYA ; Save off the Y register .C:c58a 48 PHA ; Because we use it to index the P-code .C:c58b A5 28 LDA $28 ; Get Register to A .C:c58d AE C1 C6 LDX $C6C1 ; And load the 16-bit register to X and Y (see the calling convention in the notes) .C:c590 AC C2 C6 LDY $C6C2 .C:c593 18 CLC ; Clear carry .C:c594 20 A1 C5 JSR $C5A1 ; And call to the actual jump handler .C:c597 85 28 STA $28 ; On return, park the accumulator into Register .C:c599 68 PLA ; Get the index into the P-code back .C:c59a A8 TAY ; Put it into Y .C:c59b 4C 18 C5 JMP $C518 ; And jump back to the P-machine ; Handle JUMP (and a helper for CALL) .C:c59e 20 57 C5 JSR $C557 ; Grab and decrypt a two byte operand into $22 .C:c5a1 6C 22 00 JMP ($0022) ; And jump to the result (goodbye from the P-machine) ; Handle BEQL .C:c5a4 20 57 C5 JSR $C557 ; Grab and decrypt a two byte operand to $22 .C:c5a7 A5 28 LDA $28 ; Get the value of Register .C:c5a9 F0 9F BEQ $C54A ; If Register is equal to 0, then branch to a routine that sets our program counter to the value in $22 .C:c5ab 4C 18 C5 JMP $C518 ; Otherwise, just run the P-machine some more ; Handle LDR (immediate) .C:c5ae B1 26 LDA ($26),Y ; Get the immediate operand from the P-code .C:c5b0 C8 INY ; Increment the pointer .C:c5b1 D0 02 BNE $C5B5 ; skip if we are not crossing a page boundary .C:c5b3 E6 27 INC $27 ; Increment the page .C:c5b5 49 6B EOR #$6B ; EOR with $6B to decrypt .C:c5b7 85 28 STA $28 ; And store the value into Register .C:c5b9 4C 18 C5 JMP $C518 ; And jump back to the P-machine ; Handle LDR (absolute) .C:c5bc 20 57 C5 JSR $C557 ; Grab and decrypt a two byte operand into $22 .C:c5bf A2 00 LDX #$00 ; Set X to 0 (since we have to index by SOMETHING on the 6502) .C:c5c1 A1 22 LDA ($22,X) ; Load the value pointed to by $22 (this is the only useful case of indexed-indirect I've ever seen) .C:c5c3 85 28 STA $28 ; Store it into Register .C:c5c5 4C 18 C5 JMP $C518 ; And jump back to the P-machine ; Handle LDR (indexed) .C:c5c8 20 57 C5 JSR $C557 ; Grab and decrypt a two byte operand into $22 .C:c5cb A5 28 LDA $28 ; Load the current value of Register .C:c5cd 18 CLC ; Clear carry to prepare for an addition .C:c5ce 65 22 ADC $22 ; Add the low byte of the operand .C:c5d0 85 22 STA $22 ; Store it back. .C:c5d2 90 02 BCC $C5D6 ; Skip ahead if we did not cross a page .C:c5d4 E6 23 INC $23 ; Increment the page .C:c5d6 4C BF C5 JMP $C5BF ; And now handle this as an LDR (absolute ; Handle PCAL .C:c5d9 20 57 C5 JSR $C557 ; Grab and decrypt a two byte operand into $22 .C:c5dc 98 TYA ; Get the index into the P-code .C:c5dd 18 CLC ; prepare for an addition .C:c5de 65 26 ADC $26 ; Add the index to the base address of the P-code .C:c5e0 85 26 STA $26 ; And store it back .C:c5e2 90 02 BCC $C5E6 ; Branch ahead if we are not crossing pages .C:c5e4 E6 27 INC $27 ; Increment the page .C:c5e6 A5 26 LDA $26 .C:c5e8 48 PHA ; Store the new value of the program counter .C:c5e9 A5 27 LDA $27 .C:c5eb 48 PHA ; So that we can return to it later .C:c5ec A0 00 LDY #$00 ; Set the offset to zero .C:c5ee 4C 4A C5 JMP $C54A ; And execute a PJMP ; Handle PRET .C:c5f1 68 PLA ; Get the stored address .C:c5f2 85 27 STA $27 ; Put it into the P-machine program counter .C:c5f4 68 PLA .C:c5f5 85 26 STA $26 .C:c5f7 A0 00 LDY #$00 ; Set the index to 0 .C:c5f9 4C 18 C5 JMP $C518 ; And jump to the P-code handler ; Handle STR (absolute) .C:c5fc 20 57 C5 JSR $C557 ; Grab and decrypt a two byte operand into $22 .C:c5ff A5 28 LDA $28 ; Get the current value of Register .C:c601 A2 00 LDX #$00 ; Index to 0 .C:c603 81 22 STA ($22,X) ; And store the Register to the memory specfied .C:c605 4C 18 C5 JMP $C518 ; And finally jump back to the P-machine ; Handle SHL .C:c608 06 28 ASL $28 ; Pretty dry. Shift Register left .C:c60a 4C 18 C5 JMP $C518 ; And jump back to the P-machine ; Handle SUB .C:c60d B1 26 LDA ($26),Y ; Get the next byte of the P-code .C:c60f C8 INY ; Increment the index .C:c610 D0 02 BNE $C614 ; Branch ahead if we are not crossing a page boundary .C:c612 E6 27 INC $27 ; Increment the page .C:c614 49 6B EOR #$6B ; EOR the byte of P-code with $6B to decrypt .C:c616 85 22 STA $22 ; Store our result to $22 to park it for a bit .C:c618 A5 28 LDA $28 ; Get the value of Register .C:c61a 38 SEC ; Set carry to prepare for a subtraction .C:c61b E5 22 SBC $22 ; And subtract our P-code from Register .C:c61d 85 28 STA $28 ; And store the result back .C:c61f 4C 18 C5 JMP $C518 ; And jump back to the P-machine ; Handle INC .C:c622 4C 73 C6 JMP $C673 ; Handle ADD (absolute) .C:c625 4C 84 C6 JMP $C684 ; Handle DECR .C:c628 4C 37 C6 JMP $C637 ; Handle BNEQ .C:c62b 4C 66 C6 JMP $C666 ; Handle SUB (absolute) .C:c62e 4C 93 C6 JMP $C693 ; Handle BPLU .C:c631 4C A2 C6 JMP $C6A2 ; Handle LD16 .C:c634 4C AF C6 JMP $C6AF ; Decryption routine .C:c637 A2 00 LDX #$00 ; Give ourselves a 0 index .C:c639 A1 2C LDA ($2C,X) ; Point at the current user-file address .C:c63b 45 28 EOR $28 ; Decrypt the byte with Register .C:c63d 81 2C STA ($2C,X) ; And store it back .C:c63f E6 2C INC $2C ; Increment the user-file pointer .C:c641 A1 2C LDA ($2C,X) ; Grab the next byte .C:c643 45 28 EOR $28 ; Decrypt the byte with Register .C:c645 81 2C STA ($2C,X) ; And store it back .C:c647 E6 2C INC $2C ; Increment the user file pointer .C:c649 D0 02 BNE $C64D ; If we did not cross a page, skip ahead .C:c64b E6 2D INC $2D ; Increment the page .C:c64d A5 2D LDA $2D ; Get the page pointer .C:c64f 49 7F EOR #$7F ; Exclusive-or it with 7F, to get a new encryption key, or $00 if we have reached page 7F and are done decrypting .C:c651 85 28 STA $28 ; Store it back to Register .C:c653 4C 18 C5 JMP $C518 ; And jump back to the P-machine ; Handle the AND instruction (called from an earlier JMP) .C:c656 B1 26 LDA ($26),Y ; Get the next byte of pcode .C:c658 C8 INY ; increment our index .C:c659 D0 02 BNE $C65D ; check if we have to increment the page .C:c65b E6 27 INC $27 ; increment it if we do .C:c65d 49 6B EOR #$6B ; decrypt it (are we bored with this block yet?) .C:c65f 25 28 AND $28 ; AND the value against Register .C:c661 85 28 STA $28 ; Write it back .C:c663 4C 18 C5 JMP $C518 ; And go back to the P-machine ; Handle the BNEQ instruction from the jump table .C:c666 20 57 C5 JSR $C557 ; Grab and decrypt a two byte operand to $22 .C:c669 A5 28 LDA $28 ; Get the current value of Register .C:c66b F0 03 BEQ $C670 ; If it's equal, branch ahead to jump to the P-machine .C:c66d 4C 4A C5 JMP $C54A ; If not equal, execute a PJMP to the address in $22 .C:c670 4C 18 C5 JMP $C518 ; Return the P-machine ; Handle the INC instruction from the jump table .C:c673 20 57 C5 JSR $C557 ; Read and decrypt a two byte operand to $22 .C:c676 A2 00 LDX #$00 ; Index to 0 .C:c678 A1 22 LDA ($22,X) ; Read the byte at the pointer we got from the P-code .C:c67a 18 CLC ; Prepare for an addition .C:c67b 69 01 ADC #$01 ; Add 1 .C:c67d 81 22 STA ($22,X) ; Store the incremented value back .C:c67f 85 28 STA $28 ; Store it to Register as well .C:c681 4C 18 C5 JMP $C518 ; And jump back to the P-machine ; Handle the ADD absolute from the jump table .C:c684 20 57 C5 JSR $C557 ; Read and decrypt a two byte operand to $22 .C:c687 A2 00 LDX #$00 ; Index to 0 .C:c689 A1 22 LDA ($22,X) ; Get the byte at the pointer from the P-code .C:c68b 18 CLC ; Prepare for an addition .C:c68c 65 28 ADC $28 ; Perform the addition with Register .C:c68e 85 28 STA $28 ; Store it back to Register .C:c690 4C 18 C5 JMP $C518 ; And jump back to the P-machine ; Handle the SUB (absolute) from the jump table .C:c693 20 57 C5 JSR $C557 ; Read and decrypt a two byte operand to $22 .C:c696 A2 00 LDX #$00 ; Index to 0 .C:c698 A5 28 LDA $28 ; Get Register .C:c69a 38 SEC ; Set up for a subtraction .C:c69b E1 22 SBC ($22,X) ; Subtract the value in memory .C:c69d 85 28 STA $28 ; And store to Register .C:c69f 4C 18 C5 JMP $C518 ; And return to the P-machine ; Handle the BPLU from the jump table .C:c6a2 20 57 C5 JSR $C557 ; Read and decrypt a two byte operand to $22 .C:c6a5 A5 28 LDA $28 ; Load the value of Register .C:c6a7 30 03 BMI $C6AC ; If it's negative, branch ahead to return to the P-machine .C:c6a9 4C 4A C5 JMP $C54A ; Perform a PJMP to the pointer we read .C:c6ac 4C 18 C5 JMP $C518 ; Return to the P-machine ; Handle LD16 .C:c6af 20 57 C5 JSR $C557 ; Read and decrypt a two byte operand to $22 .C:c6b2 A5 22 LDA $22 ; Grab the low byte .C:c6b4 8D C1 C6 STA $C6C1 ; And store it in the low byte .C:c6b7 A5 23 LDA $23 ; Grab the high byte .C:c6b9 8D C2 C6 STA $C6C2 ; And store it in the high byte .C:c6bc 4C 18 C5 JMP $C518 ; And jump back to the p-machine ; Draw ECA logo and set the screen mode up .C:c6e6 20 CA C7 JSR $C7CA .C:c6e9 AD 11 D0 LDA $D011 .C:c6ec 09 20 ORA #$20 .C:c6ee 8D 11 D0 STA $D011 .C:c6f1 AD 16 D0 LDA $D016 .C:c6f4 09 10 ORA #$10 .C:c6f6 8D 16 D0 STA $D016 .C:c6f9 AD 18 D0 LDA $D018 .C:c6fc 09 08 ORA #$08 .C:c6fe 8D 18 D0 STA $D018 .C:c701 AD 00 DD LDA $DD00 .C:c704 29 FC AND #$FC .C:c706 8D 00 DD STA $DD00 .C:c709 AD 02 DD LDA $DD02 .C:c70c 09 03 ORA #$03 .C:c70e 8D 02 DD STA $DD02 .C:c711 20 AB C7 JSR $C7AB .C:c714 A2 00 LDX #$00 .C:c716 8E E0 C7 STX $C7E0 .C:c719 A9 21 LDA #$21 .C:c71b 8D 4C C7 STA $C74C .C:c71e A9 C8 LDA #$C8 .C:c720 8D 4D C7 STA $C74D .C:c723 AE E0 C7 LDX $C7E0 .C:c726 BD E1 C7 LDA $C7E1,X .C:c729 18 CLC .C:c72a 69 20 ADC #$20 .C:c72c 8D 77 C7 STA $C777 .C:c72f 8D 7B C7 STA $C77B .C:c732 8D 86 C7 STA $C786 .C:c735 8D 8A C7 STA $C78A .C:c738 BD 01 C8 LDA $C801,X .C:c73b 69 E0 ADC #$E0 .C:c73d 8D 78 C7 STA $C778 .C:c740 8D 7C C7 STA $C77C .C:c743 8D 87 C7 STA $C787 .C:c746 8D 8B C7 STA $C78B .C:c749 A0 00 LDY #$00 .C:c74b AD 21 C8 LDA $C821 .C:c74e A2 08 LDX #$08 .C:c750 4A LSR A .C:c751 90 11 BCC $C764 .C:c753 38 SEC .C:c754 6E DE C7 ROR $C7DE .C:c757 6E DF C7 ROR $C7DF .C:c75a 38 SEC .C:c75b 6E DE C7 ROR $C7DE .C:c75e 6E DF C7 ROR $C7DF .C:c761 4C 70 C7 JMP $C770 .C:c764 4E DE C7 LSR $C7DE .C:c767 6E DF C7 ROR $C7DF .C:c76a 4E DE C7 LSR $C7DE .C:c76d 6E DF C7 ROR $C7DF .C:c770 CA DEX .C:c771 D0 DD BNE $C750 .C:c773 AD DE C7 LDA $C7DE .C:c776 99 00 E0 STA $E000,Y .C:c779 C8 INY .C:c77a 99 00 E0 STA $E000,Y .C:c77d 98 TYA .C:c77e 18 CLC .C:c77f 69 07 ADC #$07 .C:c781 A8 TAY .C:c782 AD DF C7 LDA $C7DF .C:c785 99 00 E0 STA $E000,Y .C:c788 C8 INY .C:c789 99 00 E0 STA $E000,Y .C:c78c EE 4C C7 INC $C74C .C:c78f D0 03 BNE $C794 .C:c791 EE 4D C7 INC $C74D .C:c794 98 TYA .C:c795 18 CLC .C:c796 69 07 ADC #$07 .C:c798 A8 TAY .C:c799 C0 00 CPY #$00 .C:c79b D0 AE BNE $C74B .C:c79d EE E0 C7 INC $C7E0 .C:c7a0 AE E0 C7 LDX $C7E0 .C:c7a3 E0 1C CPX #$1C .C:c7a5 F0 03 BEQ $C7AA .C:c7a7 4C 23 C7 JMP $C723 .C:c7aa 60 RTS .C:c7ab A9 00 LDA #$00 .C:c7ad 8D B8 C7 STA $C7B8 .C:c7b0 A9 E0 LDA #$E0 .C:c7b2 8D B9 C7 STA $C7B9 .C:c7b5 A9 00 LDA #$00 .C:c7b7 8D FF FF STA $FFFF .C:c7ba EE B8 C7 INC $C7B8 .C:c7bd D0 F8 BNE $C7B7 .C:c7bf EE B9 C7 INC $C7B9 .C:c7c2 AD B9 C7 LDA $C7B9 .C:c7c5 C9 00 CMP #$00 .C:c7c7 D0 EC BNE $C7B5 .C:c7c9 60 RTS .C:c7ca A9 10 LDA #$10 .C:c7cc A2 00 LDX #$00 .C:c7ce 9D 00 D8 STA $D800,X .C:c7d1 9D 00 D9 STA $D900,X .C:c7d4 9D 00 DA STA $DA00,X .C:c7d7 9D 00 DB STA $DB00,X .C:c7da CA DEX .C:c7db D0 F1 BNE $C7CE .C:c7dd 60 RTS ; Followed by the data for the ECA logo