Debugging

From Miosix Wiki
Revision as of 21:22, 10 May 2026 by Fede.tft (talk | contribs)
Jump to navigation Jump to search

Miosix supports multiple debugging methods, useful in different use cases.

Enabling assertions in the kernel

A proactive tool to check for potential errors in using the kernel API is to enable optional assertions in the kernel.

/// Enable additional assertions useful for debugging but adding more run-time
/// overhead. Three levels are implemented
/// None: Do not add extra checks, leading to fastest code. This is the default.
/// Application: Only add checks that are useful to debug kernel-level
/// application programming errors. Useful during application code debugging.
/// Kernel: Also add checks that are useful to debug kernel code. Useful
/// during kernel code debugging. Adds significant run-time overhead!
enum class ExtraChecks { None, Application, Kernel };
constexpr auto extraChecks=ExtraChecks::None;

The default is to disable optional assertions. This configuration, useful for a release build only performs minimal assertions in places where there is no impact on performance. You can change the assertion level by declaring the extraChecks variable to be Application, which is useful to add extra checks looking for wrong use of kernel APIs from applications. The Kernel choice is mainly meant for kernel development and mostly adds invariant checks in some kernel data structures, often causing considerable performance impact, such as turning some O(1) algorithms into O(N).

printf() debugging

On almost all boards, Miosix configures a serial port during the boot stage, which the kernel immediately uses to print boot logs. Here's an example:

Starting Kernel... Ok
Miosix v3.01 (stm32h755zi_nucleo, May  9 2026 16:53:23, gcc 15.2.0-mp4.2)
Mounting MountpointFs as / ... Ok
Mounting DevFs as /dev ... Ok
Mounting Fat32Fs as /sd ... Ok
OS Timer freq = 200000000 Hz
Available heap 511744 out of 519760 Bytes

When the boot completes and the kernelspace main() starts, the serial port is available to applications for any use, including printf() debugging. Note that also stdin (scanf() etc.) is configured and working. More information can be found in the Serial ports and printf/scanf page.

If processes are spawned, they inherit stdin, stdout and stderr from the kernel, so unless redirections are made, also the spawned processes will by default share the same serial port I/O as the kernel, and they too can use it for printf() debugging.

On the minority of boards where a serial port is not available, it is sometimes possible to use the ARM DCC (Debug Comminication Channel) to redirect printf() through the SWD debugging interface and access it with gdb/openocd. DCC support is for stdout (printf()) only, there's no stdin (scanf()).

Further reading for DCC support:

In-circuit debugging

In-circuit debugging usually takes longer to set-up, but is invaluable when debugging kernelspace application crashes and bugs that corrupt memory.

An in-circuit debugger allows to physically halt the CPU inside a microcontroller, single-step it and view all the variables at any given time. It is a powerful tool to debug software running on a microcontroller.

The only limitation of in-circuit debuggers is that they cannot be used to debug code that has to interact in real-time, as stopping the CPU blocks every activity o the kernel, including unrelated kernel threads and interrupt handlers.

When you install the Miosix Toolchain, it includes a precompiled gdb (arm-miosix-eabi-gdb for ARM microcontrollers) with support for remote debugging.

An additional tool is needed to bridge gdb and the debugger hardware, usually openocd. The following guides explain how to set up in-circuit debugging.

Debugging unexpected kernel crashes

This method allows to get a head start of unexpected bugs by tracing the source code line when it happens the first time, avoiding in some cases the need to attach a debugger and wait for the bug to trigger again.

Modern CPUs provide a fault reporting mechanism, usually through special interrupts that trigger when unexpected conditions occur, such as certain types of memory corruptions, undefined instructions, division by zero etc.

The Miosix kernel, upon encountering such fault conditions, attempts to print the program counter value at the moment of the fault, and then reboot (note that in some architectures such as ARM Cortex, bugs that corrupt the stack to a point where it becomes impossible for the CPU to save the processor state upon entering the fault interrupt leave the kernel with no program counter information, in that case the error message mentions the program counter is missing).

Applications running in kernelspace that cause such types of fault will thus cause a kernel reboot.

Here's an example of a Miosix fault handler:

***Unexpected UsageFault @ 0x08004874
Divide by zero

If you still have access to the exact elf file (main.elf) of the kernel that was flashed on the board, it's possible to trace the source code line that caused the fault from the fault address. Note that the result may not be fully precise as optimizations blur the mapping between the addresses of assembly instructions and source code lines. Rebuilding the kernel with optimizations disabled solves the issue, but you'd better off just attaching an in-circuit debugger in this case. Some architectures also employ a write buffer, so a write instruction to a nonexistent address will only be caught by the architecture a few instructions later. The kernel will try to warn you when the printed address is imprecise.

Once you have the address of the fault and the kernel elf file, tracing the source code line can be done by combining two tools which are part of the Miosix Toolchain, arm-miosix-eabi-addr2line and arm-miosix-eabi-c++filt (replace 0x08004874 with the address of your fault):

$ arm-miosix-eabi-addr2line 0x08004874 -e main.elf -f | arm-miosix-eabi-c++filt
main()
main.cpp:30

Reference:

Debugging unexpected process crashes

Whenever a userspace process crashes, the kernel will by default print an error message to the default serial port, similar to a kernel fault handler. However, the kernel will not reboot as memory protection allows the kernel to keep running even in case of memory corruptions in userspace processes.

A similar technique can be used combining arm-miosix-eabi-addr2line and arm-miosix-eabi-c++filt to trace the source code line from the fault address.

TODO: a tutorial is needed

Using gdb with userspace processes

Using an in-circuit debugger to debug userspace processes is known not to work, as the fluid kernel memory model is not understood by debuggers. Moreover, an in-circuit debugger halts the entire kernel which is often undesirable as it interferes with interrupt handlers and makes it impossible to debug code that has to interact in real-time.

Coming soon: We are currently developing an in-kernel gdb server that will allow to debug individual processes while leaving the kernel running and able to keep processing real-time code.