Appearance
WASM 编译管线 — 练习
练习 1:追踪单个 .c 文件的编译过程
目标:理解一个 C 源文件如何从源代码变为 WASM 目标文件。
任务:
- 在项目根目录找到 Makefile,定位
.c到.wasm.o的编译规则 - 选择一个简单的源文件(如
src/main.c),写出完整的编译命令 - 说明
-DMODERN=1和-DWASM=1在该文件中触发了哪些条件编译分支
参考答案
步骤分析:
- 在 Makefile 中查找
.wasm.o的模式规则,通常形如:
makefile
%.wasm.o: %.c
$(CC) --target=wasm32-unknown-unknown $(CFLAGS) -DMODERN=1 -DWASM=1 -c $< -o $@- 以
src/main.c为例,完整编译命令:
bash
clang --target=wasm32-unknown-unknown -O2 -fno-builtin -ffreestanding \
-Iinclude -DMODERN=1 -DWASM=1 \
-c src/main.c -o src/main.wasm.o- 条件编译分支示例:
c
#ifdef WASM
// WASM 特定实现:通过 WASM 线性内存地址访问
*(volatile u32*)(0x04000000) = value;
#else
// 原生 GBA:直接访问硬件寄存器
REG_DISPCNT = value;
#endif
#ifdef MODERN
// 现代实现:使用标准 C 函数
memcpy(dst, src, size);
#else
// 原始实现:使用 GBA BIOS 调用
CpuFastCopy(src, dst, size);
#endif关键理解:-DMODERN=1 使代码使用标准 C 库函数替代 GBA BIOS 调用;-DWASM=1 将硬件访问重定向到 WASM 线性内存中的模拟地址。
练习 2:分析 wasm-ld 链接选项
目标:深入理解每个链接选项对最终 WASM 模块的影响。
任务:
- 列出 wasm-ld 的所有链接选项,逐个分析其必要性
- 如果去掉
--allow-undefined,会发生什么? - 为什么
--initial-memory设置为 268435456(256MB)?
参考答案
选项逐一分析:
| 选项 | 必要性分析 |
|---|---|
--no-entry | 必要。WASM 模块由 JS 侧调用导出函数驱动,不需要标准入口点 |
--allow-undefined | 必要。游戏代码调用 GBA BIOS 函数(如 LZ77UnCompVram、BgAffineSet),这些符号在编译时不存在 |
--initial-memory=268435456 | 必要。256MB 提供足够空间存放解压后的图形资源、地图数据等 |
--export=AgbMain | 必要。JS 需要调用此函数初始化游戏 |
--export=WasmRunFrame | 必要。JS 的渲染循环每帧调用此函数 |
--export-all | 调试用。导出所有全局符号,便于调试和直接访问内部函数 |
去掉 --allow-undefined 的后果:
链接会失败,报出大量 "undefined symbol" 错误。游戏代码中引用了 GBA BIOS 函数,这些函数的实现在 JS 侧而非 WASM 模块中。--allow-undefined 允许这些符号在链接时保持未解析状态,运行时由 JS 通过 WASM 表导入提供。
256MB 初始内存的原因:
练习 3:对比 GBA ARM 构建与 WASM 构建
目标:理解同一份源代码如何通过不同编译配置生成不同目标。
任务:
- 在 Makefile 中找到 GBA 原生构建规则(ARM 目标)
- 对比 ARM 构建与 WASM 构建的编译标志差异
- 分析为什么某些文件只能用特定的构建配置编译
参考答案
编译标志对比:
| 维度 | GBA ARM 构建 | WASM 构建 |
|---|---|---|
| 目标三元组 | arm-none-eabi | wasm32-unknown-unknown |
| 处理器 | ARM7TDMI (32-bit RISC) | WASM 虚拟机 |
| 宏定义 | -DGBA=1 | -DMODERN=1 -DWASM=1 |
| 优化级别 | -O2 | -O2 |
| 标准库 | 无(裸机) | 无(-ffreestanding) |
| 链接器 | GNU ld (ARM) | wasm-ld |
| 输出格式 | ELF → ROM (.gba) | WASM (.wasm) |
| 内联汇编 | ARM 汇编 (__asm__) | 不可用,需纯 C 替代 |
只能用特定配置编译的文件:
src/gba/*.c中的 BIOS 函数实现 — ARM 构建需要这些,WASM 构建跳过(由 JS 模拟)src/agb_flash.c(Flash 存储访问)— WASM 构建使用 JS 模拟的存储接口- 包含 ARM 内联汇编的文件 —
WASM=1宏会启用纯 C 替代实现
关键差异的理解:
核心洞察:MODERN=1 和 WASM=1 这两个宏是使同一份源代码能在完全不同的硬件/虚拟机上运行的关键抽象层。
拓展挑战
自定义编译标志:尝试在 Makefile 中添加
-g调试信息标志,观察 WASM 模块体积的变化,并思考如何在浏览器 DevTools 中调试 WASM。符号导出分析:使用
wasm-objdump -x output.wasm列出所有导出符号,统计导出函数数量,并找出除了AgbMain和WasmRunFrame之外最常用的导出函数。内存布局优化:当前
--initial-memory设为 256MB。分析实际内存使用情况,尝试找到更小的合理值,并测量启动时间的变化。编译速度优化:研究如何使用
ccache或并行编译(make -j)加速 WASM 构建,记录构建时间对比。交叉对比验证:对比同一帧中 ARM 构建和 WASM 构建的游戏状态,验证两者在逻辑上的一致性。这需要理解 GBA 硬件模拟 中的内存映射。