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
%spstarting at0does not provide on its own), - initializes
%g1as 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:
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.
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:
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.