开发模式启动后 bus error 退出
主线位置:启动主线的最后一步,事件循环第一次发出
Ready。App::run先跑setup(建配置里的窗口、调用户的 setup 钩子),再把事件交给on_event_loop_event转成RunEvent::Ready(见 Builder、App 与 AppHandle)。开发模式的 macOS 构建在这次转换里多做一件事:把应用图标设成 Dock 图标。进程死在这一步。
flowchart TB
ready["第一次 Ready"] --> setupn["setup 建窗口 跑钩子"]
setupn --> conv["on_event_loop_event"]
conv --> devq{"dev 且 macOS"}
devq -->|否| cbk["用户回调收到 Ready"]
devq -->|是| nsimg["NSImage initWithData"]
nsimg --> imageio["ImageIO 解码 PNG"]
imageio --> libq{"libPng 从哪来"}
libq -->|系统| cbk
libq -->|Homebrew| bus["跳到 0xbad4007"]
本页实验:M2 Max、macOS 27.0.1、rustc 1.99.0,tauri 2.12.5,debug 构建。crate pit1-dev 由模板 base 复制,没有 devUrl;main 在 setup 钩子和 RunEvent::Ready 里各打一行,页面加载后通过 report 命令报回自己的地址。崩溃的运行一共做了两次(下文都标出),其余都是正常退出的运行。
现象
fn main() {
// 15 秒看门狗,略
println!("dev mode: {}", tauri::is_dev());
let app = tauri::Builder::default()
.setup(|_app| {
println!("setup hook done");
Ok(())
})
.invoke_handler(tauri::generate_handler![report])
.build(tauri::generate_context!())
.expect("error while building tauri application");
app.run(|_app, event| {
if let tauri::RunEvent::Ready = event {
println!("RunEvent::Ready");
}
});
}
不带 --features tauri/custom-protocol 编译(开发模式),在终端里直接运行(崩溃运行一):
dev mode: true
setup hook done
exit=138
同一份代码带 --features tauri/custom-protocol 编译(发布模式):
dev mode: false
setup hook done
RunEvent::Ready
REPORT page loaded from tauri://localhost
exit=0
退出码 138 是 128 + 10,信号 10 是 SIGBUS。Rust 没有机会打印任何东西:这不是 panic,是 CPU 取指令时出错。setup 钩子跑完了,RunEvent::Ready 那一行没出来,所以出事的地方在两者之间。
macOS 为这次崩溃写了一份报告(~/Library/Logs/DiagnosticReports/pit1-dev-dev-*.ips,JSON)。用一个小脚本取出异常、崩溃线程的前 22 帧、两个寄存器和进程里加载的图像库(符号名截到 64 个字符):
EXC_BAD_ACCESS (SIGBUS) EXC_ARM_DA_ALIGN at 0x000000000bad4007
vmRegionInfo: 0xbad4007 is not in any region. Bytes before following region: 4175364089
thread 0 (com.apple.main-thread):
0 None 0xbad4007
1 ImageIO PNGReadPlugin::InitializePluginData(IIOImageReadSession*, IIODic
2 ImageIO IIOReadPlugin::callInitialize(bool&)
3 ImageIO IIO_Reader::initImageAtOffset(CGImagePlugin*, unsigned long, uns
...
7 ImageIO CGImageSourceCopyPropertiesAtIndexEx
8 AppKit ImageSourceOptionsForCGImageSource_index_
9 AppKit +[NSBitmapImageRep _imagesWithData:hfsFileType:extension:zone:ex
10 AppKit +[NSBitmapImageRep imageRepsWithData:]
11 AppKit -[NSImage initWithData:]
...
16 pit1-dev-dev _RNvMs7_NtNtCsamhZZBAZQHX_13objc2_app_kit9generated9___NSImageNt
17 pit1-dev-dev _RINvNtCs5EVsQ9ed0VJ_5tauri3app19on_event_loop_eventINtCsFqSSV9T app.rs:2744
18 pit1-dev-dev _RNCINvMsf_NtCs5EVsQ9ed0VJ_5tauri3appNtB8_3App28make_run_event_l app.rs:1446
19 pit1-dev-dev _RINvCsFqSSV9TGOY_17tauri_runtime_wry17handle_event_loopNtCs5EVs lib.rs:4120
...
x2 = handle_error(png_struct_def*, char const*) x3 = handle_warning(png_struct_def*, char const*) x16 = 0xbad4007
loaded png/tiff/gif/jpeg images:
/opt/homebrew/*/libpng16.16.dylib
/opt/homebrew/*/libtiff.6.dylib
/opt/homebrew/*/libgif.7.2.0.dylib
/opt/homebrew/*/libjpeg.8.3.2.dylib
从下往上读:tauri 的 on_event_loop_event(app.rs 第 2744 行)调 NSImage initWithData:,AppKit 交给 ImageIO 读 PNG 的属性,ImageIO 的 PNG 插件在初始化时跳到了地址 0xbad4007。这个地址不在任何映射区域里,也不是 4 字节对齐的,ARM 上取指令报对齐错误,内核把它作为 SIGBUS 送给进程。跳转前 x2、x3 里放的是两个 libpng 错误回调,说明这是在调 libpng 的建表函数;而进程里的 libpng 来自 /opt/homebrew,不是系统的。编辑在模板上最早观察到这个现象时留下的六份 base-*.ips,异常和前几帧与这一份完全相同。
出处
开发模式下,generate_context! 在 macOS 上把图标文件原样嵌进二进制当 app_icon(context_codegen 第 228 行起:先找 bundle.icon 里的 .icns,没有就用 PNG,默认 icons/icon.png);发布模式下这一项是 None(对照表见 generate_context!)。第一次 Ready 时,on_event_loop_event 把它交给 AppKit:
RuntimeRunEvent::Ready => {
// set the app icon in development
#[cfg(all(dev, target_os = "macos"))]
{
use objc2::{AllocAnyThread, MainThreadMarker};
use objc2_app_kit::{NSApplication, NSImage};
use objc2_foundation::NSData;
if let Some(icon) = app_handle.manager.app_icon.clone() {
// TODO: Enable this check.
let mtm = unsafe { MainThreadMarker::new_unchecked() };
let app = NSApplication::sharedApplication(mtm);
let data = NSData::with_bytes(&icon);
let app_icon = NSImage::initWithData(NSImage::alloc(), &data).expect("creating icon");
unsafe { app.setApplicationIconImage(Some(&app_icon)) };
}
}
RunEvent::Ready
}
在 tauri 的源码里,NSImage 只出现在这一处;窗口图标在编译期就由 png crate 解成 RGBA(CachedIcon::new_png),运行时不再解码。所以同一个环境下,发布模式能跑,开发模式在这里崩。
这段代码由 #4385(「feat(core): set macOS app icon in development」)加入,首次发布于 tauri 1.0.1,CHANGELOG 原文只有一句:「Set the application icon in development mode on macOS.」现在的写法来自 2.0.0-rc.13 的 #10924(「feat: Migrate to objc2」,CHANGELOG:「Use objc2 internally and in examples, leading to better memory safety.」),MainThreadMarker::new_unchecked 上那句「TODO: Enable this check.」是那次留下的。
那 Homebrew 的 libpng 是怎么进来的。ImageIO 自带几个图像库,都在框架的 Resources 目录里(dyld_info -dependents /System/Library/Frameworks/ImageIO.framework/ImageIO,节选):
/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libPng.dylib
/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libTIFF.dylib
/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libGIF.dylib
/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libJP2.dylib
/usr/lib/libate.dylib
/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libJPEG.dylib
实验所在的 shell 从 shell 启动文件继承了 DYLD_LIBRARY_PATH=/opt/homebrew/lib:/usr/local/lib。man dyld 的说法是「For each dylib that a program uses, the dynamic linker looks for its leaf name in each directory in DYLD_LIBRARY_PATH.」:每个依赖先按路径的最后一段文件名在这些目录里找,找到就用,在默认位置之前。APFS 默认不区分大小写,/opt/homebrew/lib/libPng.dylib 能打开,打开的是 Homebrew 的 libpng.dylib(指向 libpng 1.6.59);libTIFF、libGIF、libJPEG 同理,这和崩溃报告里加载的四个库一一对应(liblzma、libzstd 是 Homebrew 的 libtiff 自己的依赖)。libJP2.dylib、libRadiance.dylib 在 Homebrew 目录里没有同名文件,仍然来自系统。
系统的 libPng.dylib 是 ImageIO 专用的一份,导出的名字带 cg_ 前缀;ImageIO 要的正是这些名字,Homebrew 的 libpng 里没有:
$ dyld_info -imports /System/Library/Frameworks/ImageIO.framework/Versions/A/ImageIO | grep create_read_struct
__cg_png_create_read_struct (from libPng)
$ dyld_info -exports /System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libPng.dylib | grep create_read
0x000007C8 __cg_png_create_read_struct
$ nm -gU /opt/homebrew/lib/libpng.dylib | grep create_read
0000000000007eec T _png_create_read_struct
0000000000007efc T _png_create_read_struct_2
找不到的符号没有让 dyld 在启动时报错,调用时跳到了 0xbad4007:寄存器 x16(arm64 上经桩函数调用外部符号时放目标地址的寄存器)就是这个值。这个占位地址由 dyld 的哪段逻辑填入,本页没有查到能引用的出处。
排除过的方向:
- 不是因为没有
devUrl。开发模式下get_app_url在devUrl为空时退回tauri://localhost,generate_context!也照常内嵌frontendDist。下一节去掉环境变量之后,同一个开发模式二进制加载的就是tauri://localhost。 - 不是图标文件的问题。模板的图标是 32×32 的 RGBA PNG,三个块的 CRC 都对、IDAT 解压后正好 32 × (1 + 128) 字节。换成 tauri-cli 应用模板里的 512×512 图标(
crates/tauri-cli/templates/app/src-tauri/icons/icon.png)重新编开发模式再跑(崩溃运行二,cratepit1-devicon),崩溃报告的异常和前三帧完全相同。
tauri dev 编出来的也是开发模式(它不打开 custom-protocol),走的是同一段代码;它会继承启动它的 shell 的环境变量。本页没有用 CLI 实测。
修法
让这个进程看不到 DYLD_LIBRARY_PATH。只对这一次运行去掉它:
$ (unset DYLD_LIBRARY_PATH; DYLD_PRINT_LIBRARIES=1 ./pit1-dev-dev 2>&1 | grep -E "[Pp]ng|^REPORT")
dyld[72464]: <2D9734E3-0991-3823-875A-A7A1ABDC63B4> /System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libPng.dylib
REPORT page loaded from tauri://localhost
同一个开发模式二进制,加载的是系统的 libPng.dylib,页面从 tauri://localhost 加载,正常退出。完整输出(不加 DYLD_PRINT_LIBRARIES):
dev mode: true
setup hook done
RunEvent::Ready
REPORT page loaded from tauri://localhost
exit=0
长期的修法是把 export DYLD_LIBRARY_PATH=... 从 shell 的启动文件里删掉;某个程序确实需要它时,只在启动那个程序时设置。
带 --features tauri/custom-protocol 编译能让这段图标代码不参与编译,但不是修法:发布模式的进程照样加载了 Homebrew 的 libpng,只是没有代码去调用它。
$ DYLD_PRINT_LIBRARIES=1 ./pit1-dev-prod 2>&1 | grep -E "[Pp]ng|^REPORT"
dyld[72457]: <4CC26026-4CD3-3266-8F77-BA6679FA1D42> /opt/homebrew/Cellar/libpng/1.6.59/lib/libpng16.16.dylib
REPORT page loaded from tauri://localhost
怎么确认没中招
用 shell 自己的
echo $DYLD_LIBRARY_PATH看,不要用env | grep DYLD。/usr/bin/env是受系统完整性保护(SIP)的程序,系统启动它时会去掉所有DYLD_*变量,所以它什么也看不到:$ echo $DYLD_LIBRARY_PATH /opt/homebrew/lib:/usr/local/lib $ env | grep DYLD; echo "grep exit=$?" grep exit=1DYLD_PRINT_LIBRARIES=1 <应用> 2>&1 | grep -E '[Pp]ng'应该只看到/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libPng.dylib。已经崩了的话,打开
~/Library/Logs/DiagnosticReports/<二进制名>-<时间>.ips:EXC_ARM_DA_ALIGN at 0x000000000bad4007、第 1 帧是PNGReadPlugin::InitializePluginData,就是这个问题。
小结
- 开发模式(没有
custom-protocol)的 macOS 构建,第一次Ready时在on_event_loop_event里用NSImage initWithData:设 Dock 图标;这是进程里唯一一处 ImageIO 解码。 DYLD_LIBRARY_PATH含/opt/homebrew/lib时,dyld 按文件名在那里找到libPng.dylib(大小写不敏感),ImageIO 拿到 Homebrew 的 libpng,跳到0xbad4007,进程以SIGBUS退出,退出码 138,没有输出。- 和
devUrl、图标文件本身无关。去掉这个环境变量,开发模式照常从tauri://localhost加载页面。 - 检查环境变量用
echo,不用env:SIP 会把DYLD_*从受保护程序的环境里去掉。