The Shared C Library initialisation process is somewhat mysterious and no obvious documentation seems to exist on the web to explain it. This article is an attempt to demystify the startup process and should prove useful to anyone interested in RISC OS C compilers, their assembly output, or any kind of emulation or debugging of C Library behaviour.
Initialisation occurs in three stages:
The instruction blocks for the SharedCLibrary are divided into "chunks" - there are two chunks, one for "kernel" functions and one for ANSI C functions. On entry to this SWI, R0 points to a set of 20-byte chunk descriptors as follows:
+0 Chunk ID (1 = kernel, 2 = ANSI, -1 = no more) +4 Memory block start +8 Memory block end +12 Static workspace (for ANSI chunk only) +16 Reserved
The SWI is then responsible for populating the memory from the block start to the block end with all the instructions necessary to execute each C library function. Each instruction is executed with the registers already set up according to appropriate ARM procedure call standard and R14 containing the return address as usual.
In ROBE, the branch mechanism is virtualised. To understand how this works, it is necessary to understand how ROBE represents executable code in memory: code is represented as a HashMap of blocks. Each block has an address and can be executed independently, returning control to the caller when the program counter (in R15) is modified. When blocks are generated in the usual way from ARM code, they have the address of the code they represent and if the blocks were placed sequentially then the difference in their addresses would be equal to the number of ARM instructions represented by the blocks.
When a call is made to the SharedCLibrary SWI calls in ROBE, they do not actually populate any data in the image of the application. Instead, they map a set of code blocks at the appropriate addresses for each branch instruction. Each code block implements the ANSI C call in native Java code and so may correspond to hundreds of ARM instructions. However, the length of the block as far as the software is concerned is one instruction. This mechanism can be visualised as having a special ARM code instruction for each ANSI C call which is placed directly into memory at the correct point.
An additional area of memory is populated when initialising the ANSI library. This area of memory is used to hold the standard global variables which are available in C - errno, stdin, stdout, and stderr - and a character type mapping table used by the ctype functions. ROBE does not populated errno or ctype. The offsets of the data within the static workspace are as follows:
0x000: errno 0x004: stdin 0x02c: stdout 0x054: stderr 0x290: ctype
After all the instructions to make the C library calls have been set up, the next action is to execute the _kernel_init() function for which the entry point has already been set up - it is the first entry in the kernel chunk.
On entry to this function, R0 points three words as follows:
+0 Image base +4 Language description base +8 Language description top
Prior to doing anything, a stack is setup. R10 points to the base of the stack (+768 for reasons I am not clear on). R11 and R12 are initialised to zero, and R13 points to the top of the stack so it can be used as a descending stack pointer.
The image base is pretty useless at this point, but the "language description" contains a set of blocks which may contain pointers to initialisation routines at offset 16. The first one found which does is executed, and the initialisation routine returns with R0 as the return address. If none are found with initialisation routines then the function returns using R14 as normal. Each block is of variable length and the offset to the next block can be found as the first word of the block.
Once the initialisation routines have been run, the application proper can be started. However, before this can happen, the first thing which must be done is to set up R0 and R1 with the values for argc and argv in the application's main() function. This function entry point is responsible for splitting the command-line string apart appropriately and creating the argv data structure. argv is a pointer to a set of pointers to strings, terminated with a null entry.
This function is entered with the address of the application main() function in R1, therefore is entered with a branch rather than a branch-with-link and returns with R1 as the return address, rather than using R14.