mini-linker
mini-linker 是一个面向 Linux x86_64 的用户态动态加载器。它读取受控的 ELF64ET_DYN 文件,递归解析自定义 DT_NEEDED 依赖,完成 PT_LOAD 映射、符号绑定、 RELA/RELR 重定位、GNU RELRO、构造/析构、最小动态 TLS,最后通过版本化 C ABI 调用 payload 的 mini_linker_entry。
重点展示动态加载器真正需要解决的底层问题:装载偏移、依赖图、ELF 符号可见性、 重定位公式、PLT/GOT、TLS、W^X、生命周期和故障隔离。
❝mini-linker 复用仓库内的 elf_inspector_core:elf-inspector 负责安全、只读的 ELF 解析,mini-linker 负责运行期装载语义。组件不复制 ELF parser,也不引入第三方库。
能做什么
一次 mini-linker verify <elf> 会在不调用 payload 入口、构造函数或 IFUNC resolver 的前提下, 完成整个装载计划的静态验证并输出:
| |
|---|
| 文件信息 | canonical path、Build-ID、SONAME;设备号/inode/文件大小用于内部身份固定 |
| 依赖图 | DT_NEEDED 边、稳定 load scope、搜索候选、复用对象、循环依赖断环 |
| 映射计划 | PT_LOAD 虚拟地址、运行期地址、load bias、文件/BSS 大小、最终权限 |
| 符号绑定 | global/weak、hidden/protected、版本匹配、provider、未解析 weak |
| 重定位 | RELA、packed RELR、普通/PLT/TLS/resolver relocation 分组与数量 |
| TLS | PT_TLS module id、TDATA/TBSS 大小、GD/LD 模型需求 |
| 安全属性 | W^X、GNU RELRO、可执行栈、text relocation、GNU CET IBT/SHSTK |
| 性能统计 | 符号索引、候选检查、cache hit/miss、plan/apply 阶段耗时 |
| 资源使用 | 输入快照、映射、TLS、trace 字节和释放失败计数 |
mini-linker run <elf> 会在 fork 出的子进程中执行同一加载流程,然后按依赖顺序运行构造函数、 调用入口、逆序运行析构函数。父进程使用 pidfd/signalfd 观察子进程、转发终端信号并报告 最后运行阶段。
支持范围
输入:
ELF class ELF64
endianness little-endian
machine EM_X86_64
file type ET_DYN
runtime freestanding,不依赖 glibc 启动环境
entry mini_linker_entry(可用 --entry 覆盖)
binding eager(默认)或实验性 lazy
TLS 单调用线程,general-dynamic / local-dynamic
能力状态
| | |
|---|
PT_LOAD | | |
| | |
| | |
| strong/weak/version/visibility | | |
| immutable input 与 sysroot 约束 | | |
| JSON verify / JSONL trace | | |
| | 首次调用保存 integer/SSE 状态,不支持 YMM/ZMM 上半部分 |
| | |
| | 仅 run --allow-resolvers 子进程执行 |
ordinary glibc PIE / ET_EXEC | | |
| COPY relocation、IE/LE TLS | | |
| | |
普通 PIE 带有 PT_INTERP,入口通常是 _start,依赖内核准备 argc/argv/envp/auxv、vDSO、 线程指针和 libc 自举状态。mini-linker 不伪装这些状态,因此不会声称能够运行任意系统程序。
项目架构
核心逻辑编译进静态库 mini_linker_core,CLI、测试和 benchmark 共享同一套实现:
elf_inspector_core
FileView / ELF / dynamic / symbols
|
v
CLI options ---> DependencyGraph ---> MappedImage[] ---> RelocationPlan
main.cpp dependency_graph mapped_image relocator_x86_64
| | |
v v v
SymbolResolver ------> RuntimeState ------> Lifecycle
symbol_resolver internal.hpp lifecycle.cpp
| |
v v
TLS / lazy PLT verify report
tls_runtime text / JSON / JSONL
lazy_binding |
v
fork Runner
pidfd/signalfd/stage pipe
加载主链路:
固定输入文件身份
-> 构建 DT_NEEDED 依赖图和稳定 load scope
-> 校验 ELF/segment/dynamic/资源预算
-> 为每个 image 预留匿名地址空间
-> 复制 PT_LOAD、清零 BSS
-> 建立符号索引和 TLS module
-> 预检全部 relocation target 与 symbol binding
-> 应用 RELATIVE / RELR
-> 应用普通符号、TLS、IFUNC/IRELATIVE、PLT relocation
-> mprotect 收敛 PF_R/PF_W/PF_X
-> 应用 GNU RELRO,拒绝最终 RWX
-> 依赖优先 init
-> mini_linker_entry(host, argc, argv)
-> 逆序 fini
-> 释放全部 image mapping
代码文件结构
mini-linker/
├── CMakeLists.txt # 核心库、CLI、payload、测试、benchmark、fuzzer
├── README.md
├── include/mini_linker/
│ ├── abi.h # payload 入口和 Host API 的稳定 C ABI
│ └── mini_linker.hpp # Loader、LoadedProgram、options、report、error
├── src/
│ ├── address_types.hpp # VirtualAddress / RuntimeAddress 强类型
│ ├── checked_arithmetic.hpp # 地址、长度、offset 的 checked arithmetic
│ ├── internal.hpp # RuntimeState 和内部模块契约
│ ├── dependency_graph.cpp # DT_NEEDED 搜索、token、身份去重、BFS load scope
│ ├── elf_contract.cpp # 运行期 ELF 契约、动态表、RELR/CET 校验
│ ├── mapped_image.cpp # mmap、segment copy、BSS、权限、RELRO、RAII
│ ├── symbol_resolver.cpp # 名称/版本索引、weak/visibility、IFUNC
│ ├── relocator_x86_64.cpp # relocation plan、预检、公式和写入
│ ├── lazy_binding.cpp # lazy GOT 上下文、首次解析与原子 patch
│ ├── lazy_trampoline_x86_64.S # x86_64 lazy resolver 汇编 trampoline
│ ├── tls_runtime.cpp # PT_TLS、module id、__tls_get_addr bridge
│ ├── lifecycle.cpp # preinit/init/entry/fini、双故障诊断
│ ├── runner.cpp # fork、pidfd、signalfd、stage/error pipe
│ ├── loader.cpp # 公共 API 编排
│ ├── output.cpp # text、JSON、JSON Lines
│ └── main.cpp # CLI 解析和 verify/run 分发
├── demos/
│ ├── app.cpp # 根 payload:入口、构造/析构、跨 DSO 调用
│ ├── math.cpp # DT_NEEDED provider
│ ├── advanced_app.cpp # lazy PLT、跨 DSO TLS、weak TLS
│ ├── tls.cpp # TDATA、TBSS、GD/LD TLS provider
│ ├── modern.cpp # RELR、IFUNC、IRELATIVE
│ └── scale.cpp # 8192 RELR slot + 1024 导出符号规模 fixture
├── tests/
│ ├── loader_tests.cpp # 映射、依赖、符号、生命周期和 API
│ ├── malformed_tests.cpp # 截断/越界/不支持 ELF 的 fail-closed
│ └── advanced_tests.cpp # lazy PLT、TLS、trace、resolver
├── benchmarks/
│ └── loader_benchmark.cpp # 重复 verify,输出 median/P95 与阶段统计
├── fuzz/
│ └── loader_fuzz.cpp # Clang libFuzzer 输入入口
└── docs/
├── verify-report.schema.json # verify JSON schema v7
└── trace.schema.json # JSONL event schema v5
编译
要求:
在组件目录独立构建:
cd cpp-sys-labs/mini-linker
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j$(nproc)
ctest --test-dir build --output-on-failure
组件也支持由外部工程通过 add_subdirectory(mini-linker) 聚合,但提交本身不依赖仓库顶层 CMake;独立构建是稳定入口。
独立构建默认从兄弟目录 ../elf-inspector 引入 elf_inspector_core。若源码不在默认位置:
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Debug \
-DELF_INSPECTOR_SOURCE_DIR=/absolute/path/to/elf-inspector
构建选项:
| | |
|---|
MINI_LINKER_BUILD_TESTS | ON | |
MINI_LINKER_BUILD_DEMOS | ON | 构建受控 freestanding payload |
MINI_LINKER_BUILD_BENCHMARKS | ON | |
MINI_LINKER_BUILD_FUZZER | OFF | 构建 Clang libFuzzer target |
ELF_INSPECTOR_SOURCE_DIR | ../elf-inspector | elf_inspector_core |
为下文定义常用路径:
BUILD=./build
BIN=$BUILD/mini-linker
APP=$BUILD/libmini_linker_demo_app.so
ADVANCED=$BUILD/libmini_linker_demo_advanced.so
MODERN=$BUILD/libmini_linker_demo_modern.so
LIBDIR=$BUILD
使用场景与实战
场景 1:先验证一个 ELF 能否被安全装载
问题:不希望直接执行未知 payload,先检查它需要映射哪些 segment、加载哪些依赖、应用多少 重定位,以及最终页面权限是否满足 W^X/RELRO。
$BIN verify --library-path "$LIBDIR""$APP"
重点观察:
images:根对象和 libmini_linker_demo_math.sodependency_search:候选、命中、复用和拒绝数量IMAGE:mapping base、minimum vaddr、load bias、segment 权限relocations:relative、symbolic、PLT、RELR 的分类数量symbol_bindings:普通/TLS/undefined weak 绑定WARNING:CET、partial RELRO 或 ELF parser 诊断
verify 会完成映射、重定位计划和权限校验,但不会调用 payload 构造函数、入口、析构函数或 IFUNC resolver。验证结束后全部 mapping 通过 RAII 释放。
场景 2:运行带自定义依赖的 freestanding payload
libmini_linker_demo_app.so 通过 DT_NEEDED 依赖 libmini_linker_demo_math.so。provider 的 constructor 把全局 bias 初始化为 5,根 payload 再通过 GLOB_DAT/JUMP_SLOT 调用它。
$BIN run --library-path "$LIBDIR""$APP"
echo"status=$?"
正常输出:
mini-linker demo: dependency call succeeded
status=0
入口参数通过 -- 后的部分传递。Demo 收到 exit7 时返回 7:
$BIN run --library-path "$LIBDIR""$APP" -- exit7
echo"status=$?"
这个场景同时覆盖:
- global data 和 function relocation
- root 先于 dependency 执行 destructor
场景 3:生成机器可消费的 verify 报告和加载轨迹
完整 JSON 报告:
$BIN verify --json --library-path "$LIBDIR""$APP" | jq
JSON 字段契约由 docs/verify-report.schema.json 固定。报告包括依赖边、每个 image/segment、 重定位阶段、符号 cache、资源用量和 warning。
如果更关心“为什么选中了这个库/符号、何时改变页面权限”,使用有界 JSON Lines trace:
$BIN verify --trace-jsonl --library-path "$LIBDIR""$APP" | jq -c
输出由 begin、若干 event 和 summary 组成;单条 event 可表示:
- dependency candidate / selected object
- relocation plan / applied relocation
- runtime resolver decision
想直接读人类可读轨迹:
$BIN verify --trace --library-path "$LIBDIR""$APP"
verify --trace 的事件写 stderr,最终报告写 stdout,便于分别重定向。
场景 4:观察 lazy PLT 和单线程动态 TLS
libmini_linker_demo_advanced.so 依赖 TLS provider,覆盖 TDATA、TBSS、跨 DSO TLS、 undefined-weak TLS、GD/LD 模型和首次 lazy GOT patch:
$BIN run \
--binding lazy \
--trace \
--library-path "$LIBDIR" \
"$ADVANCED"
正常输出包含:
mini-linker advanced: lazy PLT and TLS succeeded
trace 中每个 lazy slot 只在第一次调用时出现 lazy-bind/lazy-binding-resolved,之后直接经过 已经原子更新的 GOT entry。实验性 trampoline 保存通用参数寄存器、xmm0..7 和调用栈对齐, 但不保存 YMM/ZMM 上半部分;传递 AVX/AVX-512 vector 参数时应使用默认 eager binding。
lazy 模式需要保持待 patch 的 JUMP_SLOT 页面可写,因此只能提供 partial RELRO。默认 eager 路径 可以在绑定完成后应用更完整的 RELRO,也是推荐模式。
场景 5:验证 RELR、IFUNC/IRELATIVE 和 GNU CET 属性
libmini_linker_demo_modern.so 包含 packed RELR 和 GNU IFUNC。纯验证不会执行 resolver:
$BIN verify --trace "$MODERN"
报告会给出 relr_relocations 和 requires_runtime_resolvers。真正运行时必须显式授权:
$BIN run --allow-resolvers --trace "$MODERN"
echo"status=$?"
resolver 只允许在 runner 子进程执行,并要求 resolver 地址和返回地址都位于可执行映射中;递归 进入会被拒绝,结果只计算一次并缓存。直接在宿主进程调用 Loader::load() 即使配置ExecuteInRunChild 也不会获得 resolver 执行授权。
若 ELF 的 GNU property 要求 IBT,所有受控间接入口都必须以 ENDBR64 开头。SHSTK 需求会被 报告,但实际 enforcement 继承运行进程和内核的 CET 策略,loader 不模拟 shadow stack。
场景 6:用资源预算拒绝异常或超大输入
所有主要输入和运行期资源都有预算,CLI 支持 K/M/G 后缀:
# 根对象 + 一个依赖会超过 max-objects=1
$BIN verify --max-objects 1 --library-path "$LIBDIR""$APP"
# 限制单文件快照和总映射空间
$BIN verify \
--max-single-input-bytes 64K \
--max-input-bytes 128K \
--max-mapped-bytes 1M \
"$MODERN"
# 将 trace 控制在固定内存内
$BIN verify \
--trace-jsonl \
--max-trace-events 32 \
--max-trace-bytes 8K \
--library-path "$LIBDIR" \
"$APP"
预算耗尽会返回 resource-limit,不会在越界后继续部分装载。trace 超限则保留已接受的事件, 在 summary 中增加 trace_events_dropped,不影响装载验证本身。
场景 7:检查 sysroot 内的依赖闭包
可用 --sysroot 把绝对依赖路径限制到一个 rootfs 下:
$BIN verify \
--sysroot /path/to/rootfs \
--library-path /usr/lib64 \
/path/to/rootfs/opt/app/libpayload.so
每个对象只打开一次并保存不可变字节快照。解析、身份检查和映射都消费同一份数据,避免“按文件 A 验证、路径被替换后执行文件 B”的 TOCTOU。严格路径复核会通过已打开 fd 检查实际文件仍位于 sysroot 内。
--allow-system-libs 只启用固定 x86_64 系统目录搜索;工具不会读取 LD_LIBRARY_PATH、/etc/ld.so.cache 或 glibc hwcaps。
命令行参数
基本形式:
mini-linker verify [options] <elf>
mini-linker run [options] <elf> -- [args...]
| |
|---|
--library-path <dir> | |
--sysroot <dir> | |
--allow-system-libs | |
--entry <symbol> | 入口符号,默认 mini_linker_entry |
--binding eager|lazy | |
--allow-resolvers | 只在 run 子进程允许 IFUNC/IRELATIVE |
--max-objects <n> | |
--max-single-input-bytes <size> | |
--max-input-bytes <size> | |
--max-mapped-bytes <size> | |
--max-tls-bytes <size> | |
--max-trace-events <n> | |
--max-trace-bytes <size> | |
--trace | 将人类可读 loader event 写到 stderr |
--json | verify |
--trace-jsonl | verify 输出有界 JSON Lines event stream |
--help | |
--version | |
退出码:
| |
|---|
verify | 0 |
verify | 1 |
| 2 |
run | |
run | 125 |
| 128 + signal |
run 保留 payload 的 stdout/stderr;loader trace 和诊断写 stderr。payload 最好避免返回保留状态125,否则脚本无法仅凭退出码区分 payload 返回与 loader child failure。
依赖搜索与初始化顺序
稳定搜索顺序为:
- requester 自己的非传递
DT_RUNPATH - requester 及祖先的 legacy
DT_RPATH,从最近加载者向 root 搜索 - 仅在
--allow-system-libs 下启用的固定系统目录
$ORIGIN、$LIB(固定为 lib64)和 $PLATFORM(固定为 x86_64)在 DT_NEEDED、DT_RPATH、DT_RUNPATH 中只展开一次,不递归解释替换结果。
依赖图按 canonical path、device/inode/size 和 SONAME 去重;同一个 SONAME 指向不同文件会报dependency-conflict。load scope 使用稳定 BFS 顺序,符号查找遵循“第一个兼容 global/weak 定义获胜”。
循环 DT_NEEDED 图不会被当成损坏。生命周期使用三色 DFS:遇到正在访问的灰色对象时结束该边, 每个对象只初始化一次;析构严格反转实际 init 顺序。
映射、重定位与页面权限
每个 image 先计算所有 PT_LOAD 的页对齐范围:
Vmin = align_down(minimum PT_LOAD.p_vaddr)
Vmax = align_up(maximum (p_vaddr + p_memsz))
mapping_span = Vmax - Vmin
runtime(V) = mapping_base + (V - Vmin)
loader 用匿名 mmap(PROT_NONE) 预留整个 span,再复制 p_filesz 字节并把p_memsz - p_filesz 清零为 BSS。所有地址、长度、页数和 offset 使用 checked arithmetic。
支持的 x86_64 relocation:
| |
|---|
R_X86_64_NONE | |
R_X86_64_RELATIVE | B + A |
| 解码为经过范围检查的 relative target |
R_X86_64_64 | S + A |
R_X86_64_GLOB_DAT | |
R_X86_64_JUMP_SLOT | |
R_X86_64_DTPMOD64 | |
R_X86_64_DTPOFF64 | |
R_X86_64_IRELATIVE | |
所有 relocation 在第一次写入前完成目标范围、原始可写权限、符号、版本、TLS 和 resolver 策略 预检。text relocation、越界 target、重复冲突 target、未知 relocation 和最终 RWX 都会拒绝。
segment copy 后页面按 PF_R/PF_W/PF_X 收敛;eager binding 完成后,PT_GNU_RELRO 覆盖的完整页 再次 mprotect 为只读。RAII 析构中的 munmap 不抛异常,但失败会进入 report 或 runner warning。
Entry ABI
payload 必须包含 mini_linker/abi.h 并导出:
extern"C"int32_tmini_linker_entry(
const MiniLinkerHostApiV1* host,
int32_t argc,
constchar* const* argv);
ELF e_entry 必须等于该符号的 link-time value。当前 Host API v1 提供:
typedefstructMiniLinkerHostApiV1 {
uint32_t abi_version;
uint32_t struct_size;
int64_t (*write_bytes)(int32_t fd, constvoid* data, uint64_t size);
} MiniLinkerHostApiV1;
payload 必须检查 abi_version、struct_size 和回调是否为空。C++ 异常不得跨越这个 C ABI; loader 能保留自身初始化/entry/finalization 的双重故障上下文,但不能把跨 ABI 抛异常变成受支持行为。
C++ 库接口
CLI 之外也可以直接复用 mini_linker_core:
#include"mini_linker/mini_linker.hpp"
#include<iostream>
intmain(){
mini_linker::LoaderOptions options;
options.library_paths.emplace_back("./build");
options.limits.max_objects = 16;
options.limits.max_mapped_bytes = 128ULL * 1024ULL * 1024ULL;
const mini_linker::Loader loader(options);
constauto report = loader.verify("./build/libmini_linker_demo_app.so");
std::cout << "images=" << report.images.size()
<< " relocations=" << report.total_relocations << '\n';
}
主要接口:
classLoader {
public:
explicitLoader(LoaderOptions options = {});
VerifyReport verify(conststd::filesystem::path& root)const;
LoadedProgram load(conststd::filesystem::path& root)const;
};
classLoadedProgram {
public:
const VerifyReport& report()constnoexcept;
intinvoke(conststd::vector<std::string>& arguments);
};
LoadedProgram 是 move-only,拥有全部 image mapping。verify() 永远禁用 target resolver 执行。load()/invoke() 在当前进程直接执行受控代码,没有 CLI run 的 fork 隔离;普通应用优先使用 CLI runner,嵌入式调用方必须自己承担崩溃、系统调用和全局状态影响。
错误通过 LoaderError 抛出,包含稳定 ErrorCode、关联 object 和可选 file offset。常见错误:
invalid-elf / unsupported-elfdependency-not-found / dependency-conflictsymbol-not-found / symbol-conflictunsupported-relocation / relocation-overflow / relocation-out-of-rangemapping-failed / protection-failedinvalid-entry / runtime-resolver-rejectedresource-limit / child-protocol
输出契约
verify --json 使用 docs/verify-report.schema.json,当前 schema_version=7。
verify --trace-jsonl 使用 docs/trace.schema.json,当前 schema_version=5:
begin
event*
summary
输入错误则输出:
begin
error
JSONL 更适合大 trace:消费者可以逐行处理,不必等待一个巨大的 JSON 数组。事件数量和保留字节都 有上限;summary 明确给出 dropped 数量。schema 文件可以直接交给 Python jsonschema、监控系统 或后续 elf-inspector/mini-container 联动工具消费。
测试、Benchmark 与 Fuzz
运行现有 CTest:
ctest --test-dir build --output-on-failure
覆盖范围包括:
- malformed/truncated ELF、越界 relocation、资源预算
- eager/lazy PLT、跨 DSO TLS、weak TLS
- RELR、IFUNC/IRELATIVE 的受控运行路径
- payload 返回状态和 runner child 协议
规模 benchmark 默认构建:
$BUILD/mini-linker-benchmark \
"$BUILD/libmini_linker_benchmark_scale.so" \
- \
50
参数为:
mini-linker-benchmark <elf> [library-dir] [iterations]
scale fixture 包含 8192 个真实 RELR pointer slot 和 1024 个动态导出函数。benchmark 重复verify(),输出 JSON:median/P95、symbol cache、candidate checks、relocation 分类及 index/plan/apply 耗时。它用于观察规模化回归,不是功能正确性测试。
Clang libFuzzer 默认关闭:
cmake -S . -B /tmp/mini-linker-fuzz \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_CXX_COMPILER=clang++ \
-DMINI_LINKER_BUILD_FUZZER=ON
cmake --build /tmp/mini-linker-fuzz --target mini-linker-fuzz -j$(nproc)
/tmp/mini-linker-fuzz/mini-linker-fuzz -runs=1000
fuzzer 将随机输入写入独立临时文件,以严格资源预算调用 Loader::verify(),用于发现 ELF parser、 dependency contract、mapping arithmetic 和 relocation preflight 的崩溃或未定义行为。
GitHub链接
https://github.com/cpp-agan-team/cpp-labs
知识星球介绍(公认的cpp c++学习地)
星球名字:奔跑中的cpp / c++
专注cpp/c++相关求职领域的辅导
加入星球福利,后续如果有其他活动、服务,不收费,不收费,可以合理赚钱就收取下星球费用,但是不割韭菜,保持初心
如果想了解星球或者有其他疑惑的也可以加阿甘微信:

感兴趣的微信扫下面的码,然后下载知识星球app登录即可
(1)高质量的项目合集






以及最近出的ros机器人项目
同时如果项目,遇到任何困惑也会第一时间进行解答的
(2)高质量精确性八股资料


(3)详细的学习路线
(4)活跃的学习氛围,星球打卡不只是一个形式,而是每天观看,针对同学们的学习情况提出合理化的建议,同时也有高质量的星球微信内部群


(5)星球提问简历修改,提供意见的同时,还会给安排一对一腾讯会议辅导

(6)星球同学offer情况,以及对应学习情况,给大家提供参考
(7)全网最全cpp相关面经整理

(8)编程实战能力提升平台(大家都可以使用的,免费的)
访问网址 cppagancoding.top
星球同学的评价
(9)每周也会进行直播答疑,同时有时也会给星球内部同学开一些知识、路线分享会。
具体可以看B站放的视频,up名字:cpp辅导的阿甘
(10)奖励金激励,会根据大家打卡学习/ 面经打卡整理情况,每个月每个季度发放奖励金。有的人陆陆续续已经获得了数千月的奖励金,是加入星球费用的数十倍了

(11)全网最全的26届校招、27届实习/校招整理表汇总
等等,可能还有一些其他服务,目前没想起来的,以及后续也会增加的服务