全国大学生操作系统能力大赛(2026)· 功能挑战赛道 · 轻量级用户态动态追踪设计与实现
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 操作期间遍历并暂停所有远程线程 |
探针注入流程:
- 通过
/proc/<pid>/maps解析目标库的加载基址和 .text 段范围 - 调用自研 ELF 解析器(支持 GNU hash / SYSV hash / 线性扫描三级查找)获取目标函数在库内的偏移量,加上基址得到运行时地址
- 使用 Capstone 反汇编目标函数入口处指令,检测 RIP-relative 指令和
endbr64,确保覆盖区域可安全搬迁 - 在目标进程地址空间中通过 ptrace 注入
mmap调用,分配 RWX shellcode 区域、RW 堆区域,并通过 POSIX 共享内存建立 ring buffer - 将 arg_probe 和 ret_probe shellcode 与搬迁后的原指令拼接写入 mmap 区域,在函数入口写入跳转指令
- 使用 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<size_t>):
| 指针 | 写入者 | 含义 |
|---|---|---|
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=返回值无参数)
这实际上形成了一种基于数据就绪标志的无锁同步,在不引入内核锁的前提下保证了多生产者单消费者的正确性。
数据流:
- 生产者(shellcode):
head = lock xadd(head, 1)→ 写入 key 和数据 → 最后写入 type →committed = lock xadd(committed, 1) - 消费者(宿主线程):轮询 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 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注意:某些 Linux 发行版通过包管理器安装的 Capstone 可能为旧版本(v4),与项目使用的 v6 头文件存在 ABI 不兼容,会导致
cs_disasm()返回 0 而静默失败。建议从源码编译安装最新版本,确保头文件和库文件版本一致。项目的 CMake 配置已强制链接/usr/local/lib下的库文件以规避此问题。更多细节参见lab/note.md。
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提示:所有探针操作需要
ptrace权限,必须使用sudo或设置ptrace_scope为 0。
本项目使用 Capstone 反汇编引擎,用于在探针注入前对目标函数入口指令进行反汇编分析,确保被覆盖的指令不包含 RIP-relative 寻址模式,从而保证指令搬迁的安全性。
- 许可证:BSD 3-Clause
- 项目主页:https://www.capstone-engine.org/
- 源代码:https://github.com/capstone-engine/capstone
本项目开发过程中使用了 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。技术文档(含设计文档、测试报告、本 README)遵循 CC-BY-SA 4.0。
第三方依赖 Capstone 遵循 BSD 3-Clause 许可证。