Close

20241113 - An Embedded P-Code-esque Virtual Machine?

A project log for ROM Disassembly - Cefucom-21

Peering into the soul of this obscure machine

ziggurat29ziggurat29 2 days ago0 Comments

As mentioned before there are a lot of tables, and even a couple of tables of tables.  Some are lists of 'described text' (having a header indicating position), some are indexed dispatch tables, some are double-dispatch, and the others are presently unknown.

I had previously thought the table-of-tables form was a double-dispatch mechanism, but it's not.  Rather it's a list
Often they are processed by RST 10, which delegates processing of the elements to RST 8.  The elements are variably-sized blocks.  They seem to have the structure:

uint8_t     fxn;
uint16le_t  param1;
uint16le_t  param2;
...         payload;

(I am coining the term 'uint16le_t' because I have found many places that are big-endian! So elsewhere I use 'uint16be_t' for that.)

Often the blocks are short, like this:

7817 62      byte_7817:db 62h
7818 95 7A   dw unk_7A95
781A 11 00   dw 11h
781C 00      db 0

but others can be quite long.  I originally thought param1 is a pointer, and params is a length, because the RST 8 code that processed them immediately loads param1 into HL and param2 into DE.  Indeed sometimes those are used as pointers and lengths, but in other cases they are not.  But for starters I look at the ones at off_7803, which is a list of these things which have just one element in them, and seem to follow the (ptr,length) assumption.  The trailing zero is interesting.  All the ptrs and lengths worked out sanely when updating the the disassembly. 

These blocks are ultimately processed by RST 10.  This is a block sequencer, feeding individual blocks to RST 8 (which expects the block pointer in IX).  A null function code terminates processing, so that explains the trailing 'db 0' above.  So there is no explicit payload length of a block; it depends on function code.

RST 8 dispatches servicing through dispatch_4002, which has 128(!) entries.  I went through and labelled each of them like 'fxn00'.  Many of them are apparently unimplemented as their slot directs to 'fxn00', which was found to be used as the block sequence terminator.  It does have an implementation, though, which is to back up IX by 4.  This is interesting because fxn00 will not be dispatched by RST 10, but it would for the ostensibly unimplemented function codes.  Turns out that RST is optimistically loading param1 and param2 and incrementing IX just past.  So the IX-=4 is to undo that optimistic loading and continue at the byte following the unimplemented function code.  So 'ignore unimplemented function code'.

In the end, there are 76 implemented and 52 unimplemented functions.

After labelling, I looked at fxn62h, since that was the the code used in the blocks above.  The implementation boggled my mind a bit.  It did some sort of queueing into c200, which is structured as 32 8-byte entries.  A gave up with that and scrolled through the nearby disassembly casually and found fxn63 just below it, that also did something similar with c200, and then invoked my buddy RST 10 -- the 'block list dispatcher'.  So if fxn62 puts it in, and fxn63 takes it out and processes it, this seems evocative of a 'load' and 'run' functionality.  Anyway, there was still too many unknowns so I started to look for smaller fish to fry.

Scrolling through I found some shorter ones that I could comprehend, and coincidentally these tended to be the smaller numbered function codes.  The first one I found was:

43FD  ; XXX 3f: dispatch to param1 (no param2)
43FD  fxn3f_43FD: 
43FD DD 2B  dec     ix  ; (no param2)
43FF DD 2B  dec     ix
4401 E9     jp  (hl)    ; thunk over

So this would run arbitrary external code.

Another is a conditional 'goto' of sorts, where the block processing is directed to another place (hopefully within the list!) if --*((uint8_t*)param2) != 0:

43D3  ; XXX 3c: goto block @ param1
43D3  fxn3c_43D3:
43D3 EB     ex  de, hl
43D4 35     dec     (hl)
43D5 28 03  jr  z, leave_43DA   ; leave if --*((uint8_t*)param2) == 0
43D7 D5     push    de
43D8 DD E1  pop     ix
43DA  leave_43DA:
43DA C9     ret

So this seems to be implementing a form of 'next' using an implicit down counter.  Similarly, there is an 'if':

43B2  ; XXX 3a: if ( *((uint8_t*)param2) ), goto param1
43B2  fxn3a_43B2:
43B2 1A     ld  a, (de)
43B3 B7     or  a
43B4 28 03  jr  z, leave_43B9
43B6 E5     push    hl
43B7 DD E1  pop     ix
43B9  leave_43B9:
43B9 C9     ret

So these 'blocks' seem to suggest being a 'byte code' of sorts for a virtual machine.

It's worth noting also that most values referenced within this machine are treated big-endian; e.g.:

434D  ; XXX 2d(4): *(uint16be_t*)param1 <<= param2.l
434D  fxn2d_434D:
434D C5  push    bc       ; save
434E 46  ld      b, (hl)    ; high byte first!
434F 23  inc     hl
4350 4E  ld      c, (hl)    ; then low byte!
...

However, param1 and param2 are host cpu native, i.e. little-endian.  I add the notation 'param2.l' to make it clearer which byte in a neutral way.

Along the way I also found and, or, xor.  But then I needed to call it a night, but lot's more to do in this area.  Since the Cefucom seems to embed scripts for this thing, it may be useful for me to make a disassembler for it.  This will be a little challenge since the 'compiled' scripts have absolute memory references, but I can probably manage.  It will take some effort, but it might be worth it for more rapidly understanding what's going on.

It was a thing back then to use 'p-code' as a means of achieving code density.  This is not p-code per se, because it's not (p)ortable at all.  It has absolute address references and is mixed-endian.  But it seems to be in that spirit.

A non-important observation is that all this code is located from 3f00-4dff. (there is a couple dead bytes at the beginning and many at the end).  So it feels like a distinct 'component' of sorts.  3840 bytes for this 'virtual machine'.

Discussions