静态初始化面
本页所有地址均适用于来自
libtpu-0.0.40-cp314wheel 的libtpu.so(build-id89edbbe81c5b328a958fe628a9f2207d)。其他构建会有所不同。这是.init_array的二进制取证普查;执行这些槽位的运行时遍历位于 lifecycle 页面。
摘要
C++ 共享对象会把工作前置到它的构造函数表中:每个带非平凡构造函数的命名空间作用域对象、每个 REGISTER_* 静态对象、每个 Meyers 单例、每个 flag 定义,都会编译成一个翻译单元初始化器,动态链接器会在第一个导出符号可被调用前调用它。在 libtpu.so 中,这张表,也就是 ELF .init_array,包含 2900 个函数指针槽位,总计 23200 字节。这不是插件管理的运行时数据结构;它是烘进文件里的静态清单,仅其大小就说明加载 libtpu 时,在 GetPjrtApi 返回可用 vtable 之前,必须支付一笔很大的、固定的、单线程构造函数成本。
本页是这份普查:表位于何处、包含多少槽位、这些槽位如何映射到具名的 _GLOBAL__sub_I_* 和 _GLOBAL__I_* 翻译单元初始化器,以及构造函数会构建哪些对象。它完全锚定于 readelf -d/readelf -S(表位置和大小)、提供每槽构造函数 addend 的 2900 个 R_X86_64_RELATIVE 重定位(磁盘上的槽位为零,见下方 GOTCHA),以及给 2900 个构造函数中的 2644 个命名的 IDA 符号表。工具链特征是纯 LLVM/clang+lld:符号表中完全没有经典 GCC 的 frame_dummy / __do_global_ctors_aux / register_tm_clones CRT 脚手架。
本页不叙述这些槽位触发的运行时顺序、遍历它们的 DT_INIT thunk,或匹配的 __cxa_atexit/.fini_array 拆卸。该控制流故事由 lifecycle/do-init-do-fini.md 和 lifecycle/elf-entry-and-init-proc.md 负责;本页通过交叉链接引用它们,而不重复内容。
对于构建可比插件的重新实现者,本页确立的契约是:
- 表形状 — 一个包含 2900 个 8 字节槽位的
INIT_ARRAY,通过R_X86_64_RELATIVE重定位实现位置无关(磁盘上填零),外加一个 2 槽PREINIT_ARRAY和一个 2 槽FINI_ARRAY。 - 构造函数群体 — 1885 个按源文件命名的
_GLOBAL__sub_I_*.cc初始化器 + 759 个带优先级标签的_GLOBAL__I_*初始化器 + 约 234 个 span 内匿名 thunk + 22 个低地址 CRT/IFUNC 初始化器。 - 工作分类 — 哪些静态对象类别(单例、描述符池、flags、dialect/op 注册表、codec 映射、RTTI)主导这张表,以及为什么计数是数千而不是数十。
.init_array VA / file off | 0x215f26f0 / 0x213f26f0 (section [30], INIT_ARRAY, align 8) |
.init_array size | 0x5aa0 = 23200 bytes → 2900 slots (23200 / 8) |
.preinit_array | 0x22048b30, 16 bytes → 2 slots (section [33]) |
.fini_array | 0x215f8190, 16 bytes → 2 slots (section [31]) |
DT_INIT thunk | 0xe635524 (.init, 0x17 bytes) — walker entry, see lifecycle |
DT_FINI thunk | 0xe63553c (.fini, 0x09 bytes) |
| 槽位填充机制 | 2900 × R_X86_64_RELATIVE (addend = constructor VA) |
| 具名 TU 初始化器 | 1885 _GLOBAL__sub_I_* + 759 _GLOBAL__I_* = 2644 |
| 构造函数代码 span | .text.startup 0x21217490 … 0x213818e4 = 0x16a454 (1,483,860 B / 1.41 MiB); named ctors run to 0x21380980, the 22 CRT/IFUNC ctors sit below at 0x21211240 and lower |
| CRT 风格 | LLVM/clang + lld — no GCC frame_dummy/__do_global_ctors_aux |
.init_array 表
位置和大小
readelf -d 和 readelf -S 完全一致。动态标签 DT_INIT_ARRAY 指向 0x215f26f0,DT_INIT_ARRAYSZ = 23200 字节;section header [30] .init_array 确认相同的 VA、文件偏移 0x213f26f0、大小 0x5aa0、类型 INIT_ARRAY、flags WAo(可写,加载时会被重定位)、8 字节对齐。槽位数为 23200 / 8 = 2900,是没有填充尾部的精确整数。
readelf -d:
(INIT_ARRAY) 0x215f26f0
(INIT_ARRAYSZ) 23200 (bytes)
(PREINIT_ARRAY) 0x22048b30
(PREINIT_ARRAYSZ)16 (bytes)
(FINI_ARRAY) 0x215f8190
(FINI_ARRAYSZ) 16 (bytes)
(INIT) 0xe635524
(FINI) 0xe63553c
readelf -S (relevant rows):
[30] .init_array INIT_ARRAY 215f26f0 213f26f0 005aa0 WAo align 8
[31] .fini_array FINI_ARRAY 215f8190 213f8190 000010 WA align 8
[33] .preinit_array PREINIT_ARRAY 22048b30 21e48b30 000010 WA align 8
```text
首先要注意的是这种不对称:**2900 个构造函数,但只有 2 个终结器**(`.fini_array` 为 16 字节)。静态初始化时构建的对象几乎没有哪个会注册 `.fini_array` 析构函数;单例和池的拆卸反而被推迟到 `__cxa_atexit` handler(或直接在进程退出时泄漏,这对长寿命插件是正常做法)。2 槽 `.preinit_array` 在主 `.init_array` 之前运行,并保留给最早期的 hook;其排序属于生命周期问题。
### 槽位是重定位,不是磁盘上的指针
朴素的 `objdump -s -j .init_array` 会显示全零:文件中每个 8 字节槽位都读作 `00000000 00000000`。构造函数地址**不是作为字面指针存储的**;每个槽位都是一个基址相对重定位的目标,动态链接器在加载时应用该重定位。
> **GOTCHA —** 不要读取磁盘上的 `.init_array` 字节并期待得到函数指针;你会得到 2900 个零。真正的构造函数地址位于重定位 *addend* 中。恰好有 **2900 个 `R_X86_64_RELATIVE` 重定位**的 `r_offset` 落在 `[0x215f26f0, 0x215f8190)` 内,每槽一个;每个重定位的 addend 是构造函数的加载相对 VA。槽位 0(最低 `r_offset`)的 addend 是 `0x21211240`(`__cpu_indicator_init`);所有槽位中最高的 addend *值* 是 `0x21380980`(它位于槽位 2,而不是最后一个槽位;addend 不按槽位顺序单调)。任何解析 section 字节而不是重定位表的普查都会数出 2900 个空构造函数。
这是 PIE/`-fPIC` 共享对象的预期编码:位置无关要求由加载器而不是链接器物化绝对构造函数地址,而 `R_X86_64_RELATIVE`(携带 addend、无符号)是最便宜的形式。2900 个重定位占该二进制总相对重定位加载工作中的一个非平凡部分。
| 锚点 | 值 |
|---|---|
| 指向 `.init_array` 的重定位 | 2900 × `R_X86_64_RELATIVE` |
| 槽位 0 addend | `0x21211240` |
| 范围内最高 addend | `0x21380980` |
| 磁盘上槽位字节 | 全零(加载时重定位) |
---
## 构造函数群体
### 按符号族普查
在 2900 个槽位中,IDA 的符号表给 2644 个不同构造函数命名。它们分为两个 clang 命名族,外加匿名余量和一个小型 CRT 前缀。
| 族 | 数量 | 形式 | 含义 |
|---|---|---|---|
| `_GLOBAL__sub_I_<file>.cc` | 1885 | 按源文件命名 | 默认优先级 TU 初始化器,每个带非平凡命名空间作用域构造函数的 `.cc` 一个 |
| `_GLOBAL__I_<priority>` | 759 | 按优先级命名 | 带显式 `init_priority` 属性的对象初始化器 |
| 匿名 span 内 ctor | ~234 | unnamed `sub_` | TU-init span 中 IDA 未解析成 `_GLOBAL__` 符号的构造函数代码 |
| 低地址 / CRT thunk | 22 | e.g. `__cpu_indicator_init` | TU-init span 以下的 C++ 前初始化器(IFUNC/CPU 检测、运行时 helper) |
算术闭合:1885 + 759 = 2644 个具名;具名构造函数代码 span 为 `[0x21217490, 0x21380980]`,2900 个重定位 addend 中有 **2878** 个落在该 span 内,留下 2878 − 2644 = **234 个匿名 ctor** 位于 span 内,另有 **22** 个 addend 位于它*下方*(`< 0x21217490`)。没有 addend 超出该 span。234 个匿名条目是真实构造函数:重定位指向同一区域中的可执行代码,只是因为 clang 为匿名命名空间对象生成它们,或因为符号被剥离,所以缺少 `_GLOBAL__` 符号。
> **Note:** 该二进制的符号表包含 **1885** 个不同的 `_GLOBAL__sub_I_*` 符号(全部唯一),以及一个独立的优先级初始化族,包含 **759** 个 `_GLOBAL__I_*` 符号。这些是去重后的符号表计数;对反编译树做 grep(它按 artifact 文件携带一条记录)会夸大它们,不应用于此处的槽位核算。`.init_array` 表大小是 2900 个槽位 / 23200 字节。
### 两个命名族
clang 会发出两种不同形状的 TU 初始化器,这里两者都出现了:
- **`_GLOBAL__sub_I_<file>.cc`** — 默认情况。每个需要*任何*非平凡命名空间作用域初始化的翻译单元,恰好获得一个这种初始化器,按其源文件命名。因为 libtpu 静态链接了许多共享源文件名的组件,同一个基名会带 `_N` 去重后缀反复出现:`metrics.cc` 出现 8 次(`metrics.cc`, `metrics.cc_0` … `metrics.cc_6`),`flags.cc`、`trace_codec_factory.cc`、`performance_counters.cc`、`kernel_firmware_factory.cc` 和 `hardware_attributes_factory.cc` 各出现 6 次。这些确实是来自不同静态链接库的不同 TU,只是碰巧使用了同一文件名。
- **`_GLOBAL__I_<priority>`** — `init_priority` 情况。当代码用 `__attribute__((init_priority(N)))` 标注一个全局对象时,clang 会按数字优先级而不是文件名给初始化器命名,以便链接器排序。分布很偏:759 个中有 **757** 个位于优先级 `000102`,另有 `000100` 和 `000101` 各一个。优先级 100 最早触发;`0x21380980` 处的构造函数是 `_GLOBAL__I_000100`(demangled: `` `global constructor keyed to'000100``),是整个二进制中唯一最高优先级的对象。
> **NOTE —** 符号名中的优先级*数字*(102、101、100)是 C++ `init_priority` 值,控制 TU/链接单元内的相对顺序;它不是 `.init_array` 中的槽位索引。完整 2900 槽表的运行时触发顺序属于生命周期问题,见 [`do-init-do-fini.md`](../lifecycle/do-init-do-fini.md)。
### 构造函数代码 span
全部 2644 个具名构造函数以及约 234 个 span 内匿名构造函数,都位于专用的 `.text.startup` section 中:一个连续代码区域 `[0x21217490, 0x213818e4)`,大小 `0x16a454` = **1,483,860 字节(1.41 MiB)**。2878 个带内 addend 从 section 基址 `0x21217490` 延伸到最高 addend `0x21380980`(解析为 `_GLOBAL__I_000100`);剩余字节直到 `0x213818e4` 保存非 `.init_array` 启动代码,例如 `__cxx_global_array_dtor` thunk。clang/lld 将 TU 初始化器聚集进该 section,这就是为什么 `.init_array` 槽位(位于 `0x215f26f0`)指向一个紧密带,而不是散布在 745 MB `.text` 各处。22 个低 addend(`< 0x21217490`)指向该带*之外*的更早代码:这些是 CRT/IFUNC 初始化器(`0x21211240` 处的 `__cpu_indicator_init`、upb 注册表构造函数 `0x201e7360`、BoringSSL 的上电自检、Rust 的 `ARGV_INIT_ARRAY` wrapper、`__do_init`、`setup_dl_debug_hook`),它们必须在任何 C++ TU 对象构造之前运行。
---
## 构造函数构建什么
2900 个槽位并不是 2900 种不同的工作;它们是半打反复出现的静态注册习惯用法的数千个实例,每一种都会由大型 C++/MLIR/XLA 代码库按 TU 发出。按 `_GLOBAL__sub_I_*` 符号中嵌入的源文件名分类(对 1885 个具名 TU 做关键词扫描)得到下方分解。计数是下界:单个 TU 往往执行几类注册,分类由文件名加反编译函数体旁证推断。
| 类别 | TU 关键词证据 | 构造函数做什么 | 约略 TU 数 |
|---|---|---|---|
| Op / kernel 注册 | `*_ops.cc` (156 TUs) | `REGISTER_OP` / `REGISTER_KERNEL` statics push op definitions into a global op registry | ≥156 |
| Factory / 静态注册表 | `*factory*` (79), `*registr*` (26) | Self-registering factories install a `make_*` callback into a name→factory map (driver, codec, kernel-firmware, snap-analyzer, device-scanner) | ≥79 |
| Flag 注册 | `flags.cc` / `*flags*` (50) | `ABSL_FLAG`/gflags definitions register a flag descriptor and default into the global flag table | ≥50 |
| Metrics / counters | `metrics.cc` (8), `performance_counters.cc` (6), `*metric*` (17) | Construct metric/counter descriptor singletons and register them | ≥17 |
| Dialect / pass / HLO 注册 | `*_registration.cc` (22), plus `mlir_*`/`*hlo*`/`*pass*` siblings | Register MLIR dialects/passes and HLO graph-optimization passes into pass-pipeline registries | ≥22 |
| Codec / static-map 注册 | `codec_metadata_*` (ghostlite/jellyfish/pufferfish/viperfish), `trace_codec_factory.cc` (×6) | Build per-codec static descriptor maps and register codec factories keyed by ASIC generation | ≥10 |
| Proto / descriptor-pool | `*proto*`, `*descriptor*` (7) | Register generated message descriptors into the protobuf descriptor pool; reflection plugins | ≥7 |
| Meyers 单例 / RTTI | pervasive (not filename-keyed) | Construct function-local-static and namespace-scope singletons; emit type-info for polymorphic types | — |
> **QUIRK —** codec 类别按 ASIC fish-codenames 标识:`ghostlite`、`jellyfish`、`pufferfish`、`viperfish` 各有一个 `codec_metadata_*.cc` TU 初始化器,`trace_codec_factory.cc` 出现六次(`.cc_0` … `.cc_4`)。重新实现者不能把这些折叠成一个 codec init:每一代都会在加载时注册自己的静态描述符映射,因此运行时依赖的按代 dispatch(见 [`per-gen-function-dispatcher.md`](per-gen-function-dispatcher.md))*完全由静态初始化填充*,而不是惰性填充。
### 为什么计数达到数千
两个结构事实推动了 2900 这个数字,二者对任何估算可比插件加载成本的人都很重要:
1. **静态自注册按 TU 和按符号发生。** op/kernel/flag/factory 习惯用法会为*每个注册实体*发出一个全局对象。一个包含 40 个 `REGISTER_OP` 宏的文件会产生 40 个命名空间作用域对象,全部在该文件的单个 `_GLOBAL__sub_I_` 中构造,而这样的 `*_ops.cc` TU 有 156 个。因此注册表(op 表、flag 表、codec 映射、pass pipeline、描述符池)在插件回答第一个查询之前已经完全物化。
2. **libtpu 静态链接了一棵很大的树。** 常见文件名的反复出现(`metrics.cc` ×8、`flags.cc` ×6)是许多独立库的指纹:XLA、MLIR/HLO 编译器栈、absl 运行时、按代的 `gxc`/`pxc`/`vxc` 驱动和 profiler 代码,全部折叠进一个 `.so`。每个库都贡献自己的 TU 初始化器。结果是构造函数表比自包含库大一个数量级。
实际后果是,`.init_array` 遍历是 `dlopen(libtpu.so)` 中一个有意义的、单线程的、分配密集阶段:2900 个构造函数会触碰全局注册表、分配描述符映射并运行 RTTI 设置,而这一切都发生在探测任何 TPU 之前。
### CRT 和 C++ 前初始化器
槽位 0 的 addend `0x21211240` 解析为 **`__cpu_indicator_init`**,不是 `_GLOBAL__` 构造函数。这是 clang/GCC 运行时 helper,会填充 `__cpu_model` / `__cpu_features2`,让 `__builtin_cpu_supports` 和 IFUNC resolver 工作;它必须在任何 SIMD dispatch 代码或 IFUNC 被调用之前运行,因此占据最早槽位。它是 22 个低地址 addend 之一。符号表中 `frame_dummy`、`register_tm_clones`、`deregister_tm_clones` 和 `__do_global_ctors_aux` 全部零命中,确认这是一个 lld 链接的 clang-CRT 对象:不存在经典 GCC `crtbegin.o`/`crtend.o` 构造函数 walker;kernel/loader 直接调用 `DT_INIT_ARRAY`。
> **NOTE —** 因为没有 `__do_global_ctors_aux` 自遍历,构造函数顺序就是 lld 在 `.init_array` 中排出的顺序,加上把三个对象拉到前面的 `init_priority` key(100/101/102)。依赖经典 GCC 构造函数优先级语义的重新实现者在这里找不到那套机制;排序契约就是数组顺序本身。
---
## 按地址列出的重要构造函数
少数槽位值得单独指出:要么因为它们的位置(第一个/最后一个),要么因为它们锚定某个类别。地址是重定位 addend(构造函数 VA);名称来自 IDA 符号表。
| 地址 | 符号 | 作用 |
|---|---|---|
| `0x21211240` | `__cpu_indicator_init` | Slot 0 — CPU-feature / IFUNC detector; must precede all SIMD dispatch |
| `0x21380980` | `_GLOBAL__I_000100` | Highest-priority (`init_priority(100)`) global ctor — earliest C++ object |
| `0x21371040` | `_GLOBAL__sub_I_flags.cc` | Flag-table registration cluster (also `0x21378ab0`, `0x21379660`, `0x2137ace0`, …) |
| `0x21218610` | `_GLOBAL__sub_I_xla_ops.cc` | XLA op registration (recurs as `xla_ops.cc_0` at `0x2121f2d0`, ×4 total) |
| span anchor | `_GLOBAL__sub_I_codec_metadata_{ghostlite,jellyfish,pufferfish,viperfish}.cc` | Per-generation codec descriptor-map registration |
| span anchor | `_GLOBAL__sub_I_mlir_bridge_pass_registration.cc` / `*_graph_optimization_pass_registration.cc` | MLIR/HLO pass-pipeline registration |
> **NOTE —** 除上述四个锚点外,具名 span 密集填充着以一个构造函数为粒度的 op/factory/flag 初始化器;列出全部 2644 个具名槽位只会是一份 2644 行的转储,没有重新实现价值。上面的类别表和符号族普查描述了该群体的*形状*;任一单独注册表的按地址细节位于该注册表自己的页面(op 表 → [`dispatch-table-taxonomy.md`](dispatch-table-taxonomy.md),RTTI → [`rtti-vtable-census.md`](rtti-vtable-census.md))。
### 未追踪的内容
本页将表几何(大小、槽位数、重定位编码)解析到 `CERTAIN`,将具名符号族解析到 `CERTAIN`/`HIGH`。仍然是推断的内容:
- **234 个匿名 span 内构造函数** 是通过相减计数的(`[0x21217490, 0x21380980]` 中的 addend 减去具名符号),没有逐个反编译。它们的重定位目标确认它们是构造函数代码;具体类别未枚举。
- **按类别的 TU 计数** 是文件名关键词下界。名为 `*_ops.cc` 的 TU 高置信度是 op registrar,但单个 TU 也可能注册 flags 或 metrics,因此类别会重叠,总和不会等于 2644。
- 遍历的**运行时顺序和时序**,也就是哪个槽位先触发、`__cxa_atexit` 如何镜像它们,以及 2 槽 `.preinit_array` 做什么,按设计不在范围内,由生命周期页面负责。
---
## 交叉引用
- [Do-Init / Do-Fini](../lifecycle/do-init-do-fini.md) — 负责运行时 `.init_array` *遍历*和 `__cxa_atexit`/`.fini_array` 拆卸;是这份静态普查的控制流伴随页面
- [ELF 入口和 Init Proc](../lifecycle/elf-entry-and-init-proc.md) — 驱动该表的 `DT_INIT` (`0xe635524`) thunk 和 `.preinit_array` 排序
- [生命周期概览](../lifecycle/overview.md) — 静态初始化在完整 load → init → serve → teardown 时间线中的位置
- [模块初始化 / 插件发现](../lifecycle/module-init-plugin-discovery.md) — 构造函数表耗尽之后运行的内容,此时这里构建的注册表首次被查询
- [取证概览](overview.md) — 本普查所属的二进制解剖图
- [ELF 解剖](elf-anatomy.md) — 负责完整 section table;`.init_array`/`.fini_array`/`.preinit_array` section 行
- [RTTI / Vtable 普查](rtti-vtable-census.md) — 许多这些构造函数执行的 type-info 注册
- [按代函数调度器](per-gen-function-dispatcher.md) — codec/factory 构造函数在加载时填充的 gxc/pxc/vxc dispatch 表
- [调度表分类](dispatch-table-taxonomy.md) — `*_ops.cc` 和 `*_factory.cc` 初始化器填充的 op/kernel/factory 注册表