Contents

LinuxSys:动态链接

本文采用知识共享署名 4.0 国际许可协议进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,可适当缩放并在引用处附上图片所在的文章链接。

[TOC]

PLT(Procedure Linkage Table 过程链接表)及GOT(Global Offset Table 全局偏移表)

GOT(Global Offset Table)

功能:

GOT 是一个数据表,存储了所有外部符号(包括函数和全局变量)在运行时的实际地址。

特点:

  • 每个动态链接的 ELF 可执行文件或共享库都有自己的 GOT;
  • GOT 在程序加载时由动态链接器(如 ld-linux.so)填充;
  • GOT 的内容在运行时可写(早期版本),现代系统通常将其分为 .got(只读,如 TLS 相关)和 .got.plt(可写,用于函数地址);
  • 对于函数调用,GOT 中保存的是该函数的跳转目标地址

PLT(Procedure Linkage Table)

功能:

PLT 是一段代码片段,用于实现对外部函数的间接跳转。它充当“跳板”,首次调用时触发动态链接器解析函数地址,后续调用直接跳转。

工作流程(以调用 printf@plt 为例):

  1. 第一次调用 printf
    • 程序跳转到 printf@plt(PLT 中的一小段代码);
    • PLT 条目跳转到 GOT 中对应 printf 的条目(初始时指向 PLT 的下一条指令);
    • 触发一个“桩代码”(stub),压入重定位标识,跳转到 PLT[0]
    • PLT[0] 调用动态链接器(通过 _dl_runtime_resolve);
    • 动态链接器解析 printf 的真实地址,并写入 GOT 表;
    • 跳转到 printf 执行。
  2. 后续调用
    • PLT 再次跳转到 GOT;
    • 此时 GOT 中已存有 printf 的真实地址;
    • 直接跳转执行,无需再次解析。

这种机制称为 Lazy Binding(延迟绑定),即“首次使用时才解析”,可加快程序启动速度。

关键:cp 覆盖写 vs rm + cp

情况1:直接 cp new.so old.so(危险!)

  • cp 的行为是:打开目标文件(O_WRONLY | O_TRUNC),然后写入新内容
  • 如果 old.so 正被某个进程 mmap(即使只是只读映射),文件 inode 不变,但其数据块内容被覆盖。
  • 由于 mmap 是基于 inode 和 block 的,内核会认为“文件内容已变”
  • 对于共享只读映射(如代码段),Linux 会:
    • 在下次访问该内存页时,触发 page fault
    • 内核发现底层文件 block 已更新,于是从磁盘重新加载新内容到该物理页
    • 所有映射该页的进程(包括正在运行的程序)都会看到新内容!

✅ 这就是“磁盘内容改变 → 进程内存同步更新”的根本原因。

⚠️ 注意:这不是“实时推送”,而是“按需更新”(lazy update on page fault),但效果等价于内存被修改。

情况2:先 rm old.so,再 cp new.so old.so(安全!)

  • rm old.so:只是删除目录项(dentry)和减少 inode 引用计数。
  • 只要还有进程 mmap 或 open 该文件,inode 和数据块不会被释放,原进程仍可正常访问。
  • cp new.so old.so:创建一个全新的 inode,与原文件无关。
  • 原进程继续使用旧 inode 对应的内存映射,完全不受影响。
  • 新启动的进程才会加载新 inode 的 .so 文件。

为什么 GOT 表会被破坏?

动态库刚加载时,其 GOT(Global Offset Table)中的函数指针是占位符(如指向 PLT stub)。动态链接器会在加载后修改这些内存位置,填入真实函数地址(如 printf 的实际地址)。

此时:

  • 内存中的 .so 内容 ≠ 磁盘上的 .so 文件(GOT 已被重写)。
  • 当你用 cp 覆盖磁盘文件,后续 page fault 会把原始未重定位的 GOT 数据重新载入内存。
  • 导致 GOT 中存的是无效地址(如 0x516),程序跳转时 crash。