We have printf()!

The EightThirtyTwo ISA – Part 11 – 2019-11-29

One of the first library functions to implement when experimenting with a new architecture is puts(), allowing the archetypal Hello World program to announce its presence.

The C standard printf() function is a bit trickier – for a number of reasons…

Firstly, printf() is a varargs function, so we have to define and then support calling conventions for varargs on the target architecture.

Secondly, we need to be able to convert integers to textual representations – in more than one base.

In the process of getting this working I have implemented a new instruction – “byt” which, similar to the previously-defined “hlf” instruction, modifies the storage size of the next load/store operation to operate on a byte rather than a full word. Previously, the only way of accessing bytes was via “ldbinc” or “stbinc” – both of which have automatic post-increment. These are easy enough to work with in assembler, but were a bit of pain for the C compiler backend. I now use “byt, st” when I don’t want the post-increment.

I’m also seriously considering replacing the now pretty-much redundant “sth” instruction with “stinc” – store with post-increment.

Anyhow, back to implementing printf(): Firstly, how do we implement varargs?

In order to answer that we need to have a clear understanding of how parameters are passed into functions in the first place.

Leaving aside possible future optimisations involving passing parameters in registers, parameters are pushed one at a time, in reverse order, onto the stack, so a invocation such as

printf("Meaning: %s, %d\n","(you're not going to like it)",42);

will translate to the following pseudo-code:

push 42
push (pointer to)"(you're not going to like it)"
push (pointer to)"Meaning: %s, %d\n"
call printf()

Since the stack grows downwards in memory, this means that once program flow is within the printf() function, the parameters can be accessed from the stack in ascending memory order. The simplest implementation of stdarg.h will thus take the address of the first argument (the pointer to the format string in this case) and store a pointer to the memory immediately following this. This pointer serves as a second pointer into the stack, and is incremented by the appropriate size every time we pick off a parameter to be printed.

Our simplistic stdarg.h looks like this:

typedef char *va_list;
#define va_start(ap,parmn) (void)((ap) = (char*)(&(parmn) + 1))
#define va_end(ap) (void)((ap) = 0)
#define va_arg(ap, type) (((type*)((ap) = ((ap) + sizeof(type))))[-1])

The va_arg() macro leaves one feeling like a castaway in a jungle of parentheses – but it increments ap by the size of the argument’s type, while at the same time reading an object of that type from the location pointed to by its old value!

Getting this working proved to be quite a challenge and shook out several different bugs in the backend in the process, but it does now seem to be working reliably.

So now we’re able to fetch varags from the stack, the next challenge is converting them to text.

Hexadecimal is relatively easy – we can simply bit-shift each 4-bit hex digit, and mask with “and”. Decimal, on the other hand, requires working division and modulo operations – neither of which are directly supported by our instruction set – so we need some division code.

Rather than create something from scratch I found some simple division code intended for ARM and translated it to EightThirtyTwo – this is what it looks like:

stdec r6
mt r3 // Save r3
stdec r6
mt r2
and r2
cond EQ // Division by zero?
li IMW0(PCREL(.end))
add r7
li 0
mr r0
li 1
mr r3
mt r2
cmp r1
cond SGT
add r2 // Faster than lsl
mt r3
add r3
li IMW0(PCREL(.start))
add r7
mt r2
cmp r1
cond GE
sub r1
mt r3
add r0
cond EX
li 1
shr r2
shr r3
cond NEQ
li IMW0(PCREL(.next)) add r7
ldinc r6
mr r3
ldinc r6
mr r7

I make that 37 bytes in total.

This routine takes the numerator in r1, denominator in r2 and returns both quotient and remainder – in r0 and r1 respectively, so the C backend just calls it and uses whichever result is appropriate – so the only detail to take care of is to make sure the division routine is linked with the binary.

The routine above only handles unsigned division – I have a signed variant which just keeps track of the signs, negating the operands and result as necessary – I just need to figure out how to handle the remainder in that situation!

The upshot is that the vbcc backend for EightThirtyTwo is now working weill enough that printf() – or rather a cut-down version of printf() that supports %s, %d, %x and %c format strings without modifiers – is now working nicely.

Leave a Reply

Your email address will not be published. Required fields are marked *