Simple Library Creation for the Amiga
Have you ever created a library for the Amiga? No? Reasonable, since it's a PITA. But sometimes you would like to have a shared library.
It happened then and now that it would have been comfortable if there was an easy way to create libraries for the Amiga. E.g.
- when I considered to support the ixemul libraries,
- when I ported my gcc to the Amiga, recognizing that a lot of code gets duplicated,
- when I attempted to port python3 to the Amiga,
- and last: when I wrote AmigaSSH, my SSH2 implementation for the Amiga.
AmigaSSH contains now several programs which are all using the same crypto methods. That's a perfect candidate for a shared library. But I don't want the hazzle with FD, SFD, prototypes, etc.p.p.
What do I want?
- just write normal code as usual
- simple command lines
- if possible use the same headers to create a library and to use it.
- provide a link library to utilize the shared library.
Some of these goals could be achieved with a normal library, but it's not code as usual when you have to name all those registers. You also have to maintain a FD/SFD file to maintain the order of the functions. It's not simple.
The idea and the plan
Libraries which utilize the A4 base register aren't new. Maybe my data layout goes a step further: I aligned the library base register A6 with the A4 register. To do so, the datasegment starts with the library structure:
struct Library __lib = {0};
And during library init, the library structure is copied into the datasegment. I also keep the pointer to the provided structure, since it's needed when the last instance gets closed.
Now each library instance needs an own data segment. And to supports trees of libraries, within a process there is one instance per library. The mechanism to duplicate the data segement is already there and used with resident programs. So I use the same mechanism: copy the data and apply the datadatarelocs.
All used functions are provided by a lookup per name. This also allows to implement dlfcn!
There is little more to do, like open/close counters but the most important thing is to set the A4 register properly:
LibInit and LibExpunge
Set a4 to the original data segment:
LibOpen
This is a bit mor tricky. First I do the same as in LibInit
But once the instance per process is created, I switch the data segment over to the library base in A6:
move.l a6,a4
add.l #32766,a4
LibClose
Switch the data segment over to the library base in A6:
move.l a6,a4
add.l #32766,a4
And this is also what I do for all other hard coded library functions... for the ONE other library function:
ResolveSymbols
This is the function which resolves the function names.
move.l a6,a4
add.l #32766,a4
The library functions
The stub
To add functions to a library, these needs to be compiled using -fbaserel to enable data access via a4. So each function will access the data segment for the calling process. To do so I create a stub with the same name which setups the A4 accordingly which works for most functions. That sub does
- pop and save the return value
- save the A4/A5 register
- load A4 register with library base + 32766
- load function ptr into A5
- call (A5)
- restore A4
- push the original return value
I'll create a stub file per function.
Limitations
- It can be used for all functions regardless of stack or register parameters, unless the registers A4/A5 are used, which should be rare.
- The implementation is not thread safe.
- callback functions accessing the data segment are a bit tricky, but feasable
- exporting variables is possible but needs special headers, since only the variable addresses are exported.
the init function
The init functions takes care to open the library and to initialize the function pointers. Since each function stub refers to the init function it is added automatically.
Simple Creation
To create such a simple library I added the bash script mkstub. There will be needs that are not covered by this script, take it as blueprint then.
The script takes the library name as first parameter and the object files do follow. So what does it do?
It filters the objects using m68k-amigaos-objdump -t to get all symbols, discards all non exported symbols, discards all starting with two underscores __
- an empty directory ${lib}-support. And there it creates
- the init-stub source file
- an export file
- a stub file
Then all these files are compiled and all files containg stub are put into the link library. The export files are added the library, which is not done in the script.
An Example
Let's write the library libfoo containing the file foo.c:
#include <amistdio.h>
int globalInt[] = {42};
char cccc[] = {'c'};
void foo() {
puts("hello from the library");
}
We compile it using
m68k-amigaos-gcc -noixemul -resident -Os -c foo.c -o foo.o
Then run mkstub
And we create the library:
m68k-amigaos-gcc -noixemul -resident -shared foo.o libfoo-support/export-libfoo.o -o libfoo.library -Wl,-ulibVersionMajor=1,-ulibVersionMinor=42
Note that also setting a different name is supported using `-ulibName=differentName` which gets extended with .library.
Now we need test programs
A test program
The test program looks like
#include <amistdio.h>
extern void foo();
extern int * globalInt;
extern char * cccc;
int main() {
foo();
printf("%ld %c\n", *globalInt, *cccc);
Flush(stdout);
return 0;
}
Note that the variable types differ. We must use pointers here!
Compile it:
m68k-amigaos-gcc -Os -noixemul test.c -o test -lfoo -L.
And run it :-)
A DLFCN program
This opens the library dynamically! Note the A4 handling before calling into that library!
#include <amistdio.h>
#include <dlfcn.h>
int main() {
void * handle = dlopen("libfoo.library", 0);
printf("got handle=%08lx\n", handle);
if (handle) {
void (*foo)() = (void (*)())dlsym(handle, "foo");
printf("got foo=%08lx\n", foo);
if (foo) {
register void * a4 asm("a4");
asm volatile("move.l %1,%0;\nadd.l #32766,a4" : "=r"(a4) : "r"(handle));
foo();
}
dlclose(handle);
}
}
Compile it
m68k-amigaos-gcc -noixemul usedlfcn.c -o usedlfcn
And run it :-)