Skip to content

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

sudo apt install gdb

Building with --debug

By default, neither model is built with debug symbols. Rebuild with --debug:

model/cpp_model/build.sh --debug
model/sitar_model/build.py --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):

model/cpp_model/build.sh --debug
gdb model/cpp_model/sparc_sim_cpp
(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:

Breakpoint 1, debug_hook_after_execute (core=..., op=LD) at .../DebugHooks.cpp:13

From here:

(gdb) sparc-print-regs
prints the entire current-window register/PSR state. For just one register:
(gdb) sparc-print-reg l0
l0 = 0xe0 (224)
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:
(gdb) continue
Breakpoint 1, debug_hook_after_execute (core=..., op=LD) at ...

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:

(gdb) sparc-delete 1

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:

   265: 00002000   168 FUNC    GLOBAL DEFAULT    1 main
and 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.