Sitemap

RISC-V Hardfault Analysis Tool, RTTHREAD-RVBACKTRACE Principle Explanation

8 min readMay 29, 2025

--

This article is contributed by RTTXiaoyao.

Introduction

This article mainly discusses the core principles behind RV BACKTRACE.

If you haven’t worked with rvbacktrace before, you can refer to the following two articles to understand how to use RVBACKTRACE:

● RVBacktrace RISC-V Minimal Stack Trace Component: https://club.rt-thread.org/ask/article/64bfe06feb7b3e29.html

● RVBacktrace RISC-V Minimal Stack Trace Component V1.2: https://club.rt-thread.org/ask/article/09737357e4a95b06.html

RVBACKTRACE

https://github.com/Yaochenger/RvBacktrace

RVBACKTRACE leverages some features specific to RISC-V.

The component supports two methods for stack backtracing, with the default being the simpler method one.

Method 1: No additional compile parameters are needed; it performs stack backtrace by analyzing the stack structure.

Advantages: Does not occupy extra system registers.

Disadvantages: Increases code size and has lower efficiency compared to Method 2.

Method 2: Adds compile parameters to perform stack backtrace based on the FP (frame pointer) register.

Advantages: Nearly no increase in code size.

Disadvantages: Occupies the s0 register.

rvbacktrace offers two ways to do this: one is by adding the `-fno-omit-frame-pointer` flag during compilation; the other is relying on the compiler’s default behavior, which may optimize away the frame pointer.

Understanding `-fno-omit-frame-pointer`

`-fno-omit-frame-pointer` means “do not omit” or “do not optimize away” the frame pointer, which is a register representing the frame pointer.

By default, compilers use `-fomit-frame-pointer`, which omits the frame pointer for optimization.

Let’s compare the two configurations:

● One with optimization to omit the frame pointer (`-fomit-frame-pointer`) — the compiler may not save the frame pointer.

● The other with `-fno-omit-frame-pointer` — the compiler preserves the frame pointer.

Difference between optimized and non-optimized frame pointers:

Here’s an example of a function compiled with `-fno-omit-frame-pointer`:

void rvbacktrace_fno()
{
8000e02c: 1141 addi sp,sp,-16
8000e02e: c606 sw ra,12(sp)
8000e030: c422 sw s0,8(sp)
8000e032: c226 sw s1,4(sp)
8000e034: 0800 addi s0,sp,16

This is a function compiled without `-fno-omit-frame-pointer`:

void rvbacktrace_fno(){8000d7c6:    1141                    addi    sp,sp,-168000d7c8:    c606                    sw    ra,12(sp)8000d7ca:    c422                    sw    s0,8(sp)

What is the difference? It has two additional instructions.

8000e032:    c226                    sw    s1,4(sp)8000e034:    0800                    addi    s0,sp,16

Let’s briefly explain the principle:

After adding -fno-omit-frame-pointer, the s0 register can no longer be used as a general-purpose register; it can only be used as the frame pointer (FP), which stores the initial value of sp (stack pointer) before entering the function.

The frame pointer is used to store the address of the previous sp, and this address can be used to access the previous function’s data.

The ra address is similar to the Link Register (LR) in ARM, representing the address of the return function.

From the diagram below, you can see that after adding -fno-omit-frame-pointer, the default current code’s s0 is the value of sp when entering the function. Additionally, the two fixed registers store the RA and FP, and their positions do not change.

Therefore, for the core functions in `rv_backtrace_fno.c`, you can write them in the following way.

        sp = (unsigned long) _backtrace_threadn->sp;        fp = ((rt_ubase_t *) (_backtrace_threadn->sp))[BACKTRACE_FP_POS]; // get current frame pointer        while (1)        {            frame = (struct stackframe *) (fp - BACKTRACE_LEN); //   get frame pointer            if ((uint32_t *) frame > (uint32_t *) (uintptr_t) _rt_eusrstack)            {                rvstack_frame_len = num;                rvbacktrace_addr2line((uint32_t *) &rvstack_frame[0]);                num = 0;                break;            }            sp = fp;  // get stack pointer            fp = frame->s_fp; // get frame pointer            ra = frame->s_ra; // get return address            pc = frame->s_ra - 4; // get program counter            //  print stack interval, return address, program counter            BACKTRACE_PRINTF("[%d]Stack interval :[0x%016lx - 0x%016lx]  ra 0x%016lx pc 0x%016lx\n", num, sp, fp, ra, pc);            rvstack_frame[num] = pc; // save stack frame address            num++;        }

General modification method for RVBACKTRACE:

When the compiler has not added `-fno-omit-frame-pointer`, the backtrace needs to find the return address starting from the PC.

The first step is to locate instruction 1141, then extract the immediate value 16, and based on the current `sp` value, compute the previous `sp` value before entering the function.

void rvbacktrace_fno(){8000d7c6:    1141                    addi    sp,sp,-168000d7c8:    c606                    sw    ra,12(sp)8000d7ca:    c422                    sw    s0,8(sp)

In the function `riscv_backtraceFromStack`, the purpose is to calculate the `sp` value. The calculation is also designed to accommodate both 64-bit and 32-bit RISC-V architectures, determining the original `sp` value. After that, it continues to search upward for the corresponding functions and saves the information accordingly.

   /* 1. scan code, find lr pushed */    for (i = 0; i < BT_FUNC_LIMIT;) {        /* FIXME: not accurate from bottom to up. how to judge 2 or 4byte inst */        //CodeAddr = (char *)(((long)PC & (~0x3)) - i);        //非对齐访问        CodeAddr = (char *)(PC - i);        ins32 = *(unsigned int *)(CodeAddr);        if ((ins32 & 0x3) == 0x3) {            ins16 = *(unsigned short *)(CodeAddr - 2);            if ((ins16 & 0x3) != 0x3) {                i += 4;                framesize = riscv_backtrace_framesize_get1(ins32);                if (framesize >= 0) {                    CodeAddr += 4;                    break;                }                continue;            }        }        i += 2;        ins16 = (ins32 >> 16) & 0xffff;        framesize = riscv_backtrace_framesize_get(ins16);        if (framesize >= 0) {            CodeAddr += 2;            break;        }    }    if (i == BT_FUNC_LIMIT) {        /* error branch */        #ifdef BACKTRACE_PRINTF            BACKTRACE_PRINTF("Backtrace fail!\r\n");        #endif        return -1;    }    /* 2. scan code, find ins: sd ra,24(sp) or sd ra,552(sp) */    for (i = 0; CodeAddr + i < PC;) {        ins32 = *(unsigned int *)(CodeAddr + i);        if ((ins32 & 0x3) == 0x3) {            i += 4;            offset = riscv_backtrace_ra_offset_get1(ins32);            if (offset >= 0) {                break;            }        } else {            i += 2;            ins16 = ins32 & 0xffff;            offset = riscv_backtrace_ra_offset_get(ins16);            if (offset >= 0) {                break;            }        }    }

This code, based on the current PC value, determines the address where the current function’s stack frame starts. It calculates the start and end addresses of each `sp`, then finds the previous PC value, and continues searching upward for the previous function.

Usage of `-fno-omit-frame-pointer` in RVBACKTRACE

After adding the compiler flag `-fno-omit-frame-pointer`, the current `sp` value is recorded before entering a function. Through verification, it has been found that this option is enabled by default on most RISC-V 64-bit platforms. Therefore, for RISC-V64, this method can be used, and it’s estimated that RISC-V64 platforms are generally more common or larger in scale.

At this point, we need to use the `rv_backtrace_fno.c` file to handle stack backtracing.

In RVBACKTRACE, you need to enable the macro `BACKTRACE_USE_FP`.

void rvbacktrace_fno(){8000e02c:    1141                    addi    sp,sp,-168000e02e:    c606                    sw    ra,12(sp)8000e030:    c422                    sw    s0,8(sp)8000e032:    c226                    sw    s1,4(sp)8000e034:    0800                    addi    s0,sp,16

The value of `S0` is used to store stack information. By default, this register is dedicated for stack backtracing.

The main implementation function involves obtaining the value of the frame pointer (`fp`) using `__builtin_frame_address`, which is a function from libc.

    fp = (unsigned long)__builtin_frame_address(0); //  get current frame pointer    while (1)    {        frame = (struct stackframe *)(fp - BACKTRACE_LEN); //   get frame pointer        if ((uint32_t *)frame > (uint32_t *)(uintptr_t)_rt_eusrstack)        {            rvstack_frame_len = num;            return;        }        sp = fp;  // get stack pointer        fp = frame->s_fp; // get frame pointer        ra = frame->s_ra; // get return address        pc = frame->s_ra - 4; // get program counter        //  print stack interval, return address, program counter        BACKTRACE_PRINTF("[%d]Stack interval :[0x%016lx - 0x%016lx]  ra 0x%016lx pc 0x%016lx\n", num, sp, fp, ra, pc);        rvstack_frame[num] = pc; // save stack frame address        num++;    }

Let’s look at the disassembing.

 36           fp = (unsigned long)__builtin_frame_address(0); //  get current frame pointer8000b09c:   mv      s2,s0

Actually, it simply involves reading the value of `s0`.

Since `s0` stores the stack address when entering the function, it becomes easy to determine the initial `sp` address at entry, and also convenient to find the previous `ra` (return address) and `s0`. Implementing this is straightforward and doesn’t require continuous disassembly parsing.

Other thread backtracing methods are similar.

Assembly Instruction Filtering Method

After understanding the above two methods, the more challenging part is, when no compile options are used, how to find the stack address based on the PC pointer.

void rvbacktrace_fno(){8000d7c6:    1141                    addi    sp,sp,-168000d7c8:    c606                    sw    ra,12(sp)8000d7ca:    c422                    sw    s0,8(sp

For example, in the above function, we need to identify the commands:

● `addi sp, sp, -16`

● `sw ra, 12(sp)`

After reviewing the RISC-V manual, we find the following instructions:

Of course, these are compressed instructions.

The main point is to extract the immediate value (`imm`) from these instructions.

Reference link:

https://riscv.github.io/riscv-isa-manual/snapshot/unprivileged/#_integer_register_immediate_instructions

static int riscv_backtrace_framesize_get1(unsigned int inst){    unsigned int imm = 0;    /* addi sp, sp, -im     * example     * d1010113             addi    sp,sp,-752     * from spec addi FROM https://riscv.github.io/riscv-isa-manual/snapshot/unprivileged/#_integer_register_immediate_instructions     * bit[31:20] = imm[11:0]     * bit[19:15] = 00010     * bit[14:12] = 000     * bit[11:7]  = 00010     * bit[6:0]  = 0010011     */    if ((inst & 0x800FFFFF) == 0x80010113) {        imm = (inst >> 20) & 0x7FF;        imm = (~imm & 0x7FF) + 1;#if __riscv_xlen == 64        return imm >> 3; // RV64: 以 8 字节为单位#else        return imm >> 2;  // RV32: 以 4 字节为单位#endif    }    return -1;}

CM BACKTRACE

https://github.com/armink-rtt-pkgs/CmBacktrace

Core code of CM backtrace:

            /* first depth is PC */            buffer[depth++] = regs.saved.pc;            /* fix the LR address in thumb mode */            pc = regs.saved.lr - 1;            if ((pc >= code_start_addr) && (pc <= code_start_addr + code_size) && (depth < CMB_CALL_STACK_MAX_DEPTH)                    && (depth < size)) {                buffer[depth++] = pc;                regs_saved_lr_is_valid = true;            }size_t cm_backtrace_call_stack_any(uint32_t *buffer, size_t size, uint32_t sp, uint32_t stack_start_addr, uint32_t stack_size){    uint32_t pc;    size_t depth = 0;    /* copy called function address */    for (; sp < stack_start_addr + stack_size; sp += sizeof(size_t)) {        /* the *sp value may be LR, so need decrease a word to PC */        pc = *((uint32_t *) sp) - sizeof(size_t);        /* the Cortex-M using thumb instruction, so the pc must be an odd number */        if (pc % 2 == 0) {            continue;        }        /* fix the PC address in thumb mode */        pc = *((uint32_t *) sp) - 1;        if ((pc >= code_start_addr + sizeof(size_t)) && (pc <= code_start_addr + code_size) && (depth < CMB_CALL_STACK_MAX_DEPTH)                /* check the the instruction before PC address is 'BL' or 'BLX' */                && disassembly_ins_is_bl_blx(pc - sizeof(size_t)) && (depth < size)) {            /* the second depth function may be already saved, so need ignore repeat */            buffer[depth++] = pc;        }    }    return depth;}static bool disassembly_ins_is_bl_blx(uint32_t addr) {    uint16_t ins1 = *((uint16_t *)addr);    uint16_t ins2 = *((uint16_t *)(addr + 2));#define BL_INS_MASK         0xF800#define BL_INS_HIGH         0xF800#define BL_INS_LOW          0xF000#define BLX_INX_MASK        0xFF00#define BLX_INX             0x4700    if ((ins2 & BL_INS_MASK) == BL_INS_HIGH && (ins1 & BL_INS_MASK) == BL_INS_LOW) {        return true;    } else if ((ins2 & BLX_INX_MASK) == BLX_INX) {        return true;    } else {        return false;    }}

Use an array `buffer[]` to store the corresponding PC addresses and backtrace addresses.

Then, starting from the LR (Link Register), check the value `PC — 1`.

If the value falls within the code segment’s start and end addresses, add it to the backtrace buffer.

Summary:

RVBACKTRACE is a helpful tool for stack backtracing. If you find it useful during your testing, feel free to give the repository a star.

If you have any suggestions, you can also open issues or submit pull requests.

https://github.com/Yaochenger/RvBacktrace

--

--

RT-Thread IoT OS
RT-Thread IoT OS

Written by RT-Thread IoT OS

An Open-Source Community-Powered Real-Time Operating System (RTOS) Project! Let’s develop, DIY, create, share, and explore this new IoT World together!

No responses yet