Skip to content

ELF 剖析

本页中的所有偏移、虚拟地址和计数均适用于 libtpu-0.0.40 wheel 中的 libtpu.so(build-id 89edbbe81c5b328a958fe628a9f2207d,构建标签 libtpu_lts_20260413_b_RC00)。该 wheel、其 dist-info/METADATA 以及 libtpu/__init__.py 都报告 wheel 版本为 0.0.40;build-id 是唯一无歧义的锚点,也是每个页面固定引用的字段。其他构建在每个地址上都会不同。

摘要

libtpu.so 是一个 745 MiB(781,691,048 字节)的、位置无关的 ELF64 共享对象 — ET_DYNEM_X86_64、小端序、System V ABI。它把整个 Google TPU PJRT 插件静态链接进了一个文件:XLA 编译器、MLIR/LLVM 后端、TPU 运行时、gRPC、protobuf、Abseil、Eigen 以及设备驱动都在其中。它没有 DT_SONAME,没有 RPATH/RUNPATH,没有单独的调试对象,并且只依赖六个裸系统库。尽管体积巨大,它仍然是一个完全普通的 clang/LLVM 构建的 .so,只是规模放大了三个数量级。

决定性结构事实是,这个对象使用 x86-64 large code model 构建。该模型把只读数据和 BSS 数据拆成两层:普通 small-model .rodata/.bss 可通过 32 位 %rip 相对位移到达,而 large-model .lrodata/.lbss/.ldatal 标志,SHF_X86_64_LARGE)由于与 .text 的距离超过 ±2 GiB,需要通过 64 位绝对寻址到达。仅 .lrodata 就有 108 MiB。这就是运行时依赖的单例表 — 包括 PJRT API vtable — 位于 vaddr 0x227ba840 附近的 .lbss,而不是 .bss 的原因。假设只有一对 .rodata/.bss 的重实现者会把三分之一的常量数据映射错误。

本页是该二进制的权威 section/segment/dynamic 映射。本 wiki 其他地方引用的每个地址都位于下表列出的某个区域内;本页确定哪个 section 拥有哪些 vaddr 范围,哪个 PT_LOAD 携带哪些权限,以及动态链接器在加载时获得什么指令。深入内容 — .init_array 构造函数是谁,以及异常的 google_*/__rseq_cs/linkarr_* sections 持有什么 — 属于各表处标注的相邻页面。

对重实现而言,契约是:

  • 四段式加载映像 — 一个 R E text/rodata segment,后接三个 RW data segments,以及第一个 segment 之后打破 file-offset ≡ vaddr 的页移位规则。
  • large-code-model section 拆分.rodata/.bss/.data(small)相对于 .lrodata/.lbss/.ldata(large,带 l 标志),以及为什么运行时的单例落在 large 层。
  • 动态契约 — 六个 DT_NEEDED、仅 GNU-hash 的查找、带有一百多万个 R_X86_64_RELATIVE 重定位的 .rela.dyn/.rela.plt 拆分,以及 init/fini/preinit array 锚点。
文件大小781,691,048 bytes (~745 MiB)
类别 / 数据 / 类型ELF64 · LSB (2's complement, little-endian) · ET_DYN
机器 / OS-ABIEM_X86_64 · UNIX System V (ABI v0)
入口点0x0(无 — 纯共享库)
Program headers11 entries, 56 bytes each, at file offset 64
Section headers52 entries, 64 bytes each, at file offset 0x2e979ba8
.shstrtab 索引section [50]
Build-id (NT_GNU_BUILD_ID)89edbbe81c5b328a958fe628a9f2207d (16 bytes)
DT_SONAME
Code modelx86-64 large(存在 .lrodata/.lbss/.ldata
.text 范围0xe63c0000x21217484, 0x12bdb484 bytes (~299 MiB)

ELF 头部

这个 header 是 readelf -h 的原样内容。本页以及 wiki 其他地方的所有下游地址,都是该 header 所描述地址空间中的虚拟地址。

text
Class:                             ELF64
Data:                              2's complement, little endian
Type:                              DYN (Shared object file)
Machine:                           Advanced Micro Devices X86-64
Entry point address:               0x0
Start of program headers:          64 (bytes into file)
Start of section headers:          781687720 (bytes into file)   # 0x2e979ba8
Size of program headers:           56  ·  Number of program headers:   11
Size of section headers:           64  ·  Number of section headers:   52
Section header string table index: 50
```text

三个 header 事实塑造了下面的一切。

- **`Entry point = 0x0`。** 这是库,不是可执行文件;不存在 `_start`。执行通过 `DT_INIT`/`DT_INIT_ARRAY` 机制(由 loader 驱动的构造函数链)以及 `GetPjrtApi` 等导出入口符号进入。进入 init 的机制由 [`lifecycle/elf-entry-and-init-proc.md`](../lifecycle/elf-entry-and-init-proc.md) 负责。
- **section header table 位于文件最末尾**(偏移 `0x2e979ba8`,约 745 MiB 处),在巨大的 `.symtab`/`.strtab` 之后。section headers、symbol table 和 string tables 都*不是*任何 `PT_LOAD` 的一部分;它们存在于磁盘上,但运行时从不映射。
- **包含 `NULL` section `[0]` 在内共 52 个 sections。** 枚举“真实”sections 的工具会报告 51;两个计数描述的是同一张表。

> **NOTE —** 在 `.symtab` 意义上该二进制是 `not_stripped`:磁盘上存在完整的 `.symtab`,包含 `1,233,710` 个 entries(大小 `0x1c3cc50` ÷ 24),外加一个 `0xab824de` 字节(约 172 MiB)的 `.strtab`。这些为静态分析提供每个内部函数的名称,但它们是非 `SHF_ALLOC`,对运行时映像没有任何贡献。运行时只看见 741-entry 的 `.dynsym`。

| | |
|---|---|
| ELF 类别 / 字节序 / 类型 | ELF64 · LSB · `ET_DYN` |
| 机器 | `EM_X86_64` (`Advanced Micro Devices X86-64`) |
| 入口点 | `0x0`(无 `_start`) |
| PHDR 数量 / 大小 | 11 × 56 bytes at offset `0x40` |
| SHDR 数量 / 大小 | 52 × 64 bytes at offset `0x2e979ba8` |
| `.shstrtab` 索引 | `[50]` |

---

## Segment 表(Program Headers)

运行时映像是**四个 `PT_LOAD` segments**:一个 read-execute,三个 read-write。其余七个 program headers 是指向相同字节的元数据视图(`PHDR`、`TLS`、`DYNAMIC`、`GNU_RELRO`、`GNU_EH_FRAME`、`GNU_STACK`、`NOTE`)。所有对齐都是 2 MiB(`0x200000`)— 该构建以 huge pages 为目标。

```text
Type           Offset     VirtAddr     FileSiz    MemSiz     Flg  Align
PHDR           0x000040   0x00000040   0x000268   0x000268   R    0x8
LOAD  [1]      0x000000   0x00000000   0x213f25d0 0x213f25d0 R E  0x200000
LOAD  [2]      0x213f25e0 0x215f25e0   0xa62bc0   0xa63a20   RW   0x200000
LOAD  [3]      0x21e551c0 0x222551c0   0x26e6a0   0x343a70   RW   0x200000
LOAD  [4]      0x22198c30 0x22798c30   0x021c00   0x0c6650   RW   0x200000
TLS            0x213f25e0 0x215f25e0   0x000110   0x000e78   R    0x20
DYNAMIC        0x21e48b40 0x22048b40   0x000210   0x000210   RW   0x8
GNU_RELRO      0x213f25e0 0x215f25e0   0xa62bc0   0xa63a20   R    0x1
GNU_EH_FRAME   0x0c2cc634 0x0c2cc634   0x6bd684   0x6bd684   R    0x4
GNU_STACK      0x000000   0x00000000   0x000000   0x000000   RW   0x0
NOTE           0x0002a8   0x000002a8   0x000020   0x000020   R    0x4

四个 load segments

Seg权限文件偏移VaddrFileSizMemSiz包含
LOAD 1R E0x00x00x213f25d0 (~534 MiB)相同元数据、.lrodata.rodata、EH tables、全部 .text*.plt
LOAD 2RW0x213f25e00x215f25e00xa62bc0 (~10.4 MiB)0xa63a20TLS template、init/fini/preinit arrays、.data.rel.ro.dynamic.got
LOAD 3RW0x21e551c00x222551c00x26e6a0 (~2.5 MiB)0x343a70.datagoogle_*/__rseq_cs/linkarr_* sections、.got.plt.bss
LOAD 4RW0x22198c300x22798c300x021c000x0c6650 (~806 KiB).ldata.lbssgoogle_malloc_bss(large-model writable)

GOTCHA — file-offset ≡ vaddr 恒等关系只在 LOAD 1 中成立,因为两者都从 0 开始。从 LOAD 2 起,linker 将 vaddr 相对 file offset 向前按递增的 2 MiB 步长移位:LOAD 2 的 vaddr − offset 为 0x200000,LOAD 3 为 0x400000,LOAD 4 为 0x600000。计算 file_offset = vaddr 的工具(在 .text 内正确)会为 writable segments 中的任何内容读取错误字节 — 包括 .dynamic.got.lbss 单例。始终通过所属 PT_LOAD 转换,不要使用单一全局 delta。

三个 writable segments 被拆开而非合并,是因为 large-code-model writable data(.ldata/.lbss)必须留在自己的区域:LOAD 4 精确承载带 l 标志的 writable sections,与 LOAD 3 的 small-model .data/.bss 分离。

非 LOAD program headers

  • GNU_RELRO 覆盖整个 LOAD-2 字节范围(0x215f25e00xa62bc0 bytes)。动态链接器应用重定位后,这整个 10.4 MiB 跨度 — init arrays、.data.rel.ro.dynamic.got — 会被重新映射为只读。.got.plt 特意位于 LOAD 3,处于 RELRO 之外,这样 lazy PLT binding 仍可写入已解析地址。
  • TLS 指向 LOAD-2 开头:filesz 0x110.tdata 初始化映像,memsz 0xe78 加上 .tbss。对这种体量的二进制来说,thread-local block 很小(3704 bytes)。
  • DYNAMIC 是 528 字节(0x210)的 .dynamic array,也可作为 section [34] 寻址。
  • GNU_EH_FRAME 是 vaddr 0xc2cc634 处的 .eh_frame_hdr 二分搜索索引,位于 30 MiB .eh_frame 之前。
  • GNU_STACK 大小为零且 RW(无 X):非可执行栈,现代默认值。
  • NOTE0x2a8 处的 32 字节 build-id note。

QUIRK — read-execute LOAD 1 覆盖 0x00x213f25d0(约 534 MiB),物理上包含的常量数据远多于代码:.lrodata(108 MiB)、.rodata(58 MiB)和 EH tables(约 38 MiB unwind)都位于 executable segment 内,因为它们是只读的,而不是因为它们可执行。.text 是最大租户(约 299 MiB),但在小型二进制意义上该 segment 不是“代码段” — 它是“所有不可变内容”。


Section 表

52 个 section headers。下表按角色分组,并给出 readelf -S 报告的地址、文件偏移、大小和 flags。Flag key:A alloc,X execute,W write,M mergeable,S strings,T TLS,I info-link,o OS-processing,l SHF_X86_64_LARGE

动态链接元数据(LOAD 1 头部)

[Nr] 名称类型地址偏移大小标志
[1] .note.gnu.build-idNOTE0x2a80x2a80x20A
[2] .dynsymDYNSYM0x2c80x2c80x4578A
[3] .gnu.versionVERSYM0x48400x48400x5caA
[4] .gnu.version_dVERDEF0x4e0c0x4e0c0x38A
[5] .gnu.version_rVERNEED0x4e440x4e440x270A
[6] .gnu.hashGNU_HASH0x50b80x50b80x678A
[7] .dynstrSTRTAB0x57300x57300x3a3cA
[8] .rela.dynRELA0x91700x91700x1878c30A
[9] .rela.pltRELA0x1881da00x1881da00x2c58AI

只读数据(LOAD 1 主体)

[Nr] 名称类型地址大小标志包含
[10] .lrodataPROGBITS0x1884a000x6c0e7d0 (~108 MiB)AMSllarge-model RO constants 和 merged strings
[11] .rodataPROGBITS0x84a00000x39eaf28 (~58 MiB)AMSosmall-model RO constants,64 KiB 对齐
[12] protodesc_coldPROGBITS0xbe8af300x334180 (~3.2 MiB)Acold protobuf descriptor tables
[13] .gcc_except_tablePROGBITS0xc1bf0b00x10d584 (~1.0 MiB)ALSDA exception tables
[14] .eh_frame_hdrPROGBITS0xc2cc6340x6bd684 (~6.7 MiB)Aunwind search index
[15] .eh_framePROGBITS0xc989cb80x1cab86c (~28.7 MiB)ADWARF CFI unwind records

可执行代码(LOAD 1 尾部)

[Nr] 名称类型地址大小标志角色
[16] .initPROGBITS0xe6355240x17AXDT_INIT legacy ctor stub
[17] .finiPROGBITS0xe63553c0x9AXDT_FINI legacy dtor stub
[18] .text.hotPROGBITS0xe6355600x1e2eAXoprofile-hot functions
[19] google_mallocPROGBITS0xe6373c00x46f2AXoTCMalloc fast-path code
[20] .text.splitPROGBITS0xe63bab20x0AXoempty placeholder section
[21] .textPROGBITS0xe63c0000x12bdb484 (~299 MiB)AXmain code body
[22] .text.startupPROGBITS0x212174900x16a454 (~1.4 MiB)AXconstructor/startup code
[23] .text.unlikelyPROGBITS0x213819000x68469 (~427 KiB)AXcold/unlikely paths
[24] google_init_coldPROGBITS0x213e9d800x60f1AXcold init code
[25] malloc_hookPROGBITS0x213efe800x89eAXallocator hook trampolines
[26] __lcxx_overridePROGBITS0x213f07200x105AXlibc++ operator new/delete overrides
[27] .pltPROGBITS0x213f08300x1da0AXprocedure linkage table(473 slots)

可写:TLS、init arrays、RELRO data(LOAD 2)

[Nr] 名称类型地址偏移大小标志
[28] .tdataPROGBITS0x215f25e00x213f25e00x110WAT
[29] .tbssNOBITS0x215f26f00x213f26f00xd68WAT
[30] .init_arrayINIT_ARRAY0x215f26f00x213f26f00x5aa0 (23200 B)WAo
[31] .fini_arrayFINI_ARRAY0x215f81900x213f81900x10WA
[32] .data.rel.roPROGBITS0x215f81a00x213f81a00xa50990 (~10.3 MiB)WA
[33] .preinit_arrayPREINIT_ARRAY0x22048b300x21e48b300x10WA
[34] .dynamicDYNAMIC0x22048b400x21e48b400x210WA
[35] .gotPROGBITS0x22048d500x21e48d500xc450WA
[36] .relro_paddingNOBITS0x220551a00x21e551a00xe60WA

NOTE — .init_array 在 vaddr 0x215f26f0 处为 0x5aa0 = 23,200 bytes,与下面的 DT_INIT_ARRAY/DT_INIT_ARRAYSZ 精确匹配。按每个指针 8 bytes 计算,这是 2,900 个构造函数指针 — 一个巨大的静态初始化扇出。这些构造函数的名单(以及顺序风险)由 forensics/static-init.md 负责;本页只固定该 array 的位置和范围。

可写:small-model data 和 custom sections(LOAD 3)

[Nr] 名称类型地址偏移大小标志
[37] .dataPROGBITS0x222551c00x21e551c00x26a5d8 (~2.5 MiB)WA
[38] filewrapper_tocPROGBITS0x224bf7980x220bf7980x1e8WA
[39] __rseq_csPROGBITS0x224bf9800x220bf9800x2260WA
[40] __rseq_cs_ptr_arrayPROGBITS0x224c1be00x220c1be00x898WA
[41] linkarr_upb_AllExtsPROGBITS0x224c24800x220c24800x4a0WAo
[42] pb_defaultsPROGBITS0x224c29200x220c29200x18WA
[43] google_malloc_dataPROGBITS0x224c29380x220c29380x48WA
[44] .got.pltPROGBITS0x224c29800x220c29800xee0WA
[45] .bssNOBITS0x224c38800x220c38600xd53b0 (~853 KiB)WAo

filewrapper_toc__rseq_cs/__rseq_cs_ptr_array(TCMalloc 的 restartable-sequences critical-section descriptors)、linkarr_upb_AllExts(upb proto-extension link array)、pb_defaultsgoogle_malloc_data sections 是非标准的、由 linker script 放置的 sections。它们的内部布局和用途是 forensics/custom-sections.md 的主题;这里列出它们只是为了在全局映射中固定地址。

可写:large-model data(LOAD 4)

[Nr] 名称类型地址偏移大小标志
[46] .ldataPROGBITS0x22798c300x22198c300x21c00WAl
[47] .lbssNOBITS0x227ba8400x221ba8300x9f940 (~654 KiB)WAl
[48] google_malloc_bssNOBITS0x2285a1800x221ba8300x5100WAl

QUIRK — 运行时的 PJRT API 单例位于 .lbss,靠近 vaddr 0x227ba840(section 基址),而不在 .bss。由于该二进制是 large-code-model,任何编译器无法证明可用 32 位位移到达的全局对象,都会被放入带 l 标志的 large 层。对分发 PJRT 函数指针表的 dispatcher 来说,这意味着该表通过 64 位绝对加载到达;逆向工程师跟踪 GetPjrtApi 时必须映射 LOAD 4(delta 0x600000)才能找到后备字节。见 lifecycle/get-pjrt-api-thunk.md

未分配尾部(不在任何 segment 中)

[Nr] 名称类型偏移大小
[49] .symtabSYMTAB0x221ba8300x1c3cc50 (~28.2 MiB, 1,233,710 entries)
[50] .shstrtabSTRTAB0x23df74800x243
[51] .strtabSTRTAB0x23df76c30xab824de (~172 MiB)

这三个没有 SHF_ALLOC flag(address 0x0):它们存在于磁盘上供静态工具使用,从不被映射。它们合计约占文件 200 MiB — 磁盘大小超过四分之一是 loader 从不触碰的符号名称。


动态 Section

.dynamic array(section [34]PT_DYNAMIC)在 vaddr 0x22048b40 处为 33 entries / 0x210 bytes。它是 loader 执行以连接该库的完整指令集。readelf -d

text
 Tag           Type            Name/Value
 NEEDED         libm.so.6
 NEEDED         libpthread.so.0
 NEEDED         libdl.so.2
 NEEDED         librt.so.1
 NEEDED         libc.so.6
 NEEDED         ld-linux-x86-64.so.2
 RELA           0x9170          RELASZ  25660464 (bytes)   RELAENT 24
 RELACOUNT      1069006
 JMPREL         0x1881da0       PLTRELSZ 11352 (bytes)      PLTREL  RELA
 PLTGOT         0x224c2980
 SYMTAB         0x2c8           SYMENT  24
 STRTAB         0x5730          STRSZ   14908 (bytes)
 GNU_HASH       0x50b8
 PREINIT_ARRAY  0x22048b30      PREINIT_ARRAYSZ 16
 INIT_ARRAY     0x215f26f0      INIT_ARRAYSZ    23200
 FINI_ARRAY     0x215f8190      FINI_ARRAYSZ    16
 INIT           0xe635524
 FINI           0xe63553c
 VERSYM         0x4840
 VERDEF         0x4e0c          VERDEFNUM  2
 VERNEED        0x4e44          VERNEEDNUM 6
 NULL           0x0
```text

### loader 获得的指令

| 方面 | 值 | 含义 |
|---|---|---|
| `DT_NEEDED` × 6 | `libm`, `libpthread`, `libdl`, `librt`, `libc`, `ld-linux-x86-64` | 自包含:没有 C++ runtime、没有 CUDA、没有 driver `.so`。不需要 `libstdc++`/`libgcc_s` — C++ runtime 被静态链接进来。 |
| `DT_SONAME` | 不存在 | 该对象只通过路径命名自身;没有东西按 SONAME `dlopen` 它。 |
| `DT_RPATH`/`DT_RUNPATH` | 不存在 | 没有嵌入搜索路径。 |
| 符号哈希 | `DT_GNU_HASH` only (`0x50b8`); no legacy `DT_HASH` | Loader 必须支持 GNU hash;仅支持 DT_HASH 的 loader 无法在这里解析符号。 |
| `DT_INIT` / `DT_FINI` | `0xe635524` / `0xe63553c` | `.init`/`.fini` 中的 legacy single-function ctor/dtor stubs。 |
| `DT_INIT_ARRAY` / 大小 | `0x215f26f0` / 23,200 B (2,900 ptrs) | 真正的构造函数扇出 — 见 [static-init](static-init.md)。 |
| `DT_FINI_ARRAY` / 大小 | `0x215f8190` / 16 B (2 ptrs) | 只有两个析构函数。 |
| `DT_PREINIT_ARRAY` / 大小 | `0x22048b30` / 16 B (2 ptrs) | 在 `.init_array` *之前*运行 — 在库中少见。 |
| `DT_VERSYM`/`VERDEF`/`VERNEED` | `0x4840` / `0x4e0c` / `0x4e44` | 符号版本控制已启用:2 个 version definitions,6 个 version-need records。 |

> **QUIRK —** 对一个 745 MiB 插件来说,`DT_NEEDED` 只列出六个裸系统库。其他所有东西 — LLVM/MLIR 编译器、XLA、gRPC、protobuf、Abseil、Eigen、libstdc++ ABI 和 TPU driver — 都被静态归档进这一个文件。依赖面被有意压到最低,使插件可以落入任何 manylinux_2_31 host,而无需拖入传递 `.so` 图。期望以共享依赖形式找到这些组件的重实现者,会在 `.text` 中找到它们。
>
> **NOTE —** dynamic-string table `DT_STRTAB`/`STRSZ` 只有 14,908 bytes — 它只命名 741 个动态符号和 6 个库。它与 172 MiB 的 `.strtab` 无关;后者命名 1.2 M 个静态符号,加载时从不查询。

---

## 符号和重定位表面

相对于二进制体量,运行时可见的符号面很小;重定位面极大,因为一个充满绝对指针的 534 MiB PIE text segment 必须在加载时重新定址。

### 动态符号

| 指标 | 值 |
|---|---|
| `.dynsym` entries | 741(大小 `0x4578` / 24) |
| 其中 imports (`UND`) | 515 with `Ndx == UND`(index-0 `NULL` + imports)and 226 with a defined section index(exports) |
| 导出符号族 | `Tpu*` C API(`TpuExecutor`、`TpuCompiler`、`TpuProgram`、…),加上 `GetPjrtApi`、`TfTpu_Initialize`、Abseil internal entry points |
| 符号版本 | `GLIBC_2.x`(libc/libm/librt)、`VERS_1.0`(对象自身的 definition) |

导出面刻意很窄:host 加载该库并调用少数 C 入口点 — 主要是 `GetPjrtApi` 和 `TfTpu_*` bootstrap — 整个 compiler/runtime 都隐藏在它们后面作为 internal(`LOCAL`)符号。这些少数导出背后的多态分发由 [`forensics/polymorphic-entry-points.md`](polymorphic-entry-points.md) 和 [`lifecycle/get-pjrt-api-thunk.md`](../lifecycle/get-pjrt-api-thunk.md) 覆盖。

### 重定位

| 表 | 地址 | 条目 | 类型组合 |
|---|---|---|---|
| `.rela.dyn` | `0x9170` | 1,069,186(大小 `0x1878c30` / 24) | `R_X86_64_RELATIVE` 占主导(`DT_RELACOUNT` = 1,069,006 ≈ 99.98%) |
| `.rela.plt` | `0x1881da0` | 473(大小 `0x2c58` / 24,`PLTRELSZ` 11,352) | `R_X86_64_JUMP_SLOT`(PLT GOT entries) |

> **GOTCHA —** 在约 1.07 M 个动态重定位中,除约 180 个外全是 `R_X86_64_RELATIVE` — 纯粹的 load-bias 加法,没有符号引用。这是 534 MiB position-independent text segment 的代价:烘进 `.data.rel.ro`、vtables 和 jump tables 的每个绝对指针都必须重新定址。重定位表(`RELASZ` = 25,660,464 bytes ≈ 24.5 MiB)本身就比许多完整共享库更大。loader 在第一个构造函数运行前处理超过一百万个重定位;对这个插件而言,主导 `dlopen` 延迟的是这个,而不是 `.init_array`。

473 个 PLT slots 是对六个 `DT_NEEDED` 库的 lazy-bound 调用(`malloc`、`pthread_*`、`dlopen`、math 等)。`PLTGOT` = `0x224c2980`(section `[44] .got.plt`)位于 LOAD 3,处于 `GNU_RELRO` 之外,正是为了让 lazy resolver 能在运行时把已解析目标写入其中 — GOT 的其余部分(`[35] .got`)则被 RELRO 冻结。

> **Note:** 51 和 52 的 section-count 数字都一致 — `readelf -h` 报告 `Number of section headers: 52`,其中 51 统计有意义的 sections,52 包含强制的 `NULL` section `[0]`。本页使用 wire 上的计数 52。同样,`DT_SONAME` 确实不存在(`readelf -n`),但 build-id 存在且等于 `89edbbe81c5b328a958fe628a9f2207d`;每个 wiki 页面顶部的 version-pin 都依赖这个 build-id。

---

## 交叉引用

- [取证概览](overview.md) — 该二进制的并行高层导览;本页是其字节级基底
- [双二进制拆分](two-binary-split.md) — 为什么 wheel 同时发布 `libtpu.so` 和较小的 `sdk.so`,以及它们的 ELF 形态如何不同
- [静态初始化](static-init.md) — 负责 `.init_array` 构造函数名单和顺序;本页只固定该 array 的位置/大小
- [自定义 Sections](custom-sections.md) — 深入说明上面 section table 中列出的 `google_*`、`__rseq_cs`、`linkarr_upb_AllExts`、`filewrapper_toc`、`pb_defaults`
- [ELF 入口和 Init 过程](../lifecycle/elf-entry-and-init-proc.md) — 一个 entry 为 `0x0` 的库如何通过 `DT_PREINIT_ARRAY`/`DT_INIT`/`DT_INIT_ARRAY` 到达首次执行
- [GetPjrtApi 跳板](../lifecycle/get-pjrt-api-thunk.md) — 其后备单例位于本文记录的 `.lbss` large-model 区域中的导出入口
- [生命周期概览](../lifecycle/overview.md) — 消费本页 dynamic section 的加载时序列