Skip to content

Writing and Running C Programs

Hand-assembling an algorithm like a sort or an FFT one instruction at a time, the way validation/asm/'s tests do, is tedious once the program is more than a handful of instructions long (see Validation Suite). validation/C/ takes the other approach: write the test as ordinary, freestanding C, compile it with the bundled cross-toolchain, and run it the same way. This page covers writing one, building and running it, and checking its result.


No standard library, by design

This model has no MMU, no cache, no peripheral/device model, and no operating system (see Models). There is nothing for a C standard library to call into. printf has no console to write to. malloc has no OS to request pages from. C programs for this model must be freestanding: no #include <stdio.h>, no libc at all, just the C language itself plus whatever you implement by hand against the flat memory space MemCore provides.

This is the same reason model/cpp_model/sparc_sim.cpp's driver doesn't try to model a console. There's genuinely nothing on the other end of one yet.


Structuring a test program

Write an ordinary, freestanding C main(). Every test in validation/C/ follows the same self-validating shape: compute something, compare it against a golden value computed once on the host machine and hardcoded right there in the source, and report pass(1)/ fail(0) in %o0. Here's a real one in full, validation/C/array_sum/array_sum.c, summing a small array:

// array_sum.c
//
// A first bare-metal C validation test, and the template every other
// validation/C/ test follows: sums the elements of a small integer array
// in a loop, then checks the result against a golden value computed once
// on the host machine (here, by hand: 1+2+3+4+5=15) and hardcoded below.
// Self-validating this way means the .vprj only ever has to check one
// thing -- that %o0 is 1 -- rather than re-embedding (and risking a
// second, independent typo in) the actual expected sum.
//
// See docs/writing_and_running_c_programs.md and compiler/crt0.s for how
// this gets a working stack, trap table, and pass/fail halt convention,
// the same way every validation/asm/ test does.

int main(void)
{
    int values[5] = {1, 2, 3, 4, 5};
    int sum = 0;
    int i;
    int expected;
    int pass;

    for (i = 0; i < 5; i++)
        sum += values[i];

    expected = 15; // 1+2+3+4+5, computed on the host
    pass = (sum == expected) ? 1 : 0;

    // Report pass(1)/fail(0) in %o0 -- see the file comment above.
    __asm__ volatile ("mov %0, %%o0" : : "r" (pass));

    // Halt: traps are enabled (crt0.s), so this ta 0 is caught by its
    // trap-table slot, which re-traps with traps now disabled, forcing
    // the model into error_mode. That's this project's pass/fail halt
    // convention, see writing_and_running_assembly_programs.md.
    __asm__ volatile ("ta 0");

    // Never reached: ta 0 above always halts the simulation first. This
    // just satisfies the compiler, which otherwise warns that a
    // non-void main() falls off the end without returning a value.
    while (1) {}
    return 0;
}

main() must never return, since there's nothing to return into. The way to end a test is the same mechanism validation/asm/ tests use, described in full in Writing and Running Assembly Programs: a single line of inline assembly, __asm__ volatile ("ta 0"), at the end of main().

Unlike an assembly test, you don't write any of the surrounding setup by hand. compiler/crt0.s is linked ahead of your compiled .c file (entry point overridden to crt0.s's own _start) and does it for you:

  • enables traps and installs the same 256-entry trap table every validation/asm/ test installs,
  • sets up a working stack (needed for any C function using a local variable, which %sp starting at 0 does not provide on its own),
  • initializes %g1 as the pass/fail sentinel,
  • then calls your main().

Here's what that looks like once linked, array_sum's actual entry point, before main even runs:

00000000 <_start>:
       0:   01 00 00 00     nop
       4:   01 00 00 00     nop
       8:   a0 10 20 e0     mov  0xe0, %l0  ! e0 <not_reached+0x98>
       c:   81 8c 00 00     mov  %l0, %psr
      10:   01 00 00 00     nop
      14:   01 00 00 00     nop
      18:   01 00 00 00     nop
      1c:   21 00 00 04     sethi  %hi(0x1000), %l0
      20:   a0 14 20 00     mov  %l0, %l0   ! 1000 <HW_trap_0x00>
      24:   81 9c 20 00     mov  %l0, %tbr
      28:   01 00 00 00     nop
      2c:   01 00 00 00     nop
      30:   01 00 00 00     nop
      34:   1d 03 ff ff     sethi  %hi(0xffffc00), %sp
      38:   9c 13 a3 f0     or  %sp, 0x3f0, %sp ! ffffff0 <memcpy+0xfffdf48>
      3c:   82 10 2b ad     mov  0xbad, %g1
      40:   40 00 07 f0     call  2000 <main>
      44:   01 00 00 00     nop

Matching it up against the four steps above: the mov 0xe0,%l0/wr %l0,%psr pair enables traps (see Writing and Running Assembly Programs for the 0xE0 encoding), sethi/wr ...,%tbr points TBR at the trap table linked in right after it (HW_trap_0x00 onward, the annotation on that line), sethi/or ...,%sp sets up the stack, mov 0xbad,%g1 is the sentinel, and the final call main (with its delay-slot nop) is the handoff into your code. See crt0.s itself for the full explanation of each step.

The result is that a C test's ta 0 halts exactly the same way an assembly test's does, and an unexpected trap during your test is caught the same way too.

Checking the result is then a single line. Its .vprj only ever has to check that %o0 is 1:

SOURCES = array_sum.c

RESULTS =
o0=1

The alternative: checking memory directly

Writing a raw computed value to a global and checking it directly via a .vprj MEM line (the address found in the .objdump's symbol table, or readelf -s array_sum.elf directly) still works. It's the same REG/MEM format validation/asm/ uses, but means the golden value is embedded in two places (the .vprj and, since the test already computes its own pass/fail, the source) instead of one. Doing the comparison in C and reporting a single pass/fail flag avoids that, and doubles as a self-contained mini-benchmark that also happens to check itself. See the rest of validation/C/ for more worked examples.


Building and running it

A minimal, freestanding C compiler (GCC 4.4.3) is bundled alongside the existing sparc-elf assembler, linker, readelf, and objdump, installed together by Installation step 5, if you haven't already. See Cross Compiler for what's bundled and why, and for building your own, more modern cross-toolchain instead.

compiler/compile_c.sh your_program.c

This produces your_program.hex, the same kind of memory image compiler/assemble.sh produces for assembly tests, loadable the same way. It also produces your_program.objdump: a readable disassembly of the linked program (useful for seeing what the compiler actually generated, unlike a hand-written .s file, this isn't something you wrote directly), followed by the full symbol table, function and global-variable addresses included. See Examining Core State at Runtime Using GDB.

Run it directly against the model the same way as an assembly test, see Running a test program:

model/cpp_model/sparc_sim_cpp your_program.hex

Adding it to the validation suite

Same two-phase pipeline as validation/asm/, see Validation Suite. build_hex.py picks compiler/assemble.sh or compiler/compile_c.sh automatically, based on whether a test's SOURCES line names a .s or a .c file. See Validation Suite: Adding a C test for the exact steps.