【免费下载链接】LinuxRunner
Run ARM64 Linux userspace and desktop applications on iPhone and iPad.
LinuxRunner 让 iPhone 和 iPad 直接运行 ARM64 Linux 用户态程序与桌面应用,它的核心是一台名为 Asbestos 的 ARM64 指令解释器。下面拆解 Asbestos 如何把一条条 ARM64 指令,编译成 iPhone 上最快的执行路径——从默认的 Gadget 线程代码,到可选的原生 JIT 与 AOT 预编译,并给出实测性能与源码导读。
一、Asbestos 是什么:一个不启动内核的 ARM64 引擎
先澄清一个常见误解:LinuxRunner 不启动 Linux 内核、不用硬件虚拟化、也不是整机模拟器。它只提供 Linux 用户态兼容层,而驱动这些程序跑起来的"心脏"就是 Asbestos 解释器。
用官方架构文档的原话说:
The Asbestos interpreter decodes supported instructions into cached sequences of precompiled host routines (gadgets). ——docs/ARCHITECTURE.md
翻译成大白话:Asbestos 把 Linux 程序的每条 ARM64 指令,解码成一段段预先编译好的 host 例程(叫 gadget)并缓存下来。因为 guest 和 host 都是 ARM64,gadget 用的就是本机原生指令,所以速度接近"跑自己",而不是模拟一个 CPU。
每个地址空间对应一个 struct asbestos,它维护:
- 已翻译的基本块(
fiber_block)哈希表; - 软件 TLB 的失效代际(
invalidate_gen、code_seen位图),保证内存拷贝-on-write 时缓存代码能及时作废; - jetsam 回收链表 + RCU 风格的原子计数(
jit_active_threads),在没有锁的情况下安全地释放旧块。
💡 记住三个名词就够了:基本块(一段没有分支的连续指令)、gadget(一条/一小段指令的原生实现)、代码流(一串 gadget 指针,CPU 顺着它一路
br跳过去执行)。
二、三步看懂执行路径:从 guest 指令到 host 代码
把整个流程压缩成三步,你就抓住了 Asbestos 的骨架:
| 阶段 | 做什么 | 关键文件 |
|---|---|---|
| ① 解码 | 遇到新基本块时,把 guest 指令逐条解码成 gadget 指针流 | gen.c |
| ② 缓存 | 把这段"代码流"存进 fiber_block.code[] 并按地址入哈希表 | asbestos.c |
| ③ 执行 | fiber_enter 进入代码流,gadget 用 br x8 一条跳一条,直到分支/系统调用才退出回主循环 | entry.S |
① 解码发生在 asbestos/guest-arm64/gen.c(约 7800 行)。它读入 32 位定长的 ARM64 指令,边解码边做指令融合(比如把 CMP+B.cond、ADRP+ADD、LDR+CBZ 这种高频组合合成一条更快的 gadget),并做窥孔优化。一个典型的基本块平均只有 4 条左右指令,对应约 16 个 gadget 参数(见 asbestos.h 里的 FIBER_BLOCK_INITIAL_CAPACITY 16)。
③ 执行的关键在 entry.S。gadget 之间靠一个 gret 宏接力:
_cpu(x1)指向 CPU 状态,_tlb(x2)指向软件 TLB,_pc(x28)指向当前代码流位置;gret从_pc取出下一个 gadget 指针,然后br x8直接跳过去——不经过 C 主循环,这就是"线程代码"(threaded code)的精髓。
只有碰到分支、系统调用或中断,代码流才会 fiber_exit 退出,回到 C 主循环 cpu_run_to_interrupt 做调度、处理 INT_SYSCALL 等,再 fiber_enter 重新进入。
三、块链接与 ret_cache:让热路径少绕远
光靠"一条跳一条"还不够快,Asbestos 在两点上把热路径进一步压短:
块链接(Block Chaining)。 当基本块 A 跳转的下一个块 B 恰好是它在 guest 内存里的紧邻块、且同页时,A 可以直接"改指针"指向 B 的代码流,省去回主循环。entry.S 里的 fiber_ret_chain 专门走这条快路:源码注释提到,相邻同页的前向跳转约占链式转移的 69%,这部分可以直接跳过内存一致性检查继续执行。
返回地址预测(ret_cache)。 函数返回是最高频的分支之一。frame.h 为每个线程维护一张 ret_cache[4096] 表,把"返回地址 → 调用点 gadget 参数"缓存起来,ret 时命中就一步到位,不用重新查表。
这两步都是纯 C/汇编、零额外内存映射,所以即使不开 JIT,解释器本体也已经相当能打。
四、可选 JIT:把最热的指令块翻译成原生机器码
在 gadget 流之上,Asbestos 还叠了一层可选的原生 JIT(jit.c,约 3700 行)。设计写在 jit.h 顶部注释里,一句话概括:
A layer on top of the threaded-code blocks: after a block's gadget stream is generated, runs of supported guest instructions are translated to host code in a MAP_JIT region, and the first gadget pointer of each run is replaced by a pointer to that code.
也就是说:
- 仍然先按 gadget 流生成,再把连续的、受支持的指令段翻译成真正的 host 机器码,写进一块 120 MB 的
MAP_JIT区域; - 翻译时把最热的 guest 寄存器 pin 到固定 host 寄存器上,跨原生代码保持不落地;
- 访存指令内联 TLB 查找,miss 时才回退到该段的 gadget;
- 块与块之间通过
jit_link / jit_unlink打直接分支,让控制流尽量留在原生代码里。
JIT 是可选的。 默认构建(LINUXRUNNER_JIT=NO)只有解释器,不需要可执行内存权限或调试器;带 JIT 的 Build 900 IPA 里则包含 JIT 代码。在 iOS 真机上,JIT 能否激活取决于系统是否放行可执行内存(通常需调试器附加),状态可通过终端里的 /proc/ish/jit 查看,详情见 docs/engine/IOS_JIT.md。模拟器实测一次运行就翻译了 16571 个块并稳定跑在 mode: JIT。
🔧 想调参?环境变量如
ISH_JIT_STATS=1、ISH_JIT_DUMP=<file>、ISH_JIT_PIN=0等都能在 jit.h 顶部查到。
五、AOT 预编译:连可执行内存都不映射
JIT 还需要运行时生成机器码,AOT 则更"静态":把 JIT 的翻译结果提前录制成汇编,直接链进目标程序。
- aot.h 定义了镜像结构:
aot_module(带build_id、sha256校验)、aot_trans(每个块的原生码段 + 块间直接链接)、aot_loop(自环块的寄存器提升)。 - 用
ISH_JIT_RECORD录一次,tools/jit_aot把它铺成.S;一个constructor在加载时调用ish_aot_register()即可。 - 关键点:一个
-Djit_emit=false的构建只跑 AOT 镜像,从不映射可执行内存——对"完全不允许 JIT 的场景"尤其友好。
三条路径的取舍,可以这样记:
| 执行路径 | 是否运行时生成机器码 | 需要可执行内存 | 适合场景 |
|---|---|---|---|
| Gadget 解释器(默认) | 否(只生成 gadget 指针流) | 否 | 通用、无权限依赖 |
| 原生 JIT(可选) | 是 | 是(iOS 需系统放行) | 热代码块、追求极限 |
| AOT 预编译 | 否(离线录制) | 否 | 不允许 JIT、冷启动最快 |
六、实测性能:解释器比原生慢多少?
性能数据来自 docs/engine/BENCHMARKS.md。需要强调:测量在 Apple M1 的 MacBook Air 上完成,不是 iPhone;但引擎代码在 iOS 上完全相同,相对差距可迁移。
interp_bench 微基准(相对原生的剩余减速倍数,越低越好):
| 工作负载 | 相对原生 | 说明 |
|---|---|---|
| syscall | 1.3× | 系统调用开销已压得很小 |
| alu | 4.5× | 整数运算 |
| branch | 7.5× | 分支密集 |
| call | 17.3× | 函数调用 |
| string | 24.7× | memcpy 主导的字符串 |
宏基准里,git hash-object、sha256sum、gzip、xz、sort、Python 启动等真实 Debian 程序都能跑,且新旧引擎输出校验和逐位一致(正确性护栏)。文档还诚实记录了被否决的优化——比如"融合 LDP 前/后索引"在调用密集型代码上反而慢了约 8%,没有采用。
七、源码导读:从哪几个文件开始读
想深入 Asbestos,建议按下面这条"主线"读(文件均为仓库相对路径):
- asbestos/asbestos.h —— 先看
struct asbestos和struct fiber_block,理解"块"长什么样。 - asbestos/asbestos.c —— 主调度循环
cpu_run_to_interrupt与编译入口fiber_block_compile。 - asbestos/guest-arm64/gen.c —— 指令解码与 gadget 生成(融合/窥孔都在这)。
- asbestos/guest-arm64/gadgets-aarch64/entry.S ——
fiber_enter / fiber_ret_chain / fiber_exit,host↔guest 的边界。 - asbestos/guest-arm64/jit.c —— 原生 JIT:代码区域、内联 TLB、块间直接链接。
- asbestos/guest-arm64/aot.h —— AOT 镜像的布局与注册。
配套文档:整体架构看 docs/ARCHITECTURE.md,iOS 上的 JIT 激活细节看 docs/engine/IOS_JIT.md,性能方法论与数据看 docs/engine/BENCHMARKS.md。
写在最后
Asbestos 的高明之处在于分层取舍:默认的 Gadget 线程代码零权限依赖、开箱即跑;可选的 JIT 在系统放行时把热块榨到极限;AOT 则给"绝对不能 JIT"的场景留了一条快路。三者共用同一套块缓存、块链接和软件 TLB,所以你看到的"iPhone 跑 Linux 桌面",本质是这台 ARM64 指令引擎把每一层开销都压到最小后的结果。
【免费下载链接】LinuxRunner
Run ARM64 Linux userspace and desktop applications on iPhone and iPad.
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考







