SSA / NR Lifting & Pseudo-C Decompiler
Rerius's typed intermediate representation is referred to internally as NR (in src/analysis/decomp.c) and as SSA Form in the web UI: both names refer to the same pass. It uses standard SSA-style variable versioning, plus a type tag and (for direct calls) target resolution on top.
Repository: https://github.com/ECLS-Studio/rerius Implementation:
src/analysis/decomp.cCLI flags:-Q(SSA/NR form),-D(pseudo-C decompile) Interactive:ssa <func>(SSA/NR),dec <func>(pseudo-C) JS API:bin.ssa(funcIdx),bin.decompile(funcIdx)
Pipeline#
ARM64 / RISC-V instructions (x86-64: see caveat below)
|
nr_lift_arm64() / nr_lift_riscv()
|
NR statement list (nr_stmt_t[])
with call-target resolution
|
dax_ssa_lift_func() -- prints SSA/NR form (-Q)
|
decomp_print_func() -- prints pseudo-C (-D)
|
dax_decompile_all() -- program-level call graph
x86-64 caveat: the decoder correctly parses x86-64 instructions, but the resulting mnemonic/operand text is fed into the same nr_lift_arm64() function used for ARM64: there is no dedicated x86-64 lifter. Output for x86-64 functions will be sparse and unreliable outside of a few coincidentally-overlapping mnemonic patterns; treat -Q/-D on x86-64 as experimental and prefer -P (symbolic execution) or plain disassembly for x86-64 targets.
Opcode Set#
The IR has 31 opcodes (nr_op_t in decomp.c) across six categories:
| Category | Opcodes |
|---|---|
| Data movement | NR_ASSIGN, NR_LOAD, NR_STORE, NR_MEMCPY |
| Arithmetic | NR_ADD, NR_SUB, NR_MUL, NR_DIV, NR_NEG |
| Bitwise | NR_AND, NR_OR, NR_XOR, NR_NOT, NR_SHL, NR_SHR, NR_ROL, NR_ROR |
| Type | NR_CAST, NR_SEXT, NR_ZEXT |
| Control | NR_CMP, NR_BRANCH, NR_COND_BR, NR_PHI, NR_UNDEF |
| Calls / returns | NR_CALL, NR_INDIRECT_CALL, NR_SYSCALL, NR_RET |
| Misc | NR_NOP, NR_LABEL |
Type System#
Every variable (nr_var_t) carries an inferred nr_type_t:
| Type | Meaning | How inferred |
|---|---|---|
NRT_U8 |
8-bit unsigned | ldrb, strb, extend ops |
NRT_U16 |
16-bit unsigned | ldrh, strh |
NRT_U32 |
32-bit unsigned | w-register writes |
NRT_U64 |
64-bit unsigned | x-register writes (default) |
NRT_PTR |
Pointer (any width) | result of adr/adrp, or load of 64-bit |
NRT_PTR_CODE |
Code pointer | function pointer context |
NRT_BOOL |
Boolean | conditional test result |
NRT_FLAGS |
CPU flags | cmp/tst result register |
These types feed directly into the decompiler's variable declarations. They're inferred from local context (the producing instruction), not from a whole-program type-propagation pass: treat them as a reading aid rather than a verified type system.
Call-Target Resolution#
Every NR_CALL node carries:
callee_func_idx: index intobin->functions[], or-1for external/unresolvedcallee_name: resolved symbol name orsub_0xADDR
The SSA/NR print output for each function shows:
callers: main@0xd1c _start_main@0xc20
callees: vm_run smc_run __printf_chk
[SMC-modified] (shown if smc_target_addr falls inside this function)
This list is built from the xref table (for callers) and a scan of NR_CALL statements (for callees): it identifies direct call relationships for display, not a full interprocedural dataflow analysis. The decompiler annotates every resolved call site with /* func[N] */.
Program-Level Module#
dax_decompile_all() (CLI -D) lifts all functions and emits a call graph built from the collected edges:
Program module -- call graph
main -> vm_run @ 0xe30
main -> smc_run @ 0x12d4
main -> run_cipher_chain @ 0x13fc
cff_compute -> <indirect> @ 0x1098
vm_run -> <indirect> @ 0xf90
Functions flagged for suspected self-modification:
smc_run @ 0x12a0
5 call edge(s) | 31 function(s) | 3 candidate SMC patch(es)
Running SSA/NR Lifting#
CLI#
# SSA/NR form for all functions
./rerius -Q ./binary
# Pseudo-C decompiler (includes the program-level module at the end)
./rerius -D ./binary
# Combined with full analysis
./rerius -x -Q -D ./binary
Interactive shell#
> ssa main # SSA/NR form for function 'main'
> dec main # pseudo-C for 'main'
> dec opaque_false # decompile a specific function
JavaScript#
rerius.withBinary('./binary', bin => {
bin.analyze();
const ir = bin.ssa(0); // SSA/NR form for function index 0
const code = bin.decompile(0); // pseudo-C
console.log(ir);
console.log(code);
});
Reading SSA/NR Output#
Example output for a simple function:
NR: main
callers: _start_main@0xc20
callees: vm_run smc_run
0x0d1c r10:u64 = 0x43c
0x0d24 r8:u64 = 0x8
0x0e30 r0:u64 = call vm_run [-> func[0] insns=54 loops=no]
0x12d4 r0:u64 = call smc_run [-> func[4] insns=32 loops=no]
0x13fc r0:u64 = call run_cipher_chain [-> func[6] insns=28 loops=no]
0x1408 r0:u64 = indirect_call [r8]
0x1414 ret r0
69 NR stmts | 68 vars | func_idx=15
Key points:
- Each line:
address dest:type = operation callshows the resolved callee name and a summary:[-> func[N] insns=M loops=yes/no]indirect_callshows the register holding the function pointer: the target is not known staticallycallers/calleesheaders list direct call relationships (see Call-Target Resolution)- A function that took a runtime write to its own code is labelled
[SMC-modified]
Reading Pseudo-C Output#
/* 0xd0c -- 460 insns called_from=1 */
uint64_t main(uint64_t a0, uint64_t a1, uint64_t a2) {
/* locals */
u64 v10_1;
ptr p19;
u64 v0_5;
v10_1 = 0x43c;
p19 = 0x624;
v0_5 = vm_run(a0, a1); /* func[0] */
*(p19) = v0_5;
/* cmp v8_2, 0x0 */
if (flags eq) goto loc_0x1414;
v0_6 = smc_run(v0_5); /* func[4] */
return v0_6;
}
Key points:
- Type-aware locals:
u64,ptr,u8_,u16_,bool - Arguments
a0..a7are inferred from the calling convention (x0-x7on ARM64) - Resolved calls show the callee name with a
/* func[N] */annotation cmpis rendered as a comment; the flag test appears in the followingifgoto loc_0x...for intra-function branchesreturn func(/*tail*/);for detected tail calls
This is a readable approximation of the source, generated from local structural patterns: not a type-checked or verified reconstruction. Cross-check anything load-bearing (crypto constants, control-flow-critical comparisons, pointer arithmetic) against the disassembly or SSA/NR form.
Architecture Coverage#
| Architecture | SSA/NR Lifting | Decompiler |
|---|---|---|
| ARM64 | Full: integer, load/store, branch, call, sysreg instructions | Full |
| RISC-V RV64 | Full: integer, load/store, jal/jalr/ret |
Full |
| x86-64 | Experimental: see the caveat under Pipeline | Experimental |
docs/DECOMPILER.md · Rerius v1.0.0