Examining Core State at Runtime Using GDB¶
Both models are, underneath everything else, plain host C++ programs
(sparc_sim_cpp, sparc_sim_sitar). That means the host's
own gdb can attach to them directly. No cross-debugger and no special
protocol are needed. It can step through simulated SPARC execution one
instruction at a time, break on a specific PC/register/memory condition,
and inspect the entire architectural state, all without touching the
model's own source.
This page is a beginner-facing introduction and walkthrough. See
GDB Command Reference for the exhaustive,
descriptive list of every sparc-* breakpoint/watchpoint/probe command,
and Debug Support Internals for how this
support is actually built.
Why use gdb instead of logging¶
Logging is the right tool when you want to watch a program run: a full instruction-by-instruction trace, loaded into the log viewer. It has a real cost, though. Formatting the entire current-window state (~50 fields) on every single instruction is non-negligible work, and a long-running program produces a trace too large to comfortably load or scroll through.
GDB is the better tool for the opposite situation. You already know
roughly what you're looking for, a specific PC, a specific trap, a
memory location or register reaching a specific value, and want to stop
exactly there without generating a full trace at all. Selective logging
(log.turnOFF()/turnON(), see Logging) can narrow a
trace down by time/PC range, but the condition has to be decided in
advance and built into Core.sitar. Checking a genuinely dynamic
condition (a register reaching a value that depends on the program's own
data) means adding that check into the model's source and recompiling
instead. A gdb conditional breakpoint is the same check, attached at the
debugger prompt, changeable between runs with no recompile.
Installing gdb¶
Building with --debug¶
By default, neither model is built with debug symbols. Rebuild with
--debug:
This adds debug symbols and a handful of stable, named hook points gdb
can break on by name, at exactly the points an instruction can finish
(executed, trapped, or annulled) or a memory reference completes, see
Debug Support Internals for where these
are and why. --logging combines with --debug freely (all four
combinations are valid), but for a gdb session you normally don't want
it. Avoiding logging's per-instruction cost is the whole point.
For an arbitrary breakpoint or single-step deep inside SparcCore
itself, beyond those hook points, use --debug-o0 instead, see
Debug Support Internals
for when and why.
A first walkthrough¶
This uses a small, frozen copy of the array_sum test committed
alongside this page (docs/source/examples/array_sum/), rather than
validation/C/array_sum/ directly. It was built once with the toolchain
this repository vendors, so the addresses below stay correct regardless
of which cross-compiler version you have installed. The source:
// 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;
}
It sums {1,2,3,4,5} in a loop and reports pass/fail in %o0. Build the
cpp model with --debug (the sitar model works identically, see the note
at the end of this section) and load the convenience commands from
debug/sparc.gdb
(this walkthrough only uses a few, see
GDB Command Reference for every one):
(gdb) source debug/sparc.gdb
(gdb) sparc-break pc=0x203c
(gdb) run docs/source/examples/array_sum/array_sum.hex
0x203c is the loop body's own address (ld [%fp+-12],%g1, see "Finding
addresses" below for where that number comes from). This stops once per
loop iteration, right after that iteration's instruction has fully
executed:
From here:
prints the entire current-window register/PSR state. For just one register:continue re-hits the same breakpoint at the next loop iteration. This
is effectively "single-step one SPARC instruction," for free, since it's
the same condition matching the next occurrence:
Delete it before moving on, an active pc=0x203c breakpoint would keep
firing on every loop iteration for the rest of this walkthrough
otherwise, getting in the way of everything below:
To watch the running sum itself change, you first need its address.
array_sum.c never spells it out, sum is a local, stack-resident
variable (see "Finding addresses" below for why that's the one thing an
objdump can't give you directly). Discover it live instead, by breaking
on every store and looking at where each one goes. run again first,
restarting the program from the very beginning (the pc=0x203c
breakpoint would otherwise have already skipped past the stores we want
to see):
(gdb) sparc-break-mem kind=STORE
(gdb) run
(gdb) sparc-print-mem-access
kind=STORE address=0xfffffc8 word0=0x0 word1=0x1 MAE=0
(gdb) continue
(gdb) sparc-print-mem-access
kind=STORE address=0xfffffd0 word0=0x2 word1=0x1 MAE=0
(gdb) continue
(gdb) continue
(gdb) continue
(gdb) continue
(gdb) sparc-print-mem-access
kind=STORE address=0xfffffe0 word0=0x0 word1=0x5 MAE=0
The first several hits are the array literal {1,2,3,4,5} itself being
written into values[] (fp-36 through fp-20), 0xfffffc8 above is
actually earlier still, crt0.s's own setup, before main even starts.
A few addresses repeat once each along the way too, that's just two
adjacent 4-byte array elements sharing one 8-byte-aligned doubleword,
the granularity this hook reports at, not a loop yet. Keep
continue-ing (six times total gets you to 0xfffffe0 above) and past
the array initialization, one address starts repeating on every loop
iteration instead: that's sum. Delete the now-done store breakpoint
first, it would otherwise keep firing on every store from here on,
including the ones the watchpoint below is about to catch, then set the
watchpoint on it:
(gdb) sparc-delete 2
(gdb) sparc-watch-mem addr=0xfffffe0
(gdb) continue
Hardware watchpoint 3: *(unsigned int*)...
Old value = 0
New value = 1
That's the first loop iteration adding values[0] (1) to sum
(starting at 0). Delete this watchpoint too before the next step, for
the same reason, then catch the final pass/fail write into %o0
directly:
(gdb) sparc-delete 3
(gdb) sparc-watch-reg o0
(gdb) continue
Breakpoint N, debug_hook_after_execute (core=..., op=OR) at ...
(gdb) sparc-print-reg o0
o0 = 0x1 (1)
Sitar model: everything above works identically against
model/sitar_model/executable/sparc_sim_sitar (built with
model/sitar_model/build.py --debug), same commands, same addresses,
same CLI. Both models drive the same SparcCore, and the debug hooks
live in the shared cpp_common_code/, not in either driver.
gdb model/sitar_model/executable/sparc_sim_sitar
(gdb) source debug/sparc.gdb
(gdb) sparc-break pc=0x203c
(gdb) run docs/source/examples/array_sum/array_sum.hex docs/source/examples/array_sum/array_sum.expected
Finding addresses¶
Every .objdump (produced by compiler/compile_c.sh/assemble.sh
alongside a test's .hex) now includes the full symbol table after the
disassembly (sparc-elf-readelf -s), not just the incidental branch/call
target annotations disassembly alone provides. For array_sum,
docs/source/examples/array_sum/array_sum.objdump shows:
main's own disassembly gives the loop body's address used above:
00002000 <main>:
2000: 9d e3 bf 78 save %sp, -136, %sp
2004: 82 10 20 01 mov 1, %g1
2008: c2 27 bf dc st %g1, [ %fp + -36 ]
200c: 82 10 20 02 mov 2, %g1
2010: c2 27 bf e0 st %g1, [ %fp + -32 ]
2014: 82 10 20 03 mov 3, %g1
2018: c2 27 bf e4 st %g1, [ %fp + -28 ]
201c: 82 10 20 04 mov 4, %g1
2020: c2 27 bf e8 st %g1, [ %fp + -24 ]
2024: 82 10 20 05 mov 5, %g1
2028: c2 27 bf ec st %g1, [ %fp + -20 ]
202c: c0 27 bf f0 clr [ %fp + -16 ]
2030: c0 27 bf f4 clr [ %fp + -12 ]
2034: 10 80 00 0c b 2064 <main+0x64>
2038: 01 00 00 00 nop
203c: c2 07 bf f4 ld [ %fp + -12 ], %g1
2040: 83 28 60 02 sll %g1, 2, %g1
2044: 82 07 80 01 add %fp, %g1, %g1
2048: c2 00 7f dc ld [ %g1 + -36 ], %g1
204c: c4 07 bf f0 ld [ %fp + -16 ], %g2
2050: 82 00 80 01 add %g2, %g1, %g1
2054: c2 27 bf f0 st %g1, [ %fp + -16 ]
2058: c2 07 bf f4 ld [ %fp + -12 ], %g1
205c: 82 00 60 01 inc %g1
2060: c2 27 bf f4 st %g1, [ %fp + -12 ]
2064: c2 07 bf f4 ld [ %fp + -12 ], %g1
2068: 80 a0 60 04 cmp %g1, 4
206c: 04 bf ff f4 ble 203c <main+0x3c>
2070: 01 00 00 00 nop
2074: 82 10 20 0f mov 0xf, %g1 ! f <_start+0xf>
2078: c2 27 bf f8 st %g1, [ %fp + -8 ]
207c: c4 07 bf f0 ld [ %fp + -16 ], %g2
2080: c2 07 bf f8 ld [ %fp + -8 ], %g1
2084: 82 18 80 01 xor %g2, %g1, %g1
2088: 80 a0 00 01 cmp %g0, %g1
208c: 82 60 3f ff subx %g0, -1, %g1
2090: c2 27 bf fc st %g1, [ %fp + -4 ]
2094: c2 07 bf fc ld [ %fp + -4 ], %g1
2098: 90 10 00 01 mov %g1, %o0
209c: 91 d0 20 00 ta 0
20a0: 10 80 00 00 b 20a0 <main+0xa0>
20a4: 01 00 00 00 nop
One real limitation, accepted rather than worked around: this gives you
function and global-variable addresses, but not addresses for local C
variables (sum, i, values in array_sum.c). Those are
stack-resident at -O0 and never appear in the ELF symbol table at all
(that would need DWARF debug info compiled into the target SPARC
binary, a separate, heavier thing this repository doesn't do). Reading
the disassembly's %fp-relative offsets directly, as the walkthrough's
0xfffffe0 address for sum did, is the way around this. Once you have
one such address from a live run, it stays valid for that test.