Skip to content

libtpu.so / sdk.so 双二进制拆分

本页中的所有事实均适用于 libtpu 0.0.40 wheel,标记为 cp314-cp314-manylinux_2_31_x86_64。该构建由 build-id 固定,而不是由任何内部版本字符串固定:libtpu.so build-id 89edbbe81c5b328a958fe628a9f2207d;sdk.so build-id 4e9025466f71009fccb46a803806411c63744a0a。其他 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 表面(包括 GetPjrtApiGetLibtpuSdkApi),解释 sdk.so 的 Python 模块性质,并记录如何让这个更大对象的地址空间可用于分析。二者是恰好共享一个目录的无关链接单元,这是打包事实,不是加载期依赖。

对重新实现来说,契约是:

  • 打包契约: wheel 实际包含什么,Python loader 解析哪个文件,以及 C runtime dlopen 哪个文件。
  • 链接关系: 这两个对象是独立的 — 没有 DT_NEEDED 边,没有符号重导出,动态符号角色不相交(PJRT C-ABI 对 CPython 模块)。
  • libtpu.so 的导出表面模型: 一个 226 符号 C ABI(218 个 @@VERS_1.0 thunk 加 8 个 linker-set 边界),由静态链接 C++ 核心之上的薄 thunk 组成,其中 GetPjrtApiGetLibtpuSdkApi 是两个根。
  • 分析导航模型: 为什么分析一个 745 MiB、884,832 个函数的对象,要按地址窗口和段审计,而不是按符号名审计。
Wheellibtpu-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.0libtpu.so 0x0e6a83a0(5 字节 thunk)
SDK C-ABI 入口GetLibtpuSdkApi@@VERS_1.0libtpu.so 0x109028c0(5 字节 thunk)
sdk.so 入口PyInit_sdksdk.so 0x00739c02(16 字节)
对象间链接 — 没有 DT_NEEDED,没有共享/重导出符号

一览:两个对象并排对比

每一行都直接从两个 ELF 文件读取;版本固定项给出 build-id。

属性libtpu.sosdk.so
磁盘大小781,691,048 B (745 MiB)22,541,240 B (21 MiB)
wheel 中的文件模式0755(设置了可执行位)0644
ELF 类型DYN(共享对象),x86-64DYN(共享对象),x86-64
file ELF 风味version 1 (SYSV)version 1 (GNU/Linux)
Build-id 格式md5/uuid, 89edbbe8…2207dsha1, 4e902546…44a0a
Gold-version note缺失存在(.note.gnu.gold-version
DT_SONAME
DT_NEEDED 数量66
DT_NEEDED 列表libm libpthread libdl librt libc ld-linuxlibpthread libm libstdc++ libgcc_s libc ld-linux
链接 libstdc++(静态嵌入)
符号版本化VERS_1.0 (@@VERS_1.0)
.dynsym 条目74136,787
已定义全局导出226(218 个 @@VERS_1.0 FUNC + 8 个 linker-set 边界)~36,600(模块符号)
.symtab FUNC 数量918,69878,311
IDA 函数数量884,832(记录)/ 884,843(制品覆盖)94,732
DT_FLAGS缺失BIND_NOW (FLAGS_1: NOW)
RELACOUNT(RELATIVE 重定位)1,069,006618
加载方式宿主运行时通过 dlopenCPython 通过 import
模块入口GetPjrtApi, GetLibtpuSdkApi, …PyInit_sdk

注意: libtpu.sosdk.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 只做一件功能性事情:把一个环境变量指向这个文件:

text
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.solibtpu.sdk Python 子模块的编译形态;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,311FUNC 条目;差异来自 IDA 恢复出的无存留符号函数(thunk、outlined cold path、编译器生成的 helper)。其数据库中的 45,156 个字符串和 164 个 switch 比 libtpu.so1,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.so781,691,048 BELF DYNPJRT 插件(被加载的对象)
libtpu/sdk.so22,541,240 BELF DYNCPython 扩展(libtpu.sdk
libtpu/__init__.py1,131 BPython设置 TPU_LIBRARY_PATHlibtpu.so
libtpu/LICENSE229 BtextGoogle Cloud Platform 条款
libtpu/THIRD_PARTY_NOTICES.txt731,537 Btextlibtpu.so 的 OSS notices
libtpu/SDK_THIRD_PARTY_NOTICES.txt103,306 Btextsdk.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

字段
Distributionlibtpu 0.0.40
Wheel tagcp314-cp314-manylinux_2_31_x86_64
Requires-Python>=3.11
Requires-Dist(none)
Entry points(none)
purelibfalse(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 上限和反编译器限制迫使分析采用窗口化策略。该对象通过两种方式变得可导航:

  • 按段。 可加载映射由一个巨大的 .text0x0e63c0000x21217484,约 300 MiB 代码)加大型只读常量池主导:.lrodata(108 MiB)、.rodata(58 MiB)和 28 MiB .eh_frame。可写数据相对很小(.data.rel.ro 约 10 MiB、.data 2.4 MiB、.bss 853 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。这就是如何确定性审计一个过大、无法用单个数据库承载的目标:每个函数都位于已知且可复现的窗口中。
text
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`