ELF 入口与 init_proc
本页所有地址均适用于
libtpu-0.0.40-cp314wheel 中的libtpu.so(构建libtpu_lts_20260413_b_RC00,build-id md589edbbe81c5b328a958fe628a9f2207d,781,691,048 字节,ELF x86-64 DYN,未 stripped;反修饰后的 C++ 符号按原文引用)。其他版本会有所不同。
摘要
本页记录的是当框架调用 dlopen("libtpu.so") 时,动态链接器 (ld-linux-x86-64.so.2) 运行的 ELF 加载期机制:dynamic section 的 init/fini 标签,通过 DT_INIT/DT_FINI 到达的 _init/_fini 传统入口桩,以及把链接器的数组遍历连接到 libtpu 静态构造器风暴的 C-runtime (CRT) trampoline。它是 链接编辑器 / 加载器契约,也就是一组 Elf64_Dyn 标签和每个合规加载器都必须遵守的固定形状桩, 在链接时建立并由加载器消费。它不复现逐构造器迭代,也不复现这些数组指向的 _GLOBAL__sub_I_* 集合;那部分归 do-init-do-fini.md 所有。 它记录的是管线:哪些标签存在、每个桩做什么,以及加载器按什么顺序触发它们。
熟悉的参照框架是标准 ELF init/fini ABI:任何用 GCC 的 crtbegin.o/crtend.o 构建的 -shared 对象都会产生 DT_INIT -> _init、DT_FINI -> _fini, 再加上由加载器遍历的 DT_PREINIT_ARRAY / DT_INIT_ARRAY / DT_FINI_ARRAY 指针数组。反直觉的发现是,libtpu 使用的是 LLVM/clang CRT,而不是 GCC 的: 经典的 crtbegin.o 样板代码,即 frame_dummy、register_tm_clones、 deregister_tm_clones、会迭代传统 .ctors 列表的 __do_global_ctors_aux trampoline,全部不存在。没有 .ctors/.dtors,.tm_clone_table 长度为零, 唯一留下的 CRT "trampoline" 是一对 17/39 字节的数组入口桩 (__do_init @ 0xe63c000,__do_fini @ 0xe63c020)。_init/_fini 本身退化为残留的 glibc 桩。整个“迭代构造器”的工作被委托给加载器直接遍历 DT_INIT_ARRAY,二进制内部没有需要逆向的迭代器。
对重新实现而言,契约是:
- 八个
Elf64_Dyn标签,负责连通 init/fini,包含它们的值以及加载器规定的触发顺序(PREINIT -> INIT -> ... -> FINI)。 - 两个传统桩
_init/_fini(DT_INIT/DT_FINI):精确字节,以及它们为什么不承载真实工作。 - 此二进制中的 CRT trampoline 形状:LLVM 的
__do_init/__do_fini数组入口桩及其背后的 set-once 字节 guard,以及缺失的 GCC 样板代码,这样重新实现者不会去寻找frame_dummy。 - GOT/PLT 启动,加载器会在上述任何步骤之前执行,因为 init 代码本身通过已重定位的槽运行。
DT_INIT -> _init | 0xe635524 (.init, 23 B) — __gmon_start__ 检查并调用桩 |
DT_FINI -> _fini | 0xe63553c (.fini, 9 B) — 空的 sub rsp,8 / add rsp,8 / ret |
DT_PREINIT_ARRAY | 0x22048b30, DT_PREINIT_ARRAYSZ 16 B(2 个条目) |
DT_INIT_ARRAY | 0x215f26f0, DT_INIT_ARRAYSZ 23200 B(2900 个条目) |
DT_FINI_ARRAY | 0x215f8190, DT_FINI_ARRAYSZ 16 B(2 个条目) |
| CRT init 桩 | __do_init @ 0xe63c000 (17 B),guard __do_init.__initialized |
| CRT fini 桩 | __do_fini @ 0xe63c020 (39 B),guard __do_fini.__finalized -> __cxa_finalize(__dso_handle) |
| GOT / PLT | .got @ 0x22048d50 (50256 B),.got.plt @ 0x224c2980 (3808 B),.plt @ 0x213f0830 (7584 B) |
| CRT 风格 | LLVM/clang — 没有 frame_dummy / register_tm_clones / __do_global_ctors_aux;未发出 .tm_clone_table section |
| 置信度 | CONFIRMED(以字节锚定,对照 readelf -d、segment table 和反汇编),除非某行另有说明 |
加载器契约 — Elf64_Dyn Init/Fini 标签
目的
本页上的每个动作都由加载器读取 dynamic section 驱动。链接编辑器把入口点和数组边界记录为 Elf64_Dyn 标签;加载器(重定位之后)按照 System V x86-64 gABI 规定的固定顺序遵守它们。 整个“dlopen 时运行什么”的问题可化简为:这些八个标签中哪些存在、它们指向哪里,以及顺序规则是什么。
标签清单
直接从针对该二进制的 readelf -d 读取;每个值都是字节精确的。
| 标签 (d_tag) | 十六进制 | 值 | 含义 |
|---|---|---|---|
DT_INIT | 0x0c | 0xe635524 | _init 的地址 — 在 DT_INIT_ARRAY 之后运行?不是:按 gABI 顺序,在数组之前。 |
DT_FINI | 0x0d | 0xe63553c | _fini 的地址 — 在 DT_FINI_ARRAY 之后运行。 |
DT_INIT_ARRAY | 0x19 | 0x215f26f0 | 构造器指针数组(.init_array)的基址。 |
DT_INIT_ARRAYSZ | 0x1b | 23200 | 2900 x 8 B — 数量 = size / 8。 |
DT_FINI_ARRAY | 0x1a | 0x215f8190 | 析构器指针数组(.fini_array)的基址。 |
DT_FINI_ARRAYSZ | 0x1c | 16 | 2 x 8 B — 2 个条目。 |
DT_PREINIT_ARRAY | 0x20 | 0x22048b30 | pre-init 指针数组的基址 — 在任何构造器之前运行。 |
DT_PREINIT_ARRAYSZ | 0x21 | 16 | 2 x 8 B — 2 个条目。 |
NOTE — 这里不存在需要消解的
DT_INIT_ARRAYSZ歧义:加载器仅按ARRAYSZ / sizeof(void*)推导条目数量。libtpu 不在任何数组中编码终止 sentinel,因此重新实现者必须依赖 size 标签,而不是 NULL/-1结束标记。
gABI 触发顺序
加载器以严格的标准化顺序触发这些项。下图是此对象的规范 dlopen 顺序;每个带状区域都标出加载器正在消费的标签。
dlopen("libtpu.so") → ld-linux-x86-64.so.2
│
├─ (1) map segments, then apply ALL relocations
│ .got / .got.plt / the three init arrays are filled here
│ (see "GOT / PLT Bring-Up" below)
│
├─ (2) DT_PREINIT_ARRAY @ 0x22048b30 [2 entries] ── runs FIRST, before any ctor
│ [0] cpu_feature_fail_fast 0x2110abc0 ── CPU ISA hard gate; SIGILL on miss
│ [1] setup_dl_debug_hook 0x2114eec0
│
├─ (3) DT_INIT @ 0xe635524 (_init) ── __gmon_start__ check-and-call stub
│
├─ (4) DT_INIT_ARRAY @ 0x215f26f0 [2900 entries] ── the static-constructor storm
│ [0] __cpu_indicator_init 0x21211240 ── GCC ifunc CPU-feature resolver (slot 0)
│ [1] Rust ARGV_INIT_ARRAY 0x20a0d2b0 ── std::sys::args::unix init_wrapper
│ └─ … 2898 more: _GLOBAL__sub_I_* / _GLOBAL__I_* / __cxx_global_var_init,
│ with __do_init (0xe63c000) among them — slot order owned by static-init.md
▼
libtpu mapped & constructed; nothing TPU-specific has RUN yet
───────────────────────────── at unload / process exit ─────────────────────────────
├─ (5) DT_FINI_ARRAY @ 0x215f8190 [2 entries] ── reverse-order destructors
│ [0] __do_fini 0xe63c020 ── guarded __cxa_finalize(__dso_handle)
│ [1] rand_thread_state_clear_all 0x2063df60 ── per-thread BoringSSL/RNG clear
│
└─ (6) DT_FINI @ 0xe63553c (_fini) ── empty stub
```text
> **GOTCHA —** `DT_PREINIT_ARRAY` 在 `DT_INIT` 和 `DT_INIT_ARRAY` 之前运行,但 gABI 只允许 **executable** 中存在 `DT_PREINIT_ARRAY`,不允许在通过 `dlopen` 加载的 shared object 中使用。libtpu 仍然带了一个(`PREINIT_ARRAYSZ = 16`)。在 glibc 的加载器上,`dlopen` 的 `.so` 的 preinit array 会被尊重,并作为重定位之后的第一件事运行;更严格的加载器可能会跳过它,这会静默禁用 CPU-feature hard gate。要精确匹配 libtpu 行为的重新实现者必须为 shared object 运行 `DT_PREINIT_ARRAY`,不能假设 executable-only 限制。11-feature CPU gate 解码见 [module-init-plugin-discovery.md](module-init-plugin-discovery.md#21-the-cpu-feature-hard-gate-preinit_array0)。
>
> **QUIRK —** 三个数组的*文件镜像*全为零。每个槽都是一个 `R_X86_64_RELATIVE` 重定位;磁盘上的 `.init_array` 是 23200 字节的零,加载器在步骤 (1) 中写入每个目标 VA。重定位前静态 dump 数组 section 看不到任何目标;它们只在运行时存在。这就是为什么数组内容是从重定位表记录,而不是从 section 字节记录。
---
## `_init` — `DT_INIT` 桩(`init_proc @ 0xe635524`)
### 目的
`DT_INIT` 指向加载器调用一次的函数,调用时机在 `DT_PREINIT_ARRAY` 之后、`DT_INIT_ARRAY` 之前。
在 GCC 工具链中,这是 `_init`,也就是 `.init` section 的头部,由 `crti.o` 打开、`crtn.o` 关闭,
历史上会链到 `__do_global_ctors_aux`。在 libtpu 中,它只是**残留的 glibc profiling 桩**,
没有其他内容;所有真实构造器工作都移到了 `DT_INIT_ARRAY`。
### 反汇编
整个 `.init` section 为 23 字节(`0xe635524`-`0xe63553b`):
```asm
0xe635524: 48 83 ec 08 sub rsp, 8
0xe635528: 48 8b 05 31 38 a1 13 mov rax, [rip+0x13a13831] ; __gmon_start__ GOT slot @ 0x22048d60
0xe63552f: 48 85 c0 test rax, rax
0xe635532: 74 02 jz loc_E635536 ; skip if unbound
0xe635534: ff d0 call rax ; __gmon_start__()
0xe635536: 48 83 c4 08 add rsp, 8
0xe63553a: c3 ret算法
function _init(): // .init_proc @ 0xe635524 (DT_INIT)
// Standard glibc gmon (gprof) bring-up hook.
if (&__gmon_start__ != NULL) // weak symbol via relocated GOT slot
__gmon_start__(); // arms profiling if linked; else no-op
return;
// No __do_global_ctors_aux tail-call. The .ctors list does not exist
// in this binary; constructor iteration is the loader's DT_INIT_ARRAY walk.
```text
> **NOTE —** `__gmon_start__` 是**弱未定义导入**(`readelf --dyn-syms`: `NOTYPE WEAK DEFAULT UND __gmon_start__`,值为 `0`);`_init` 读取其位于 `0x22048d60` 的 GOT 槽(一个 `R_X86_64_GLOB_DAT` reloc)。只有在 profiling runtime 存在时该槽才会绑定,而此 wheel 中没有。`test rax,rax` guard 使该调用在生产环境中成为 no-op。对 `rsp` 的 8 字节 `sub`/`add` 只是来自 `crti.o` 的 `_init` 序言的栈对齐样板。重新实现者可以把 `_init` 当作“什么也不做”,不会有行为损失。
---
## `_fini` — `DT_FINI` 桩(`term_proc @ 0xe63553c`)
### 目的
`DT_FINI` 是对称的 teardown 入口:加载器在 `DT_FINI_ARRAY` 之后最后调用它。在 GCC 中它会链到
`__do_global_dtors_aux`。这里它是**空的**,所有析构器工作都在 `DT_FINI_ARRAY` 中。
### 反汇编
整个 `.fini` section 为 9 字节(`0xe63553c`-`0xe635545`):
```asm
0xe63553c: 48 83 ec 08 sub rsp, 8
0xe635540: 48 83 c4 08 add rsp, 8
0xe635544: c3 ret它压入并弹出栈然后返回,是 no-op。没有 __cxa_finalize,也没有析构器迭代器。实际 finalize 调用位于 DT_FINI_ARRAY 条目 __do_fini,如下文所述。
CRT Trampoline — __do_init / __do_fini
目的
这是页面摘要中称为 "CRT boilerplate" 的部分,也是 libtpu 与 GCC 布局差异最明显的地方。 在 GCC .so 中,crtbegin.o 会贡献 frame_dummy(注册 exception-handling frame)、 register_tm_clones / deregister_tm_clones(transactional-memory clone table), 以及一个会在 .ctors 列表上循环的 __do_global_ctors_aux trampoline。 用 LLVM/clang CRT 构建的 libtpu 不携带这些。仅剩的 CRT artifact 是两个最小数组入口函数:
__do_init@0xe63c000—DT_INIT_ARRAY条目之一(一个_GLOBAL__sub_I_*同级槽,不是 slot 0;其构造器顺序位置归 static-init.md 所有),一个 set-once 字节 guard。它不迭代任何东西。__do_fini@0xe63c020— 一个DT_FINI_ARRAY条目,运行受 guard 保护的__cxa_finalize(__dso_handle),这通常由 GCC 的__do_global_dtors_aux运行。
反汇编 — __do_init(17 字节)
0xe63c000: 80 3d 79 78 e8 13 00 cmp cs:__do_init.__initialized, 0
0xe63c007: 75 07 jnz locret_E63C010 ; already done → return
0xe63c009: c6 05 70 78 e8 13 01 mov cs:__do_init.__initialized, 1
0xe63c010: c3 ret
```text
### 反汇编 — `__do_fini`(39 字节)
```asm
0xe63c020: 80 3d 5a 78 e8 13 00 cmp cs:__do_fini.__finalized, 0
0xe63c027: 75 1d jnz locret_E63C046 ; already done → return
0xe63c029: c6 05 51 78 e8 13 01 mov cs:__do_fini.__finalized, 1
0xe63c030: 48 83 3d 30 cd a0 13 00 cmp cs:__cxa_finalize_ptr, 0 ; weak null-check
0xe63c038: 74 0c jz locret_E63C046
0xe63c03a: 48 8b 3d 7f 91 c1 13 mov rdi, cs:__dso_handle
0xe63c041: e9 fa 47 db 12 jmp __cxa_finalize ; tail-call
0xe63c046: c3 ret算法
function __do_init(): // 0xe63c000 (a DT_INIT_ARRAY entry)
if (__do_init.__initialized) // 1-byte guard in .bss
return; // idempotent: re-entry is a fast no-op
__do_init.__initialized = 1;
// That is the entire body. No frame_dummy, no ctor loop, no atexit().
// GCC's __do_global_ctors_aux would loop .ctors here; clang's CRT leaves
// constructor iteration entirely to the loader's DT_INIT_ARRAY walk.
function __do_fini(): // 0xe63c020 (a DT_FINI_ARRAY entry)
if (__do_fini.__finalized) // 1-byte guard in .bss
return;
__do_fini.__finalized = 1;
if (&__cxa_finalize != NULL) // weak — present here
__cxa_finalize(__dso_handle); // run libtpu's registered atexit/dtor list
```text
> **QUIRK —** 构造器 "trampoline" *不承载任何工作*。`__do_init` 只翻转一个字节。期待 GCC `__do_global_ctors_aux` 循环的读者,即获取 `__CTOR_LIST__`、反向遍历并调用每个函数指针直到 `-1` sentinel,在这里找不到它,因为 clang 把构造器作为直接的 `DT_INIT_ARRAY` 条目发出,并让加载器迭代。"static-constructor storm" 是 2900 次由加载器驱动的调用,而不是一个二进制内部循环。逐条目细节(这 2900 个指针是什么、`__do_init` guard 的作用、`_GLOBAL__sub_I_*` 集合)见 [do-init-do-fini.md](do-init-do-fini.md)。
>
> **NOTE —** 在 tail-call 之前会检查 `__cxa_finalize_ptr` 是否为 NULL,因为 `__cxa_finalize` 是弱导入;该槽在此二进制中会绑定(libc 提供它),所以卸载时会触发调用。`__do_fini` 是运行已注册析构器/`atexit` 列表的*唯一路径*,`_fini` 是空的。如果重新实现者把 teardown 接到 `DT_FINI` 而不是 `DT_FINI_ARRAY[0]`,就会静默跳过每个 C++ 析构器。完整 `FINI_ARRAY` 主体和析构顺序见 [do-init-do-fini.md](do-init-do-fini.md)。
### 缺失的内容(以及为什么重要)
从 GCC 心智模型移植的重新实现者*不应*寻找以下内容,因为它们在此二进制中不存在:
| GCC `crtbegin.o` artifact | libtpu 中的状态 | 后果 |
|---|---|---|
| `frame_dummy` | 不存在 | EH-frame 注册由 `.eh_frame_hdr` + 加载器处理,而不是 ctor |
| `register_tm_clones` / `deregister_tm_clones` | 不存在 | 未发出 `.tm_clone_table` section;没有 TM clone fixup |
| `__do_global_ctors_aux`(ctor 循环) | 不存在 | 构造器是直接的 `DT_INIT_ARRAY` 条目;加载器迭代 |
| `__do_global_dtors_aux`(dtor 循环) | 被 `__do_fini` 取代 | 只有 `__cxa_finalize(__dso_handle)`;不遍历 `.dtors` |
| `.ctors` / `.dtors` sections | 不存在 | 被 `.init_array` / `.fini_array` 取代 |
> **Note:** 不存在 GCC `__do_global_ctors_aux` 类型的全局构造器 trampoline 可供逆向。幸存的 CRT trampoline 是 `__do_init` / `__do_fini` 这一对,并且只有 `__do_fini` 做真实工作(`__cxa_finalize`)。构造器*迭代*由加载器在 `DT_INIT_ARRAY` 上执行。这是 clang-CRT 与 GCC-CRT 的差异,不是缺失函数。
---
## GOT / PLT 启动
### 目的
在*任何* init 代码运行之前(触发顺序的步骤 1),加载器会应用该二进制的重定位。上面的 init 桩已经依赖这一点:
`_init` 读取 `__gmon_start__` GOT 槽(`0x22048d60`,一个 `R_X86_64_GLOB_DAT`),
`__do_fini` 从已重定位数据读取 `__cxa_finalize` 槽和 `__dso_handle`(`0x222551c0`),
并且 2900 个 `DT_INIT_ARRAY` 槽中的每一个本身都是加载器必须写入的 `R_X86_64_RELATIVE` 目标。
因此 GOT/PLT 重定位是生命周期中真正的*第一*事件,在逻辑上先于 `DT_PREINIT_ARRAY`。
### Section 布局
| Section | VA | 大小 | 作用 |
|---|---|---|---|
| `.plt` | `0x213f0830` | 7584 B | 进入导入函数(如 libc、libdl)的 lazy/eager 调用 thunk。 |
| `.got` | `0x22048d50` | 50256 B | 全局偏移表,即已重定位的数据/函数指针。 |
| `.got.plt` | `0x224c2980` | 3808 B | PLT 专用 GOT 槽;首次调用时解析(或在 `BIND_NOW` 下立即解析)。 |
libtpu 中占主导的重定位类型是 `R_X86_64_RELATIVE`(三个 init 数组全都是 `RELATIVE`);
导入符号槽使用 `R_X86_64_GLOB_DAT`(99 个条目)/ `R_X86_64_JUMP_SLOT`(`.rela.plt`,473 个条目)。
加载器必须满足的系统导入来自六个 `DT_NEEDED` 库,即 `libm.so.6`、`libpthread.so.0`、
`libdl.so.2`、`librt.so.1`、`libc.so.6`、`ld-linux-x86-64.so.2`,并且在 `.dynsym`
中有 **383 个 versioned `UND FUNC` imports**(`@GLIBC_*` / `@GCC_*`);`dlopen`/`dlsym`/`dlerror`/`dlclose`
(来自 `libdl`)是框架的发现握手稍后依赖的符号。(另有 **123** 个弱 `UND NOTYPE` 符号,即 67 个 `TF_*`
可选 TensorFlow C-API hook 加上 56 个其他弱引用(`__gmon_start__`、`_ZTH*` thread-init helpers、
`__morestack`、grpc/xla weak globals),只有在 host 绑定它们时才会满足,并非加载器强制。)
> **NOTE —** 这里的 GOT/PLT 机制完全是标准 x86-64 PSABI;libtpu 在这一层没有添加任何自定义内容。与重新实现相关的唯一事实是*顺序依赖*:init 数组本身就是重定位目标,因此如果加载器在完成 `R_X86_64_RELATIVE` 处理之前运行 `DT_INIT_ARRAY`,就会通过清零的槽跳转。重定位严格先于所有 init 触发。
---
## 相关组件
| 组件 | 关系 |
|---|---|
| `cpu_feature_fail_fast @ 0x2110abc0` | `DT_PREINIT_ARRAY[0]` — 加载器运行的第一件事;CPU ISA hard gate |
| `setup_dl_debug_hook @ 0x2114eec0` | `DT_PREINIT_ARRAY[1]` — dynamic-linker debug rendezvous hook |
| `__cpu_indicator_init @ 0x21211240` | `DT_INIT_ARRAY[0]` — GCC ifunc CPU-feature resolver,在构造器之前运行 |
| `__do_init @ 0xe63c000` | CRT init-array 条目;set-once 字节 guard `__do_init.__initialized` |
| `__do_fini @ 0xe63c020` | `DT_FINI_ARRAY[0]` — 受 guard 保护的 `__cxa_finalize(__dso_handle)` |
| `rand_thread_state_clear_all @ 0x2063df60` | `DT_FINI_ARRAY[1]` — 卸载时清理每线程 BoringSSL/RNG |
| `__gmon_start__`(弱 UND,GOT 槽 `0x22048d60`) | `_init` 调用的弱 gmon hook;此 wheel 中未绑定(no-op) |
## 交叉引用
- [overview.md](overview.md) — 完整的加载到卸载生命周期图;本页负责其中的 Stage 0 ELF 机制
- [do-init-do-fini.md](do-init-do-fini.md) — 逐构造器的 `DT_INIT_ARRAY` 迭代、`_GLOBAL__sub_I_*` 集合、上下文中的 `__do_init`/`__do_fini` guard,以及完整 `FINI_ARRAY` 主体
- [module-init-plugin-discovery.md](module-init-plugin-discovery.md) — `PREINIT_ARRAY` CPU-feature gate 解码、仅寄存器构造器风暴,以及延迟的 `PJRT_Plugin_Initialize` bootstrap