库

库(Library)是一组预编译的代码集合,供其他程序调用。Windows 环境下主要分为静态库(Static Library, .lib)和动态链接库(Dynamic Link Library, .dll)。


1. 静态库

1.1 概念

静态库在编译/链接阶段将其代码直接复制到目标可执行文件中。最终生成的 .exe 独立运行,不再依赖静态库文件。

编译过程:
  .c/.cpp  → 编译 →  .obj      ↓ 链接
  .lib (静态库) —————————————→  .exe (自带代码)

1.2 创建静态库

// MyLib.c
int Add(int a, int b) { return a + b; }
int Sub(int a, int b) { return a - b; }

编译为 .lib(Visual Studio 命令行):

cl /c MyLib.c                           # 生成 MyLib.obj
lib MyLib.obj /OUT:MyLib.lib            # 打包为 MyLib.lib

1.3 使用静态库

调用方需要两个东西:

文件作用
.h 头文件提供函数原型,编译器做类型检查
.lib 库文件链接器提取目标代码嵌入 .exe
// MyLib.h
#ifndef MYLIB_H
#define MYLIB_H
 
int Add(int a, int b);
int Sub(int a, int b);
 
#endif
// main.c
#include <stdio.h>
#include "MyLib.h"
 
#pragma comment(lib, "MyLib.lib")   // 告诉链接器链接此库
 
int main() {
    printf("%d\n", Add(10, 5));
    return 0;
}

也可以在 VS 项目属性 → 链接器 → 输入 → 附加依赖项 中配置。

1.4 优缺点

优点缺点
部署简单,单个 .exe 即可运行生成文件体积大
不会因目标系统缺少 DLL 而报错多个程序用同一库时,代码重复占用磁盘/内存
加载速度快,无运行时开销库更新需重新编译所有依赖它的程序

2. 动态链接库 (DLL)

2.1 概念

DLL 将代码独立打包,程序编译时仅记录符号引用,运行时才将 DLL 加载到进程地址空间。多个进程可共享内存中同一份 DLL 代码。

编译时:
  .exe  ─→ 仅记录需要哪些 DLL 和函数
  .dll  ─→ 生成独立文件,不嵌入 .exe

运行时:
  .exe 启动 → 加载器解析导入表 → 将 .dll 映射到进程地址空间 → 调用函数

2.2 创建 DLL

// MyDll.c
#include <windows.h>
 
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
    switch (fdwReason) {
    case DLL_PROCESS_ATTACH:
        // DLL 被加载到进程时执行
        break;
    case DLL_THREAD_ATTACH:
        // 进程创建新线程时执行
        break;
    case DLL_THREAD_DETACH:
        // 线程退出时执行
        break;
    case DLL_PROCESS_DETACH:
        // DLL 被卸载时执行
        break;
    }
    return TRUE;
}
 
// 导出函数
__declspec(dllexport) int Add(int a, int b) { return a + b; }
__declspec(dllexport) int Sub(int a, int b) { return a - b; }

编译为 .dll:

cl /LD MyDll.c /Fe:MyDll.dll

会生成 MyDll.dll + MyDll.lib(导入库,含符号表,用于隐式链接)。

2.2.1 DllMain 详解

DllMain 是 DLL 的入口函数,在以下时机被系统自动调用:

BOOL WINAPI DllMain(
    HINSTANCE hinstDLL,   // DLL 模块句柄(基址),进程唯一
    DWORD     fdwReason,  // 调用原因
    LPVOID    lpvReserved // 静态/动态加载标记
);

fdwReason 四种取值和调用时机:

原因时机
DLL_PROCESS_ATTACHDLL 首次映射到进程地址空间(进程启动或 LoadLibrary)
DLL_PROCESS_DETACHDLL 从进程地址空间卸载(进程退出或 FreeLibrary)
DLL_THREAD_ATTACH进程创建新线程
DLL_THREAD_DETACH线程正常退出

DllMain 中应避免调用 LoadLibrary/FreeLibrary、等待同步对象、或其他可能引发死锁的操作。

2.3 导出函数

2.3.1 __declspec(dllexport) 方式

通常配合条件编译,一套头文件兼供 DLL 内部编译和外部调用方使用:

// MyDll.h
#pragma once
 
#ifdef MYDLL_EXPORTS
    #define MYDLL_API __declspec(dllexport)
#else
    #define MYDLL_API __declspec(dllimport)
#endif
 
extern "C" {
    MYDLL_API int Add(int a, int b);
    MYDLL_API int Sub(int a, int b);
}
// MyDll.c
#define MYDLL_EXPORTS
#include "MyDll.h"
 
MYDLL_API int Add(int a, int b) { return a + b; }
MYDLL_API int Sub(int a, int b) { return a - b; }
  • MYDLL_EXPORTS 宏在 DLL 项目属性 → C/C++ → 预处理器定义 中设置
  • 调用方只需包含 MyDll.h,此时 MYDLL_API 展开为 __declspec(dllimport),链接器据此生成更优的调用指令

2.3.2 .def 文件方式

模块定义文件(Module-Definition File)可精确控制导出符号名与序号,避免 C++ 名字修饰。

; MyDll.def
LIBRARY MyDll
EXPORTS
    Add   @1
    Sub   @2
  • @1 / @2 为导出序号(Ordinal),调用方可按序号而非名字查找
  • 适合需要隐藏符号名或遵循特定 ABI 的场景

2.4 调用 DLL

2.4.1 隐式链接(Load-Time Linking)

程序启动时由操作系统加载器自动加载 DLL。需要:`.h` + `.lib`(导入库)+ `.dll`。

#include <iostream>
#include "MyDll.h"
 
#pragma comment(lib, "MyDll.lib")
 
int main() {
    std::cout << Add(10, 5) << std::endl;
    return 0;
}
  • 若启动时找不到 .dll,程序直接崩溃(报”找不到 XXX.dll”错误)

2.4.2 显式链接(Run-Time Linking)

通过 API 在运行期加载,无需 .h 和 .lib,更灵活。

#include <iostream>
#include <windows.h>
 
typedef int (*AddFunc)(int, int);
 
int main() {
    HMODULE hDll = LoadLibrary(TEXT("MyDll.dll"));
    if (!hDll) {
        std::cerr << "Failed to load DLL" << std::endl;
        return 1;
    }
 
    AddFunc add = (AddFunc)GetProcAddress(hDll, "Add");
    if (add) {
        std::cout << "Result: " << add(10, 5) << std::endl;
    }
 
    FreeLibrary(hDll);
    return 0;
}
隐式链接显式链接
代码简单,使用方便代码复杂,但可控性强
DLL 必须存在于搜索路径可指定任意路径
启动时加载,失败则无法启动可按需加载,处理加载失败

2.5 使用 rundll32 调用 DLL

rundll32.exe 是 Windows 自带的工具,用于执行 32 位 DLL 中导出的函数。常用于免杀、进程隐藏(进程中仅显示 rundll32.exe)。

导出函数必须符合特定原型:

void CALLBACK FunctionName(
    HWND     hwnd,
    HINSTANCE hinst,
    LPTSTR   lpCmdLine,
    INT      nCmdShow
);

示例:

#include <windows.h>
 
extern "C" __declspec(dllexport)
void CALLBACK ShowMsg(HWND hwnd, HINSTANCE hinst, LPTSTR lpCmdLine, INT nCmdShow) {
    MessageBox(NULL, TEXT("Hello from rundll32"), TEXT("DLL"), MB_OK);
}

命令行调用:

rundll32.exe MyDll.dll, ShowMsg

2.6 优缺点

优点缺点
多个程序共享同一份代码,节省内存存在”DLL Hell”(版本冲突)风险
更新 DLL 即可更新功能,无需重新编译主程序部署需附带 DLL,易丢失
支持按需加载,减少启动时间加载/卸载有额外运行时开销
语言无关,不同语言可调用同一 DLL显式链接代码较为复杂

3. 对比总结

维度静态库 .lib动态库 .dll
链接时机编译/链接期运行期
产物单个 .exe.exe + .dll
文件体积大小
部署简单复杂(需分发 DLL)
更新重新编译整个程序替换 DLL 即可
内存占用每进程独立副本多进程共享代码段
加载速度快有运行时加载开销

4. 逆向视角补充

  • 静态库 — 反编译时函数代码直接混在 .text 段中,无明确边界
  • DLL — 通过 PE 文件的 导入表(IAT) 可清晰看到依赖的 DLL 和函数名;可通过 IAT HOOK / 导入地址表劫持实现 API 拦截
  • 导出表(EAT) — 记录 DLL 对外暴露的函数名及序号,是逆向分析 DLL 功能的重要入口