官方文档: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库

这一步也就是在工程的属性中去配置库的位置和头文件的位置,主要为以下三个步骤:

  1. 添加工程的头文件目录:工程---属性---配置属性---c/c++---常规---附加包含目录:加上头文件存放目录。
  2. 添加文件引用的lib静态库路径:工程---属性---配置属性---链接器---常规---附加库目录:加上lib文件存放目录。
  3. 然后添加工程引用的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文件中定义

  1. 定义之前需要先声明你所导入的动态库是谁:
const TCHAR MINIMATH[] = _T("Minimath.dll");
HMODULE m_hModule;
m_hModule = ::LoadLibrary(MINIMATH);
  1. 定义
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;
}
  1. 使用
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;  
}

参数说明:

  1. hModule:指向DLL本身的实例句柄

  2. ul_reason_for_call:指明了DLL被调用的原因,可以有一下四个取值,操作上分为加载和卸载,启动方式分为线程和进程两种:

    1. 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函数。

    1. 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函数。因此,必须确保清理那些没有成功初始化的东西。

    1. 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函数,哪怕是线程中建立线程也一样。

    1. DLL_THREAD_DETACH

    如果线程调用了ExitThread来结束线程(线程函数返回时,系统也会自动调用ExitThread),系统查看当前映射到进程空间中的所有DLL文件映像,并用DLL_THREAD_DETACH来调用DllMain函数,通知所有的DLL去执行线程级的清理工作。

    注意:如果线程的结束是因为系统中的一个线程调用了TerminateThread,系统就不会用值DLL_THREAD_DETACH来调用所有DLL的DllMain函数。

以上就是GetProcAddress的相关内容了

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐