Home > CBM > Disk > Fast Load > Microprose 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This fast-loader was used at least in Gunship and Pirates! For the longest time, I believed this was an exclusive to Microprose Software. However I later saw it used in some Accolade titles. I'm thinking this might be the scheme known as RapidLoc (or Rapid-Lock), but I'm not sure. Interestingly, almost every file on disk points to the same 'corrupt' file chain! You see, the first sector of these files has a next-sector pointer which is invalid (for example track 37 sector 21). That first sector is the start of an 'auto-run' file (discussed below). The actual start of the file is hidden (and scrambled) in the directory entry. That is, this fast-loader scheme uses a custom directory structure so is not suitable for a general-purpose fast-loader (without some modification). Let's take a look at a hex-dump of such a directory (from Card Sharks): >8:0600 00 ff 82 12 12 43 4f 50 .....COP >8:0608 59 52 49 47 48 54 20 31 YRIGHT 1 >8:0610 39 38 37 a0 a0 00 00 00 987..... >8:0618 b2 0d 00 88 04 00 21 00 ......!. The 00 ff is not part of the directory entry (ignore it). Next is an 82 which indicates a properly-closed PRG file. Next is the starting track and sector of the file 12 12 = track 18 sector 18. The first sector of the file is located on the directory track, which is unusual since files normally start anywhere except the directory track! It also should be the last sector (18) on the directory track (but custom formats can reorder sectors). All 'fast-load' files in this scheme 'start' at track 18 sector 18. One nice feature is this can be loaded quickly since the drive-head doesn't need to move to another track. Next is the filename 50 59 52 49 47 48 54 20 31 39 38 37 A0 A0. Following that is 3 zero bytes (reserved for REL files). Following that should be 6 zero bytes (normally unused, except for save-with-replace), but here we see mysterious B2 0D 00 88 04 00. This indirectly indicates where the real file begins (we'll come back to this). The entry ends with bytes 21 00 which indicate a file size of 33 blocks.
Normally a program is loaded in BASIC using LOAD"*",8 and after loading is complete READY will appear and the user normally enters the comman RUN. That process loads the file data at the "start of BASIC" (which $801 on the C64). However, by using LOAD"*",8,1 the file will be loaded to the address specified at the beginning of the file. This is normally used to load non-BASIC data (such as graphic images or machine-language code). Normally, LOAD"*",8,1 doesn't do anything special, but if the "right" data gets loaded into the "right" memory used by the KERNAL or CPU, we can trick the operating system into running code without user interaction -- much like a computer virus. In summary, for this kind of file the user only need type LOAD"*",8,1 and the computer will automatically RUN the code even if it's not BASIC code. This fast-loader scheme loads at $2ED; $2ED~2FF is normally unused by C64 so no problem. But then (starting at $300) comes the BASIC/KERNAL vectors table. This scheme over-writes most of those vectors with "junk" (except IRQ and NMI vectors are valid). Importantly, the KERNAL Stop routine vector is over-written with address $2ED (perhaps not surprising). At this point of LOAD only part of the sector has been transferred to the host computer (presumably a C64). Let's see what this code does: ;note .Y = 0 .C:02ed 84 08 STY $08 ;decrypt low base .C:02ef 8C 15 D0 STY $D015 ;turn off sprites .C:02f2 A0 F2 LDY #$F2 ;index 242 (counter -14) .C:02f4 C6 AF DEC $AF ; 3 -> 2 .C:02f6 A5 AF LDA $AF .C:02f8 85 C4 STA $C4 ;2 (pointer1 high) .C:02fa 85 09 STA $09 ;pointer 2 high .C:02fc 45 AE EOR $AE .C:02fe 85 C3 STA $C3 ;$28 (pointer1 low) .C:0300 49 9E EOR #$9E .C:0302 85 AE STA $AE ;$B6 (pointer2 low) ;decrypt loop .C:0304 B1 C3 LDA ($C3),Y ;read ($228+F2+ = $31A+) .C:0306 51 08 EOR ($08),Y ;decrypt .C:0308 91 C3 STA ($C3),Y ;save .C:030a C8 INY ;index next and count down .C:030b D0 F7 BNE $0304 ;not all 14 bytes, loop So basically it disables sprites (which can interfere with timing) and decrypts part of the data that has already been loaded ($31A~327). This is known as code obfusication. The code continues by loading Y with # byte pairs to resume loading from the first sector. The data being loaded consists of two streams! The second stream has been encrypted so we must decrypt it too. Also the data is being loaded 'backwards', that is top-down into memory. If this isn't obfusication, I don't know what is!.C:030d A0 59 LDY #$59 ;90 bytes to load per stream; $b4 (180) bytes total ;loader loop 1 .C:030f 88 DEY ;count down, are we finished? .C:0310 30 A4 BMI $02B6 ;yes, exit .C:0312 10 06 BPL $031A ;not yet ~ .C:031a 20 A5 FF JSR $FFA5 ;KERNAL Acceptor: read byte from disk .C:031d 91 AE STA ($AE),Y ;save stream 1 ($2B6~30e) .C:031f 20 A5 FF JSR $FFA5 ;read another byte from disk .C:0322 51 AE EOR ($AE),Y ;decrypt .C:0324 91 08 STA ($08),Y ;save stream 2 ($200~258) .C:0326 50 E7 BVC $030F ;always loop After the two code streams have been loaded, execution jumps to $2B6. Note 241 (of 254) bytes have been read from the first sector -- and that's all we'll get from the "loader stub". Anyway, let's see what's at $2B6: .C:02b6 A0 00 LDY #$00 ;start of strings .C:02b8 A5 BA LDA $BA ;disk unit (normally 8) .C:02ba 20 B1 FF JSR $FFB1 ;KERNAL LISTEN .C:02bd A9 FF LDA #$FF ;secondary address 15 (command channel) .C:02bf 20 93 FF JSR $FF93 ;KERNAL SECOND ;sender loop .C:02c2 B9 EF 02 LDA $02EF,Y ;read data string .C:02c5 20 A8 FF JSR $FFA8 ;KERNAL serial out .C:02c8 C8 INY ;index next character in string .C:02c9 C0 1B CPY #$1B ;end 1st string? .C:02cb F0 04 BEQ $02D1 ;yes .C:02cd C0 20 CPY #$20 ;end 2nd string? .C:02cf 90 F1 BCC $02C2 ;no, sender loop ;end of string .C:02d1 20 AE FF JSR $FFAE ;KERNAL UNLISTEN (device will process command) .C:02d4 C0 20 CPY #$20 ;end 2nd string? .C:02d6 90 E0 BCC $02B8 ;no, sender loop ~ >C:02ef 4d 2d 57 b5 00 15 58 49 M-W...XI >C:02f7 36 85 06 b1 05 59 17 04 6....Y.. >C:02ff 91 05 88 10 f6 6c 05 00 .....l.. >C:0307 9b 36 ca 4d 2d 45 b0 07 .6.M-E.. So the code sends two instructions to the disk command channel. The first writes a code snippet to drive zero page RAM (more on this later). The second instruction tells the drive to execute code stored in RAM address $7b0. But what's in that RAM? The C1541 and C1571 store BAM (side 1) in page 7 (memory $700~7FF). The C1541/71 always load the BAM whenever a disk swap is detected (or the first disk access after power-up). This information comes from track 18 sector 0 (the so-called disk header). On a single-sided disk, the bytes $7b0~7ff are normally unused (except GEOS). So the disk drive has secretly already loaded code into its memory before the user LOADs his/her first program! Let's take a look at this drive code: .8:07b0 78 SEI ;disable interrupts .8:07b1 AD 90 02 LDA $0290 ;directory sector .8:07b4 A2 05 LDX #$05 ;index controller sector# .8:07b6 D5 08 CMP $08,X ;X is correct buffer? .8:07b8 F0 04 BEQ $07BE ;yes .8:07ba CA DEX ;no, next sector# .8:07bb CA DEX .8:07bc 10 F8 BPL $07B6 ;'always' -- what if buffer 0 = $300 ??? .8:07be 8A TXA ;buffer# *2 .8:07bf 69 99 ADC #$99 ;calc ZP address ($9B, $9D or $9F) .8:07c1 8D CF 07 STA $07CF ;set ZP address (self-modifying code) What the code does is find which buffer ($400, $500, or $600) holds the directory listing. Inerestingly (bug?) it doesn't check buffer $300. Once it's found, it sets a ZeroPage (ZP) pointer into its own code. Next we copy five bytes from the directory entry into ZP location $77~7B. These are the last five mystery bytes from the directory entry we saw above at the beginning of this article. (As far as I can tell, the first three mystery bytes are junk = red herring.): .8:07c4 A2 04 LDX #$04 ;#bytes-1 to copy (i.e. 5 bytes) .8:07c6 A0 1A LDY #$1A ;index directory entry .8:07c8 8C 07 1C STY $1C07 ;timer high-byte (why?) .8:07cb 8C 00 18 STY $1800 ;pull CLK and DATA serial-bus lines low ;save loop .8:07ce B1 9F LDA ($9F),Y ;read mystery directory byte .8:07d0 95 77 STA $77,X ;save .8:07d2 88 DEY .8:07d3 CA DEX .8:07d4 10 F8 BPL $07CE ;loop until done With the mystery directory bytes stashed away in zero page, the code continues by setting up the track/sector for all five buffers! That's 1.25K of code if your counting: .8:07d6 A5 22 LDA $22 ;current track (18) .8:07d8 A2 08 LDX #$08 ;last buffer's sector index .8:07da A0 03 LDY #$03 ;starting sector ;setup loop .8:07dc 94 07 STY $07,X ;set sector# for buffer .8:07de 95 06 STA $06,X ;set track# (18) for buffer .8:07e0 C8 INY ;sector interleave of 3 .8:07e1 C8 INY .8:07e2 C8 INY .8:07e3 CA DEX ;prior buffer (track) .8:07e4 CA DEX ;prior buffer (sector) .8:07e5 10 F5 BPL $07DC ;not all buffers, setup loop If you find that code confusing, maybe this will help:
Now that the track and sector has been set for the five buffers, it's time to tell the disk-drive controller to read those sectors into its buffers! Well, all buffers except the $700 buffer (where our code is executing): .8:07e7 A2 03 LDX #$03 ;4 buffers to read ;buffer loop (retry error) .8:07e9 A0 80 LDY #$80 ;controller code for 'Read Sector' .8:07eb 94 00 STY $00,X ;set command for controller .8:07ed 58 CLI ;enable interrupts (allow controller to do its job) ;wait loop .8:07ee B5 00 LDA $00,X ;check controller status .8:07f0 30 FC BMI $07EE ;still running, wait loop .8:07f2 4A LSR A ;test result .8:07f3 D0 F4 BNE $07E9 ;retry if error .8:07f5 A5 3A LDA $3A ;this doesn't belong in the loop! (.A = $35) .8:07f7 CA DEX ;next buffer control .8:07f8 10 EF BPL $07E9 ;not all buffers, loop ; .8:07fa 78 SEI ;disable interrupts .8:07fb 84 04 STY $04 ;set 'Read Sector' for buffer $700 .8:07fd 4C 06 02 JMP $0206 ;continue C64 pre-load If you've been counting, the disk drive has now loaded 6 sectors (not counting directory nor BAM), and we haven't even moved the disk-head to the starting sector! In other words, this 'fast-loader' has a lot of overhead for each file! There are better fast-loaders, but also there are much worse ones! So I consider this scheme mediocre. Remember the code snippet the C64 wrote to drive-RAM zero page? Well a copy of that code is in the drive's "parser buffer" at $200. More precisely at $206~217. In other words the "Memory-Write" to zero-page was another form of code obfusication. We'll actually execute code at $206 (and not at $00b5). Anyway, let's look at the drive code at $206: .8:0206 58 CLI ;enable interrupts; the controller can now load buffer $700 .8:0207 49 36 EOR #$36 ; make .A = $03 .8:0209 85 06 STA $06 ;pointer high-byte (i.e., buffer $300) ;loop (note we begin with .Y = $80) .8:020b B1 05 LDA ($05),Y ;read buffer $380- .8:020d 59 17 04 EOR $0417,Y ;decrypt .8:0210 91 05 STA ($05),Y ;fix code at $380- .8:0212 88 DEY ;previous byte .8:0213 10 F6 BPL $020B ;not all, loop .8:0215 6C 05 00 JMP ($0005) ;jump to $300 So what it does is allow buffer $700 to load (the CLI instruction), and it also decrypts the code at $300~380. Then it jumps to the code at $300: .8:0300 A2 80 LDX #$80 ;controller code to 'Read Sector' .8:0302 D0 02 BNE $0306 ;always ;error loop .8:0304 86 04 STX $04 ;set command for controller ;wait loop .8:0306 58 CLI ;allow controller to run (superfluous) .8:0307 A5 04 LDA $04 ;get status of buffer $700 .8:0309 30 FB BMI $0306 ;still running, wait loop .8:030b 4A LSR A ;test result .8:030c D0 F6 BNE $0304 ;loop if error That $300 code begins by simply waiting for buffer $700 to be loaded without error. The next part of the code erases the "$206 code" from both the parser buffer ($200 region) and from zero page. It must be a big a secret if they're going through the trouble to erase it twice! Anyway the code isn't complex: .8:030e 78 SEI .8:030f 85 24 STA $24 ;zero (why?) .8:0311 A2 11 LDX #$11 ;17+1 bytes to erase ;erase loop .8:0313 95 B5 STA $B5,X ;scrub zero page .8:0315 9D 08 02 STA $0208,X ;scrub parser buffer (trivia: code bytes $206 and $207 remain!) .8:0318 CA DEX ;prior byte .8:0319 10 F8 BPL $0313 ;loop until done Next, the first two of the five mystery bytes in zero page are decrypted and stored into $d6 and $d7: .8:031b A2 01 LDX #$01 ;2 bytes to decrypt .8:031d A5 C7 LDA $C7 ; $9b .8:031f 4A LSR A ; $4d .8:0320 85 0B STA $0B ;decryption count for $0420 .8:0322 AC 94 02 LDY $0294 ;directory index ($02, $22, $42, $62, $82, $a2, $c2 or $e2) ;decrypt mystery loop .8:0325 B5 77 LDA $77,X ;get encrypted data .8:0327 59 17 07 EOR $0717,Y ;decrypt .8:032a 95 D6 STA $D6,X ;set for loader .8:032c 88 DEY ;next decrypt index .8:032d CA DEX ;count down .8:032e 10 F5 BPL $0325 ;not both track and sector, loop In case it isn't obvious, the decryption key varies based on which directory entry is accessed. For example, if the directory entry is the first in a sector, the decrypt index would be $02. If the file is the second entry in a sector, the decrypt index would be $22. Let's not worry about that! The import thing is the decrypted data was stored into $d6 and $d7. Anyway, the high-bit of the last decrypted byte is used to toggle bit 4 of byte $3B5 (why?): .8:0330 29 80 AND #$80 ;isolate bit 7 .8:0332 4A LSR A ;shift to bit 6 .8:0333 4A LSR A ;shift to bit 5 .8:0334 4A LSR A ;shift to bit 4 .8:0335 4D B5 03 EOR $03B5 ;toggle bit .8:0338 8D B5 03 STA $03B5 ;update code Do you remember how the drive code at $300 to $380 was decrypted? Well what about the rest of the drive code; is it good to run? No! Most of it is also encrypted, and with a much stronger '32-bit encryption' scheme. The following code decrypts $39c to $7ff: .8:033b A2 03 LDX #$03 ; 4 buffers (part $300, all $400, $500 and $600) .8:033d 86 31 STX $31 ; transfer poiner (used later) .8:033f E8 INX ; make that 5 buffers ($700 too) .8:0340 A0 9C LDY #$9C ; index for buffer $300 (don't over-write ourself!) ;decrypt loop .8:0342 B1 05 LDA ($05),Y ; read encrypted data .8:0344 45 C7 EOR $C7 ; decrypt .8:0346 91 05 STA ($05),Y ; save fixed code .8:0348 66 24 ROR $24 ; get a bit .8:034a 26 C9 ROL $C9 ;32-bit shift left .8:034c 26 C8 ROL $C8 .8:034e 26 C7 ROL $C7 .8:0350 26 24 ROL $24 .8:0352 C8 INY ;index next byte in buffer .8:0353 D0 ED BNE $0342 ;not all of buffer, loop .8:0355 E6 06 INC $06 ;next buffer (high byte) .8:0357 CA DEX ;count buffers .8:0358 10 E8 BPL $0342 ;not all buffers, loop Next the drive code clears about half of page two ($22A~29E) and also erases the decryption code we just used ($2F3~$367). They don't want us pesky hackers to know what's happening! It also initializes $b5 and $b6 (for buffer transfer of C64 code) .8:035a 86 C7 STX $C7 ; .X = $ff .8:035c 86 C8 STX $C8 .8:035e 86 C9 STX $C9 .8:0360 A0 74 LDY #$74 ; $75 (117) bytes to erase .8:0362 84 B5 STY $B5 ;(actual transfer low-byte count... used later) .8:0364 A9 01 LDA #$01 .8:0366 85 B6 STA $B6 ;(mangled transfer high-byte count... used later; count = $074) .8:0368 4A LSR A ; .A = 0 ;erase loop .8:0369 99 F3 02 STA $02F3,Y ;scrub $2F3~367 .8:036c 99 2A 02 STA $022A,Y ;scrub $22A~29E (why?) .8:036f 88 DEY ;prior byte .8:0370 10 F7 BPL $0369 ;not all erased, loop Up to this point, data communication between the host computer (presumably C64) and disk drive have been using standard (slow) serial communication. Now we come to the first part of true fast-loading. Here we transfer more code to the C64 because that first sector wasn't enough! (Later the $625 code is used to transfer actual file data.) .8:0372 A0 8C LDY #$8C ;index in buffer $300 .8:0374 20 25 06 JSR $0625 ;fast transfer to C64 ($38C~3FF) ~ .8:0625 A9 D0 LDA #$D0 ;command code to execute buffer .8:0627 85 01 STA $01 ;exec buffer $400 (only execute if transfer takes too long) .8:0629 8D 05 1C STA $1C05 ;hardware timer set .8:062c 58 CLI ;enable interrupts (if transfer goes well, no IRQ will happen) ;xfer loop .8:062d B1 30 LDA ($30),Y ;read data .8:062f AA TAX ;save (low nibble) .8:0630 4A LSR A ;shift high nibble down .8:0631 4A LSR A .8:0632 4A LSR A .8:0633 4A LSR A .8:0634 48 PHA ;save shifted high nibble .8:0635 A9 02 LDA #$02 ;serial bus DATA low, CLK high .8:0637 8D 00 18 STA $1800 ;tell C64 byte is ready to xfer .8:063a 8A TXA ;low nibble of data .8:063b AE 00 18 LDX $1800 ;current serial bus state ;wait for C64 part 1 .8:063e EC 00 18 CPX $1800 ;test serial bus .8:0641 F0 FB BEQ $063E ;wait for C64 ready .8:0643 29 0F AND #$0F ;mask low nibble ;wait for C64 part 2 .8:0645 EC 00 18 CPX $1800 ;test serial bus .8:0648 D0 FB BNE $0645 ;wait for C64 ready ;here we go! .8:064a 8D 00 18 STA $1800 ;put 2 bits on the serial bus (bits 3 and 1) .8:064d 0A ASL A ;shift bits 2 and 0 up (to bits 3 and 1) .8:064e 29 0A AND #$0A ;clear high nibble .8:0650 8D 00 18 STA $1800 ;put 2 bits on the serial bus .8:0653 68 PLA ;high nibble (shifted down) .8:0654 8D 00 18 STA $1800 ;put 2 bits on the serial bus .8:0657 0A ASL A ;shift bits 2 and 0 up .8:0658 29 0A AND #$0A ;clear high nibble .8:065a 8D 00 18 STA $1800 ;put 2 bits on the serial bus .8:065d A9 0A LDA #$0A ;value for CLK and DATA both high .8:065f C6 B5 DEC $B5 ;count bytes in stream (low) .8:0661 8D 00 18 STA $1800 ;signal 'not ready' to C64 .8:0664 D0 04 BNE $066A ;not stream end, index next byte .8:0666 C6 B6 DEC $B6 ;maybe stream end, test high byte .8:0668 F0 2B BEQ $0695 ;exit (end of stream) ;index next byte .8:066a C8 INY .8:066b D0 C0 BNE $062D ;not all bytes in buffer, xfer loop ;switch buffer (not used for C64 code transfer) .8:066d A9 03 LDA #$03 ;last buffer high-byte .8:066f C5 31 CMP $31 ;did we just do last buffer? .8:0671 85 31 STA $31 ;(set high byte) .8:0673 D0 B8 BNE $062D ;no, loop to do last buffer now .8:0675 18 CLC ;yes, flag buffer transfer complete (stream not finished) .8:0676 78 SEI .8:0677 60 RTS ~end of streeam (end of C64 code transfer) .8:0695 38 SEC ;flag end-of-stream .8:0696 78 SEI .8:0697 60 RTS
The last we saw of the C64 was when it sent Memory-Execute command to the disk drive. Let's see what's happened since: .C:02d8 78 SEI ;disable interrupts .C:02d9 18 CLC .C:02da AD 00 DD LDA $DD00 ;get serial bus bits .C:02dd 48 PHA ;waste time .C:02de 68 PLA .C:02df 30 F9 BMI $02DA ;wait for C1541/71 code start .C:02e1 BA TSX ;get stack pointer .C:02e2 E8 INX ;skip over return address .C:02e3 E8 INX ;(from JSR KERNAL_Stop) .C:02e4 86 C4 STX $C4 ;store fixed stack pointer .C:02e6 A4 AE LDY $AE ;index for loader ($b6) .C:02e8 86 C3 STX $C3 ;store SP again ?! .C:02ea A9 91 LDA #$91 ;opcode for STA (zp),Y .C:02ec 20 00 02 JSR $0200 ;call fast load That looks pretty simple, but it isn't going to load file data yet. Oh no, there is more code to load into $2B6~$329. Anyway, let's look at the C64 side of the fast-loader: .C:0200 8D 41 02 STA $0241 ;set opcode after read byte (RTS or STA (zp),Y) .C:0203 A2 00 LDX #$00 ;zero buffer poiner low (.Y is the low-byte index) .C:0205 86 AE STX $AE .C:0207 CA DEX ;-1... if no bytes xfer then INX = 0 ;loader loop (numbers in [brackets] are instruction cycle times) .C:0208 A9 10 LDA #$10 ;[2]CLK low (DATA high) ;wait for C1541 .C:020a 0D 00 DD ORA $DD00 ;[4]merge serial line status .C:020d 30 3C BMI $024B ;[2]DATA high, exit! .C:020f C9 40 CMP #$40 ;[2]test CLK input .C:0211 90 F7 BCC $020A ;[2]currently low, wait for C1541 ;the C1541 let the clock line go high ;now we(C64) set the CLK line back to low ;which signals we are (almost) ready to receive a byte .C:0213 8D 00 DD STA $DD00 ;[4]pull CLK low ;the C1541 knows we're ready, but it can't transmit ;while we're holding CLK low... ;Critically we must avoid bad lines, so test the VIC chip... .C:0216 29 03 AND #$03 ;[2]keep VIC bank (and clear User-Port bit) .C:0218 AA TAX ;[2]save low bits in .X ;wait for VIC (note carry is set) .C:0219 AD 12 D0 LDA $D012 ;[4]get VIC raster .C:021c E9 32 SBC #$32 ;[2]test with top-of-screen .C:021e 90 04 BCC $0224 ;[2]no bad lines in border, skip ahead .C:0220 29 07 AND #$07 ;[2]mask raster-in-char .C:0222 F0 F5 BEQ $0219 ;[2]last raster-in-char, wait for VIC ;here we go! .C:0224 8E 00 DD STX $DD00 ;[4]allow both CLK and DATA to go high (we're REALLY ready now) .C:0227 8E 40 02 STX $0240 ;[4]save low bits for correction (self-modifying code) .C:022a EA NOP ;[2]waste 4 cycles .C:022b EA NOP ;[2] .C:022c 8A TXA ;[2]zero upper bits .C:022d 0D 00 DD ORA $DD00 ;[4]read two bits (7 and 6) .C:0230 4A LSR A ;[2]shift down two bits .C:0231 4A LSR A ;[2] .C:0232 0D 00 DD ORA $DD00 ;[4]read two bits .C:0235 4A LSR A ;[2]shift down two bits .C:0236 4A LSR A ;[2] .C:0237 0D 00 DD ORA $DD00 ;[4]read two bits .C:023a 4A LSR A ;[2]shift down two bits .C:023b 4A LSR A ;[2] .C:023c 4D 00 DD EOR $DD00 ;[4]read upper two bits and scramble lower two bits .C:023f 49 FF EOR #$FF ;[2]unscramble lower two bits (value set by code above: 0,1,2 or 3) .C:0241 91 AE STA ($AE),Y ;[6]store byte in RAM .C:0243 C8 INY ;[2]next byte in page .C:0244 D0 C2 BNE $0208 ;[3]no overflow, loop .C:0246 E6 AF INC $AF ;next page .C:0248 4C 08 02 JMP $0208 ;loop ;DATA high exit .C:024b 84 AE STY $AE ;save low byte of end-load-address .C:024d 29 40 AND #$40 ;test serial CLK status .C:024f F0 02 BEQ $0253 ;low okay, skip ahead .C:0251 A9 88 LDA #$88 ;mangled error code .C:0253 0A ASL A ;carry set if error, clear if none .C:0254 85 90 STA $90 ;set result code (0=okay, $10=error) .C:0256 60 RTS That's a nice fast-load routine. It takes 57 cycles (less than 1 raster) to transfer and store a byte (assuming drive is ready, no VIC bad-line, and VIC not in border). Just remember the first time that subroutine is called we're not loading file data, but loading 116 ($74) more bytes of fast-loader code (includes default KERNAL vectors). When we RTS above, we'll be returning to the interior of that freshly loaded code. Let's take a peek: ;post-stream loop .C:02ef 2C 00 DD BIT $DD00 ;test serial lines .C:02f2 70 C2 BVS $02B6 ;CLK high, exit loader .C:02f4 30 F9 BMI $02EF ;DATA high, wait ;load new stream .C:02f6 68 PLA ;discard old return address .C:02f7 68 PLA .C:02f8 A0 04 LDY #$04 ;get 4 bytes = return/execute address and load address ;stream header loop .C:02fa A9 60 LDA #$60 ;RTS opcode (fetch just one byte) .C:02fc 20 00 02 JSR $0200 ;fast load (byte) .C:02ff E8 INX ;any data? .C:0300 F0 B4 BEQ $02B6 ;no, exit loader .C:0302 48 PHA ;save on stack .C:0303 88 DEY ;count, all four bytes? .C:0304 D0 F4 BNE $02FA ;no, stream header loop ;get load address off stack .C:0306 68 PLA ;high byte .C:0307 85 AF STA $AF ;poiner high .C:0309 68 PLA ;low byte .C:030a A8 TAY ;set index (pointer low = 0) .C:030b A9 91 LDA #$91 ;opcode for STA (zp),Y .C:030d 20 00 02 JSR $0200 ;fast load byte stream .C:0310 4C EF 02 JMP $02EF ;post-stream loop Microprose's loader scheme uses custom file data. A single file can consist of multiple streams. Each stream begins with a 7-byte header: (mangled) 2-byte stream length, last stream flag, 2-byte execute (return) address and 2-byte load address. The stream length and last-stream flag bytes are not transmitted to the C64 (as far as the computer knows, each stream has a 4-byte header). Also the drive code doesn't care about the load and execute addresses -- as far as the drive is concerned, a stream header is only 3 bytes. (The 4-byte portion of the header is included in the stream length.) Here's an example:
This is a bit wasteful as the execute/return address is discarded for all but the final stream. The important thing is now (finally!) the C64 is ready to receive real file data. However, the drive code still needs to do its (main) copy-protection code. .8:0377 A9 08 LDA #$08 ;force CLK low (allow DATA high) .8:0379 8D 00 18 STA $1800 ;update serial bus lines (tell C64 to wait for stream header) .8:037c 4C 86 07 JMP $0786 ;do copy protection (?) ~ .8:0786 78 SEI .8:0787 A6 0B LDX $0B ;($4d) 78 bytes to decrypt .8:0789 4A LSR A ;.A = 4 .8:078a A8 TAY .8:078b 20 7F 03 JSR $037F ~ .8:037f 3E 20 04 ROL $0420,X ;decryptor buffer .8:0382 CA DEX ;count down .8:0383 10 FA BPL $037F ;not all, loop .8:0385 CA DEX .8:0386 CA DEX .8:0387 86 79 STX $79 ;.X = $fd (-3 for stream header) .8:0389 60 RTS ~ .8:078e 49 0A EOR #$0A ;.A = $0E .8:0790 AA TAX .8:0791 B1 38 LDA ($38),Y ;$38 -> $807; missing RAM (get .A = 8) .8:0793 10 01 BPL $0796 ;always? .8:0795 E8 INX ;.A = $0F .8:0796 86 13 STX $13 .8:0798 8E 00 18 STX $1800 ;set CLK and DATA low (prep to send stream header) .8:079b A9 88 LDA #$88 .8:079d 8D A9 02 STA $02A9 .8:07a0 A9 09 LDA #$09 .8:07a2 85 C6 STA $C6 .8:07a4 A9 80 LDA #$80 .8:07a6 85 11 STA $11 .8:07a8 85 0A STA $0A ;flag stream header at beginning of file Remember the mystery directory bytes that we 'decrypted' and stored at $d6, $d7? Well, it's a mangled track/sector pair. Specifically
;decode track# .8:07aa A5 D6 LDA $D6 ;mangled track# .8:07ac 29 3F AND #$3F ;clear high two bits .8:07ae 18 CLC .8:07af 69 13 ADC #$13 ;add 19 .8:07b1 C9 24 CMP #$24 ;test limit (track 36) .8:07b3 90 08 BCC $07BD ;less okay, skip ahead .8:07b5 E9 24 SBC #$24 ;sub 36 .8:07b7 85 C2 STA $C2 ;save delta (possibly zero) .8:07b9 A9 11 LDA #$11 ;track 17 .8:07bb E5 C2 SBC $C2 ;subtract delta .8:07bd 85 C2 STA $C2 ;destination track of file The drive code then continues with: .8:07bf 18 CLC .8:07c0 A5 16 LDA $16 ;sector's disk ID1 .8:07c2 65 17 ADC $17 ;sector's disk ID2 .8:07c4 AA TAX .8:07c5 4D 51 01 EOR $0151 ;initially zero .8:07c8 4D 52 01 EOR $0152 ;initially zero .8:07cb 8E 51 01 STX $0151 ;update ?? .8:07ce 84 16 STY $16 ;4 (buffer high byte) .8:07d0 84 D4 STY $D4 ;4 .8:07d2 20 58 05 JSR $0558 ;do main (copy protection, file load) ~ .8:0558 49 6B EOR #$6B .8:055a 48 PHA ;$ea first sector .8:055b 20 B6 06 JSR $06B6 ;step head in (bigger track#; reads $152,x into $0e) .8:055e 20 2A 05 JSR $052A .8:0561 68 PLA .8:0562 F0 23 BEQ $0587 Let's look at the code at $052A. I don't understand it's purpose, but what it does is calculate a value in $c5 before VIA timer expires. If $c5 underflows before the timer, then the code will attempt to reposition the drive head by half-track steps. .8:052a A9 18 LDA #$18 ;24 initial value .8:052c 85 C5 STA $C5 ;calculate $c5 loop .8:052e A2 1D LDX #$1D ;initial X value .8:0530 20 33 07 JSR $0733 ;set X counter after sync .8:0533 B0 17 BCS $054C ;no Sync, skip ahead .8:0535 49 83 EOR #$83 ;.A = $85 .8:0537 38 SEC .8:0538 8D 05 1C STA $1C05 ;set timer .8:053b 8A TXA ;counter for next-byte ;disk-head byte wait loop .8:053c AC 05 1C LDY $1C05 ;test H/W timer .8:053f 10 11 BPL $0552 ;timeout! .8:0541 50 F9 BVC $053C ;wait for head read byte .8:0543 E5 BD SBC $BD ;counter -delta .8:0545 B8 CLV ;ready for next head byte .8:0546 B0 F4 BCS $053C ;counter underflow? no, byte wait loop .8:0548 C6 C5 DEC $C5 ;all attemps done? .8:054a D0 E2 BNE $052E ;no, find calc $c5 loop .8:054c 20 04 06 JSR $0604 ;reposition head by half-step .8:054f 90 D9 BCC $052A ;not out of error-steps, start over .8:0551 60 RTS ;$c5 has been set .8:0552 88 DEY ;$7e .8:0553 84 17 STY $17 ;address low for $400 buffer .8:0555 4C 64 07 JMP $0764 ;finish routine ~head on new track, update timer .8:0764 38 SEC .8:0765 E5 1D SBC $1D .8:0767 85 11 STA $11 ;timer constant for $6f3 delay routine ;head on new track, set magic byte $0e .8:0769 A6 22 LDX $22 ;track# .8:076b E0 13 CPX #$13 ;19 or more? .8:076d 90 02 BCC $0771 ;no skip ahead .8:076f CA DEX ;X-=2 .8:0770 CA DEX .8:0771 BD 52 01 LDA $0152,X ;magic table .8:0774 85 0E STA $0E ;magic byte .8:0776 A9 EE LDA #$EE .8:0778 8D 0C 1C STA $1C0C ;request byte-ready (set overflow) .8:077b 18 CLC .8:077c 60 RTS I'm not sure what the magic table is used for (besides updating magic byte $0e per track). Less importantly there is a custom delay routine (via X register) at $6f3: .8:06f3 86 10 STX $10 .8:06f5 A6 11 LDX $11 ;'constant' (like $72=114) set by $764 new track routine .8:06f7 CA DEX .8:06f8 D0 FD BNE $06F7 .8:06fa C6 10 DEC $10 .8:06fc D0 F7 BNE $06F5 .8:06fe 18 CLC .8:06ff 60 RTS More importantly, let's go back and look at the code at $564: .8:0564 20 B2 06 JSR $06B2 ;que $700 buffer to execute; step in one track .8:0567 AD 00 1C LDA $1C00 .8:056a 49 60 EOR #$60 ;toggle bit-rate .8:056c 20 E8 06 JSR $06E8 ;update bit-rate, update head on new track (magic $0e byte) .8:056f 20 0D 05 JSR $050D ;wait for sync-mark, count up X into $150 .8:0572 50 FE BVC $0572 ;wait for byte after sync .8:0574 A0 FD LDY #$FD ;253 byte limit limit .8:0576 8C 05 1C STY $1C05 ;set hardware timer ;count byte loop .8:0579 58 CLI .8:057a A6 04 LDX $04 .8:057c 10 09 BPL $0587 ;buffer code $700 executed, exit .8:057e 50 F9 BVC $0579 ;wait for normal byte received (sync bytes don't update this) .8:0580 78 SEI .8:0581 B8 CLV ;ready for next byte .8:0582 88 DEY ;byte limit reached? .8:0583 D0 F4 BNE $0579 ;no, count loop .8:0585 F0 DD BEQ $0564 ;yes, try next greater track (step in) What that code appears to do is search for a track that has a really long sync-mark(s). In other words, no sequence of normal bytes greater than 253. (A normal sector contains way more than 253 bytes). On the disks I've tested, this special track is always on track 36, although that's not required. Interestingly, if no track has the special "lots of sync marks" signature then the disk-head will eventually jam against the track-stop next to the spindle motor. On the C1541 and C1571 disk drives I've owned, this causes the disk-head to get 'permanently' stuck: no disk commands can move the head back to a normal track! In other words, the device must be opened and the disk-head manually repositioned before it can be used again. Anyway, when the special track is found, the hardware timer triggers an IRQ at which point the code in buffer $700 executes: ;do main copy protection (read magic table $152) .8:0700 78 SEI ;superflous (we're in an interrupt) .8:0701 A9 09 LDA #$09 ;result code .8:0703 85 04 STA $04 ;update controller code (this breaks the $564 search loop) .8:0705 85 C6 STA $C6 .8:0707 A9 DB LDA #$DB .8:0709 85 BB STA $BB .8:070b A9 4E LDA #$4E ;bit-rate %10 .8:070d 20 E8 06 JSR $06E8 ;update bit-rate, update head on new track (magic $0e byte) .8:0710 20 89 04 JSR $0489 ;read-in magic $152 table .8:0713 B0 6E BCS $0783 ;error decoding, terminate loader! .8:0715 46 01 LSR $01 ;clear 'buffer-execute' command for buffer $400 .8:0717 60 RTS What that code does is either fill $152~174 with 'magic bytes' that correspond to tracks 1~35 (less track 18 and 19), or aborts the load on failure. Whenever the disk head moves, byte $0e is updated based on that table. I still haven't figured out what byte $0e does. Anyway, let's see what happens after the magic table has been read. Spoiler: it's the main load-and-transfer routine. .8:0587 78 SEI .8:0588 A9 D0 LDA #$D0 ;'execute buffer' command .8:058a 85 01 STA $01 ;for buffer $400 -- only if things go horribly wrong .8:058c 20 5D 07 JSR $075D ;update $11, update $0e from magic table .8:058f A5 D7 LDA $D7 ;starting sector# for file .8:0591 85 23 STA $23 ;current sector# to read .8:0593 A6 C2 LDX $C2 ;track# for file .8:0595 24 ;skip next opcode .8:0596 E8 INX ;next greater track (inward) .8:0597 24 ;skip next opcode .8:0598 CA DEX ;next lower track (outward) .8:0599 86 C2 STX $C2 ;save (new) track# for file .8:059b 20 B9 06 JSR $06B9 ;move head to new track (multi step possible) ;read file from current track .8:059e A9 09 LDA #$09 .8:05a0 85 C6 STA $C6 .8:05a2 85 E7 STA $E7 .8:05a4 20 22 04 JSR $0422 ;find sector (# in $23) and read into $47e~4ff and $300~3ff .8:05a7 B0 59 BCS $0602 ;couldn't read sector (after multiple tries), exit .8:05a9 A6 7B LDX $7B ;4 .8:05ab 06 0A ASL $0A .8:05ad B0 06 BCS $05B5 ;begin w/stream header ;.A is normally zero .8:05af A6 16 LDX $16 ;4 .8:05b1 65 15 ADC $15 ;+$88 typical .8:05b3 85 7A STA $7A ;set initial Y index ;read stream header into $b5~b7 if needed .8:05b5 86 31 STX $31 ;set pointer high-byte ; .8:05b7 A4 7A LDY $7A ;initial value typically $88 (136) .8:05b9 A6 79 LDX $79 ;initial value typically $fd (-3) or 0 .8:05bb 10 15 BPL $05D2 ;no/end stream header, skip ahead .8:05bd B1 30 LDA ($30),Y ;read buffer .8:05bf 95 B8 STA $B8,X ;stream header info ($b5~b7) .8:05c1 E8 INX .8:05c2 86 79 STX $79 .8:05c4 C8 INY .8:05c5 84 7A STY $7A .8:05c7 D0 EE BNE $05B7 ;typically .8:05c9 A6 16 LDX $16 ;4 .8:05cb E4 31 CPX $31 .8:05cd D0 08 BNE $05D7 ;stream header crosses sector boundry .8:05cf CA DEX ;3 .8:05d0 D0 E3 BNE $05B5 ;always ; .8:05d2 20 25 06 JSR $0625 ;transfer data to C64 .8:05d5 B0 12 BCS $05E9 ;end of file stream .8:05d7 A5 23 LDA $23 ;next sector on same track? .8:05d9 D0 C3 BNE $059E ;yes, loop .8:05db A6 C2 LDX $C2 ;file track# .8:05dd E0 13 CPX #$13 ;19 or more? .8:05df 90 B7 BCC $0598 ;no, decrement track# (step out) .8:05e1 E0 23 CPX #$23 ;less than 35? .8:05e3 90 B1 BCC $0596 ;yes, increment track# (step in) .8:05e5 A2 11 LDX #$11 ;no, go to track #17 .8:05e7 D0 B0 BNE $0599 ;always ;finished file stream (check for another) .8:05e9 A9 08 LDA #$08 ;pull CLK low (allow DATA high) .8:05eb 8D 00 18 STA $1800 ;update serial lines .8:05ee A2 12 LDX #$12 ;wait counter .8:05f0 CA DEX .8:05f1 D0 FD BNE $05F0 .8:05f3 A5 B7 LDA $B7 ;last stream? .8:05f5 D0 0B BNE $0602 ;yes .8:05f7 A9 0A LDA #$0A ;pull CLK and DATA low .8:05f9 8D 00 18 STA $1800 ;update serial lines (signal new stream for C64) .8:05fc A9 FD LDA #$FD ;reset index (-3) for new stream header .8:05fe 85 79 STA $79 .8:0600 D0 C2 BNE $05C4 ;always ;sector not found or last stream done .8:0602 38 SEC .8:0603 60 RTS If you study the main loop above, you should see a large part (almost 50%?) deals with managing the stream-header (which is a small part of the total data). This is because the stream-header can be split across either a page boundary or a sector boundry. Before moving on to see what happens after all data has been loaded, lets have a detailed look at Read Sector ($422): ;normal error .8:0415 C6 BE DEC $BE ;count retries on track .8:0417 10 0D BPL $0426 ;okay, keep going .8:0419 C6 E7 DEC $E7 ;count retries off-track .8:041b 30 E3 BMI $0400 ;out of attemps, abort load .8:041d 20 4C 05 JSR $054C ;reposition head by half-track .8:0420 B0 DE BCS $0400 ;out of attemps, abort load ;entry point for Read Sector .8:0422 A9 24 LDA #$24 ;36 tries to find sector on track .8:0424 85 BE STA $BE .8:0426 A5 16 LDA $16 ;4 .8:0428 85 31 STA $31 ;high-byte pointer .8:042a 20 78 06 JSR $0678 ;extended wait for Sync Mark, get .Y = 0 .8:042d B0 EA BCS $0419 ;error, no sync found .8:042f A4 17 LDY $17 ;$7e initial index for page $400 .8:0431 A9 6B LDA #$6B ;desired byte (sector start flag) .8:0433 50 FE BVC $0433 ;wait for first byte after sync .8:0435 B8 CLV ;ready for next byte .8:0436 4D 01 1C EOR $1C01 ;test byte from disk-head .8:0439 D0 DA BNE $0415 ;mismatch error ;main sector-read loop .8:043b 50 FE BVC $043B ;wait for data byte .8:043d B8 CLV ;ready for next byte .8:043e AD 01 1C LDA $1C01 ;get data byte .8:0441 AA TAX ;save a copy .8:0442 29 49 AND #$49 ;magic mask .8:0444 85 29 STA $29 ;save addend 1 .8:0446 8A TXA ;data byte .8:0447 29 24 AND #$24 ;magic mask .8:0449 85 2A STA $2A ;save addend 2 .8:044b A9 B6 LDA #$B6 ;magic mask .8:044d 50 FE BVC $044D ;wait for data byte .8:044f 2D 01 1C AND $1C01 ;get and mask .8:0452 65 29 ADC $29 ;addend 1 (clear overflow) .8:0454 91 30 STA ($30),Y ;save decoded byte 1 .8:0456 45 D5 EOR $D5 ;update checksum .8:0458 85 D5 STA $D5 ;store it .8:045a C8 INY ;index next (odd) data byte .8:045b A9 DB LDA #$DB ;magic mask .8:045d 50 FE BVC $045D ;wait for data byte .8:045f 2D 01 1C AND $1C01 ;get and mask .8:0462 65 2A ADC $2A ;addend 2 (clear overflow) .8:0464 91 30 STA ($30),Y ;save decoded byte 2 .8:0466 45 D5 EOR $D5 ;update checksum .8:0468 85 D5 STA $D5 ;store it .8:046a C8 INY ;index next (even) data byte .8:046b D0 CE BNE $043B ;same page of RAM, loop .8:046d A2 03 LDX #$03 ;last page of RAM used .8:046f E4 31 CPX $31 ;was that the last page? .8:0471 B8 CLV ;ready for next byte .8:0472 86 31 STX $31 ;update page-pointer .8:0474 AE 01 1C LDX $1C01 ;get dummy byte .8:0477 90 C2 BCC $043B ;not last page, loop .8:0479 18 CLC ;flag no error (assume) .8:047a A8 TAY ;test checksum .8:047b D0 98 BNE $0415 ;error, try starting again ;this part ($47e~4ff) comes from the sector! ;only executed if the checksum is valid .8:047d A6 EE LDX $EE ;get dummy byte ($EE is the sector checksum) .8:047f A9 05 LDA #$05 ;this is sector 5 .8:0481 45 23 EOR $23 ;test with desired sector in file chain .8:0483 D0 90 BNE $0415 ;wrong sector, try starting again .8:0485 E6 23 INC $23 ;calc next sector# in file chain .8:0487 60 RTS If you study that code, you should see that this Microprose disk-scheme uses a custom sector format. In particular it uses a 3-byte to 2-byte decoding scheme (24-bit to 16-bit), which has a 50% overhead. Compare this with a standard sector decoding which is 5-byte to 4-byte (40-bit to 32-bit) which has just a 25% overhead. In other words, this encoding scheme has twice as much overhead! On the other hand, decoding is simple and quick (standard decoding is quite a mess and slow). To help make up for the reduced encoding efficiency, sectors are larger than standard. Instead of 260 ($104) decoded bytes, this scheme produces 386 ($182) decoded bytes per sector. However, not all 386 bytes are file data. One byte is a checksum and typically 9 bytes are 6502-code used to identify the current sector and set the next sector. In other words, 376 bytes are typically file data (could be less depending on how big the loaded code is). Interstingly, the start of the sector includes machine-language code which over-writes the end of the Read_Sector routine! Note this loaded code is not executed unless the sector checksum is valid. Because the sectors are bigger in this disk-scheme, there are fewer sectors per track. As far as I can tell, tracks 1~17 have 12 sectors (0 to 11) while tracks 19~35 have 11 sectors (0 to 10). Hopefully this table will help:
After all streams are transferred, we return to code at $7d5. Which is basically just clean-up code. It erases most RAM, but in particular the magic table at $152~175 is left untouched. It also moves the disk-head back to track 18 (to read BAM/directory again): .8:07d5 A9 12 LDA #$12 ;18 .8:07d7 78 SEI .8:07d8 85 13 STA $13 ;??? .8:07da 20 BA 06 JSR $06BA ;move disk-head to track .A .8:07dd A9 00 LDA #$00 .8:07df AA TAX ;zero almost all RAM .8:07e0 95 00 STA $00,X ;clear zero page .8:07e2 9D 00 03 STA $0300,X ;all of page $300 .8:07e5 9D 15 04 STA $0415,X ;most of page $400 (don't erase $400 stub) .8:07e8 9D 15 05 STA $0515,X ;all of page $500 .8:07eb 9D 15 06 STA $0615,X ;all of page $600 .8:07ee 9D E0 06 STA $06E0,X ;most of page $700 (don't erase ourself!) .8:07f1 E8 INX .8:07f2 D0 EC BNE $07E0 ; .8:07f4 A9 D0 LDA #$D0 ;command code for "execute buffer" .8:07f6 85 01 STA $01 ;code for buffer $400 .8:07f8 A9 2C LDA #$2C ;opcode BIT (don't JMP) .8:07fa 8D 00 04 STA $0400 ;modify code .8:07fd 4C 22 EB JMP $EB22 ;ROM initialize zero-page After the ROM initializes zero-page RAM, the controller will execute the following code: .8:0400 2C D5 07 BIT $07D5 ;don't JMP $7D5 (aka, NOP) .8:0403 A9 C6 LDA #$C6 ;disk motor on, drive light off .8:0405 8D 00 1C STA $1C00 ;update hardware .8:0408 46 01 LSR $01 ;'erase' controller code for this buffer .8:040a E6 3E INC $3E ; 0 -> 1; one active drive .8:040c 20 05 D0 JSR $D005 ;read BAM into RAM .8:040f A2 45 LDX #$45 ;reset CPU stack .8:0411 9A TXS .8:0412 4C C5 EB JMP $EBC5 ;enter command-wait loop And that's the end of the drive code -- yeah! But the C64 needs to finsh too. Let's take a look: .C:02b6 A0 58 LDY #$58 .C:02b8 2C ;skip next opcode .C:02b9 E6 09 INC $09 ;next page of RAM .C:02bb 91 08 STA ($08),Y ;erase $201~258 (or EVERYTHING if error) .C:02bd 88 DEY .C:02be D0 FB BNE $02BB .C:02c0 A6 9D LDX $9D ;show KERNAL messages (like LOADING)? .C:02c2 F0 02 BEQ $02C6 ;no .C:02c4 B0 F3 BCS $02B9 ;error, erase more RAM ($300+) ;this code gets called regardless of $9d .C:02c6 20 53 E4 JSR $E453 ;restore BASIC vectors ($300~30b) .C:02c9 A2 47 LDX #$47 .C:02cb 8E 18 03 STX $0318 ;change NMI low byte .C:02ce A6 AE LDX $AE ;load YX with load end address .C:02d0 A4 AF LDY $AF .C:02d2 A5 9D LDA $9D ;show KERNAL messages? .C:02d4 F0 04 BEQ $02DA ;no, skip ahead .C:02d6 86 2D STX $2D ;save end address into BASIC VarTab .C:02d8 84 2E STY $2E .C:02da 90 03 BCC $02DF ;no error exit .C:02dc A6 C4 LDX $C4 ;error -- restore stack pointer .C:02de 9A TXS .C:02df 58 CLI .C:02e0 60 RTS If the loaded file is a BASIC program, the return address is typically set to $2e1: .C:02e1 A2 02 LDX #$02 ;set BASIC TxtPtr to $2EC .C:02e3 86 7B STX $7B .C:02e5 A2 EC LDX #$EC .C:02e7 86 7A STX $7A .C:02e9 58 CLI ;enable interrupts .C:02ea 6C 08 03 JMP ($0308) ;IGONE -- execute BASIC code (reads from $2ED) .C:02ed 8A TXA ;BASIC token for RUN .C:02ee 00 BRK ;BASIC end-of-line
© Hydrophilic.net, 2026 |