CHERIoT Programmers' Guide

David Chisnall

Table of contents

9. Features for debug builds

CHERIoT provides a small set of APIs for use in debug builds in debug.hh. These include:

The message-producing aspects of these APIs use direct access to the UART. This can cause the messages to be interleaved but ensures that they are generated even if part of the system has crashed or deadlocked.

Access to the UART will show up in the linker report. The implementation of the logging functions is in the debug library. You should typically add an audit check that ensures this compartment is not present in release builds.

9.1. Enabling per-component debugging

Debug builds can often be significantly larger than release builds. They contain more code and potentially large strings for debug messages. CHERIoT RTOS is designed to allow debugging features to be turned on on a per-compartment basis to help mitigate this. You can see this in the core components. If you run xmake config --help in a firmware build, you will see this at the end of the output:

--debug-token_library=[y|n] Enable verbose output and assertions in the token_library
--debug-allocator=[y|n]     Enable verbose output and assertions in the allocator
--debug-loader=[y|n]        Enable verbose output and assertions in the loader
--debug-scheduler=[y|n]     Enable verbose output and assertions in the scheduler

Each of the core components allows extra debugging modes to be enabled independently, rather than via a global debug-mode switch. Adding something similar requires two changes in your xmake.lua file. The first line, at top-level scope, declares the option, as shown in Listing 56.

debugOption("debug_compartment")
-- debug_option#end

-- use_debug#begin
compartment("debug_compartment")
	add_rules("cheriot.component-debug")
	add_deps("unwind_error_handler")
	add_files("example.cc", "example.c")
-- use_debug#end
-- Explicitly setting the debug option name is not necessary
-- here because it matches the compartment name, but if we
-- did it explicitly then it would look like this:
-- set_debug_option#begin
	on_load(function (target)
		target:set('cheriot.debug-name', "debug_compartment")
	end)

Listing 56. Build system code for defining a debug option.examples/debug_helpers/xmake.lua

With this, you will get a message in xmake config --help like the one above, but it won't actually do anything. You can test that this actually works by trying the command:

$ xmake config --help
...
        --debug-debug_compartment=[y|n]                      Enable verbose output and assertions in the debug_compartment
...

You must also opt your compartment or library into debugging support by adding the corresponding rule in the description of your compartment or library. This is shown in Listing 57, which adds a debug option to an example compartment.

compartment("debug_compartment")
	add_rules("cheriot.component-debug")
	add_deps("unwind_error_handler")
	add_files("example.cc", "example.c")

Listing 57. Build system code for using a debug option.examples/debug_helpers/xmake.lua

By default, this assumes that the debugOption that you've provided has the same name as the target. Sometimes, it's useful to have a single debug option that enables or disables debugging for multiple components. You can set the cheriot.debug-name target property in your component to the name that you expect in the on_load hook, as shown in Listing 58.

on_load(function (target)
		target:set('cheriot.debug-name', "debug_compartment")
	end)

Listing 58. Build system code for providing the debug option name explicitly.examples/debug_helpers/xmake.lua

Now, the compartment will be compiled with a macro that starts with DEBUG_ and ends with the name of the debug option in all capitals. In the first example above, this would be DEBUG_DEBUG_COMPARTMENT.

This can then be used with the ConditionalDebug class from debug.hh. This is typically used with a using directive as shown in Listing 59 to connect the debug option.

using Debug = ConditionalDebug<DEBUG_DEBUG_COMPARTMENT,
                               "Debug compartment">;

Listing 59. Connecting the debug option to a debug type in codeexamples/debug_helpers/example.cc

The first template parameter is a boolean value that indicates whether this component is being debugged. The second is a free-form string literal that will be prepended (in magenta) to any debug line.

The rest of this chapter will assume that the Debug type has been defined in this way.

9.2. Generating log messages

Printing log messages is the simplest use of the debug APIs. The Debug::log() function takes a format string and then a set of arguments. This is similar to printf or std::format, inserting the arguments into the output, replacing placeholders. The syntax here is modelled on std::format, but does not currently accept any format modifiers. The {} syntax for placeholders makes it possible to add modifiers in the future. This class is designed to avoid needing heap allocator or large amounts of stack space and so is intentionally less flexible than a general-purpose formatting library.

Unsigned integers are printed as hex. Signed integers are printed as decimal. Floating point numbers are not supported. Individual characters are printed as characters, strings (either const char* or std::string_view) are printed as strings.

Enumerated types are converted to strings using the Magic Enum library and printed with their numeric value in brackets. This has some limitations (in particular, by default, it does not work with very large enumeration values). It also requires capability relocations because it generates tables of strings. If you compile a compartment with CHERIOT_AVOID_CAPRELOCS defined then enumerations will be printed as numeric values.

Two other types have rich formatted output. PermissionSet objects (see Section 5.9. Manipulating capabilities with CHERI::Capability) are printed using the characters from the tables in Section 2.2. Decomposing permissions in CHERIoT. Capabilities (either as raw pointers or instances of the CHERI::Capability class) are printed in full detail.

Listing 60 shows an example of most of these. Note the last two log lines, which print enumeration and capability values.

	NetworkAddress addr;
	addr.ipv4 = 0x0100007f;
	addr.kind = NetworkAddress::AddressKindIPv4;
	Debug::log("There's no place like {}", addr);
	memset(addr.ipv6, 0, sizeof(addr.ipv6));
	addr.ipv6[15] = 1;
	addr.kind     = NetworkAddress::AddressKindIPv6;
	Debug::log("There's no place like {}", addr);

Listing 60. Printing log messages with the debug log API.examples/debug_helpers/example.cc

When you run this, the output should look like this:

Debug compartment: Hello world!
Debug compartment: Here is a C string hello from a C string, A C++ string view hello from a C++ string, an int 52, and an unsigned 64-bit value 0xabcd
Debug compartment: Here is an enum value: AddressKindIPv4(0x2)
Debug compartment: Here is a pointer: 0x80000ef8 (v:1 0x80000ef8-0x80000efc l:0x4 o:0x0 p: - RWcgml -- ---)

On the penultimate line, both the name and value of the enumeration are printed. This has some limitations. It will not work for enumerations that have multiple names for the same values or enumerations with very large numbers of elements.

The capability (pointer) format starts with the address and then has the metadata in brackets. The metadata includes the tag (valid) bit, then the range, then the length, object type, and permissions. The letters for the permissions are described in Chapter 2. CHERIoT Concepts.

9.3. Printing custom types

The standard formatting machinery in C++ can result in large code. The CHERIoT debug logging mechanism is intended to be small and intentionally omits features. It does provide a mechanism for pretty printing custom types.

Most of the printing is done in the debug library, which contains code for printing different primitive types, including capabilities. The function from this library takes an array of arguments for printing, where each is identified by two uintptr_t variables. One contains the value, the other the discriminator. If the discriminator is untagged, it is treated as an enumeration for the built-in handlers. If it is tagged, it is a pointer to a function that knows how to pretty-print the value. If you want to print a custom type, you first need to define a function that will print it. Listing 61 contains an example of such a function.

This is printing a network address, which is a discriminated union of a 32-bit IPv4 address or a 128-bit IPv6 address.

void debug_network_address(uintptr_t    value,
                           DebugWriter &writer)
{
	auto *address = reinterpret_cast<NetworkAddress *>(value);
	if (address->kind == NetworkAddress::AddressKindIPv6)
	{
		for (int i = 0; i < 14; i += 2)
		{
			writer.write_hex_byte(address->ipv6[i]);
			writer.write_hex_byte(address->ipv6[i + 1]);
			writer.write(':');
		}
		writer.write_hex_byte(address->ipv6[14]);
		writer.write_hex_byte(address->ipv6[15]);
	}
	else if (address->kind == NetworkAddress::AddressKindIPv4)
	{
		writer.write_decimal((address->ipv4 >> 0) & 0xff);
		writer.write('.');
		writer.write_decimal((address->ipv4 >> 8) & 0xff);
		writer.write('.');
		writer.write_decimal((address->ipv4 >> 16) & 0xff);
		writer.write('.');
		writer.write_decimal((address->ipv4 >> 24) & 0xff);
	}
	else
	{
		writer.write("<invalid address>");
	}
};

Listing 61. Defining a print function for a custom type.examples/debug_helpers/example.cc

This takes two arguments. The first is the value to print, the second is a reference to an object that provides methods for printing individual methods. In this example, the value will be a pointer to the real object and must be explicitly cast. Remember that, although this is not type safe, it is memory safe on a CHERIoT system. If the value is not a pointer of the correct size or larger, you will get traps.

The other argument is the writer, which is passed as an abstract class (interface) and provides callbacks into the debug library. This has various overloads of a write method that will print primitive values as if they were passed as arguments to the log function, as well as some with explicit control over formatting.

This function also needs to be accompanied by an adaptor, as shown in Listing 62 that constructs the pair of uintptr_ts that will be passed into the library. This is simply casting the pointer to a uintptr_t for the value and providing the helper function (also cast to uintptr_t) as the type value.

template<>
struct DebugFormatArgumentAdaptor<NetworkAddress>
{
	static DebugFormatArgument
	construct(NetworkAddress &address)
	{
		return {
		  reinterpret_cast<uintptr_t>(&address),
		  reinterpret_cast<uintptr_t>(debug_network_address)};
	}
};

Listing 62. Defining an adaptor for a custom type.examples/debug_helpers/example.cc

With those in scope, you can now print network addresses using the same APIs. Listing 63 shows printing an IPv4 and IPv6 address using this API.

	NetworkAddress addr;
	addr.ipv4 = 0x0100007f;
	addr.kind = NetworkAddress::AddressKindIPv4;
	Debug::log("There's no place like {}", addr);
	memset(addr.ipv6, 0, sizeof(addr.ipv6));
	addr.ipv6[15] = 1;
	addr.kind     = NetworkAddress::AddressKindIPv6;
	Debug::log("There's no place like {}", addr);

Listing 63. Printing a custom type with the debug APIs.examples/debug_helpers/example.cc

This should print:

Debug compartment: There's no place like 127.0.0.1
Debug compartment: There's no place like 00:00:00:00:00:00:00:01

The second line isn't quite perfect IPv6 output (it should be simply :1), but it's good enough to understand what's happening.

9.4. Asserting invariants

Assertions and invariants use the same formatting infrastructure as lthe log message code. The terms are often used interchangeably. In builds with the debug option enabled, both behave in the same way. They take a condition and a message (including a format string and arguments, as with the logging APIs). If the condition is false, they will print the message and then execute a trap instruction.

If debugging is disabled, assertions do nothing. Invariants still perform the check and trap but do not print the message. Listing 64 shows an example of an assertion and an invariant. These use the scoped error handlers described in Section 6.11. Using scoped error handling to catch the failure. If they trigger a trap, execution will resume in the CHERIOT_HANDLER block. This uses printf to print a message independent of the debug mode.

	bool someCondition = false;
	CHERIOT_DURING
	{
		Debug::Assert(someCondition,
		              "Assertion failed, condition is {}",
		              someCondition);
	}
	CHERIOT_HANDLER
	{
		printf("Assertion triggered error handler\n");
	}
	CHERIOT_END_HANDLER

	CHERIOT_DURING
	{
		Debug::Invariant(someCondition,
		                 "Invariant failed, condition is {}",
		                 someCondition);
	}
	CHERIOT_HANDLER
	{
		printf("Invariant triggered error handler\n");
	}
	CHERIOT_END_HANDLER

Listing 64. Assertions and invariants with the debugging APIs.examples/debug_helpers/example.cc

If you configure this example with the --debug-debug_compartment=y flag, this section will output something like the following:

example.cc:115 Assertion failure in entry
Assertion failed, condition is false
Assertion triggered error handler
example.cc:122 Invariant failure in entry
Invariant failed, condition is false
Invariant triggered error handler

The first line of each failure is printed by the assertion or invariant itself, the second is the log message. The next line is the printf. If you build it with --debug-debug_compartment=n then the only line of output from this section should be:

Invariant triggered error handler

The invariant is still checked and is triggering a trap, which leads to an unwind, but no debug APIs are printing messages.

In some cases, you may find that the expression that calculates the assertion condition is expensive and the compiler does not successfully optimise it away in release builds. If the checks calls functions in other compilation units, for example, or reads from volatile memory, the compiler cannot remove them even if the result is unused. To avoid this, you can replace the condition with a lambda that takes no arguments and returns a bool. The lambda is never executed in release builds and so the compiler will strip it away.

9.5. Using the debug APIs from C

The log APIs are designed to be used from C++ but C11's _Generic keyword made it possible to expose a subset of the functionality into C as well. C++ templates allow users to provide their own specialisations. This is sadly not possible in C and so the logging APIs can print only primitive types. Listing 65 shows the C versions of the C++ APIs from this chapter.

	CHERIOT_DEBUG_LOG("C example",
	                  "Printing a number {} and a string {}",
	                  42,
	                  "hello from C");
	CHERIOT_DURING
	{
		CHERIOT_INVARIANT(
		  false, "Invariant check in C failed: {}", 12);
	}
	CHERIOT_HANDLER
	{
		printf("Invariant triggered unwind in C\n");
	}
	CHERIOT_END_HANDLER

Listing 65. Assertions and invariants with the debugging APIs.examples/debug_helpers/example.c

Running this will print the following:

C example: Printing a number 42 and a string hello from C
example.c:12 Invariant failure in print_from_c
Invariant check in C failed: 12
Invariant triggered unwind in C

In C++, these are enabled conditionally based on a template parameter. In C, the macros are defined in such a way that you can wrap them in your own macros, which provide the context parameter and may be conditional.