动态链接库
MFC dll
概述
MFC 规则 dll 可以使用 MFC 类库,编译后的 dll 文件可以被所有支持 dll 技术的语言调用。
默认情况下 DLL 的入口点函数都是 DllMain ,MFC 规则 DLL 也不例外,但是 DllMain 函数已经被 MFC 所封装,导致在工程中是看不到 DllMain 函数的,就好像在 MFC 对话框工程中找不到 WinMain 函数一样,不过也有一些补救的方法,就是 InitInstance 和 ExitInstance 函数,当进程初始化调用 DLL 时, DLL会默认调用函数 InitInstance,当 exe 退出时 DLL 会调用 ExitInstance 函数,但是当有新线程时处于程序安全性考虑则没有什么好的办法进行处理。
分类
静态链接
静态链接方式使用 MFC规则 DLL时,编译器会将 MFC 类库的代码直接编译生成到 DLL 文件中,在调用这种DLL的接口时,MFC使用DLL内部的资源,因此不需要模块状态的切换,但是缺点就是使用这种方式生成的 DLL 文件比较大。
动态链接
动态链接 MFC规则DLL 方式中,dll 需要动态链接到 MFC 库,在这种情况下 MFC 使用主应用程序(即 EXE 程序) 的资源句柄来加载资源模板,这样,当 DLL 和应用程序中存在相同 ID 的资源时,就要进行模块切换,以便 MFC 能够找到正确的资源模板。本方式编译的文件较小。
案例
创建MFC DLL项目
创建MFC动态链接库的方法

共享的 MFC DLL 编译后可被所有支持 DLL 技术的程序使用,如果是 MFC 扩展 DLL 则编译后只能给 MFC 程序使用
按照下面的步骤为 MFC DLL 添加对话框,但是 DLL 运行起来后不会自动打开该对话框

每个可视化窗口都要有对应的类,通过上面方法在 MFC DLL 项目中添加的对话框是没有相关类的,所以要先在对话框窗体上通过右键菜单关联一个类



在注入用 dll 中可以直接调用宿主进程的函数,不需要像外部进程(先获取窗口句柄,再获取进程PID,再获取进程句柄,再通过 CreateRemoteThread 创建远程线程调用目标进程函数)
通过一系列操作才能调用目标进程的函数。继续上一步在 MFC DLL 中手动添加对话框后在其中再放置一个按钮,并在按钮单击事件中填写下面的代码 - 调用目标进程函数
void CPage01::OnBnClickedButton1()
{
int funAddr = 0x00381070;
_asm {
push 3
call funAddr
add esp,4
}
}
然后通过 代码注入器32位x86用.exe 将本 MFC DLL 文件注入到目标进程后会自动显示该 DLL 文件的对话框窗体,点击按钮可以调用目标进程的函数。
动态链接方式
在 vs ide 中显示的名称是 使用共享 MFC DLL 的常规 DLL,所以创建动态链接方式的 MFC DLL 项目时要选择该类型

VS IDE 中为 MFC 项目添加对话框资源的方法

IDE创建的项目默认只有函数 InitInstance ,要添加其他生命周期函数按照下图的步骤

资源标识冲突
DLL中资源的ID和EXE中资源的ID冲突是资源头文件中的数字标识相同

如 DLL 项目中的对话框的资源ID是129,EXE 项目中对话框资源的ID也是129,在 DLL 代码中在使用资源之前要用到宏限定使用本DLL项目内的资源,即下面第二行代码
void ShowDlg() {
AFX_MANAGE_STATE(AfxGetStaticModuleState());
CDialog dlg(IDD_DIALOG1);
dlg.DoModal();
}
如果使用了第三方 dll ,那么可以在自己的 EXE 程序中使用下面方法解决资源ID冲突
void CMFCApplication1Dlg::OnBnClickedBtnDll()
{
HINSTANCE hExeInst = GetModuleHandle(NULL);
HINSTANCE hDllInst = GetModuleHandle(_T("MFCDll.dll"));
ASSERT(hExeInst && hDllInst);
AfxSetResourceHandle(hDllInst);
ShowDlg();// dll导出的函数,在此前后更换资源模块
AfxSetResourceHandle(hExeInst);
}
即在使用 dll 导出函数的前后更换模块句柄
win32 dll
概述
简介
动态链接库可以看成是一种仓库,一种资源的集合:函数,变量,类,资源(图标、鼠标)等等,都可以由动态链接库导出供其他程序使用,相对于静态链接库(不可包含其他静态、动态库),动态链接库可以包含其他静态库、动态库。
查看导出的函数
使用 vc++ 自带的工具 depends 进行查看,只能查看到函数的名称,具体的参数和返回值看不到,所以一般都将动态链接库中的头文件交给开发者 工具 depends 在 home.esxi.win7.全新6T硬盘 F:\homeWin7new6t\setup\Dev\cpp 下的文件 Dependencies_x64_Release.zip,通过 file > open 打开 dll 文件后如下图:

分类
- 非MFC的DLL - 即使用 SDK API 进行编程,能被其他素有语言调用
- MFC规则DLL - 可以使用 MFC 进行编程,能被其他所有语言调用
- MFC 扩展 DLL - 可以使用 MFC进行编程,但只能被用MFC编写的程序调用(扩展 MFC 程序的DLL)
导出函数的方式
概述
通过 *.def 文件或者 __declspec(dllexport) 关键字
def 文件方式导出
通过 *.def 文件方式导出函数要做到如下几点:
- 文件中的第一个语句必须是
LIBRARY语句,此语句将.def文件标识为属于DLL。LIBRARY语句的后面是DLL的名称,连接器将此名称放到 DLL 的导入库中 - EXPORTS 语句列出名称,可能的话还会列出 DLL 导出函数的序号值。通过在函数名称的后面加上符号
@和一个数字,给函数分配序号值,序号值的范围必须是从1到N,其中 N 是DLL 导出函数的个数 - 英文符号分号开头表示注释语句
见案例代码:
LIBRARY "DllTest"
EXPORTS
add @1 ;分号开始是注释,调用者可通过序号调用也可以通过函数名称调用
普通 dll 项目中在自定义文件 funs.cpp 中创建自定义函数并导出的案例看下图

__declspec 关键字导出
为了防止函数名称被编译器篡改,要在函数名称之前使用 extern "C" _declspec(dllexport) 例如:
extern "C" _declspec(dllexport) int add(int a, int b);
如果要进行函数的导入(exe 程序中要使用 dll 导出的函数称作导入)则把 dllexport 换成 dllimport 即可,如:extern "C" _declspec(dllimport) 。通过关键字导出函数则不需要 def 文件,但是要注意需要为自定义 cpp 源码文件创建对应的头文件,源码文件中按照常规做法制作函数,头文件中声明函数时前面要带有导出的关键字

入口函数
IDE 创建动态库项目后自动创建的入口函数代码如下:
BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
参数介绍:
hModule:指向DLL本身的实例句柄
ul_reason_for_call:提供4个枚举值:DLL_PROCESS_ATTACH、DLL_THREAD_ATTACH、DLL_THREAD_DETACH、DLL_PROCESS_DETACH DLL_PROCESS_ATTACH - 表示第一次被进程调用,同一个进程后来的多次调用都不会再触发本函数,只会增加DLL的使用次数 DLL_THREAD_ATTACH - 当进程创建新线程时,系统(这里理解为本进程)查看当前映射到进程地址空间中的所有 DLL 文件映像,并用值
DLL_THREAD_ATTACH调用DLL的DllMain函数,新创建的线程负责执行这次的DLL的 DllMain 函数,只有当所有 DLL 都处理完这一通知后,系统才允许线程开始执行它自身的线程函数。自己的理解:即新线程使用值DLL_THREAD_ATTACH向本进程的所有注入DLL做了广播。 DLL_THREAD_DETACH - 如果线程调用了ExitThread来结束线程(线程函数返回时,系统也会自动条用ExitThread)系统查看当前映射到进程空间中的所有DLL文件映像,并用DLL_THREAD_DETACH来调用DllMain函数,通知所有 DLL 去执行线程级的清理工作。 注意:如果线程的结束是因为系统中的一个线程调用了TerminateThread则系统就不会用值DLL_THREAD_DETACH来调用所有DLL的DllMain函数DLL_PROCESS_DETACH - 进程将
DLL解除地址空间映射时调用(进程卸载动态链接库)lpReserved:保留参数,微软还没有给出具体的使用
制作案例
dll
2024年7月1日 12:10:28 之前的一个案例:案例源码仓库
今日制作的案例如下

注意创建的头文件中声明函数时要带有导出关键字,中间被注释的一行是没有导出函数的,最后一行的代码有导出功能

#pragma once
//int add(int a,int b); 没有导出的函数
extern "C" _declspec(dllexport) int add(int a, int b);
在 源文件 目录下新增源文件 add.cpp ,或者也可以在系统自动创建的默认入口文件 dllmain.cpp 中制作实现函数

#include "pch.h"
#include "inc.h"
int add(int a, int b) {
return a + b;
}
最后编译项目通过 depends 工具查看导出的函数
calldll
在原解决方案上通过右键菜单新建调用 dll 的项目

exe调用dll
dll 中导出函数有两种方式,类似的 exe 中调用 dll 函数也有两种方式:隐式链接,显式链接
隐式链接
概述
隐式链接的特点是由编译器完成对 DLL 的加载和程序结束时对 DLL 的卸载工作,程序结束时如果还有其他应用程序使用该 DLL 那么系统会使 DLL 的使用计数减少1,当 DLL 的使用计数降为0时会将 DLL 从内存中删除
优缺点
隐式链接 DLL 的方法简单实用,但是缺少灵活性
使用方法
将 dll 的引入库文件 *.lib( DLL 项目编译时随同目标 DLL 文件一起生成的后缀是 lib 的文件)与应用程序进行静态链接,因为引入库文件包含 DLL 的各种输出资源,如导出函数,导出类等信息,这些信息指向 dll 的函数指针等等, exe 执行时, dll 被 “自动” 加载, exe 退出时 dll 被 “自动” 卸载。
演示案例

在 vs2017 中新建的控制台应用程序没有默认生成头文件,要手动创建一个头文件 app.h
#pragma once
// 本项目与兄弟项目 dlldemo(制作生成dll的项目)编译的结果在同一个目录下(解决方案下的 debug 目录下)
// - 测试项目中静态链接目标 dll
// #pragma comment(lib, "D:\\source\\cpp\\dlldemo\\Debug\\dlldemo.lib") 测试成功
// #pragma comment(lib, "./dlldemo.lib") 测试失败
#pragma comment(lib, "../debug/dlldemo.lib") 测试成功,虽然编译后的exe文件和依赖的lib文件在同一个目录下,使用相对路径的当前路径引用也会失败
extern "C" {
_declspec(dllimport) int add(int a, int b);
}
源码文件中只要引入上面的头文件 app.h 就可以使用第三方 dll 的函数了
#include <iostream>
#include "app.h"
int main()
{
//std::cout << "Hello World!\n";
int r = add(2, 3);
printf("add函数返回的结果是:%d\r\n", r);
system("pause");
}
隐式调用方式见项目 https://gitee.com/chanchaw/dlldemodd.git 中的子项目 calldll
显式链接
概述
显示链接方式是完全由编程者使用 API 加载和卸载 dll ,编程者可以决定何时加载 dll ,加载哪个 dll ,何时卸载 dll 下载哪个 dll 等。
优缺点
显式链接方式充分体现了 dll 的灵活性,是比较常用的调用 dll 的方式,但是与静态链接相比稍微复杂了些
使用方法
LoadLibrary(...)用于加载指定的dllGetProcAddress(...)用于获取dll中导出函数的指针,即导出函数的入口点FreeLibrary(...)用于卸载指定的dll- 注意如果多次调用
LoadLibrary加载同一个dll则需要多次调用FreeLibrary卸载
dll文件的默认加载路径
使用 LoadLibrary 显式链接 dll 时如果没有指定文件的路径 windows 将遵循下面的顺序搜索 dll 文件
exe文件所在的目录- 进程的当前工作目录
windows系统目录,例如:c:\windows\system32- 系统环境变量
进程的当前工作目录可以使用函数 SetCurrentDirectory 设置,或者从父进程继承而来,使用 GetCurrentDirectory 可以得到 exe 文件所在目录可以使用 GetModuleFileName 得到,两个有可能不同
案例
下面是源码项目 https://gitee.com/chanchaw/dlldemodd.git 中子项目 explicitcall 入口函数文件的所有源码,注意要包含头文件代码的下面声明函数签名
// explicitcall.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <Windows.h>// 显式加载 dll 时用到的库函数:LoadLibrary,GetProcAddress,FreeLibrary
#include <tchar.h>// 加载 lib 文件时的路径是宽字符
typedef int(*DEC_FUNC)(int a,int b);// 声明dll中的导出函数
int main()
{
std::cout << "Hello World!\n\r";
HMODULE hModule = LoadLibrary(_T("./dlldemo.dll"));
if (hModule == NULL) {
printf("加载 dlldemo.lib 失败,程序退出!\r\n");
system("pause");
return 0;
}
//DEC_FUNC dec_fp = (DEC_FUNC)GetProcAddress(hModule, "add");// 没有获取到函数则返回 NULL
// 也可以通过导出函数的序号(depends可以查看序号),注意使用后缀有A的窄字节函数
// MAKEINTRESOURCE 返回的是宽字节
DEC_FUNC dec_fp = (DEC_FUNC)GetProcAddress(hModule, MAKEINTRESOURCEA(1));
if (dec_fp == NULL) {
printf("没有找到名称是 add 的函数!\r\n");
return 0;
}
int r = dec_fp(3, 5);
FreeLibrary(hModule);
printf("3+5 = %d\r\n", r);
system("pause");
}
def 文件导出
如果创建项目时没有自动创建 def 文件则手动创建与项目同名的该文件,要求在资源目录下

其完整代码如下
; MFCDll.def: 声明 DLL 的模块参数。
LIBRARY
EXPORTS
; 此处可以是显式导出
ShowDlg @ 1
最后一行 ShowDlg @ 1 是导出函数的意思,该函数在文件 MFCDll.cpp 中
