文章目录
- 前言
- 一、使用obj保护源码
- 生成obj文件
- 导入并使用obj文件
- 方式一 拖入解决方案
- 方式二 附加依赖项
- 适配C语言文件
- 二、静态库的概述
- 三、静态库的创建与使用
- 四、动态库的概述
- 五、动态库的创建
- 六、动态库的两种调用方式
- 七、动态链接库的隐式加载
- __declspec(dllimport) 声明外部函数
- 使用宏优化导关键字dllimport
- 动态库的创建优化
- 动态库的加载优化
- 八、动态加载库的显示加载
- LoadLibrary加载DLL
- FreeLibray卸载DLL
- GetProcAddress函数
- 使用
- 九、Def导出
- Def语法
- entryname——导出函数名称 internalname——内部名称
- ordinal —— 导出函数的序号
- NONAME——只有序号,没有名字
- DATA —— 导出变量用的
- PRIVATE —— 只能显示加载,不能隐式加载
- 十、DLLMAIN
- 十一、DLL加载后内存空间
- 十二、DLL劫持
- 总结
前言
- 各位师傅大家好,我是qmx_07,今天讲解静态库和动态库的使用
一、使用obj保护源码
- 程序链接过程:
问题: 如果想让第三方使用 我们设计的程序功能,但是 不想源码构造实现泄露,该怎么办呢?
答案:通过编译后的obj文件,链接到exe程序使用
生成obj文件
-
Math头文件
-
Math.cpp文件
-
运行程序
-
成功生成obj文件
导入并使用obj文件
方式一 拖入解决方案
- 将Math.h 函数使用说明 和 Math.obj 编译封装好的函数实现,拖入解决方案,就可以直接使用啦
方式二 附加依赖项
- 属性->链接器->输入->附加依赖项,填写obj文件(注意每个obj一行)
适配C语言文件
- 可以观察到无法解析外部符号,没办法识别函数,涉及到名称粉碎机制
C语言的名称粉碎是:_Sub,_Add;
C++的名称粉碎是: ?Sub@@YAHHH@Z,?Add@@YAHHH@Z
解决方式: 在obj文件中,设置为C语言的名称粉碎机制,我们也可以使用条件编译,自动识别C语言文件还是CPP文件
- 使用上述obj文件,保护源码的方式,需要拷贝大量文件,所以引申出新的概念 - 静态库
二、静态库的概述
- 函数和数据被编译进一个二进制文件(通常扩展名为.lib)。在使用静态库的情况下,在编译可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其他模块组合起来创建最终的可执行文件(.exe文件)
- 本质:将大量obj文件打包成lib文件
- 缺点:
- 维护困难:如果.lib更新,使用的工程如需更新,则必须重新编译。
- 磁盘冗余:如果很多工程使用,就要拷贝很多份.lib文件,这些lib都是一样的
无法很好的同时兼容C和C++ - 其他语言无法使用
三、静态库的创建与使用
- 选择静态库程序
- 取消预编译头
- 点击生成选项,选择生成lib文件
使用静态库和使用 .obj 类似
- 添加头文件,使用者才能知道传的什么参数以及其他
- 拷贝lib文件和.h头文件到VS工程根目录
- 添加lib文件到工程的方式(用法):
a. 直接拖入项目中
b. 依赖项添加.lib文件
c. 代码内添加.lib文件 # pragma comment(lib,lib路径)
- 加载lib文件,成功运行
- 静态库中还可以定义 变量和类(仅限cpp使用,c语言中 没有类的概念)
四、动态库的概述
动态链接库(DLL) 通常不能直接运行,也不能接收信息,只有在其他模块调用动态链接库中的函数时,才能发挥作用。通常我们把完成某种功能的函数放在一个动态链接库中,提供给其他程序调用。DLL就是整个windows操作系统的基础。动态链接库不能直接运行,也不能接收消息。他们是一些独立的文件
Windows API中所有的函数都包含在DLL中,其中有3个重要的DLL:
- Kernel32.dll:包含用于管理内存、进程和线程的函数、例如CreateThread函数。
- User32.dll:它包含用于执行用户界面任务(如窗口的创建和消息的传送)的函数。例如CreateWindow函数。
- GDI32.dll:它包含用于画图和显示文本的函数
- 使用动态库的优点:
- 可以采用多种编程语言来编写。
- 增强产品的功能(扩展插件)
- 提供二次开发的平台(扩展插件)
- 简化项目管理(一个团队负责自己团队的dll)
- 可以节省磁盘空间和内存
- 有助于资源的共享
- 有助于实现应用程序的本地化
五、动态库的创建
- 选择动态链接库程序
- 不使用预编译头
Add.h:
#pragma once
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus__declspec(dllexport) int Add(int x, int y);
#ifdef __cplusplus
}
#endif // __cplusplus
Sub.h
#pragma once
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus__declspec(dllexport) int Sub(int x, int y);
#ifdef __cplusplus
}
#endif // __cplusplus
DLL中导出函数,需要在每一个将要被导出的函数前面添加标识符__declspec(dllexport)
Add.cpp
#include "Add.h"int Add(int x, int y)
{return x + y;
}
#include "Sub.h"int Sub(int x, int y)
{return x - y;
}
编译:生成DLL文件和LIB文件
动态库的LIB文件 与 静态库的LIB文件不同,LIB文件包含该DLL导出的函数和变量的符号名
DLL文件存在实际函数与数据
- 使用dependency工具查看导出函数
六、动态库的两种调用方式
隐式加载:
1. 在编译时,程序会将对 DLL 的引用嵌入到可执行文件中。
2. 在程序运行时,操作系统会自动加载并初始化 DLL。
3. 隐式加载不需要手动加载 DLL 或指定 DLL 的路径。
4. 函数调用时,直接使用函数名进行调用,编译器会根据嵌入的引用找到对应的函数地址。
5. DLL 的导入函数表会在程序加载时自动解析,可以直接访问 DLL 中的函数。
显式加载:
- 程序需要显式地通过代码来加载 DLL 并获取其函数地址。
- 使用 LoadLibrary 函数加载 DLL,并返回一个句柄,表示已加载的 DLL。
- 使用 GetProcAddress 函数根据函数名获取 DLL 中的函数地址。
- 加载后的 DLL 需要手动卸载,使用 FreeLibrary 函数释放 DLL 句柄。
- 函数调用时,需要通过函数指针来调用 DLL 中的函数。
显式加载和隐式加载主要的区别在于加载时机和加载方式。隐式加载在程序运行时自动加载 DLL,并且可以直接调用 DLL 中的函数。而显式加载需要手动加载 DLL,并使用函数指针来调用 DLL 中的函数。
七、动态链接库的隐式加载
__declspec(dllimport) 声明外部函数
__declspec(dllimport) 来表明函数是从动态链接库中引入
Add.h
#pragma once
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus__declspec(dllimport) int Add(int x, int y);
#ifdef __cplusplus
}
#endif // __cplusplus
Sub.h
#pragma once
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus__declspec(dllimport) int Add(int x, int y);
#ifdef __cplusplus
}
#endif // __cplusplus
- 成功加载使用动态库
使用宏优化导关键字dllimport
- 创建DLL文件的时候,需要使用dllimport, 使用DLL文件时需要使用dllexport ,需要经常更改 非常麻烦
- 使用条件编译,进行优化
#pragma once
#ifdef DLL_EXPORT
#define DLL_API __declspec(dllexport)
#else
#define DLL_API __declspec(dllimport)
#endif
代码含义:
-
#ifdef DLL_EXPORT:这个条件编译指令用于检查是否定义了DLL_EXPORT宏。如果定义了,表示当前是在编译DLL库的源代码,需要导出函数和数据。如果没有定义,则表示当前是在使用DLL的客户端代码,需要导入函数和数据。
-
#define DLL_API __declspec(dllexport):如果DLL_EXPORT被定义了,那么将DLL_API宏定义为__declspec(dllexport)。__declspec(dllexport)是在Windows平台上用于标记要导出的函数和数据的修饰符。
-
#else:如果DLL_EXPORT未被定义,执行下面的代码块。
-
#define DLL_API __declspec(dllimport):将DLL_API宏定义为__declspec(dllimport)。__declspec(dllimport)是在Windows平台上用于标记要导入的函数和数据的修饰符。
动态库的创建优化
- 创建DLL_API 根据DLL_EXPORT 选择是导出还是加载
- 预处理器加载DLL_EXPORT
- 将导入导出替换成DLL_API,编译DLL文件
- 导出成功
动态库的加载优化
- 将common.h文件加载,可以正常运行
- 补充:在实现动态链接库时,可以不导出整个类,而只导出该类中的某些函数,在导出类的成员函数的时候需要注意,该函数必须具有public类型的访问权限。
八、动态加载库的显示加载
隐式加载并不能满足所有需求;
- 比如有运行的过程中加载dll的需求。
- 生成exe的时候并不知道后面可能用到的dll
- 运行过程中加载dll,运行完之后就卸掉
LoadLibrary加载DLL
- LoadLIbraruy 函数 不仅能加载DLL(.dll) ,还可以加载可执行模块(.exe) 一般来说,当加载可执行模块时,主要为了访问该模块内的一些资源,例如对话框资源、位图资源或图标资源等。
- HMODULE和HINSTANCE类型可以通用,成功加载,返回模块句柄,失败返回NULL
FreeLibray卸载DLL
参数就是函数句柄 FreeLibrary(hDll);
GetProcAddress函数
- 介绍:通过LoadLibrary加载DLL之后,获取导出函数的地址或者导出变量的地址,函数通过函数指针访问,变量通过解引用访问
- 参数说明: 1.获取DLL的句柄
- 2.指向常量的字符指针,指定DLL导出的 函数名称,或者序号
- 返回值:调用成功返回指定导出函数的地址,否则返回NULL
使用
- 使用GetProcAddress找到DLL导出的函数地址,需要创建函数指针来承接
九、Def导出
让其他编程语言使用DLL文件,但是其他语言 没有C、CPP的语法,不能使用隐式加载
当混合使用C和C++编程的时候,要使用extern "C"修饰符来导出dll,因为c++的导出会对函数进行名称粉碎后的导出,所以为了保证在开发可执行程序的时候能够找到,所以需要使用同一个编译厂商进行可执行程序的开发。所以我们要用C的约定来进行开发
-
_stdcall 使用c的名称粉碎
-
给函数名添加下划线前缀和一个特殊的后缀,该后缀由一个@符号后跟作为参数传给函数的字节数组成
-
DEF导出:为了别的编译厂商的编译器在显示链接的时候能够链接到这个DLL,告知编译器不要对导出的函数名进行改编。我们就可以直接使用函数名来调用
-
可以观察到,使用c语言的名称粉碎,但是通过def导出名称,函数名没有增加任何修饰
-
注意: Def导出文件,函数各占一行
Def语法
entryname——导出函数名称 internalname——内部名称
- 函数内部Add 导出外部函数 SuperAdd
意义:函数做更新时候,可以在使用时直接换=后的函数名字,别名不用换
ordinal —— 导出函数的序号
默认从1开始,序号大小是两个字节,也就是极限是FFFF,是65535.
- id对于显示加载很重要,GetProcAddress()第二个参数,实际上动态加载拿到的是函数的地址
- 使用:通过序号获取函数地址
NONAME——只有序号,没有名字
DATA —— 导出变量用的
PRIVATE —— 只能显示加载,不能隐式加载
十、DLLMAIN
- DLL也可以有一个入口函数DllMain(),做初始化,当DLL加载的时候会自动调用。同时他也是一个可选函数,有需求就实现,系统调用,没有需求的话则编译器会自动生成一个空的DllMain函数,啥也没干。DllMain函数是进入动态链接库(DLL)的可选入口点。如果使用了该函数,则在进程和线程初始化和终止时,或者在调用LoadLibrary和FreeLibrary函数时,系统将调用该函数
1.创建动态库
Dllmain.cpp
#include <windows.h>__declspec(dllexport) void Foo()
{OutputDebugString("[DLL] Foo");
}BOOL WINAPI DllMain(_In_ void* _DllHandle,_In_ unsigned long _Reason,_In_opt_ void* _Reserved
)
{switch (_Reason){//不管 DLL_PROCESS_ATTACH 返回值是啥,都会在调用 DllMain 执行 DLL_PROCESS_DETACHcase DLL_PROCESS_ATTACH:{OutputDebugString("[DLL] DLL_PROCESS_ATTACH");break;}case DLL_PROCESS_DETACH:{OutputDebugString("[DLL] DLL_PROCESS_DETACH");break;}default:break;}return TRUE;
}
得到动态链接库文件
显式加载:
#include <iostream>
#include <windows.h>
using namespace std;int main()
{HMODULE hDll = LoadLibrary(R"(Dllmain.dll)");if (hDll == NULL){cout << "加载失败" << endl;return 0;}FreeLibrary(hDll);cout << "执行成功" << endl;system("pause");return 0;
}
- 如果使用隐式加载,返回FALSE,则进程回弹出 应用程序无法正常启动XXX的的错误,并且会在次调用Dllmain并传入标志DlL_PROCESS_DETACH
- 如果是显示加载,loadlibrary 返回NULL,并且会在次调用Dllmain并传入标志DLL_PROCESS_DETACH
- 第一次加载,attach ,第二次加载,没有DLL_PROCESS_ATTACH ;
- 第一次释放,没有来,第二次释放,DLL_PROCESS_DETACH ;
解释:因为有引用计数,第二次加载的时候不再把相同dll导入,但是计数会+1;再释放的时候,只有计数为0 了,才会走DLL_PROCESS_DETACH 分支!
LoadLibrary 加载库时的引用计数与内核对象有关。
当调用 LoadLibrary 函数加载一个 DLL 时,操作系统会为该 DLL 创建一个内核对象,该对象负责管理该 DLL 的引用计数。每次成功调用 LoadLibrary 函数时,引用计数会递增。而当调用 FreeLibrary 函数卸载 DLL 时,引用计数会递减。只有当引用计数为零时,操作系统才会卸载 DLL 并释放相关资源。
这种引用计数的机制确保了在多个模块或线程使用同一个 DLL 的情况下,DLL 不会被意外卸载。只有当最后一个使用该 DLL 的模块或线程调用 FreeLibrary 时,才会真正卸载 DLL。
需要注意的是,引用计数是在内核对象层级上维护的,因此它是在操作系统内核中进行管理,而不是在用户空间的进程中。这种机制确保了引用计数的准确性和一致性。
十一、DLL加载后内存空间
- dll导出全局变量,两个进程使用,A进程对全局变量修改会不会影响B进程的变量?
A进程修改dll 里面的变量 ,不会影响 B进程 ,因为存在写时拷贝 (A进程 或者 B进程修改一个变量时,系统会为这个变量重新申请一块空间,修改的只是新申请内存里面的值,不会改动原来的内存
- 两个进程加载同一个dll,虚拟内存中dll有几份?物理内存中dll有几份?
虚拟内存有2份,物理内存有1份
- dll什么时候从物理内存中卸载掉?
AB都不用dll的时候,只要有一个在使用dll,dll就一直在物理内存中加载着
A , B进程加载同一个DLL
- DLL进入物理内存(RAM)
- DLL映射到A进程和B进程里不同的位置(物理内存占一份,虚拟内存占两份)
- DLL内部创建的对象或则全局变量不属于DLL,而是属于进程。
- DLL映射到的虚拟内存修改时,进行写实拷贝。
DLL从物理内存卸载的时机:既没有DLL在物理内存的引用计数为0的时候
十二、DLL劫持
#include <windows.h>#pragma comment(linker, "/EXPORT:?Foo@@YAXXZ=DLL_OLD.?Foo@@YAXXZ")
BOOL WINAPI DllMain(
_In_ void* _DllHandle,
_In_ unsigned long _Reason,
_In_opt_ void* _Reserved
)
{switch (_Reason){case DLL_PROCESS_ATTACH:{OutputDebugString("[DLL] NewDLL_PROCESS_ATTACH");break;}case DLL_PROCESS_DETACH:{OutputDebugString("[DLL] NewDLL_PROCESS_DETACH");break;}default:break;}return TRUE;
}
总结
- 介绍了静态库和动态库 显示链接、静态链接的使用,Def导出、DLLMAIN函数,以及DLL劫持