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

CppCon 2017 学习:Everything You Ever Wanted to Know about DLLs

这是关于 DLL(Dynamic Link Library,动态链接库) 的简明介绍。以下是内容的中文整理与理解

什么是 DLL?

DLL 是 “动态链接库(Dynamic Link Library)” 的缩写,其本质是一个可重用的 代码和数据容器。特点如下:

  • 是一种库文件,通常以 .dll 为扩展名。
  • 可以在程序运行时动态加载(不是编译时静态链接)。
  • 多个程序可以共享其中的代码和数据,无需各自保存副本。

为什么使用 DLL?

使用 DLL 有许多优势,尤其在大型系统开发中非常重要:

✳ 共享资源

  • 多个程序共享相同的库文件,节省 磁盘空间和内存使用

延迟加载(Lazy Loading)

  • 某些功能不是一直都需要,可以在运行时决定是否加载。
    • 示例:插件系统、模块化组件。

可维护性与可扩展性

  • 组件化架构(Componentization):将功能分成多个 DLL,结构更清晰。
  • 独立更新:修补 bug 或安全问题时,只需替换单独的 DLL 文件,无需重新发布整个程序。
  • 支持插件系统或动态扩展(如浏览器插件、图形引擎模块等)。

为什么不使用 DLL?

虽然 DLL 很强大,但也存在一些劣势或复杂性:

分发复杂

  • 安装分发变复杂,不如单个 EXE 文件简单易用。

DLL 地狱(DLL Hell)

  • 版本冲突问题:系统中多个程序依赖不同版本的同一个 DLL,容易出错或程序崩溃。

性能限制

  • 跨 DLL 调用不能进行编译器优化
    • 函数调用是间接跳转(indirect call),比静态调用慢。
    • 无法内联(inline)优化或跨模块优化。

总结

优点缺点
节省资源部署更复杂
延迟加载存在兼容性风险
组件化结构性能不可控
更容易维护和升级调试更难

从零开始构建和使用 DLL 的完整示例。我们一起来逐步解析这个过程,并附上中文说明,帮助你彻底理解。

目标

创建一个简单的 DLL(Hello.dll),它暴露一个函数:

extern "C" char const* __cdecl GetGreeting()
{return "Hello, C++ Programmers!";
}

步骤讲解(含中文注释)

第一步:编写源文件 Hello.cpp

// Hello.cpp
extern "C" char const* __cdecl GetGreeting()
{return "Hello, C++ Programmers!";
}
  • extern "C":避免 C++ 名字修饰(name mangling),使函数名在 DLL 中可以被其他语言或程序识别。
  • __cdecl:调用约定,说明栈的清理方式(Windows 常用)。

第二步:编译成目标文件(.obj)

在命令行输入:

cl /c Hello.cpp
  • cl 是微软的 C++ 编译器命令。
  • /c 表示只编译,不链接。
  • 生成的文件是 Hello.obj

第三步:链接为 DLL

link Hello.obj /DLL /NOENTRY /EXPORT:GetGreeting

含义解释:

  • /DLL:告诉链接器要创建一个 DLL。
  • /NOENTRY:告诉链接器没有 DllMain 入口点(我们只导出函数,不做初始化)。
  • /EXPORT:GetGreeting:导出函数名 GetGreeting 供外部调用。
    生成结果:
  • Hello.dll:动态链接库。
  • Hello.lib:用于静态链接的导入库。
  • Hello.exp:导出符号表。

第四步(误区):尝试直接运行 DLL

A:\> Hello.dll
The system cannot execute the specified program.

解释:

DLL 不能直接运行!

DLL 是供程序“调用”的,不是用来独立执行的可执行文件(EXE)。

正确使用:创建一个调用 DLL 的程序

编写 PrintGreeting.cpp

// PrintGreeting.cpp
#include <windows.h>
#include <iostream>
typedef const char* (__cdecl* GetGreetingFunc)();
int main()
{// 加载 DLLHMODULE hDLL = LoadLibrary("Hello.dll");if (!hDLL) {std::cerr << "无法加载 DLL" << std::endl;return 1;}// 获取函数地址GetGreetingFunc GetGreeting = (GetGreetingFunc)GetProcAddress(hDLL, "GetGreeting");if (!GetGreeting) {std::cerr << "无法获取函数地址" << std::endl;return 1;}// 调用函数std::cout << GetGreeting() << std::endl;// 卸载 DLLFreeLibrary(hDLL);return 0;
}

编译运行:

cl PrintGreeting.cpp
PrintGreeting.exe

输出:

Hello, C++ Programmers!

总结

步骤操作说明
1Hello.cppDLL 中的函数
2cl /c Hello.cpp编译成对象文件
3link Hello.obj /DLL /EXPORT:函数名链接为 DLL
4PrintGreeting.cpp调用 DLL 中函数
5编译运行加载 DLL 并调用函数

下面是一个 简单的 CMake 配置,用于构建你刚才提到的 Hello.dllPrintGreeting.exe 程序。

目录结构建议

MyProject/
├── CMakeLists.txt
├── Hello/
│   ├── CMakeLists.txt
│   └── Hello.cpp
└── PrintGreeting/├── CMakeLists.txt└── PrintGreeting.cpp

根目录 CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(MyDLLDemo)
add_subdirectory(Hello)
add_subdirectory(PrintGreeting)

Hello/CMakeLists.txt —— 构建 DLL

add_library(Hello SHARED Hello.cpp)
# 设置导出函数名(Windows 平台需要明确)
# 你可以用 DEF 文件或者直接导出函数(这例中直接导出)
target_compile_definitions(Hello PRIVATE EXPORTING_HELLO_DLL
)

Hello/Hello.cpp

// Hello.cpp
extern "C" __declspec(dllexport) const char* __cdecl GetGreeting()
{return "Hello, C++ Programmers!";
}

注意 __declspec(dllexport):用于明确告诉编译器导出这个函数(不使用 /DEF 文件时)。

PrintGreeting/CMakeLists.txt —— 构建 EXE

add_executable(PrintGreeting PrintGreeting.cpp)
# 链接 DLL 的 import lib
target_link_libraries(PrintGreeting PRIVATE Hello)

PrintGreeting/PrintGreeting.cpp

#include <windows.h>
#include <iostream>
typedef const char* (__cdecl* GetGreetingFunc)();
int main() {HMODULE hDLL = LoadLibraryA("Hello.dll"); // 注意是 .dll 文件名if (!hDLL) {std::cerr << "无法加载 DLL" << std::endl;return 1;}GetGreetingFunc GetGreeting = (GetGreetingFunc)GetProcAddress(hDLL, "GetGreeting");if (!GetGreeting) {std::cerr << "无法获取函数地址" << std::endl;return 1;}std::cout << GetGreeting() << std::endl;FreeLibrary(hDLL);return 0;
}

构建命令

mkdir build
cd build
cmake ..
cmake --build .

构建结果说明

  • Hello.dll 会在 build/Hello/ 生成
  • PrintGreeting.exe 会在 build/PrintGreeting/ 生成
  • 运行 PrintGreeting.exe 前,请确保 Hello.dll 和它在同一目录下,或设置好 PATH
    你可以用以下命令拷贝 DLL:
copy Hello/Hello.dll PrintGreeting/

CMakePresets.json

{"version": 3,"configurePresets": [{"name": "windows-base","hidden": true,"generator": "Ninja","binaryDir": "${sourceDir}/out/build/${presetName}","installDir": "${sourceDir}/out/install/${presetName}","cacheVariables": {"CMAKE_C_COMPILER": "cl.exe","CMAKE_CXX_COMPILER": "cl.exe"},"condition": {"type": "equals","lhs": "${hostSystemName}","rhs": "Windows"}},{"name": "x64-debug","displayName": "x64 Debug","inherits": "windows-base","architecture": {"value": "x64","strategy": "external"},"cacheVariables": {"CMAKE_BUILD_TYPE": "Debug"}},{"name": "x64-release","displayName": "x64 Release","inherits": "x64-debug","cacheVariables": {"CMAKE_BUILD_TYPE": "Release"}},{"name": "x86-debug","displayName": "x86 Debug","inherits": "windows-base","architecture": {"value": "x86","strategy": "external"},"cacheVariables": {"CMAKE_BUILD_TYPE": "Debug"}},{"name": "x86-release","displayName": "x86 Release","inherits": "x86-debug","cacheVariables": {"CMAKE_BUILD_TYPE": "Release"}}]
}
PS C:\Users\16956\Documents\game\CppCon\out\build\x64-debug\day163\code> tree /F
卷 OS 的文件夹 PATH 列表
卷序列号为 4C98-E53B
C:.
├─Hello
│  │  cmake_install.cmake
│  │  Hello.dll
│  │  Hello.exp
│  │  Hello.ilk
│  │  Hello.lib
│  │  Hello.pdb
│  │
│  └─CMakeFiles
│      └─Hello.dir
│              embed.manifest
│              Hello.cpp.obj
│              intermediate.manifest
│              manifest.rc
│              manifest.res
│              vc140.pdb
│
└─PrintGreeting│  cmake_install.cmake│  PrintGreeting.exe│  PrintGreeting.ilk│  PrintGreeting.pdb│└─CMakeFiles└─PrintGreeting.dirembed.manifestintermediate.manifestmanifest.rcmanifest.resPrintGreeting.cpp.objvc140.pdb
PS C:\Users\16956\Documents\game\CppCon\out\build\x64-debug\day163\code>

你构建了一个动态链接库(DLL) Hello.dll

并且它提供了一个函数:

extern "C" char const* __cdecl GetGreeting()
{return "Hello, C++ Programmers!";
}

这个 DLL 编译之后提供一个导出函数 GetGreeting(),返回一个字符串。

然后你编写了一个主程序 PrintGreeting.cpp

它使用 Windows API 手动加载 DLL 并调用其中的函数:

#include <stdio.h>
#include <Windows.h>
int main() {// 加载 DLLHMODULE const HelloDll = LoadLibraryExW(L"Hello.dll", nullptr, 0);// 定义函数指针类型using GetGreetingType = char const* (__cdecl*)();// 获取函数地址GetGreetingType const GetGreeting = reinterpret_cast<GetGreetingType>(GetProcAddress(HelloDll, "GetGreeting"));// 调用函数并打印结果puts(GetGreeting());// 卸载 DLLFreeLibrary(HelloDll);
}

运行 PrintGreeting.exe 成功输出:

Hello, C++ Programmers!

所以你已经成功实现了:

  • 动态链接库 Hello.dll
  • 导出函数 GetGreeting
  • 主程序 PrintGreeting.exe 动态加载该 DLL 并调用其函数
    这演示了 动态链接(DLL)机制在 Windows 下的实际使用,也是 DLL 编程的最小可运行示例之一。

后续建议(可选):

如果你要写更大项目,建议使用 CMake 管理构建流程,你可以这样写:

CMakeLists.txt 示例
cmake_minimum_required(VERSION 3.15)
project(HelloDLL)
add_library(Hello SHARED Hello.cpp)
set_target_properties(Hello PROPERTIES PREFIX "" SUFFIX ".dll")
add_executable(PrintGreeting PrintGreeting.cpp)
target_link_libraries(PrintGreeting PRIVATE Hello)

注意:在使用 LoadLibraryExW 手动加载 DLL 的时候,CMake 不需要负责链接 DLL,但你可以通过 target_link_libraries 来声明依赖(适合直接链接 .lib 的情况)。

这是在探索 Windows 可执行文件格式(PE 格式),特别是 .dll 文件的结构。我们来一步一步解释你看到的内容:

你看到了什么?

你输入了命令:

type Hello.dll

结果显示一堆乱码,开头是:

MZ...
This program cannot be run in DOS mode.
...
Rich...
PE..

这说明你正在“用文本方式”查看一个二进制文件(DLL)。这些乱码其实是有结构的数据,它是符合 PE(Portable Executable)文件格式的。

DLL 文件结构解析(PE 文件格式)

每一个 .dll.exe 都是基于 PE 文件格式,其结构如下:

[ DOS Stub ]
[ PE Signature ]
[ COFF File Header ]
[ Optional Header ]
[ Section Table ]
[ Sections (.text, .data, .rdata, etc.) ]

1. MZ — DOS 头(DOS Stub)

  • 所有 Windows 可执行文件的开头是 MZ(0x4D 0x5A),表示这是一个可执行文件。
  • 紧随其后是一个 DOS 模拟程序,它只是简单打印一句:
    This program cannot be run in DOS mode.
    

2. Offset 0x3C 处的值(通常是 C8 或类似)是一个偏移量:

  • 它告诉你 PE Header(真正的 Windows 结构体)从哪里开始。
  • 在你的示例中,这个偏移处的值为 C8,所以 PE header 从偏移 0xC8 开始。

3. PE\0\0 — PE Signature

  • 从 offset 0xC8 开始应该看到:50 45 00 00,即 PE\0\0
  • 这就是 PE 文件的“正式入口”。

Section Table 与内容

从 PE Header 开始,后面有一系列的部分(节):

  • .text:代码段(你的 GetGreeting() 函数就在这里)
  • .rdata:只读数据段(字符串文字如 "Hello, C++ Programmers!"
  • .edata:导出表(DLL 的导出函数名就存这)
  • .idata:导入表(如果有调用别的 DLL)
  • .rsrc:资源段(图标、对话框等)
    你在十六进制 dump 中看到的 .text, .rdata, .edata,这些其实都是这些段的标记。

为啥看到 Rich

这个是微软的 Visual Studio linker 添加的 “Rich Header”,介于 DOS Stub 和 PE Signature 之间的一段非文档化结构,存储编译器工具链信息。对程序运行无影响,只是调试工具能用。

总结:你做了什么?

你从字节层面 分析了 DLL 文件结构,识别出了:

  • MZ 标志
  • DOS stub
  • PE 签名位置
  • .text, .rdata, .edata 等节的名称
  • GetGreeting 函数名的存在(导出表)
  • "Hello, C++ Programmers!" 字符串也在 .rdata

如果你想继续深入:

你可以使用更专业的工具查看 DLL 内部结构,比如:

  • dumpbin /exports Hello.dll(查看导出函数)
  • Dependency Walker(分析依赖)
  • PE ExplorerCFF Explorer(图形化 PE 查看器)
  • objdumpIDA(反汇编器)

给出的 dumpbin /headers Hello.dll 输出内容,涉及到PE文件(Portable Executable,Windows可执行文件格式)结构的几个关键点,我帮你用中文总结和解释一下:

1. PE签名(PE signature found)

  • 表示这是一个合法的PE文件。

2. 文件类型(File Type: DLL)

  • 这是一个动态链接库(DLL)文件。

3. 文件头(FILE HEADER VALUES)

  • 机器类型(machine): 8664,代表这是x64架构的文件(64位)。
  • 节(sections)数量: 2个节(sections),节是PE文件中存放代码、数据等的单位。
  • 时间戳: 编译时间,Sat Sep 16 20:04:17 2017。
  • 可选头大小: F0(十六进制)即240字节大小。
  • 特性(characteristics):
    • 可执行文件(Executable)
    • 支持大于2GB的地址空间(Large Address Aware)
    • 是一个DLL文件(DLL)

4. 可选头(OPTIONAL HEADER VALUES)

  • Magic # (魔数): 20B,代表这是PE32+格式,即64位PE格式。
  • 入口点(Entry Point): 0,可能表示DLL的入口点为0(通常DLL入口函数)。
  • 镜像基址(Image Base): 70000000,这是DLL被加载时的默认基址(内存地址)。
  • 节对齐(Section Alignment): 0x1000,即4096字节。
  • 文件对齐(File Alignment): 0x200,即512字节。
  • 镜像大小(Size of Image): 0x3000(12KB),整个映像大小。
  • 头大小(Size of Headers): 0x400(1KB)。
  • DLL特性(DLL Characteristics):
    • 支持高熵虚拟地址空间(High Entropy Virtual Addresses)
    • 支持动态重定位(Dynamic Base)
    • 支持NX保护(NX Compatible,即防止执行非代码区)

5. 目录表(Data Directories)

  • 导出表(Export Directory): RVA=2040,大小48字节。DLL对外导出的函数信息。
  • 导入表(Import Directory): RVA=2020,大小0,表示没有导入(不一定准确,可能需要进一步确认)。
  • 资源目录(Resource Directory)、异常目录(Exception Directory)等: 大多为0,说明该DLL可能不包含资源或异常目录。

6. 地址空间示意

  • 这个DLL被加载到虚拟地址 0x70000000 开始的空间。
  • 这里也有堆(Heap)、线程堆栈、程序等不同的虚拟地址空间划分。

总结

这个DLL是一个64位的Windows动态链接库,符合PE32+格式,具有现代的安全特性(如NX保护和高熵地址空间支持)。它有2个节,体积较小(12KB)。导出目录里有函数对外暴露的信息,但导入目录是空的或者没有明显导入其他DLL的函数。
如果你想深入理解PE文件结构,这个输出展示了从DOS头到PE头,再到COFF头,最后到可选头的详细信息。

你的这段描述是关于进程虚拟地址空间中各个区域的排布,特别是DLL和线程栈在内存中的相对位置。下面帮你用中文详细讲解这两段内容的含义和区别:

1. 第一张内存布局图

0xF..F  Some Other Data
----------------------------------------
----------------------------------------Thread T0 StackThread T1 Stack
----------------------------------------
----------------------------------------Hello.dll         0x7000’0000
----------------------------------------PrintGreeting.exe
----------------------------------------
----------------------------------------Heap
0x0..0
  • Heap(堆) 在虚拟地址空间的低端,地址从 0x0..0 开始向上。
  • PrintGreeting.exe 是主程序,位于堆上方。
  • Hello.dll 期望加载在 0x7000’0000 这个基址。
  • Thread T0 Stack 和 T1 Stack 在 DLL 上方,说明线程的栈空间占用这个区域。
  • Some Other Data 处于地址空间的最高端(0xF…F 表示虚拟地址的高位段),可能是其他系统数据、映射内存等。
    重点: 这幅图显示的是DLL的默认加载基址 0x70000000,但它的上方有线程栈已经占用空间,意味着不能在这里加载DLL,因为地址冲突。

2. 第二张内存布局图

0xF..F  Some Other Data
----------------------------------------
----------------------------------------Hello.dll         0x7000’0000
----------------------------------------        
----------------------------------------Thread T0 StackThread T1 StackThread T2 StackThread T3 Stack
----------------------------------------PrintGreeting.exe
----------------------------------------
----------------------------------------Heap
0x0..0
  • 这张图展示的情况是,DLL仍然在 0x70000000 加载,但线程栈分布变化了:
    • 线程栈的数量从2个增加到了4个(T0~T3)。
    • 线程栈区域现在位于 DLL 的下方。
  • 注意: 线程栈和 DLL 之间空间的调整表明系统在虚拟地址空间里重新规划了线程栈和DLL的加载位置。

你的理解重点

  • 进程的虚拟地址空间是动态划分和调整的。
  • DLL 默认基址是 0x70000000,如果有冲突(比如线程栈占用了这个位置),系统会重定位 DLL到别的地址。
  • 线程栈(Thread Stack)可能在 DLL 上方或者下方,根据系统和线程创建情况有所变化。
  • 堆(Heap) 总是在较低地址空间。
  • Some Other Data 代表其它内核或系统占用的高地址空间。

这里是一个简洁的示意图,帮你快速理解DLL和线程栈在进程虚拟地址空间中的相对位置

虚拟地址空间(高地址 ↓)
+--------------------------+
| Some Other Data (系统数据) |
+--------------------------+
|      Thread T3 Stack      |
+--------------------------+
|      Thread T2 Stack      |
+--------------------------+
|      Thread T1 Stack      |
+--------------------------+
|      Thread T0 Stack      |
+--------------------------+
|        Hello.dll          | ← DLL默认加载基址(如 0x70000000)
+--------------------------+
|      PrintGreeting.exe    | ← 主程序映像
+--------------------------+
|           Heap           | ← 堆空间(低地址方向)
+--------------------------+
虚拟地址空间(低地址 ↑)

重点提示:

  • DLL一般加载在某个固定的基址(例如0x70000000),但如果有地址冲突,可能会被重定位。
  • 线程栈在DLL附近,有时在线程多时,栈的空间会扩展,可能在DLL上下都有。
  • 堆位于低地址区,主程序映像在堆和DLL之间。
  • 最高地址区(Some Other Data)是系统占用空间。

你的这段内容详细解释了DLL的地址布局、RVA(Relative Virtual Address,相对虚拟地址)的意义,以及DLL在内存中的映射和节(section)结构。下面帮你用中文整理理解重点:

RVA(相对虚拟地址)的含义

  • RVA = 内存地址 - DLL基址(Image Base)
  • 也就是说,RVA是从DLL加载基址开始算起的偏移量。
  • 计算公式:
    • 内存地址 = DLL基址 + RVA
    • 例如:函数GetGreeting()的RVA是0x2000,DLL加载地址是0x70000000,那么函数地址就是0x70002000

DLL加载后的内存布局和节(Section)结构

Optional Header (可选头部)

  • Magic = 20B 表示是PE32+格式(64位)
  • Entry point = 0(无明确入口点,DLL通常没有主入口)
  • Image base = 0x70000000(DLL期望加载的基址)
  • Section alignment = 0x1000(4KB对齐)
  • File alignment = 0x200(512字节对齐)
  • Size of image = 0x3000(DLL整体占用内存大小3页,每页4KB)
  • Number of directories = 10(包含导出表、导入表、资源表、调试表等)

主要节区(Sections)

节名称虚拟大小虚拟地址 (RVA)文件中偏移权限描述
.text8 字节0x1000 (0x70001000起)0x400代码区,执行+读
.rdata0xD8字节0x2000 (0x70002000起)0x600只读数据区
  • DLL在内存中占用3页:
    • 1页放头部(headers)
    • 1页放.text节(代码)
    • 1页放.rdata节(只读数据,包括导出目录和调试目录等元数据)

额外信息

  • 导出目录(Export Directory)和调试目录(Debug Directory)数据包含在.rdata节中。
  • 这两项目录是DLL的重要元数据,分别用于导出函数地址和调试信息。

总结

  1. RVA是相对基址的偏移,定位DLL内存中的函数和数据位置。
  2. DLL由多个节组成,常见有代码节(.text)和只读数据节(.rdata)。
  3. 加载时,DLL整体映射成连续的虚拟内存页,包含头部、代码、数据等。
  4. 导出目录和调试目录存放在.rdata节内,便于外部程序查找函数和调试信息。

这段内容是一个用 Windows 工具 dumpbin 分析 DLL 文件结构的示例,重点说明了 DLL 的各个部分如何映射到内存地址,以及如何通过 RVA 访问函数和数据。帮你总结理解要点:

1. DLL 文件结构关键部分

  • DOS Stub
    这是为了兼容旧的 DOS 程序,能显示“这个程序不能在 DOS 下运行”之类的信息,实际现在没什么用。
  • PE Signature(PE标记)
    4字节,固定为 “PE\0\0”,表示这是个 Windows Portable Executable 文件。
  • COFF File Header
    标准的文件头,包含机器类型、节数等信息。
  • Optional Header(可选头部)
    包含镜像基址、入口点、节对齐等重要参数。
  • Section Headers(节头)
    描述各节的位置、大小、权限,.text是代码段,.rdata是只读数据段等。
  • Sections(节)
    包含代码、数据和资源等内容。

2. 示例中主要节的内容和分析

.text 节(代码段)

  • RVA = 0x1000,实际加载地址 = Image Base + RVA = 0x70000000 + 0x1000 = 0x70001000。
  • 代码示例:
    70001000: 48 8D 05 F9 0F 00 00  lea rax,[70002000h]
    70001007: C3                    ret
    
  • 这段代码是 GetGreeting() 函数,执行一个lea(装载有效地址)指令,把 0x70002000 地址加载到寄存器 rax,然后返回。

.rdata 节(只读数据段)

  • RVA = 0x2000,加载后地址 = 0x70002000。
  • 包含导出目录、调试目录和字符串等数据。
  • 示例数据是字符串 "Hello, C++ Programmers!\0",即DLL导出的问候语。

3. 导出目录与函数地址

  • 通过 dumpbin /exports Hello.dll 得到导出函数列表。
  • GetGreeting() 的RVA是 0x1000,实际地址是 0x70001000
  • 结合上面的反汇编代码,函数会返回指向 .rdata 段中字符串的指针。

4. 结合理解

  • DLL的基址为 0x70000000
  • 函数在 .text 节,数据(如字符串)在 .rdata 节。
  • 函数使用RVA寻址数据,实现功能。
  • 通过工具能看到文件的二进制内容和反汇编结果。

总结

  1. RVA 是相对DLL基址的偏移,方便定位函数和数据。
  2. 代码节 .text 包含函数实现,.rdata 包含只读数据如字符串和导出目录。
  3. 导出表 显示DLL暴露的函数名及其RVA。
  4. 反汇编 能看函数实现细节,比如 GetGreeting() 返回字符串指针。
  5. dumpbin工具 是查看PE结构和内容的强大工具。

你提供的这段代码示例,是典型的 显式链接(Explicit Linking) 用法,用来演示如何动态加载 DLL 并调用其中的函数。帮你详细讲解一下:

显式链接(Explicit Linking)

代码重点

#include <stdio.h>
#include <Windows.h>
int main()
{// 1. 动态加载 DLLHMODULE const HelloDll = LoadLibraryExW(L"Hello.dll", nullptr, 0);// 2. 定义函数指针类型(返回 const char*,调用约定 __cdecl)using GetGreetingType = char const* (__cdecl*)();// 3. 获取 DLL 中函数地址GetGreetingType const GetGreeting =reinterpret_cast<GetGreetingType>(GetProcAddress(HelloDll, "GetGreeting"));// 4. 调用函数,并打印返回的字符串puts(GetGreeting());// 5. 卸载 DLLFreeLibrary(HelloDll);
}

逐步解析

  1. LoadLibraryExW
    • 这个函数动态加载名为 "Hello.dll" 的 DLL 文件。
    • 成功时返回一个模块句柄(HMODULE),失败返回 NULL
  2. 定义函数指针类型
    • DLL中的 GetGreeting() 函数返回一个 const char* 字符串,调用约定是 __cdecl
    • 这里用 using 定义了对应的函数指针类型。
  3. GetProcAddress
    • 通过模块句柄和函数名 "GetGreeting" 获取函数在 DLL 内的地址。
    • 然后用 reinterpret_cast 把它转换成定义好的函数指针类型。
  4. 调用函数和打印结果
    • 通过函数指针调用 GetGreeting(),返回字符串指针。
    • puts 输出字符串(如前面分析,应该是 "Hello, C++ Programmers!")。
  5. FreeLibrary
    • 卸载 DLL,释放资源。

这就是 显式链接 的典型流程:

  • 应用程序运行时才决定是否加载 DLL。
  • 需要手动调用 LoadLibraryGetProcAddress
  • 优点是灵活,可以根据情况加载不同的 DLL 或不同的函数。
  • 缺点是代码更复杂,需要管理 DLL 句柄和函数指针。

相比之下:隐式链接(Implicit Linking)

  • 编译时就链接 .lib 导入库文件。
  • 程序启动时操作系统自动加载 DLL。
  • 程序可以直接调用 DLL 函数,不需要手动加载和获取函数指针。

显式链接(Explicit Linking)隐式链接(Implicit Linking),以及它们在实际程序中表现出的依赖关系。帮你总结和讲解一下:

1. 显式链接(Explicit Linking)回顾

  • 代码示例:
#include <stdio.h>
#include <Windows.h>
int main()
{HMODULE const HelloDll = LoadLibraryExW(L"Hello.dll", nullptr, 0);using GetGreetingType = char const* (__cdecl*)();GetGreetingType const GetGreeting =reinterpret_cast<GetGreetingType>(GetProcAddress(HelloDll, "GetGreeting"));puts(GetGreeting());FreeLibrary(HelloDll);
}
  • 这里程序 自己调用 LoadLibraryExWGetProcAddress 来动态加载 DLL 和获取函数地址。
  • 依赖关系仅限于系统 DLL(如 KERNEL32.dll),你的程序对 Hello.dll 只是通过运行时加载,不是链接时绑定。

2. 通过 dumpbin /dependents PrintGreeting.exe 查看依赖:

Image has the following dependencies:
KERNEL32.dll
  • 只有系统 DLL KERNEL32.dll 被列为依赖,说明 Hello.dll 并非隐式链接的依赖。
  • 这是显式链接的典型特征:运行时才加载 DLL,编译时不需要知道 DLL。

3. 查看导入函数(dumpbin /imports PrintGreeting.exe):

  • 看到的都是来自 KERNEL32.dll 的函数,如 LoadLibraryExWGetProcAddressFreeLibrary 等。
  • 说明程序是通过调用系统 API 来实现显式加载。

4. 隐式链接(Implicit Linking)示例

#include <stdio.h>
extern "C" char const* __cdecl GetGreeting();
int main()
{puts(GetGreeting());
}
  • 这里程序声明了 DLL 中的 GetGreeting 函数,编译时链接器会用到 DLL 的导入库(.lib 文件)。
  • 运行时 Windows 会自动加载 Hello.dll,不需要程序员手动调用 LoadLibrary

5. 隐式链接的依赖关系

  • 通过 dumpbin /dependents,会看到程序除了系统 DLL,还会依赖 Hello.dll
  • 因为链接时绑定了 DLL,所以运行时必须能找到 DLL,否则程序启动失败。

6. 导入库(Import Libraries)

  • 隐式链接用到的 .lib 文件实际上只是“导入库”,不包含代码,只告诉链接器:
    • 该函数在某个 DLL 中
    • 运行时系统帮你加载 DLL 和绑定函数地址
  • 你编译时链接这个导入库,程序启动时由系统自动加载 DLL。

总结

特点显式链接隐式链接
依赖声明只依赖系统 DLL依赖系统 DLL + 目标 DLL
加载时间运行时由程序调用 LoadLibrary 加载程序启动时自动加载 DLL
函数调用通过 GetProcAddress 获取函数地址调用直接调用函数,链接器解析
灵活性高,可动态选择加载的 DLL低,必须有 DLL 否则启动失败
编写复杂度高,需要手动管理 DLL 加载卸载和函数地址低,写法简单,像普通函数调用

你这段流程非常完整地展示了 隐式链接(Implicit Linking) 的典型过程和细节,我帮你梳理总结一下,方便你更清晰理解:

1. 创建 DLL 和导入库

link Hello.obj /DLL /NOENTRY /EXPORT:GetGreeting
  • 这条命令:
    • 生成了 Hello.dll 动态库
    • 并且生成了 Hello.lib,这其实是一个导入库(Import Library),它不包含代码,只包含符号和指向 DLL 的信息。
  • dumpbin /exports Hello.lib 显示了导出的函数 GetGreeting

2. 导入库的结构(dumpbin /all Hello.lib

  • 可以看到导入库里:
    • 定义了导入符号 __imp_GetGreeting
    • 它是一个“魔法”指针,链接器和加载器用它来定位实际 DLL 中的函数 GetGreeting
  • 伪代码表示:
extern "C" char const* __cdecl GetGreeting()
{return __imp_GetGreeting();  // 实际调用 DLL 中函数
}

3. 隐式链接调用示例

#include <stdio.h>
extern "C" char const* __cdecl GetGreeting();
int main()
{puts(GetGreeting());
}
  • 这个程序直接调用 GetGreeting,编译时链接器用到了 Hello.lib 导入库。
  • 运行时,系统自动加载 Hello.dll,绑定 GetGreeting 的地址。

4. 编译和链接程序:

link PrintImplicit.obj Hello.lib
  • 把导入库链接进程序,生成的 PrintImplicit.exe 会自动依赖 Hello.dll

5. 运行和依赖检查

  • 运行程序,输出:
Hello, C++ Programmers!
  • dumpbin /dependents PrintImplicit.exe 显示依赖:
Hello.dll
KERNEL32.dll
  • dumpbin /imports PrintImplicit.exe 显示导入的函数符号:
Hello.dllGetGreeting
KERNEL32.dll...

总结

过程说明
使用 /DLL /EXPORT 生成 DLL 和导入库导出函数供隐式链接使用
导入库 .lib 是链接器的桥梁不含代码,描述 DLL 符号和地址
程序引用导入库链接运行时 Windows 自动加载 DLL
运行时动态绑定 DLL 中函数程序内直接调用函数,和调用本地函数一样
依赖工具显示程序隐式依赖 DLL,系统帮你加载

你这段操作展示了DLL 导出符号的多种方式,特别是如何用链接器参数控制导出函数的名字、顺序、以及可见性。下面帮你总结和解释:

1. 基本导出 /EXPORT

extern "C" int GetOne()   { return 1; }
extern "C" int GetTwo()   { return 2; }
extern "C" int GetThree() { return 3; }
link Numbers.obj /DLL /NOENTRY /EXPORT:GetOne /EXPORT:GetTwo /EXPORT:GetThree
  • 通过 /EXPORT 告诉链接器这些符号要导出,生成的 Numbers.dll 中这三个函数都可被外部调用。
  • dumpbin /exports Numbers.dll 会显示导出函数和它们的序号(ordinal)。

2. 导出重命名(Renamed Exports)

link Numbers.obj /DLL /NOENTRY /EXPORT:GetOne /EXPORT:GetTwo /EXPORT:GetOnePlusTwo=GetThree
  • 这里 /EXPORT:GetOnePlusTwo=GetThree 表示:
    • DLL 中实际导出函数名是 GetOnePlusTwo
    • 但是它对应的函数实现是 GetThree
  • 导出表中会出现 GetOnePlusTwo,外部调用时用这个名字,但内部其实调用 GetThree
  • 可以用这种方式隐藏或重命名符号。

3. 设定导出为私有(Private Export)

link Numbers.obj /DLL /NOENTRY /EXPORT:GetOne /EXPORT:GetTwo /EXPORT:GetThree,PRIVATE
  • ,PRIVATE 标记该导出为私有,不出现在导出表中,但依然存在于库里(链接器可以内部用)。
  • dumpbin /exports 只会列出公开的 GetOneGetTwoGetThree 不显示。
  • 这用于控制导出符号的可见性,避免暴露不想被外部调用的符号。

总结

导出方式用法示例作用
基本导出/EXPORT:GetOne直接导出函数,外部可调用
导出重命名/EXPORT:NewName=OldName用新名字导出已有函数,方便符号隐藏或重命名
私有导出/EXPORT:GetName,PRIVATE不在导出表显示,防止外部调用,符号仅供内部或链接时用

你这段展示了如何使用 模块定义文件(DEF 文件) 来控制 DLL 的导出符号。下面是详细解释:

什么是 DEF 文件?

DEF 文件(Module Definition File)是一个文本文件,向链接器明确说明一个 DLL 应该导出哪些函数、库名是什么等。
它的作用类似于在链接命令中写 /EXPORT,但更清晰、集中、可维护,特别是在导出多个符号时。

示例:Numbers.def

LIBRARY Numbers
EXPORTSGetOneGetTwo PRIVATEGetOnePlusTwo=GetThree

含义解释:

含义
LIBRARY Numbers指定 DLL 名称为 Numbers.dll
GetOne正常导出 GetOne 函数
GetTwo PRIVATE导出 GetTwo,但标记为 PRIVATE,不会在导入库中显示
GetOnePlusTwo=GetThreeGetThree 函数导出为新的名字 GetOnePlusTwo(对外公开的是这个名字)

链接命令

link Numbers.obj /DLL /NOENTRY /DEF:Numbers.def

这条命令:

  • 把 OBJ 文件变成 DLL
  • 使用 Numbers.def 文件来决定导出内容
  • 自动生成 Numbers.lib 作为导入库

验证结果

dumpbin /exports Numbers.dll

输出大致是:

OrdinalRVAName
100001000GetOne
200001020GetOnePlusTwo
300001010GetTwo
dumpbin /exports Numbers.lib

输出只包含:

  • GetOne
  • GetOnePlusTwo
    (因为 GetTwo 是 PRIVATE,所以不在导入库中出现)

使用 DEF 文件的优势

优点描述
更可读比多个 /EXPORT: 命令行参数更清晰
支持重命名导出PublicName=InternalName
支持隐藏导出PRIVATE 标志不会出现在 .lib 中
版本控制友好可放入源码仓库,方便多人协作和维护

总结

特性方式
正常导出GetOne
重命名导出GetOnePlusTwo=GetThree
私有导出GetTwo PRIVATE
使用 DEF 文件link ... /DEF:filename.def
DEF 文件是构建大型 DLL 项目的专业工具,建议在需要精细控制导出接口时使用它。

展示的是使用 __declspec(dllexport) 导出函数的方法,这是一种最常用、最直接的导出 DLL 接口的方式。以下是详细解释:

什么是 __declspec(dllexport)

__declspec(dllexport) 是 Microsoft 的扩展关键字,用来告诉编译器“这个符号需要被导出为 DLL 的接口”,用于自动生成 .dll.lib 文件中的导出符号。

示例代码回顾:

extern "C" __declspec(dllexport) int GetOne()   { return 1; }
extern "C" __declspec(dllexport) int GetTwo()   { return 2; }
extern "C" __declspec(dllexport) int GetThree() { return 3; }
  • extern "C":关闭 C++ 的名称修饰(name mangling),确保导出名称是简单的 GetOne 而不是 _Z7GetOnev 之类。
  • __declspec(dllexport):告诉编译器这些函数应导出。

编译链接命令

cl /c Numbers.cpp
link Numbers.obj /DLL /NOENTRY
  • /DLL: 构建 DLL
  • /NOENTRY: 表示没有 DllMain,对简单 DLL 有用
    这会创建:
  • Numbers.dll: 实际的动态库
  • Numbers.lib: 导入库,用于链接依赖于此 DLL 的其他模块

dumpbin 查看导出结果

dumpbin /exports Numbers.dll

输出类似:

OrdinalRVAName
100001000GetOne
200001020GetThree
300001010GetTwo
说明 DLL 中已经正确导出了这些函数。

dumpbin /directives Numbers.obj

这个命令会显示 .obj 文件中的“编译器内嵌指令”。
输出结果:

/EXPORT:GetOne
/EXPORT:GetTwo
/EXPORT:GetThree

说明编译器 自动地在 OBJ 文件中嵌入了 /EXPORT 指令,链接器看到这些指令时,会自动导出这些函数,无需写 DEF 文件或 /EXPORT: 命令行参数。

优点总结:__declspec(dllexport)

优点描述
简洁只需加个关键字,不需要额外的 DEF 文件
自动导出编译器帮你写好导出指令
支持导入(与 __declspec(dllimport) 配合)
好维护接口一目了然

缺点/注意事项

限制描述
不支持 PRIVATE 导出所有 __declspec(dllexport) 的符号都会出现在 .lib
不能重命名导出不支持 ExportedName=RealFunctionName 的语法(DEF 文件可以)
不适合导出类时的 ABI 稳定性控制导出类建议用更复杂的导出控制机制

建议使用场景

  • 小型 DLL 或实验项目
  • 简单函数导出
  • 不需要重命名、隐藏、复杂控制的项目

对比 DEF 文件导出

方式特点
__declspec(dllexport)快捷、简单、编译时控制
DEF 文件精细控制导出名称、是否隐藏、序号等更强大

已经成功演示了使用 #pragma comment(linker, "/export:...") 来控制 DLL 导出符号的方法。下面是对这个用法的完整解释:

#pragma comment(linker, "...") 是什么?

这是 Microsoft 编译器提供的一种 在源代码中插入链接器指令 的方式。它等效于在链接命令行中加参数。

示例代码解释

extern "C" int GetOne()   { return 1; }
extern "C" int GetTwo()   { return 2; }
extern "C" int GetThree() { return 3; }
#pragma comment(linker, "/export:GetOne")
#pragma comment(linker, "/export:GetTwo")
#pragma comment(linker, "/export:GetThree")
  • #pragma comment(linker, "...") 会将指定的参数传给链接器。
  • 这就像在 link 命令中写了 /EXPORT:GetOne 一样。
  • __declspec(dllexport) 不同,这种方式可以不修改函数声明本身。

编译和链接命令

cl /c Numbers.cpp
link Numbers.obj /DLL /NOENTRY

即使在 link 命令中没有手动指定 /EXPORT,这些指令已经在 .obj 文件里生效。
你用 dumpbin /directives Numbers.obj 查看到了:

Linker Directives----------------/export:GetOne/export:GetTwo/export:GetThree

查看导出函数(验证)

dumpbin /exports Numbers.dll

输出说明这 3 个函数被正确导出:

OrdinalRVAName
100001000GetOne
200001020GetThree
300001010GetTwo

优点总结

优点描述
不需要改函数声明(不像 __declspec(dllexport)
可以导出静态库中的函数
可写在 .cpp 中集中管理导出列表
支持重命名导出(/export:Alias=RealName
支持条件编译灵活控制

缺点或注意事项

限制描述
与函数分离,不够直观
不支持 PRIVATE 关键字(需要 DEF 文件)
不如 __declspec(dllexport) 显式,维护大项目时难管理

适用场景

  • 想把导出逻辑放在 .cpp 而非 .def 文件中
  • 不想污染函数签名(如第三方源代码)
  • 编写跨平台代码时,希望保留平台相关逻辑在 #pragma

高级技巧:重命名导出

#pragma comment(linker, "/export:AliasName=RealFunctionName")

这样,用户通过 AliasName 使用,实际调用的是 RealFunctionName

总结

#pragma comment(linker, "/export:...") 是一种介于命令行和 __declspec(dllexport) 之间的中间方案,适合对链接过程有细节控制需求的开发者。你已经正确掌握了它的使用方式

以下是 加载 DLL(例如 Hello.dll)时发生的全过程 的中文解释,按照操作系统实际执行顺序分步骤说明,便于你更深入理解 Windows 的 DLL 加载机制。

加载 Hello.dll 时发生了什么?

第一步:查找 Hello.dll

系统会按照一定的搜索顺序去寻找 DLL 文件:

  1. 应用程序所在目录
  2. 系统目录(如 C:\Windows\System32
  3. Windows 目录
  4. 当前工作目录
  5. PATH 环境变量中指定的目录
    如果找不到文件,LoadLibrary() 调用会失败,返回 ERROR_MOD_NOT_FOUND

第二步:将 Hello.dll 映射到内存

系统不会直接将整个 DLL 文件复制到内存,而是使用内存映射文件(memory-mapped file)技术,将 DLL 映射进进程地址空间中。

  • 会根据 DLL 的 PE 文件结构,将 .text(代码段)、.data(数据段)、.rdata(常量)等加载到指定的虚拟地址中。
  • 这一步类似“注册 DLL 到当前进程”。

第三步:加载依赖的其他 DLL

如果 Hello.dll 引用了其他 DLL(例如 KERNEL32.dll 中的函数),系统会递归地去加载这些被依赖的 DLL。
可用 dumpbin /imports Hello.dll 查看这些依赖项。
如果任何一个依赖 DLL 无法加载,整个加载过程将失败。

第四步:绑定导入函数(Import Binding)

系统接着会解析 DLL 中的导入表(Import Table),把里面的函数名映射成实际内存地址,并填充到 导入地址表(IAT) 中。
举例:

extern "C" __declspec(dllimport) int GetGreeting();

这类函数在编译时只是声明,实际地址是运行时由系统通过这个步骤填入的。
如果导入的函数找不到,系统会返回 ERROR_PROC_NOT_FOUND

第五步:调用入口函数(DllMain)

如果 DLL 中定义了入口函数 DllMain(),系统会调用它,通知 DLL 它已经被加载:

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{if (fdwReason == DLL_PROCESS_ATTACH) {// 初始化代码}return TRUE;
}

如果链接时用了 /NOENTRY 参数,那么 DLL 中没有入口函数,此步骤会被跳过。

可选高级知识

概念说明
延迟加载(Delay-load)使用 __declspec(dllimport) 延迟到函数真正调用时再加载 DLL。
重定位(Relocation)如果 DLL 不能被加载到默认基地址,系统会进行地址修正。
安全机制(安全加载)Windows 有 DLL 搜索顺序安全限制、防 DLL 劫持策略等。
并行组件(SxS)支持多个版本的 DLL 共存,使用清单(manifest)管理版本。

总结表格

步骤描述
1查找 DLL 文件位置
2将 DLL 映射到内存
3加载其依赖的 DLL
4解析并绑定导入函数地址
5调用 DllMain(如果有)完成初始化

小结一句话:

加载 DLL 就像打开一本书,系统先找到这本书,翻开封面,把里面需要的章节(函数)贴上书签(函数地址),最后提醒作者你已经“打开”了这本书(调用 DllMain)。

你这段代码展示了 Windows 系统中 DLL 是引用计数(Reference Counted)管理的,下面是详细的中文解读和你应该理解的关键点:

你的代码分析

#include <Windows.h>
int main()
{HMODULE Hello1 = LoadLibraryExW(L"Hello.dll", nullptr, 0); // Hello.dll refcount: 1HMODULE Hello2 = LoadLibraryExW(L"Hello.dll", nullptr, 0); // Hello.dll refcount: 2// Hello1 and Hello2 will be the sameFreeLibrary(Hello1); // refcount: 1 (not unloaded)FreeLibrary(Hello2); // refcount: 0 (now unloaded)
}

理解重点:DLL 的引用计数机制

在 Windows 中,每个进程对每个 DLL 维护一个“加载计数”

1. LoadLibrary 增加计数

每次你调用 LoadLibrary(或 LoadLibraryEx),即使是同一个 DLL 名称,系统会:

  • 检查是否已经加载过这个 DLL(路径一致,名字大小写忽略);
  • 如果已经加载,不会重新加载,而是:
    • 增加引用计数(refcount += 1)
    • 返回已有的模块句柄(HMODULE

所以在你代码中,Hello1 == Hello2,都是指向已经映射到内存中的 Hello.dll

2. FreeLibrary 减少计数

每次你调用 FreeLibrary(HMODULE),系统会:

  • 将引用计数减 1
  • 当引用计数变成 0 时:
    • 执行 DllMain(..., DLL_PROCESS_DETACH),清理资源
    • 从进程地址空间卸载该 DLL

模拟执行过程

操作引用计数说明
LoadLibraryEx("Hello.dll")1加载 DLL
LoadLibraryEx("Hello.dll")2未重新加载,只是加引用计数
FreeLibrary(Hello1)1DLL 仍驻留内存
FreeLibrary(Hello2)0现在才真正卸载 DLL

实用补充知识

概念说明
DllMain在 DLL 加载和卸载时会被系统自动调用
HMODULE本质上是 DLL 加载后在内存中的地址(模块基地址)
GetModuleHandle获取已经加载的模块,不增加引用计数
FreeLibrary必须匹配 LoadLibrary 次数调用,否则 DLL 永远不会卸载

小结一句话:

在 Windows 中,DLL 加载是引用计数式的共享资源管理 —— 只有最后一次 FreeLibrary() 才会真正卸载 DLL。

这段内容详细说明了 Windows 加载 DLL 的查找机制,尤其是使用 LoadLibraryExW 时,系统是如何决定加载哪个 DLL 的。下面是清晰的中文解读,帮助你系统性理解:

问题:系统是如何找到正确的 Hello.dll 的?

HMODULE HelloDll = LoadLibraryExW(L"Hello.dll", nullptr, 0);

一、绝对路径加载(推荐、最清晰)

HMODULE HelloDll = LoadLibraryExW(LR"(A:\Hello.dll)", nullptr, 0);
  • 如果 A:\Hello.dll 已经加载过了,系统就返回现有的 HMODULE不会重复加载
  • 如果还没加载,才会真的加载这个 DLL,并分配内存。
    结论:用绝对路径(如 A:\)可以同时加载多个同名但不同路径的 DLL。
HMODULE A = LoadLibraryExW(LR"(A:\Hello.dll)", nullptr, 0);
HMODULE B = LoadLibraryExW(LR"(B:\Hello.dll)", nullptr, 0);
// A ≠ B:它们分别是不同的 DLL

二、使用 DLL 名称加载

HMODULE HelloDll2 = LoadLibraryExW(L"Hello.dll", nullptr, 0);

此时不指定路径,系统会开始按以下顺序搜索 DLL:

DLL 搜索顺序(标准搜索路径)

  1. 应用程序所在目录(即 .exe 所在目录)
  2. C:\Windows\System32\(64位系统使用)
  3. C:\Windows\SysWOW64\(32位程序在 64 位系统上使用)
  4. C:\Windows\System\(16位系统遗留)
  5. C:\Windows\(Windows 根目录)
  6. 当前工作目录(从 XP SP2 起不再优先
  7. 所有在 %PATH% 环境变量中的路径
    如果你写的是 LoadLibraryExW(L"Hello.dll", nullptr, 0);,系统将按上面顺序找这个 DLL,并使用第一个找到的有效版本

三、Known DLL(已知 DLL)

Windows 系统有一类特殊的 DLL,叫做 Known DLLs,是系统事先在注册表里注册的,性能更优。

示例:

HMODULE h1 = LoadLibraryExW(L"kernel32.dll", nullptr, 0);
HMODULE h2 = LoadLibraryExW(L"ole32.dll", nullptr, 0);

这些 DLL 被认为是系统核心组件,系统会直接从 C:\Windows\System32\ 加载它们(甚至可能是内核共享内存中映射过来的),不会去别处找
注册表位置如下:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs

四、同名 DLL 加载顺序示例

LoadLibraryExW(LR"(A:\Hello.dll)", nullptr, 0); // 首次加载,加载A盘的版本
LoadLibraryExW(LR"(B:\Hello.dll)", nullptr, 0); // 不同路径,也加载成功
LoadLibraryExW(L"Hello.dll", nullptr, 0);       // 会选择已加载的A盘版本

注意:只要有同名 DLL 已被加载,LoadLibrary 会复用已有模块,除非你用绝对路径强制加载其他的

总结一句话:

Windows 加载 DLL 时,除非你指定绝对路径,否则系统会按搜索顺序查找第一个匹配的 DLL,或使用已加载版本。Known DLL 则直接使用系统内置映射。

如果你想查看系统实际加载了哪个 DLL,可以使用:

tasklist /m Hello.dll

或者在程序中用:

GetModuleFileName(hDll, buffer, sizeof(buffer));

这段列出了 Windows DLL 加载过程中的各种自定义搜索机制,说明 DLL 加载行为不仅依赖默认搜索顺序,还可以通过一些机制或标志加以控制或修改。下面我为你详细中文解释每一项的含义和作用:

DLL 加载搜索过程是可自定义的(The Search Process is Customizable)

Windows 提供多种方式改变或精确控制 DLL 的加载路径,以提高安全性、兼容性或满足应用需求。

1. DLL 重定向 (.local 文件)

如果你创建一个名为 YourApp.exe.local 的空文件,并放在你的可执行文件旁边:

  • Windows 将只从该目录加载 DLL,不再搜索系统路径或 PATH。
  • 常用于开发和调试,使程序优先使用本地版本 DLL。
  • Windows Vista 起,该方法只对 .exe非系统路径加载的进程有效。

2. Side-by-Side (SxS) 组件

Windows 提供 SxS 技术用于解决“DLL Hell”(DLL 冲突)问题。

  • 应用程序可以指定使用某个特定版本的共享 DLL。
  • DLL 安装在 C:\Windows\WinSxS\ 下。
  • 使用清单文件(.manifest)定义依赖项。
  • 示例:多个应用可以共存不同版本的 comctl32.dll

3. 环境变量 %PATH%

系统会从环境变量 %PATH% 中列出的目录中查找 DLL。

  • 放置 DLL 的路径可被加入系统或用户 PATH。
  • 但这也存在安全风险:恶意 DLL 被放入高优先级路径中。

4. AddDllDirectory 函数

  • 允许程序动态添加 DLL 搜索目录
  • 配合 SetDefaultDllDirectories 使用后,只搜索你显式指定的目录。
  • 安全性好,推荐在现代应用中使用。
AddDllDirectory(L"C:\\MySafeDLLs");

5. LoadLibraryEx 的 Flags(标志)

LoadLibraryExW 允许你传入额外参数,控制搜索行为:

LOAD_WITH_ALTERED_SEARCH_PATH
  • 修改搜索顺序:将 DLL 所在目录排在最前
  • 用于加载依赖 DLL 的 DLL 文件时非常有用。
LOAD_LIBRARY_SEARCH_APPLICATION_DIR
  • 只从应用程序所在目录加载 DLL。
LOAD_LIBRARY_SEARCH_DEFAULT_DIRS
  • 仅从默认安全目录(不含当前目录)中查找 DLL。
LOAD_LIBRARY_SEARCH_DLL_LOADER_DIR
  • 当前加载的 DLL 所在目录开始查找依赖项。
  • 用于防止依赖项从错误位置加载。
LOAD_LIBRARY_SEARCH_SYSTEM32
  • 仅从 C:\Windows\System32 加载。
  • 非常安全,常用于加载系统 DLL。
LOAD_LIBRARY_SEARCH_USER_DIRS
  • AddDllDirectory() 添加的自定义目录中查找。

6. Windows Store / UWP 应用中的 DLL 加载

  • UWP 应用是沙盒环境,DLL 加载机制被进一步限制。
  • 不允许访问传统系统 DLL 搜索路径。
  • 必须通过声明依赖项、使用 API Contract 等方式。

为什么这么复杂?

为了兼顾:

  • 兼容性(老程序)
  • 安全性(防止 DLL 劫持)
  • 灵活性(允许自定义逻辑)

总结

Windows 提供了一系列工具(.local、SxS、AddDllDirectory、LoadLibraryEx Flags 等)来让开发者灵活、安全地控制 DLL 加载路径,避免冲突或劫持。

你提到的内容是关于 DLL 文件在磁盘上的大小和其加载到内存后占用空间的差异,这是 Windows PE(Portable Executable)文件格式的一个重要特性。下面我用中文为你详细解释:

主题:为什么 DLL 文件磁盘上才 2KB,内存中却占用了 12KB?

1. 磁盘大小 vs 内存映射大小

属性
磁盘文件大小2,048 字节(2KB)
加载到内存后的大小0x3000 = 12,288 字节(12KB)
这就是为什么你看到:
A:\>dir Hello.dll
2,048 Hello.dll
A:\>dumpbin /headers Hello.dll
3000 size of image

2. 原因:文件对齐(File Alignment)与内存对齐(Section Alignment)不同

PE 文件格式定义了:

属性含义
File Alignment文件在磁盘上的最小对齐单位(比如 512 字节)
Section Alignment映射进内存时的对齐单位(通常是 0x1000 = 4KB)

举个例子(实际情况):

你 DLL 中可能有 2 个段(section):

段名实际内容大小在文件中大小(按 512B 对齐)内存映射大小(按 4KB 对齐)
.text300 字节512 字节4KB(0x1000)
.data100 字节512 字节4KB(0x1000)
加上可能还有 .rdata 等段,每个段无论内容多少,加载到内存后都必须占据一个完整的页面(通常是 4KB),所以:
总内存大小 = 各段的 Section Alignment 加总 ≈ 0x3000 = 12KB

但:

总文件大小 = 各段的 File Alignment 加总 ≈ 2048 字节

3. dumpbin /headers 的关键信息

OPTIONAL HEADER VALUES3000 size of image          ; 内存中总共占用 0x3000 字节1000 section alignment      ; 每个段对齐到 4KB200  file alignment         ; 每个段在文件中对齐到 512B

4. 操作系统为什么这么做?

  • 页对齐(4KB) 是为了配合虚拟内存的页面管理机制。
  • 加载时,每段直接映射到内存的独立页面。
  • 减少页故障,提高加载效率。

总结

原因
DLL 文件小(2KB)因为文件中的内容被 512 字节对齐,仅存放实际指令和数据
DLL 内存映像大(12KB)因为每个段都被对齐到 4KB 页面边界以便映射进内存
dumpbin 显示 size of image = 0x3000表示操作系统会分配 3 个内存页来加载这个 DLL

你的这段内容解释了 Windows 加载 DLL 到内存的过程,尤其强调了内存对齐和虚拟内存使用的机制。我们来用中文逐步解释:

主题:为什么 DLL 文件体积小,但加载后内存占用大?

一、每个段(Section)在内存中必须按页面(Page)对齐

比如你运行的:

A:\>dumpbin /headers Hello.dll

输出类似:

SECTION HEADER #1.textVirtual Size: 8Virtual Address: 1000Size of Raw Data: 200File Pointer: 400
SECTION HEADER #2.rdataVirtual Size: D8Virtual Address: 2000Size of Raw Data: 200File Pointer: 600

说明每个段都被强制映射到新的内存页地址上,即使内容很小也如此:

  • .text 段只用了 8 字节代码,但分配在从 0x1000 开始的整页(4KB);
  • .rdata 段只用了 D8 字节(约 216 字节),也被映射到 0x2000 页。
    为什么这样做?
    这是因为 Windows 使用分页机制(页通常为 4KB),每个内存段都需要页对齐才能设置不同的访问权限(如 .text 是可执行的,.data 是可写的)。

二、为什么大数组没有出现在文件中?

你写了这段代码:

char GlobalBuffer[1024 * 1024];  // 1MB

但生成的 DLL 文件:

A:\>dir HelloBuffer.dll
2048 bytes

并没有变大,这是为什么?
原因:

  • 这个数组虽然声明了 1MB 大小,但没有初始化
  • 编译器会将它放到 .bss.data 段中;
  • 如果没有初始值,链接器不需要在文件中存储内容,只需要告诉 loader:“这段内存需要预留,但你不需要读文件内容”。
    你看到的:
SECTION HEADER #3.dataVirtual Size: 100000  (= 1MB)Size of Raw Data: 0File Pointer: 0Flags: Read Write

说明:

  • 内存中会分配 1MB;
  • 文件中根本不包含这部分内容;
  • loader 会初始化它为全零(标准行为);

三、加载 DLL 的完整过程是这样的:

  1. 打开 DLL 文件;
  2. 读取 “SizeOfImage” 字段(比如 103000);
  3. 申请一块连续的、按页对齐的内存空间(比如 0x103000 字节 ≈ 1MB + 一些段);
  4. 依次把 .text, .rdata, .data 等段从文件复制到内存相应位置;
  5. 对每段设置访问权限(例如 .textExecute | Read.dataRead | Write);
  6. 处理导入表、重定位表等。

四、总结:我们不需要把所有东西都存到 DLL 文件里

内容是否写入文件内存中是否存在原因
初始化数据(如字符串)放入 .rdata.data
未初始化数据(如大数组).bss,由 loader 分配并清零
小函数(如 GetGreeting)编译生成在 .text

核心概念小结

  • 文件中的内容 ≠ 内存中的内容
  • 未初始化变量只占虚拟内存,不占磁盘空间
  • 每个段都必须按页(4KB)对齐加载到内存
  • Windows 加载器(loader)根据 PE 头中的信息,分配空间并完成映射。
    如果你还想深入了解如 .bss 段、导入表、重定位表或 PE 文件格式,我可以继续讲解。是否要继续深入?

你展示的代码和输出,讲的是 DLL 里的 全局变量指针和重定位(Relocation) 概念。让我帮你用中文详细解释一下:

你给出的代码(PointerGlobal.cpp)

extern "C" __declspec(dllexport) const size_t Two = 2;
extern "C" __declspec(dllexport) const size_t* const PointerToTwo = &Two;
  • Two 是导出的一个常量变量,值为 2。
  • PointerToTwo 是指向 Two 的指针,也被导出。

DLL 导出情况(dumpbin /exports PointerGlobal.dll

ordinal hint RVA
0       00001008 PointerToTwo
1       00001000 Two
  • Two 地址是 0x1000
  • PointerToTwo 地址是 0x1008

通过 dumpbin /rawdata:8 /section:.rdata PointerGlobal.dll.rdata 段的数据:

0000000070001000: 0000000000000002 0000000070001000
  • 前 8 字节:0000000000000002 —— 代表 Two 变量的值 2
  • 后 8 字节:0000000070001000 —— 指针 PointerToTwo 的值,是 Two 的地址(0x70001000

这体现了什么?

1. 指针地址是“重定位地址”

  • PointerToTwo 是个指针,指向变量 Two 的地址。
  • 这个地址在 DLL 文件里是“虚拟地址”(RVA + Image Base)。
  • 加载时,操作系统 loader 需要把这些地址“重定位”成进程中真实的内存地址。

2. 为什么要重定位?

  • DLL 在文件里的地址是相对于 ImageBase 的(假设是 0x70000000)。
  • 实际加载时,DLL 可能被加载到不同的内存地址。
  • 指针里面存的地址必须修正(relocate)成 DLL 真正的加载基址 + 偏移。

3. 所以这个指针并不是简单的“写死”的地址

  • 它的值是 DLL 的加载基址 + Two 的 RVA。
  • 当 DLL 加载时,系统会调整(修正)这个地址,使它正确指向内存中的 Two

总结:

  • 你写的 PointerToTwo 是指向 Two 的指针,二者都导出;
  • DLL 文件中指针的值是相对于 DLL ImageBase 的“虚拟地址”;
  • 加载时操作系统 loader 会对这些地址进行重定位(Relocation)
  • 这就是为什么 DLL 中能正确让指针指向同一个模块内的变量。

你这个讲解非常清楚,重点是重定位(Relocation)机制如何修正 DLL 中指针的值,我帮你总结下核心要点,用中文说得更明白些:

核心问题

  • DLL 文件内部的指针(比如 PointerToTwo 指向 Two)存的是基于 DLL 首选加载地址(Preferred Base Address,比如 0x70000000)的绝对地址。
  • 但实际上,DLL 不一定能加载到这个地址,它可能被加载到一个不同的内存地址(比如 0x90000000)。
  • 这时,指针里面的地址就不对了,因为它还指向旧的“首选地址”空间。

重定位机制的作用

  • DLL 文件里有一张 重定位表(Relocation Table),列出了文件中哪些地址是需要动态调整的。
  • 操作系统 Loader 会根据:
    新指针 = 旧指针 - 首选加载地址 + 实际加载地址
    
    来修正指针。

你的例子

  • DLL 的首选加载地址(Preferred Base Address)是 0x70000000
  • 文件中 PointerToTwo 的指针值是 0x70001000(指向 Two)。
  • DLL 实际被加载到了 0x90000000
  • Loader 重新计算:
    0x70001000 - 0x70000000 + 0x90000000 = 0x90001000
    
  • 所以,PointerToTwo 的值被修正成 0x90001000,指向正确的内存位置。

为什么这重要?

  • 如果没有重定位,DLL 里面指针指向的地址会是错误的,导致程序崩溃或数据错乱。
  • 重定位保证了无论 DLL 加载到哪里,内部的指针和引用都能正确指向对应对象。

补充说明

  • 这个过程是系统 loader 自动完成的,程序员一般不需要手动操作。
  • 但理解这个机制有助于调试 DLL 加载问题,或者设计支持重定位的模块。
    如果你想,我可以帮你详细讲解:
  • PE 文件中重定位表的结构
  • 载入时 loader 如何逐条处理重定位
  • 重定位和 ASLR(地址随机化)之间的关系

这段内容很好地说明了 DLL 依赖加载和导入函数绑定的流程,我帮你总结成简单易懂的几个关键点:

DLL 依赖加载和导入绑定过程

  1. 检查 DLL 依赖
    • 通过 dumpbin /imports Hello.dll 可以看到当前 DLL 依赖哪些外部 DLL 和函数。
    • 你演示的最初 Hello.dll 没有依赖,但后来你添加了调用 MultiByteToWideChar(属于 KERNEL32.dll),这就产生了依赖。
  2. 加载依赖的 DLL
    • 对于每个依赖的 DLL(比如 KERNEL32.dll),操作系统 loader 会调用 LoadLibraryExW 去加载它。
    • 如果依赖 DLL 已经加载过,则返回已有模块句柄。
  3. 绑定导入的函数地址
    • 依赖 DLL 加载后,loader 通过 GetProcAddress 找到 DLL 中对应的函数地址(如 MultiByteToWideChar)。
    • 将这些函数地址写入导入地址表(IAT),使程序调用时直接跳转到正确地址。
  4. 如果加载失败或找不到函数
    • 返回失败,加载整个 DLL 失败。

你的伪代码意思就是:

for (auto& DllDependency : DllDependencies)
{DllDependency.Handle = LoadLibraryExW(DllDependency.Name, nullptr, 0);if (!DllDependency.Handle){// 加载失败,返回错误}for (auto& Import : DllDependency.Imports){Import.Address = GetProcAddress(DllDependency.Handle, Import.Name);if (!Import.Address){// 找不到函数,返回错误}}
}

简单理解

  • DLL 会有一张“依赖表”,列出所有需要的外部 DLL 和函数。
  • 载入时,系统帮你把所有依赖都加载进来,并把函数地址“绑”好。
  • 这样,程序调用 DLL 的导入函数时,就可以顺利跳转执行。
    如果你需要,我还可以帮你详细讲下:
  • 导入地址表(Import Address Table,IAT)的结构
  • Delay Loading(延迟加载)机制
  • LoadLibrary 和 GetProcAddress 的细节
  • 动态链接失败时的常见问题和调试技巧

你这段内容讲得很清楚,DLL 的入口点函数 DllMain 是 DLL 被加载和卸载时的关键回调,具体说明了:

DLL 的入口点:DllMain

BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, LPVOID reserved);
  • instance:DLL 模块的句柄(和 LoadLibrary 返回的一样)。
  • reason:调用原因,常见值:
    • DLL_PROCESS_ATTACH:进程加载 DLL 时调用一次。
    • DLL_PROCESS_DETACH:进程卸载 DLL 时调用一次。
    • DLL_THREAD_ATTACH:新线程启动时调用(线程附加)。
    • DLL_THREAD_DETACH:线程退出时调用(线程分离)。
  • reserved:额外信息。
    • 对于 PROCESS_ATTACH,如果 DLL 是通过 LoadLibrary 显式加载,reservedNULL,如果是隐式加载(EXE 的依赖),非 NULL
    • 对于 PROCESS_DETACH,如果是 FreeLibrary 卸载,reservedNULL,如果是进程退出,非 NULL

返回值

  • TRUE 表示初始化成功,DLL 可以正常加载。
  • FALSE 表示失败,会导致 DLL 加载失败。

同步机制

  • 系统对调用 DllMain 使用一个全局的“Loader Lock”来同步,防止同时多个线程竞争加载/卸载 DLL。

不是所有 DLL 都需要实现 DllMain

  • 你举例的 Hello.cpp 就没有 DllMain,直接导出函数即可。
  • 没有定义 DllMain,系统会使用默认入口,正常加载。
    如果你想,我可以帮你演示写一个典型的 DllMain,包括打印日志、初始化资源、释放资源,或者解释 Loader Lock 可能带来的死锁问题。

你这一部分讲的是 DLL 的入口点 (DllMain) 的有无与配置,具体流程和背后的机制如下:

不含入口点的 DLL

extern "C" char const* __cdecl GetGreeting() {return "Hello, C++ Programmers!";
}

构建命令:

link Hello.obj /DLL /NOENTRY /NODEFAULTLIB /EXPORT:GetGreeting
  • /NOENTRY:告诉链接器 不要添加 DLL 的入口点(不生成 DllMain)
  • /NODEFAULTLIB:不要链接默认的 CRT(避免隐式依赖)

验证:

dumpbin /headers Hello.dll

输出中会显示:

0 entry point

表示此 DLL 没有 DllMain,这在某些轻量级场景下是合法的。

添加入口点的 DLL 示例

示例代码:

#include <stdio.h>
#include <Windows.h>
extern "C" BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, LPVOID reserved) {switch (reason) {case DLL_PROCESS_ATTACH:puts("DllMain called for DLL_PROCESS_ATTACH");break;case DLL_PROCESS_DETACH:puts("DllMain called for DLL_PROCESS_DETACH");break;}return TRUE;
}

构建命令:

link DllWithEntryPoint.obj /DLL /ENTRY:DllMain

这告诉链接器:

  • /DLL:生成 DLL
  • /ENTRY:DllMain:将 DllMain 设置为入口点函数
    当你用 LoadLibrary 加载这个 DLL 或 FreeLibrary 卸载时,就会看到控制台输出提示,例如:
DllMain called for DLL_PROCESS_ATTACH

总结对比

情况是否有入口点构建参数说明
Hello.dll/DLL /NOENTRY纯导出函数,不含初始化逻辑
DllWithEntryPoint.dll/DLL /ENTRY:DllMain带生命周期钩子处理逻辑
如你希望,我也可以解释:
  • 什么时候避免使用 DllMain
  • DllMain不能安全做哪些事(比如调用 LoadLibrary 等)
  • 如何正确地初始化/清理资源

演示 DLL 的入口点 (DllMain) 的行为

测试程序代码:

#include <stdio.h>
#include <Windows.h>
int main() {printf("About to load DLL...\n");HMODULE const TestDll = LoadLibraryExW(L"DllWithEntryPoint.dll", nullptr, 0);printf("DLL loaded.  About to unload DLL...\n");FreeLibrary(TestDll);printf("DLL unloaded.\n");
}

程序运行输出:

About to load DLL...
DllMain called for DLL_PROCESS_ATTACH
DLL loaded.  About to unload DLL...
DllMain called for DLL_PROCESS_DETACH
DLL unloaded.

说明:

  • 加载时触发 DLL_PROCESS_ATTACH
  • 卸载时触发 DLL_PROCESS_DETACH

MSDN 的建议:DllMain 中要非常小心

出自:
“Dynamic-Link Library Best Practices”
MSDN DLL Best Practices

推荐实践摘要:

  1. 尽可能少做事:
    • 不要在 DllMain 里执行复杂逻辑或初始化大量资源。
  2. 避免调用其他 DLL 函数:
    • DllMain 中调用 LoadLibrary 或其他 DLL 的导出函数是危险的,可能导致死锁。
  3. 不要和其他线程同步:
    • 不要加锁、等待事件等,因为 DllMain 被调用时是持有 Loader Lock 的。

C/C++ 中默认入口点处理

大多数时候你 不需要手动写 DllMain,特别是在使用 C++ 的构造/析构全局对象等机制时,C 运行时 (CRT) 会自动安排这些初始化工作。手动指定入口点只在特殊情况下需要,比如:

  • 你需要控制 DLL 初始化顺序
  • 或者你不想链接 CRT(如 /NODEFAULTLIB

想深入了解的话,我还可以讲解:

  • Windows 如何处理 TLS (Thread Local Storage) 回调 vs. DllMain
  • 在 C++ DLL 中如何正确使用构造函数/析构函数
  • 如何避免常见的 DllMain 死锁陷阱

相关文章:

  • 打破物理桎梏:CAN-ETH网关如何用UDP封装重构工业网络边界
  • 破局基建困局:国有平台公司数字化转型的生态重构
  • 网页后端开发(基础5--JDBC VS Mybatis)
  • 二叉树基本学习
  • API 接口:程序世界的通用语言与交互基因
  • ABI与API定义及区别
  • JVM内存模型与Arthas诊断实战
  • AR/VR显示为何视场受限?OAS对标波导案例来解决
  • Ubuntu 和 CentOS 中配置静态 IP
  • 微信小程序获取指定元素,滚动页面到指定位置
  • 为什么主动关闭 TCP 连接的一方需要 TIME_WAIT 状态?
  • 使用Haporxy搭建Web群集
  • 嵌入Linux快速入门第3篇
  • JavaEE->多线程2
  • 【Bluedroid】蓝牙启动之 btif_init_ok 流程源码解析
  • 小智AI玩具市场爆发:四大品牌领跑情感交互新赛道
  • 3DTiles三维模型
  • Linux 核心知识点整理(高频考点版)
  • Mac电脑 Office 2024 LTSC 长期支持版(Excel、Word、PPT)
  • 《超级处理器》怎么安装到WPS/excel(最后有下载地址)
  • 党校网站建设/河源新闻最新消息
  • 做网站与平台的区别/seo常见优化技术
  • 做网站不给提供ftp/网店培训教程
  • 青岛做外贸网站建设/seo 页面链接优化
  • 收费的网站怎么做/营销型网站建设案例
  • 网站开发怎么去接单/国外产品推广平台