【DLL】【一文搞懂】(四)GetProcAddress函数
官方文档:
GetProcAddress()
函数用于获取DLL中导出函数的地址。(显式链接时使用)GetProcAddress将 DLL 模块处理 (由LoadLibrary、或 GetModuleHandle 返回的参数 ) , 并采用要调用的函数的名称或函数的导出序号。
因为通过指针调用 DLL 函数,并且没有编译时类型检查,所以请确保函数的参数正确,以便不会超过在堆栈上分配的内存以及导致访问冲突。 帮助提供类型安全的一种方法是查看导出函数的函数原型,并为函数指针创建匹配的 typedef。
再来看一下官方demo:
#include "windows.h"
typedef HRESULT (CALLBACK* LPFNDLLFUNC1)(DWORD,UINT*);
HRESULT LoadAndCallSomeFunction(DWORD dwParam1, UINT * puParam2)
{
HINSTANCE hDLL; // Handle to DLL
LPFNDLLFUNC1 lpfnDllFunc1; // Function pointer
HRESULT hrReturnVal;
hDLL = LoadLibrary("MyDLL");
if (NULL != hDLL)
{
lpfnDllFunc1 = (LPFNDLLFUNC1)GetProcAddress(hDLL, "DLLFunc1");
if (NULL != lpfnDllFunc1)
{
// call the function
hrReturnVal = lpfnDllFunc1(dwParam1, puParam2);
}
else
{
// report the error
hrReturnVal = ERROR_DELAY_LOAD_FAILED;
}
FreeLibrary(hDLL);
}
else
{
hrReturnVal = ERROR_DELAY_LOAD_FAILED;
}
return hrReturnVal;
}
如何指定调用 GetProcAddress
时所需的函数取决于 DLL
的生成方式。
仅当要链接到的 DLL
使用模块定义 (.def) 文件生成,并且序号随函数在 DLL .def
文件的 EXPORTS
节中列出时,才能获取导出序号。 如果 DLL
具有许多导出函数,则与使用函数名称相比,使用导出序号调用 GetProcAddress
会稍微快一些,因为导出序号充当 DLL
导出表中的索引。 使用导出序号,GetProcAddress
可以直接查找函数,而不是将指定名称与 DLL
导出表中的函数名进行比较。 但是,仅当可控制将序号分配给 .def
文件中的导出函数时,才应使用导出序号调用GetProcAddress
。
本文从 工程的角度来演示一下如何使用
GetProcAddress
,以及上文中的
def
文件。
一、导入库和导入文件
也可以看官方的解释👉使用导入库和导出文件 | Microsoft Docs
这里边主要分三个部分:使用、生成、使用。一般在项目属性中能看到相应的命令行,如下图所示的位置:
生成lib库
新建工程时选择新建lib库工程即可,关于lib库中的__declspec
关键字说明如下:
__declspec
是Microsoft VC中专用的关键字,它配合着一些属性可以对标准C/C++进行扩充。__declspec
关键字应该出现在声明的前面。
__declspec(dllexport)
用于Windows中的动态库中,声明导出函数、类、对象等供外面调用,省略给出.def文件。即将函数、类等声明为导出函数,供其它程序调用,作为动态库的对外接口函数、类等。.def文件(模块定义文件)是包含一个或多个描述各种DLL属性的Module语句的文本文件。
.def
文件或__declspec(dllexport)
都是将公共符号导入到应用程序或从DLL导出函数。如果不提供__declspec(dllexport)
导出DLL函数,则DLL需要提供.def文件。
__declspec(dllimport)
用于Windows中,从别的动态库中声明导入函数、类、对象等供本动态库或exe文件使用。当你需要使用DLL中的函数时,往往不需要显示地导入函数,编译器可自动完成。不使用__declspec(dllimport)
也能正确编译代码,但使用__declspec(dllimport)
使编译器可以生成更好的代码。编译器之所以能够生成更好的代码,是因为它可以确定函数是否存在于DLL中,这使得编译器可以生成跳过间接寻址级别的代码,而这些代码通常会出现在跨DLL边界的函数调用中。声明一个导入函数,是说这个函数是从别的DLL导入。一般用于使用某个DLL的exe中。
导入lib库
这一步也就是在工程的属性中去配置库的位置和头文件的位置,主要为以下三个步骤:
- 添加工程的头文件目录:
工程---属性---配置属性---c/c++---常规---附加包含目录
:加上头文件存放目录。- 添加文件引用的lib静态库路径:
工程---属性---配置属性---链接器---常规---附加库目录
:加上lib文件存放目录。- 然后添加工程引用的lib文件名:
工程---属性---配置属性---链接器---输入---附加依赖项
:加上lib文件名。
自己在本地尝试了一下,没能成功引入lib库,但是在项目工程中可以,后续我再补充详细的步骤。
二、使用def文件
这里我就项目经验来说明一下使用def文件获取动态库函数的一些操作。
生成动态库
首先还是得有一个可用的动态库*.dll
。按照上文中引入动态库的方法添加到你的vs工程中,这里需要说明的是,你生成的动态库中应该有一个def
文件用以说明你的动态库的导出配置。
def文件的说明如下所示:
; Minimath.def : Declares the module parameters for the DLL.
LIBRARY "Minimath"
EXPORTS
; Explicit exports can go here
add
power
如上文件,进一步说明:
< LINE3 > 指明动态库的文件名
< LINE5 > 需要导出的函数接口
< LINE7/8 > 导出的函数名,这个是你在调用GetProcAddress
时需要使用的
这里补充一下add()
、power()
函数的原型,后边会用到:
.h
#ifdef __cplusplus
extern "C" {
#endif
int WINAPI add(int a, int b);
void WINAPI power(int &a);
#ifdef __cplusplus
}
#endif
.cpp
int WINAPI add(int a, int b)
{
return a + b;
}
void WINAPI power(int &a)
{
a = a * a'
}
使用
如上我们导出了一个Minimath.dll
的动态库,在你保证需要该动态库的工程中引入该动态库之后,
.h
在头文件声明接受的指针函数:
int (WINAPI *pAdd)(int a, int b);
void (WINAPI *pPower)(int &a);
.cpp
在cpp文件中定义
- 定义之前需要先声明你所导入的动态库是谁:
const TCHAR MINIMATH[] = _T("Minimath.dll");
HMODULE m_hModule;
m_hModule = ::LoadLibrary(MINIMATH);
- 定义
pAdd = (int (WINAPI *)(int a, int b))GetProcAddress(m_hModule, ("add"));
if ( NULL == pFunDestroySocketClient)
{
return;
}
pPower = (void (WINAPI *)(int &a))GetProcAddress(m_hModule, ("power"));
if( NULL == pGetLocalIPForUSM )
{
return;
}
- 使用
int a = 1, b = 2;
int sum = pAdd(a,b); // sum = 3
pPower(sum) // sum = 9
三、入口函数
和一般的程序一样,dll也有一个自己的入口函数:DllMain
(大小写是有区别的)。对于动态链接库,DllMain
是一个可选的入口函数。
函数原型:
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
return TRUE;
}
参数说明:
-
hModule
:指向DLL本身的实例句柄 -
ul_reason_for_call
:指明了DLL被调用的原因,可以有一下四个取值,操作上分为加载和卸载,启动方式分为线程和进程两种:DLL_PROCESS_ATTACH
一个程序要调用Dll里的函数,首先要先把Dll文件映射到进程的地址空间。要把一个DLL文件映射到进程的地址空间,有两种方法,静态链接和动态链接的
LoadLibrary
或者LoadLibraryEx
。当一个
DLL
文件被映射到进程的地址空间时,系统调用该DLL的DllMain函数,传递的fdwReason
参数为DLL_PROCESS_ATTACH
,这种调用只会发生在第一次映射时。如果同一个进程后来为已经映射进来的DLL再次调用LoadLibrary或者LoadLibraryEx,操作系统只会增加DLL的使用次数,它不会再用DLL_PROCESS_ATTACH调用DLL的DllMain函数。不同进程用LoadLibrary同一个DLL时,每个进程的第一次映射都会用DLL_PROCESS_ATTACH调用DLL的DllMain函数。DLL_PROCESS_DETACH
当DLL被从进程的地址空间解除映射时,系统调用了它的DllMain,传递的fdwReason值是
DLL_PROCESS_DETACH
。当DLL处理该值时,它应该执行进程相关的清理工作。那么什么时候DLL被从进程的地址空间解除映射呢?两种情况:
◆ FreeLibrary解除DLL映射(有几个LoadLibrary,就要有几个FreeLibrary)
◆ 进程结束而解除DLL映射,在进程结束前还没有解除DLL的映射,进程结束后会解除DLL映射。(如果进程的终结是因为调用了TerminateProcess,系统就不会用
DLL_PROCESS_DETACH
来调用DLL的DllMain函数。这就意味着DLL在进程结束前没有机会执行任何清理工作。)注意:当用
DLL_PROCESS_ATTACH
调用DLL的DllMain函数时,如果返回FALSE,说明没有初始化成功,系统仍会用DLL_PROCESS_DETACH
调用DLL的DllMain函数。因此,必须确保清理那些没有成功初始化的东西。DLL_THREAD_ATTACH
当进程创建一线程时,系统查看当前映射到进程地址空间中的所有DLL文件映像,并用值
DLL_THREAD_ATTACH
调用DLL的DllMain函数。新创建的线程负责执行这次的DLL的DllMain函数,只有当所有的DLL都处理完这一通知后,系统才允许进程开始执行它的线程函数。
注意跟
DLL_PROCESS_ATTACH
的区别,我们在前面说过,第n(n>=2)次以后地把DLL映像文件映射到进程的地址空间时,是不再用DLL_PROCESS_ATTACH
调用DllMain的。而DLL_THREAD_ATTACH
不同,进程中的每次建立线程,都会用值DLL_THREAD_ATTACH
调用DllMain函数,哪怕是线程中建立线程也一样。DLL_THREAD_DETACH
如果线程调用了
ExitThread
来结束线程(线程函数返回时,系统也会自动调用ExitThread),系统查看当前映射到进程空间中的所有DLL文件映像,并用DLL_THREAD_DETACH
来调用DllMain函数,通知所有的DLL去执行线程级的清理工作。注意:如果线程的结束是因为系统中的一个线程调用了TerminateThread,系统就不会用值
DLL_THREAD_DETACH
来调用所有DLL的DllMain函数。
以上就是GetProcAddress
的相关内容了
更多推荐
所有评论(0)