DLL 是什么
读前 30 秒 · 先认识 4 个词
整页只会反复用到这几个基础词,花半分钟认识它们,后面就畅通了:
一个正在运行的程序实例。每个进程都有自己独立的一套内存空间,互不干扰。
内存里每个字节的"门牌号"。"0x"开头只是十六进制写法,当成编号看就行,别被它吓到。
一段可以反复调用的功能。Windows 把大量系统功能做成 API,存放在各种 DLL 里。
Windows 的可执行文件格式。EXE 和 DLL 都是 PE 文件,所以它俩"长得很像"。
DLL(Dynamic Link Library,动态链接库)是 Windows 上一种特殊的可执行文件。它装着可被多个程序共享的代码和数据,自己却不直接运行——而是等某个程序把它“请”进内存后再使用。
要理解“动态”,先看它的反面——“静态链接”:
- 编译时把库的代码复制进 EXE
- EXE 自带全部功能,体积大
- 更新库要重新编译整个程序
- 每个程序各存一份,重复占内存
- 库单独放在 DLL 文件里
- 程序运行时才把 DLL 装入内存
- 换 DLL 即可升级,不用动 EXE
- 多个程序共享同一份 DLL 代码
EXE 与 DLL:长得像,分工不同
两者都是 PE 格式(Windows 的可执行文件格式),结构相似,但角色完全不同:
- 可以独立运行(双击即启动)
- 有程序主入口(
main/WinMain) - 通常加载在
0x00400000 - 包含完整的程序逻辑
- 不能独立运行,需被进程加载
- 有入口点
DllMain,但不会自己启动 - 加载到进程地址空间后供调用
- 对外“导出”函数和资源
DLL 并非没有入口点。它有 DllMain,会在被加载/卸载、线程创建/退出时被系统调用。区别在于:EXE 的入口会“驱动”整个程序跑起来,而 DLL 的入口只是被动地被通知一下,自己不会主动运行。
为什么用 DLL
Windows 几乎离不开 DLL——系统本身就由大量 DLL 组成(kernel32.dll、user32.dll、gdi32.dll……)。原因主要有五点:
代码复用
多个程序共享同一个 DLL,避免重复造轮子。
模块化
把程序拆成独立模块,结构清晰、好维护。
节省内存
多进程共享一份 DLL 代码,内存只算一份。
便于更新
修 Bug 只换 DLL,不用重发整个程序。
插件机制
第三方可用 DLL 扩展功能,不改主程序。
程序的关键功能(弹窗、文件、网络……)大多实现在 DLL 里,通过导入表调用。看懂这层关系,才能快速定位关键函数。
加载与内存
双击一个 EXE 后,Windows 的加载器(负责把程序装进内存、并解析依赖的系统组件)会做一系列工作,把 EXE 和它依赖的 DLL 一起搬进进程的虚拟内存。分四步演示(可点步骤跳转,或点“下一步”):
“映射” ≠ “整个搬进物理内存”
第 3 步说“映射到进程地址空间”,这里的“地址空间”指的是虚拟内存。很多人误以为系统会“先把整个 DLL 读进物理内存(RAM),再映射”——其实不是。先把两个“内存”分清:
- 每个进程独立一份地址空间
- 代码里的地址都是它(如
0x7C800000) - 32 位下约
0x0 ~ 0x7FFFFFFF - 靠页表(地址对照表)对应到物理内存
- 全系统共享一份(RAM)
- 程序看不到真实物理地址
- 容量有限
- 按需装入、可被换出
真实的加载是“用到哪页、才读哪页”(按需调页):
所以 DLL 映射完成后,内存里可能是这样——只有被访问过的页才在物理内存,其余仍留在磁盘文件里,用到才调进来:
多个进程映射同一个 DLL 时,共享的是同一份物理页帧;没被调用的代码页根本不占 RAM;文件本身就是后备存储,代码页甚至不占 pagefile(页面文件)。
EXE 和各 DLL 被映射进同一个进程地址空间。下图按真实地址从高到低排列(上方=高地址,下方=低地址):
把鼠标移到任意区域上(或 Tab 聚焦),可查看该区域的详细说明。
EXE 通常在较低地址(0x00400000),系统 DLL 在较高地址(0x7xxxxxxx)。IAT 属于 EXE 的一部分,记录从各 DLL 导入的函数的真实地址。
上面这些是经典示意地址。现代 Windows 默认开启 ASLR,每次运行时 EXE、DLL、栈、堆的地址都会随机变化,所以你在调试器里看到的真实地址会跟这里不同——但相对关系是一致的。
导入表与 IAT
这是初学者最容易搞混的地方。导入表和 IAT 是相关但不相同的两个结构:
- 位于 PE 文件的
.idata - 加载时由加载器读取
- 记录“从哪个 DLL 要哪些函数”
- 函数此时还只有名字,没有地址
- 一张函数指针表(在 EXE 内)
- 加载前:存函数名字 / 序号 hint
- 加载后:被改写成真实内存地址
- 运行时调用就经它跳转到 DLL
每个 DLL 还有一张导出表(Export Table)——它对外公布的“菜单”,列出自己能提供哪些函数、各自在 DLL 里的地址。加载器就是拿着 EXE 的导入表(购物清单),到 DLL 的导出表(菜单)里查到地址,再抄进 IAT(电话簿)。
导入表是“购物清单”(加载时用一次),导出表是 DLL 的“菜单”(它有什么、在哪),IAT 是“已填好的电话簿”(运行时反复查)。加载器照着清单、对着菜单,把电话簿填好;之后程序打电话(调用函数)只查电话簿。
运行时,EXE 里调用一个导入函数的指令长这样——它不直接跳到 DLL,而是经 IAT 间接跳转:
点击“解析导入”,看加载器如何照着导入表,到各 DLL 的导出表里找到真实地址,再填进 IAT:
① EXE 的导入表 (只有名字)
② DLL 的导出表 (带真实地址)
综合演示:调用 DLL
主程序想“显示消息”和“创建文件”,但这两个能力分别在 user32.dll 和 kernel32.dll 里。点“下一步”看它们如何连起来:
一图总结
把前几节串起来,整个关系就是:
① DLL 有 DllMain 入口点,但不自己运行。 ② 导入表是加载时的“清单”,IAT 是运行时的“电话簿”。 ③ 现代系统有 ASLR,地址会随机,但相对关系不变。
自测一下
这 8 道题涵盖本页的核心概念与易错点:前 4 题点选项即判,多选和排序题需点“查看答案”。 尚未作答
DllMain 入口点,但只是被动被系统调用,不会像 EXE 那样自己启动。call [IAT项],经 IAT(电话簿)间接跳到 DLL 函数。导出表则是 DLL 提供地址的“菜单”。