WAMR 的 XIP 特性:让 AOT 代码直接在 Flash/ROM 中执行(Execution In Place)实战指南

发布时间:2026/9/18 8:58:28
WAMR 的 XIP 特性:让 AOT 代码直接在 Flash/ROM 中执行(Execution In Place)实战指南 WAMR 的 XIP 特性让 AOT 代码直接在 Flash/ROM 中执行Execution In Place实战指南【免费下载链接】fluent-bitFast and Lightweight Logs, Metrics and Traces processor for Linux, BSD, OSX and Windows项目地址: https://gitcode.com/GitHub_Trending/fl/fluent-bit导读在 Flash/ROM 只读存储上直接运行 AOTAhead-Of-Time代码是 WAMRWebAssembly Micro Runtime面向 IoT 嵌入式场景提供的一项关键能力它既能大幅降低运行时内存占用AOT 代码无需拷贝到 RAM也能在设备没有可执行内存可用时正常运行业务。本文以 WAMR 2.4.1 仓库中的官方文档 xip.md 为骨架结合 wamrc 编译器源码与 WAMR 运行时实现完整讲解 XIP 的工作原理、AOT 文件生成方法、已知限制以及如何通过--enable-builtin-intrinsics针对具体 MCU 微调内建函数读完即可在自己的目标平台上生成可用的 XIP 二进制。XIP 要解决什么问题传统 AOT 加载方式的瓶颈常规模式下WAMR 加载 AOT 文件时会把 AOT 代码text section从文件拷贝到 RAM 中的可执行内存区域再执行**重定位relocation**操作把函数调用地址、全局数据地址等打补丁进 AOT 代码。这带来两个典型问题内存开销大AOT 代码需要一份 RAM 副本对只有几十到几百 KB RAM 的 MCU 来说非常奢侈可执行内存可能不存在不少 IoT 芯片的 Flash/ROM 是只读的RAM 可能不具备可执行权限如 NX 限制此时 AOT 代码根本没有地方落地运行。XIP 的答案代码不搬移、不修改XIPExecution In Place就地执行的思路是让 AOT 文件直接放在只读的 Flash/ROM 上加载后不做拷贝、不做代码段重定位直接从原地执行。为此WAMR 在编译 AOT 时通过两条手段把重定位数量压到最少参见 xip.md函数调用全部改为间接调用AOT 函数不再直接 call 其他函数而是从函数指针表function pointer table中查表获得目标函数指针后再调用。该函数指针表由函数的第一个参数exec_env携带从而消除了指向其他函数的重定位消除 LLVM 内建函数调用或者把 LLVM intrinsic 的调用替换为 WAMR 运行时自实现函数的调用例如把llvm.experimental.constrained.fadd.f32替换为aot_intrinsic_fadd_f32。这类替换在源码中有直接证据aot_intrinsic.c 定义了aot_intrinsic_fadd_f32等运行时函数并在其映射表中登记了llvm.experimental.constrained.fadd.f32 - aot_intrinsic_fadd_f32的对应关系见 aot_intrinsic.c。生成 XIP 版 AOT 文件两种等价命令XIP 文件本质上是没有或极少针对 AOT 代码段重定位的 AOT 文件。开发者用 wamrc 编译时开启两个编译选项即可官方文档给出了两种写法xip.md# 写法一显式指定两个选项 wamrc --enable-indirect-mode --disable-llvm-intrinsics -o aot_file wasm_file # 写法二使用 --xip 短选项等价 wamrc --xip -o aot_file wasm_file其中--xip是--enable-indirect-mode --disable-llvm-intrinsics的简写。在 wamrc 命令行解析源码 main.c 中可以看到--xip就是同时把is_indirect_mode与disable_llvm_intrinsics两个编译选项置为 trueelse if (!strcmp(argv[0], --xip)) { option.is_indirect_mode true; option.disable_llvm_intrinsics true; }wamrc 的帮助信息也明确标注了这两个选项的用途main.c--enable-indirect-mode通过符号表调用函数而非直接调用--disable-llvm-intrinsics禁用 LLVM 内建 intrinsics--enable-builtin-intrinsicsflags开启特定内建 intrinsics仅在--disable-llvm-intrinsics生效时有效。编译结果如何标记从编译器实现看开启is_indirect_mode后生成的 AOT 文件会在 ELF 头的e_type字段上标记为E_TYPE_XIP值为 4见 aot_emit_aot_file.c 与 aot_emit_aot_file.c。运行时加载器会依据该标记识别并处理 XIP 文件从而跳过代码段的重定位与拷贝。为什么必须禁用 LLVM intrinsicsLLVM intrinsic 无法直接映射到原生指令直接使用 LLVM intrinsic 会破坏 XIP 的前提。以整数除法i32.div_s为例如果目标 CPU 没有除法指令LLVM 无法把该 intrinsic 直接映射为原生指令只能退化为调用 libgcc/compiler-rt 中的运行时函数。这样一来AOT 代码中就会产生指向 libgcc/compiler-rt 的重定位而这些外部符号的地址在设备上无法在加载时解析且会要求打补丁修改代码段——这对 XIP 完全不可接受。替换策略运行时自实现函数因此WAMR 的策略是用运行时自实现的函数替换 LLVM intrinsic这些自实现函数可以通过函数指针表调用对应--enable-indirect-mode不产生直接调用重定位它们不依赖 libgcc/compiler-rt因此不产生指向外部运行库的重定位对应--disable-llvm-intrinsics。在编译器代码中可以看到disable_llvm_intrinsics开关被广泛用于控制浮点运算、比较、类型转换等指令的发射路径例如 aot_emit_numberic.c 中根据该开关选择发射 intrinsic 调用还是发射对运行时函数的调用。已知问题官方文档明确列出目前仍存在的一个已知问题xip.md可能仍存在部分针对.rodata之类节的 relocation这些 relocation 需要 patch AOT 代码未来会做更多工作来解决它。也就是说当前版本下完全没有重定位是理想状态个别场景例如常量数据段相关的地址修正仍可能在代码段上留下少量重定位后续版本会持续改进。在使用 XIP 时应对此有预期必要时检查生成文件的段信息。调优 XIP 内建 intrinsics为什么需要调优WAMR 为部分目标平台提供了默认的 intrinsic 映射表但官方文档明确指出默认映射表不一定是你目标平台的最佳选择它并未覆盖所有受支持的目标平台。所以 wamrc 提供了--enable-builtin-intrinsicsintr1,intr2,...选项允许针对目标硬件能力手工指定要开启的内建 intrinsic 集合xip.md。可调优的 intrinsic 全集下表是文档给出的完整可用 intrinsic 列表xip.md按功能分组LLVM intrinsic 名称说明llvm.experimental.constrained.fadd.f32/.f64float32 / float64 加法llvm.experimental.constrained.fsub.f32/.f64float32 / float64 减法llvm.experimental.constrained.fmul.f32/.f64float32 / float64 乘法llvm.experimental.constrained.fdiv.f32/.f64float32 / float64 除法llvm.fabs.f32/.f64float32 / float64 绝对值llvm.ceil.f32/.f64float32 / float64 向上取整llvm.floor.f32/.f64float32 / float64 向下取整llvm.trunc.f32/.f64float32 / float64 截断llvm.rint.f32/.f64float32 / float64 就近取整llvm.sqrt.f32/.f64float32 / float64 平方根llvm.copysign.f32/.f64float32 / float64 符号复制llvm.minnum.f32/.f64float32 / float64 最小值llvm.maxnum.f32/.f64float32 / float64 最大值llvm.ctlz.i32/.i64int32 / int64 前导零计数llvm.cttz.i32/.i64int32 / int64 尾随零计数llvm.ctpop.i32/.i64int32 / int64 置位计数f64_convert_i32_s/f64_convert_i32_uint32 / uint32 转 float64f32_convert_i32_s/f32_convert_i32_uint32 / uint32 转 float32f64_convert_i64_s/f64_convert_i64_uint64 / uint64 转 float64f32_convert_i64_s/f32_convert_i64_uint64 / uint64 转 float32i32_trunc_f32_s/i32_trunc_f32_ufloat32 转 int32 / uint32i32_trunc_f64_s/i32_trunc_f64_ufloat64 转 int32 / uint32i64_trunc_f64_s/i64_trunc_f64_ufloat64 转 int64 / uint64i64_trunc_f32_s/i64_trunc_f32_ufloat32 转 int64 / uint64f32_demote_f64float64 转 float32降精度f64_promote_f32float32 转 float64升精度f32_cmp/f64_cmpfloat32 / float64 比较i64.div_s/i64.div_uint64 / uint64 除法i32.div_s/i32.div_uint32 / uint32 除法i64.rem_s/i64.rem_uint64 / uint64 取余i32.rem_s/i32.rem_uint32 / uint32 取余i64.or/i64.andint64 按位或 / 按位与i32.const将 i32 常量写入常量表i64.const将 i64 常量写入常量表f32.const将 f32 常量写入常量表f64.const将 f64 常量写入常量表组合 intrinsic一键开启一组能力逐个列出太繁琐时文档提供了组合 intrinsic 来简化调优xip.md组合名展开内容all上表全部 intrinsici32.commoni32.div_s、i32.div_u、i32.rem_s、i32.rem_ui64.commoni64.div_s、i64.div_u、i64.rem_s、i64.rem_u、i64.or、i64.andf32.commonf32_cmp、fadd/fsub/fmul/fdiv.f32、llvm.fabs.f32、llvm.ceil.f32、llvm.floor.f32、llvm.trunc.f32、llvm.rint.f32、llvm.sqrt.f32、llvm.copysign.f32、llvm.minnum.f32、llvm.maxnum.f32f64.commonf32_demote_f64、f64_promote_f32、f64_cmp、fadd/fsub/fmul/fdiv.f64、llvm.fabs.f64、llvm.ceil.f64、llvm.floor.f64、llvm.trunc.f64、llvm.rint.f64、llvm.sqrt.f64、llvm.copysign.f64、llvm.minnum.f64、llvm.maxnum.f64f32xi32i32_trunc_f32_s、i32_trunc_f32_u、f32_convert_i32_s、f32_convert_i32_uf64xi32i32_trunc_f64_s、i32_trunc_f64_u、f64_convert_i32_s、f64_convert_i32_uf32xi64i64_trunc_f32_s、i64_trunc_f32_u、f32_convert_i64_s、f32_convert_i64_uf64xi64i64_trunc_f64_s、i64_trunc_f64_u、f64_convert_i64_s、f64_convert_i64_uconstopi32.const、i64.const、f32.const、f64.constfpxintf32xi32、f64xi32、f32xi64、f64xi64fp.commonf32.common、f64.common这些组合在运行时的映射逻辑中有对应实现例如 aot_intrinsic.c 中通过字符串匹配把all、i32.common、i64.common、fp.common、f32.common、f64.common、f32xi32、f64xi32、f32xi64、f64xi64、fpxint、constop等组合名逐个展开为具体 intrinsic 集合。实战示例按 MCU 硬件能力选型选择开启哪些 intrinsic 的原则是根据目标平台硬件能力CPU 能直接原生执行的运算就不需要替换CPU 无法原生执行的运算缺 FPU、缺除法指令、字长不足等才需要开启对应 intrinsic。示例一ARM Cortex-M55Cortex-M55 带有双精度浮点单元因此 f32/f64 浮点运算都能原生支持但它作为 32 位 MCU只能原生支持 32 位整数运算64 位整数运算必须走运行时函数。因此文档给出的命令是xip.mdwamrc --targetthumbv8m.main --cpucortex-m55 --xip \ --enable-builtin-intrinsicsi64.common -o hello.aot hello.wasm即XIP 模式间接调用 禁用 LLVM intrinsics下仅开启i64.common64 位整数除法、取余、按位或/与。示例二ARM Cortex-M3Cortex-M3没有浮点单元且只能原生支持 32 位整数运算因此浮点运算、32/64 位整数与浮点互转全部需要运行时函数兜底。文档给出的命令是xip.mdwamrc --targetthumbv7m --cpucortex-m3 --xip \ --enable-builtin-intrinsicsi64.common,fp.common,fpxint -o hello.aot hello.wasm其中i64.common覆盖 64 位整数除法/取余/位运算fp.common覆盖 f32/f64 全套浮点运算与比较fpxint覆盖 f32/f64 与 i32/i64 之间的全部类型转换。其他平台的调优思路其他平台照此办理即可先盘点目标 CPU 的原生指令能力有无 FPU、有无除法指令、整数位宽再把无法原生执行的运算对应的 intrinsic 通过--enable-builtin-intrinsics开启。多开会让代码更通用但可能引入不必要的运行时调用开销少开会因缺失运行时函数而无法正确链接/执行需要按硬件能力精确取舍。加载与运行侧的配合XIP 不只是编译期的事运行时也要能识别并正确处理这类文件编译阶段wamrc 会把is_indirect_mode信息写入 AOT 文件的e_typeE_TYPE_XIP见 aot_emit_aot_file.c加载阶段运行时加载器 aot_loader.c 会读取该标记对 XIP 文件走不拷贝、不 patch 代码段的加载路径直接从只读存储区加载执行平台侧需要在 board/链接脚本中把 AOT 文件放在可执行权限的只读段Flash/ROM内并把函数指针表等只读数据一并放置妥当。此外WAMR 在编译时还针对 XIP 场景做了防御性处理从源码注释看aot_emit_aot_file.c 中特别针对不支持直接执行no exec的平台上的 XIP做了相关处理避免生成不可执行的代码布局。小结与使用建议关键点结论XIP 的本质AOT 代码直接运行在只读 Flash/ROM不拷贝、不 patch 代码段两个核心机制间接调用函数指针表 禁用/替换 LLVM intrinsics生成命令wamrc --xip -o aot wasm等价于两个长选项已知限制部分.rodata类 relocation 仍可能要求 patch 代码段官方承诺后续改进调优入口--enable-builtin-intrinsicsintr1,intr2,...支持组合名批量开启选型原则按目标 CPU 的原生指令能力决定开启哪些 intrinsic如果你的目标平台没有可执行内存或者RAM 极为紧张XIP 是让 AOT 跑起来的首选方案生成后建议用 WAMR 自带的 aot-analyzer见 aot-analyzer 源码等工具检查生成的 AOT 文件确认代码段重定位情况符合预期想深入了解 wamrc 各编译选项与 WAMR 构建流程可继续阅读 build_wamr.md涉及内存占用优化的更多讨论见 memory_tune.md。说明本文所述命令与行为均以仓库内 WAMR 2.4.1 版本的源码与文档为准具体目标平台如其它 MCU/编译器组合的可用性请以该版本 wamrc 的实际输出和 target 支持情况为准。【免费下载链接】fluent-bitFast and Lightweight Logs, Metrics and Traces processor for Linux, BSD, OSX and Windows项目地址: https://gitcode.com/GitHub_Trending/fl/fluent-bit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考