热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

编写DLL所学所思(1)

动态链接库的使用有两种方式,一种是显式调用。一种是隐式调用。(1)显式调用:使用LoadLibrary载入动态链接库、使用
动态链接库的使用有两种方式,一种是显式调用。一种是隐式调用。

(1)       显式调用:使用LoadLibrary载入动态链接库、使用GetProcAddress获取某函数地址。

(2)       隐式调用:可以使用#pragma comment(lib, “XX.lib”)的方式,也可以直接将XX.lib加入到工程中。

 

DLL的编写

编写dll时,有个重要的问题需要解决,那就是函数重命名——Name-Mangling。解决方式有两种,一种是直接在代码里解决采用extent”c”、_declspec(dllexport)、#pragma comment(linker"/export:[Exports Name]=[Mangling Name]"),另一种是采用def文件。

(1)编写dll时,为什么有 extern “C”

原因:因为C和C++的重命名规则是不一样的。这种重命名称为“Name-Mangling”(名字修饰或名字改编、标识符重命名,有些人翻译为“名字粉碎法”,这翻译显得有些莫名其妙)

据说,C++标准并没有规定Name-Mangling的方案,所以不同编译器使用的是不同的,例如:Borland C++跟Mircrosoft C++就不同,而且可能不同版本的编译器他们的Name-Mangling规则也是不同的。这样的话,不同编译器编译出来的目标文件.obj 是不通用的,因为同一个函数,使用不同的Name-Mangling在obj文件中就会有不同的名字。如果DLL里的函数重命名规则跟DLL的使用者采用的重命名规则不一致,那就会找不到这个函数。

C标准规定了C语言Name-Mangling的规范(林锐的书有这样说过)。这样就使得,任何一个支持C语言的编译器,它编译出来的obj文件可以共享,链接成可执行文件。这是一种标准,如果DLL跟其使用者都采用这种约定,那么就可以解决函数重命名规则不一致导致的错误。

影响符号名的除了C++和C的区别、编译器的区别之外,还要考虑调用约定导致的Name Mangling。如extern “c” __stdcall的调用方式就会在原来函数名上加上写表示参数的符号,而extern “c” __cdecl则不会附加额外的符号。

dll中的函数在被调用时是以函数名或函数编号的方式被索引的。这就意味着采用某编译器的C++的Name-Mangling方式产生的dll文件可能不通用。因为它们的函数名重命名方式不同。为了使得dll可以通用些,很多时候都要使用C的Name-Mangling方式,即是对每一个导出函数声明为extern “C”,而且采用_stdcall调用约定,接着还需要对导出函数进行重命名,以便导出不加修饰的函数名。

注意到extern “C”的作用是为了解决函数符号名的问题,这对于动态链接库的制造者和动态链接库的使用者都需要遵守的规则。

动态链接库的显式装入就是通过GetProcAddress函数,依据动态链接库句柄和函数名,获取函数地址。因为GetProcAddress仅是操作系统相关,可能会操作各种各样的编译器产生的dll,它的参数里的函数名是原原本本的函数名,没有任何修饰,所以一般情况下需要确保dll’里的函数名是原始的函数名。分两步:一,如果导出函数使用了extern”C” _cdecl,那么就不需要再重命名了,这个时候dll里的名字就是原始名字;如果使用了extern”C” _stdcall,这时候dll中的函数名被修饰了,就需要重命名。二、重命名的方式有两种,要么使用*.def文件,在文件外修正,要么使用#pragma,在代码里给函数别名。

(2)_declspec(dllexport)和_declspec(dllimport)的作用

       _declspec还有另外的用途,这里只讨论跟dll相关的使用。正如括号里的关键字一样,导出和导入。_declspec(dllexport)用在dll上,用于说明这是导出的函数。而_declspec(dllimport)用在调用dll的程序中,用于说明这是从dll中导入的函数。

       因为dll中必须说明函数要用于导出,所以_declspec(dllexport)很有必要。但是可以换一种方式,可以使用def文件来说明哪些函数用于导出,同时def文件里边还有函数的编号。

而使用_declspec(dllimport)却不是必须的,但是建议这么做。因为如果不用_declspec(dllimport)来说明该函数是从dll导入的,那么编译器就不知道这个函数到底在哪里,生成的exe里会有一个call XX的指令,这个XX是一个常数地址,XX地址处是一个jmp dword ptr[XXXX]的指令,跳转到该函数的函数体处,显然这样就无缘无故多了一次中间的跳转。如果使用了_declspec(dllimport)来说明,那么就直接产生call dword ptr[XXX],这样就不会有多余的跳转了。(参考《加密与解密》第三版279页)

(3)__stdcall带来的影响

       这是一种函数的调用方式。默认情况下VC使用的是__cdecl的函数调用方式,如果产生的dll只会给C/C++程序使用,那么就没必要定义为__stdcall调用方式,如果要给Win32汇编使用(或者其他的__stdcall调用方式的程序),那么就可以使用__stdcall。这个可能不是很重要,因为可以自己在调用函数的时候设置函数调用的规则。像VC就可以设置函数的调用方式,所以可以方便的使用win32汇编产生的dll。不过__stdcall这调用约定会Name-Mangling,所以我觉得用VC默认的调用约定简便些。但是,如果既要__stdcall调用约定,又要函数名不给修饰,那可以使用*.def文件,或者在代码里#pragma的方式给函数提供别名(这种方式需要知道修饰后的函数名是什么)。

 

举例:

 

·extern “C” __declspec(dllexport) bool  __stdcall cswuyg();

·extern “C”__declspec(dllimport) bool __stdcall cswuyg();

 

·#pragma comment(linker"/export:cswuyg=_cswuyg@0")

 

(4)*.def文件的用途

指定导出函数,并告知编译器不要以修饰后的函数名作为导出函数名,而以指定的函数名导出函数(比如有函数func,让编译器处理后函数名仍为func)。这样,就可以避免由于microsoft VC++编译器的独特处理方式而引起的链接错误。

也就是说,使用了def文件,那就不需要extern “C”了,也可以不需要__declspec(dllexport)了(不过,dll的制造者除了提供dll之外,还要提供头文件,需要在头文件里加上这extern”C”和调用约定,因为使用者需要跟制造者遵守同样的规则,除非使用者和制造者使用的是同样的编译器并对调用约定无特殊要求)。

举例def文件格式:

LIBRARY  XX(dll名称这个并不是必须的,但必须确保跟生成的dll名称一样)

EXPORTS

[函数名] @ [函数序号]

 

编写好之后加入到VC的项目中,就可以了。

       另外,要注意的是,如果要使用__stdcall,那么就必须在代码里使用上__stdcall,因为*.def文件只负责修改函数名称,不负责调用约定。

也就是说,def文件只管函数名,不管函数平衡堆栈的方式。

 

如果把*.def文件加入到工程之后,链接的时候并没有自动把它加进去。那么可以这样做:

手动的在link添加:

1)工程的propertiesàConfiguration PropertiesàLinkeràCommand Lineà在“Additional options”里加上:/def:[完整文件名].def

2)工程的propertiesàConfiguration PropertiesàLinkeràInputàModule Definition File里加上[完整文件名].def

 

注意到:即便是使用C的名称修饰方式,最终产生的函数名称也可能是会被修饰的。例如,在VC下,_stdcall的调用方式,就会对函数名称进行修饰,前面加‘_’,后面加上参数相关的其他东西。所以使用*.def文件对函数进行命名很有用,很重要。

(5)、DllMain函数

每一个动态链接库都会有一个DllMain函数。如果在编程的时候没有定义DllMain函数,那么编译器会给你加上去。

DllMain函数格式:

BOOL APIENTRY DllMain( HANDLE hModule,

                       DWORD  ul_reason_for_call,

                       LPVOID lpReserved

                              )

{

     switch(ul_reason_for_call)

     {

     case DLL_PROCESS_ATTACH:

            printf("\nprocess attach of dll");

            break;

     case DLL_THREAD_ATTACH:

            printf("\nthread attach of dll");

            break;

     case DLL_THREAD_DETACH:

            printf("\nthread detach of dll");

            break;

     case DLL_PROCESS_DETACH:

            printf("\nprocess detach of dll");

            break;

     }

    return TRUE;

}

(6)、很多都还没学,如:导出Class、导出变量、DLL更高级的应用。目前先了解点基础知识。以后补上。

 

 

 

 

 

 

 

 

 

 

 2011-8-14补充

 

编写dll可以使用.def文件对导出的函数名进行命名。

 

1、动态装入dll,重命名(*.def)的必要性?

因为导出的函数尽可能使用__stdcall的调用方式。而__stdcall的调用方式,无论是C的Name Mangling,还是C++的Name Mangling都会对函数名进行修饰。所以,采用__stdcall调用方式之后,必须使用*.def文件对函数名重命名,不然就不能使用GetProcAddress()通过函数名获取函数指针。

 

2、隐式调用时,头文件要注意的地方?

因为使用静态装入,需要有头文件声明这个要被使用的dll中的函数,如果声明中指定了__stdcall或者extern “C”,那么在调用这个函数的时候,编译器就通过Name Mangling之后的函数名去.lib中找这个函数,*.def中的内容是对*.lib里函数的名称不产生作用,*.def文件里的函数重命名只对dll有用。这就有lib 跟dll里函数名不一致的问题了,但并不会产生影响,DLL的制造者跟使用者采用的是一致函数声明。

 

3、所以到底要不要使用__stdcall 呢?

我看到一些代码里是没有使用__stdcall的。如果不使用__stdcall,而使用默认的调用约定_cdecl,并且有extern ”C”。那么VC是不会任何修饰的。这样子生成的dll里的函数名就是原来的函数名。也就可以不使用.def文件了。

也有一些要求必须使用__stdcall,例如com相关的东西、系统的回调函数。具体看有没有需要。

 

 

4、导出函数别名怎么写?

可以在.def文件里对函数名写一个别名。

例如:

EXPORTS

cswuygTest(别名) = _showfun@4(要导出的函数)

 

或者:

#pragma comment(linker"/export:[别名] =[NameMangling后的名称]")

 

这样做就可以随便修改别名了,不会出现找不到符号的错误。

 

5、用不用*.def文件?

如果采用VC默认的调用约定,可以不用*.def文件,如果要采用__stdcall调用约定,又不想函数名被修饰,那就采用*.def文件吧,另一种在代码里写的重命名的方式不够方便。

6、什么情况下(不)需要考虑函数重命名的问题?

1)、隐式调用(通过lib)

如果dll的制造者跟dll的使用者采用同样的语言、同样编程环境,那么就不需要考虑函数重命名。使用者在调用函数的时候,通过Name Mangling后的函数名能在lib里找到该函数。

如果dll的制造者跟dll使用不同的语言、或者不同的编译器,那就需要考虑重命名了。

2)、显示调用(通过GetProcessAddress)

       这绝对是必须考虑函数重命名的。

7、总结

    总的来说,在编写DLL的时候,写个头文件,头文件里声明函数的NameMingling方式、调用约定(主要是为了隐式调用)。再写个*.def文件把函数重命名了(主要是为了显式调用)。提供*.DLL\*.lib\*.h给dll的使用者,这样无论是隐式的调用,还是显式的调用,都可以方便的进行。

 

附:

  一个简单DLL导出函数的例子:http://files.cnblogs.com/cswuyg/%E7%BC%96%E5%86%99DLL%E6%89%80%E5%AD%A6%E6%89%80%E6%80%9D.rar

 

学习资料:

http://www.cnblogs.com/dongzhiquan/archive/2009/08/04/1994764.html

http://topic.csdn.net/u/20081126/14/70ac75b3-6e79-4c48-b9fe-918dce147484.html


实例:

1、DLL项目

    MyDll.h文件

 

#ifdef WUYGDLL_EXPORTS
#define WUYGDLL_API __declspec(dllexport)
#else
#define WUYGDLL_API __declspec(dllimport)
#endifextern "C" WUYGDLL_API int _stdcall wuygFun(void); 


MyDll.cpp

#include "stdafx.h"
#include "MyDll.h"
#include
using namespace std;#ifdef _MANAGED
#pragma managed(push, off)
#endifBOOL APIENTRY DllMain( HMODULE hModule,DWORD ul_reason_for_call,LPVOID lpReserved)
{switch (ul_reason_for_call){case DLL_PROCESS_ATTACH:case DLL_THREAD_ATTACH:case DLL_THREAD_DETACH:case DLL_PROCESS_DETACH:break;}return TRUE;
}#ifdef _MANAGED
#pragma managed(pop)
#endifextern "C" int _stdcall wuygFun( void )
{wcout <}


MyDll.def

LIBRARY "wuygDll"
EXPORTS
wuygFun &#64; 1




2、调用dll

 userDll.cpp

#include "stdafx.h"
#include
#include

#include "MyDll.h"
#pragma comment(lib, "wuygDll")int _tmain(int argc, _TCHAR* argv[]){//隐式调用wuygFun();//显式调用typedef int (__stdcall *Fun)(void); HMODULE hDll &#61; ::LoadLibrary(L"wuygDll");Fun TestFun &#61; ::GetProcAddress(hDll, "wuygFun");TestFun();system("pause");return 0;}












推荐阅读
  • 动态壁纸 LiveWallPaper:让您的桌面栩栩如生(第二篇)
    在本文中,我们将继续探讨如何开发动态壁纸 LiveWallPaper,使您的桌面更加生动有趣。作为 2010 年 Google 暑期大学生博客分享大赛 Android 篇的一部分,我们将详细介绍 Ed Burnette 的《Hello, Android》第三版中的相关内容,并分享一些实用的开发技巧和经验。通过本篇文章,您将了解到如何利用 Android SDK 创建引人入胜的动态壁纸,提升用户体验。 ... [详细]
  • 本文详细介绍了在 Vue.js 前端框架中集成 vue-i18n 插件以实现多语言支持的方法。通过具体的配置步骤和示例代码,帮助开发者快速掌握如何在项目中实现国际化功能,提升用户体验。同时,文章还探讨了常见的多语言切换问题及解决方案,为开发人员提供了实用的参考。 ... [详细]
  • 本文深入解析了Java面向对象编程的核心概念及其应用,重点探讨了面向对象的三大特性:封装、继承和多态。封装确保了数据的安全性和代码的可维护性;继承支持代码的重用和扩展;多态则增强了程序的灵活性和可扩展性。通过具体示例,文章详细阐述了这些特性在实际开发中的应用和优势。 ... [详细]
  • 本文探讨了 Kafka 集群的高效部署与优化策略。首先介绍了 Kafka 的下载与安装步骤,包括从官方网站获取最新版本的压缩包并进行解压。随后详细讨论了集群配置的最佳实践,涵盖节点选择、网络优化和性能调优等方面,旨在提升系统的稳定性和处理能力。此外,还提供了常见的故障排查方法和监控方案,帮助运维人员更好地管理和维护 Kafka 集群。 ... [详细]
  • 深入解析 Golang 中 Context 的功能与应用
    本文详细探讨了 Golang 中 Context 的核心功能及其应用场景,通过深入解析其工作机制,帮助读者更好地理解和运用这一重要特性,对于提升代码质量和项目开发效率具有重要的参考价值。 ... [详细]
  • 在Ubuntu系统中配置Python环境变量是确保项目顺利运行的关键步骤。本文介绍了如何将Windows上的Django项目迁移到Ubuntu,并解决因虚拟环境导致的模块缺失问题。通过详细的操作指南,帮助读者正确配置虚拟环境,确保所有第三方库都能被正确识别和使用。此外,还提供了一些实用的技巧,如如何检查环境变量配置是否正确,以及如何在多个虚拟环境之间切换。 ... [详细]
  • 在深入研究 React 项目的过程中,特别是在探索 react-router 源码时,我发现了其中蕴含的中间件概念。这激发了我对中间件的进一步思考与整理。本文将详细探讨 Redux 中间件的原理及其在实际项目中的应用,帮助读者更好地理解和使用这一强大工具。通过具体示例和代码解析,我们将揭示中间件如何提升应用的状态管理和异步操作处理能力。 ... [详细]
  • 本文介绍了UUID(通用唯一标识符)的概念及其在JavaScript中生成Java兼容UUID的代码实现与优化技巧。UUID是一个128位的唯一标识符,广泛应用于分布式系统中以确保唯一性。文章详细探讨了如何利用JavaScript生成符合Java标准的UUID,并提供了多种优化方法,以提高生成效率和兼容性。 ... [详细]
  • React项目基础教程第五课:深入解析组件间通信机制 ... [详细]
  • 探索偶数次幂二项式系数的求和方法及其数学意义 ... [详细]
  • 第六章:枚举类型与switch结构的应用分析
    第六章深入探讨了枚举类型与 `switch` 结构在编程中的应用。枚举类型(`enum`)是一种将一组相关常量组织在一起的数据类型,广泛存在于多种编程语言中。例如,在 Cocoa 框架中,处理文本对齐时常用 `NSTextAlignment` 枚举来表示不同的对齐方式。通过结合 `switch` 结构,可以更清晰、高效地实现基于枚举值的逻辑分支,提高代码的可读性和维护性。 ... [详细]
  • Vue应用预渲染技术详解与实践 ... [详细]
  • 在 CentOS 7 中,为了扩展可用软件包的数量,通常需要配置多个第三方软件源。这些第三方源包括 EPEL、Nux Dextop 和 ELRepo 等,它们提供了大量官方源中未包含的软件包,从而增强了系统的功能性和灵活性。通过正确配置这些源,用户可以轻松安装和管理更多种类的软件,满足不同的需求。 ... [详细]
  • 本文探讨了资源访问的学习路径与方法,旨在帮助学习者更高效地获取和利用各类资源。通过分析不同资源的特点和应用场景,提出了多种实用的学习策略和技术手段,为学习者提供了系统的指导和建议。 ... [详细]
  • SQLite数据库CRUD操作实例分析与应用
    本文通过分析和实例演示了SQLite数据库中的CRUD(创建、读取、更新和删除)操作,详细介绍了如何在Java环境中使用Person实体类进行数据库操作。文章首先阐述了SQLite数据库的基本概念及其在移动应用开发中的重要性,然后通过具体的代码示例,逐步展示了如何实现对Person实体类的增删改查功能。此外,还讨论了常见错误及其解决方法,为开发者提供了实用的参考和指导。 ... [详细]
author-avatar
欢姨oi
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有