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.