DLL × EXE
图解教学 · INFOGRAPHIC · 2026
系统基础 · 交互式图解

DLL 与 EXE,
到底是什么关系?

用杂志式图解 + 可交互演示,零基础看懂 Windows 程序是如何“组装”起来的——从双击启动,到调用 DLL 里的函数。

EXE 主程序 IAT 导入地址表 DLL 动态链接库
面向初学者可交互演示x86 · 32 位
01
第 1 章 · SECTION 01

DLL 是什么

先建立一个直觉
01 / 05

读前 30 秒 · 先认识 4 个词

整页只会反复用到这几个基础词,花半分钟认识它们,后面就畅通了:

进程 PROCESS

一个正在运行的程序实例。每个进程都有自己独立的一套内存空间,互不干扰。

内存地址 0x…

内存里每个字节的"门牌号"。"0x"开头只是十六进制写法,当成编号看就行,别被它吓到。

函数 / API FUNCTION

一段可以反复调用的功能。Windows 把大量系统功能做成 API,存放在各种 DLL 里。

PE 文件 PE FORMAT

Windows 的可执行文件格式。EXE 和 DLL 都是 PE 文件,所以它俩"长得很像"。

DLL(Dynamic Link Library,动态链接库)是 Windows 上一种特殊的可执行文件。它装着可被多个程序共享的代码和数据,自己却不直接运行——而是等某个程序把它“请”进内存后再使用。

要理解“动态”,先看它的反面——“静态链接”:

静态链接 STATIC
  • 编译时把库的代码复制进 EXE
  • EXE 自带全部功能,体积大
  • 更新库要重新编译整个程序
  • 每个程序各存一份,重复占内存
动态链接 DYNAMIC
  • 库单独放在 DLL 文件里
  • 程序运行时才把 DLL 装入内存
  • 换 DLL 即可升级,不用动 EXE
  • 多个程序共享同一份 DLL 代码

EXE 与 DLL:长得像,分工不同

两者都是 PE 格式(Windows 的可执行文件格式),结构相似,但角色完全不同:

EXE(主程序) PROCESS
  • 可以独立运行(双击即启动)
  • 有程序主入口(main / WinMain
  • 通常加载在 0x00400000
  • 包含完整的程序逻辑
DLL(动态链接库) LIBRARY
  • 不能独立运行,需被进程加载
  • 有入口点 DllMain,但不会自己启动
  • 加载到进程地址空间后供调用
  • 对外“导出”函数和资源
常见误区 · 澄清

DLL 并非没有入口点。它有 DllMain,会在被加载/卸载、线程创建/退出时被系统调用。区别在于:EXE 的入口会“驱动”整个程序跑起来,而 DLL 的入口只是被动地被通知一下,自己不会主动运行。

02
第 2 章 · SECTION 02

为什么用 DLL

五个核心理由
02 / 05

Windows 几乎离不开 DLL——系统本身就由大量 DLL 组成(kernel32.dlluser32.dllgdi32.dll……)。原因主要有五点:

01

代码复用

多个程序共享同一个 DLL,避免重复造轮子。

02

模块化

把程序拆成独立模块,结构清晰、好维护。

03

节省内存

多进程共享一份 DLL 代码,内存只算一份。

04

便于更新

修 Bug 只换 DLL,不用重发整个程序。

05

插件机制

第三方可用 DLL 扩展功能,不改主程序。

为什么逆向要懂 DLL

程序的关键功能(弹窗、文件、网络……)大多实现在 DLL 里,通过导入表调用。看懂这层关系,才能快速定位关键函数。

03
第 3 章 · SECTION 03

加载与内存

加载流程 + 内存布局
03 / 05
FIG. 3.1 — 程序启动流程

双击一个 EXE 后,Windows 的加载器(负责把程序装进内存、并解析依赖的系统组件)会做一系列工作,把 EXE 和它依赖的 DLL 一起搬进进程的虚拟内存。分四步演示(可点步骤跳转,或点“下一步”):

“映射” ≠ “整个搬进物理内存”

第 3 步说“映射到进程地址空间”,这里的“地址空间”指的是虚拟内存。很多人误以为系统会“先把整个 DLL 读进物理内存(RAM),再映射”——其实不是。先把两个“内存”分清:

虚拟内存 VIRTUAL
  • 每个进程独立一份地址空间
  • 代码里的地址都是它(如 0x7C800000
  • 32 位下约 0x0 ~ 0x7FFFFFFF
  • 页表(地址对照表)对应到物理内存
物理内存 PHYSICAL
  • 全系统共享一份(RAM)
  • 程序看不到真实物理地址
  • 容量有限
  • 按需装入、可被换出

真实的加载是“用到哪页、才读哪页”(按需调页):

1建立虚拟映射只登记映射关系,页表标“不存在”,此时不读文件内容
2首次访问某页CPU 发现该页“不存在”
3缺页中断一种“这页还没进内存”的信号,操作系统随即介入
4读该页进 RAM只读 4KB,更新页表,重试指令

所以 DLL 映射完成后,内存里可能是这样——只有被访问过的页才在物理内存,其余仍留在磁盘文件里,用到才调进来:

页 1在 RAM 页 2在磁盘 页 3在磁盘 页 4在 RAM 页 5在磁盘 页 6在 RAM
已被访问 → 调入物理内存 未访问 → 留在磁盘文件
这正是“省内存”的真正原因

多个进程映射同一个 DLL 时,共享的是同一份物理页帧;没被调用的代码页根本不占 RAM;文件本身就是后备存储,代码页甚至不占 pagefile(页面文件)。

FIG. 3.2 — 进程内存布局(加载完成后)

EXE 和各 DLL 被映射进同一个进程地址空间。下图按真实地址从高到低排列(上方=高地址,下方=低地址):

把鼠标移到任意区域上(或 Tab 聚焦),可查看该区域的详细说明。

高地址 ↑低地址 ↓
内核空间 用户态程序不可访问
内核 0x80000000+
kernel32.dll 核心 API:文件、内存、进程
DLL 0x7C800000
gdi32.dll 图形绘制 API
DLL 0x77E10000
user32.dll 窗口、消息框 API
DLL 0x77D10000
堆空间 动态分配的内存(malloc / new)
↑ 向上生长
EXE 映像 代码段 / 数据段 / IAT 位于此处(约 0x00402000)
EXE 0x00400000
栈空间 函数调用、局部变量、返回地址
↓ 向下生长 ~0x0012FF80
NULL 空区 保留,禁止访问(空指针即落在此处)
0x00000000
关键点

EXE 通常在较低地址(0x00400000),系统 DLL 在较高地址(0x7xxxxxxx)。IAT 属于 EXE 的一部分,记录从各 DLL 导入的函数的真实地址。

关于地址随机化(ASLR)

上面这些是经典示意地址。现代 Windows 默认开启 ASLR,每次运行时 EXE、DLL、栈、堆的地址都会随机变化,所以你在调试器里看到的真实地址会跟这里不同——但相对关系是一致的。

04
第 4 章 · SECTION 04

导入表与 IAT

两个容易混淆的结构
04 / 05

这是初学者最容易搞混的地方。导入表IAT相关但不相同的两个结构:

导入表 IMPORT TABLE
  • 位于 PE 文件的 .idata
  • 加载时由加载器读取
  • 记录“从哪个 DLL 要哪些函数”
  • 函数此时还只有名字,没有地址
IAT IMPORT ADDR TABLE
  • 一张函数指针表(在 EXE 内)
  • 加载前:存函数名字 / 序号 hint
  • 加载后:被改写成真实内存地址
  • 运行时调用就经它跳转到 DLL
那“真实地址”从哪来?靠导出表

每个 DLL 还有一张导出表(Export Table)——它对外公布的“菜单”,列出自己能提供哪些函数、各自在 DLL 里的地址。加载器就是拿着 EXE 的导入表(购物清单),到 DLL 的导出表(菜单)里查到地址,再抄进 IAT(电话簿)。

一句话区分

导入表是“购物清单”(加载时用一次),导出表是 DLL 的“菜单”(它有什么、在哪),IAT 是“已填好的电话簿”(运行时反复查)。加载器照着清单、对着菜单,把电话簿填好;之后程序打电话(调用函数)只查电话簿。

运行时,EXE 里调用一个导入函数的指令长这样——它不直接跳到 DLL,而是经 IAT 间接跳转:

; 调用 MessageBoxA —— 通过 IAT 间接跳转 call dword ptr [0x00402008] ; 0x00402008 是 IAT 里 MessageBoxA 那一项 ; 该项里存的是 user32.dll 中 MessageBoxA 的真实地址 0x77D5050B
FIG. 4.1 — 模拟加载器解析导入

点击“解析导入”,看加载器如何照着导入表,到各 DLL 的导出表里找到真实地址,再填进 IAT:

① EXE 的导入表 (只有名字)

主程序导入项待解析
kernel32!CreateFileA名字
kernel32!ReadFile名字
user32!MessageBoxA名字
user32!CreateWindowExA名字

② DLL 的导出表 (带真实地址)

kernel32.dll 导出@ 0x7C800000
CreateFileA0x7C801D77
ReadFile0x7C801A1B
WriteFile0x7C801E1A
user32.dll 导出@ 0x77D10000
MessageBoxA0x77D5050B
CreateWindowExA0x77D504EA
DefWindowProcA0x77D5049A
③ IAT(导入地址表 · 运行时查这里)0 / 4
加载前为空 / 存的是名字 hint —— 点击下方按钮让加载器来填
导入项被读取 导出表匹配命中 / 写入 IAT
05
第 5 章 · SECTION 05

综合演示:调用 DLL

把全过程串起来
05 / 05
FIG. 5.1 — 主程序调用 DLL

主程序想“显示消息”和“创建文件”,但这两个能力分别在 user32.dllkernel32.dll 里。点“下一步”看它们如何连起来:

主程序 EXE
显示消息→ 调用 MessageBoxA
创建文件→ 调用 CreateFileA
user32.dll
MessageBoxA0x77D5050B
kernel32.dll
CreateFileA0x7C801D77
点“开始演示”
程序已就绪,但尚未加载任何 DLL。

一图总结

把前几节串起来,整个关系就是:

// 1. 编译期:EXE 写好“我要用这些 DLL 函数” —— 记进导入表 // 2. 加载期:加载器读导入表 → 映射 DLL → 查导出表 → 把真实地址填进 IAT // 3. 运行期:EXE 执行 call [IAT项] → 间接跳转到 DLL 中函数的真实地址 EXE ──(导入表,加载时)──▶ DLL │ │ 导出表里有真实地址 └──(IAT,运行时)─────────────▶ 函数真实地址
记牢三句话

① DLL 有 DllMain 入口点,但不自己运行。 ② 导入表是加载时的“清单”,IAT 是运行时的“电话簿”。 ③ 现代系统有 ASLR,地址会随机,但相对关系不变。

学完自测 · QUIZ

自测一下

8 道题 · 对错 / 单选 / 多选 / 排序,即做即判

这 8 道题涵盖本页的核心概念与易错点:前 4 题点选项即判,多选和排序题需点“查看答案”。 尚未作答

Q1DLL 可以像 EXE 一样,双击就能独立运行。
答案:错。DLL 不能独立运行,必须被某个进程加载后才能使用。它 DllMain 入口点,但只是被动被系统调用,不会像 EXE 那样自己启动。
Q2程序“运行时”调用一个导入函数,是靠哪个结构找到它的真实地址?
答案:IAT。导入表是加载时给加载器看的“清单”;运行时,EXE 执行 call [IAT项],经 IAT(电话簿)间接跳到 DLL 函数。导出表则是 DLL 提供地址的“菜单”。
Q3“映射 DLL 到内存”,就是把整个 DLL 一次性读进物理内存。
答案:错。是先建立虚拟映射(只登记关系),物理内存“用到哪页、才读哪页”——首次访问某页触发缺页中断,才把那一页从磁盘调入 RAM。
Q4在现代 Windows 上,每次运行同一个程序,其 EXE / DLL 的内存地址都固定不变。
答案:错。现代系统默认开启 ASLR,每次运行地址都会随机化;但各部分之间的相对关系(如 EXE 在低、系统 DLL 在高)保持不变。
Q5以下哪些属于“使用 DLL 带来的好处”?(多选)
答案:代码复用 / 节省内存 / 便于更新维护。“让每个程序体积更大”是静态链接的特点;动态链接恰恰通过共享 DLL 来避免重复、节省空间。
Q6把“程序加载的 4 个步骤”按先后顺序排一排(依次点击,再点“查看答案”)。
正确顺序:程序启动 → 定位 DLL → 映射 DLL → 填写 IAT。先启动并读导入表,再按搜索顺序找 DLL,然后映射进地址空间,最后查导出表把真实地址填进 IAT。
Q7EXE 通常被加载到哪个地址附近?
答案:0x00400000。这是 EXE 的经典加载基址;0x00000000 是禁用的 NULL 区,0x7C800000 一带则是系统 DLL 的区域。
Q8加载完成“之前”,IAT 里存的是什么?
答案:函数的名字或序号。加载后,加载器才把 IAT 改写成真实地址——这正是“导入表(清单)→ 填 IAT(电话簿)”的过程。