Repository navigation
[Bug] Ghidra RISC-V lanes lose DWARF and no-return facts for ESP-IDF flash code #8461
Description
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
- Build ESP-IDF v6.1
examples/get-started/hello_worldforesp32c6with the default configuration. - Run
rea function build/hello_world.elf app_main --provider ghidra --jsonwith Ghidra 12.1.4. - Compare
normalized_result.procedure.addresswith the ELF symbol address ofapp_main, and read the end ofnormalized_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_restartandpanic_restartget 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:
- byte search without bounds skips overlay blocks, so it skips all flash code;
- the ilp32f lane's signature refusal check (feat(ghidra): admit RISC-V ilp32f ELF with a derived calling convention #8401) cannot see the DWARF types of flash functions.
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, sha256b61ed35815f56287f5ae32454e9aaf75f600e3772a11079fac6f7b21e8cb8794), ELF32 RISC-VET_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, sha256e9b477a3f32cc273498baf8ff04b46d135b1f4a56198d397fd33e2919240025f), single-float, DWARF from GCC 14.2.0. - Layout: in both images,
.flash.text(PROGBITS) and.flash_rodata_dummy(NOBITS) start at0x42000020, in twoPT_LOADsegments.
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:
MemorySectionResolverprocesses overlapping sections in reverse order: "last-in takes precedence" (MemorySectionResolver.java:281). So the NOBITS reservation takes the default address space, and.flash.textbecomes an overlay.DWARFProgram.getCodeAddressmaps every DWARF address into the default space. There,DWARFFunction.syncWithExistingGhidraFunctionfinds no executable block and skips the function (DWARFFunction.java:369). The only call tosetNoReturnis 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.moveBlockkeeps a block in its address space.- Removing any overlay space stops the native decompiler from registering the program.
ProgramDB.moveAddressRangemoves the references from a range, not the references into it: the entry points and the loader's section-header reference.
Questions for the maintainer:
- 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…to0x…, and the analysis profiles of these images would change. - Should RISC-V lanes refuse every other section overlap that involves executable code, as the draft does, or admit such images with a limitation?
- Is
ProgramDB.moveAddressRangeacceptable in a bridge script verified against Ghidra 12.1.4? It is internal to Ghidra's database layer, and it is the callMemory.moveBlockitself makes. The alternative is to move labels, functions and entry points by hand through the public API, which covers less. - ESP-IDF's C5 and C61 builds reserve the text twice, with
.flash_rodata_dummyand.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? - Should a separate follow-up make computed calls to Ghidra-flagged no-return functions terminal in the Listing?
auipc+jalrcalls from flash to IRAM functions such asabortand__assert_funckeep 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