Docs  /  Architecture & Internals

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.c CLI 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 into bin->functions[], or -1 for external/unresolved
  • callee_name: resolved symbol name or sub_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
  • call shows the resolved callee name and a summary: [-> func[N] insns=M loops=yes/no]
  • indirect_call shows the register holding the function pointer: the target is not known statically
  • callers/callees headers 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..a7 are inferred from the calling convention (x0-x7 on ARM64)
  • Resolved calls show the callee name with a /* func[N] */ annotation
  • cmp is rendered as a comment; the flag test appears in the following if
  • goto loc_0x... for intra-function branches
  • return 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
Edit this page on GitHub Source: docs/DECOMPILER.md · Rerius v1.0.0
On this page
SSA / NR Lifting & Pseudo-C Decompiler Pipeline Opcode Set Type System Call-Target Resolution Program-Level Module Running SSA/NR Lifting CLI Interactive shell JavaScript Reading SSA/NR Output Reading Pseudo-C Output Architecture Coverage
ESC
↑↓ navigate openesc close