Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

本页目录

Windows 编译产物详解:exe、DLL、lib —— 彻底分清两种 lib / Windows Build Artifacts: EXE, DLL, and the Two Kinds of LIB ​

📅 创建时间:2026-07-13 🏷️ 标签:#Windows #C++ #DLL #静态库 #导入库 #MSVC 📚 前置知识:cpp project fundamentals


📋 本章目标 ​

  • 彻底分清 Windows 上两种 .lib 文件的本质区别——这是 Windows C++ 开发最大的认知陷阱
  • 理解 .exe、.dll、.obj、.lib、.pdb、.exp 等所有编译产物的身份和用途
  • 掌握 DLL 导出/导入机制:__declspec(dllexport) 和 __declspec(dllimport)
  • 理解运行时库链接:/MT vs /MD 的选择和混用的灾难性后果
  • 熟悉 MSVC 工具链中的关键工具:cl.exe、link.exe、lib.exe、dumpbin.exe

第1部分:Windows 编译产物全景图 ​

1.1 一次完整编译产生了什么? ​

┌─────────────────────────────────────────────────────────────────────────────┐
│               Windows C++ 编译产物全景                                       │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  源码                       编译过程                      最终产物           │
│                                                                             │
│  mylib.cpp ──→ cl.exe ──→ mylib.obj ──→ link.exe ──→ mylib.dll             │
│                                                 │                          │
│                           mylib.h               ├──→ mylib.lib  ← 导入库!  │
│                                                 │                          │
│                           (如果有 .def)         ├──→ mylib.exp              │
│                                                 │                          │
│                           (如果 /DEBUG)         └──→ mylib.pdb              │
│                                                                             │
│  main.cpp ────→ cl.exe ──→ main.obj  ──→ link.exe ──→ main.exe             │
│                           mylib.lib ──┘              main.pdb               │
│                                                                             │
│  产生的文件列表:                                                            │
│  ┌───────────┬──────────────────────────────────────────────────────────┐  │
│  │ 文件       │ 身份                                                      │  │
│  ├───────────┼──────────────────────────────────────────────────────────┤  │
│  │ .obj      │ 目标文件——编译产生,一个 .cpp 对应一个 .obj               │  │
│  │ .exe      │ PE 可执行文件——有 main/WinMain 入口点,可以直接运行      │  │
│  │ .dll      │ 动态链接库——没有 main,被 .exe 或其他 .dll 加载          │  │
│  │ .lib(1)   │ 静态库——.obj 的集合,链接时嵌入到 .exe/.dll 中           │  │
│  │ .lib(2)   │ 导入库——DLL 的"目录",链接时告诉链接器符号在 DLL 里       │  │
│  │ .pdb      │ 调试符号文件——存函数名、文件名、行号映射,给调试器用的    │  │
│  │ .exp      │ 导出文件——链接器生成的中间产物,通常可以删除              │  │
│  │ .ilk      │ 增量链接文件——加速增量链接,可以删除                      │  │
│  │ .res      │ 编译后的资源文件(图标、对话框、菜单等)                   │  │
│  └───────────┴──────────────────────────────────────────────────────────┘  │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

第2部分:彻底分清两种 .lib —— 本章最核心的内容 ​

2.1 你的困惑是完全正常的 ​

在 Windows 上,.lib 这个后缀名被两种完全不同的东西共用。这就像两个人都叫"张三",但一个是会计一个是建筑工人——同名但干的活完全不同。这是 Windows C++ 开发中最大的认知陷阱。

┌─────────────────────────────────────────────────────────────────────────────┐
│           两种 .lib 的本质区别                                                │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │              静态库 .lib(Static Library)                     │         │
│  │                                                               │         │
│  │  本质:一堆 .obj 文件打包在一起(类似 ZIP 或 tar 归档)         │         │
│  │  大小:比较大(包含实际机器码)                                 │         │
│  │  生成方式:lib.exe /OUT:mylib.lib obj1.obj obj2.obj ...       │         │
│  │  使用时机:链接时把代码嵌入到最终 .exe/.dll 中                  │         │
│  │  运行时:不需要额外文件(代码已经在 .exe 里了)                 │         │
│  │  查看:dumpbin /symbols mylib.lib(可以看到函数名和代码)      │         │
│  │                                                               │         │
│  │  .cpp → cl.exe → .obj ─→ lib.exe → .lib(静态库)             │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │              导入库 .lib(Import Library)                      │         │
│  │                                                               │         │
│  │  本质:DLL 的"目录"——只含 stub 代码,不含实际实现               │         │
│  │  大小:比较小(只有跳转 stub,没有实际代码)                     │         │
│  │  生成方式:link.exe 生成 .dll 时自动生成(/IMPLIB 选项)        │         │
│  │  使用时机:链接时告诉链接器"这些符号在 .dll 里"                 │         │
│  │  运行时:必须有对应的 .dll 存在(否则程序启动报错)             │         │
│  │  查看:dumpbin /exports mylib.lib(只有导出符号列表)          │         │
│  │                                                               │         │
│  │  .cpp → cl.exe → .obj ─→ link.exe → .dll + .lib(导入库)     │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
│  ⚠️ 怎么区分?                                                              │
│  • 看大小:几 MB 以上多半是静态库;几 KB 多半是导入库                       │
│  • dumpbin /symbols:静态库能看到函数名+实现;导入库只有 __imp_ 标记        │
│  • 场景判断:有对应 .dll 的 .lib 是导入库;单独的 .lib 是静态库             │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

2.2 深入理解导入库的工作机制 ​

导入库是怎么"偷梁换柱"的?看这个例子:

cpp
// 你的代码里写了:
add(3, 5);   // add 函数在 mylib.dll 里
1
2

编译和链接时发生了什么:

┌─────────────────────────────────────────────────────────────────────────────┐
│                    导入库的 stub 机制                                         │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  你在代码中调用 add(3, 5)                                                    │
│       │                                                                     │
│       ▼ 编译                                                                 │
│  main.obj 中有一条 call 指令:call ???(还不知道 add 在哪)                   │
│       │                                                                     │
│       ▼ 链接(链接器读入 mylib.lib 导入库)                                  │
│  导入库告诉链接器:                                                           │
│    "add 这个符号在 mylib.dll 里,我给你一个 stub:                            │
│     jmp [__imp_add]   ← 间接跳转,实际地址在 IAT 表中                        │
│     "                                                                       │
│       │                                                                     │
│       ▼ 生成                                                                 │
│  main.exe 的导入表里记录了:需要从 mylib.dll 加载 add                         │
│       │                                                                     │
│       ▼ 运行时(PE 加载器)                                                   │
│  1. 加载 main.exe                                                            │
│  2. 发现 main.exe 依赖 mylib.dll                                             │
│  3. 找到并加载 mylib.dll                                                     │
│  4. 找到 mylib.dll 中 add 的实际地址                                         │
│  5. 把地址填入 main.exe 的 IAT(Import Address Table)                        │
│  6. __imp_add → 指向 add 的真实地址                                          │
│       │                                                                     │
│       ▼                                                                     │
│  你的 call add → jmp [__imp_add] → DLL 中 add 的真实代码                     │
│                                                                             │
│  结论:导入库不含 add 的代码实现,只含一条"去 DLL 里找 add"的指令             │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

2.3 实战场景:甲方给你文件,怎么用? ​

┌─────────────────────────────────────────────────────────────────────────────┐
│     甲方给你                           你需要做什么                           │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  场景A:.h + .lib(静态库)                                                  │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │ 1. #include 他们的 .h                                           │         │
│  │ 2. 链接时添加 .lib                                             │         │
│  │ 3. 编译完成,不需要额外文件                                     │         │
│  │ 4. 发布你的程序时:只需要你的 .exe                              │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
│  场景B:.h + .lib(导入库) + .dll                                          │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │ 1. #include 他们的 .h                                           │         │
│  │ 2. 链接时添加 .lib(导入库)                                    │         │
│  │ 3. 编译完成                                                    │         │
│  │ 4. 运行时 .dll 必须在搜索路径中——这步最容易忘!                  │         │
│  │ 5. 发布你的程序时:你的 .exe + 他们的 .dll                      │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
│  场景C:.h + .dll(没有 .lib!)                                             │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │ 这通常是甲方漏给了导入库,或者 DLL 本来就是给运行时加载用的      │         │
│  │ 方案1:生成导入库:dumpbin /exports foo.dll → 写 .def → lib.exe │         │
│  │ 方案2:运行时动态加载:LoadLibrary + GetProcAddress             │         │
│  │        (这就是第 06 篇要讲的内容)                              │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

第3部分:DLL 的符号导出机制 ​

3.1 __declspec(dllexport) 和 __declspec(dllimport) ​

这是 Windows DLL 最核心的两个宏:

cpp
// ====== 编译 DLL 时(MYLIB_EXPORTS 被定义)======

#ifdef MYLIB_EXPORTS
    #define MYLIB_API __declspec(dllexport)
#else
    #define MYLIB_API __declspec(dllimport)
#endif

// 在 DLL 内部:dllexport → 告诉链接器"这个函数要放到导出表里"
class MYLIB_API Calculator {
public:
    int add(int a, int b);
    int multiply(int a, int b);
};

MYLIB_API int global_compute(int x);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌─────────────────────────────────────────────────────────────────────────────┐
│           dllexport vs dllimport 的工作原理                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  __declspec(dllexport) —— 编译 DLL 时用                                      │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │ • 告诉链接器:把这个符号放入 DLL 的导出表                       │         │
│  │ • 导出表 = DLL 提供给外界的符号目录                             │         │
│  │ • dumpbin /exports mylib.dll 可以看到所有导出符号               │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
│  __declspec(dllimport) —— 使用 DLL 时用(链接 .exe 或其他 .dll)            │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │ • 告诉编译器:这个符号来自外部 DLL,不要生成直接调用            │         │
│  │ • 改为通过 __imp_ 间接调用(走 IAT)                          │         │
│  │ • 优化:编译器知道是从 DLL 导入,可能生成更高效的代码           │         │
│  │ • 不写 dllimport 通常也能工作,但会产生一次不必要的间接跳转      │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
│  典型模式(几乎所有 Windows DLL 项目都用):                                  │
│  #ifdef MYLIB_EXPORTS      // 只在编译 DLL 时定义                            │
│      #define MYLIB_API __declspec(dllexport)                                │
│  #else                     // 使用者 include 这个头文件时                     │
│      #define MYLIB_API __declspec(dllimport)                                │
│  #endif                                                                    │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

3.2 .def 文件——显式控制导出 ​

除了 __declspec(dllexport),还有另一种方式控制 DLL 导出——.def 文件。

cpp
// mylib.def
LIBRARY mylib
EXPORTS
    add         @1        // @1 是序号(ordinal)
    multiply    @2
    compute     @3  NONAME  // 只按序号导出,隐藏函数名
    internal_fn @4  PRIVATE  // 不对外可见
1
2
3
4
5
6
7

.def 文件的优势:

  • 可以按序号导出(不用函数名,更难逆向)
  • 可以导出没有 dllexport 标记的函数
  • 精确控制哪些符号对外可见
  • 可以重命名导出符号

实际项目中的选择:大多数项目用 __declspec(dllexport) + 宏方式,因为更简单、和代码在一起。.def 用于需要精确控制的场景(如系统级 DLL)。

3.3 从 DLL 的角度看符号可见性 ​

用 dumpbin 查看 DLL 的导出表:

bash
dumpbin /exports mylib.dll

# 输出示例:
#     ordinal hint RVA      name
#           1    0 00001200 ?add@@YAHHH@Z    ← C++ name mangling
#           2    1 00001250 multiply          ← extern "C" 的干净名字
#           3    2 00001300 compute
1
2
3
4
5
6
7

注意:如果不用 extern "C",C++ 函数在 DLL 里的名字会是 mangled 形式(如 ?add@@YAHHH@Z)。调用方如果也是同版本编译器编译的,这个不是问题。但如果跨编译器或跨语言调用,就要用 extern "C" 保平安。


第4部分:/MT 和 /MD —— 运行时库链接方式 ​

4.1 这个问题有多重要? ​

混用 /MT 和 /MD 会导致程序崩溃,而且错误信息完全看不出原因。 这是 Windows C++ 开发中排名前 3 的坑。

┌─────────────────────────────────────────────────────────────────────────────┐
│             运行时库链接选项                                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  MSVC 提供了四种运行时库链接方式:                                            │
│                                                                             │
│  ┌──────────┬────────────────────┬──────────────┬──────────────────────┐   │
│  │ 选项     │ 含义                │ CRT 位置     │ 场景                 │   │
│  ├──────────┼────────────────────┼──────────────┼──────────────────────┤   │
│  │ /MT      │ 静态链接CRT(Release)│ 嵌入 .exe    │ 单文件分发           │   │
│  │ /MTd     │ 静态链接CRT(Debug)  │ 嵌入 .exe    │ Debug 开发           │   │
│  │ /MD      │ 动态链接CRT(Release)│ vcruntime.dll │ 多个模块共享CRT     │   │
│  │ /MDd     │ 动态链接CRT(Debug)  │ vcruntimed.dll│ Debug 开发           │   │
│  └──────────┴────────────────────┴──────────────┴──────────────────────┘   │
│                                                                             │
│  CRT = C Runtime Library = 包含 malloc/free/printf/new/delete 等的库         │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

4.2 混用 /MT 和 /MD 为什么会崩溃? ​

┌─────────────────────────────────────────────────────────────────────────────┐
│               /MT 和 /MD 混用的灾难                                           │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  假设你的程序用 /MD 编译,链接了一个用 /MT 编译的静态库:                     │
│                                                                             │
│  main.exe (/MD)                    mylib.lib (/MT)                          │
│  ┌──────────────────┐             ┌───────────────────┐                    │
│  │ 调用库函数         │             │                   │                    │
│  │ create_object(){  │──调用────→│ Object* create()  │                    │
│  │   p = create();   │             │ {                 │                    │
│  │   delete p;  ←崩溃!│            │   return new Obj; │ ← 这个 new 用了     │
│  │ }                 │             │ }                 │   /MT 的 CRT       │
│  │ // 这个 delete 用 │             └───────────────────┘                    │
│  │ // /MD 的 CRT     │                                                     │
│  └──────────────────┘                                                      │
│                                                                             │
│  /MT 的 .lib 有自己的 CRT 副本(有自己的 malloc 堆)                         │
│  /MD 的 .exe 有共享的 CRT(另一个 malloc 堆)                                │
│                                                                             │
│  结果:库函数在堆A上 new,程序在堆B上 delete → 堆管理器不认识 → 崩溃          │
│                                                                             │
│  更隐蔽的问题:                                                               │
│  • 一个模块的 fopen() 返回的 FILE* 给另一个模块用 → 可能崩溃                │
│  • 一个模块 setlocale() 不影响另一个 → 行为不一致                           │
│  • 跨模块传递 STL 对象(如 std::string)→ Debug/Release 布局不同 → 崩溃     │
│                                                                             │
│  铁律:一个进程中的所有 .exe 和 .dll 必须使用相同的 CRT 链接方式              │
│  (特别是 /MT vs /MD 不能混用)                                               │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

4.3 实际建议 ​

┌─────────────────────────────────────────────────────────────────────────────┐
│              运行时库选择建议                                                  │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ✅ 推荐 /MD(动态链接 CRT):                                               │
│  • 多个模块(.exe + 多个 .dll)共享一份 CRT                                 │
│  • .dll 和 .exe 之间可以安全传递堆对象                                       │
│  • 安全更新:Windows Update 可以修补 vcruntime.dll 的安全漏洞               │
│  • 缺点:部署时必须带 vcruntime140.dll 或要求用户安装 VC Redist             │
│                                                                             │
│  ✅ 推荐 /MT(静态链接 CRT):                                               │
│  • 单一 .exe 无需安装任何运行时                                             │
│  • 零依赖单文件部署                                                         │
│  • 缺点:多个模块不能混用 /MT;exe 体积增大                                   │
│                                                                             │
│  插件开发场景:必须统一用 /MD!                                              │
│  • 宿主 .exe 和所有插件 .dll 必须用相同的 CRT 链接方式                       │
│  • 否则宿主 new 的对象给插件 delete → 崩溃                                   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

第5部分:MSVC 工具链速览 ​

5.1 核心工具 ​

工具全名做什么
cl.exeMicrosoft C/C++ Compiler编译 .cpp → .obj
link.exeMicrosoft Linker链接 .obj + .lib → .exe / .dll
lib.exeMicrosoft Library Manager创建/管理 .lib(静态库或导入库)
dumpbin.exeDump Binary查看 PE/COFF 文件的内部信息
ml.exeMicrosoft Macro Assembler汇编 .asm → .obj
rc.exeResource Compiler编译 .rc(资源文件) → .res
editbin.exeEdit Binary修改已有 PE/COFF 文件的属性

5.2 dumpbin——你的二进制侦探 ​

bash
dumpbin /headers mylib.dll     # 查看 PE 头信息
dumpbin /exports mylib.dll     # 查看 DLL 导出了哪些函数(最重要!)
dumpbin /imports main.exe      # 查看 exe 依赖哪些 DLL 的哪些函数
dumpbin /symbols mylib.lib     # 查看 .lib 里的符号
dumpbin /dependents main.exe   # 列出依赖的 DLL(类似 ldd)
dumpbin /disasm mylib.dll      # 反汇编
1
2
3
4
5
6

最常用的场景:甲方给的 .lib 是静态库还是导入库?用 dumpbin /symbols 看,如果符号是 __imp_ 开头 → 导入库;如果是普通函数名 → 静态库。


核心总结 ​

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Windows 编译产物核心速查                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  .obj    编译产物(= Linux 的 .o)                                            │
│  .exe    PE 可执行文件,有入口点                                              │
│  .dll    动态链接库,没有 main,被加载                                        │
│                                                                             │
│  ⚠️ .lib 有两种,完全不同:                                                   │
│    静态库 = .obj 的 tar 包,链接时嵌入代码                                    │
│    导入库 = DLL 的目录,链接时只记录"这个符号去 XXX.dll 里找"                 │
│    区分方法:dumpbin /symbols → 有 __imp_ = 导入库                           │
│                                                                             │
│  dllexport / dllimport 宏模式 → 控制符号导出/导入                            │
│  .def 文件 → 精确控制导出(序号导出、隐藏函数名)                             │
│  .pdb → 调试信息(调试时必须,发布时可不带)                                  │
│                                                                             │
│  /MT = 静态链接 CRT → 每个模块有自己的 CRT                                   │
│  /MD = 动态链接 CRT → 所有模块共享 CRT                                      │
│  绝对不能混用 /MT 和 /MD!跨模块 new/delete 会崩溃                           │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

章节测试 ​

测试1:两种 .lib ​

Windows 上的 .lib 文件有哪两种?它们的本质区别是什么?你怎么区分一个 .lib 文件是哪种?

测试2:导入库的工作原理 ​

导入库(Import Library)里有 add 函数的实际代码吗?程序运行时 add 的实际代码是怎么被找到的?

测试3:dllexport/dllimport 宏模式 ​

下面这段代码有什么作用?MYLIB_EXPORTS 应该在什么时候被定义?

cpp
#ifdef MYLIB_EXPORTS
    #define MYLIB_API __declspec(dllexport)
#else
    #define MYLIB_API __declspec(dllimport)
#endif
1
2
3
4
5

测试4:甲方给文件场景 ​

甲方给了你 engine.h、engine.lib、engine.dll 三个文件。请说明:

  • 编译你的程序时,需要哪些文件?
  • 运行你的程序时,需要哪些文件?
  • engine.lib 是哪一种 .lib?

测试5:/MT vs /MD ​

为什么 /MT 编译的 .exe 和 /MD 编译的 .dll 一起使用可能会导致崩溃?具体举一个会崩溃的场景。

测试6:工具使用 ​

你拿到一个 .dll 文件,想确认它导出了哪些函数,用什么命令?


参考答案 ​

测试1答案 ​

答案:

  1. 静态库:.obj 文件的集合(类似 tar 包),链接时把实际代码嵌入到目标 .exe/.dll 中。
  2. 导入库:DLL 的"符号目录",只包含 stub 代码和导入信息,不含实际实现。

区分方法:用 dumpbin /symbols foo.lib 查看——如果有 __imp_ 前缀的符号,是导入库;如果看到完整函数名(非 __imp_),是静态库。也可以根据大小和是否有伴随的 .dll 来判断。

测试2答案 ​

答案:不包含。导入库只包含指向 IAT 的跳转 stub。运行时:

  1. PE 加载器加载 .exe,发现依赖 .dll
  2. 加载 .dll 到进程地址空间
  3. 从 .dll 的导出表获取 add 的实际地址
  4. 将地址填入 .exe 的 IAT 表
  5. 程序调用 add 时通过 __imp_add 间接跳转到 IAT 中记录的地址

测试3答案 ​

答案:这是 Windows DLL 开发的经典宏模式。MYLIB_EXPORTS 应该在编译 DLL 本身时定义(通过 CMake 的 PRIVATE 宏定义或编译器命令行 -DMYLIB_EXPORTS)。编译 DLL 时,MYLIB_API 展开为 __declspec(dllexport),把符号放入导出表。使用者 include 头文件时(没有定义 MYLIB_EXPORTS),MYLIB_API 展开为 __declspec(dllimport),告诉编译器符号来自外部 DLL。

测试4答案 ​

答案:

  • 编译时需要:engine.h(声明) + engine.lib(导入库,确认符号存在)
  • 运行需要:你的 .exe + engine.dll(实际代码)
  • engine.lib 是导入库(因为有伴随的 .dll)

如果运行时报"找不到 engine.dll",需要把 engine.dll 放到 .exe 同目录或 PATH 中。

测试5答案 ​

答案:崩溃场景:.dll(/MT 编译)内部用 new 在它自己的 CRT 堆上分配了一个对象,然后返回指针给 .exe。.exe(/MD 编译)用完后 delete 这个指针,但它的 delete 调用的是共享 CRT 的堆管理器。共享 CRT 的堆不认识这个地址(它不在共享堆上),导致 heap corruption 或直接崩溃。铁律:所有模块(.exe + .dll)必须统一 CRT 链接方式。

测试6答案 ​

答案:dumpbin /exports foo.dll,这会列出所有导出函数的名字、序号(ordinal)和 RVA(相对虚拟地址)。


相关笔记 ​

  • cpp project fundamentals - C++ 项目工作机制全景
  • linux build outputs - Linux 编译产物详解
  • static vs dynamic linking - 静态链接 vs 动态链接深度对比
  • cpp abi compatibility - C++ ABI 兼容性

下一步学习 ​

  • [ ] 阅读 02 - Linux 编译产物详解 —— 对应理解 Linux 侧的 .o/.a/.so

学习状态:🟡 开始学习

工程深化:把本篇知识落到项目里 ​

这一节不是为了凑篇幅,而是把《Windows 编译产物详解:exe、DLL、lib —— 彻底分清两种 lib / Windows Build Artifacts: EXE, DLL, and the Two Kinds of LIB》从“知道概念”推进到“能在项目里稳定使用”。 阅读时可以把每个知识点都追问成四件事:它保护什么边界,失败时有什么现象,怎样最小复现,怎样写进团队流程。

text
concept -> boundary -> failure signal -> minimal proof -> project rule
1

工程切片 1:最小可复现样例 ​

**场景。**围绕 DLL 建一个很小的案例,不要一开始就放进完整业务系统。把问题压缩到一个可以提交给同事的目录,保留源码、构建命令、版本输出和预期现象。

**边界。**先判断这里讨论的是 DLL、导入库 还是 PDB 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> DLL -> 导入库 -> PDB -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

本篇收束 ​

掌握《Windows 编译产物详解:exe、DLL、lib —— 彻底分清两种 lib / Windows Build Artifacts: EXE, DLL, and the Two Kinds of LIB》的标志,不是记住所有名词,而是能把 DLL、导入库、PDB、运行库、导出符号、部署目录 放进一条可验证的工程链路。 先用最小样例建立判断,再用测试和脚本固定判断,最后把失败证据留给未来的自己和团队。

最后更新于:

Pager
下一篇← C++ 编程 / C++ Programming

持续记录,持续成长

Copyright © Tidenflow