下面把 DLL(Dynamic-Link Library,动态链接库) 从概念、内部结构、加载机制、跨语言调用、构建与发布、常见问题与安全风险,全流程讲清楚。

DLL 是什么?

  • 本质:一组可重用的函数/类/资源的集合,以 .dll 结尾,在 运行时 被进程加载并共享使用。
  • 场景:把通用能力(如图形、网络、数据库、算法、UI控件、国际化资源)拆成模块,多个程序可共用。
  • 平台:主要用于 Windows。类 Unix 对应的是 .so(shared object),macOS 是 .dylib

与 EXE、静态库的区别

  • EXE:可执行程序入口是 main/WinMain,用户直接双击运行。
  • DLL:没有主入口供用户启动(有 DllMain 但不用于当“程序入口”),必须由进程加载后调用其导出函数。
  • 静态库(.lib):在编译期把目标文件直接打包进可执行文件;DLL是在运行期由系统装载器加载。
  • 关联文件:很多 DLL 会配套一个 导入库 .lib(只包含符号,不含实现),用于链接时解析外部符号。

内部结构(简述)

  • PE/COFF 格式:与 EXE 同为 Windows PE 文件。
  • 导出表(EAT):列出 DLL 对外暴露的符号(按名称序号 ordinal)。
  • 导入表(IAT):列出 DLL 自身依赖的其他 DLL。
  • 重定位表:配合 ASLR 在不同基址装载。
  • 资源段:可嵌入图标、对话框模板、字符串表,本地化资源等。
  • TLS 回调:线程局部存储初始化回调。
  • 入口函数 DllMain:在装载/卸载/线程创建等时机被系统调用,功能受严格约束(不要阻塞/不要再 LoadLibrary 以免死锁)。

DLL 如何被加载与调用

三种方式:

  1. 隐式加载(链接器方式)

    • 编译时链接到导入库 .lib,进程启动时由系统装载器按需加载依赖 DLL,填充 IAT。
  2. 延迟加载(/DELAYLOAD)

    • 直到第一次调用该 DLL 的函数才触发加载,缩短启动时间、降低初始占用。
  3. 显式加载(运行时手动)

    • 代码中调用 LoadLibrary(Ex)GetProcAddressFreeLibrary 自行管理(常见于插件系统和可选依赖)。

搜索顺序(简化)

  • 进程目录(应用所在目录)→ 系统目录(System32 等)→ Windows 目录 → PATH 中的目录。
  • SafeDllSearchModeSetDefaultDllDirectories/AddDllDirectory、应用清单、KnownDLLs 等策略影响。

小提示:Windows 64 位系统上,C:\Windows\System32 存放 64 位 DLL,而 SysWOW64 存放 32 位,名字很“反直觉”。

导出与调用约定

  • C/C++ 导出常见两种方式:

    • 头文件中声明并在实现中使用 __declspec(dllexport) 导出,使用者侧 __declspec(dllimport)
    • 使用 .def 文件明确导出函数名/序号,避免名称修饰。
  • 调用约定__cdecl__stdcall__fastcallthiscall 等,影响栈清理与符号修饰;跨语言接口一般用 extern "C" + __stdcall/__cdecl 并固定参数布局,确保 ABI 稳定。

  • 按名/按序号 调用:按名可读性好,按序号加载更快但易碎。

跨语言与高级用法

  • .NET / C#(P/Invoke)

    [DllImport("mylib.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern int Add(int a, int b);
    
  • Python(ctypes)

    from ctypes import CDLL, c_int
    lib = CDLL("mylib.dll")
    lib.Add.argtypes = (c_int, c_int)
    lib.Add.restype = c_int
    
  • Node.js:使用 node-ffi-napinode-addon-api(后者编写原生扩展)调用。

  • COM 组件:很多 ActiveX/OCX 实际上是 DLL,以 regsvr32 注册,提供二进制接口与类型库。

  • 资源/本地化资源专用 DLL卫星 DLL 存放多语言资源,按 UI 语言动态加载。

版本、部署与“DLL 地狱”

  • 优势:复用与模块化、节省内存(共享代码页)、独立更新。

  • 痛点:版本不兼容、ABI 变更、覆盖冲突导致“DLL Hell”。

  • 缓解

    • 与应用同目录部署(私有 DLL),避免全局覆盖;
    • 使用 Side-by-Side(SxS)/清单 manifest 绑定特定版本;
    • 采用 语义化版本 + 向后兼容的导出接口;
    • 对 C 运行库等使用官方 VC++ Redistributable 安装包;
    • 明确区分 x86/Win32x64/ARM64 版本,不可混用。

构建一个最小 DLL(MSVC 示例)

// mathlib.h
#pragma once
#ifdef MATHLIB_EXPORTS
#define API __declspec(dllexport)
#else
#define API __declspec(dllimport)
#endif
extern "C" {
  API int add(int a, int b);
}
// mathlib.cpp
#define MATHLIB_EXPORTS
#include "mathlib.h"
int add(int a, int b) { return a + b; }
  • CMake/VS 工程设为 动态库;生成 mathlib.dllmathlib.lib
  • 调用方链接 mathlib.lib(隐式加载)或用 LoadLibrary/GetProcAddress(显式加载)。

常见报错与处理

  • “xxx.dll was not found / missing VCRUNTIME140.dll”

    • 未安装对应 VC++ 运行库;或目标机器缺少依赖 DLL。
  • “is not a valid Win32 application” / BadImageFormat

    • 架构不匹配(x86 vs x64/ARM64);或文件损坏。
  • “Entry Point Not Found”

    • 函数名/序号不一致,导出表变更;或延迟加载函数不存在。
  • 加载不到自定义 DLL

    • 搜索路径不包含;使用 绝对路径AddDllDirectory;检查签名/策略限制。
  • 权限/锁定

    • DLL 被进程占用无法替换,需先关闭进程或使用“就地升级”策略(版本号区分)。

安全注意事项

  • DLL 劫持 / 旁加载:攻击者利用搜索顺序在应用目录放置同名恶意 DLL。

    • 缓解:使用绝对路径、启用 SafeDllSearchModeSetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32)、签名验证(WinVerifyTrust/签名策略)。
  • 代码签名:对发布的 DLL 进行 Authenticode 签名,增强信任链与完整性验证。

  • ASLR/DEP:依赖系统安全机制;不要在 DllMain 做复杂逻辑,避免死锁与竞态。

诊断与工具(知道名字就好)

  • dumpbin /exports(VS 工具):查看导出符号
  • Dependencies / Dependency Walker:分析依赖树与缺失函数
  • Process Explorer / Process Hacker:查看进程已加载的模块
  • tasklist /m:列出进程加载的 DLL
  • sigcheck(Sysinternals):查看数字签名与版本信息

速查清单

  • 需要跨语言/跨项目复用?—— 做 DLL,导出 extern "C" 接口保证 ABI 稳定。
  • 部署到客户机器?—— 与 EXE 同目录放置 + 对应架构 + 必要的运行库。
  • 报找不到 DLL?—— 检查 路径/架构/依赖,用 Dependencies 工具定位。
  • 担心劫持?—— 绝对路径 + 限制搜索目录 + 数字签名
  • 不确定导出的名字?—— 用 dumpbin /exports.def 文件钉死导出名/序号。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐