DLL开发事项
本文章面向对dll的使用和基本的开发有初步了解的开发者,读者至少要了解如何使用dll,并且懂得开发简单的dll。
概念介绍
导出:
dll通常一定要提供导出的函数接口,所谓导出的接口就是能被库的使用者看到的接口内容,在c、c++中通过在函数前或者类名前加__declspec(dllexport)就可以导出函数或者类。
dll也是PE(可执行文件格式)文件,拥有导出表结构。导出表中有导出函数的符号名(和函数名关联,但不一定等于函数名)和整数标识号以及函数地址,用一些exe查看器就能看到导出表。显式链接(又名动态加载):
程序通过调用系统api直接加载指定名字的dll,然后从dll中根据函数名字查找到函数的地址。- 优点:
- 可以在程序运行的过程中加载,使用完释放,节约内存。
- dll和函数的名字可以在运行时决定。
- 可以用于非c、c++语言。
- 缺点:
- 只能加载以c风格导出的函数,不能加载c++的导出类。
- 优点:
隐式链接(又名静态加载):
程序编译dll时会一起生成lib文件,该文件包含了每一个DLL导出函数的符号名和可选的标识号以及地址。程序通过使用#program comment或者在链接选项里设置来链接lib,应用程序中的调用函数与LIB文件中导出符号相匹配,这些符号或标识号进入到生成的EXE文件中。LIB文件中也包含了对应的DLL文件名(但不是完全的路径名),链接程序将其存储在EXE文件内部。当应用程序运行过程中需要加载DLL文件时,Windows根据这些信息发现并加载DLL,然后通过符号名或标识号实现对DLL函数的动态链接。我们使用的大部分系统Dll就是通过这样的方式链接的。若找不到需要的Dll则会给出一个Dll缺少的错误消息。- 优点:
- 省略加载过程的代码
- 在程序加载时就确定dll是否存在。防止异常
- 可以加载c++导出类
- 缺点:
- 不灵活,无法控制加载的时机和过程
- 优点:
- 调用约定:
函数调用约定,是指当一个函数被调用时,函数的参数会被传递给被调用的函数和返回值会被返回给调用函数。函数的调用约定就是描述参数是怎么传递和由谁平衡堆栈的,还有返回值如何返回。大多数时候是不需要了解的,但是如果需要跨语言的编程,比如VC写的dll要C#调用,或者进行汇编开发调试,则需要了解。
- __cdecl
C调用规则。也是vc的默认规则,按从右至左的顺序压参数入栈,由函数的调用者负责维护堆栈平衡。返回值由EAX返回。变长参数的函数必须使用__cdecl。 - __stdcall
pascal语言的规则,所以又名__pascal。也是WINDOWS的API的调用规则。按从右至左的顺序压参数入栈,由函数自己负责维护堆栈平衡。返回值由EAX返回。
vc中的WINAPI宏就是__stdcall,C#调用dll默认也是此约定。 - __fastcall
一种非标准的调用方式,尽量使用寄存器来传递参数,第一个参数进ECX,第2个进EDX,其他参数是从右向左的入栈。返回仍然通过EAX。但是这并不是一个标准,不同的编译器有可能有不同的实现。所以跨编译器的时候不能用。 - __thiscall
这是一种不能被指定的约定,它其实是人们口口相传的一种说法,目前没有什么标准规定了这种约定。它只用于c++的成员函数,因为c++类的成员函数都有一个隐含的this指针参数而得名。当然,没有标准同样也就意味着他不能跨编译器使用。在vc中将this指针通过ecx传递,其他参数从右向左。所以在调试窗口中看到的this就是ECX的值,而release中为性能考虑,ECX可能会被挪用,所以有时会看到this的值在变,不要惊讶。
- __cdecl
注意事项
以下是一些dll编写过程中容易出问题的地方。
尽量不要跨dll操作分配释放内存
尽量不要在dll的内部释放dll外部分配的内存。因为你不知道dll和外部的调用者是不是使用同一个堆,有可能外面使用new,dll里面却在用free。同理,也不要在dll的外部释放dll内部分配的内存。当如果你能控制dll的使用者的行为,还是可以这么做的。不同通常来说这是不可能的。不要跨dll传递stl参数
在dll接口的参数中传递stl、或者说类是一种危险的行为,这通常是通往彻夜debug的班车。- stl的方法的内部实现中有大量内存分配和释放(例如向vector里插入元素、给string赋值等),所以你一旦在dll的内部或者外部使用了这些方法,那么你就回到了上一条问题。stl有很多实现,甚至编译器的不同版本都不一样,也就是说你更加不可能控制使用者的行为了。
- stl都是内嵌代码进行编译的,也就是说每一个可执行映像(通常是.dll或.exe文件)就会存在 一份只属于自己的、给定类的静态数据成员。当一个需要访问这些静态成员的类方法执行时,它使用的是“这个方法的代码当前所在的那份可执行映像”里的静态成 员变量。由于两份可执行映像各自的静态数据成员并未同步,这个行为就可能导致访问违例,或者数据看起来似乎丢失或被破坏了。
- dll和调用者有可能使用不同版本的stl库,不同版本的stl对象的内存布局有可能是不同的,但是他们都会按照自己的版本来解释stl对象,这就可能导致错误。除非你能保证dll和调用者使用同一版本的stl库才能避免此问题。
提供专门的创建释放导出类的接口
因为dll内部和外部的编译环境以一定一致,也就导致两边的内存对齐可能不一致,这样两边对一个类的内存大小可能有不同的定义,所以通常应该定义创建和释放类的接口,这样就可以避免这种情况出现。dll的debug和release版区别命名
因为如果debug和release版名字一致那么使用者有很大的几率弄混debug版和release版的lib和dll。一旦lib和dll不匹配那程序就完全无法知道会运行成什么样子,因为lib中通常会存储函数的入口地址,而debug和release版的函数入口地址通常是不一样的。也就是你可能直接进入一个函数的中间去执行。不要跨编译器使用含导出类的dll
正如上面对__thiscall的描述,c++标准没有规定该如何传递this指针 。所以不同的编译器可能有不同的实现。所以当编译dll的编译器和使用dll的编译器不一致时就会出问题。
理论上这些事项都可以不遵守,但是因为dll的使用者是无法控制的,所以最好注意以上的事项,不给使用者犯错误的机会。以上事项是在动态库编写过程中的问题,但是他们大部分在编写静态库的时候也是适用的。