目录
方式二:__declspec(dllimport) 声明外部函数
1. entryname——导出函数名称 internalname——内部名称
一、如何保护源码
程序编译链接过程
不想让别人拿到源代码,但是想让其使用功能,根据上图观察,把自己生成的obj给对方,对方拿到obj后,链接到自己的程序中。
新建一个控制台项目进行测试,目录结构
Math.h
Math.cpp
test.cpp
编译后,会生成一个 Math.obj的文件
再新建一个工程使用Math.obj
首先,包含头文件,其次需要导入 .obj文件
方式一:直接托进解决方案里;
方式二:项目-属性-链接器-输入-附加依赖项-箭头-编辑-添加obj文件(一行一个obj文件)
项目目录结构
Math.h
如何兼容C?
test.c
链接时报错
原因:
C语言的名称粉碎是:_Sub,_Add;
C++的名称粉碎是: ?Sub@@YAHHH@Z,?Add@@YAHHH@Z
编译器拿着“?Sub@@YAHHH@Z”,在obj中匹配C的_Sub,当然匹配不上解决办法:告诉编译器,名称粉碎的时候,按照C的名称粉碎规则进行粉碎。
C++ 项目使用时,函数声明加上extern "C"后,C++支持extern "C"语法,能够直接使用
C项目使用时,由于函数声明上extern "C",但是C不支持该语法,不认识,所以编译不通过解决办法:头文件被C 包含的时候前面不加extern "C" int Add(int n1, int n2);
头文件被C++ 包含的时候,声明前面加上extern "C" ,说明用C风格名称粉碎去找实现extern "C"条件编译宏:这样使用的时候就可以不管是C包含还是Cpp包含了
//要想C 和C++ 都能所用该obj,声明的前面必须加上extern "C",生成的obj文件名称粉碎是C风格的。C++可以使用,C也可以使用
//C++ 项目使用时,函数声明加上extern "C"后,C++支持extern "C"语法,能够直接使用
//C 项目使用时,由于函数声明上extern "C",但是C不支持该语法,不认识,所以编译不通过。
//解决办法:头文件被C 包含的时候前面不加extern "C" int Add(int n1, int n2);
// 头文件被C++ 包含的时候,声明前面加上extern "C" ,说明用C风格名称粉碎去找实现extern "C" int Add(int n1, int n2);
//条件编译宏:这样使用的时候就可以不管是C包含还是Cpp包含了
#ifdef __cplusplus
extern "C"
{
#endif // __cplusplus
int Add(int n1, int n2);
#ifdef __cplusplus
}
#endif // __cplusplus
上述的obj的方法中,当有很多obj时候,需要拷贝很多的obj,很不方便,考虑将这些obj合并成一个大的“obj”,这时就引出了静态库的概念。
补充:
#pragma once
是一种预处理指令,用于确保头文件只被编译一次。当一个头文件被多次包含在不同的源文件中时,使用 #pragma once
可以防止重复包含,从而避免编译错误和重复定义的问题。
#pragma once
的作用类似于传统的头文件保护宏(header guard),但更加简洁和方便。传统的头文件保护宏需要在头文件开头和结尾分别使用条件编译语句,如 #ifndef HEADER_NAME_H
、#define HEADER_NAME_H
和 #endif
,以确保头文件只被编译一次。而 #pragma once
只需要在头文件的开头使用一次,即可达到相同的效果。
使用 #pragma once
的好处是可以提高编译速度,因为编译器可以直接根据指令判断是否需要重新编译头文件。而传统的头文件保护宏需要进行条件判断,会增加编译时间和额外的预处理工作。
二、静态库 动态库 概述
函数和数据被编译进一个二进制文件(通常扩展名为.lib)。在使用静态库的情况下,在编译可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其他模块组合起来创建最终的可执行文件(.exe文件)。
本质:把所有的obj文件全部打包到一个.lib文件内。
缺点:
- 维护困难:如果.lib更新,使用的工程如需更新,则必须重新编译。
- 磁盘冗余:如果很多工程使用,就要拷贝很多份.lib文件,这些lib都是一样的
- 无法很好的同时兼容C和C++
- 其他语言无法使用
动态链接库(DLL) 通常不能直接运行,也不能接收信息,只有在其他模块调用动态链接库中的函数时,才能发挥作用。通常我们把完成某种功能的函数放在一个动态链接库中,提供给其他程序调用。DLL就是整个windows操作系统的基础。动态链接库不能直接运行,也不能接收消息。他们是一些独立的文件。
Windows API中所有的函数都包含在DLL中,其中有3个重要的DLL:
- Kernel32.dll:包含用于管理内存、进程和线程的函数、例如CreateThread函数。
- User32.dll:它包含用于执行用户界面任务(如窗口的创建和消息的传送)的函数。例如CreateWindow函数。
- GDI32.dll:它包含用于画图和显示文本的函数。
使用动态链接库的好处:
- 可以采用多种编程语言来编写。
- 增强产品的功能(扩展插件)
- 提供二次开发的平台(扩展插件)
- 简化项目管理(一个团队负责自己团队的dll)
- 可以节省磁盘空间和内存
- 有助于资源的共享
- 有助于实现应用程序的本地化。
三、静态链接库创建与使用
VS2019中直接找到静态链接库,一路确认即可
不适用预编译头即可
项目目录:
pch.h framework.h 文件是作用是减少重复文件编译,提升性能有关。不用管
如果想建立一个自己的静态链接库,直接添加 .h .cpp文件即可,编译后就可以得到 .lib 文件
使用静态库和使用 .obj 类似
1. 添加头文件,使用者才能知道传的什么参数以及其他
2. 拷贝lib文件和.h头文件到VS工程根目录
3. 添加lib文件到工程的方式(用法):
a. 直接拖入项目中
b. 依赖项添加.lib文件
c. 代码内添加.lib文件 # pragma comment(lib,lib路径)
如何把两个 obj 合成为 lib
静态库中还可以放 全局变量,类(通过源文件右击添加-类)
四、动态链接库创建
新建>>类向导>>项目类型>>.dll动态链接库。
动态链接库中有导出函数和非导出函数:
- 导出函数:DLL提供给其他应用程序调用的函数
- 非导出函数:给DLL内的函数调用的函数,中间函数等。
如果想导出函数给外面的工程使用,需要指定函数,告诉编译器哪个函数需要导出
从DLL中导出函数:
为了让DLL导出一些函数,需要在每一个将要被导出的函数前面添加标识符__declspec(dllexport)
编译:生成DLL文件和LIB文件
LIB文件:称为DLL的导入库文件,是一个特殊的库文件,和静态库文件有着本质上的区别,引入库文件包含该DLL导出的函数和变量的符号名;而DLL文件包含该DLL实际函数和数据。
工程结构:
CTest.h
#pragma once
class __declspec(dllexport) CTest
{
public:
void Show();
};
Add.h
#pragma once
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
__declspec(dllexport) int Add(int n1, int n2);
__declspec(dllexport) extern int g_nVal;
#ifdef __cplusplus
}
#endif // __cplusplus
Sub.h
#pragma once
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
__declspec(dllexport) int Sub(int n1, int n2);
#ifdef __cplusplus
}
#endif // __cplusplus
Add.cpp
#include "Add.h"
int Add(int n1, int n2)
{
return n1 + n2;
}
int g_nVal = 0x12345678;
CTest.cpp
#include "CTest.h"
#include <iostream>
using namespace std;
void CTest::Show()
{
cout << "CTest::Foo()" << endl;
}
dll.cpp
#include <iostream>
#include "CTest.h"
#include "Add.h"
#include "Sub.h"
int main()
{
std::cout << Add(1, 2) << std::endl;
std::cout << Sub(2, 1) << std::endl;
CTest test;
test.Show();
}
Sub.cpp
#include "Sub.h"
int Sub(int n1, int n2)
{
return n1 - n2;
}
编译后生成以下文件:
在下面动态链接库的debug目录下:生成了dll文件;dll.exp 文件是一个输出库文件。
LIB文件:称为DLL的导入库文件,是一个特殊的库文件,和静态库文件有着本质上的区别,引入库文件包含该DLL导出的函数和变量的符号名;而DLL文件包含该DLL实际函数和数据。
查看导出函数工具-DEPENDS,拖进去使用即可
五、动态链接库的两种调用方式
显式加载和隐式加载是在使用动态链接库(DLL)时的两种加载方式。下面我将为你解释这两种加载方式的区别:
-
隐式加载(Implicit Loading):
- 在编译时,程序会将对 DLL 的引用嵌入到可执行文件中。
- 在程序运行时,操作系统会自动加载并初始化 DLL。
- 隐式加载不需要手动加载 DLL 或指定 DLL 的路径。
- 函数调用时,直接使用函数名进行调用,编译器会根据嵌入的引用找到对应的函数地址。
- DLL 的导入函数表会在程序加载时自动解析,可以直接访问 DLL 中的函数。
-
显式加载(Explicit Loading):
- 程序需要显式地通过代码来加载 DLL 并获取其函数地址。
- 使用
LoadLibrary
函数加载 DLL,并返回一个句柄,表示已加载的 DLL。 - 使用
GetProcAddress
函数根据函数名获取 DLL 中的函数地址。 - 加载后的 DLL 需要手动卸载,使用
FreeLibrary
函数释放 DLL 句柄。 - 函数调用时,需要通过函数指针来调用 DLL 中的函数。
显式加载和隐式加载主要的区别在于加载时机和加载方式。隐式加载在程序运行时自动加载 DLL,并且可以直接调用 DLL 中的函数。而显式加载需要手动加载 DLL,并使用函数指针来调用 DLL 中的函数。显式加载提供了更大的灵活性和控制权,适用于需要在运行时动态加载和卸载 DLL 的情况,而隐式加载则更加简单和方便。
六、动态链接库的隐式加载
静态调用步骤:
- 新建应用工程。
- 通过编译器供给应用程序关于DLL的名称,以及DLL函数的链接参考(.h文件)。这种方式不需要在程序中用代码将DLL加载到内存。
- 将DLL和LIB文件拷贝到工程目录下
- 将lib文件添加到工程
- 方式一:项目>>属性>>链接>>依赖项>>lib名称
- 方式二:拖入到项目
- 添加头文件>>直接调用头文件中的函数即可。
新建一个控制台项目,目录结构如下
Add.h
#pragma once
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
__declspec(dllimport) int Add(int n1, int n2);
__declspec(dllimport) extern int g_nVal;
#ifdef __cplusplus
}
#endif // __cplusplus
Sub.h
#pragma once
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
__declspec(dllimport) int Sub(int n1, int n2);
#ifdef __cplusplus
}
#endif // __cplusplus
CTest.h
#pragma once
class __declspec(dllimport) CTest
{
public:
void Show();
};
.cpp
#include <iostream>
#include "CTest.h"
#include "Add.h"
#include "Sub.h"
#pragma comment(lib,"dll.lib")
int main()
{
std::cout << Add(1, 2) << std::endl;
std::cout << Sub(3, 4) << std::endl;
std::cout << std::hex << g_nVal << std::endl;
CTest test;
test.Show();
}
动态链接库与可执行文件放在同一目录下:
lib文件放到根目录下
方式一:使用 extern 声明外部函数
extern
关键字在C和C++中都有着重要的作用,它的具体含义取决于它所修饰的变量或函数。
在C语言中,extern
关键字用于声明一个变量或函数是在别处定义的,告诉编译器该变量或函数的定义在其他地方,不在当前文件中。具体来说:
-
外部变量声明:在C语言中,当你在一个文件中使用了一个全局变量,而该变量的定义在另外一个文件中时,你可以使用
extern
来声明该变量,以便编译器知道该变量的定义在其他地方。// 在一个文件中声明外部变量 extern int global_var; // 声明global_var是在其他文件中定义的全局变量
-
外部函数声明:
extern
也可以用于声明外部函数,在这种情况下,它告诉编译器该函数的定义在其他地方,不在当前文件中。// 外部函数声明 extern void external_function(); // 声明external_function是在其他文件中定义的函数
方式二:__declspec(dllimport) 声明外部函数
除了使用extern 关键字表明函数是外部定义的之外,还可以使用标识符:__declspec(dllimport) 来表明函数是从动态链接库中引入的。
__declspec(dllimport) 与使用extern 关键字这种方式相比,再使用__declspec(dllimport) 标识符声明外部函数时,它将告诉编译器该函数是从动态链接库中引入的,编译器可以生成运行效率更高的代码。所以调用的函数来自于动态链接库,则应该使用这种方式来声明外部函数。
标准来说,无论是全局变量,还是函数都是需要使用关键字dllimport
使用宏优化导关键字dllimport
代码如下:
#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库时使用DLL_API
宏来修饰要导出的函数和数据,而在使用DLL库的客户端代码中使用DLL_API
宏来修饰要导入的函数和数据。这样可以保证在编译时正确地处理导出和导入函数的修饰符。
动态链接库创建优化
预处理器包含宏:DLL_EXPORT
DLL_API 替换 __declspec(dllexport),并包含 common.h 头文件
加载动态链接库优化
不需要包含这个宏即可, .h 文件 包含 common.h 文件 替换宏即可
读取全局变量来说,最好直接无脑加 extern 关键字
补充:在实现动态链接库时,可以不导出整个类,而只导出该类中的某些函数,在导出类的成员函数的时候需要注意,该函数必须具有public类型的访问权限。
兼容 C
如果使用C++语言编写了一个DLL,那么使用C语言编写的客户端程序访问DLL中的函数就会出现问题,因为后者将使用函数原始名称来调用DLL中的函数,而C++编译器已经对该名称进行了改编,所以C语言编写的客户端程序就找不到所需的DLL导出函数。
#pragma once
#ifdef DLL_EXPORT
#define DLL_API extern "C" __declspec(dllexport)
#else
#define DLL_API extern "C" __declspec(dllimport)
#endif
利用限定符 extern “C” 可以解决C++和C语言之间的相互调用是函数命名问题。但是这种方法有一个缺陷,就是不能用于导出一个类的成员函数和全局变量,只能用于导出全局函数这种情况。
如果导出函数的调用约定发生了变化,那么即使使用了限定符 extern “C” ,该函数的名字仍然会发生改编。
在这种情况下,可以通过一个称为模块定义文件(DEF) 的方式来解决名字改编问题。
七、动态链接库的显示加载
隐式加载并不能满足所有需求;
- 比如有运行的过程中加载dll的需求。
- 生成exe的时候并不知道后面可能用到的dll
- 运行过程中加载dll,运行完之后就卸掉;
加载DLL API: LoadLibrary
LoadLIbraruy 函数 不仅能加载DLL(.dll) ,还可以加载可执行模块(.exe) 一般来说,当加载可执行模块时,主要为了访问该模块内的一些资源,例如对话框资源、位图资源或图标资源等。
参数:dll的文件名,或者是dll的路径
返回值是HMODULE类型,和HINSTANCE类型可以通用,成功加载,返回模块句柄,失败返回NULL;
返回的句柄不是12345678那样的值,这个值如下图
和hinstance很像,其实就是一个模块句柄,dll在进程中的首地址叫做hMoudle;
卸载DLL API: FreeLibray
使用很简单:参数就是函数句柄 FreeLibrary(hDll);
GetProcAddress函数
当获取到动态链接库模块的句柄后,接下来想办法获取该动态链接库中导出函数的地址,这可以通过调用GetProcAddres 函数来实现。
使用:获取导出函数的地址或者导出变量的地址,函数通过函数指针访问,变量通过解引用访问。
参数说明:
- 模块句柄,即LoadLibrary函数的返回值
- 一个指向常量的字符指针,指定DLL导出函数的名字或函数的序号,如果该参数指定的是导出的函数序号,那么该序号必须在低位字中,高位字必须是0。
返回值:调用成功返回指定导出函数的地址,否则返回NULL
加载DLL目录优先级
工具 - Process Hacker
Process Hacker ——用来查看系统里面进程的信息,任务管理器可以查看,但是太少了,做开发需要更详细的。
下载地址:Overview - Process Hacker
界面:
下面是一段加载DLL的代码:
HMODULE hDll = LoadLibrary(R"(dll.dll)");
1. 优先在和exe同一目录下查找
2. 系统目录
3. Winodws目录下
4. PATH环境变量查找
代码如下
#include <iostream>
#include <Windows.h>
using namespace std;
using PFN_ADD = int (*)(int, int);
int main()
{
HMODULE hDll = LoadLibrary(R"(E:\CR41\第二阶段\Windows\01-静态库和动态库\UseDllLoad\Debug\dll.dll)");
//HMODULE hDll = LoadLibrary("E:\\CR41\\第二阶段\\Windows\\01-静态库和动态库\\UseDll\\Debug\\dll.dll");
//HMODULE hDll = LoadLibrary("E:/CR41/第二阶段/Windows/01-静态库和动态库/UseDll/Debug/dll.dll");
if (hDll == NULL)
{
cout << "加载失败" << endl;
return 0;
}
//使用导出函数
PFN_ADD pfnAdd = (PFN_ADD)GetProcAddress(hDll, "?Add@@YAHHH@Z");
if (pfnAdd == NULL)
{
cout << "获取函数地址失败" << endl;
return 0;
}
int nVal = pfnAdd(1, 2);
nVal = pfnAdd(3, 4);
//使用导出变量
int* pVal = (int*)GetProcAddress(hDll, "?g_nVal@@3HA");
*pVal = 0x1111111;
FreeLibrary(hDll);
return 0;
}
八、DEF导出
def导出是专门给其他语言使用的,给其他语言用dll有一个问题,不能通过隐式加载的方式使用;
隐式有lib,头文件是c,c语法的,其他语言未必兼容c,c语法;其他语言可以使用显示加载,但是会有一个小问题:函数名字是粉碎后的,太恶心,想直接使用函数名(Add,Sub);也就是直接使用函数名进行调用
我们知道,当混合使用C和C++编程的时候,要使用extern "C"修饰符来导出dll,因为c++的导出会对函数进行名称粉碎后的导出,所以为了保证在开发可执行程序的时候能够找到,所以需要使用同一个编译厂商进行可执行程序的开发。所以我们要用C的约定来进行开发。
但是使用C语言导出时,如果调用约定是 __stdcall
给函数名添加下划线前缀和一个特殊的后缀,该后缀由一个@符号后跟作为参数传给函数的字节数组成。
DEF都出目的:为了别的编译厂商的编译器在显示链接的时候能够链接到这个DLL,告知编译器不要对导出的函数名进行改编。我们就可以直接使用函数名来调用
新建一个空项目
简单的写两个加减函数,此时编译链接,生成dll文件;是空的,说明此时还没有导出任何函数
注意:名字不重要,后缀一定要是.def
注意:
- 每个函数占一行;
- c/c++依然可以使用隐式加载的方式使用
- def只有c语法,def用c语法是没有办法描述重载的!
演示隐式加载
把 lib 文件放到根目录,dll文件放到和 exe同一目录下,测试代码如下:
演示显示加载
DEF语法
1. entryname——导出函数名称 internalname——内部名称
使用
意义:函数做更新时候,可以在使用时直接换=后的函数名字,别名不用换
2. ordinal —— 导出函数的序号
默认从1开始,序号大小是两个字节,也就是极限是FFFF,是65535.
id对于显示加载很重要,GetProcAddress()第二个参数,实际上动态加载拿到的是函数的地址
使用:通过序号获取函数地址
3. NONAME——只有序号,没有名字
4. _DATA —— 导出变量用的
报错
5. PRIVATE —— 只能显示加载,不能隐式加载
注意:不能导出类
using 和typedef一样,但是typedef不能定义模板,using可以定义模板
九、DLLMAIN
DLL也可以有一个入口函数DllMain(),做初始化和反初初始,当DLL加载的时候会自动调用。同时他也是一个可选函数,有需求就实现,系统调用,没有需求的话则编译器会自动生成一个空的DllMain函数,啥也没干。DllMain函数是进入动态链接库(DLL)的可选入口点。如果使用了该函数,则在进程和线程初始化和终止时,或者在调用LoadLibrary和FreeLibrary函数时,系统将调用该函数。
模板
#include <windows.h>
extern "C" BOOL WINAPI DllMain (
HINSTANCE const instance, // handle to DLL module
DWORD const reason, // reason for calling function
LPVOID const reserved) // reserved
{
// Perform actions based on the reason for calling.
switch (reason)
{
case DLL_PROCESS_ATTACH:
// Initialize once for each new process.
// Return FALSE to fail DLL load.
break;
case DLL_THREAD_ATTACH:
// Do thread-specific initialization.
break;
case DLL_THREAD_DETACH:
// Do thread-specific cleanup.
break;
case DLL_PROCESS_DETACH:
// Perform any necessary cleanup.
break;
}
return TRUE; // Successful DLL_PROCESS_ATTACH.
}
创建项目
新建 .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_DETACH
case 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>
using namespace std;
#pragma comment(lib,"Project1.lib")
void Foo();
int main()
{
Foo();
cout << "执行成功" << endl;
system("pause");
}
DLL加载释放调用DLLMAIN
显示加载
#include <iostream>
#include <windows.h>
using namespace std;
int main()
{
HMODULE hDll = LoadLibrary(R"(Project1.dll)");
if (hDll == NULL)
{
cout << "加载失败" << endl;
return 0;
}
FreeLibrary(hDll);
cout << "执行成功" << endl;
system("pause");
return 0;
}
对之前的dll代码做一点修改,DLL_PROCESS_ATTACH分支返回FALSE;
#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_DETACH
case DLL_PROCESS_ATTACH:
{
OutputDebugString("[DLL] DLL_PROCESS_ATTACH");
return FALSE;
break;
}
case DLL_PROCESS_DETACH:
{
OutputDebugString("[DLL] DLL_PROCESS_DETACH");
break;
}
default:
break;
}
return TRUE;
}
错误:当DLL_PROCESS_ATTACH返回FALSE的时候,会出现如下错误,并调用DLL_PROCESS_DETACH
两种方式都会输出调试信息,但是隐式加载会弹框
- 如果使用隐式加载,返回FALSE,则进程回弹出 应用程序无法正常启动XXX的的错误,并且会在次调用Dllmain并传入标志DlL_PROCESS_DETACH
- 如果是显示加载,
loadlibrary
返回NULL,并且会在次调用Dllmain并传入标志DLL_PROCESS_DETACH
原因:返回false,初始化失败的时候,他也会再调用你的dllmain,传一个detach的标志进来;就是再给你个机会,有部分资源已经初始化,有部分资源没有初始化,没初始化的,已经失败了,初始化的——反初始化;
动态加载失败——句柄是空的,dll没有被导入;
注意:return FALSE 只对DLL_PROCESS_ATTACH有效!
同一个文件使用LoadLibrary加载两次,释放一次。只会有一次ATTACH,而没有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加载后内存空间
问1:dll导出全局变量,两个进程使用,A进程对全局变量修改会不会影响B进程的变量
答1:A进程修改dll 里面的变量 ,不会影响 B进程 ,因为存在写时拷贝 (A进程 或者 B进程修改一个变量时,系统会为这个变量重新申请一块空间,修改的只是新申请内存里面的值,不会改动原来的内存。
问2:两个进程加载同一个dll,虚拟内存中dll有几份?物理内存中dll有几份?
答2:虚拟内存有2份,物理内存有1份
问3 dll什么时候从物理内存中卸载掉?
答3:AB都不用dll的时候,只要有一个在使用dll,dll就一直在物理内存中加载着
A , B进程加载同一个DLL
- DLL进入物理内存(RAM)
- DLL映射到A进程和B进程里不同的位置(物理内存占一份,虚拟内存占两份)
- DLL内部创建的对象或则全局变量不属于DLL,而是属于进程。
- DLL映射到的虚拟内存修改时,进行写实拷贝。
DLL从物理内存卸载的时机:既没有DLL在物理内存的引用计数为0的时候。
物理内存和虚拟内存上都有引用计数。
十、DLL劫持
劫持步骤:
函数转发是Windows操作系统提供的
重写旧的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;
}
劫持导致的病毒-lpk病毒
低版本可以,win10之前,win10改变了dll加载的策略,系统dll会直接到系统的目录下加载dll,如果病毒想替换dll,需要管理员权限;这就需要提权,一般的提权方式都已经被杀软查杀;
dll加载到进程,会拥有和进程一样的权限,进程有什么权限,dll就有什么权限;dll转发后的dll也是这样
win7之后,加了注册表选项,如果修改注册表选项,dll加载顺序就会改变,就不会先走exe的目录了