Function pointers in C

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.

Leave a Reply

  

  

  

Time limit is exhausted. Please reload the CAPTCHA.