库
库(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.lib1.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_ATTACH | DLL 首次映射到进程地址空间(进程启动或 LoadLibrary) |
DLL_PROCESS_DETACH | DLL 从进程地址空间卸载(进程退出或 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, ShowMsg2.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 功能的重要入口