在撰写本文时,Bun 在 GitHub 上有超过 2,600 个未解决的 issue。我们很高兴拥有用户和反馈,但有些 issue 真的很难让我们重现和调试。
应用和 SaaS 产品可以使用像 Sentry 这样出色的崩溃报告服务,但对于像 Bun 这样的 CLI 工具,上传核心转储在隐私、性能和可执行文件大小方面需要权衡,这更难证明是合理的。
这就是为什么在 Bun v1.1.5 中,我为 Zig 和 C++ 崩溃报告编写了一种紧凑的新格式。崩溃报告可以放入约 150 字节的 URL 中,其中不包含任何个人信息。

为什么不直接使用操作系统崩溃报告器?
某些操作系统(如 macOS)具有内置的崩溃报告器,但这通常意味着应用程序需要附带调试符号。对于 Linux,这些调试符号约为 30 MB,macOS 约为 9 MB。
du -h ./bun
60M ./bun
llvm-strip bun
du -h ./bun
51M ./bun
而在 Windows 上,.pdb
文件超过 250 MB
(gi bun.pdb).Length / 1mb
252.44921875
30 MB - 250 MB 对于添加到每个 Bun 安装包中来说,是巨大的臃肿。
但是如果没有调试符号,崩溃信息就非常有限。而且在混合使用 地址空间布局随机化 (Address space layout randomization) 的情况下,所有的函数地址都变得毫无用处。
uh-oh: reached unreachable code
bun will crash now 😭😭😭
----- bun meta -----
Bun v1.1.0 (5903a614) Windows x64
AutoCommand:
Builtins: "bun:main"
Elapsed: 27ms | User: 0ms | Sys: 0ms
RSS: 91.69MB | Peak: 91.69MB | Commit: 0.14GB | Faults: 22579
----- bun meta -----
Search GitHub issues https://bun.net.cn/issues or join in #windows channel in https://bun.net.cn/discord
thread 104348 panic: reached unreachable code
???:?:?: 0x7ff62a629f17 in ??? (bun.exe)
???:?:?: 0x7ff62a907a83 in ??? (bun.exe)
???:?:?: 0x7ff62a61f392 in ??? (bun.exe)
???:?:?: 0x7ff62ade7ff1 in ??? (bun.exe)
???:?:?: 0x7ff62ab2193c in ??? (bun.exe)
???:?:?: 0x7ff62ab21166 in ??? (bun.exe)
???:?:?: 0x7ff62cd3ddeb in ??? (bun.exe)
???:?:?: 0x7ff62b7a4bb6 in ??? (bun.exe)
???:?:?: 0x7ff62b7a33bd in ??? (bun.exe)
???:?:?: 0x1bab9ca115d in ??? (???)
???:?:?: 0x1bab9ca111f in ??? (???)
全新的崩溃报告器
在 Bun v1.1.5 中,当发生崩溃或 panic 时,Bun 会打印类似这样的消息
Bun v1.1.5 (0989f1a) Windows x64
Args: "C:\Users\chloe\.bun\bin\bun.exe", ".\crash.js"
Builtins: "bun:main"
Elapsed: 40ms | User: 15ms | Sys: 15ms
RSS: 92.80MB | Peak: 92.80MB | Commit: 0.14GB | Faults: 22857
panic(main thread): Internal assertion failure
oh no: Bun has crashed. This indicates a bug in Bun, not your code.
To send a redacted crash report to Bun's team,
please file a GitHub issue using the link below:
https://bun.report/1.1.5/wa10989f1aAAg6xyL+rqoIwzn0F+oqC0v5R+52pGkr6Om7h+Oy3voK+9qoKA0eNrzzCtJLcpLzFFILC5OLSrJzM9TSEvMzCktSgUAiSkKPg
这个 bun.report
链接在点击后,会重定向打开一个预先填写的 GitHub issue 表单,其中重新映射的堆栈跟踪编码在 URL 中。
使地址变得有用
函数地址是指向内存中应用程序代码加载位置的指针,其中包含出于安全原因的随机偏移量。这意味着如果我们尝试解构这些地址,我们将一无所获。
llvm-symbolizer --exe ./bun.pdb 0x7ff62a629f17 0x7ff62a907a83
??
??:0:0
诀窍是简单地从二进制文件的基地址中减去该地址。
pub fn getRelativeAddress(address: usize) ?usize {
const module = getModuleFromAddress(address) orelse {
// Could not resolve address! This can be hit for some
// Windows internals, as well as JIT'd JavaScript.
return null;
};
return address - module.base_address;
}
实际上,这个函数 要复杂得多,因为每个平台都有不同的 API。
注意 – 我上面提到的“模块”仅适用于 Windows。在 macOS 上称为“镜像 (image)”,在 Linux 上称为“共享对象 (shared object)”。它们都指的是内存中加载的库或可执行文件的相同概念。为了简单起见,我将继续将它们称为“模块”。
- Windows:使用
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS
标志调用GetModuleHandleExW
。基地址是模块的指针。 - Linux:使用
dl_iterate_phdr
遍历加载的模块,一旦找到包含原始地址的模块,dl_phdr_info
结构上的.dlpi_addr
将作为基地址。 - macOS:函数
_dyld_image_count
和_dyld_get_image_header
可用于遍历模块,然后_dyld_get_image_vmaddr_slide
获取 ASLR 滑动值。- 结果地址仍然包含镜像的偏移量(对于 Bun 来说是
0x100000000
,可以使用image list
在 lldb 中列出这些偏移量)。为了编码更短的 URL,此偏移量被移除,但 必须在重新映射之前重新添加,否则llvm-symbolizer
将会失败。
- 结果地址仍然包含镜像的偏移量(对于 Bun 来说是
对于 Linux 和 MacOS,第一个模块指的是主应用程序二进制文件。在 Windows 上,您可以将模块的名称与 peb.ProcessParameters.ImagePathName
进行比较,以确定它是否是主二进制文件。
通常,一旦模块和相对地址被解析,应用程序将立即打开调试符号并解构函数名。为了避免下载和解析调试符号的成本,让我们将解构卸载到服务器。该服务器可以缓存所有的调试符号,并在几秒钟内解构堆栈跟踪。同时,它可以作为打开新的 GitHub issue 的链接。
bun.report 的 URL 结构

让我们再次查看这个 URL,并分解它是如何编码的
- 平台:一个字符表示平台。
w
代表 x86_64 Windows,M
代表 aarch64 macOS,以及 等等。 - 子命令:一个字符表示 子命令,例如
bun test
、bun install
或bun run
。 - Commit SHA:当前 Bun 版本的 commit SHA。这用于稍后获取调试符号。
- 功能标志:指示 Bun 崩溃前使用了哪些 API 和功能的标志。
- 堆栈跟踪地址:之前计算的地址。
- 崩溃类型:一个字符表示 崩溃类型。
- 崩溃消息:来自崩溃的消息,此消息的格式取决于类型。
注意 – URL 中的版本号实际上只是为了展示。这样一来,仅凭上述信息,人们就可以手动了解有关崩溃的很多信息。例如,您可以通过 w
平台标识符快速识别 Windows 崩溃。更不明显的是,您可以通过在字符串末尾附近查找 A2
来识别段错误。
VLQ 很有趣
为了保持 URL 足够短,堆栈跟踪地址使用 base64 可变长度数量 (Variable Length Quantity) 数字进行编码。这允许用更少的字符编码小数字,同时仍然能够编码大数字。这与 JavaScript 源代码映射中用于存储行号的技术相同。
转换过程如下所示。请注意 VLQ 如何将较小的地址编码为较小的数字。

服务器可以 将这些解码回相对地址,使用 commit 哈希和平台下载调试符号,并使用 llvm-symbolizer
来解构函数名。

现在,发生的情况变得显而易见:dirInfoCachedMaybeLog
中存在触发的断言,它来自 Windows 上的模块解析器代码的一部分。
什么是“功能 (Features)”
URL 还编码了一个 64 位整数,其中每一位对应于是否使用了 Bun 中的某个功能。这些标志提示了哪些 API 和系统可能导致了崩溃。例如,当自动加载任何 .env
文件时,会设置 dotenv
功能;当使用 fetch()
时,会设置 fetch
功能,等等。(完整列表)
Zig 的编译时元编程使得创建此位字段变得容易。我们已经有一个全局变量容器用于跟踪功能。
pub const Features = struct {
pub var bunfig: usize = 0;
pub var http_server: usize = 0;
pub var shell: usize = 0;
pub var spawn: usize = 0;
pub var macros: usize = 0;
// ... and so on
};
在各种 API 内部,我们将增加这些数字以标记功能的使用情况。
为了将这些编码为单个 u64
整数,我们可以使用 std.meta
遍历功能列表并创建一个列表。
pub const feature_list = brk: {
const decls = std.meta.declarations(Features);
var names: [decls.len][:0]const u8 = undefined;
var i = 0;
for (decls) |decl| {
if (@TypeOf(@field(Features, decl.name)) == usize) {
names[i] = decl.name;
i += 1;
}
}
const names_const = names[0..i].*;
break :brk names_const;
};
然后,可以动态派生创建一个打包的结构体,以便每个功能使用一位。此结构体的功能类似于整数,但交互方式类似于结构体。
// note: some fields omitted for brevity
pub const PackedFeatures = @Type(.{
.Struct = .{
.layout = .@"packed",
.backing_integer = u64,
.fields = brk: {
var fields: [64]StructField = undefined;
for (feature_list, 0..) |name, i| {
fields[i] = .{ .name = name, .type = bool };
}
fields[feature_list.len] = .{
.name = "__padding",
.type = @Type(.{ .Int = .{ .bits = 64 - feature_list.len } }),
};
break :brk fields[0..feature_list.len + 1];
},
},
});
最后,当 Bun 崩溃时,可以使用 inline for
非常简单地构建位字段,这是一种在编译时迭代某些内容,但在运行时执行内部内容的方法。
pub fn packedFeatures() PackedFeatures {
var bits = PackedFeatures{};
inline for (feature_list) |name| {
if (@field(Features, name) > 0) {
@field(bits, name) = true;
}
}
return bits;
}
现在,向原始结构体 Features
添加新功能将在崩溃报告器中正确处理它,而无需重复我们自己。
使用 C 或 Rust 的宏可以做到这类事情,但我感觉使用 Zig comptime
更加简单和可读。
这与核心转储相比如何?
核心转储包含更多信息,但它们体积庞大,需要调试符号才能有用,并且包含大量潜在的敏感或机密信息。
我们希望避免在报告中发送任何 JavaScript/TypeScript 源代码、环境变量或其他敏感信息的可能性。这就是为什么我们只发送 Zig/C++ 堆栈跟踪和一些其他细节。这种方法不是默认发送所有内容,而是仅发送我们(可能)需要诊断问题的内容。如果我们需要更多信息,我们可以要求用户提供,但这比之前我们拥有的那一堆未映射的地址要好得多。
演示
为了将所有内容放在一起,我编写了一个小型 Web 应用程序,让您可以测试崩溃报告器,该应用程序可在主页 bun.report 上找到。如果您在任何崩溃报告 URL 的末尾附加 /view
,也会最终到达这里。
Bun 正在旧金山招聘
如果您有兴趣参与此类项目,我们正在旧金山招聘工程师!我们正在寻找系统工程师来帮助构建 JavaScript 的未来。在此申请