Custom assert handling in newer SDKs

For firmware development purposes, my code makes heavy use of asserts, primarily the standard library assert() macro because that is available on all platforms that my code has to support (Zephyr, PC, etc). Most of these shared libraries use C++, including the std headers

Recently we have shifted from the v2.3.x SDK to the 2.6.x SDK and this has been relatively problem free, but the handling asserts is causing issues. I'm currently unsure if this is an NCS / Zephyr / ... problem so starting here and will move down as required

The issue appears to be based around the move to picolibc which was one of the benefits which motivated the shifting forward of SDK versions and if possible we would like to not have to shift back to newlib

With newlib, the assert handler is a weak symbol so we can override that directly and have our custom handler store all the relevant details to deal with post reboot. picolibc has similar functionality but the toolchain / precompiled picolibc just drops all the relevant info in favour of smaller binary sizes (which is totally understandable but does make asserts a real pain to debug)

That seems reasonable, so we'll compile piclibc ourselves using the handy module so we can tweak the build options. Except the module doesn't include a C++ standard library so none of our library code compiles (it also doesn't have an obvious way of replacing the assert handler with our custom one?). Clearly it can handle a C++ build, because the toolchain version does so what am I missing?

Summary

  • toolchain picolibc - no useful assert() logging at all
  • module picolibc - no C++ std includes, how to override the full assert handler for custom reporting?
  • newlib - larger, slower

It seems like the module version is almost what I require but the specifics of the required configuration are not obvious

  • Hi,

    One option is to define your own `ASSERT` macro in a file included by all files that needs this macro. This macro can do whatever you want to do, depending on which platform you are building for. Then you have full control.

    That said, Zephyr has very good support for asserts. Documentation is here: Fatal Errors — Zephyr Project Documentation. In short, add the following to enable asserts: `CONFIG_ASSERT=y`.

    I have not used picolibc myself, but in `lib/libc/picolibc/assert.c` it looks like Zephyr has implemented the `__assert_func` hook for you already so you should not have to compile picolibc yourself. I am assuiming that the `assert()` function/macro you are using ends up in that hook when using picolibc.

    Now, if you are using the asserts from Zephyr you can configure it to your liking. You can for example override the function `assert_post_action()` defined as a weak symbol in `lib/os/assert.c`.

    The default `assert_post_action()` calls `k_oops()` or `k_panic()` which will eventually generate a call to `z_fatal_error()` and finally `k_sys_fatal_error_handler()` which is defined as a weak function in `kernel/fatal.c`. You can ovveride that one as well to your liking.

    It should be quite straight forward to test this by manually trigger an assert in different contexts and use a debugger to follow the call hierarchy to make sure that things works as you want.

    Note that `z_fatal_error()` can be called for other reasons than because of an assert. So you should make sure you can examine the output from it in some way on your platform.

  • `assert_post_action` was what I thought the answer would be but `assert()` (`__assert_func()`) gets redirected to here: https://github.com/zephyrproject-rtos/zephyr/blob/main/lib/libc/picolibc/assert.c#L22 in the packaged libc. That means `assert_post_action` always reports that handler function, NOT the actual call site (same issue with the fatal error handler. It points to the handler hook)

    `CONFIG_PICOLIBC_ASSERT_VERBOSE` only seems to work if I include the module source, but also doesn't help me much because I need to intercept the file/line and the call to `__ASSERT` means the `assert_post_action` again gets the handler location, not the source location

    • Note that I agree with the default choice here to minimise footprint by dropping the assert data. I'm targeting a larger MCU (5340) so would prefer a full diagnostic interface

    Zephyrs assert macros are fine if you're developing solely inside Zephyr. I'm sharing code across several projects only some of which Zephyr applies to. Additionally, I have several libraries I do not control that use `assert()` in debug configurations and would require intrusive modifications for a custom assert

    Newlib works because I can just directly override `assert_func(...)` before any info is lost. This isn't an option with the current picolibc hooks (and additionally, I still need to compile it from source to get the verbose handler, which then runs into C++ issues)

  • I see what you mean. Your (and mine) expectation was perhaps that the function `__assert_func()` in the file you were referring to, instead of ending up calling:

    assert_post_action(__FILE__, __LINE__);

    would have made the following call:

    assert_post_action(file, line);

    Such a patch could make sense and is probably the easiest way for you to make progress on this.

    Here is another approach:

    By enabling `CONFIG_PICOLIBC_ASSERT_VERBOSE` you should get a very verbose error message printed on an assert, which includes the filename and line number of the initial assert, right?

    Now, perhaps you can instead just catch this output and put this message in your error log buffer?

    That should be possible to do by disabling `CONFIG_LOG_PRINTK` and configuring printk output using the function `__printk_hook_install()`.

    That said, I am afraid I did not get what you meant with this:

    `CONFIG_PICOLIBC_ASSERT_VERBOSE` only seems to work if I include the module source

    Perhaps that is something that makes things even more complicated in reality than what I can see when I just look at the source code (instead of actually testing anything). 

  • Such a patch could make sense and is probably the easiest way for you to make progress on this.

    I think this is the answer. The issue is how Zephyr has chosen to integrate with picolibc. After further investigation I don't believe this is a missing option or other such issue that I was initially assuming it could be. I'll report this directly to Zephyr

    you can instead just catch this output and put this message in your error log buffer?

    Redirecting the log and analysing individual messages is a very heavyweight and indirect solution to what I was attempting to do so this would be an absolute last resort

    Perhaps that is something that makes things even more complicated in reality than what I can see when I just look at the source code (instead of actually testing anything)

    It's not obvious in the linked file but `PICOLIBC_ASSERT_VERBOSE` is only a valid option when building picolibc from source (which then runs into my C++ issue). see the Kconfig dependency here between `PICOLIBC_USE_MODULE` and `PICOLIBC_ASSERT_VERBOSE`

Related