为什么不能获取动态库接口函数的地址

为什么不能获取动态库接口函数的地址,第1张

学会使用

dumpbinexe /exports xxxdll

命令查看xxxdll导出的所有函数

dll 导出函数名的那些事

关键字: VC++DLL 导出函数 

经常使用VC6的Dependency查看DLL导出函数的名字,会发现有DLL导出函数的名字有时大不相同,导致不同的原因大多是和编译DLL时候指定DLL导出函数的界定符有关系。

VC++支持两种语言:即C/C++,这也是造成DLL导出函数差异的根源

我们用VS2008新建个DLL工程,工程名为"TestDLL"

把默认的源文件后缀 CPP改为C(C文件)

输入测试代码如下:

01 int _stdcall MyFunction(int iVariant)

02 {

03 return 0;

04 }

为了导出上面这个函数,我们有以下几个方法:

1 使用传统的模块定义文件 (def)

新建一个 后缀为def的文本文件(这里建一个TestDllDef),文件内容为:

LIBRARY TestDll

EXPORTS

MyFunction

在 Link 时指定输入依赖文件:/DEF:"TestDllDef"

2 Visual C++ 提供的方便方法

在01行的int 前加入 __declspec(dllexport) 关键字

通过以上两种方法,我们就可以导出MyFunction函数。

我们用Dependency查看导出的函数:

第一种方法导出的函数为:

MyFunction

第二种方法导出的函数为:

_MyFunction@4

__stdcall会使导出函数名字前面加一个下划线,后面加一个@再加上参数的字节数,比如_MyFunction@4的参数(int iVariant)就是4个字节

__fastcall与 __stdcall类似,不过前面没有下划线,而是一个@,比如@MyFunction@4

__cdecl则是始函数名。

小结:如果要导出C文件中的函数,并且不让编译器改动函数名,用def文件导出函数。

下面我们来看一下C++文件

我们用VS2008新建个DLL工程,工程名为"TestDLL"

默认的源文件后缀为 CPP (即C++文件)。

输入测试代码如下:

01 int _stdcall MyFunction(int iVariant)

02 {

03 return 0;

04 }

为了导出上面这个函数,我们有以下几个方法:

3 使用传统的模块定义文件 (def)

新建一个 后缀为def的文本文件(这里建一个TestDllDef),文件内容为:

LIBRARY TestDll

EXPORTS

MyFunction

在 Link 时指定输入依赖文件:/DEF:"TestDllDef"

4 Visual C++ 提供的方便方法

在01行的int 前加入 __declspec(dllexport) 关键字

通过以上两种方法,我们就可以导出MyFunction函数。

我们用Dependency查看导出的函数:

第一种方法导出的函数为:

MyFunction

第二种方法导出的函数为:

MyFunction@@YGHH@Z

可以看到 第二种方法得到的 导出函数名 并不是我们想要的,如果在exe中用显示方法(LoadLibrary、GetProcAddress)调用 MyFunction 肯定会失败。

但是用引入库(LIB)的方式调用,则编译器自动处理转换函数名,所以总是没有问题。

解决这个问题的方法是:

用VC 提供的预处理指示符 "#pragma" 来指定链接选项。

如下:

#pragma comment(linker, "/EXPORT:MyFunction=MyFunction@@YGHH@Z")

这时,就会发现导出的函数名字表中已经有了我们想要的MyFunction。但我们发现原来的那个 MyFunction@@YGHH@Z 函数还在,这时就可以把 __declspec() 修饰去掉,只需要 pragma 指令即可。

而且还可以使如下形式:

#pragma comment(linker, "/EXPORT:MyFunction=_MyFunction@4,PRIVATE")

PRIVATE 的作用与其在 def 文件中的作用一样。更多的#pragram请查看MSDN。

小结:如果要导出C++文件中的函数,并且不让编译器改动函数名,用def文件导出函数。

同时可以用#pragma指令(C 中也可以用)。

总结:

C++编译器在生成DLL时,会对导出的函数进行名字改编,并且不同的编译器使用的改编规则不一样,因此改编后的名字也是不同的(一般涉及到C++ 中的重载等)。

如果利用不同编译器分别生成DLL和访问DLL的exe程序,后者在访问该DLL的导出函数时就会出现问题。如上例中函数MyFunction在C++编译器改编后的名字是MyFunction@@YGHH@Z。我们希望编译后的名字不发生改变,这里有几种方法。

第一种方法是通过一个称为模块定义文件DEF来解决。

LIBRARY TestDll

EXPORTS

MyFunction

LIBRARY 用来指定动态链接库内部名称。该名称与生成的动态链接库名一定要匹配,这句代码不是必须的。

EXPORTS说明了DLL将要导出的函数,以及为这些导出函数指定的符号名。

第二种是定义导出函数时加上限定符:extern "C"

如:#define DLLEXPORT_API extern "C" _declspec(dllexport)

但extern "C"只解决了C和C++语方之间调用的问题(extern "C" 是告诉编译器,让它按C的方式编译),它只能用于导出全局函数这种情况 而不能导出一个类的成员函数。

同时如果导出函数的调用约定发生改变,即使使用extern "C",编译后的函数名还是会发生改变。例如上面我们加入_stdcall关键字说明调用约定(标准调用约定,也就是WINAPI调用约定)。

#define DLLEXPORT_API extern "C" _declspec(dllexport)

01 DLLEXPORT_API int _stdcall MyFunction(int iVariant)

02 {

03 return 0;

04 }

编译后函数名MyFunction改编成了_MyFunction@4

通过第一种方法模块定义文件的方式DLL编译后导出函数名不会发生改变。

DLL(动态库)导出函数名乱码含义

C++编译时函数名修饰约定规则:

__stdcall调用约定:

1、以""标识函数名的开始,后跟函数名;

2、函数名后面以"@@YG"标识参数表的开始,后跟参数表;

3、参数表以代号表示:

X--void

D--char

E--unsigned char

F--short

H--int

I--unsigned int

J--long

K--unsigned long

M--float

N--double

_N--bool

PA--表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以"0"代替,一个"0"代表一次重复;

4、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前;

5、参数表后以"@Z"标识整个名字的结束,如果该函数无参数,则以"Z"标识结束。

其格式为"functionname@@YG@Z"或"functionname@@YGXZ",例如

int Test1(char var1, unsigned long)-----"Test1@@YGHPADK@Z" void Test2()-----"Test2@@YGXXZ"

__cdecl调用约定:

规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的"@@YG"变为"@@YA"。

__fastcall调用约定:

规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的"@@YG"变为"@@YI"。

如果要用DEF文件输出一个"C++"类,则把要输出的数据和成员的修饰名都写入def模块定义文件

所以 通过def文件来导出C++类是很麻烦的,并且这个修饰名是不可避免的

1、将DLL文件复制到编译路径目录(系统目录或项目目录等等);

2、导出DLL的功能接口;如:[DllImport("WDT_DIO64dll")]

internal static extern int InitDIO();

这个DLL在C中的头文件是这样表达的:BOOL InitDIO(void);

BOOL 在C中是int类型的,对应的C#也是int 类型,调用DLL通常都是静态的外部引进的(static extern),internal 是可见属性定义(自己根据需要定义);

3、参考DLL对应的头文件的信息。如果C/C++封装的dll是打算通用的,通常发布头文件,头文件在C中是编译器所需的文件,DLL的关键信息都在头文件展示出来。我们可以通过阅读头文件的包含信息(如结构体、宏定义,函数原型、功能注释等),使用C#方式来导出DLL的函数接口来调用DLL(如上2、)。

4、在需要的地方加上参数调用导出的函数接口。

介绍 自从Windows的开始阶段动态链接库(DLL)就是Windows平台的一个组成部分。动态链接库允许在一个独立的模块中封装一系列的功能函数然后以一个显式的C函数列表提供外部使用者使用。

使用electron开进行桌面程序的开发,似乎成了WEB前端开发人员转桌面程序开发的首选。近期有一些使用在electron中使用加密锁的需求,学习了一下在Nodejs中通过ffi-napi模块调用动态链接库,把几款加密锁产品的动态库使用javascript封装了一下,实现了electron中使用加密锁功能。

开发过程中遇到了一些问题,踩了一些坑,这里总结记录一下。这里使用接口函数参数类型比较复杂的ROCKEY-ARM的动态链接库来进行开发。

NOTE: javascript封装的ROCKEY-ARM接口模块源码,我已经分享出来,如果只是需要electron或者Nodejs工程中使用ROCKEY-ARM的网友,可以直接使用。

首先需要在nodejs项目中安装调用动态链接库时需要依赖的模块 ffi-napi,ref-napi,ref-array-napi,ref-struct-napi

下面大概介绍一下这几个模块的用途:

向 飞天诚信 购买ROCKEY-ARM加密锁产品,可以获得ROCKEY-ARM的SDK,可以获得Windows和Linux的动态链接库,文件名一般为Dongle_d和libRockeyARMso03。

ffi-napi支持Windows,Linux系统,所以dll和so都可以支持,在不同的 *** 作系统下去加载不同的动态库文件就可以了。加载动态库的方法如下:

Library()第一个参数是dll的路径,Linux系统是so的路径。第二个参数rockeyInterface是动态库导出函数的声明,ROCKEY-ARM的导出函数比较多,我单独拿出来定义。具体下面会讲到。

首先从ROCKEY-ARM中找几个参数简单的函数来声明一下。

首先看一下上面几个接口用到的数据类型有:DONGLE_HANDLE,DWORD,DONGLE_HANDLE ,int,BYTE 这几种。

再看下ffi-napi支持的ref-napi支持的数据类型有以下类型:

参数这里应该用长度一致的数据类型,可以有以下匹配。

声明的写法如下:

一个json,key是动态库导出函数名,比如'Dongle_Open',value是个列表,第一个元素是返回值,第二个元素是参数。其中参数还是个列表。这个ref-napi中有适合类型的,直接写称具体类型即可,比如返回值DWORD和传入的长度int,我这里都用'int'。其他的参数我额外定义了句柄ryHandle、句柄的指针ptrHandle、字节的指针ptrByte。其中ryHandle,ptrryHandle,ptrByte的定义如下:

DONGLE_HANDLE本质是void 类型, void 类型最开始的时候妄图定义一个void的数组,然后用void数组来表示void ,然后发现报断言错误,数组不支持void类型。所以就直接用无符号数来表示void指针,在64位系统是8字节,32位系统是4字节,使用uint类型就可以了。DONGLE_HANDLE 。

在ROCKEY-ARM的函数中也有很多带参数的接口,比如:

拿以上两个函数接口举例,Dongle_Enum中的第一个参数是一个指向DONGLE_INFO结构体的指针,运行后返回设备信息的列表,使用ROCKEY-ARM的时候需要通过枚举函数获得设备信息列表,然后比较产品ID或者硬件ID决定打开哪一个设备。为了方便从枚举函数返回的设备信息中方便的解析出产品ID或者硬件ID等信息,需要把DONGLE_INFO pDongleInfo这个参数声明成一个结构体数组。Dongle_RsaGenPubPriKey()函数中有RSA_PUBLIC_KEY ,RSA_PRIVATE_KEIY 两个结构体指针参数,因为在这里一般用户并不需要解析RSA密钥中的n,d,e等分量,可以直接做作为一个字节数组,直接声明成上面的ptrByte类型即可。所以在声明如下:

调用ffi-napi声明的函数,主要是给自己定义的数据类型赋初值以及获得自定义参数的返回值。下面分别说明。

这里的int,是让函数返回设备的数量,或者传入输入数据的长度或者传出输出数据的长度,所以只要定义一个长度为1的int数组即可,如下:

给传入的数据赋值,只要给下标为0的元素赋值即可。

这个参数是枚举函数传出枚举到设备信息的列表,枚举到多少设备,就传出多少个DONGLE_INFO,所以需要传入足够数量的的DONGLE_INFO,如下:

这个参数一般是作为传入传出数据的缓冲区的,所以创建数组的时候,需要创建足够长的空间,如下:

开发的过程中,踩到一些坑耽误了不少时间,这里总结一下。

ROCKEY-ARM的结构体是按字节对齐的,ref-struct-napi没有找到设置字节对齐的方法。当时声明的结构体如下:

测试的时候会发现定义的结构体和ROCKEY-ARM定义的结构体对齐方式不一样,于是把m_Birthday和m_HID两个成员从reftypesuint64,拆分成左右两个uint32,这样就可以让结构体对齐方式和ROCKEY-ARM的一致。使用m_Birthday和m_HID的时候,需要讲左右两个uint32拼接一些,稍微麻烦一点,但是在没找到配置StructType对齐方的情况,保证结果正确,还是可以接受的。

将一个动态库添加到资源文件

(1)DLL导出函数,可供应用程序调用;

(2) DLL内部函数,只能在DLL程序使用,应用程序无法调用它们。

DLL定义和调用的一般概念:

(1)DLL中需以某种特定的方式声明导出函数(或变量、类);

(2)应用工程需以某种特定的方式调用DLL的导出函数(或变量、类)。

DLL中导出函数的声明有两种方式:一种为在函数声明中加上__declspec(dllexport),这里不再举例说明;另外一种方式是采用模块定义(def) 文件声明,def文件为链接器提供了有关被链接程序的导出、属性及其他方面的信息。

以上就是关于为什么不能获取动态库接口函数的地址全部的内容,包括:为什么不能获取动态库接口函数的地址、C语言的动态库在C#中如何声明及调用、QT里怎么导出C接口的动态链接库等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/9513972.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-04-29
下一篇 2023-04-29

发表评论

登录后才能评论

评论列表(0条)

保存