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, 性能监控, 系统编程