目录
[二、静态库 动态库 概述](#二、静态库 动态库 概述)
[方式一:使用 extern 声明外部函数](#方式一:使用 extern 声明外部函数)
[方式二:__declspec(dllimport) 声明外部函数](#方式二:__declspec(dllimport) 声明外部函数)
[兼容 C](#兼容 C)
[加载DLL API: LoadLibrary](#加载DLL API: LoadLibrary)
[卸载DLL API: FreeLibray](#卸载DLL API: FreeLibray)
[1. entryname------导出函数名称 internalname------内部名称](#1. entryname——导出函数名称 internalname——内部名称)
[2. ordinal ------ 导出函数的序号](#2. ordinal —— 导出函数的序号)
[3. NONAME------只有序号,没有名字](#3. NONAME——只有序号,没有名字)
[4. _DATA ------ 导出变量用的](#4. _DATA —— 导出变量用的)
[5. PRIVATE ------ 只能显示加载,不能隐式加载](#5. PRIVATE —— 只能显示加载,不能隐式加载)
一、如何保护源码
程序编译链接过程
不想让别人拿到源代码,但是想让其使用功能,根据上图观察,把自己生成的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 类似
-
添加头文件,使用者才能知道传的什么参数以及其他
-
拷贝lib文件和.h头文件到VS工程根目录
-
添加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)");
- 优先在和exe同一目录下查找
- 系统目录
-
Winodws目录下
-
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可以定义模板