libtpu.so / sdk.so 双二进制拆分
本页中的所有事实均适用于
libtpu0.0.40 wheel,标记为cp314-cp314-manylinux_2_31_x86_64。该构建由 build-id 固定,而不是由任何内部版本字符串固定:libtpu.so build-id89edbbe81c5b328a958fe628a9f2207d;sdk.so build-id4e9025466f71009fccb46a803806411c63744a0a。其他 wheel 构建会有所不同。
摘要
libtpu wheel 在同一目录中并排发布两个 ELF 共享对象:libtpu.so(745 MiB,即 PJRT/XLA-TPU 插件)和 sdk.so(21 MiB,即它较小的同族对象)。自然的假设是它们是同一系统的两个半边,由 sdk.so 提供 libtpu.so 在启动时加载的运行时层。这个假设是错误的,本页首先纠正它。 这两个对象是独立链接的,不共享符号,并且没有任何一方在 DT_NEEDED 中命名另一方。sdk.so 是一个自包含的 CPython 扩展模块:它导出 PyInit_sdk 并导入 Python C-API,因此只能由 Python 解释器中的 import libtpu.sdk 到达。libtpu.so 是一个原生 PJRT 插件,只能由宿主运行时(JAX、XLA TPU client)通过 dlopen 到达;它导入零个 Python 符号。它们共处同一个 wheel 是为了打包便利,而不是因为存在任何链接期或加载期依赖。
对重新实现者真正重要的对比,是符号群体形态,而不是依赖边。libtpu.so 是一个近乎封闭的对象:完整 .symtab 中有 918,698 个 FUNC 符号,但其 741 项 .dynsym 中只有 226 个已定义导出(218 个带 @@VERS_1.0 的版本化 C-ABI thunk,加上 8 个 __start_*/__stop_* linker-set 边界)。它静态嵌入了完整的 C++ 世界,不链接 libstdc++。sdk.so 正好相反:78,311 个 FUNC 符号,但有 36,787 个动态符号,完全没有符号版本化,并且像普通扩展一样确实链接 libstdc++/libgcc_s。一个对象把所有东西隐藏在 226 项稳定 ABI 后面;另一个是正常的、开放链接的 Python 模块。wheel 中的“拆分”是同一供应商代码的两种交付载体之间的拆分,而不是 host/device 或 compiler/runtime 的分解。
本页建立这两个并排 ELF 的事实,从动态表证明无共享符号 / 无 DT_NEEDED 关系,识别 libtpu.so 的 226 项 C-ABI 表面(包括 GetPjrtApi 和 GetLibtpuSdkApi),解释 sdk.so 的 Python 模块性质,并记录如何让这个更大对象的地址空间可用于分析。二者是恰好共享一个目录的无关链接单元,这是打包事实,不是加载期依赖。
对重新实现来说,契约是:
- 打包契约: wheel 实际包含什么,Python loader 解析哪个文件,以及 C runtime
dlopen哪个文件。 - 链接关系: 这两个对象是独立的 — 没有
DT_NEEDED边,没有符号重导出,动态符号角色不相交(PJRT C-ABI 对 CPython 模块)。 libtpu.so的导出表面模型: 一个 226 符号 C ABI(218 个@@VERS_1.0thunk 加 8 个 linker-set 边界),由静态链接 C++ 核心之上的薄 thunk 组成,其中GetPjrtApi和GetLibtpuSdkApi是两个根。- 分析导航模型: 为什么分析一个 745 MiB、884,832 个函数的对象,要按地址窗口和段审计,而不是按符号名审计。
| Wheel | libtpu-0.0.40-cp314-cp314-manylinux_2_31_x86_64 |
| 对象 | libtpu/libtpu.so (745 MiB), libtpu/sdk.so (21 MiB) |
| Python loader 目标 | 仅 libtpu.so — __init__.py::get_library_path() |
| PJRT 入口 | GetPjrtApi@@VERS_1.0 — libtpu.so 0x0e6a83a0(5 字节 thunk) |
| SDK C-ABI 入口 | GetLibtpuSdkApi@@VERS_1.0 — libtpu.so 0x109028c0(5 字节 thunk) |
| sdk.so 入口 | PyInit_sdk — sdk.so 0x00739c02(16 字节) |
| 对象间链接 | 无 — 没有 DT_NEEDED,没有共享/重导出符号 |
一览:两个对象并排对比
每一行都直接从两个 ELF 文件读取;版本固定项给出 build-id。
| 属性 | libtpu.so | sdk.so |
|---|---|---|
| 磁盘大小 | 781,691,048 B (745 MiB) | 22,541,240 B (21 MiB) |
| wheel 中的文件模式 | 0755(设置了可执行位) | 0644 |
| ELF 类型 | DYN(共享对象),x86-64 | DYN(共享对象),x86-64 |
file ELF 风味 | version 1 (SYSV) | version 1 (GNU/Linux) |
| Build-id 格式 | md5/uuid, 89edbbe8…2207d | sha1, 4e902546…44a0a |
| Gold-version note | 缺失 | 存在(.note.gnu.gold-version) |
DT_SONAME | 无 | 无 |
DT_NEEDED 数量 | 6 | 6 |
DT_NEEDED 列表 | libm libpthread libdl librt libc ld-linux | libpthread libm libstdc++ libgcc_s libc ld-linux |
链接 libstdc++? | 否(静态嵌入) | 是 |
| 符号版本化 | VERS_1.0 (@@VERS_1.0) | 无 |
.dynsym 条目 | 741 | 36,787 |
| 已定义全局导出 | 226(218 个 @@VERS_1.0 FUNC + 8 个 linker-set 边界) | ~36,600(模块符号) |
.symtab FUNC 数量 | 918,698 | 78,311 |
| IDA 函数数量 | 884,832(记录)/ 884,843(制品覆盖) | 94,732 |
DT_FLAGS | 缺失 | BIND_NOW (FLAGS_1: NOW) |
RELACOUNT(RELATIVE 重定位) | 1,069,006 | 618 |
| 加载方式 | 宿主运行时通过 dlopen | CPython 通过 import |
| 模块入口 | GetPjrtApi, GetLibtpuSdkApi, … | PyInit_sdk |
注意:
libtpu.so和sdk.so是独立链接单元,不是同一系统的两个半边:没有一方在DT_NEEDED中列出另一方,没有一方定义另一方导入的符号,并且它们的动态符号角色类别完全不同(226 项 PJRT C-ABI 对导出PyInit_sdk的 CPython 扩展)。任何合并的“函数总数”(例如 ~979k)都是两个无关数据库的和,而不是一个二进制的大小。应把它们视为恰好共享一个目录的两个二进制。陷阱 — wheel 被描述为“stripped 745 MB plugin”,但按
file(1),这两个对象都不是stripped。它们完整的.symtab保留了下来(libtpu.so中有 918,698 个FUNC符号),这就是为什么 IDA 恢复出约 884k 个具名函数,而不是一片sub_空白。“stripped”的直觉在这里失效;分析深度由保留下来的.symtab决定,而不是由.dynsym决定。
libtpu.so — PJRT 插件
目的
libtpu.so 是原生 Google-TPU PJRT/XLA 插件:宿主 ML 运行时会 dlopen 该对象,以获得指向 TPU 编译器、运行时和设备群的 C-ABI 句柄。它是 libtpu Python 包解析并公布的文件。包的 __init__.py 只做一件功能性事情:把一个环境变量指向这个文件:
get_library_path() -> <pkg>/libtpu.so
configure_library_path():
if not os.environ.get('TPU_LIBRARY_PATH'):
os.environ['TPU_LIBRARY_PATH'] = get_library_path()
```text
`sdk.so` 从未在 `__init__.py` 中被命名。Python loader 中没有任何东西触碰它。因此,读取 `TPU_LIBRARY_PATH` 并 `dlopen` 结果的宿主运行时只会加载 **`libtpu.so`**。
### 导出的 C ABI
`libtpu.so` 暴露一个有意缩小的表面:在其 741 项 `.dynsym` 中只有 **226 个已定义全局符号**(741 − 515 个 `UND`/null = 226 个已定义)。其中 218 个是 `FUNC` 导出,每一个都携带来自该对象单个 `VERS_1.0` version definition(`VERDEFNUM 2`,即 base 加 `VERS_1.0`)的 `@@VERS_1.0` 版本标签;其余 8 个是 `NOTYPE` `__start_*`/`__stop_*` linker-set 边界。相对于完整 `.symtab` 中的 918,698 个 `FUNC` 符号,导出比例低于 0.025% — 该对象是一个密封的 C++ 单体,只提供一扇手工挑选的 C 门。
导出聚集成若干 C-ABI 家族,每个成员都是 `*_DoWork` / `Tpu*_*` / `TfTpu_*` 风格的入口:
| ABI 家族(前缀) | 角色 |
|---|---|
| `GetPjrtApi` | PJRT C-API vtable 根 — 宿主运行时的主入口 |
| `GetLibtpuSdkApi` | libtpu “SDK” C-API vtable 根(第二个独立 ABI 表面) |
| `TpuCompiler_*` | XLA-for-TPU 编译器:`New`、`Compile`、`RunHloPasses`、`RunBackend`、`ShapeSize`、`Free` |
| `TpuCompile_*` | 编译缓存 + 程序构建:`CompileAndBuild`、`CreateCompilationCacheKey`、fingerprints |
| `TpuConfigurationApi_*` | Host/pod 配置:server address、memory limit、TPUs-per-host |
| `TpuComputationPlacer_*` | 设备分配:`New`、`AssignDevices`、`AssignLocalDevices` |
| `TpuCoreLocation_*`, `TpuDeviceDescription_*` | 拓扑 / 设备描述访问器 |
| `TfTpu_*`, `TfTpuOrdinalSelector_*` | 运行时引导 + core-ordinal 选择 |
| `HardwareLayout_*`, `SparseCore_*` | 布局数学和 SparseCore 查询 |
| `TF_InitKernel`, `TFNPD_InitPlugin` | TensorFlow kernel / next-pluggable-device 初始化 |
| `__start_*` / `__stop_*` | Linker-set 边界(`google_malloc`、`malloc_hook`、`pb_defaults`、`linkarr_upb_AllExts`) |
> **怪癖 —** `GetPjrtApi@@VERS_1.0` 是 `0x0e6a83a0` 处的一个 **5 字节** `FUNC`,`GetLibtpuSdkApi@@VERS_1.0` 同样是 `0x109028c0` 处的 5 字节函数。五字节就是一条 `jmp rel32` — 这些是 tail-call thunk,不是实现。导出的名称是进入静态链接内部函数的稳定 trampoline,该内部函数自己的符号是内部的。重新实现者不应在导出地址*处*寻找真正的 PJRT-API 构造器;要跟随这条跳转。thunk 间接层使 745 MiB 内部能够在不同构建间变化,同时 226 项入口保持二进制稳定。(`GetPjrtApi` thunk 及其返回的对象在独立页面中详述。)
>
> **说明 —** `libtpu.so` 携带 `RELACOUNT 1069006`,也就是超过一百万个 `R_X86_64_RELATIVE` 重定位;还有一个 23,200 字节的 `INIT_ARRAY`(2,900 个 init 指针)加一个 16 字节的 `PREINIT_ARRAY`。大量 relative-reloc 和大型 constructor array,是把完整 C++ runtime 和 protobuf/Abseil 机制静态链接进一个 PIE 对象的加载期成本。动态链接 `libstdc++` 的 `sdk.so` 只需要 618 个 relative relocations。
### 为什么 DT_NEEDED 中没有 libstdc++
`libtpu.so` 的 `DT_NEEDED` 是 `libm, libpthread, libdl, libc, librt, ld-linux`,即纯 C runtime,**没有 `libstdc++`,没有 `libgcc_s`**。该对象显然是 C++(导出家族是 C shim 后面的 C++ 子系统),因此 C++ 标准库是**静态嵌入**的。这是可再分发插件的标准 hermetic-build 姿态:只依赖 `manylinux_2_31` 标签保证的 glibc baseline,其余全部随对象携带。由此产生的静态链接库清单另有目录记录(参见 Embedded-Library Atlas)。
---
## sdk.so — CPython 扩展模块
### 目的
`sdk.so` **不是** `libtpu.so` 下方的运行时层。它的动态表证明它是一个普通的 CPython 扩展模块:
- 它导出 **`PyInit_sdk`**(`0x00739c02` 处的 16 字节 `FUNC`),即 CPython 模块初始化入口。按照 import 协议,`import libtpu.sdk` 会使 CPython `dlopen` 该文件并调用 `PyInit_sdk`。
- 它的未定义导入主要由 **Python C-API** 构成:`PyObject_GenericGetAttr`、`PyType_GenericNew`、`PyCapsule_*`、`PyExc_ValueError`、`PyInterpreterState_Get`、`PyModule_NewObject`,以及约 170 个其他非 glibc `UND` 符号,基本全是 `Py*`。这些符号由正在运行的 CPython 解释器满足,而不是由任何同目录 `.so` 满足。
- 它动态链接 `libstdc++.so.6` 和 `libgcc_s.so.1`,这是构建进已提供 C++ runtime 的 Python 环境中的扩展所采用的常见姿态。
### 独立性证明
这两个对象之间的关系是**没有关系**,并且可以从二进制中以三种方式证明:
```text
1. DT_NEEDED edges:
libtpu.so NEEDED: { libm, libpthread, libdl, librt, libc, ld-linux }
sdk.so NEEDED: { libpthread, libm, libstdc++, libgcc_s, libc, ld-linux }
-> "libtpu.so" appears in neither list. "sdk.so" appears in neither list.
No load-time edge in either direction.
2. Symbol re-export / import:
sdk.so UND set has ZERO Tpu*/Pjrt*/GetLibtpu*/HardwareLayout*/SparseCore* names.
-> sdk.so does not consume libtpu.so's 226-entry ABI.
libtpu.so UND set has ZERO Py* symbols.
-> libtpu.so does not consume CPython, so it is not loaded as a Python module.
3. Entry-point category:
libtpu.so module root: GetPjrtApi / GetLibtpuSdkApi (versioned C-ABI thunks)
sdk.so module root: PyInit_sdk (CPython module init)
-> Categorically different load mechanisms (dlopen-by-runtime vs import-by-CPython).陷阱 — 共享目录以及几乎相同的 6 项
DT_NEEDED列表,会让这两个对象看起来像一对。它们在链接层面不是一对。sdk.so是libtpu.sdkPython 子模块的编译形态;libtpu.so是 C 插件。把sdk.so接成libtpu.so的依赖(或反过来)的重新实现,是在建模 ELF 中不存在的边。它们唯一共享的是供应商、wheel 和 glibc baseline。
符号群体形态
sdk.so 的形态与 libtpu.so 相反。它有 36,787 个动态符号,约为 libtpu.so 741 个的 50 倍,因为 Python 扩展会在 .dynsym 中广泛暴露它绑定的 C++ 类和 protobuf 消息类型,而不是把它们隐藏在版本化 C 门后面。它没有符号版本化(对其 .dynsym 执行 grep VERS_1.0 返回 0);@@VERS_1.0 纪律仅属于 libtpu.so。它的 STRSZ 是 4,544,547 字节(约 4.3 MiB)的动态字符串表,即宽导出表面的 demangled-symbol 成本。BIND_NOW / FLAGS_1: NOW 请求 eager binding,这对一个希望在缺少 Py* 符号时于导入阶段快速失败的扩展来说很正常。
说明 —
sdk.so向 IDA 报告94,732个函数,但.symtab中只有78,311个FUNC条目;差异来自 IDA 恢复出的无存留符号函数(thunk、outlined cold path、编译器生成的 helper)。其数据库中的45,156个字符串和164个 switch 比libtpu.so(1,249,324个字符串、33,016个 switch)低一个数量级,符合 21 MiB 模块与 745 MiB 单体之间的差异。
两个对象周围的 Wheel 打包
清单
该 wheel 是单个发行物 — libtpu 0.0.40,一个 wheel tag,import-root 为 libtpu — 在 libtpu/ 包目录下和 dist-info 中总共只有六个载荷文件。两个 .so 文件占据主体;其他都是小文本。
| 文件 | 大小 | 种类 | 角色 |
|---|---|---|---|
libtpu/libtpu.so | 781,691,048 B | ELF DYN | PJRT 插件(被加载的对象) |
libtpu/sdk.so | 22,541,240 B | ELF DYN | CPython 扩展(libtpu.sdk) |
libtpu/__init__.py | 1,131 B | Python | 设置 TPU_LIBRARY_PATH → libtpu.so |
libtpu/LICENSE | 229 B | text | Google Cloud Platform 条款 |
libtpu/THIRD_PARTY_NOTICES.txt | 731,537 B | text | libtpu.so 的 OSS notices |
libtpu/SDK_THIRD_PARTY_NOTICES.txt | 103,306 B | text | sdk.so 的 OSS notices |
说明 — 两个独立的 third-party-notices 文件(插件使用
THIRD_PARTY_NOTICES.txt,SDK 使用SDK_THIRD_PARTY_NOTICES.txt)是独立性结论的额外佐证:这两个对象有不同的 OSS dependency closure,并作为不同 deliverable 授权/审计。如果sdk.so只是libtpu.so的一个切片,一个 notices 文件就足够了。
Wheel metadata
| 字段 | 值 |
|---|---|
| Distribution | libtpu 0.0.40 |
| Wheel tag | cp314-cp314-manylinux_2_31_x86_64 |
Requires-Python | >=3.11 |
Requires-Dist | (none) |
| Entry points | (none) |
purelib | false(platform wheel) |
| Summary | "Google Cloud TPU runtime library." |
该 wheel 声明没有 Requires-Dist,即不会拉取任何 Python 包。它唯一的 Python 文件是 1.1 KiB 的 __init__.py;所有功能都位于两个原生对象中。cp314 ABI tag 将其固定到 CPython 3.14,但注意 libtpu.so 本身不导入 Python 符号;ABI tag 由 sdk.so 决定(PyInit_sdk 面向 CPython 3.14 C-ABI),不是由插件决定。这就是为什么该 platform wheel 是 cp314 专用的,即便其主载荷是与解释器无关的原生代码。
怪癖 — 因为
libtpu.so不携带Py*依赖,并且完全通过TPU_LIBRARY_PATH+dlopen到达,所以该插件在任何 CPython(或非 Python host)下都会以相同方式运行;它的cp314标签是把受解释器约束的sdk.so打包进同一个 wheel 的副产物。只提取 PJRT 插件的重新实现者可以完全忽略 CPython 版本固定;提取sdk.so的则不能。
745 MiB 对象的分析导航
为什么按地址窗口,而不是按符号名
一个有 884,832 个 IDA 函数的 745 MiB 对象(function-record count;artifact coverage 覆盖 884,843 项 — 参见 Overview)无法一次性反编译;IDA 上限和反编译器限制迫使分析采用窗口化策略。该对象通过两种方式变得可导航:
- 按段。 可加载映射由一个巨大的
.text(0x0e63c000–0x21217484,约 300 MiB 代码)加大型只读常量池主导:.lrodata(108 MiB)、.rodata(58 MiB)和 28 MiB.eh_frame。可写数据相对很小(.data.rel.ro约 10 MiB、.data2.4 MiB、.bss853 KiB)。段映射是一阶坐标系:某个地址上的发现,首先按拥有它的段来放置。(完整 ELF 节映射归 ELF-Anatomy 页面所有。) - 按函数窗口。 制品空间(884,843 个 manifest 条目;884,832 条已登记 function record)按 offset/limit 分块反编译,每块约 10,000 个函数(
off=… lim=10000),约 97 个窗口覆盖 offsets 56,102 → 876,102。每个窗口记录自己的 decompiled/ctree 数量和 failure tally;整个对象上的聚合反编译失败总数为 511(约 0.06%),analysis problems 峰值为 7,915。这就是如何确定性审计一个过大、无法用单个数据库承载的目标:每个函数都位于已知且可复现的窗口中。
libtpu.so loadable-segment skeleton (selected, by size):
.text 0x0e63c000 .. 0x21217484 ~300 MiB code (perm r-x)
.lrodata 0x01884a00 .. 0x084931d0 ~108 MiB large const (perm r--)
.rodata 0x084a0000 .. 0x0be8af28 ~58 MiB const (perm r--)
.eh_frame 0x0c989cb8 .. 0x0e635524 ~28 MiB unwind (perm r--)
.data.rel.ro 0x215f81a0 .. 0x22048b30 ~10 MiB reloc data(perm rw-)
.data 0x222551c0 .. 0x224bf798 ~2.4 MiB data (perm rw-)
.bss 0x224c3880 .. 0x22598c30 ~853 KiB zero-init (perm rw-)
[init thunks] .text.startup / .text.unlikely / google_malloc / malloc_hook
```text
> **说明 —** `sdk.so` 的数据库足够小,不需要窗口化;它的 `.text` 是 `0x7394a0`–`0xb6b7a2`(约 4.4 MiB),位于单个 6.6 MiB `LOAD` 中,并且 IDA 基本一次性反编译了它的 94,732 个函数。窗口化策略仅是 `libtpu.so` 的需要,源于其大三个数量级的代码段。
>
> **陷阱 —** 不要把 IDA 的“total function”数字(`libtpu.so` 884,832 条记录 / 884,843 个 artifact entries;跨对象合计 `~979,575`)理解为不同源函数的计数。它们包含 thunk、模板实例化、outlined cold block,以及选择元数据中的跨窗口重叠。这些数字适合作为*相对规模*(一个对象约为另一个的 9 倍)和*导航索引*,而不是源函数普查。
---
## 相关组件
| 组件 | 关系 |
|---|---|
| `GetPjrtApi` thunk | `0x0e6a83a0` 处的 5 字节导出,返回 PJRT C-API vtable;进入 `libtpu.so` 的主要 `dlopen` 入口 |
| `GetLibtpuSdkApi` thunk | `0x109028c0` 处的第二个 5 字节导出;`libtpu.so` 内部的独立 “SDK” C-ABI 根(名称相似,但与 `sdk.so` 文件无关) |
| `PyInit_sdk` | `sdk.so` 的 CPython module-init 入口;由 `import libtpu.sdk` 到达,从不由 `libtpu.so` 到达 |
| Embedded C++ runtime | `libtpu.so` 内静态链接的 `libstdc++` + Abseil/protobuf,解释了其缺失的 `libstdc++` `DT_NEEDED` 边 |
> **说明 —** 导出 `GetLibtpuSdkApi`(`libtpu.so` *内部*的函数)和文件 `sdk.so` 之间的命名碰撞是一个陷阱。二者无关:`GetLibtpuSdkApi` 是插件提供的 C-ABI vtable 根;`sdk.so` 是不导入它的 Python 模块。“SDK” 在两个上下文中表示两件不同的事。
## 交叉引用
- [Overview](overview.md) — forensics 入口点;将这个双对象拆分置于二进制解剖结构中
- [ELF Anatomy](elf-anatomy.md) — 拥有此处总结的 `libtpu.so` 完整 section/segment map
- [Embedded-Library Atlas](embedded-library-atlas.md) — 静态链接的 C++/protobuf/Abseil 清单,解释 `libtpu.so` 缺失的 `libstdc++` `DT_NEEDED`
- [GetPjrtApi Thunk & tpu_plugin Object](../lifecycle/get-pjrt-api-thunk.md) — 跟随 5 字节 `GetPjrtApi` thunk 进入 PJRT-API 构造器
- [Module-Init & Plugin Discovery](../lifecycle/module-init-plugin-discovery.md) — 宿主运行时如何解析 `TPU_LIBRARY_PATH` 并 `dlopen` `libtpu.so`