尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

南京大学 操作系统 (JYY) 学习笔记:动态链接的黑魔法与内存入侵 (Dynamic Linking)

南京大学 操作系统 (JYY) 学习笔记:动态链接的黑魔法与内存入侵 (Dynamic Linking)
📅 发布时间:2026/7/29 20:06:21

写在前面:这是本系列的第十一篇。

在前几讲中,我们已经知道进程从execve的初始状态开始,可以通过mmap改变地址空间,通过fork创建新进程。只要有 ELF 可执行文件,把它的PT_LOAD数据段正确映射到内存里,程序就能跑起来(就像我们的 Funny Little Executable 实验)。

但本讲要面对一个更庞大的工程问题:当开发者希望把库函数(如 libc)和应用程序“分离”开,但又希望在运行时能随时调用库函数,该怎么办?

课前补课:静态链接 vs 动态链接

  • 静态链接库 (.a / .lib):
    就像是一个提前打包好的工具箱。当你写程序时,编译器会把你需要的加法、减法等功能代码,**直接死死地“拷贝”**到你的最终可执行文件(ELF)里。程序变大了,但它完全独立,不需要依赖外界。
  • 动态链接库 (.so / .dll):
    就好比是一个放在外面广场上的“共享工具箱”。程序里只留一张欠条(符号表)。当程序真正运行起来时,操作系统再去找到这个共享库,在运行时把它们拼在一起。就像租用工具,随用随借。

动态链接:机制与为什么需要它?

我们熟悉的 Windows 下的.dll,或者 Linux 下的.so(Shared Object),都是动态链接的产物。

为什么要“拆解”应用程序?

早期的时候,游戏如果需要用到d3dx9_xxx.dll里的功能,就会把整个库静态链接进游戏本体。结果每个游戏都复制了一份庞大的图形库,硬盘和内存瞬间爆炸。

1. 实现运行库和应用代码分离 (应用间共享)

  • 每个 C 程序都需要glibc。
  • 如果使用动态链接,整个操作系统内存里只需要一个 libc 的物理副本!成百上千个进程同时共享它。
  • 运行库和应用程序可以独立升级,互不干扰。

2. 大型项目的工程分解

  • 比如庞大的 Android 源码,改一行代码如果需要重新链接生成 2GB 的大文件,程序员会疯掉的。
  • 拆分成libjvm.so,libart.so,每次只编译更新那个.so文件即可。

可以用ldd和file命令来观察:

root@LAPTOP-GT06V0GS:~# ldd /bin/lslinux-vdso.so.1(0x00007ffe950d8000)libc.so.6=>/lib/x86_64-linux-gnu/libc.so.6(0x00007f829a846000)... root@LAPTOP-GT06V0GS:~# file /lib/x86_64-linux-gnu/libselinux.so.1/lib/x86_64-linux-gnu/libselinux.so.1: ELF64-bit LSB shared object, dynamically linked...

动态链接的阴暗面:供应链攻击与依赖地狱

任何技术都有代价。库的依赖本质上也是一种代码克隆(甚至把风险也克隆了过来)。

  • xz-utils (liblzma) 投毒事件 (CVE-2024-3094):
    震惊全球的安全事件!黑客 JiaT75 潜伏开源社区长达三年,获取信任成为维护者后,巧妙绕开 fuzz 测试,向基础压缩库投毒。导致全球依赖这个.so的 Linux 系统的 SSHD 均面临后门风险。
  • 如果 Linux 全是静态链接的呢?
    如果不用动态链接,一旦libc出了安全漏洞,你需要把全系统几万个应用程序全部重新编译链接一次!这是不可能完成的任务。

Dependency Hell (依赖地狱):
A 依赖 B 的 v1 版本,C 依赖 B 的 v2 版本,而你的项目同时依赖 A 和 C。当这种依赖嵌套成网状时,版本冲突和不兼容更新就会让人彻底崩溃。

感谢动态链接库,虽然有坑,但如果没有它,现代计算机世界早就崩塌了。


揭秘底层引擎:mmap 和虚拟内存

一个极其狂野的实验

  • 我们构造一个包含 100MB 无用代码 (nop指令) 的巨型共享库libbloat.so。
  • 然后同时启动1000个动态链接了这个库的进程!
  • 问题:系统的物理内存会被消耗 100MB 还是 100GB?(如果是 100GB,电脑瞬间死机宕机)。

答案:只消耗 100MB!

共享库加载的真相 (没有魔法)

  • 你执行./a.out时,第一条被执行的指令根本不在你的代码里!
  • 它是操作系统通过读取 ELF 文件头的INTERP(Interpreter) 段,去调用了动态链接器(如/lib64/ld-linux-x86-64.so.2)。
  • 链接器使用mmap系统调用,把libc等共享库映射到内存中。
  • 核心魔法:只读方式mmap同一个文件,物理内存中只有一份真实的副本。1000 个进程的页表全都指向同一块物理内存。

操作系统的内存 Tricks

地址空间表面上看是“若干连续的内存段”,实际全是操作系统用分页机制虚构出来的幻象:

  1. 延迟加载 (Lazy Allocation):不到万不得已(触发 Page Fault),绝不给进程分配真物理内存。
  2. 写时复制 (Copy-on-Write, COW):fork()时,父子进程共享内存。只有当某一方试图写入时,操作系统才紧急复制一份(Page fault 时,写者复制一份)。
  3. 内存去重 (Memory Deduplication):操作系统在后台悄悄扫描内存,如果发现有两页内容完全一样的只读页,就悄悄合并它们!
  4. 内存压缩与 Swapping:扫描发现某些内存页很久没人用了(Cold pages),就把它压缩或者扔到硬盘交换区里。

实现动态链接:GOT 与 PLT 的诞生

如果把动态链接交给你来设计,你会怎么做?

  • 方案1:加载时重定位 (libc.o)
    把程序和库一起搬进内存,加载时暴力修改所有引用了库函数的地址。
    缺点:极慢!链接器需要解析成千上万个根本不会被运行的符号,并且修改代码会导致代码页无法被多个进程共享(因为每个进程加载的基地址不同)。
  • 方案2:位置无关代码 (Position-Independent Code, PIC)
    这就对了!我们需要映射同一个libc.so,并且无论它被加载到哪个虚拟地址,代码都必须能正常运行。

A Layer of Indirection (引入一个间接层)

难题:main调用了.so库里的printf,但printf在运行时的地址是未知的。我们该用什么汇编指令跳过去?

  • 编译器的选择 1:全部查表跳转。每次调用都多查一次内存表。对于极其高频的函数,性能损失不可忍受。
  • 编译器的选择 2:全部直接跳转。但 x86 的call指令只支持 4 字节的相对偏移,而libc.so可能被映射到了离当前代码十万八千里远的高位地址,偏移量超出了 32 位极限,根本跳不过去!

伟大的发明:PLT 与 GOT

为了兼顾性能和位置无关,计算机先驱们发明了组合拳:

  1. GOT (Global Offset Table - 全局偏移表):
    这本质上是一个存放指针的数组,放在.data数据段里。加载时,动态链接器把真正的printf物理地址填进这个数组。
  2. PLT (Procedure Linkage Table - 过程链接表):
    在可执行文件的代码段里,“合成”一段跳板代码(蹦床):
printf@plt: jmp *GOT_PRINTF_OFFSET(%rip)

调用流程:
你的代码call printf@plt$ \rightarrow $ 跳转到蹦床 $ \rightarrow $ 蹦床去 GOT 表里读取真正的绝对地址 $ \rightarrow $ 跳进libc.so!

对于全局变量extern int x怎么办?
不能用间接跳转指令了。编译器一旦开启了-fPIC,就会强制为所有的外部变量增加一层间接寻址,必须去 GOT 表里拿一遍指针,再解引用。这必然带来一定的性能损耗。


终极黑魔法:LD_PRELOAD

计算机世界里没有黑魔法,所有的“外挂”都有迹可循。

LD_PRELOAD是 Linux 动态链接器提供的一个环境变量。它允许你强行指定一个动态链接库,使其在程序启动时被最高优先级加载。

LD_PRELOAD=./mylib.so ./a.out

它是怎么实现拦截(Hook)的?

因为动态链接器在解析符号(比如找malloc在哪)时,是按照顺序查找的。
如果你的mylib.so里也写了一个叫malloc的函数,链接器找到它之后,就不会再往下找glibc里的malloc了!

这样,你就在不修改原本a.out代码的情况下,成功劫持并替换了它的系统底层函数。

root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec11/ldpreload# ./test_programTesting malloc/free hooks...# 成功的拦截!程序每一次申请内存,都被我们预先加载的恶意/监控代码记录了下来!Allocated100bytes at 0x55ba218c86b0 Allocated200bytes at 0x55ba218c8720

Take-away Messages (总结):
找到正确的思路,我们就能在复杂的机制中找到主干:在动态链接的例子里,我们理解了为了实现位置无关和代码共享,先驱们创造了 ELF 中的GOT(Global Offset Table) 和PLT(Procedure Linkage Table)。而正是这种动态寻址机制,赋予了我们利用LD_PRELOAD进行函数 Hook 的极客能力。

相关新闻

  • 南京大学 操作系统 (JYY) 学习笔记:从 UNIX 到 Linux 与庞大的应用生态
  • 保险理赔AI化转型白皮书(2024监管合规版):覆盖92%拒赔争议场景的NLP+规则引擎双模架构
  • 2026年7月目前靠谱的真空袋直销厂家推荐,服装自粘袋/食品袋/加厚平口袋/肉类真空袋/立体风琴袋,真空袋企业怎么选择 - 品牌推荐师

最新新闻

  • 5分钟搞定API网关:Apache APISIX Dashboard终极可视化指南 [特殊字符]
  • PUBG罗技鼠标宏终极指南:用电脑视觉实现智能压枪
  • WSL2 sudo apt update 卡在 0% [Connecting to security.ubuntu.com]
  • 江门小规模企业注册公司口碑好口碑排行榜:江门本地机构推荐榜单与对比 - GrowthUME
  • 口碑相传的气垫源头工厂,究竟藏着多少美妆品牌的秘密? - 热点速览
  • Docker安全最佳实践:基于awesome-docker-security的15个关键技巧

日新闻

  • 金融舆情监测系统:多语言情感分析与实时可视化技术解析
  • QT C++调用Python异常处理:PyBind11实战与跨语言编程指南
  • A-47双麦回音消除模块:主次麦空间分布与差分连接对ENC性能的影响

周新闻

  • 大连理工大学与东京大学联手打造的“主动型AI助手“
  • 170.2026年国家级科研瓶颈:超精密单点金刚石切削(SPDT)光学表面生成
  • SongBloom:革命性歌曲生成框架深度解析——如何通过交织自回归与扩散模型创作完整音乐

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号