---
name: re-fp-runtime
type: atomic
description: >
  函数式语言运行时逆向（Haskell/OCaml）：闭包/堆对象模型、调用约定、数据流优先策略。
  触发词：Haskell逆向、OCaml逆向、GHC RTS、thunk、STG、OCaml runtime、闭包、函数式产物。
capabilities: [lang-runtime-analysis]
---

# 函数式运行时逆向（Haskell / OCaml）

## 何时使用 / 何时不用

- 用：Haskell/OCaml 产物（GHC RTS 符号、OCaml block 头特征），需要还原闭包/堆对象、求值顺序、模式匹配分支
- 用：OCaml 原生/字节码产物判别与字节码分析（ocamlrun 脚本头特征）
- 不用：命令式语言产物（各归各技能：C++ → [[re-cpp-abi]]、Go → [[re-go]]、Rust → [[re-rust]]）
- 不用：只需函数逻辑且控制流完整（函数式产物控制流打散，直接反编译收益低，见步骤 4）

## 工具准备

### readelf / llvm-nm（符号分析）

- 安装与验证见 [[re-cpp-abi]] 工具准备

### ghc 工具链（Haskell 侧，可选）

- Linux/macOS: GHC 安装包（`apt install ghc` / `brew install ghc` / ghcup）；Windows: ghcup（`winget install ghcup` 或官网安装器）；验证: `ghc --version`
- 用途: 同版本编译对照产物，验证 closure/info table 形态（GHC 版本差异大）

### ocamlobjinfo / ocamlopt（OCaml 侧，可选）

- Linux/macOS: OCaml 工具链（`apt install ocaml` / `brew install ocaml`）；Windows: opam（`winget install OCaml.opam`）或官网安装器；验证: `ocamlobjinfo` 处理任意 .cmx 输出 CRC 与导入表
- 用途: 字节码产物/对象文件结构分析（ocamlobjinfo 可读 .cmo/.cmx/字节码可执行文件）

### Ghidra / IDA（反编译底座）

- 安装与验证见 [[re-ghidra]] / [[re-ida]]

### file / xxd / Python struct（字节级核对）

- 系统自带（`file`/`xxd`）；Python 3 自带 `struct`
- 用途: 字节码产物判别（ocamlrun 脚本头）、closure 首字段/block 头字节验证（示例见 [[examples]]）

## 操作步骤

按顺序执行；逆向着重**数据流**而非控制流（函数式产物控制流被打散，见坑 4）。每步产物存档（路径 + sha256，见 [[re-triage]]）。

1. **运行时识别**：
   ```sh
   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 入口）+ RTS 运行时符号（`stg_*`/`hs_*`）+ 业务符号 `Main_main_closure`/`Main_main_info`（`模块_名字_closure/info` 形态）
   - OCaml 原生：`main` → `caml_main` → `caml_startup_common` → `caml_start_program` → `caml<模块>__entry`；`caml_startup`/`caml_startup_pooled` 是供 C 嵌入调用的等价入口（签名同为 void (char_os **argv)，区别在 pooling 标志与异常行为），勿误当主链；`caml_*` 运行时符号（caml_alloc/caml_apply2/3 等）
   - **字节码 vs 原生**：`caml_start_program` 仅存在于 native 运行库（4.14.2 libasmrun.a 实测），是 native 特征；字节码判据用 `caml_interprete`（仅 libcamlrun.a 有）；字节码产物判别用 `file`（`ocamlrun script executable`）/`xxd` 头（`#!...ocamlrun\n` 脚本头 + `T`/`C` 魔数 + 分节）
   - 入口链各版本一致：runtime/main.c 定义 `main` 并调用 `caml_main(argv)`（`caml_main` 定义于 startup_byt.c/startup_nat.c；原生链 `main → caml_main → caml_startup_common → caml_start_program`，4.14.2 实测地址见 [[examples]]）；字节码运行库入口为 `caml_main → caml_startup_aux → caml_interprete`
   - 判别速查：GHC = `stg_*` 机械符号群 + `模块_名_closure/info` 对；OCaml 原生 = `caml_*` 群 + `caml<模块>__<名>_<id>`；OCaml 字节码 = `#!ocamlrun` 脚本头

2. **闭包与堆对象**：
   - GHC：thunk（未求值闭包）与已求值值的堆对象布局——closure 首字段即 info table 指针（实测字节验证见 [[examples]]）；CAF 以 thunk 形式静态分配，首次引用才求值（惰性）；`Main_main_closure` 是 CAF，其 info 指向 thunk 求值代码
   - GHC 值形态：未求值（thunk，info 指向求值代码）vs 已求值 WHNF（info 指向构造器头/函数头）——同地址空间的两种状态，求值后 closure 内容被覆写
   - OCaml：block 头（tag + 大小，64 位下 header = (size<<10)|(color<<8)|tag）；tagged int 判定用值的最低位（bit 0，奇数=整数，偶数=指针/block）
   - 分析：字段与构造器是主要线索（数据流优先）

3. **调用约定**：
   - GHC：参数经栈传递；返回值在寄存器 R1-R3（盒值在 R1）；entry 代码以 info table 为枢纽（`_info` 符号 = entry code）
   - OCaml：参数经寄存器（前 N 个）传递，闭包调用经 `caml_applyN`；原生代码调用闭包 = 寄存器装载 + `caml_apply2/3` 或直接跳 entry
   - 分析：先识别运行时包装（`caml_apply` / stg 入口）再进用户逻辑；尾调用优化使递归变跳转（无增长栈帧），按循环读
   - GHC 与 OCaml 共点：函数不是"被 call"，而是"跳到 entry"——反编译里的 `jmp` 目标地址即函数入口，别按 call/ret 配对思维读

4. **分析策略（数据流优先）**：
   - 控制流打散：惰性求值导致求值顺序不可预测——静态控制流分析价值低
   - 数据流线索：闭包字段初始化点（构造器参数）、模式匹配分支（构造器标签分发）、字符串/常量引用
   - 产出：数据流图（构造器 → 字段 → 使用点）替代控制流图（与 [[re-analyze/analysis-contract]] 数据契约衔接）
   - 模式匹配还原：分支按构造器 tag 分发（OCaml）或 info 表指针比较（GHC）——tag/指针值 → 构造器序号；还原出构造器集合即还原出数据类型
   - 产出格式（供分析报告与下一环节消费）：
     ```
     构造器 C1 (tag 0, 2 字段) ← 分配点 A (caml_alloc2 / info 表)
       字段0 ← 函数参数/常量（数据来源）
       字段1 ← 字符串池引用
     使用点: tag 比较 → 分支 B（业务逻辑）
     ```

5. **字节码产物（OCaml 特有）**：
   ```sh
   head -c 64 sample | xxd          # #!...ocamlrun 脚本头 + 魔数 T/C + 长度
   ocamlobjinfo sample              # 直接解析字节码可执行文件（导入单位/CRC）
   ```
   - 字节码 exe = 脚本头 + 魔数 `T` + 代码区 + 各分节数据；分节名（CODE/PRIM/DATA/SYMB/CRCS 等 4 字符）与大端长度表在文件尾部 TOC，文件以 `Caml1999X031` 收尾（结构见 [[layout]]，字节样例见 [[examples]]）
   - 字节码反汇编不是常规反编译（指令集为 OCaml bytecode 自定），分析入口用 ocamlobjinfo 的结构视图

## 跨域联合

- [[re-binary-core]] 网关：本技能归属（选择树「Haskell/OCaml 产物」分支）
- [[re-analyze/analysis-contract]]：数据流图按数据契约传递
- [[re-cpp-abi]]：vtable/info table 对照思路（表指针分派同构）

## 常见坑与陷阱

- **RTS 版本差异**：现象——closure 布局解读失败；原因——GHC/OCaml 版本演进；对策——按目标版本确认布局（本技能字段表基于 9.14/4.14 实测，见 [[layout]]）
- **thunk 惰性求值误导**：现象——未求值闭包被当已求值数据；原因——惰性求值；对策——区分 thunk 头（info 指向求值代码）与已求值值（info 指向 WHNF 头）；CAF 首引用前都是 thunk
- **OCaml 字节码非 native**：现象——反编译全是运行时包装；原因——字节码产物；对策——识别 ocamlrun 脚本头/字节码段特征后按字节码结构分析（非常规反编译；`caml_start_program` 仅 native 运行库有，是 native 特征；字节码判据用 `caml_interprete`）
- **控制流打散导致静态分析失效**：现象——函数体无连续逻辑；原因——函数式编译产物；对策——转数据流分析（步骤 4），不硬追控制流
- **tagged int 误读**：现象——整数被当指针/指针被当整数；原因——OCaml 值标记位；对策——按最低位区分（1=整数，0=指针），访问前先解标记（int >> 1 取真值）
- **GHC 模块名带 z 编码**：现象——符号 `GHCziInternalziTopHandler_runMainIO1_info` 难读；原因——`z`+小写转义特殊字符（GHC mangling：`zi`=`.` `zu`=下划线 `zz`=z `zc`=: `zh`=# 等）；对策——按转义规则手工还原模块名（`GHCziInternal` → `GHC.Internal`），还原后与源码模块结构对应
- **info table 与 entry code 是同一指针的两个视图**：现象——info 指针处反汇编出的是字段表数据而非代码；原因——info table 指针指向 entry code，表字段在 entry code 之前；对策——反汇编从 info 指针处开始（即 entry），字段表按负偏移读
- **尾调用优化把递归变跳转**：现象——按 call 树分析递归逻辑断裂；原因——函数式编译器的尾调用优化（TCO）；对策——`jmp` 回函数自身地址 = 递归，按循环语义读，别找增长栈帧
- **惰性求值顺序不可预测**：现象——按源码顺序单步动态分析对不上；原因——thunk 首次引用才求值，求值触发点在"需要值的地方"而非"产生值的地方"；对策——动态分析聚焦数据依赖（哪个闭包被强制求值），静态聚焦字段初始化点，别假设执行顺序
- **跨运行时误判（GHC 机械符号当业务代码）**：现象——`stg_ap_*`/`stg_upd_frame_info` 等被当成业务逻辑分析；原因——STG 机械符号是求值机制；对策——先按 `stg_`/`hs_` 前缀把 RTS 机械符号排除，业务代码集中在 `模块_名_info` 与调用 `caml_applyN`/`caml_alloc*` 的片段
