Inside vamos (Part 2)

Today we continue our little journey through my newest project and have a look at libraries and structures in the “virtual” Amiga RAM of vamos.

Make sure to read Part 1 of the series first…

Todays topics:

  • Amiga Libraries
  • Amiga Structures

1. Amiga Libraries

We already know from our first expirments that an Amiga library has a large jump table for all functions which we already use to trap the calls and redirect them to our Python vamos code.

The open question is now what functions does a library have and what registers are filled by the application before calling this function. Furthermore, we need to know what we have to return in the registers before returning to the caller. The last question is answered quickly: data registrer D0 is the return value of all lib calls (if the lib call isn’t void then no register contains a return value). So mapping the python trapped call return value to D0 was quickly done.

For the function description in a library Commodore invented a textual programming language neutral description called the FD files describing each function call and the arguments required along with the CPU registers where the argument will be found.

To simplify my work with vamos I first wrote a parser for the FD files (that can be found in the Amiga NDKs). This little tool is called fdtool and also available in my amitools source tree. You simply call it with a single FD file and it shows all functions and the relative jump offset in the table:

> ./fdtool NDK_3.1/Includes\&Libs/fd/exec_lib.fd 
NDK_3.1/Includes&Libs/fd/exec_lib.fd
 base: _SysBase
 #0001     30  0x001e                Supervisor [userFunction,a5]
 #0002     72  0x0048                  InitCode [startClass,d0][version,d1]
 #0003     78  0x004e                InitStruct [initTable,a1][memory,a2][size,d0]
 #0004     84  0x0054               MakeLibrary [funcInit,a0][structInit,a1][libInit,a2][dataSize,d0][segList,d1]
 #0005     90  0x005a             MakeFunctions [target,a0][functionArray,a1][funcDispBase,a2]
 #0006     96  0x0060              FindResident [name,a1]
 #0007    102  0x0066              InitResident [resident,a1][segList,d1]
 #0008    108  0x006c                     Alert [alertNum,d7]
 #0009    114  0x0072                     Debug [flags,d0]
 #0010    120  0x0078                   Disable 
 #0011    126  0x007e                    Enable 
 #0012    132  0x0084                    Forbid 
 #0013    138  0x008a                    Permit 
 #0014    144  0x0090                     SetSR [newSR,d0][mask,d1]
 #0015    150  0x0096                SuperState 
 #0016    156  0x009c                 UserState [sysStack,d0
..

Its already very useful to decode jumps into the lib (Note: the jump uses a negative offset while the tools displays positive ones).

The next step was to create a code generator that writes stubs for Python that describe all functions:

./fdtool -g NDK_3.1/Includes\&Libs/fd/exec_lib.fd 
 (30, 'Supervisor', (('userFunction', 'a5'),)),
 (36, 'execPrivate1', None),
 (42, 'execPrivate2', None),
 (48, 'execPrivate3', None),
 (54, 'execPrivate4', None),
 (60, 'execPrivate5', None),
 (66, 'execPrivate6', None),
 (72, 'InitCode', (('startClass', 'd0'), ('version', 'd1'))),
 (78, 'InitStruct', (('initTable', 'a1'), ('memory', 'a2'), ('size', 'd0'))),
 (84, 'MakeLibrary', (('funcInit', 'a0'), ('structInit', 'a1'), ('libInit', 'a2'), ('dataSize', 'd0'), ('segList', 'd1'))),
 (90, 'MakeFunctions', (('target', 'a0'), ('functionArray', 'a1'), ('funcDispBase', 'a2'))),
 (96, 'FindResident', (('name', 'a1'),)),
 (102, 'InitResident', (('resident', 'a1'), ('segList', 'd1'))),
 (108, 'Alert', (('alertNum', 'd7'),)),
 (114, 'Debug', (('flags', 'd0'),)),
 (120, 'Disable', None),
 (126, 'Enable', None),
 (132, 'Forbid', None),
 (138, 'Permit', None),
 (144, 'SetSR', (('newSR', 'd0'), ('mask', 'd1'))),
 (150, 'SuperState', None),

I simply copy & paste this output in my ExecLibrary class and this table now describes all functions available and also their parameters. This information is not used for trapping in the first place but for logging: Now I can decode each jump into the Lib and display the named function call…

19:39:29.159        lib:   INFO:  [    exec.library]  { CALL:  198 AllocMem( byteSize[d0]=def4, requirements[d1]=10001 ) from PC=00205a
19:39:29.159        lib:   INFO:  [    exec.library]  } END CALL: d0=000108ac
19:39:29.159        lib:   INFO:  [    exec.library]  { CALL:  732 StackSwap( newStack[a0]=113e8 ) from PC=0020e8
19:39:29.159        lib:   INFO:  [    exec.library]  } END CALL: d0=0000000

With this information in place I could add trapped functions by adding a new table to my Library object: this one maps the function offset (again positive) to an actual Python method inside the class. I can do this for only the functions I need – all others have a default handler that returns D0=0 and issues a warning trace that this function is still unimplemented.

An excerpt of the Python ExecLibrary class looks like this:

def __init__(self):
  exec_funcs = (
     (408, self.OldOpenLibrary),
     (414, self.CloseLibrary),
     ...
  )
  self.set_funcs(exec_funcs)

def OpenLibrary(self, lib, ctx):
  name_ptr = ctx.cpu.r_reg(REG_A1)
  ...
  return lib_addr

You see the mapping of Python functions in this example. Note that each Python call gets the same parameters and not the Amiga function calls directlly: It gets a context object and via this context the function can access the virtual CPU and read out the registers it needs…

The return value of the Python method will be directly written to D0 automatically. Returning None will not alter D0. This is useful for void function calls.

2. Amiga Structures

The library jump tables are not the only kind of data an Amiga application directly accesses outside its own memory segments: A lot of public data structures reside in memory and function calls often pass in pointers to them and also private ones. Some functions even require to create new structures and we then need to return their pointers. Furthermore, each library itself has (beside the jump table) a so called “pos size range” that contains data structures of “public” accessible information. E.g. exec.library has a large pos size with lots of system constants and also information like your own task structure…

Our task in vamos is now to correctly fill these structures as needed and provide a convinient mechanism in Python to access those data members. Unfortunately, those data structures are only defined in the language headers of Commodore’s NDKs and there is no generic description like there is the FDs for the calls 🙁

Without having a tool to decode some generic data structure definitions I started to convert the C Amiga headers manually into a Python structure definition. Those definition are not structures in Python itself but rather meta objects that describe a structure in the “virtual” Amiga Memory of vamos. This tedious process was only performed on demand, i.e. onyl the structures currently needed were transcribed to Pyhon.

Similar to fdtool I have written a typetool to provide a small command line utility that uses the structures defined in Python and displays them (This is again useful for disassembly reading as you can quickly look up and index and find the structure entry):

./typetool Library
 @0000        Library {
 @0000          Node {
0000 @0000/0000 +0004      Node*      ln_Succ               (ptr=True, sub=False)
0001 @0004/0004 +0004      Node*      ln_Pred               (ptr=True, sub=False)
0002 @0008/0008 +0001      UBYTE      ln_Type               (ptr=False, sub=False)
0003 @0009/0009 +0001      BYTE       ln_Pri                (ptr=False, sub=False)
0004 @0010/000a +0004      char*      ln_Name               (ptr=True, sub=False)
 @0014 =0014    } lib_Node
0005 @0014/000e +0001    UBYTE      lib_Flags             (ptr=False, sub=False)
0006 @0015/000f +0001    UBYTE      lib_pad               (ptr=False, sub=False)
0007 @0016/0010 +0002    UWORD      lib_NegSize           (ptr=False, sub=False)
0008 @0018/0012 +0002    UWORD      lib_PosSize           (ptr=False, sub=False)
0009 @0020/0014 +0002    UWORD      lib_Version           (ptr=False, sub=False)
0010 @0022/0016 +0002    UWORD      lib_Revision          (ptr=False, sub=False)
0011 @0024/0018 +0004    APTR       lib_IdString          (ptr=False, sub=False)
0012 @0028/001c +0004    ULONG      lib_Sum               (ptr=False, sub=False)
0013 @0032/0020 +0002    UWORD      lib_OpenCnt           (ptr=False, sub=False)
 @0034 =0034  } 

This query for the Library structure of Exec displays all entries including nested structures, their byte offset from the beginning, the C type and the name to access each entry.

This structure information is now used for two purposes in vamos: First it provides a convinient way to access structures inside memory for the Python lib calls when they have to access data that was provided by giving structure pointers. The second use of structures is to provide so called memory labels for logging. Every time vamos allocates a structure (e.g. ExecLib pos space) it also registers a struct memory label for the same address. A label manager keeps all registered labels and if memory tracing is enabled then you can look up an arbitrary address if it can be labelled. A structure label looks like this:

19:22:23.891        mem:   INFO:  R(4): 0102a8: 00000000  Struct  [@010210 +000098 ThisTask] Process+152 = pr_CurrentDir(BPTR)+0
19:22:23.892        mem:   INFO:  R(4): 0102bc: 00004070  Struct  [@010210 +0000ac ThisTask] Process+172 = pr_CLI(BPTR)+0
19:22:23.892        mem:   INFO:  R(4): 0102bc: 00004070  Struct  [@010210 +0000ac ThisTask] Process+172 = pr_CLI(BPTR)+0
19:22:23.892        mem:   INFO:  R(4): 0101d0: 00004080  Struct  [@0101c0 +000010 CLI] CLI+16 = cli_CommandName(BSTR)+0

So a read access to memory address 0x0102a8 is detected as structure access to ThisTask.pr_CurrentDir. That’s what I call comfortable debugging 😉  Note that the labelling look ups cost quite a lot of performance and that’s the reason why you have to enable memory tracing with a command line switch (-t/-T).

The most common use of Structures is to decode lib call arguments. In Python I provide a so called AccessStruct object that assist reading/writing structures:

def StackSwap(self, lib, ctx):
 stsw_ptr = ctx.cpu.r_reg(REG_A0)
 stsw = AccessStruct(ctx.mem, StackSwapDef, struct_addr=stsw_ptr)
 # get new stack values
 new_lower = stsw.r_s('stk_Lower')
 new_upper = stsw.r_s('stk_Upper')
 new_pointer = stsw.r_s('stk_Pointer')

This is an excerpt of the Python implemenation of the Exec StackSwap call: In A0 a pointer to a StackSwap structure is passed in. We have already declared the function in Python as a defintion (hence the trailing Def):

class StackSwapStruct(AmigaStruct):
 _name = "StackSwap"
 _format = [
 ('APTR', 'stk_Lower'),
 ('ULONG', 'stk_Upper'),
 ('APTR', 'stk_Pointer')
 ]
StackSwapDef = StackSwapStruct()

With a memory pointer and a structure definition we can create an access object. Now the reads and writes to the entries of the structure a simple calls can be performed with the entry name of the structure… Ok its slower than direct offset handling but far more confortable and less error prone…

Beside the structure access vamos also provides a more generic memory access object that allows to read and write blocks of data, reads c-type strings and bcpl strings with a single call:

def OpenLibrary(self, lib, ctx):
 ver = ctx.cpu.r_reg(REG_D0)
 name_ptr = ctx.cpu.r_reg(REG_A1)
 name = ctx.mem.access.r_cstr(name_ptr)
 ..

That’s it for today… I hope you got more insight on the Library and Struct handling and the confortable features available in vamos for working with them.

Leave a Reply