Function merging — fmerge¶
Module-only. Collapses functions that share a group= label into one selector-dispatched
super-function, so distinct routines become indistinguishable cases of a single body. Runs
first, before the function pipeline — so the merged super-function is then obfuscated by
everything downstream.
Grouping¶
Functions with the same group= label merge into the same super-function:
__attribute__((annotate("obf: fmerge(group=alpha)"))) int parse_hdr(const u8*, int);
__attribute__((annotate("obf: fmerge(group=alpha)"))) long crc_step(long, const u8*);
__attribute__((annotate("obf: fmerge(group=alpha)"))) void reset_ctx(ctx_t*);
// group=beta would form a separate super-function
fmerge(group=NAME)— explicit bucket; all same-NAMEfunctions become one super-function.- bare
fmerge— dropped into the default_autopool and chunked bychunk=(default 4). - A function belongs to exactly one group. A group needs ≥ 2 eligible members; smaller groups are skipped (a single-function trampoline adds nothing).
Options¶
Only group= is per-function; the rest are group-wide (resolved from the group's members).
| Key | Scope | Default | Meaning |
|---|---|---|---|
group |
per-func | — | Merge-bucket label (string). |
chunk |
group | 4 | _auto-pool chunk size (bare fmerge only, 2–16). |
opaqueSel |
group | 1 | Obfuscate the selector. |
dispatch |
group | switch |
Dispatch shape — switch or indirectbr. |
minInsts |
group | 4 | Skip functions smaller than this. |
maxInsts |
group | 2000 | Skip functions larger than this (blow-up bound). |
stripDbg |
group | 1 | Drop debug info on merged bodies. |
thunkAddrTaken |
group | 0 | Merge address-taken / external functions via a thunk. |
launderSel |
group | 0 | Load call-site selectors from a mutable global (defeats automated devirtualization). |
How dispatch is hidden¶
The call-site selector is not the literal case index. A group-wide key K obfuscates it: each
call site passes an obfuscated selector, and dispatch computes idx = selector XOR K. Downstream
mba / constenc then bury K further.
Selector laundering (launderSel=1)¶
With static selectors, a symbolic engine can trace each call straight to its dispatched behaviour — automated devirtualization. Laundering gives each group a mutable global holding its selectors; every call site (and thunk) reads its own via a volatile load, so the constant is no longer visible to static analysis.
Thunks (thunkAddrTaken=1)¶
Address-taken or externally-visible functions can't simply be erased. With this knob their body is replaced by a thin forwarder (thunk) into the super-function; internal direct calls are rewritten straight to the super-function, and only indirect / external uses go through the thunk.
Eligibility & safety¶
Members are dropped from a group (with a warning) if they violate the merge constraints (e.g.
byval/sret, naked, inline asm, musttail, blockaddress-in-body). If dropping members takes a
group below 2, the whole group is skipped. Each group is committed transactionally — if any
member can't be merged, the group is rolled back and the originals are left untouched.
Erasing annotated functions
fmerge erases merged originals. Any pass or tool that reads llvm.global.annotations must
account for this — the annotation use is consumed and the function may no longer exist after
merge. See DEV internals.