APX Deep Blue C, where's my 8080?
Continuing our exploration of Atari 8-bit languages and virtual machines, we come to APX20166 Deep Blue C. Reverse engineering here is fairly simple as the entire source code for the runtime and compiler was published as an additional product, APX20179 Deep Blue Secrets. I'm going to only use that as a reference and proceed to produce a listing of the runtime via disassembly for a few reasons. First, it's always nice to have both the source code and the actual assembly output together. Second, the source code contains a fair amount of debug code we can ignore as it's not present in the shipped runtime. And lastly (but most importantly), by producing a listing from the object file we can spot if the provided source code actually matches the version in the product (it does, but with some quirks I'll discuss below).
The first thing to do is to get the runtime (DBC.OBJ) from the disk. It's a 2098 bytes in size, and like all Atari object files (OBX), contains the object data as a sequence of (start address, end address, <data>) bytes, with two $FFs at the beginning of the file. A long time ago, I wrote a tool to examine OBX files, and running that on this file reveals this one is more complicated than strictly necessary:
OFFSET: 0006 3000->30fb
OFFSET: 0106 30fc->31f7
OFFSET: 0206 31f8->32f3
OFFSET: 0306 32f4->33ef
OFFSET: 0406 33f0->34eb
OFFSET: 0506 34ec->35e7
OFFSET: 0606 35e8->36e3
OFFSET: 0706 36e4->37df
OFFSET: 0806 37e0->3805
OFFSET: 0830 02e0->02e1
Notice that each sequence is exactly 256 bytes long, with 252 bytes of data. This is a quirk of the Atari Macro Assembler, as that's the most data it will put in a section, even if the next section can be merged into it. I think I first noticed this when looking at Atari Pilot II. In any event, this makes disassembly directly from the file complicated, so I went ahead and merged all the adjacent sections together, producing a runtime that is smaller in size (2066 bytes), but otherwise equivalent.
Given the merged runtime, we can produce our usual reverse engineering tool reusing code I've published before on this blog. It is remarkably short, since it needs no special features, just the disassembler.
use strict; use m6502; sub open_lst { open my $fh, '<', 'dbc.lst' or die; $fh; } sub read_img { my ($addr, $size) = @_; read_img_core( $addr, $size, '../dbc.obj', [0x0006 - 0x3000, 0x3000, 0x3805], [0x0810 - 0x02e1, 0x02e1, 0x02e2]); } assem(@ARGV);
We start the listing with some constants we can pull from the Secrets source code.
=0001 REVNUM = 1 ; Revision # of c-code =00D0 PC = $D0 ; c-code program counter =00D2 SP = $D2 ; c-code stack pointer =00D4 P = $D4 ; Primary register =00D8 T = $D8 =00E0 ACC = $E0 ; BASIC USR RTN =00E2 MQ = $E2 ; MLTPLIER/QUOT =00E4 ENT = $E4 ; MCAND/DIVSOR =00E6 SC = $E6 ; SIGN CONTROL =00F0 UA = $F0 =00F2 UB = $F2 =00F4 UC = $F4
These are the registers the virtual machine use. The virtual machine has a program counter, a stack pointer and two 16-bit registers (or four 8-bit registers). As this product is derived from Ron Cain's Small-C, it is expected this should resemble an 8080 cpu. It must be true, it says so on Wikipedia ;-). As we'll see, there is some similarities but this virtual machine is by no means an 8080 emulator.
Producing the listing now is straightforward. There's a jump table at $3274 with entries for the 47 opcodes supported by the virtual machine (which are called simply P00P to P46P), and a few places with strings or data instead of code, but largely the 2K is made up of easy to recognize 6502 assembly code.
As always, the most exciting bit is to find the inner loop of the interpreter. It's called NEXT, and is more or less as expected.
3250: A0 00 NEXT LDY #0 3252: B1 D0 LDA (PC),Y 3254: E6 D0 INC PC 3256: D0 02 BNE NEXTR 3258: E6 D1 INC PC+1 325A: 0A NEXTR ASL A 325B: A8 TAY 325C: B9 74 32 LDA JUMPT,Y 325F: 85 D8 STA T 3261: B9 75 32 LDA JUMPT+1,Y 3264: 85 D9 STA T+1 3266: 6C D8 00 JMP (T)
It pulls the opcode from the PC, increments the PC, multiplies the opcode by 2 to lookup the opcode from the jump table, and then jumps to the opcode implementation. Each opcode then must end by calling JMP NEXT to continue the execution. This routine could have been made slightly faster if we only used even numbers for opcodes. It could also have been made a little faster by placing this loop in page zero and self-modifying the jump target, but overall this is probably only a few cycles longer than optimal.
The remaining code is largely unsurprising. The runtime starts with vectors for I/O support, strcpy, move, USR, find, PEEK and POKE, and curiously a SOUND vector. I'm not sure why a SOUND is part of the runtime but no graphics to speak of, but there must be a reason. There's a fair amount of code to implement 16-bit signed and unsigned multiply, divide and modulus which some of the opcodes use.
Now the runtime is just six bytes more than 2K in size. Surely there are six bytes to be saved in there which might allow some shifting of the locations to give the programmer an extra kilobyte? Well, here's the first quirk that comes from using the Atari Macro assembler. There are 12 bytes lost in the runtime due to the assembler choosing the absolute version of LDA or STA when a zero page version would have been fine. You can see this in P24P (at $3533), an opcode to multiply P by the top of the stack.
3533: A5 D4 P24P LDA P 3535: 8D E0 00 STA.W ACC 3538: A5 D5 LDA P+1 353A: 8D E1 00 STA.W ACC+1 353D: A0 00 LDY #0 353F: B1 D2 LDA (SP),Y 3541: 8D E2 00 STA.W MQ 3544: C8 INY 3545: B1 D2 LDA (SP),Y 3547: 8D E3 00 STA.W MQ+1 354A: 20 96 37 JSR SMUL 354D: AD E2 00 LDA.W MQ 3550: 85 D4 STA P 3552: AD E3 00 LDA.W MQ+1 3555: 85 D5 STA P+1 3557: 4C AB 33 JMP POPNEX
There are 6 opcodes here the assembler translated inefficiently. Similarly, P25P and P26P (divide and remainder) and the DIV subroutine also were assembled less than optimally.
An aside: These inefficiently implemented opcodes further undermine the argument that this is an 8080 emulator. The 8080 had no multiply or divide instructions. This runtime package implements the opcodes needed to support the compiler, so it implements some 8080-like opcodes plus some additional opcodes that the compiler requires. Calling it an 8080 emulator is a bit misleading.
We could save memory in a few other ways as well. For instance, there's a routine TRUE2, which is a jump trampoline because the opcodes that do tests (P36P through P45P) use up more than 128 bytes. The FALSE and TRUE routines could have been located in the middle of the tests instead of near the beginning to avoid needing the trampoline and another case of JMP FALSE, saving five bytes overall. There are also three places I noticed where JMP is used where a branch would have sufficed ($3223: BMI BEG; $34B7: BEQ P19W; $34DD: BEQ P18P), which would save three bytes. So it seems we could easily trim this down to fit within 2K.
Overall, this is a nicely designed very compact runtime. There's a lot to be learned from studying this code.
0 Comments
Recommended Comments
There are no comments to display.