CHERIoT does not currently have support for interactive debugging. For now, the debugging options are logging, assertions and tracing.
The SDK supports logging in C++ using debug.hh. To use this you should include something like the following in your code:
#include <debug.hh> constexpr bool DebugFoo = DEBUG_FOO; using Debug = ConditionalDebug<DebugFoo, "Foo">;
Here DEBUG_FOO
is a compile time macro that should be set to true
or false
to enable or disable debug output in the Foo
module. "Foo"
is a prefix that will be prepended to debug messages.
The SDK's xmake file provides support for defining debug macros of the form DEBUG_FOO
. To use it call debugOption("foo")
in your xmake.lua
and add add_rules("cherimcu.component-debug")
to your target definitions. You can then specify --debug-foo=true
when running xmake config
to build with debugging enabled.
Once you have defined an alias for ConditionalDebug
as above you can then use Debug::log
to output relevant debug messages, for example:
uint32_t anInt = 42; const char *aString = "CHERIoT"; Debug::log("The value of anInt is {}. aString is {}.", anInt, aString);
will output:
Foo: The value of anInt is 0x2a. aString is CHERIoT.
Note that {}
in the template string is replaced by the formatted value of the variable passed in. The formatter supports some common types such as integers and strings. By default, it will attempt to use magic_enum.hpp to pretty-print enumerations. Pretty-printing enumerations can significantly increase firmware size and so can be disabled by defining CHERIOT_AVOID_CAPRELOCS
, which will then fall back to printing their integer values. Pointers are displayed as capabilities using the following format:
0x800058e0 (v:1 0x800058e0-0x800058ec l:0xc o:0xb p: G RWcgm- -- ---) address tag base top length otype permissions
The capability permissions are displayed as a string of the form G RWcgml Xa SU0
, where a letter indicates that a permission is present and dash in the corresponding position indicates it is absent. The letters correspond to the permissions as follows:
Note that lower case letters denote permissions that are ‘dependent’ on other permissions. For example, load / store capabilities requires either Read or Write; load global requires both Read and load / store capabilities.
The ConditionalDebug
class also supports assertions and invariants, for example:
Debug::Assert(anInt == 42, "anInt was not 42, was {}.", anInt);
If debugging is enabled this will test whether anInt
is equal to 42. If the condition is false a message will be printed with the line number of the assertion and the formatted message, for example:
answer.cc:13 Assertion failure in check_answer anInt was not 42, was 0x36.
After printing the message the assertion failure will trigger a trap using a reserved instruction. The effect of this trap will depend on the rest of the application, for example whether there is an error handler registered for the compartment. Take care not to write conditions that have side-effects (e.g. using the ++
operator to increment a variable) because these will execute differently depending on whether debugging is enabled.
Debug::Invariant
is identical to Debug::Assert
except that the condition is checked even if debugging is not enabled. It will cause a trap whenever the condition evaluates to false, but the message will be printed only if debugging is enabled.
For detailed debugging the Sail simulator supports instruction level tracing. This can be enabled using the -v
option. By default it is extremely verbose, printing all memory accesses (including instruction fetch), instructions executed, and registers written, for example:
mem[X,0x80000000] -> 0x4081 [0] [M]: 0x80000000 (0x4081) c.li ra, 0 x1 <- t:0 s:0 perms:0b0000000000000 type:0x00000000 offset:0x00000000 base:0x00000000 length:0x000000000 mem[X,0x80000002] -> 0x4101 [1] [M]: 0x80000002 (0x4101) c.li sp, 0 x2 <- t:0 s:0 perms:0b0000000000000 type:0x00000000 offset:0x00000000 base:0x00000000 length:0x000000000
This slows down execution and makes it very difficult to see UART output, therefore tracing can be selectively enabled using --trace=instr|reg|mem|exception|platform|all
. For example --trace=instr
will output just the instruction count, PC, opcode and disassembly for each instruction:
[0] [M]: 0x80000000 (0x4081) c.li ra, 0 [1] [M]: 0x80000002 (0x4101) c.li sp, 0 [2] [M]: 0x80000004 (0x4181) c.li gp, 0 [3] [M]: 0x80000006 (0x4501) c.li a0, 0 [4] [M]: 0x80000008 (0x2231) c.jal 268 [5] [M]: 0x80000114 (0x4201) c.li tp, 0 [6] [M]: 0x80000116 (0x4281) c.li t0, 0
It is possible to specify more than one --trace
option to enable multiple kinds of trace output.
To see terminal output more easily when tracing you can redirect it to a file using the -t / --terminal-log
option. For example, if using bash:
cheriot_sim --trace=instr --trace=reg -t terminal.txt <path to elf> >trace.txt
will run the given ELF file, putting the console output in terminal.txt
and a trace with instructions and register writes in trace.txt
.