DLL开发事项

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的值在变,不要惊讶。

注意事项

以下是一些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的使用者是无法控制的,所以最好注意以上的事项,不给使用者犯错误的机会。以上事项是在动态库编写过程中的问题,但是他们大部分在编写静态库的时候也是适用的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值