Beneath Apple Manor
I was reading Dungeon Hacks, by David L. Craddock, the other day and found a short reference to the Atari 8-bit in the chapter on the first Rogue-like, Beneath Apple Manor:
QuoteFor Beneath Apple Manor Special Edition, I reverse engineered the 6502 runtime environment for the Galfo Integer BASIC compiler and ported it to 8086 machines (IBM PC). Bob Christiansen of Quality Software also ported it to the Atari. I did not write Ali Baba--Bob might have--but it was in integer BASIC too I think, so Quality used the Galfo compiler on it to port it between machines too. Bob must have given me a copy, which is why I had it.
So there's another virtual machine hiding inside this game, so I had to find it and figure out how it works.
The first step of course was to download the xex file from www.atarimania.com and hook up a my disassembler to the image with appropriate offsets. The image has a number of parts:
OFFSET: 0006 bf00->bf9f OFFSET: 00aa 02e2->02e3 OFFSET: 00b0 0c34->803f OFFSET: 74c0 8100->a7ff OFFSET: 9bc4 0110->0132 OFFSET: 9beb 02e0->02e1
Given this, my disassembler becomes:
use strict; use m6502; sub open_lst { open my $fh, '<', 'bam.lst' or die; $fh; } sub read_img { my ($addr, $size) = @_; read_img_core( $addr, $size, '../bam.xex', [0x0006 - 0xbf00, 0xbf00, 0xbf9f], [0x00aa - 0x02e2, 0x02e2, 0x02e3], [0x00b0 - 0x0c34, 0x0c34, 0x803f], [0x74c0 - 0x8100, 0x8100, 0xa7ff], [0x9bc4 - 0x0110, 0x0110, 0x0132], [0x9beb - 0x02e0, 0x02e0, 0x02e1]); } assem(@ARGV);
The first chunk at $BF00 sets of a simple display list for the load. The load into $02E2 invokes that code before continuing the rest of the image. The next two chunks consist of the main portion of the code, with a small gap that isn't used by the program. The last two chunks load a small init routine and invoke it.
Let's look at the init routine:
0110: A2 00 LDX #0 0112: BD 00 A0 LDA $A000,X 0115: 9D 34 04 STA $0434,X 0118: A9 00 LDA #0 011A: 9D 00 A0 STA $A000,X 011D: E8 INX 011E: D0 F2 BNE $0112 0120: EE 14 01 INC $0114 0123: EE 17 01 INC $0117 0126: EE 1C 01 INC $011C 0129: AD 1C 01 LDA $011C 012C: C9 A8 CMP #$A8 012E: D0 E0 BNE $0110 0130: 4C 34 04 JMP $0434
This piece of self-modifying code relocates the data at $A000 to $A7FF to $0434 to $0C33. So should add a relocation definition to the read_img routine: [0x74c0 - 0x8100 + 0xa000 - 0x434, 0x0434, 0x0c33].
Now let's look at $0434:
0434: A9 06 LDA #6 0436: 85 04 STA 4 0438: 85 05 STA 5 043A: A2 7F LDX #$7F 043C: BD 80 04 LDA $0480,X 043F: 95 80 STA $80,X 0441: CA DEX 0442: E0 FF CPX #$FF 0444: D0 F6 BNE $043C 0446: A9 20 LDA #<$8020 0448: 8D 30 02 STA SDLSTL 044B: A9 80 LDA #>$8020 044D: 8D 31 02 STA SDLSTL+1 ... 047D: 4C AF 00 JMP $00AF
Another relocation routine, this time relocating $480 to $4FF to $80 to $FF, so we add one more relocation element to read_img: [0x74c0 - 0x8100 + 0xa000 - 0x434 + 0x480 - 0x80, 0x80, 0xff].
If we disassemble $80 to $FF we see a lot of data, but there is some code hiding in there. Labeling $B0 as IP, it looks like this:
; pull IP from stack 009A: 68 PLA 009B: 85 B0 STA IP 009D: 68 PLA 009E: 85 B1 STA IP+1 ; increment IP by 1 00A0: E6 B0 INC IP 00A2: D0 0B BNE $00AF 00A4: E6 B1 INC IP+1 00A6: D0 07 BNE $00AF ; increment IP by A 00A8: 18 CLC 00A9: 65 B0 ADC IP 00AB: 85 B0 STA IP 00AD: B0 F5 BCS $00A4 ; read byte code and jump 00AF: AD 40 1D LDA $1D40 ; start at 1d40 00B2: 85 B5 STA $B5 00B4: 6C FA 63 JMP ($63FA)
This is a little virtual machine fetch loop, with page $63 containing the bytecode vectors (which point to code in pages 64 to 6F), and entry points here to increment the IP by 1 or A, or pull the IP-1 from the stack (implementing an RTS-like return). The machine even has the start address built in ($1D40). It patches the bytecode into the indirect jump address directly, so it only supports 128 even byte-codes. The Atari Pascal virtual machine I posted about recently supported 256, but at the cost of a more expensive fetch loop.
We can now write a bytecode disassembler by disassembling the codes, figuring out how long each is, and making a table of those lengths. My preliminary disassembler looks like this:
my $codeLen = [ 0, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 0x 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 2x 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 4x 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 6x 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, # 8x 0, 0, 0, 0, 0, 0, 3, 0, 0, 3, 0, 0, 0, 0, 0, 0, # ax 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # cx 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 2, 0, 0, 0, 0, # ex]; sub basic_buf { my ($buff, $addr, $size) = @_; for (my $i = 0; $i + 1 < $size; ) { my $val = unpack "C", substr($buff, $i, 1); my $len = $codeLen->[$val >> 1]; if ($len >= 1) { bytes_buf($len, substr($buff, $i, $len), $addr + $i, $len); $i += $len; } else { die sprintf "%02x", $val; } } } sub basic { my ($buff, $addr, $size) = read_img(@_); basic_buf($buff, $addr, $size); } sub main { return if assem(@_); if (0) { } else { basic(@_); } } main(@ARGV);
And my preliminary bytecode disassembly of the start address is:
1D40: F0 .BYTE $F0 1D41: 06 00 10 .BYTE $06,$00,$10 1D44: F0 .BYTE $F0 1D45: 0A 00 10 .BYTE $0A,$00,$10 1D48: F6 EA .BYTE $F6,$EA 1D4A: F6 12 .BYTE $F6,$12 1D4C: C8 .BYTE $C8 1D4D: 02 0A 71 .BYTE $02,$0A,$71 1D50: 9A .BYTE $9A 1D51: F0 .BYTE $F0 1D52: 08 12 10 .BYTE $08,$12,$10 1D55: F0 .BYTE $F0 1D56: 08 14 10 .BYTE $08,$14,$10 ...
For example, the most common bytecode here, F0, pushes 0 onto the stack.
Some other comments about the image:
- $81F0-$9FFF is the data for an ANTIC mode E picture, the title screen, with the display list at $8126. So 1/5 of the image is for this picture.
- $7C40-$8000 is the data for an ANTIC mode 2 (graphics 0) screen, with the display list at $7C20. The code modifies most of it to ANTIC mode 4 during the game for use as the main screen.
- A 1k character set is located at $0C00-$0CFF. The game doesn't just modify the Atari set.
- The game requires 48K to load, but likely runs in 32K. A little creativity (compressing the title screen and character set might be enough) could have squeezed it into 32K, but 48K was probably quite common when this hacked image was circulating.
I'll probably return to do more work on disassembling this, but I've established how the virtual machine works, so I'm happy enough for a few hours work.
P.S. I took a quick look at Ali Baba and didn't see the same virtual machine. I'll need to explore that more fully to determine if it has a similar architecture.
2 Comments
Recommended Comments