EGFanTuan/alis
GitHub: EGFanTuan/alis
ALIS 是一个基于 ptrace 的轻量级纯用户态动态插桩库,通过在用户态完成函数级探针注入来避免内核陷入开销。
Stars: 0 | Forks: 0
# ALIS — A Lightweight Instrumentation System
ALIS 是一个轻量级用户态动态探针注入库,面向 Linux x86_64 平台,基于 ptrace 系统调用实现纯用户态函数级动态插桩。项目采用经典的生产者-消费者架构,配合无锁环形缓冲区(lock-free ring buffer)和宿主侧多线程轮询处理技术,能够在远程进程中以极低开销执行探针。自研的动态函数地址解析器和第三方 Capstone 反汇编库等外围组件为 ALIS 提供强大支持,使其能够兼容 ASLR 和 PIE,成为一个真正可用的库而不仅是实验项目。
## 赛题背景
Linux 内核提供的 uprobe 机制通过修改目标进程指定地址的指令为 trap 指令陷入内核态执行探针,存在两方面局限:一是 trap 陷入内核对性能敏感函数影响不可忽视,二是 eBPF 编程受限于目标进程符号及结构体信息的缺失,表达能力有限。本赛题要求设计一种**纯用户态跳转执行探针代码**的方案,借助 ptrace 系统调用的内存修改能力,在不产生内核陷出的前提下完成函数级动态插桩,旨在深入实践进程内存布局、指令编码、上下文保存恢复、信号与异常处理、ptrace 等操作系统核心知识。
## 已完成功能
所有 7 项基础功能(F1–F7)均已实现并通过测试。
| 编号 | 功能 | 实现要点 |
|------|------|----------|
| **F1** | 函数入口探针插入 | 通过 ptrace 将目标函数入口处指令替换为 `jmp`,跳转至 mmap 分配的 shellcode 区域,全程在用户态执行,不产生 trap 陷入内核 |
| **F2** | 函数参数获取 | 手写 x86_64 汇编在探针入口保存 rdi/rsi/rdx/rcx/r8/r9(System V ABI 前 6 个参数)至 ring buffer,通过共享内存回传宿主进程 |
| **F3** | 函数返回值捕获 | 通过篡改栈上返回地址(push ret_probe 地址),使原函数返回时自动跳入 ret_probe,捕获 rax/rdx/xmm0 后返回原调用者 |
| **F4** | 探针卸载与恢复 | `unpin()` 将入口指令恢复为原始字节码,释放 mmap 区域,关闭共享内存,无内存泄漏 |
| **F5** | 探针动态开关 | shellcode 内部通过读取共享内存中的 enable 标志位,运行时条件跳过 arg probe 或 ret probe;宿主侧通过 `shared_mutex` 安全切换 |
| **F6** | 多探针共存 | 支持同一进程内 ≥16 个并发探针,每个探针拥有独立的 shellcode 区域、ring buffer 和监控线程,互不干扰 |
| **F7** | 多线程安全 | 入队操作使用 `lock xadd` 原子指令,宿主侧使用 `std::shared_mutex`,ptrace 操作期间遍历并暂停所有远程线程 |
### 实现细节
**探针注入流程**:
1. 通过 `/proc//maps` 解析目标库的加载基址和 .text 段范围
2. 调用自研 ELF 解析器(支持 GNU hash / SYSV hash / 线性扫描三级查找)获取目标函数在库内的偏移量,加上基址得到运行时地址
3. 使用 Capstone 反汇编目标函数入口处指令,检测 RIP-relative 指令和 `endbr64`,确保覆盖区域可安全搬迁
4. 在目标进程地址空间中通过 ptrace 注入 `mmap` 调用,分配 RWX shellcode 区域、RW 堆区域,并通过 POSIX 共享内存建立 ring buffer
5. 将 arg_probe 和 ret_probe shellcode 与搬迁后的原指令拼接写入 mmap 区域,在函数入口写入跳转指令
6. 使用 ptrace `SINGLESTEP` 使所有线程越过被覆盖的指令区域(damaged zone)到达安全点后恢复运行
**用户自定义回调**:
用户可通过 `setProbeCallback()` 设置任意回调函数,在探针触发时接收参数和返回值。回调签名:
void callback(const ProbeReturnDataHelper& data, probe_id_t id,
bool arg_enabled, bool ret_enabled);
- `ProbeReturnDataHelper` 提供 `getArg(index)`、`getRetval()`、`getFloatRetval()` 等便捷接口
- 每个探针独立调用回调,同一进程的多个探针可设置不同回调
- 回调在监控线程中执行,用户可自由选择输出方式(控制台、日志文件、网络等)
- 演示程序 `alis.cc` 利用 `MessageQueue` 实现类似 GDB 的交互式体验,将探针数据异步投递到主线程输出
**Ring Buffer 设计**:
+----------+----------+-----+----------+--------+--------+-----------+
| entry[0] | entry[1] | ... | entry[n] | head | tail | committed |
+----------+----------+-----+----------+--------+--------+-----------+
|<------------- 64B per entry ------------>|<---- 64B each ----->|
每条目 64 字节(缓存行对齐,避免 false sharing),布局如下:
[ type:4B ][ key(rsp):8B ][ arg1-arg6:48B ][ padding:4B ]
或
[ type:4B ][ key(rsp):8B ][ rax:8B ][ rdx:8B ][ xmm0:8B ][ padding:... ]
**三个控制指针**(各占独立缓存行,`std::atomic`):
| 指针 | 写入者 | 含义 |
|------|--------|------|
| `head` | 生产者(shellcode) | 已分配的最大槽位索引(`lock xadd` 原子递增) |
| `committed` | 生产者(shellcode) | 所有数据字段写入完毕后递增,用于粗粒度通知消费者 |
| `tail` | 消费者(宿主线程) | 已处理的最大槽位索引 |
**竞态解决与 `type` 标志位**:
早期的两指针设计(head + tail)存在竞态问题:head 移动后数据可能尚未写入,消费者会读到脏数据。为此引入了 `committed` 指针——生产者写完所有字段后递增 committed,消费者仅读取 `[tail, committed)` 区间。
但在多线程环境下仍存在问题:线程 A 先获得槽位,线程 B 后获得;若 B 先完成写入并推进 committed,消费者可能读取到 A 的未完成数据。最终解决方案是**将 `type` 字段作为逐条目的就绪标志**:
- 生产者**最后写入 `type`**(在 key、参数/返回值全部写入之后)
- 消费者检查 `type`:若为 0 则表示该条目尚在写入中,不推进 tail 并重新读取同一槽位(忙等),直到生产者写入 type 或超时放弃
- `type` 的非零值同时携带语义信息(1=仅参数,2=仅返回值,3=参数+返回值,4=返回值无参数)
这实际上形成了一种基于数据就绪标志的无锁同步,在不引入内核锁的前提下保证了多生产者单消费者的正确性。
**数据流**:
1. 生产者(shellcode):`head = lock xadd(head, 1)` → 写入 key 和数据 → **最后写入 type** → `committed = lock xadd(committed, 1)`
2. 消费者(宿主线程):轮询 committed,当 `tail != committed` 时遍历 `[tail, committed)` → 检查每条的 `type`,type==0 则不自增 tail 并重新循环读取(等待数据就绪)→ type!=0 的条目按类型匹配 arg/ret → 更新 tail → 调用用户回调
**ELF 符号解析器**:
- 优先使用 `.gnu_hash`(O(1) 均摊),回退 `.hash`(SYSV),最终回退线性扫描 `.dynsym`
- 支持 `.symtab` 静态符号表回退(解析非导出函数)
- 支持 `.gnu_debuglink` 自动加载独立调试文件(解析被 strip 的符号)
- 允许返回 IFUNC 符号地址;通过 `e_type` 区分 PIE / 非 PIE 可执行文件
## 构建指南
### 依赖
| 依赖 | 最低版本 | 说明 |
|------|---------|------|
| CMake | 3.16 | 构建系统 |
| GCC 或 Clang | 支持 C++20 | 编译器 |
| Capstone | 5.0+ (推荐 6.0) | 反汇编库,用于指令分析和安全校验 |
| Linux | 内核 4.0+ | ptrace / shm / mmap 等系统调用 |
### 安装 Capstone
# 从源码编译安装 Capstone v6
git clone https://github.com/capstone-engine/capstone.git
cd capstone
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
sudo make install
### 构建
cd alis
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
构建产物:
| 产物 | 路径 | 说明 |
|------|------|------|
| `libalis_probe.a` | `build/src/probe/` | 探针静态库 |
| `alis` | `build/src/examples/` | 交互式示例程序 |
| `alis_target` | `build/src/test/` | 多线程测试目标 |
| `alis_bm_*` | `build/src/benchmark/` | 性能基准测试套件 |
## 运行指南
### 示例程序
# 终端 1:启动目标进程
./build/src/test/alis_target &
# 输入任意字符开始执行多线程循环
# 终端 2:以 root 权限启动探针交互程序
sudo ./build/src/examples/alis
# 按 Ctrl+C 进入交互模式,可使用以下命令:
# pin — 对 sum/sum2 函数插入探针
# unpin — 卸载探针
# enable — 启用 arg/ret 探针
# disable— 禁用 arg/ret 探针
# exit — 退出程序
### 性能基准测试
# 基本性能测试(getpid 100 万次循环)
cd build/src/benchmark/
sudo ./alis_bm_host ./alis_bm_target
# 输出包含:
# Baseline — 无探针耗时
# Uprobe — 内核 uprobe 耗时
# ALIS — 用户态探针耗时
# Overhead — 单次探针额外开销 (ns)
# 多线程测试
sudo ./alis_mt_host ./alis_mt_target
# 16 函数多探针测试
sudo ./alis_f16_host ./alis_f16_target
# 探针开关测试
sudo ./alis_toggle_host ./alis_toggle_target
# 信号安全测试
sudo ./alis_sig_host ./alis_sig_target
## 第三方库
本项目使用 [Capstone](https://www.capstone-engine.org/) 反汇编引擎,用于在探针注入前对目标函数入口指令进行反汇编分析,确保被覆盖的指令不包含 RIP-relative 寻址模式,从而保证指令搬迁的安全性。
- 许可证:[BSD 3-Clause](https://github.com/capstone-engine/capstone/tree/next/LICENSES)
- 项目主页:
- 源代码:
## 人工智能使用说明
本项目开发过程中使用了 AI 辅助编程工具,使用情况如下:
- **开发环境**:主要使用 VS Code,部分使用 TraeCN。
- **Inline Suggestions(VS Code)和 CUE(TraeCN)**:在编码全程保持开启,用于代码补全和局部建议。尤其在编写重复性的日志输出时(如`DEBUG_LOG`, `ERROR_LOG`内的信息和格式化内容等),自动补全提供了极大便利。
- **elf_resolver 模块**(`src/probe/elf_resolver.cc` / `elf_resolver.h`)以及 **`src/benchmark/` 下的测试代码**:由我们提供详细的设计需求和实现思路,交由 AI Agent 完成绝大部分编码工作,随后经过人工审查和修正。
- **log.h**:主要由 AI 生成,是一些标准的日志输出宏定义。
- **其他核心模块**(包括探针汇编 shellcode、环形缓冲区设计与实现、探针注入流程中的辅助函数等):AI 直接上手参与或利用agent直接生成大段代码的占比极低(< 5%),主要由人工设计和编写。但会涉及与 AI 的讨论、问题求解和代码审查。
- **项目文档**(含本 README):由我们提供详细大纲和要点,使用 AI 辅助生成初稿,经过严格人工核对和修改后定稿。
- **Commit message**:由AI翻译得到英文提交信息
- **使用的模型**:一部分(前期工作和elf_resolver)使用了`Gemini-3.1-Pro-Preview`,其余工作使用`DeepSeek-V4-Pro`
## 项目目录结构
alis/
├── CMakeLists.txt # 顶层 CMake 配置
├── README.md # 项目说明(本文件)
├── doc/ # 文档
│ ├── task.md # 赛题要求
│ ├── design.md # 设计文档
│ ├── test_report.md # 测试报告
│ ├── note.md / note_zh.md # 开发笔记(踩坑记录)
│ ├── note2.md # 函数入口指令多样性调研
│ └── note3.md # ELF 解析器设计分析
├── src/ # 主库源码
│ ├── CMakeLists.txt
│ ├── common/ # 公共头文件(类型、日志、RAII 辅助类)
│ ├── probe/ # 探针核心库(alis_probe)
│ │ ├── probe.h / probe.cc # 探针主类(pin/unpin/toggle)
│ │ ├── arg_probe.s # 参数探针汇编
│ │ ├── ret_probe.s # 返回值探针汇编
│ │ ├── elf_resolver.h/cc # ELF 符号解析器
│ │ ├── opcodes.h # x86_64 指令 opcode 常量
│ │ └── utils.h # 注入辅助函数(remoteMMap/skipDamagedZone 等)
│ ├── examples/ # 示例程序(交互式 alis.cc)
│ ├── test/ # 测试目标程序
│ └── benchmark/ # 性能基准测试套件
│ ├── host.cc / target.cc # 基本性能对比(baseline/uprobe/ALIS)
│ ├── f16_*.cc # 16 函数多探针测试
│ ├── mt_*.cc # 多线程并发测试
│ ├── toggle_*.cc # 探针动态开关测试
│ └── sig_*.cc # 信号安全测试
└── lab/ # 早期实验原型
├── probe/ # 单文件探针原型
├── target/ # 简单目标程序
└── objdump/ # 反汇编输出
## 团队分工
| 成员 | 职责 |
|------|------|
| **我** | 探针注入引擎、Shellcode 汇编、环形缓冲区设计与实现、探针生命周期管理、指令安全分析、性能基准测试、多线程测试、探针开关测试、信号安全测试、项目文档 |
| **队友A** | RIP-relative 指令重定位器(未合入主分支)、汇报 PPT |
| **队友B** | ELF 符号解析器、函数入口指令多样性调研(`note2.md`)、ELF 解析器设计分析(`note3.md`) |
## 开发时间线
| 时间 | 里程碑 |
|------|--------|
| 2026-05-10 | 项目启动,初始化仓库和基础 CMake 配置 |
| 2026-05-21 | 完成 lab 原型(ptrace 探针注入基本流程) |
| 2026-06-08 | lab 重构为主库(`src/probe/`),多线程模型引入 |
| 2026-06-22 | 探针动态开关、环形缓冲区竞态修复(type 就绪标志) |
| 2026-06-24 | ELF 符号解析器合入主分支 |
| 2026-06-30 | 测试套件完成、文档定稿 |
原计划在初赛阶段完成全部基础功能(F1–F7)及进阶功能 A1(ARM64)和 A2(条件探针),因时间原因进阶功能未及实装。项目中部分为 A1/A2 铺设的基础设施(`opcodes.h` 中的 `__aarch64__` 分支、ring buffer 中的条件 type 标记等)已就位。
## 许可证
本项目源代码遵循 **BSD 3-Clause License**。技术文档(含[设计文档](doc/design.md)、[测试报告](doc/test_report.md)、本 README)遵循 **CC-BY-SA 4.0**。
第三方依赖 Capstone 遵循 BSD 3-Clause 许可证。
标签:Bash脚本, ptrace, x86_64, 性能监控, 系统编程