Writing and Running Assembly Programs¶
In Getting Started, we discussed how you can view
and run an existing program provided in this repository on the C++/Sitar
models.
A variety of asm and C test programs are present in the validation/
folder. For each program, the test source (.s or .c), the generated
memory hex file, and the expected-results file are already included.
This section deals with:
- writing your own asm programs,
- assembling them to generate a memory image for the simulator,
- running them on the simulator, and
- the standard format for writing a test for inclusion in this project's validation framework.
This section assumes that you have built the models and the toolchain release. See steps 2 and 5 in Installation.
Let's create a copy of the given simple asm program test_simple_ADD,
and modify it, for example, using sub instead of add.
Assembling to generate a memory image (.hex)¶
The conversion of asm/C programs to a memory image requires a cross-compiler toolchain, consisting of an assembler, utilities such as objdump/readelf, and also a compiler (for compiling C to asm).
If not already done in Installation (step 5), run compiler/install_toolchain.sh to create a release.
The toolchain release lives at compiler/toolchain/. Add it to your PATH variable (on Linux systems) by sourcing compiler/toolchain_env.sh, so that its binaries and libraries can be accessed from any location:
Next, this project provides scripts (compiler/assemble.sh, compiler/compile_c.sh) which are convenience wrappers around these toolchain executables.
To assemble a .s to .hex:
For example, from temp_asm/:
This also generates test_simple_SUB.objdump, its disassembly plus
full symbol table, useful for finding an address to give gdb, see
Examining Core State at Runtime Using GDB.
See Cross Compiler for more on what's in the
bundled toolchain.
Running a test program¶
Rebuild the models with --logging, then run them on this test. From
the repo root:
This prints the final register state once the program halts, see Getting Started for the full walkthrough, including the Sitar-timed model (which takes the identical CLI) and observing the trace in the log viewer.
Creating a test¶
A test is not just any program that runs on a model, but one that gives a pass/fail indication after checking something. In this project, a test consists of a source file and an expected-results file in a specific format, see "Recommended format for a test program" below for how the source file itself needs to be structured to make this possible.
The simulator executables take the following arguments:
- A hex file (required): the memory image to run.
- An expected-results file (optional): if given, once the program halts, the final state is checked against it, and the executable prints a PASS/FAIL verdict per check plus an OVERALL result, instead of the detailed state.
- A cycle limit (optional): caps how long the simulator runs before giving up.
When no expected-results file is passed, as above, the executable just prints the final register state. When one is passed, it's checked directly instead.
Let's write an expected-results file for our test, and run with that.
o0 = 5 - 7 = 0xfffffffe, so temp_asm/test_simple_SUB.expected:
Then run it:
Format for the expected-results file¶
test_simple_SUB.expected is a plain text file, one check per line:
REG <name> <hex value> [mask]checks a register (mnemonicsg1-g7,o0-o7,l0-l7,i0-i7,f0-f31,psr,fpsr,y,wim,tbr,pc,npc,asr0-asr31).MEM <hex addr> <hex value> [mask]checks a word-aligned 32-bit memory read.
Either kind accepts an optional trailing mask, to check only specific bits.
Recommended format for a test program¶
Because this model does not yet have any operating system, the test
program has to be self-sufficient. This means initializing the
processor state, setting up a trap table, then running the test part
and exiting while indicating pass/fail in some way to the simulator. In
the current scheme, the latter is achieved using %g1 as a sentinel
register together with a deliberate final trap (ta 0), described in
full under "The pass-fail convention" below.
The following shows a typical structure of a test program:
.global main
main:
_start:
! 1. Enable traps: PSR ET=1, PS=1, S=1
mov 0xE0, %l0
wr %l0, %psr
nop
nop
nop
! 2. Point TBR at this file's own trap table
set trap_table_base, %l0
wr %l0, 0x0, %tbr
nop
nop
nop
! 3. Sentinel: overwritten by the trap number if anything unexpected traps
mov 0xBAD, %g1
!======================================
! 4. The instruction(s) under test go here
add %o0, %o1, %o2
!======================================
ta 0 ! 5. normal exit
nop
nop
not_reached:
set 0xDEAD, %g1 ! control should never reach here
ta 0
nop
nop
.align 4096 ! TBR only captures bits 31:12, so the trap table
! must start on a 4096-byte boundary
trap_table_base:
! 256 four-instruction slots, one per trap type (0x00-0xff):
! mov <trap number>, %g1 ; restore ; ta 0 ; nop
! ... (copy this verbatim from any existing test, see below)
Our own test_simple_SUB skips the trap table by simply disabling
traps entirely (wr %g0, %psr), the simplest thing that works when
nothing in the test could ever trap. Use the fuller structure above
instead for anything meant to join validation/asm/, where an
unexpected trap needs to be caught rather than silently forcing
error_mode for the wrong reason.
Copy the trap table, don't retype it
The 256-entry trap table is always the same boilerplate. Copy it
verbatim from any existing test under validation/asm/, then edit
only the one slot you actually care about, see "The trap table"
below.
The pass-fail convention¶
Every test always halts the same way: a ta 0 taken with traps disabled
(ET=0, cleared automatically on trap entry) forces the processor into
error_mode per the manual (Appendix C, Section C.8). This is what
PROCESSOR STATE: ERROR in sparc_sim_cpp's output means, and it's
expected, not a bug.
%g1 distinguishes a clean run from an unexpected trap:
- If the test's own final
ta 0(step 5 above) is what triggers this, its trap-table slot (SW_trap_0x80) sets%g1 = 0x80. This is success: nothing unexpected happened. - If any other trap occurs first (a bug, a mistake in the test, or a
trap you're deliberately testing for), it's caught by that trap's own
table slot, which records the trap number into
%g1before re-trapping the same way. So%g1's final value tells you exactly what happened, whether the test passed or not.
The trap table¶
Each slot is a fixed 4 instructions: mov <tt>, %g1; restore; ta 0;
nop. To make a test verify a specific trap actually fires (rather
than just detecting an unexpected one), replace that one slot with
custom code. For example,
validation/asm/floating_point/fp_exceptions/overflow.s replaces its
HW_trap_0x08 slot with inc %g2; rett %r18; nop; nop, which increments
a counter and resumes execution right after the trapping instruction
(via rett), instead of halting. This lets the test both confirm the
trap fired (%g2) and continue on to a normal exit (%g1 = 0x80).
Adding it to the validation suite¶
Hand-writing a .expected file and running it yourself works for one
test at a time. To run your test alongside the rest of the suite
automatically instead, and have it re-checked every time the model
changes, the recommended practice is to wrap it as a .vprj file for use with the validation script.
See Validation Suite: Adding an asm test for details.
See validation/asm/README.md for the AJIT-adaptation attribution and
docs/compliance/README.md for documented, specific divergences from AJIT's
own reference results (kept separate from the maintained suite since they
represent understood implementation differences, not bugs).