SuperAPI AI Directory

re-riscv

General & other

View repository

Updated 10-16 05:06

About this skill

RISC-V architecture reverse engineering (RV32/RV64, RVC, gp-relative addressing)

When to use / when not to use

  • Use: RV32/RV64 architecture binaries - Linux user-space programs, bare-metal firmware, RISC-V MCU firmware (ESP32-C3 class RV32IMC)
  • Use: compressed instruction (RVC) mixed code streams, gp-relative addressing recovery, RISC-V ABI identification, ecall system call/environment call boundaries
  • Use: toolchain and library function fingerprints (glibc/musl/newlib family, instruction extension features)
  • Don't use: other architectures such as ARM / x86 / MIPS (go through the [[re-binary-core]] general foundation; this skill only supplements RISC-V-specific semantics)
  • Don't use: when the toolchain is extremely customized and there's no lead, first go through [[re-fw-emulate]] emulation as a fallback (if it can run, observe behavior first, then return to static)
  • Don't use: when you only need to unpack firmware to see contents ([[re-fw-extract]]) / analyze filesystem and configuration ([[re-fw-rootfs]]) / boot firmware as a whole ([[re-fw-emulate]])

Tool preparation

Verify all tools before use. Static analysis may skip the sandbox; qemu dynamic execution defaults to sandbox + network isolation ([[re-analyze/platform-tips]] highest principle).

Decompiler (Ghidra / IDA, choose one)

  • Ghidra (built-in RISC-V processor module, RV32/RV64 + I/M/A/F/D/C extensions, automatically handles RVC compressed instructions):
    • Linux: official release package (requires JDK); some distribution repositories have apt install ghidra / pacman -S ghidra
    • macOS: brew install --cask ghidra; Windows: official zip
    • Verify: analyzeHeadless -help(headless mode) or import a RISC-V ELF via the GUI
  • IDA: commercial edition includes a RISC-V module; Freeware edition architecture support scope is subject to the official page
  • When importing, confirm RV32/RV64 and endianness - standard RISC-V is little-endian, big-endian variants are rare, and choosing wrong makes everything misaligned

readelf / file - architecture and byte order confirmation (binutils)

  • Linux: included with binutils (apt install binutils etc.); macOS: brew install binutils or LLVM-based readelf; Windows: inside WSL
  • Verify: readelf --version

binutils cross toolchain - RISC-V disassembly/symbol table (objdump)

  • Debian/Ubuntu: apt install binutils-riscv64-linux-gnu(provides riscv64-linux-gnu-objdump; add to the compile chain gcc-riscv64-linux-gnu, gcc dependencies pull in the binutils package)
  • Fedora: dnf install binutils-riscv64-linux-gnu gcc-riscv64-linux-gnu(cross-binutils / cross-gcc source packages, provides riscv64-linux-gnu-objdump; the official repositories have no riscv64-unknown-elf bare-metal toolchain, so for bare-metal disassembly use riscv64-linux-gnu-objdump, RVC support is consistent)
  • Arch: pacman -S riscv64-elf-binutils riscv64-elf-gcc(bare-metal, provides riscv64-unknown-elf-objdump) or pacman -S riscv64-linux-gnu-binutils riscv64-linux-gnu-gcc(user-space)
  • macOS: brew install riscv64-elf-binutils riscv64-elf-gcc(homebrew-core, provides riscv64-unknown-elf-objdump); Windows/WSL: the Linux version inside WSL
  • Verify: one-to-one correspondence per installed package -binutils-riscv64-linux-gnu → riscv64-linux-gnu-objdump --version(Debian/Ubuntu/Fedora);binutils-riscv64-unknown-elf → riscv64-unknown-elf-objdump --version(Arch/brew; Debian/Ubuntu need that package listed separately when a bare-metal triplet is required); objdump automatically recognizes RVC mixed streams (2/4 bytes)

qemu-riscv64 / qemu-riscv32 - RISC-V user-space emulation (dynamic verification)

  • Linux: apt install qemu-user / dnf install qemu-user / pacman -S qemu-user
  • macOS: brew install qemu(includes user-space); Windows/WSL: the Linux version inside WSL
  • For 64-bit use qemu-riscv64, for 32-bit use qemu-riscv32;-L <rootfs> to specify the dynamic library/linker source,-strace tracks system calls
  • Verify: qemu-riscv64 --version、qemu-riscv32 --version
  • Bare-metal RISC-V (no OS) qemu-user is not applicable — use qemu-system-riscv64/32(Debian trixie onwards / Ubuntu 25.10 onwards standalone package apt install qemu-system-riscv, Ubuntu 24.04 and earlier, Debian bookworm within qemu-system-misc ; Fedora / Arch dnf install qemu-system-riscv / pacman -S qemu-system-riscv)-machine virt or board-level models (-s -S connect to gdb), or [[re-emulation]] with Unicorn instruction-by-instruction

gdb-multiarch — cross debugging

  • Linux: Debian/Ubuntu apt install gdb-multiarch(built-in for riscv32/riscv64 targets); Fedora dnf install gdb(official gdb supports multiple targets); Arch pacman -S gdb(built-in multiarch)
  • macOS: brew install gdb(requires Developer Tools authorization, see [[re-analyze/platform-tips]] macOS branch) or the Linux version inside WSL; Windows/WSL: the Linux version inside WSL
  • Verify: Debian/Ubuntu gdb-multiarch --version;Fedora/Arch gdb --version; after loading the firmware, set architecture riscv:rv64(or riscv:rv32) then info registers to confirm

Procedure

Execute in order, archive the result of each step; dynamic execution defaults to sandbox.

  1. Architecture confirmation (RV32/RV64 + endianness):

    file target.bin                 # "RISC-V" + 32-bit/64-bit + little-endian 提示
    file target.elf
    readelf -h target.elf           # Machine=RISC-V (243);Data=little-endian;Flags 含 RVC/浮点 ABI 位
    readelf -A target.elf | head    # Tag_RISCV_arch 属性,如 rv64imafdc
    
    • e_flags (readelf -h Flags field):EF_RISCV_RVC(0x1)=contains compressed instructions; floating-point ABI bitmask 0x6 (0=soft float, 2=single-precision F, 4=double-precision D, 6=quad-precision Q);EF_RISCV_RVE(0x8)=RV32E reduced registers
    • Standard RISC-V is little-endian; big-endian variants are rare, go by the actual output of file/readelf
    • Bare-metal .bin (no ELF header) is determined by the reset vector context and instruction form from step 2 (RV32 fixed-width 4 bytes vs RVC mixed 2/4 bytes)
  2. Entry point location (reset / startup code):

    • Linux user space: the entry point is ELF e_entry (readelf -h 's Entry point) = _start; dynamically linked programs go through ld.so (PT_INTERP) first, breakpoints/analysis start from _start
    • Bare metal: RISC-V has no fixed vector table (unlike ARM Cortex-M), the entry point = the reset vector from the SoC datasheet; the QEMU virt machine's reset vector is in MROM 0x1000(the reset stub then jumps to start_addr, by default DRAM 0x80000000, for pflash firmware scenarios it is 0x20000000)——don't mistake the DRAM base address for the reset entry; some MCU flash base addresses are of the form 0x08000000; the exception/interrupt vector base address is in the mtvec (M-mode) / stvec (S-mode) CSR, trap handling code is located by the CSR value
    • The startup flow is generally: set up the stack (la sp, ...) → copy .data / zero .bss → jump to main (bare metal) or __libc_start_main (Linux)
    • After locating the entry point, annotate it in the decompiler, expand the main logic along the call chain
  3. RVC compressed instruction recognition (2/4-byte mixed, do not cut at fixed width):

    • RVC makes code 2-byte aligned (halfword), addresses are all even; 32-bit instructions can land at a 2 mod 4 odd halfword position — you cannot cut at a fixed 4-byte stride
    • Wrong starting point → the entire instruction stream is misaligned into garbage; the correct approach: use a disassembler that supports RVC (Ghidra / riscv64-*-objdump recognize it automatically), align from the entry point/jump target, and when valid compressed instructions appear (c.addi/c.li/c.j/c.lw etc.) the alignment is correct
    • Recognition signals: e_flags RVC bit (0x1), objdump output contains 2-byte instructions, bare-metal firmware compiled with -Os
    • Interleaving of 16/32-bit instructions within the same function is the norm, there is no fixed stride
  4. gp-relative addressing recovery (lui+addi pairs, .sdata/.sbss):

    • gp(x3)= __global_pointer$, generally falls within the .sdata section, with a 12-bit signed offset (±2048) covering the .sdata/.sbss small data area
    • Access pattern: at startup lui gp, %hi(...) + addi gp, gp, %lo(...) set once → subsequently lw/ld aN, off(gp) small-offset data access (global variables within -msmall-data-limit, default 8 bytes, go through gp)
    • After symbol stripping: compute the actual gp value from the lui+addi sequence in the _start/crt0 prologue → annotate in the decompiler → recover data references; when ELF is present readelf -s look up __global_pointer$ symbol value
    • When gp is missing, these accesses resolve to "unknown global" and the data flow is scattered—determine gp first before analyzing (see pitfall 2)
  5. ABI identification (integer/float ABI, a0-a7 conventions):

    • Integer conventions: arguments a0-a7 (x10-x17, the 9th onward passed on the stack), return value a0, callee-saved s0-s11 (x8-x9, x18-x27); ra=x1, sp=x2, gp=x3, tp=x4, fp=s0(x8); sp is 16-byte aligned at call boundaries
    • ABI names: RV32 is ilp32/ilp32f/ilp32d, RV64 is lp64/lp64f/lp64d—the f/d suffix indicates float arguments go in floating-point registers (F/D extension)
    • Float conventions: arguments fa0-fa7 (f10-f17), return value fa0, callee-saved fs0-fs11 (f8-f9, f18-f27); hard-float ABI passes float arguments in fa0-fa7, soft-float passes them in a0-a7
    • Determination: rely on ELF e_flags & EF_RISCV_FLOAT_ABI(mask 0x6) as authoritative—it is theentire object's Float ABI field.You cannot infer soft/hard-float from whether floating-point instructions appear:-march=rv64ifd -mabi=lp64 is a legal combination; the artifact still uses hardware floating-point instructions such as fld/fmul, but function arguments still go through integer registers (measured: lp64 artifacts use fmv.d.x fa5, a1 to move a0/a1 into floating-point registers, while lp64d artifacts directly use fsd fa0 to receive floating-point registers). Floating-point instruction density can only corroborate that the ISA includes the F/D/Q extension, not the argument ABI
    • Objects with different Float ABIs cannot be linked together: the linker directly reports cannot link object files with different floating-point ABI, and under the standard workflow there is no soft/hard mixed artifact that is "confirmed per unit". If during reverse engineering you observe differing local calling conventions, first consider hand-written assembly, non-standard ABI,STO_RISCV_VARIANT_CC, FFI/thunk, or incorrectly identified function boundaries
  6. System call boundary (ecall + a7 number):

    • Linux user space:ecall triggers a system call, with the number in a7 (x17), arguments a0-a5, return a0; RISC-V uses asm-generic numbering (read=63, write=64, openat=56, exit=93, mmap=222)—different from x86/ARM numbering, so don't apply numbers from other architectures
    • Locate: objdump search for ecall(0x73), then search backward for the li a7, imm immediate; identify system call sites in Ghidra decompilation
    • Bare metal (no OS): ecall has custom semantics—M-mode firmware/SBI calls (a7=extension number, a6=function number) or vendor monitor, and must not be interpreted using Linux numbers; consult SBI/vendor SDK documentation according to the runtime environment
    • Behavior verification:qemu-riscv64 -strace directly print the system call sequence (linked with [[re-tracing]])
  7. Ecosystem fingerprint (toolchain/library function characteristics):

    • User space: glibc (dynamic, PT_INTERP, __libc_start_main) / musl (commonly static, small entry) / target board distribution characteristics (busybox, etc.)
    • Bare metal: newlib/picolibc (crt0: _start → __libc_init_array → main); vendor SDK boot chain (ROM → bootloader → app) and partition table structure
    • Instruction extensions: vector instructions (RVV, v0-v31 register file) suggest a newer toolchain; high density of compressed instructions mostly indicates -Os compilation
    • Feature strings locate library function versions (same idea as [[re-imports]]), cross-library fingerprints assist recovery

Cross-domain combination

  • [[re-binary-core]]: general RISC-V ELF initial survey/decompilation/debugging foundation ([[re-ghidra]], [[re-ida]], [[re-gdb]], [[re-radare2]] used as usual)
  • [[re-firmware]]: firmware sample gateway path (re-firmware → re-fw-extract → re-fw-rootfs → this skill)
  • [[re-fw-extract]] / [[re-fw-rootfs]]: prerequisites for firmware unpacking and filesystem/configuration analysis
  • [[re-fw-emulate]]: boot the whole firmware or run RISC-V programs in user mode (qemu series)
  • [[re-emulation]]: when qemu is unavailable, use Unicorn/Qiling for emulated execution (bare-metal instruction-by-instruction verification)
  • [[re-rtos]]: after locating RTOS task tables/TCBs in MCU firmware, decompile task by task
  • [[re-hardware-io]]: JTAG/SWD on-device debugging and flash reading (OpenOCD + gdb)
  • [[re-cpp-abi]]: RTTI/exception/vtable recovery for C++ artifacts on RISC-V
  • [[re-variant]]: multi-version firmware comparison and patch diff
  • [[re-tracing]]: qemu -strace / strace syscall tracing to verify ecall boundaries
  • [[re-sandbox]]: mandatory prerequisite for all dynamic execution (default sandbox principle in [[re-analyze/platform-tips]])
  • Companions: [[re-patching]] (RVC-aware byte patching), [[re-triage]] (prerequisite for initial survey/architecture identification)

Common pitfalls and traps

  • Splitting a compressed instruction stream at 4-byte boundaries is completely wrong: Symptom—disassembling at a fixed 4-byte stride yields garbled instructions and all references misaligned; Cause—RVC mixed streams interleave 2/4 bytes at 2-byte alignment, so fixed-width splitting can start mid-instruction; Countermeasure—use a disassembler that supports RVC (Ghidra/objdump auto-detect it), align start points from the entry/jump targets, and when valid compressed instructions (c.addi/c.j/c.lw) appear, alignment is correct; during patching, instruction length may change between 2↔4 bytes
  • Missing gp global pointer makes data access scattered: Symptom—a large number of lw/ld off(gp) data accesses whose addresses cannot be resolved, global variables not linked to each other, fragmented logic; Cause—after symbol stripping __global_pointer$ is lost, or the decompiler fails to infer gp; Countermeasure—compute the actual gp value from the lui gp+addi gp sequence in _start/crt0 and annotate it in the decompiler; when an ELF is available, readelf -s check __global_pointer$
  • la/li pseudo-instruction expansion forms vary: Symptom—the same logic has different instruction sequences in different functions (single addi / lui+addi / auipc+addi) and is misjudged as different logic; Cause—the assembler chooses the expansion form based on immediate size and relocation type, and after linker relax it may even shrink to a single instruction; Countermeasure—rely on machine-code semantics (the same target address means the same reference), cross-check across tools, and verify critical logic back at the instruction level
  • ecall syscall numbers are bound to the runtime environment; without an OS they have custom semantics: Symptom — in bare-metal firmware, ecall is interpreted as a Linux system call, so the semantics are completely wrong; Cause — ecall is merely an "environment call" instruction, and the meaning of the number is defined by the runtime environment (Linux: a7 = system call number; bare metal: SBI extension/vendor monitor); Countermeasure — first confirm the runtime environment (whether there is an OS, dynamic libraries, the form of _start), and when there is no OS, check the SBI extension number or the vendor SDK documentation against a7/a6
  • Linker relax (R_RISCV_RELAX) affects disassembly boundaries: Symptom — the disassembly of the .o intermediate product does not match the final binary instruction stream, or addresses found according to the unrelaxed layout are misaligned; Cause — linker relax shrinks auipc+addi to addi, shortens jal to c.j, etc., and the size of sequences marked with R_RISCV_RELAX becomes smaller after linking; Countermeasure — analyze the final linked product (not the .o), and base address references on the final binary; before modifying bytes, first confirm whether they are affected by relax (relink or compare disassembly)