M0AGX / LB9MG

Amateur radio and embedded systems

Today I learned: How to read address zero on RISC-V

I have been working on a "yet another" flashloader with standard flash operations like readback and blank checks. The chip has flash mapped starting from address 0x0 so I wrote the code with obvious pointers like uint32_t *p = 0x0;. I built the code with gcc like countless times before but I did not get the behaviour I expected. The only thing "special" this time was that the CPU was a RISC-V.

Of course gcc generated a warning about a null pointer but I could deal with the warning later. When running the code the debugger always stopped on an ebreak instruction. ebreak in RISC-V ISA is the equivalent of bkpt from the ARM world. Simply stop and signal the debugger. But I did not put any breakpoint instructions?!

objdump revealed that the blank check function compiled basically into a breakpoint and nothing else:

1
2
3
4
5
Disassembly of section .text.blank_check:

00000000 <blank_check>:
   0:   00002783                lw      a5,0(zero) # 0 <blank_check>
   4:   9002                    ebreak

A quick search lead me to a discovery of a gcc optimization feature controlled by -fdelete-null-pointer-checks and its counterpart -fno-delete-null-pointer-checks. With -fdelete-null-pointer-checks the compiler can "optimize" any known null pointer accesses to traps (breakpoints). This is a pretty nice thing if dereferencing a pointer to 0x0 is undefined behaviour but on my chip address zero is perfectly legal.

It turns out that, depending on the CPU architecture, an access to address zero is or is not treated "suspiciously" by gcc. The documentation clearly mentions the null check feature is not available on AVR and MSP430. Apparently it is also not switched on by default on Cortex-M (otherwise I would have encountered it before). Guess with which exact three architectures I had previous bare-metal experience before starting development for RISC-V?

It turns out that for RISC-V gcc a pointer to zero is more null than on other architectures. 🙂

Solution

It seems that the smallest granularity that optimization options can be applied to is a complete function. I wanted to change the default gcc behaviour as little as possible so I did not change any global build flags but instead kept my "special" pieces of code short and only applied the attribute to a few functions.

Accessing address zero is still pretty unusual outside of flashloaders & bootloaders (and maybe startup code) so I am very happy to keep the check enabled and I like that potentially bad code screams with an ebreak.

The complete function attribute is __attribute__((optimize("no-delete-null-pointer-checks"))). For example the flash blank check code looks similar to this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
__attribute__((optimize("no-delete-null-pointer-checks"))) bool blank_check(void) {
    uint32_t *p = 0x0;
    while ((uint32_t)p < FLASH_SIZE) {
        if (*p != 0xFFFFFFFF) {
            return false;
        }
        p++;
    }
    return true;
}