Jump to content





Beneath Apple Manor

Posted by Atari_Ace, 12 March 2019 · 69 views

Virtual Machine

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:
 

For 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-1 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.
 

Attached Files






I've read Craddock's "Break Out ..." book, but not "Dungeon Hacks". Adding it to my To Read list. I wrote an article in Old School Gamer magazine about Ali Baba beginning on page 30ish.

  • Report

FYI, Dungeon Hacks, Expanded Edition is in the current StoryBundle (Spring Fired Up at storybundle.com).  It includes Dungeon Hacks, One-Week Dungeons, Anything But Sports: The Making of FTL: Faster Than Light and Red to Black: The Making of Rogue Legacy.  There are a number of other books in the bundle as well that are interesting.

  • Report

March 2019

S M T W T F S
     12
3456789
10111213141516
17181920212223
24 25 2627282930
31      

Categories