When to use / when not to use
- Use: Haskell/OCaml artifacts (GHC RTS symbols, OCaml block header signatures), need to recover closures/heap objects, evaluation order, pattern-matching branches
- Use: OCaml native/bytecode artifact discrimination and bytecode analysis (ocamlrun script header signature)
- Don't use: imperative language artifacts (each goes to its own skill: C++ → [[re-cpp-abi]], Go → [[re-go]], Rust → [[re-rust]])
- Don't use: when only function logic is needed and control flow is intact (functional artifacts have scattered control flow, direct decompilation yields low returns, see step 4)
Tool preparation
readelf / llvm-nm (symbol analysis)
- For installation and verification see [[re-cpp-abi]] tool preparation
ghc toolchain (Haskell side, optional)
- Linux/macOS: GHC installer (
apt install ghc/brew install ghc/ ghcup);Windows: ghcup(winget install ghcupor the official website installer); verify:ghc --version - Purpose: compile comparison artifacts with the same version, verify closure/info table shapes (varies greatly between GHC versions)
ocamlobjinfo / ocamlopt (OCaml side, optional)
- Linux/macOS: OCaml toolchain (
apt install ocaml/brew install ocaml);Windows: opam(winget install OCaml.opam) or the official website installer; verify:ocamlobjinfohandles any .cmx output CRC and import table - Purpose: bytecode artifact/object file structure analysis (ocamlobjinfo can read .cmo/.cmx/bytecode executables)
Ghidra / IDA (decompilation base)
- For installation and verification see [[re-ghidra]] / [[re-ida]]
file / xxd / Python struct (byte-level verification)
- Bundled with the system (
file/xxd); Python 3 ships withstruct - Purpose: bytecode artifact discrimination (ocamlrun script header), closure first field/block header byte verification (see [[examples]] for examples)
Procedure
Execute in order; reverse engineering focuses ondata flowrather than control flow (functional artifacts have scattered control flow, see pitfall 4). Archive each step's artifacts (path + sha256, see [[re-triage]]).
-
Runtime identification:
readelf -s sample | grep -iE 'ghc|stg_|RTS|HsMain|_closure|_info' | head # GHC 特征 readelf -s sample | grep -iE 'caml_' | head # OCaml 特征 file sample # 字节码产物判别(脚本头)- GHC:
main(C RTS entry) + RTS runtime symbols (stg_*/hs_*) + business symbolsMain_main_closure/Main_main_info(模块_名字_closure/infoshape) - OCaml native:
main→caml_main→caml_startup_common→caml_start_program→caml<模块>__entry;caml_startup/caml_startup_pooledis the equivalent entry for C embedding calls (signature is also void (char_os **argv), the difference lies in the pooling flag and exception behavior), do not mistake it for the main chain;caml_*runtime symbols (caml_alloc/caml_apply2/3 etc.) - Bytecode vs native:
caml_start_programexists only in the native runtime library (measured in 4.14.2 libasmrun.a), is a native signature; bytecode criteria usecaml_interprete(only libcamlrun.a has it); bytecode artifact discrimination usesfile(ocamlrun script executable)/xxdheader (#!...ocamlrun\nscript header +T/Cmagic number + sectioning) - Entry chain is consistent across versions: runtime/main.c defines
mainand callscaml_main(argv)(caml_maindefined in startup_byt.c/startup_nat.c; native chainmain → caml_main → caml_startup_common → caml_start_program, see [[examples]] for measured addresses in 4.14.2); the bytecode runtime entry iscaml_main → caml_startup_aux → caml_interprete - Discrimination quick reference: GHC =
stg_*mechanical symbol group +模块_名_closure/infoYes; OCaml native =caml_*group +caml<模块>__<名>_<id>; OCaml bytecode =#!ocamlrunscript header
- GHC:
-
Closures and heap objects:
- GHC: heap object layout for thunks (unevaluated closures) and evaluated values—the first field of a closure is the info table pointer (for byte-level verification see [[examples]]); CAFs are statically allocated as thunks and are only evaluated on first reference (lazy);
Main_main_closureis a CAF, and its info points to the thunk evaluation code - GHC value forms: unevaluated (thunk, info points to evaluation code) vs evaluated WHNF (info points to constructor header/function header)—two states in the same address space; after evaluation, the closure contents are overwritten
- OCaml: block header (tag + size; on 64-bit, header = (size<<10)|(color<<8)|tag); tagged int determination uses the value's least significant bit (bit 0; odd = integer, even = pointer/block)
- Analysis: fields and constructors are the main clues (data flow first)
- GHC: heap object layout for thunks (unevaluated closures) and evaluated values—the first field of a closure is the info table pointer (for byte-level verification see [[examples]]); CAFs are statically allocated as thunks and are only evaluated on first reference (lazy);
-
Calling convention:
- GHC: arguments are passed via the stack; return values are in registers R1-R3 (boxed values in R1); entry code uses the info table as the hub (
_infosymbol = entry code) - OCaml: arguments are passed via registers (the first N), closure calls go through
caml_applyN; native code calling a closure = loading registers +caml_apply2/3or jumping directly to entry - Analysis: first identify the runtime wrappers (
caml_apply/ stg entry) before entering user logic; tail-call optimization turns recursion into jumps (no growing stack frames), so read it as loops - Common point between GHC and OCaml: functions are not "called", they are "jumped to entry"—the
jmptarget address in the decompilation is the function entry; do not read it with a call/ret pairing mindset
- GHC: arguments are passed via the stack; return values are in registers R1-R3 (boxed values in R1); entry code uses the info table as the hub (
-
Analysis strategy (data flow first):
- Control flow is fragmented: lazy evaluation makes evaluation order unpredictable—static control flow analysis has low value
- Data flow clues: closure field initialization points (constructor arguments), pattern-matching branches (constructor tag dispatch), string/constant references
- Output: a data flow graph (constructor → field → use site) replaces the control flow graph (connecting with the [[re-analyze/analysis-contract]] data contract)
- Pattern-matching recovery: branches dispatch by constructor tag (OCaml) or by info table pointer comparison (GHC)—tag/pointer value → constructor index; recovering the constructor set means recovering the data type
- Output format (for consumption by the analysis report and the next stage):
构造器 C1 (tag 0, 2 字段) ← 分配点 A (caml_alloc2 / info 表) 字段0 ← 函数参数/常量(数据来源) 字段1 ← 字符串池引用 使用点: tag 比较 → 分支 B(业务逻辑)
-
Bytecode artifacts (OCaml-specific):
head -c 64 sample | xxd # #!...ocamlrun 脚本头 + 魔数 T/C + 长度 ocamlobjinfo sample # 直接解析字节码可执行文件(导入单位/CRC)- Bytecode exe = script header + magic number
T+ code section + per-section data; the section names (CODE/PRIM/DATA/SYMB/CRCS, etc., 4 characters) and the big-endian length table are in the TOC at the end of the file, and the file ends withCaml1999X031(for the structure see [[layout]], for byte examples see [[examples]]) - Bytecode disassembly is not conventional decompilation (the instruction set is OCaml bytecode-specific); the analysis entry point uses the structural view of ocamlobjinfo
- Bytecode exe = script header + magic number
Cross-domain combinations
- [[re-binary-core]] gateway: this skill belongs to it (select the tree's "Haskell/OCaml artifacts" branch)
- [[re-analyze/analysis-contract]]: the data flow graph is passed according to the data contract
- [[re-cpp-abi]]: vtable/info table comparison approach (table pointer dispatch is isomorphic)
Common pitfalls and traps
- RTS version differences: Symptom — closure layout interpretation fails; Cause — GHC/OCaml version evolution; Countermeasure — confirm the layout according to the target version (this skill's field table is based on actual testing of 9.14/4.14, see [[layout]])
- thunk lazy evaluation misleads: Symptom — an unevaluated closure is treated as evaluated data; Cause — lazy evaluation; Countermeasure — distinguish a thunk header (info points to evaluation code) from an evaluated value (info points to a WHNF header); CAFs are thunks until first referenced
- OCaml bytecode is not native: Symptom — decompilation is all runtime wrappers; Cause — bytecode artifact; Countermeasure — after identifying the ocamlrun script header/bytecode section characteristics, analyze according to bytecode structure (not conventional decompilation;
caml_start_programexists only in the native runtime library and is a native characteristic; the bytecode criterion usescaml_interprete) - control flow scattering causes static analysis to fail: Symptom — the function body has no continuous logic; Cause — functional compilation artifact; Countermeasure — switch to data flow analysis (step 4), do not stubbornly chase control flow
- tagged int misread: Symptom — an integer is treated as a pointer/a pointer is treated as an integer; Cause — OCaml value tag bits; Countermeasure — distinguish by the lowest bit (1=integer, 0=pointer), decode the tag before access (int >> 1 to get the true value)
- GHC module names carry z encoding: Symptom — symbol
GHCziInternalziTopHandler_runMainIO1_infois hard to read; Cause —z+lowercase escapes special characters (GHC mangling:zi=.zu=underscorezz=zzc=:zh=# etc.); Countermeasure — manually restore the module name according to the escape rules (GHCziInternal→GHC.Internal), and after restoration it corresponds to the source module structure - info table and entry code are two views of the same pointer: Symptom — disassembling at the info pointer yields field table data rather than code; Cause — the info table pointer points to entry code, and the table fields are before the entry code; Countermeasure — disassemble starting from the info pointer (i.e., entry), and read the field table by negative offset
- tail call optimization turns recursion into jumps: Symptom — analyzing recursive logic by call tree breaks down; Cause — tail call optimization (TCO) by functional compilers; Countermeasure —
jmpreturning to the function's own address = recursion, read it with loop semantics, do not look for growing stack frames - lazy evaluation order is unpredictable: Symptom — single-step dynamic analysis in source order does not match; Cause — a thunk is evaluated only on first reference, and the evaluation trigger point is "where a value is needed" rather than "where a value is produced"; Countermeasure — dynamic analysis focuses on data dependencies (which closure is forced to evaluate), static analysis focuses on field initialization points, do not assume execution order
- cross-runtime misjudgment (GHC mechanical symbols treated as business code): Symptom —
stg_ap_*/stg_upd_frame_infoetc. are treated as business logic for analysis; Cause — STG mechanical symbols are the evaluation mechanism; Countermeasure — first use thestg_/hs_prefix to exclude RTS mechanical symbols, business code is concentrated in模块_名_infoand fragments that callcaml_applyN/caml_alloc*fragment of