一、问题现场

将浏览器内核从 Chromium 132 升级到 148,执行全量编译:

bash

autoninja -C out/Debug chrome

编译顺利通过,但最终卡死在链接阶段:

text

lld-link.exe FAILED
unresolved external symbol
google::protobuf::internal::RepeatedPtrFieldBase::AddInternal(...)

LNK2019: unresolved external symbol
目标: chrome.dll
相关依赖:
  third_party_protobuf_protobuf_lite.dll
  mojo_base_protobuf_support.dll

第一反应:增量编译污染。

text

del out\Debug\obj\third_party\protobuf\protobuf_lite\*.obj
del out\Debug\third_party_protobuf_protobuf_lite.dll
del out\Debug\third_party_protobuf_protobuf_lite.dll.lib

重新全量编译,问题依旧。不是缓存问题。

二、定位过程

2.1 确认依赖链

bash

findstr /i protobuf out\Debug\chrome.dll.rsp

输出:

text

third_party_protobuf_protobuf_lite.dll.lib
mojo_base_protobuf_support.dll.lib
mojo_base_mojom_protobuf_support.dll.lib

依赖链确认存在:

text

chrome.dll → mojo → protobuf

2.2 检查符号导出

bash

dumpbin /exports out\Debug\third_party_protobuf_protobuf_lite.dll \
  | findstr AddInternal

结果:AddInternal 符号存在于 DLL 导出表中。

bash

dumpbin /exports out\Debug\mojo_base_protobuf_support.dll

结果:没有导出该符号。

结论:符号存在但引用方式变了。

2.3 检查编译宏

bash

gn desc out/Debug //mojo/public/cpp/base:protobuf_support
# 发现 public_configs 中有 third_party/protobuf:protobuf_use_dlls
# 目标开启了 PROTOBUF_USE_DLLS

gn desc out/Debug //third_party/protobuf:google_protobuf_timestamp_lite_proto defines
# 输出: PROTOBUF_USE_DLLS

gn desc out/Debug //third_party/protobuf:protobuf_full defines
# 输出: (无 PROTOBUF_USE_DLLS)

关键发现:

  • protobuf_lite → 走 DLL(开启 PROTOBUF_USE_DLLS

  • protobuf_full → 走静态链接(未开启)

ABI 不一致的嫌疑出现了。

三、根因分析

3.1 Chromium 132 vs 148:Protobuf 导出策略的根本变化

Chromium 132 的实现方式

cpp

// repeated_ptr_field.h
#if !defined(PROTOBUF_USE_DLLS)
inline void* RepeatedPtrFieldBase::AddInternal(...) {
  // 实现在头文件中
}
#endif

text

架构:
  每个调用方 .obj
    ↓
  各自展开 inline 代码
    ↓
  没有 DLL 边界
  • 静态链接时:实现放在头文件,每个 .obj 各自展开

  • DLL 模式时:实现放在 .cc,由 DLL 统一导出

Chromium 148 的变化

cpp

// repeated_ptr_field.cc
#if defined(PROTOBUF_USE_DLLS)
void* RepeatedPtrFieldBase::AddInternal(...) {
  // 实现移到 .cc 文件
}
#endif

// repeated_ptr_field.h
// 只有声明,没有 inline 实现

text

架构:
  所有调用方 .obj
    ↓
  生成 dllimport 调用
    ↓
  统一走 DLL 导出符号

核心变化:

  • 132:inline 在头文件展开,调用方各自生成代码

  • 148:实现收归 .cc,调用方统一走 DLL 导入

3.2 为什么会 LNK2019

问题的根源在于 编译单元之间的 ABI 不一致

编译目标 PROTOBUF_USE_DLLS 对 AddInternal 的理解
protobuf_lite.dll 开启 我负责导出这个符号
protobuf_full 未开启 这是 inline,我展开就好
mojo_base_protobuf_support.dll 开启 我要从 DLL 导入

当 chrome.dll 链接时:

text

chrome.dll
  ↓ 引用
mojo_base_protobuf_support.dll (认为应该从 protobuf_lite.dll 导入)
  ↓ 期望
protobuf_lite.dll (导出 AddInternal)
  ↓ 但
protobuf_full (inline 展开,不导出)
  ↓ 结果
链接器找不到符号 → LNK2019

这是经典的 ODR(One Definition Rule)违反 + ABI 不匹配。

3.3 架构对比图

132 架构(正常工作)

text

┌─────────────────────────────────────────────┐
│              chrome.dll                      │
│  ┌───────────────────────────────────────┐  │
│  │  caller.obj                           │  │
│  │  AddInternal() → inline 展开到本地    │  │
│  └───────────────────────────────────────┘  │
│                                              │
│  ┌───────────────────────────────────────┐  │
│  │  another_caller.obj                   │  │
│  │  AddInternal() → inline 展开到本地    │  │
│  └───────────────────────────────────────┘  │
│                                              │
│  每个 .obj 各自有一份 AddInternal 的代码     │
│  链接时不存在跨目标符号引用                  │
└─────────────────────────────────────────────┘
148 架构(链接失败)

text

┌─────────────────────────────────────────────────┐
│                  chrome.dll                      │
│                                                  │
│  ┌───────────────────────────────────────────┐  │
│  │  caller.obj (PROTOBUF_USE_DLLS 开启)      │  │
│  │  call __imp_AddInternal  ────────────┐    │  │
│  └───────────────────────────────────────│───┘  │
│                                          │      │
│  ┌───────────────────────────────────────│───┐  │
│  │  another.obj (PROTOBUF_USE_DLLS 未开启)│   │  │
│  │  AddInternal() → 期望 inline 展开      │   │  │
│  └────────────────────────────────────────┘──┘  │
│                                          │      │
│                  期望导入                 │      │
│                          ┌───────────────┘      │
│                          ▼                      │
│               ┌─────────────────────┐           │
│               │  protobuf_lite.dll  │           │
│               │  导出 AddInternal   │           │
│               └─────────────────────┘           │
│                          ↑                      │
│               protobuf_full (静态链接)           │
│               不导出 AddInternal                │
│               链接器:❌ 找不到符号              │
└─────────────────────────────────────────────────┘

3.4 为什么全量编译仍然失败

最容易误判的地方:已经全量编译了,为什么还失败?

  • 编译正确:每个 .obj 的生成都是正确的

  • 生成正确:DLL 和 LIB 的生成也都是正确的

  • ABI 错误:但各个目标对"如何找到 AddInternal"的理解不一致

重新编译无法修复 ABI 层面的不一致,因为生成的就是"错误接口"的产物,不是旧产物的问题。

四、深入理解:为什么删 inline 保留 cc 能修,反过来不行

4.1 方案 A(失败):保留 inline,删除 cc 实现

cpp

// repeated_ptr_field.h
inline void* RepeatedPtrFieldBase::AddInternal(...) {
  // 实现保留在头文件
}

// repeated_ptr_field.cc
// 删除实现

为什么不行?三层原因:

第一层:inline 不一定生成符号

编译器规则:inline 函数只有在被当前编译单元实际使用时才会生成代码。如果某个 .obj 调用了 AddInternal,但实际实例化发生在别的 DLL 中,该 .obj 本身未必展开 inline,导致链接时找不到定义。

第二层:dllimport 禁止 inline 展开(关键)

这是最核心的原因。当目标开启了 PROTOBUF_USE_DLLS 时,宏展开为:

cpp

class __declspec(dllimport) RepeatedPtrFieldBase {
  // ...
};

编译器看到 obj.AddInternal() 时:

不会展开 inline,而是生成:

asm

call __imp_AddInternal  ; 导入符号

此时编译器不再相信 inline,强制生成 DLL 导入调用。如果 DLL 没有导出该符号,必然 LNK2019。

text

失败路径:

chrome.obj
  call __imp_AddInternal
       ↓
protobuf.dll
  符号不存在 → LNK2019
第三层:ODR 违规

更隐蔽的是,如果存在混用的情况:

目标 PROTOBUF_USE_DLLS AddInternal 的理解
A 开启 dllimportsizeof 可能不同
B 未开启 inline,本地展开

这违反 C++ 的 ODR(单一定义规则),可能导致:

  • 链接失败(最好情况)

  • 链接成功但运行时崩溃(sizeof 不一致导致内存布局错乱)

4.2 方案 B(成功):删除 inline,保留 cc 实现

cpp

// repeated_ptr_field.h
void* RepeatedPtrFieldBase::AddInternal(...);  // 只声明

// repeated_ptr_field.cc
void* RepeatedPtrFieldBase::AddInternal(...) {
  // 唯一实现
}

为什么能修?

text

成功路径:

chrome.obj
  call __imp_AddInternal
       ↓
protobuf_lite.dll
  导出 AddInternal ✓
       ↓
  找到符号 → 链接成功

所有目标统一走 DLL 导入,只有一份实现,没有 ABI 分歧。

本质上是在做一个架构迁移:

text

Header-only ABI (132)
  → Shared Library ABI (148)

五、为什么 Chromium 要这样改

Chromium 148 这个改动不是随意的,背后是明确的架构目标:

5.1 组件化(Componentization)

text

旧模型:1000 个目标 → 可能 1000 份 AddInternal 的 inline 展开
新模型:1000 个目标 → 1 份 AddInternal 的 DLL 导出

5.2 减少二进制体积

  • 消除重复代码:每个 inline 展开都是一份代码拷贝

  • 缩小 chrome.dll:符号统一走 DLL,减少主 DLL 体积

5.3 优化启动性能

  • 减少 page fault:DLL 符号可以在多个进程间共享物理内存页

  • 更好的缓存局部性:统一实现减少指令缓存(I-Cache)的碎片化

5.4 支持跨组件 Mojo 通信

Mojo 需要在不同 DLL 间传递 protobuf 消息,要求所有组件对 protobuf 的 ABI 理解一致。Header-only 模式容易产生"同一个类在不同 DLL 中有不同布局"的问题。

六、升级 Chromium 如何避免这类坑

6.1 检查清单

遇到"符号存在但链接失败"时,按以下顺序排查:

步骤 命令 目的
1 gn desc <target> defines 检查关键宏是否一致
2 dumpbin /exports <dll> 确认符号是否导出
3 dumpbin /imports <obj> 确认符号是否被导入
4 findstr <symbol> <target>.dll.rsp 确认依赖链
5 gn desc <target> public_configs 检查配置传递

6.2 关键信号

以下信号意味着可能是 ABI 问题而非缓存问题:

  • ✅ 全量编译后仍然失败

  • ✅ 符号在 DLL 中存在但链接失败

  • ✅ 同一个符号在不同目标中有不同的 __declspec 属性

  • ✅ 编译通过,只有链接失败

6.3 不要第一反应删 out/

删 out/ 对 ABI 问题无效。 问题不在产物是否陈旧,而在产物的接口约定本身就是错的。

七、总结

问题本质

text

132: Header-only inline
       ↓
148: Component DLL export
       ↓
    ABI 变更
       ↓
    LNK2019

修复核心

统一 ABI 模型:所有目标对 AddInternal 的来源达成一致——都走 DLL 导入。

架构趋势

Chromium 的演进方向已经非常明确:

text

少 inline → 多 component → 多 DLL → 多 Mojo

这意味着未来升级还会遇到更多类似的 ABI 边界问题。掌握这类问题的排查方法,比记住具体修复更重要。

一句话总结

删除 cc 保留 inline 不行,不是因为 inline 代码不对,而是开启 PROTOBUF_USE_DLLS 后,调用方已经不再相信 inline,强制生成 dllimport 调用;没有 cc 导出,就一定 LNK2019。


本文基于真实的浏览器内核升级工程经验编写,所有代码示例已脱敏处理,仅保留架构相关的核心逻辑。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐