当前位置: 首页 > news >正文

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

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

在前几讲中,我们已经知道进程从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文件即可。

可以用lddfile命令来观察:

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 的极客能力。

http://www.jsqmd.com/news/1289215/

相关文章:

  • 2026年07月反光安防行业优质制造厂家与供货商全景观察 - 优企名品
  • Starccm浮式风机CFD仿真与七自由度运动分析
  • Ventoy:一个U盘装下所有系统!告别反复格式化的终极启动盘方案
  • Windows下MinIO对象存储安装配置全指南
  • 奇摩技术说:大模型时代的工作流新范式 - 奇摩-workbuddy
  • 猫抓插件:三步解锁网页资源下载新体验
  • Edge-TTS语音合成:如何绕过微软限制实现跨平台免费TTS服务
  • 基于STELLA系统动态模拟技术及在农业、生态及环境等科学领域中的应用
  • 2026年7月目前靠谱的真空袋直销厂家推荐,服装自粘袋/食品袋/加厚平口袋/肉类真空袋/立体风琴袋,真空袋企业怎么选择 - 品牌推荐师
  • ★大润发购物卡回收几折最划算?2026?★ - 沃卡回收
  • 如何在安卓设备上快速实现摄像头视频替换:终极免费指南
  • 保险理赔AI化转型白皮书(2024监管合规版):覆盖92%拒赔争议场景的NLP+规则引擎双模架构
  • 南京大学 操作系统 (JYY) 学习笔记:从 UNIX 到 Linux 与庞大的应用生态
  • 猫抓浏览器扩展:终极网页媒体资源嗅探与下载解决方案
  • 2026 厦门黄金回收避坑指南,本地老店回收黄金不压秤 - 日常比对手册
  • VideoDownloadHelper终极指南:Chrome视频下载插件的专业配置与使用教程
  • (2026)合肥理工学校招生老师电话是多少?班型对比四大班型怎么选? - hflgzz
  • 终极视频下载助手:Simple Video Download Helper完全使用指南
  • 武汉新房装修费用|新房装修不一定要花很多,意米设计把钱放在你真正待的地方 - 品牌红黑榜
  • 深圳比较好的活动管理软件推荐,通过技术评估分析
  • 计算机毕业设计之基于SpringBoot的爱心捐助平台的设计与实现
  • 2026 成都主城区卫生间免砸砖维修服务商推荐 8 家 正规机构甄选攻略与签约避坑 FAQ 大全 - 鼎万建筑修缮
  • Windows 安全基线:组策略、UAC、Defender 深度配置
  • CHZZK:如何用5分钟打造你的Naver直播数据监控系统?[特殊字符]
  • 苏州自考一年考几次,最快多久拿毕业证? - 学历提升信息早知道
  • 服装生产系统选型的五个关键判断点:从设计到交付的数字化落地思考
  • 如何快速解锁Steam游戏DLC:SmokeAPI完整使用指南
  • 2026北京翡翠回收避坑指南:如何让你的旧翡翠卖出合理高价? - 一日一测评
  • 3步解锁ZTE光猫工厂模式:网络管理员的终极权限获取指南
  • Windows 权限提升实战:令牌窃取、UAC 绕过、服务劫持