用户态虚拟进程运行时,基于 minicoro 协程库,在单个 Android 进程内运行多个"虚拟进程"。
Android 只看到 1 个进程,内部通过 minicoro 协程调度器在虚拟进程之间切换上下文,每个虚拟进程拥有独立的栈、fd 表、工作目录和信号队列。
Termux/hermux 每执行一个命令 = fork()+exec() = Android 多看到 1 个进程。Android 通过 RLIMIT_NPROC 限制每 UID 进程数(通常几百),后台进程被 cgroup 冻结,LMK 随时可能 SIGKILL。apt install 期间可能 fork 数十个子进程,容易触发限制。
Go 运行时在 1 个 OS 进程内调度数万个 goroutine,上下文切换仅 ~100ns。但 Go 控制所有代码的编译和执行,Termux 运行的是预编译 ELF 二进制。vproc 的目标是在不修改现有二进制的前提下,用协程替代真实进程。
┌───────────────────────────────────────────────────┐
│ 主进程 (Android 看到 1 个进程) │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ vproc Runtime (Rust + minicoro) │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ VP 1 │ │ VP 2 │ │ VP 3 │ │ │
│ │ │ (coro) │ │ (coro) │ │ (coro) │ │ │
│ │ │ own stack│ │ own stack│ │ own stack│ │ │
│ │ │ own fds │ │ own fds │ │ own fds │ │ │
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
│ │ └──────┬──────┘──────────┘ │ │
│ │ Scheduler (minicoro mco_resume/yield) │ │
│ │ + fd swap around coroutine resume │ │
│ │ │ │ │
│ │ ┌───────────┴───────────┐ │ │
│ │ │ ELF Loader / dlopen │ │ │
│ │ │ Interpreter Delegation│ │ │
│ │ │ Virtual FD Table │ │ │
│ │ │ Virtual Pipes │ │ │
│ │ │ C LD_PRELOAD .so │ │ │
│ │ │ Rust FFI Runtime │ │ │
│ │ └───────────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ libvproc_preload.so (LD_PRELOAD, pure C) │
│ → dlopen("libvproc.so") → Rust FFI │
└───────────────────────────────────────────────────┘
基于 minicoro 的生产级协程调度器。
minicoro (coro/minicoro.c): 轻量 C 协程库(~2000 行),aarch64 汇编上下文切换。vproc fork 并扩展了 minicoro,新增 API:
mco_create_with_elf_entry()— 自定义 ELF 入口点,aarch64 汇编 trampoline__mco_elf_entrymco_fork_from()— 复制父协程栈 + ctxbuf,创建子协程mco_set_user_data()— minicoro 原版只有 getter,vproc 需要 settermco_get_ctx(),mco_get_back_ctx(),mco_set_stack()— 辅助 API
协程创建 (src/coroutine.rs): CoroUserdata 模式存储 per-coroutine 元数据。minicoro 的 user_data 指向 CoroUserdata(exit_code + closure)。
调度器 (src/executor.rs): UnsafeCell<Executor> 避免引用冲突。HashMap 存储 coroutine,VecDeque 就绪队列,轮转调度。EXECUTOR_PTR AtomicPtr 替代 TLS(存活于 __libc_init 重初始化)。
关键设计决策:
- CoroUserdata per-coroutine — 避免全局 closure slot 被第一个协程消费
- mco_running_raw() — 绕过 Rust wrapper,避免 double-&mut
- AtomicPtr 替代 thread_local — dlopen 的
__libc_init会重置 TLS
性能: 1000 协程 3000 次切换在 ~107ms 内完成(debug),~36μs/switch。
纯协程虚拟化,无需真实 fork。
virtual_fork() 创建子协程而非真实进程,virtual_waitpid() 通过 yield 等待子协程完成。所有父子逻辑运行在根协程内(yield 需要协程上下文)。支持嵌套 fork(子协程 → 孙协程)。
src/preload.rs 实现了 LD_PRELOAD 拦截层(fork/execve/waitpid/_exit),用 VPROC=1 环境变量开关。
加载 PIE ELF 二进制到协程内执行,替代真实 execve()。
ELF 解析器 (src/elf.rs): 纯安全 Rust,零依赖。解析 Elf64_Ehdr/Phdr/Dyn/Rela 结构体,验证 magic/class/endian/machine/type。#[repr(C, packed)] + read_unaligned 避免对齐问题。
PIE 加载器 (src/loader.rs):
- 解析 ELF 头,找到 PT_LOAD 段
- 在 0x2000000000(128 GiB 区域,Android VA 空间上限 ~512 GiB)bump 分配地址
- mmap LOAD 段(R-X for text, RW for data),复制文件内容,BSS 零填充
- 应用 R_AARCH64_RELATIVE 重定位(
base + addend)
ELF 入口跳板 (minicoro __mco_elf_entry):
mov sp, x20 // 切换到 ELF 栈布局
br x19 // 跳转到入口点
ELF 栈布局:
sp → argc (u64)
argv[0] ... argv[argc-1], NULL
envp[0] ... envp[n], NULL
auxv: {AT_PHDR, addr}, {AT_PHNUM, n}, ... {AT_NULL, 0}
双路径加载:
- 静态 PIE: 自定义加载器(mmap + 重定位 + 跳板),在协程内直接跳转到 ELF 入口
- 动态二进制: dlopen() 加载为共享对象,dlsym("main") 找入口,在常规协程内调用
拦截关键 libc 函数,让加载的二进制不会杀死宿主进程。
退出码传播:
- Coroutine 新增
exit_code: i32字段 vproc_exit_with_code(code)设置退出码并终止协程get_exit_code(pid)仅对已完成的协程返回Some(code),避免过早返回默认值 0- preload 层拦截
exit()/_exit()→ 调用vproc_exit_with_code()而非真实系统调用
虚拟 fd 表 (src/vfd.rs):
enum Vfd {
Real(i32), // 直通真实内核 fd
PipeRead(*mut PipeBuffer), // 管道读端
PipeWrite(*mut PipeBuffer), // 管道写端
}- 每个虚拟进程有独立 fd 命名空间(thread-local HashMap)
- fd 0/1/2 默认直通真实 stdin/stdout/stderr
- 支持 open/close/dup/dup2 操作
管道模拟:
PipeBuffer64 KiB 环形缓冲区- 读端空时 yield(协作式),写端满时 yield
- 支持协程间双向通信
LD_PRELOAD 拦截层 (src/preload.rs):
- 拦截 10 个函数: exit/_exit, fork, waitpid/wait4, execve, pipe, read, write, close, dup, dup2
- 所有函数检查
VPROC=1环境变量,未设置时直通真实 libc - dlsym(RTLD_NEXT) 结果缓存,避免重复符号解析
- 未在协程上下文时(current_vpid() == None)自动回退到真实 libc
测试结果:
--- Test 1: exit code propagation ---
[child] about to exit(42)
[parent] child exited with 42 (expected 42) ✅
--- Test 2: virtual pipe ---
[writer] wrote 12 bytes
[reader] got: hello pipe! ✅
Rust 编译的 .so 带入 Rust stdlib(panic handler、allocator),与宿主进程 glibc/bionic 冲突导致 crash。改用纯 C .so 做 LD_PRELOAD,Rust 运行时通过 dlopen 按需加载。
双层 .so 架构:
libvproc_preload.so (LD_PRELOAD, 纯 C ~310 行)
→ dlopen("libvproc.so") 或 RTLD_DEFAULT
→ 调用 vproc_ffi_* 函数
libvproc.so (Rust cdylib, FFI 运行时)
→ 导出 14 个 vproc_ffi_* 函数
→ 包装协程调度器、虚拟 fd、管道、ELF 加载
C preload 层 (preload/preload.c):
struct vproc_ffi一次解析所有 FFI 函数指针,dlsym批量加载- 先尝试
dlopen("libvproc.so"),失败则回退RTLD_DEFAULT(支持主二进制内嵌 FFI) - 拦截 12 个 libc 函数,全部检查
VPROC=1开关 - 无协程上下文时(
current_vpid() == 0)自动直通真实 libc
Rust FFI 层 (src/ffi.rs):
- 14 个
#[no_mangle] extern "C"函数导出运行时能力 vproc_ffi_exit()无协程上下文时用 rawsvc #0终止进程(避免 LD_PRELOAD 递归)- 支持 virtual fd 操作:
pipe,is_virtual_fd,read,write,close,dup,dup2
E2E 测试 (examples/e2e_preload.rs):
Test 1: exit(42) interception — 协程内 libc::exit(42) 被拦截,exit code 42 捕获 ✅
Test 2: multiple coroutines — 两个协程分别 exit(10)/exit(20) ✅
Test 3: _exit(99) interception — libc::_exit(99) 也被正确拦截 ✅
switches: 7, exit: 0
加载任意预编译动态 ELF 二进制,无需 -rdynamic 或符号导出。
问题: Phase 3 的 dlopen 路径要求 dlsym("main"),但 Termux 预编译二进制(apt, dpkg, ls, true)不导出 main 符号。
方案: dlopen + 入口点跳转。
1. 解析 ELF 头获取 e_entry(入口偏移)
2. dlopen() 加载二进制 → 动态链接器加载所有 DT_NEEDED 依赖
3. dl_iterate_phdr() 找到加载基址(canonicalize 解析符号链接)
4. 计算入口地址 = base + e_entry
5. 清空 DT_INIT_ARRAY/DT_INIT(防止 dlopen 已执行的构造器被 _start 再次调用)
6. Coroutine::new_elf() 构造协程栈(argc/argv/envp/auxv)
7. 跳转到入口点 → _start → __libc_init → main() → exit()
关键实现细节 (src/vexec.rs):
find_loaded_base(): 先canonicalize()解析符号链接(true→coreutils),再用dl_iterate_phdr按路径匹配基址clear_init_arrays(): 遍历 PT_DYNAMIC 中的 DT_INIT_ARRAY/DT_INIT 条目,将 d_val 清零。#[repr(C, packed)]结构体用addr_of_mut!+write_unaligned避免对齐 UBvirtual_execve()调度:优先 via_entry,失败回退 dlsym("main")
验证 — 加载真实 Termux 预编译二进制:
true → exit code 0 ✅
false → exit code 1 ✅
echo → exit code 0 ✅
在协程内实现 fork() 语义,子进程获得父进程栈的完整副本,无需创建真实 OS 进程。
问题: apt/dpkg 大量使用 fork() 创建子进程处理下载、解压、配置。没有虚拟 fork,所有依赖 fork 的程序都无法运行。
方案: helper 协程 + mco_fork_from 栈复制。
1. vproc_ffi_fork() 被 preload fork() 调用
2. 检查 is_fork_child 标志 → 子进程直接返回 0
3. 父进程: spawn helper 协程
4. do_yield() → mco_yield 保存协程上下文到父进程栈
5. helper 调用 spawn_fork_child():
- mco_fork_from() 复制父进程整个栈 + minicoro ctxbuf
- 设置 is_fork_child=true, 复制 fd 表
- 记录父子关系到 Executor.children
6. helper 退出 → 调度器切回父进程
7. 父进程读 fork_child_pid → 返回 child_pid
8. 子进程被调度时恢复到 do_yield() 之后 → fork_child_pid=0 → 返回 0
关键实现:
Coroutine::fork_from() (src/coroutine.rs):
- 调用
mco_fork_from()复制父协程完整栈(minicoro 管理) - 设置
ppid,is_fork_child,fork_child_pid=0
Executor::spawn_fork_child() (src/executor.rs):
- 封装 pid 分配 + 栈复制 + 队列插入 + children 追踪 + fd 表复制
children: HashMap<VPid, Vec<VPid>>追踪父子关系is_child_of(),reap_child()辅助 waitpid
VfdTable::clone_for_fork() (src/vfd.rs):
- Real fd: 直接复制(共享内核 file description,匹配 Linux fork 语义)
- Pipe fd: 共享同一个 PipeBuffer 指针(匹配 Linux pipe 语义)
验证:
--- Test 1: fork -> _exit(42) -> waitpid ---
[parent] child vpid = 3
[child] exiting with 42
[parent] child exited with 42 (expected 42) ✅
--- Test 2: 3 sequential fork children ---
[parent] child A vpid = 5
[child A] exiting with 10
[parent] child B vpid = 7
[child B] exiting with 20
[parent] child C vpid = 9
[child C] exiting with 30
[parent] child A exited with 10 (expected 10) ✅
[parent] child B exited with 20 (expected 20) ✅
[parent] child C exited with 30 (expected 30) ✅
switches: 15
修复 ELF 协程不执行的核心 bug,实现 sh -c "echo hello" 端到端通过。
问题: dlopen 的 ELF 二进制的 _start → __libc_init → exit() 路径中,exit() 终止了整个进程而非切回调度器。
根因分析:
__libc_init重新初始化 TLS → minicoro 的_mco_main_ctx线程局部变量丢失preload.rs的enabled()使用std::env::var("VPROC")(Rust TLS 依赖),TLS 重初始化后失效enabled()在set_var("VPROC","1")之前被首次调用(通过println!→write()链),缓存了false
修复:
enabled()改用libc::getenv()(C 级别,不受 TLS 重初始化影响)- 不缓存
false结果,每次检查直到发现VPROC=1 - 全局
AtomicPtr<Executor>替代 TLS(存活于__libc_init重初始化) exit()/_exit()→vproc_exit_with_code()+ rawexit_groupsyscall(不使用自旋循环)
验证:
=== sh -c "echo hello" test ===
hello
[parent] shell exited with 0
=== done ===
entry_demo: true→0, false→1, echo→0 ✅
4 路并行推进。
Track 1 — hermux 集成原型: 创建了 3 个文件:
vproc_wrapper.h— FFI 声明 + dlopen 运行时解析器termux_vproc.c— termux.c 替代版,运行时分发到 real fork 或 vproc 协程Android.mk.vproc— 构建配置- 关键设计: PTY 保持真实 fd,Java 层无需改动,需新 FFI 函数
vproc_ffi_create_process()
Track 2 — 复杂场景测试 (38 用例):
- ✅ exit code 传播 (0/1/42/255/true/false): 6/6 通过
- ✅ 基本 shell 命令: 3/6 通过
- ✅ 并发执行 (3 线程): 3/3 通过
- ✅ 复杂 shell (for/&&/||/变量/算术/引用): 11/12 通过
- ✅ 多命令会话 (export/子shell/分号): 10/11 通过
- ❌ 管道
|和命令替换$(): 5 个失败(虚拟 fork+pipe 路径不完整)
Track 3 — 代码清理:
- 删除
patch_got_entries()死代码 124 行 - 修复 3 个编译器警告 → 0 warnings
- 所有回归测试通过
Track 4 — syscall 拦截扩展:
- 新增
kill(pid, sig)拦截(虚拟 pid 返回 0,真实 pid 透传) - 新增
getpgid(pid)/setpgid(pid, pgid)stub - 新增
raise(sig)透传 - Rust + C 双层同步更新
修复管道操作符 | 导致的 SIGSEGV 和同一二进制连续 virtual_execve 挂起,解除阶段 9 识别的两个关键阻塞问题。
sh -c "echo hello | cat" 触发 SIGSEGV。
根因: 虚拟 fork(协程栈复制)+ 虚拟 pipe(内存环形缓冲区)无法满足 shell 的真实 pipe/fork 语义。bionic 的 waitpid() 内部调用 wait4(),而我们也拦截了 wait4(),形成 waitpid → libc waitpid → libc wait4 → 我们的 wait4 → 我们的 waitpid 无限递归 → 栈溢出 → SIGSEGV。
修复:
- 真实 fork:
fork()拦截器改用dlsym(RTLD_NEXT)获取真正的 libc fork(libc::fork()会解析到我们自己的符号),子进程设REAL_FORK_CHILD标志 - 真实 pipe:
pipe()始终创建真实 OS 管道 - raw wait4 系统调用: 绕过 libc 直接用
syscall(__NR_wait4)等待真实子进程,消除递归 - 直通模式:
REAL_FORK_CHILD的所有拦截器(read/write/close/dup/dup2/exit/_exit/execve)直通真实 libc
验证 — 8 项集成测试全部通过:
test_simple_echo .............. ok (echo hello)
test_sequential_builtins ...... ok (echo hello; echo world)
test_pipe_builtin_builtin ..... ok (echo hello | true)
test_pipe_builtin_cat ......... ok (echo hello | cat)
test_multi_stage_pipe ......... ok (echo hello world | cat | cat)
test_subshell_pipe ............ ok ((echo a; echo b) | cat)
test_pipe_with_grep ........... ok (printf 'foo\nbar\nbaz\n' | grep ba)
同一进程内第二次调用 virtual_execve_via_entry("sh", ...) 挂起。
根因:
extract_main_addr()的 LDR 指令掩码0xFFC003FF检查 Rn=0,但 dash 的_start用ldr x2, [x2, #off](Rn=2),导致 main() 地址提取失败- 即使提取成功,直接调用
main()时 shell 的全局变量(job table、fd tracking 等)残留第一次运行的脏状态,初始化挂死
修复:
- 放宽 LDR 掩码:
0xFFC003FF→0xFFC0001F,只检查 Rt=2 不检查 Rn - 寄存器配对: 改为从
ldr x2, [xN, #imm]反向搜索匹配的adrp xN,支持adrp+add指令对 - 可写段快照/恢复:
BINARY_CACHE保存 main 地址 + 所有可写 PT_LOAD 段的完整内容。后续调用先恢复段数据再调 main(),模拟 execve 的"干净进程映像"语义 - GOT re-patch: 段恢复后重新执行
patch_got_for_loaded_binary(),确保拦截器在__libc_init可能的 GOT 修改后仍然生效 - 原始权限恢复:
WritableSegment记录 PT_LOAD flags,restore 时恢复正确权限而非硬编码 RWX - DlHandle 保留: cache entry 保存 dlopen handle,为未来多 binary 卸载做准备
验证:
test_sequential_pipe_invocations ... ok (echo hello | cat → echo world | cat)
PR #6, #9, #10 — 资源管理、文件系统拦截、信号、per-coroutine cwd、二进制缓存生命周期。
资源管理 (PR #6):
Coroutine::mapped_regions— 协程退出时自动 munmap 加载的 ELF 段Coroutine::c_strings— 协程退出时释放 C 字符串内存VfdTable::Drop— 自动关闭所有虚拟管道,标记 EOFremove_table_and_get_fds()— 协程退出时关闭真实内核 fd,Arc 去重避免 double-close
文件系统拦截 (PR #6):
open()→ 拦截,创建Vfd::File(Arc<FileRef>)真实 fd 映射fstat()→ 拦截,透传到真实 fdlseek()→ 拦截,透传到真实 fdclose-on-exec—VfdTable::close_cloexec()execve 时关闭标记的 fd
信号 + per-coroutine cwd (PR #9):
Coroutine::pending_signals— 每协程信号队列Executor::deliver_signals()— 调度前投递,SIGKILL/SIGTERM 立即终止协程Coroutine::cwd— 每协程工作目录,chdir/getcwd虚拟化SIGPIPE— pipe 写端关闭后写操作触发 SIGPIPE 信号投递
二进制缓存生命周期 (PR #10):
BinaryCacheEntry::active_users: AtomicU32— 引用计数跟踪Coroutine::binary_path— 记录协程使用的二进制路径track_binary_user()— spawn 时递增 refcountrelease_binaries()— 协程退出时递减,zero-users 自动 dlcloseDlGuardRAII — 防止 error path 的 dlopen handle 泄漏reap_done_coroutines()— 统一清理:children map、fd table、mapped regions、binary cache
PR #13 — 多会话并发安全(Issue #11, #12)。
Spawn Queue 架构:
Java Thread A ──→ SPAWN_QUEUE ──→ Driver Thread ──→ Executor (exclusive)
Java Thread B ──→ SPAWN_QUEUE ──↗ │
Java Thread C ──→ WAITERS ────────── yield → check waiters → signal Condvar
SpawnRequest+SPAWN_QUEUE— Java 线程提交 spawn 请求,不直接修改 ExecutorDRIVER_WAKECondvar — 有新工作时唤醒 driver thread,空闲时阻塞(非轮询)vproc_ffi_create_process()— 提交队列请求 → 阻塞等待 Condvar → 返回 vpidvproc_ffi_run_until_exit()— 注册 waiter → 阻塞等待 Condvar → 返回 exit coderun_driver_loop()— drain queue → spawn → yield → reap done coroutines → check waiters → block
修复:
- Issue #12: 数据竞争 — driver thread 独占 Executor,消除并发修改
- Issue #11 Bug 2: 协程泄漏 —
reap_done_coroutines()在 driver loop 中清理资源 - Issue #11 Bug 1: signal 118 crash — 同 #12 根因,消除数据竞争后解决
PR #14, #15, #16 — 推送 v* tag 自动构建发布。
GitHub Actions Workflow (.github/workflows/release.yml):
- 触发:
push tags: ["v*"] - 构建:
nttld/setup-ndk@v1+cargo-ndk+aarch64-linux-android - 产出:
vproc-{version}-aarch64-linux-android.tar.gz(内含libvproc.so) - 发布:
softprops/action-gh-release@v2自动创建 Release + release notes build.rs移除 hardcodedgcc,让cccrate 自动使用 NDK clang
- ✅ 协程调度器(minicoro, 1000 协程,~36μs/switch debug)
- ✅ 虚拟 fork(栈复制 + fd 表复制,0 真实进程)
- ✅ waitpid(exit code 传播,父子关系追踪)
- ✅ PIE ELF 加载(静态 + 动态二进制)
- ✅ 退出码传播(exit() 不杀进程)
- ✅ 虚拟 fd 表 + 管道模拟
- ✅ 纯 C LD_PRELOAD .so(避免 Rust stdlib 冲突)
- ✅ 解释器委托(加载任意预编译动态二进制)
- ✅ 端到端验证:
sh -c "echo hello"完整工作流 - ✅ 信号拦截(kill/getpgid/setpgid/raise)
- ✅ 代码清理(0 compiler warnings)
- ✅ 管道操作符
|: 真实 fork + 真实 pipe + raw wait4(8 项集成测试) - ✅ 同一二进制连续 execve: 可写段快照/恢复 + GOT re-patch
- ✅ 多阶段管道:
echo hello | cat | cat三级管道通过 - ✅ 子 shell 管道:
(echo a; echo b) | cat通过 - ✅ hermux 集成 FFI:
create_process+run_until_exit(spawn queue 并发安全) - ✅ 多会话并发: driver thread 独占 Executor,Java 线程通过队列提交
- ✅ 文件系统拦截: open/fstat/lseek +
Vfd::Filefd 表管理 - ✅ per-coroutine cwd:
chdir/getcwd虚拟化 - ✅ 信号传递: SIGPIPE/SIGKILL/SIGTERM 虚拟化
- ✅ 二进制缓存生命周期: 引用计数 + auto-dlclose
- ✅ CI Release: 推送
v*tag 自动构建并发布libvproc.so - ✅ 40 项集成测试: pipe/file/cwd/signal/stress 全部通过
- ✅ C FFI E2E 测试: 10/10 通过(echo, exit, pipes, grep, stress)
- ✅ Termux 会话模拟: 18/18 通过(管道/Shell特性/Sequential sessions/压力测试)
- ✅ minicoro 生产级调度器: 替代自研 asm 上下文切换,修复 7 个 yield/resume bug
- setsid/作业控制 (Issue #8): 拦截 setsid/getpgrp/tcsetpgrp 返回虚拟值,或回退真实 fork
- 静态 PIE raw syscall: 直接
svc #0系统调用无法拦截
vproc/
├── Cargo.toml
├── build.rs # 编译 coro/minicoro.c
├── .github/workflows/
│ └── release.yml # CI: tag 触发构建 + GitHub Release
├── coro/
│ └── minicoro.c # minicoro v0.2.0 fork + 扩展 (ELF entry, fork_from, set_user_data)
├── preload/
│ ├── preload.c # 纯 C LD_PRELOAD 层 (~320 行)
│ ├── Makefile # 构建 libvproc_preload.so + 测试
│ └── test_preload.c # 基础拦截测试
├── src/
│ ├── lib.rs # 公开 API + c_array_to_vec 工具函数
│ ├── coroutine.rs # minicoro FFI 包装 + CoroUserdata + new_elf() + fork_from()
│ ├── executor.rs # minicoro 调度器 + fd swap + reap_done_coroutines()
│ ├── elf.rs # ELF64 解析器 (纯安全 Rust)
│ ├── ffi.rs # C FFI 接口 + spawn queue + driver thread + raw_dup3
│ ├── loader.rs # PIE 加载器 (mmap + 重定位) + build_auxv()
│ ├── vexec.rs # virtual_execve + binary cache lifecycle
│ ├── vfd.rs # 虚拟 fd 表 + 环形缓冲区管道 + clone_for_fork() + get_real_fds
│ └── preload.rs # Rust LD_PRELOAD 拦截层 (18 函数)
├── examples/
│ ├── basic.rs # 3 协程交替
│ ├── stress.rs # 1000 协程压力测试
│ ├── fork_sim.rs # 虚拟 fork/waitpid
│ ├── fork_exec_demo.rs # 虚拟 fork + exit + waitpid 演示
│ ├── vexec_demo.rs # 静态 PIE 加载演示
│ ├── vexec_dynamic_demo.rs # dlopen 动态加载演示
│ ├── pipe_demo.rs # 退出码传播 + 虚拟管道演示
│ ├── e2e_preload.rs # C preload + Rust runtime E2E 测试
│ ├── entry_demo.rs # 解释器委托演示 (加载 Termux 二进制)
│ ├── sh_test.rs # sh -c "echo hello" 端到端测试
│ └── test_single.rs # 测试工具,支持多命令顺序执行
└── tests/
├── pipe_tests.rs # 管道 + 连续 execve 集成测试 (8 项)
├── infra_tests.rs # 基础设施测试 (8 项)
├── file_tests.rs # 文件系统测试 (8 项)
├── cwd_tests.rs # 工作目录测试 (4 项)
├── signal_tests.rs # 信号测试 (3 项)
├── stress_tests.rs # 压力测试 (6 项)
├── hermux_sim/
│ ├── test_c_session.c # C FFI E2E 测试 (Hermux JNI 路径, 10 项)
│ └── test_termux_session.c # Termux 会话模拟 (管道/Shell特性/压力, 18 项)
├── test_static_pie.S # 最小 aarch64 静态 PIE (write+exit)
└── test_fork.c # fork/waitpid 拦截测试
cargo build --release
cargo run --example sh_test --release # sh -c "echo hello" 端到端
cargo run --example entry_demo --release # 加载 Termux 二进制
cargo run --example fork_exec_demo --release # 虚拟 fork 演示
cd preload && make e2e # C preload E2E 测试需要 aarch64 Linux/Android 环境。
从 GitHub Releases 下载 vproc-{version}-aarch64-linux-android.tar.gz:
tar -xzf vproc-v0.1.0-aarch64-linux-android.tar.gz
# 得到 libvproc.so推送 v* tag 自动触发 CI 构建并发布。
- hermux issue #526 — 原始提案和进展跟踪
- Go runtime — goroutine M:N 调度启发
- gVisor — 用户态内核参考
- Graphene — Library OS 参考