Skip to content

[Bug] Ghidra RISC-V lanes lose DWARF and no-return facts for ESP-IDF flash code #8461

Open

Description

@Entrpi

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Native analysis or decompilation

Observed behavior

On main 6e9ee97a6 with Ghidra 12.1.4, the RISC-V ilp32 lane (#8229) admits ESP-IDF v6.1's hello_world for ESP32-C6, but Ghidra loads the image's .flash.text into an overlay address space. rea function v61-c6.elf app_main --provider ghidra --json reports app_main at .flash.text:0x42008398 as undefined __rea_ilp32 app_main(), although the image's DWARF declares void app_main(void). Its pseudocode continues past the final esp_restart() call into the next function, riscv_decode_offset_from_jal_instruction. No limitation mentions this.

A Ghidra probe that used REA's import arguments and post-scripts in analyzeHeadless measured the wider effect in the same image. Only 18 of the 549 DWARF-described flash functions carry an imported signature, and 16 of those 18 are C library names whose parameters come from Ghidra's archives; in IRAM, 405 of 405 do. esp_restart and panic_restart are not flagged no-return, so pseudocode continues past 4 call sites in 4 functions. Ghidra creates a 1-byte shadow function in the reservation for each of the 549 flash functions. A second probe found 48 computed calls from .iram0.text that resolve to those shadows. ESP-IDF v5.5.2's ESP32-H4 image, run the same way with the post-scripts of draft #8401's ilp32f lane, shows the same: 6 of 278 DWARF-described flash functions have an imported signature, there are 278 shadows, and 27 computed calls from .iram0.text resolve to them. There, rea function v552-h4.elf 0x42002ab6 (the ELF address of app_main) on #8401's branch selects a shadow: it exits 0 with halt_baddata() pseudocode.

Reproduction

  1. Build ESP-IDF v6.1 examples/get-started/hello_world for esp32c6 with the default configuration.
  2. Run rea function build/hello_world.elf app_main --provider ghidra --json with Ghidra 12.1.4.
  3. Compare normalized_result.procedure.address with the ELF symbol address of app_main, and read the end of normalized_result.pseudocode.

This reproduced on every run of the C6 image. A source-owned fixture in the linked draft PR (tests/conformance/c/riscv-esp-layout.c linked with tests/conformance/esp/esp32c6-shared-window.ld) shows the same on main: rea_riscv_esp_main at .flash.text:0x42000064, pseudocode that ends rea_riscv_esp_restart(); rea_riscv_esp_sink = 0x5a5a; (the next function's store), and rea_riscv_esp_sum(int param_1,int param_2) without its DWARF parameter names.

Expected behavior

Flash code is analyzed at its ELF address with its DWARF facts, as IRAM code already is. app_main should appear at 0x42008398 as void app_main(void), its pseudocode should end at esp_restart();, and there should be no shadow functions. When REA cannot establish a correct memory map for a layout, it should refuse the image or state a limitation, not return analysis of a partial map. docs/ghidra-riscv.md does not mention this case (source).

Impact and scope

Affected: every ESP-IDF image whose linker script places flash text and rodata in one virtual window and reserves the text's addresses with the NOBITS .flash_rodata_dummy. ESP-IDF v5.5.2's linker scripts do this for ESP32-C5, C6, C61, H2, H21 and H4: sections.ld.in defines the reservation, and memory.ld.in asserts that it starts the rodata segment (source). C2 and C3 put rodata in a separate window and should be unaffected (source, not run). The merged ilp32 lane admits C5, C6, C61, H2 and H21.

Observed in Ghidra on C6 (REA's CLI and analyzeHeadless) and H4 (analyzeHeadless, plus the selector run on #8401's branch):

  • most DWARF-described flash functions get no imported signature, and so no DWARF local variables either (source-derived);
  • esp_restart and panic_restart get no no-return flag;
  • shadow functions appear;
  • computed calls from IRAM to flash resolve to the shadows (the probe counts above);
  • ELF-address selectors select a shadow (H4);
  • pseudocode continues past no-return calls.

Observed with readelf -SW on ESP-IDF v5.5.2 hello_world builds for C5, C61, H2 and H21 (C5 and C61 also with PSRAM enabled): all reserve the text with .flash_rodata_dummy at 0x42000020. The C5 and C61 builds add a second NOBITS section, .ext_ram.dummy, at the same address; it covers the text and the rodata (0x1ffe0 and 0x17fe0 bytes). On main 6e9ee97a6, REA's CLI reports the C5 build's app_main at .flash.text:0x42006430 and the H2 build's at .flash.text:0x420077b4; neither pseudocode ends at the final esp_restart() call (observed). ESP-IDF v5.5.2 defines .ext_ram.dummy unconditionally in components/esp_system/ld/esp32c5/sections.ld.in:490 and esp32c61/sections.ld.in:335 (source).

Source-derived, not run:

REA version

Source main 6e9ee97a6. The H4 measurement used draft #8401 at ba174afe6.

Runtime and platform

Debian container on x86-64, Ghidra 12.1.4 PUBLIC, OpenJDK 21, Node.js 24.22.1.

Target and artifact context

  • ESP32-C6: ESP-IDF v6.1 hello_world (v61-c6.elf, sha256 b61ed35815f56287f5ae32454e9aaf75f600e3772a11079fac6f7b21e8cb8794), ELF32 RISC-V ET_EXEC, soft-float. DWARF 4 from GCC 15.2.0 (-gdwarf-4 -ggdb -Og).
  • ESP32-H4: ESP-IDF v5.5.2 hello_world (v552-h4.elf, sha256 e9b477a3f32cc273498baf8ff04b46d135b1f4a56198d397fd33e2919240025f), single-float, DWARF from GCC 14.2.0.
  • Layout: in both images, .flash.text (PROGBITS) and .flash_rodata_dummy (NOBITS) start at 0x42000020, in two PT_LOAD segments.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed with readelf -SW v61-c6.elf:

  [15] .flash.text         PROGBITS  42000020 00c020 00fff2 00  AX  0   0  2
  [16] .flash_rodata_dummy NOBITS    42000020 01c020 018000 00  WA  0   0  1

Observed end of app_main's pseudocode on main 6e9ee97a6 (rea function v61-c6.elf app_main --provider ghidra --json):

  if (((*puVar4 & 0x7f) != 0x6f) && ((*puVar4 >> 7 & 0x1f) != 0)) {
                    /* WARNING: Subroutine does not return */
    abort();
  }
  uVar8 = *puVar4;
  return ((int)(uVar8 << 1) >> 0x16 | ((int)(uVar8 << 0xb) >> 0x1f) << 10 |
         ((int)(uVar8 << 0xc) >> 0x18) << 0xb | ((int)uVar8 >> 0x1f) << 0x13) << 1;
}

The same session's limitation reads "RISC-V calling-convention outcome: 1502 functions and 541 function definitions use __rea_ilp32". The analyzeHeadless probe of the same image found 1,506 functions, 549 of them shadows.

Typed outcome or recovery expected

Either the flash code of an admitted image is analyzed in the default address space, or the session is refused, or it states a limitation that names the overlap.

Analysis or suspected cause

Source-derived, Ghidra 12.1.4:

  • MemorySectionResolver processes overlapping sections in reverse order: "last-in takes precedence" (MemorySectionResolver.java:281). So the NOBITS reservation takes the default address space, and .flash.text becomes an overlay.
  • DWARFProgram.getCodeAddress maps every DWARF address into the default space. There, DWARFFunction.syncWithExistingGhidraFunction finds no executable block and skips the function (DWARFFunction.java:369). The only call to setNoReturn is in the signature update (DWARFFunction.java:480), so the skipped functions never get the flag.
  • Call-frame (FDE) entries and constant references from IRAM resolve in the default space too. That is where the shadow functions come from.

Additional context

Proposed fix. The fix is a draft PR from Entrpi/rea fix-ghidra-esp-idf-flash-overlay (78a413531), linked to this issue. It works in three steps:

  • REA's ELF reader checks whether an executable section shares allocated addresses with another section.
  • When one NOBITS section starts at an executable section's address and covers it, and no other section overlaps either, the RISC-V lanes commit both sections to the analysis profile.
  • A Ghidra pre-script then gives the code the default address space before auto-analysis, and keeps the reservation as an uninitialized overlay.

The draft PR refuses every other overlap that involves executable code, including the C5 and C61 builds' second reservation. It follows the GameCube DOL lane, whose pre-script rebuilds Ghidra's memory map from facts REA validated.

Ghidra 12.1.4 constraints. The pre-script works around three of them:

  • Memory.moveBlock keeps a block in its address space.
  • Removing any overlay space stops the native decompiler from registering the program.
  • ProgramDB.moveAddressRange moves the references from a range, not the references into it: the entry points and the loader's section-header reference.

Questions for the maintainer:

  1. May the RISC-V lanes prepare Ghidra's memory map before analysis for this layout, as the DOL lane does? Flash code addresses would change from .flash.text:0x… to 0x…, and the analysis profiles of these images would change.
  2. Should RISC-V lanes refuse every other section overlap that involves executable code, as the draft does, or admit such images with a limitation?
  3. Is ProgramDB.moveAddressRange acceptable in a bridge script verified against Ghidra 12.1.4? It is internal to Ghidra's database layer, and it is the call Memory.moveBlock itself makes. The alternative is to move labels, functions and entry points by hand through the public API, which covers less.
  4. ESP-IDF's C5 and C61 builds reserve the text twice, with .flash_rodata_dummy and .ext_ram.dummy, and the draft refuses them. Should a follow-up policy, verified on those builds, also prepare a second reservation such as .ext_ram.dummy, so that these images are admitted?
  5. Should a separate follow-up make computed calls to Ghidra-flagged no-return functions terminal in the Listing? auipc+jalr calls from flash to IRAM functions such as abort and __assert_func keep a Listing fall-through in 44 (H4) and 118 (C6) functions. The pseudocode is already right.

Related. #3615 (ESP32 firmware) and the embedded roadmap #7955.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions