- 虚拟化
- 系统编程
【免费下载链接】ipasim
iOS emulator for Windows
本文聚焦 ipasim 项目构建问题文档i4:在 Windows 上用 ipasim 的补丁 Clang 编译 Apple Objective-C runtime 的objc-exception.mm时,Clang 的异常处理 CodeGen 代码CGException.cpp触发fell into catch-all case!断言失败。读完本文,你将理解 ipasim 的 superbuild 补丁编译器构建链是如何组织 Clang 与主项目的先后关系的、objc-exception.mm在objcruntime 库中的编译与导出配置,以及这个断言的触发机理和可操作的调试路径。
问题定义:一条断言失败背后的构建障碍
ip4 的原始记录非常短,它只描述了现象:
Assertion failed: typeValue && "fell into catch-all case!", file C:\project\deps\clang\lib\CodeGen\CGException.cpp, line 965这条记录给出了三个关键事实:
- 触发源:用 ipasim 的补丁 Clang("our patched Clang")构建 Apple Objective-C runtime 的
objc-exception.mm文件; - 崩溃点:Clang 代码生成阶段的
lib/CodeGen/CGException.cpp——即 Clang 处理try/catch/throw异常语义的模块; - 断言含义:
typeValue && "fell into catch-all case!"表明 CodeGen 在为某个 catch 子句解析其类型对应的异常表条目(TypeInfo)时落到了"兜底"分支,而该分支期望已经拿到一个非空的typeValue,否则说明该 catch 类型在异常类型表中根本无法解析。
需要说明两点边界:
- 错误信息里的绝对路径
C:\project\deps\clang\...是项目早期构建环境的路径布局。按当前的 Dockerfile 与 scripts/build.ps1,容器内源码挂载在C:\ipaSim\src、构建目录为C:\ipaSim\build,所以该路径与断言行号(965 行)对应的是当时使用的 Clang fork 版本; i4文档只记录了问题现象,仓库中并未记录其解决方式。实际的 Clang 补丁保存在.gitmodules声明的ipasimulator/clangfork(port_70分支)中,本文镜像里该子模块目录为空,因此无法直接展示补丁前后的CGException.cpp代码差异。
背景:ipasim 为什么必须维护一棵补丁 Clang
理解i4的前提,是理解 ipasim 的依赖体系。ipasim 把 iOS 应用跑在 Windows 上,其中"机器码用 Unicorn 模拟,系统功能翻译成 Windows 等价实现"。要做到这一点,它不能直接用原版 LLVM/Clang,而必须大规模修改 Clang:新增ipasimObjective-C runtime、在lld-link中加入承载 Mach-O 头的.mhdr节、新增.fixbind节用于运行时修正__declspec(dllimport)的间接绑定等等。这些动机在 issues 与 tags 总索引 的LLVM一节中有系统性说明。
所有第三方依赖都以递归 Git 子模块的形式引入,目的是方便打补丁(README 明确要求git clone --recurse-submodules)。.gitmodules中与i4相关的条目是:
[submodule "deps/clang"] path = deps/clang url = https://github.com/ipasimulator/clang branch = port_70 [submodule "deps/llvm"] path = deps/llvm url = https://github.com/ipasimulator/llvm branch = port_70 [submodule "deps/lld"] path = deps/lld url = https://github.com/ipasimulator/lld branch = port_70 [submodule "deps/objc4"] path = deps/objc4 url = https://github.com/ipasimulator/objc4 branch = portdeps/llvm、deps/clang、deps/lld统一使用port_70分支(基于 LLVM 7.0 系列移植),而deps/objc4(提供objc-exception.mm的上游 Apple objc4 runtime 源码)使用port分支。
构建链:原版 Clang 先编译补丁 Clang,补丁 Clang 再编译主项目
i4的断言发生在"补丁 Clang 编译主项目源码"这一步。这一步能成立,依赖 ipasim 精心设计的 superbuild 结构。
两阶段编译器切换
第一阶段的入口是容器内的 scripts/build.ps1:
mkdir -Force "C:/ipaSim/build" >$null pushd "C:/ipaSim/build" cmake -G Ninja "C:/ipaSim/src" # ... ninja ipaSim-x86-Debug ipaSim-x86-Release配置阶段在 根 CMakeLists.txt 中默认开启SUPERBUILD,此时项目语言设为NONE,只负责串联各子构建(clang、ipasim、winobjc、lief、Libffi 的 x86 Debug/Release 八组配置)。关键技巧在于 CMakeLists.txt 第 9 行起的 superbuild 分支 通过add_superbuild_target逐个生成子 CMake 构建,其中 clang 子构建由 scripts/clang-x86-Debug.cmake 驱动——注意它用系统原版 Clang(clang-cl.exe,由 Dockerfile 中choco install llvm --version 7.0.0安装)来编译补丁 Clang 源码树deps/clang:
COMMAND "${CMAKE_COMMAND}" -G Ninja "-DLLVM_TARGETS_TO_BUILD=X86;ARM" "-DLLVM_EXTERNAL_CLANG_SOURCE_DIR=${SOURCE_DIR}/deps/clang" "-DLLVM_EXTERNAL_LLD_SOURCE_DIR=${SOURCE_DIR}/deps/lld" "-DCMAKE_CXX_COMPILER=${LLVM_BIN_DIR}/clang-cl.exe" "-DCMAKE_LINKER=${LLVM_BIN_DIR}/lld-link.exe" "-DCMAKE_C_FLAGS=-m32" "-DCMAKE_CXX_FLAGS=-m32" "${SOURCE_DIR}/deps/llvm"第二阶段,主项目的配置脚本 scripts/ipaSim-x86-Debug.cmake 把编译器切到刚编出来的补丁clang.exe/lld-link.exe(CLANG_EXE由 scripts/CommonVariables.cmake 定义)。这个"先测编译器再换编译器"的思路源自 i2(构建补丁 Clang 作为依赖的问题),其实现落在 CMakeLists.txt 非 superbuild 分支:
# Now, the compiler was tested, so we can change it to our # possibly-not-yet-existing patched Clang. See i2. if (NOT USE_ORIG_CLANG) if (USE_DEBUG_CLANG) set (CLANG_EXE "${BUILT_DEBUG_CLANG_EXE}") set (LLD_LINK_EXE "${BUILT_DEBUG_LLD_LINK_EXE}") else (USE_DEBUG_CLANG) set (CLANG_EXE "${BUILT_CLANG_EXE}") set (LLD_LINK_EXE "${BUILT_LLD_LINK_EXE}") endif (USE_DEBUG_CLANG) endif (NOT USE_ORIG_CLANG) set (CMAKE_C_COMPILER "${CLANG_EXE}") set (CMAKE_CXX_COMPILER "${CLANG_EXE}")两个选项值得注意:USE_ORIG_CLANG(默认OFF)允许回退到原版 Clang——这正是排查i4这类"补丁引入的回归"时的对照开关;USE_DEBUG_CLANG则允许选用 Debug 版补丁 Clang,便于在 Clang 本体里下断点观察 CodeGen 行为。
强制重编与链接命令改写
补丁 Clang 一旦重建,整个主项目必须跟着重编。CommonVariables.cmake 中的add_prep_dep通过OBJECT_DEPENDS "${BUILT_CLANG_EXE};${BUILT_LLD_LINK_EXE}"把每个目标的所有源文件声明为依赖clang.exe与lld-link.exe,编译器一变即全量重编。同时,CMakeLists.txt 的update_commands宏 改写了链接命令模板,让所有链接都走-fuse-ld=lld-link,保证链接也使用补丁 lld(它带有.mhdr、.fixbind等与 runtime 初始化相关的定制)。
objc-exception.mm:被断言击中的文件及其编译配置
objc-exception.mm属于objc目标(即移植版libobjc.dll)的一部分。src/objc/CMakeLists.txt 说明了它的来源与编译方式。
文件来源:上游 objc4 runtime 的移植
ORIG_SOURCE_FILES 列表 中列有objc-exception.mm,全部上游源文件通过
list (TRANSFORM ORIG_SOURCE_FILES PREPEND "${SOURCE_DIR}/deps/objc4/runtime/")(CMakeLists.txt 第 52 行)定位到deps/objc4/runtime/,即子模块ipasimulator/objc4的port分支。整个objc库的移植方法、宏定义(TARGET_OS_*、__OBJC2__、HAVE_STRUCT_TIMESPEC)与代理头文件机制(proxy#includes)在 src/objc/README.md 中有完整说明——由于不想引入整个 MacOSX SDK,项目为需要的头文件添加了只转发到正确位置的代理.h。
编译宏与运行时版本选择
config_target 宏 为objc目标注入了决定异常行为的关键参数:
target_compile_definitions ("${target}" PRIVATE BUILDING_OBJC=1 NOMINMAX OBJC_PORT __OBJC2__=1) target_compile_options ("${target}" PRIVATE -fblocks -fobjc-runtime=macosx-10.13.0)其中-fobjc-runtime=macosx-10.13.0决定了 Clang 为 Objective-C 异常选用哪一套机制——从 CMake 配置看,项目选择了 macOS 10.13 对应的较新 Objective-C 异常路径(这与src/objc/README.md中"构建新的、也是 iPhone 上唯一可用的 runtime"(__OBJC2__)的说明一致)。异常机制的选用直接影响CGException.cpp在 CodeGen 阶段如何为 catch 子句建立类型表:运行时版本选项、_HAS_EXCEPTIONS等定义与源码中实际出现的异常语法共同决定了 CodeGen 会走哪条分支。可以推断,i4的断言正是这条路径上某个 catch 类型未能被解析进异常表的体现——具体是补丁对符号、mangling 或异常表布局的改动与这里的选择失配,需要从 clang fork 的补丁历史中定位,本文镜像中该子模块为空,无法进一步给出代码级证据。
链接导出:objc_ehtype_vtable
编译完成后,objc库通过模块定义文件显式导出符号(CMakeLists.txt 第 126-128 行):
target_link_libraries (objc PRIVATE # Specify module definition file `libobjc.def`. "-Wl,/def:${CMAKE_CURRENT_SOURCE_DIR}/libobjc.def")而 libobjc.def 中专门留有一段针对本文件的导出:
; From objc-exception.mm.obj. ; TODO: Why is this not exported even though it has `OBJC_EXPORT` in source code? objc_ehtype_vtable DATAobjc_ehtype_vtable是 C++ 异常与 Objective-C 异常互操作的关键:Apple runtime 通过向 C++ 异常处理器注册这套异常类型表(exception type table),让 C++ 侧的catch也能捕获 Objective-C 异常(以及反之)。这个符号的存在提醒我们:objc-exception.mm并不只是处理@try/@catch,它还深度介入 C++ 异常表——而这恰恰是 Clang CodeGen 中CGException.cpp所负责的领域。两条线索交汇,解释了为什么是"异常处理 CodeGen 模块"对这个文件断言失败。
断言机理:catch 子句类型表中的兜底分支
结合 Clang 的通用实现(deps/clang子模块在本镜像中为空,以下机理说明基于 Clang 的公开设计,属于对该断言的合理技术解释,不是仓库内可直接验证的代码):
- CodeGen 处理
try块时先生成 landing pad,并为每个 catch 子句记录"catch 类型 → 异常表条目"的映射; - 当某个 catch 子句的类型需要查找其对应的 TypeInfo / 异常表描述符时,代码按类型逐级查找;
- 若一路查不到,便落入
catch-all兜底分支,该分支要求typeValue已经非空,否则触发typeValue && "fell into catch-all case!"断言。
因此i4断言的实质是:在补丁 Clang 处理objc-exception.mm时,某个 catch 子句的类型在异常类型表中找不到对应条目。对 ipasim 这样深度修改 Clang(新增 runtime、改动符号导出与节布局)的项目,补丁与异常 CodeGen 假设之间出现失配,是这类断言的典型来源;本文只能在此指出排查方向,不能替代对 fork 补丁的逐行审查。
调试与复现路径
仓库提供的工具链支持以下实操步骤(所有命令都在 Dockerfile 描述的 Windows 容器内执行,入口为docker-compose run --name ipasim ipasim powershell,容器WORKDIR为c:/ipaSim/src):
- 对照原版 Clang:以
-DUSE_ORIG_CLANG=ON配置主项目(见 CMakeLists.txt 选项定义)。若原版 Clang 能编过objc-exception.mm而补丁 Clang 失败,即可确认问题出自deps/clang的补丁; - 使用 Debug 版 Clang:加
-DUSE_DEBUG_CLANG=ON切换为${DEBUG_CLANG_CMAKE_DIR}/bin/clang.exe,便于在CGException.cpp的断言处下断点,观察是哪个 catch 类型、来自objc-exception.mm的哪个上下文; - 快速迭代 Clang 本体:CMakeLists.txt 的 superbuild 目标 把 Clang 构建切成
tblgens-x86-Release(tablegen 工具)与clang-x86-Release(ninja clang lld)两步,且 build.ps1 支持BUILD_TABLEGENS_ONLY=1环境变量只编 tablegen——增量修改 Clang 补丁时无需全量重编; - 定位编译命令行:主项目构建开启了
-DCMAKE_EXPORT_COMPILE_COMMANDS=On(scripts/ipaSim-x86-Debug.cmake),可用compile_commands.json取出objc-exception.mm的精确编译参数,脱离 CMake 手动复现。
相关构建问题索引与仓库资源
i4并非孤例,docs/issues/ 按同样的"现象 + 解决/绕过方式"格式记录了整条构建链上的障碍,与本文最相关的几篇:
- i1:构建 lld 时
windows_version_resource.rc.res无法生成(Docker 环境下 RC 资源处理问题); - i2:如何让 CMake 项目以"刚编译出的补丁 Clang"为编译器(本文 superbuild 结构的动机);
- i3:Docker 挂载目录的 I/O 错误导致
lld-link无法写出文件,以及backup.ps1/restore.ps1的增量构建方案。
继续深入本文主题,可直接查看的仓库资源:
| 资源 | 相对路径 | 作用 |
|---|---|---|
| 问题原文 | docs/issues/4.md | i4现象记录 |
| 子模块声明 | .gitmodules | deps/clang、deps/objc4等 fork 与分支 |
| superbuild 配置 | CMakeLists.txt | 两阶段编译器切换、Ninja 目标串联 |
| Clang 构建脚本 | scripts/clang-x86-Debug.cmake | 用原版clang-cl编译补丁 Clang |
| 编译器变量 | scripts/CommonVariables.cmake | CLANG_EXE定义与OBJECT_DEPENDS强制重编 |
| runtime 构建 | src/objc/CMakeLists.txt | objc-exception.mm的宏、-fobjc-runtime、DEF 链接 |
| runtime 移植说明 | src/objc/README.md | 预处理器定义与代理头文件机制 |
| 符号导出 | src/objc/libobjc.def | objc_ehtype_vtable等导出 |
| issues 体系 | docs/issues/README.md | LLVM 补丁动机(.mhdr、.fixbind等)汇总 |
小结
i4用一条断言浓缩了 ipasim 构建体系中最脆弱的一环:补丁 Clang 的 CodeGen 与移植版 Objective-C runtime 的异常代码之间存在假设失配。围绕它的仓库证据链相当完整——superbuild 决定了"补丁 Clang 何时被使用"(CMakeLists.txt + scripts/),objc目标的宏与-fobjc-runtime=macosx-10.13.0决定了 Clang 会走哪条异常 CodeGen 路径(src/objc/CMakeLists.txt),而libobjc.def中objc_ehtype_vtable的导出则标明了objc-exception.mm与 C++ 异常机制的交汇点(src/objc/libobjc.def)。对于后续维护者,USE_ORIG_CLANG对照构建 +compile_commands.json手动复现,是定位这类 CodeGen 回归最直接的两把工具。
- 虚拟化
- 系统编程
【免费下载链接】ipasim
iOS emulator for Windows
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考