Bun

bun.report 是 Bun 的全新崩溃报告器


Chloe Caruso · 2024 年 4 月 26 日

在撰写本文时,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 将会失败。

对于 Linux 和 MacOS,第一个模块指的是主应用程序二进制文件。在 Windows 上,您可以将模块的名称与 peb.ProcessParameters.ImagePathName 进行比较,以确定它是否是主二进制文件。

通常,一旦模块和相对地址被解析,应用程序将立即打开调试符号并解构函数名。为了避免下载和解析调试符号的成本,让我们将解构卸载到服务器。该服务器可以缓存所有的调试符号,并在几秒钟内解构堆栈跟踪。同时,它可以作为打开新的 GitHub issue 的链接。

bun.report 的 URL 结构

让我们再次查看这个 URL,并分解它是如何编码的

  • 平台:一个字符表示平台。w 代表 x86_64 Windows,M 代表 aarch64 macOS,以及 等等
  • 子命令:一个字符表示 子命令,例如 bun testbun installbun 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 的未来。在此申请