Debugging

CHERIoT does not currently have support for interactive debugging. For now, the debugging options are logging, assertions and tracing.

Debug logging

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:

  • Global (GL)
  • Readable (LD)
  • Writeable (SD)
  • Load / store capability (MC)
  • Load global (LG)
  • Load mutable (LM)
  • Store local (SL)
  • EXecutable (EX)
  • Execute with access system registers (SR)
  • Seal (SE)
  • Unseal (US)
  • User0 (U0)

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.

Assertions / invariants

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.

Tracing

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.