Some time ago I ran a stress test of my compiler toolchain by compiling portions of Minix C library (compilation only, with no intention of running the code). This was to see if I encounter any problems. Overall I am happy with the results. I fixed several LCC machine description bugs, and I am now confident that my LCC port is up to the task of compiling real life code, not just test lab snippets.
I have found one problem which required me to update the instruction set, though. I had two CALL instructions, the CALL #i16 to push PC to the stack and perform a PC-relative jump to an absolute address, and CALL (SP) to push program counter to stack and jump to subroutine address on previous top of the stack. With CALL #i16 the jump address has to be determined at compile time and is fixed throughout program execution. This was not a problem for hand-crafted assembly programs but a serious limitation for a C compiler. There was no way to perform a CALL to an address computed at run-time, without self-modifying code or by performing stack pointer gymnastics. As a consequence, LCC was not able to compile code with function pointers, such as this sample program:
int a() { return 1; } int b() { return 2; } int (*fun[2])(); int main() { fun[0] = a; fun[1] = b; return fun[1](); }
The solution to this was to add a CALL A to the instruction set and change the LCC machine description rules to also take a register as their arguments. Instead of:
albl: ADDRGP2 "%a" reg: CALLI2(albl) "\tcall\t%0\n" 2 reg: CALLU2(albl) "\tcall\t%0\n" 2 reg: CALLP2(albl) "\tcall\t%0\n" 2 stmt: CALLV(albl) "\tcall\t%0\n" 2
I now have something like:
albl: ADDRGP2 "%a" ar: reg "%0" ar: albl "%0" reg: CALLI2(ar) "\tcall\t%0\n" 2 reg: CALLU2(ar) "\tcall\t%0\n" 2 reg: CALLP2(ar) "\tcall\t%0\n" 2 stmt: CALLV(ar) "\tcall\t%0\n" 2
The above works for 16-bit register width. Things get a bit more complicated for 32-bit and 64-bit wide operations but this was handled in LCC’s emit2()
function. The change to work properly also required properly targeting the CALLn rules to register A in LCC’s target()
.
I have run out of free opcodes already so I had to sacrifice some instruction to implement CALL A. I chose CALL (SP) which was of little use anyway. This exotic call-the-stack-top instruction was invented by me when hand coding the previous Monitor/OS (to ease kernel function mapping for SYSCALL instruction). So, CALL (SP) was replaced by a simpler CALL A. Now the sample code above compiles to:
; BYTEC/16 assembly, generated by lcc 4.2 .global _a .text _a: ld a,1 L1: ret .global _b _b: ld a,2 L2: ret .global _main _main: lea a,(dp:_fun) lea x,(dp:_a) st (dp:_fun),x lea x,(dp:_b) st (a:2),x lea a,(dp:_fun) ld a,(a:2) call a L3: ret .data .global _fun _fun: db 0x00:4
I am publishing the updated ISA and microcode source today. The corresponding compiler, a new relocatable object linker along with recent changes to LCC port will be posted soon. They deserve a whole new post.