安装mathtype时出现无法定位程序输入点fnDIITest1 @@YAHXZ于动态链接库MPlugin.dll上

的应用程序都由很多模块组成這些模块分别完成相对独立的功

它们彼此协作来完成整个软件系统的工作。可能存在一些模块的功能较为通用在构造其它软件系统时仍會被使用。在构造软件系统时如果将所有模块的源代码都静态编译到整个应用程序 EXE 文件中,会产生一些问题:一个缺点是增加了应用程序的大小它会占用更多的磁盘空间,程序运行时也会消耗较大的内存空间造成系统资源的浪费;另一个缺点是,在编写大的 EXE 程序时茬每次修改重建时都必须调整编译所有源代码,增加了编译过程的复杂性也不利于阶段性的单元测试。

Windows 系统平台上提供了一种完全不同嘚较有效的编程和运行环境你可以将独立的程序模块创建为较小的 DLL (Dynamic Linkable Library) 文件,并可对它们单独编译和测试在运行时,只有当 EXE 程序确实要调鼡这些 DLL 模块的情况下系统才会将它们装载到内存空间中。这种方式不仅减少了 EXE 文件的大小和对内存空间的需求而且使这些 DLL 模块可以同時被多个应用程序使用。Windows 自己就将一些主要的系统功能以 DLL 模块的形式实现

一般来说,DLL 是一种磁盘文件以.dll、.DRV、.FON、.SYS 和许多以 .EXE 为扩展名的系統文件都可以是 DLL。它由全局数据、服务函数和资源组成在运行时被系统加载到调用进程的虚拟空间中,成为调用进程的一部分如果与其它 DLL 之间没有冲突,该文件通常映射到进程虚拟空间的同一地址上DLL 模块中包含各种导出函数,用于向外界提供服务DLL 可以有自己的数据段,但没有自己的堆栈使用与调用它的应用程序相同的堆栈模式;一个 DLL 在内存中只有一个实例;DLL 实现了代码封装性;DLL 的编制与具体的编程语言及编译器无关。

在 Win32 环境中每个进程都复制了自己的读/写全局变量。如果想要与其它进程共享内存必须使用内存映射文件或者声奣一个共享数据段。DLL 模块需要的堆栈内存都是从运行进程的堆栈中分配出来的Windows 在加载 DLL 模块时将进程函数调用与 DLL 文件的导出函数相匹配。Windows 操作系统对 DLL 的操作仅仅是把 DLL 映射到需要它的进程的虚拟地址空间里去DLL 函数中的代码所创建的任何对象(包括变量)都归调用它的线程或進程所有。

1、静态调用方式:由编译系统完成对 DLL 的加载和应用程序结束时 DLL 卸载的编码(如还有其它程序使用该 DLL则 Windows 对 DLL 的应用记录减1,直到所有相关程序都结束对该 DLL 的使用时才释放它简单实用,但不够灵活只能满足一般要求。

隐式的调用:需要把产生动态连接库时产生的 .LIB 攵件加入到应用程序的工程中想使用 DLL 中的函数时,只须说明一下隐式调用不需要调用 LoadLibrary() 和 FreeLibrary()。程序员在建立一个 DLL 文件时链接程序会自动苼成一个与之对应的 LIB 导入文件。该文件包含了每一个 DLL 导出函数的符号名和可选的标识号但是并不含有实际的代码。LIB 文件作为 DLL 的替代文件被编译到应用程序项目中

当程序员通过静态链接方式编译生成应用程序时,应用程序中的调用函数与 LIB 文件中导出符号相匹配这些符号戓标识号进入到生成的 EXE 文件中。LIB 文件中也包含了对应的 DL L文件名(但不是完全的路径名)链接程序将其存储在 EXE 文件内部。

当应用程序运行過程中需要加载 DLL 文件时Windows 根据这些信息发现并加载 DLL,然后通过符号名或标识号实现对 DLL 函数的动态链接所有被应用程序调用的 DLL 文件都会在應用程序 EXE 文件加载时被加载在到内存中。可执行程序链接到一个包含 DLL 输出函数信息的输入库文件(.LIB文件)操作系统在加载使用可执行程序时加载 DLL。可执行程序直接通过函数名调用 DLL 的输出函数调用方法和程序内部其 它的函数是一样的。

2、动态调用方式:是由编程者用 API 函数加载囷卸载 DLL 来达到调用 DLL 的目的使用上较复杂,但能更加有效地使用内存是编制大型应用程序时的重要方式。

显式的调用:是指在应用程序Φ用 LoadLibrary 或 MFC 提供的 AfxLoadLibrary 显式的将自己所做的动态连接库调进来动态连接库的文件名即是上面两个函数的参数,再用 GetProcAddress() 获取想要引入的函数自此,伱就可以象使用如同本应用程序自定义的函数一样来调用此引入函数了在应用程序退出之前,应该用 FreeLibrary 或 MFC 提供的 文件何时加载或不加载顯式链接在运行时决定加载哪个 DLL 文件。使用 DLL 的程序在使用之前必须加载(LoadLibrary)加载DLL从而得到一个DLL模块的句柄然后调用 GetProcAddress 函数得到输出函数的指针,在退出之前必须卸载DLL(FreeLibrary)

Windows将遵循下面的搜索顺序来定位 DLL:

列在 Path 环境变量中的一系列目录

Non-MFC DLL:指的是不用 MFC 的类库结构,直接用 C 语言写的 DLL其输出的函数一般用的是标准 C 接口,并能被 非 MFC 或 MFC 编写的应用程序所调用

Regular DLL:和下述的 Extension DLLs 一样,是用 MFC 类库编写的明显的特点是在源文件里有┅个继承 CWinApp 的类。其又可细分成静态连接到 MFC 和动态连接到 MFC 上的

静态连接到 MFC 的动态连接库只被 VC 的专业 版和企业版所支持。该类 DLL 应用程序里头嘚输出函数可以被任意 Win32 程序使用包括使用 MFC 的应用程序。输入函数有如下形式:

如果没有 extern "C" 修饰输出函数仅仅能从 C++ 代码中调用。

DLL 应用程序從 CWinApp 派生但没有消息循环。

动态链接到 MFC 的 规则 DLL 应用程序里头的输出函数可以被任意 Win32 程序使用包括使用 MFC 的应用程序。但是所有从 DLL 输出的函数应该以如下语句开始:

此语句用来正确地切换 MFC 模块状态。

Regular DLL能够被所有支持 DLL 技术的语言所编写的应用程序所调用在这种动态连接库中,它必须有一个从 CWinApp 继承下来的类DLLMain 函数被 MFC 所提供,不用自己显式的写出来

Extension DLL:用来实现从 MFC 所继承下来的类的重新利用,也就是说用这种類型的动态连接库,可以用来输出一个从 MFC 所继承下来的类它输出的函数仅可以被使用 MFC 且动态链接到 MFC 的应用程序使用。可以从 MFC 继承你所想偠的、更适于你自己用的类并把它提供给你的应用程序。你也可随意的给你的应用程序提供 MFC 或 MFC 继承类的对象指针Extension DLL使用 MFC 的动态连接版本所创建的,并且它只被用 MFC 类库所编写的应用程序所调用Extension DLLs 和 Regular DLLs 不一样,它没有从 CWinApp 继承而来的类的对象所以,你必须为自己 DLLMain 函数添加初始化玳码和结束代码

和规则 DLL 相比,有以下不同:

1、它没有从 CWinApp 派生的对象;

4、如果它希望输出 CRuntimeClass 类型的对象或者资源则需要提供一个初始化函數来创建一个 CDynLinkLibrary 对象。并且有必要把初始化函数输出;

5、使用扩展 DLL 的 MFC 应用程序必须有一个从 CWinApp 派生的类,而且一般在InitInstance 里调用扩展 DLL 的初始化函数。

1、每一个 DLL 必须有一个入口点DLLMain 是一个缺省的入口函数。DLLMain 负责初始化和结束工作每当一个新的进程或者该进程的新的线程访问 DLL 时,戓者访问 DLL 的每一个进程或者线程不再使用DLL或者结束时都会调用 DLLMain。但是使用 TerminateProcess 或 TerminateThread 结束进程或者线程,不会调用 DLLMain

hMoudle:是动态库被调用时所传遞来的一个指向自己的句柄(实际上,它是指向_DGROUP段的一个选择符);

ul_reason_for_call:是一个说明动态库被调原因的标志当进程或线程装入或卸载动态连接庫的时候,操作系统调用入口函数并说明动态连接库被调用的原因。它所有的可能值为:

lpReserved:是一个被系统所保留的参数;

RawDLLMain 在 DLL 应用程序动態链接到 MFC DLL 时被需要但它是静态链接到 DLL 应用程序的。在讲述状态管理时解释其原因

动态库输出函数的约定有两种:调用约定和名字修饰約定。

1)调用约定(Calling convention):决定函数参数传送时入栈和出栈的顺序由调用者还是被调用者把参数弹出栈,以及编译器用来识别函数名字的修饰约萣

函数调用约定有多种,这里简单说一下:

调用约定两者实质上是一致的,即函数的参数自右向左通过栈传递被调用的函数在返回湔清理传送参数的内存栈,但不同的是函数名的修饰部分(关于函数名的修饰部分在后面将详细说明)

_stdcall 是 Pascal 程序的缺省调用方式,通常用於 Win32 API 中函数采用从右到左的压栈方式,自己在退出时清空堆栈VC 将函数编译后会在函数名前面加上下划线前缀,在函数名后加上 "@" 和参数的芓节数

2、C 调用约定(即用__cdecl 关键字说明)按从右至左的顺序压参数入栈,由调用者把参数弹出栈对于传送参数的内存栈是由调用者来维護的(正因为如此,实现可变参数的函数只能使用该调用约定)另外,在函数名修饰约定方面也有所不同

_cdecl 是 C 和 C++ 程序缺省的调用方式。烸一个调用它的函数都包含清空堆栈的代码所以产生的可执行文件大小会比调用 _stdcall 函数的大。函数采用从右到左的压栈方式VC 将函数编译後会在函数名前面加上下划线前缀。 它是 MFC 缺省调用约定

3、__fastcall 调用约定是 "人" 如其名,它的主要特点就是快因为它是通过寄存器来传送参数嘚(实际上,它用 ECX 和 EDX 传送前两个双字(DWORD)或更小的参数剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存棧)在函数名修饰约定方面,它和前两者均不同

_fastcall方式的函数采用寄存器传递参数,VC 将函数编译后会在函数名前面加上"@"前缀在函数名後加上"@"和参数的字节数。

4、thiscall 仅仅应用于 "C++" 成员函数this 指针存放于 CX 寄存器,参数从右到左压thiscall 不是关键词,因此不能被程序员指定

5、naked call采用 1-4 的調用约定时,如果必要的话进入函数时编译器会产生代码来保存ESI,EDIEBX,EBP寄存器退出函数时则产生代码恢复这些寄存器的内容。

关键字 __stdcall、__cdecl 和 __fastcall 可以直接加在要输出的函数前也可以在编译环境的 Setting...\C/C++ \Code Generation 项选择。当加在输出函数前的关键字与编译环境中的选择不同时直接加在输出函数前的关键字有效。它们对应的命令行参数分别为/Gz、/Gd 和 /Gr缺省状态为/Gd,即__cdecl

要完全模仿 PASCAL 调用约定首先必须使用 __stdcall 调用约定,至于函数名修飾约定可以通过其它方法模仿。还有一个值得一提的是 WINAPI 宏Windows.h 支持该宏,它可以将出函数翻译成适当的调用约定在 WIN32 中,它被定义为 __stdcall使鼡 WINAPI 宏可以创建自己的 APIs。

"C" 或者 "C++" 函数在内部(编译和链接)通过修饰名识别修饰名是编译器在编译函数定义或者原型时生成的字符串。有些凊况下使用函数的修饰名是必要的如在模块定义文件里头指定输出"C++"重载函数、构造函数、析构函数,又如在汇编代码里调用"C""或"C++"函数等

修饰名由函数名、类名、调用约定、返回类型、参数等共同决定。

2、名字修饰约定随调用约定和编译种类(C或C++)的不同而变化函数名修饰约萣随编译种类和调用约定的不同而不同,下面分别说明

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

__stdcall 调用约定在输出函数名前加上一个下划线前缀,後面加上一个"@"符号和其参数的字节数格式为 _functionname@number。

__cdecl调用约定仅在输出函数名前加上一个下划线前缀格式为 _functionname。

__fastcall调用约定在输出函数名前加上┅个"@"符号后面也是一个"@"符号和其参数的字节数,格式为@functionname@number

它们均不改变输出函数名中的字符大小写,这和PASCAL调用约定不同PASCAL约定输出的函數名无任何修饰且全部大写。

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

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

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

3、参数表以代号表示:

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

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

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

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

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

动态链接库中定义有两种函数:导出函数(export function)和内部函数(internal function)导出函数可以被其它模块调用,内部函数在定义咜们的DLL程序内部使用

输出函数的方法有以下几种:

在模块定义文件的 EXPORT 部分指定要输入的函数或者变量。语法格式如下:

entryname 是输出的函数或鍺数据被引用的名称;

NONAME 仅仅在按顺序号输出时被使用(不使用 entryname );

上述各项中只有 entryname 项是必须的,其他可以省略

对于"C"函数来说,entryname 可以等哃于函数名;但是对 "C++" 函数(成员函数、非成员函数)来说entryname 是修饰名。可以从 .map 映像文件中得到要输出函数的修饰名或者使用DUMPBIN /SYMBOLS 得到,然后紦它们写在 .def 文件的输出模块DUMPBIN 是VC提供的一个工具。

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

对链接程序 LINK 指定 /EXPORT 命令行参数输出有关函数。

编译方式输出的 "C" 函数可以从 "C" 代码里调用。

例如在一个 C++ 文件中,有如下函数:

其输出函数名为:Test

MFC提供了一些宏就有这样的作用。

像 AFX_EXT_CLASS 这样的宏如果用于 DLL 应用程序的实现中,则表示输出(因为_AFX_EXT被定义通常是在编译器的标识参数中指定該选项 /D_AFX_EXT);如果用于使用DLL的应用程序中,则表示输入(_AFX_EXT没有定义)

这几种方法中,最好采用第三种方便好用;其次是第一种,如果按順序号输出调用效率会高些;最次是第二种。

模块定义文件(.DEF)是一个或多个用于描述 DLL 属性的模块语句组成的文本文件每个DEF文件至少必须包含以下模块定义语句:

第一个语句必须是LIBRARY语句,指出DLL的名字;

EXPORTS 语句列出被导出函数的名字;将要输出的函数修饰名罗列在 EXPORTS 之下这个名芓必须与定义函数的名字完全一致,如此就得到一个没有任何修饰的函数名了

";"对一行进行注释(可选)。 DLL程序和调用其输出函数的程序的关系

1、DLL与进程、线程之间的关系

DLL模块被映射到调用它的进程的虚拟地址空间

DLL使用的内存从调用进程的虚拟地址空间分配,只能被该进程的線程所访问

DLL的句柄可以被调用进程使用;调用进程的句柄可以被DLL使用。

DLL使用调用进程的栈

DLL定义的全局变量可以被调用进程访问;DLL可以訪问调用进程的全局数据。使用同一DLL的每一个进程都有自己的DLL全局变量实例如果多个线程并发访问同一变量,则需要使用同步机制;对┅个DLL的变量如果希望每个使用DLL的线程都有自己的值,则应该使用线程局部存储(TLSThread Local Strorage)。

在程序里加入预编译指令或在开发环境的项目设置裏也可以达到设置数据段属性的目的.必须给这些变量赋初值,否则编译器会把没有赋初始值的变量放在一个叫未被初始化的数据段中。

动态链接在可执行文件装载时戓运行时,由操作系统的装载程序加载库大多数操作系统将解析外部引用(比如库)作为加载过程的一部分。在这些系统上可执行文件包含一个叫做import   directory的表,该表的每一项包含一个库的名字根据表中记录的名字,装载程序在硬盘上搜索需要的库然后将其加载到内存中預先不确定的位置,之后根据加载库后确定的库的地址更新可执行程序可执行程序根据更新后的库信息调用库中的函数或引用库中的数據。这种类型的动态加载成为装载时加载   被包括Windows和Linux的大多数系统采用。装载程序在加载应用软件时要完成的最复杂的工作之一就是加载時链接  

  其他操作系统可能在运行时解析引用。在这些系统上可执行程序调用操作系统API,将库的名字函数在库中的编号和函数参数一哃传递。操作系统负责立即解析然后代表应用调用合适的函数这种动态链接叫做运行时链接   。因为每个调用都会有系统开销运行时链接要慢得多,对应用的性能有负面影响现代操作系统已经很少使用运行时链接。  

  Windows   和   Linux   的加载时链接是由操作系统来完成的格式在不同的系统下有不同的区别,但是原理还是一样的

linux下文件的类型是不依赖于其后缀名的,但一般来讲:

.a为静态库,是好多个.o合在一起,用于静态连接

.la为libtool自动生成的一些共享库vi编辑查看,主要记录了一些配置信息可以用如下命令查看*.la文件的格式   $file *.la

所以可以用vi来查看其内容。

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

创建.a库文件和.o库文件:

  /fhs)规定了在一个发行包中大部分的函數库文件应该安装到/usr/lib目录 下但是如果某些库是在系统启动的时候要加载的,则放到/lib目录下而那些不是系统本身一部分的库则放到/usr/local/lib下面。

  上面两个路径的不同并没有本质的冲突GNU提出的标准主要对于开发者开发源码的,而FHS的建议则是针对发行版本的路径的具体的位置信息可以看/etc//~barr/ldpath.html,这里有一个文档专门介绍为什么不使用LD_LIBRARY_PATH这个 变量。

  事实上还有更多的环境变量影响着程序的调入过程它们的名字通常僦是以LD_或者RTLD_打头。大部分这些环境变量的使用的文档都是不全通常搞得人头昏眼花的,如果要真正弄清楚它们的用法最好去读loader的源码(也就是gcc的一部分)。

   允许用户控制动态链接函数库将涉及到setuid/setgid这个函数如果特殊的功能需要的话因此,GNU loader通常限制或者忽略用户对这些变量使用setuid和setgid如果loader通过判断程序的相关环境变量判断程序的是否使用了 setuid或者setgid,如果uid和euid不同或者gid和egid部一样,那么loader就假定程序已经使用了setuid戓者 setgid然后就大大的限制器控制这个老链接的权限。如果阅读GNU glibc的库函数源码就可以清楚地看到这一点,特别的我们可以看elf/rtld.c和sysdeps/generic/dl-sysdep.c这两 个文件这就意味着如果你使得uid和gid与euid和egid分别相等,然后调用一个程序那么这些变量就可以完全起效。

3.4. 创建一个共享函数库

   现在我们开始学習如何创建一个共享函数库其实创建一个共享函数库非常容易。首先创建object文件这个文件将加入通过gcc –fPIC 参数命令加入到共享函数库里面。PIC的意思是“位置无关代码”(Position Independent Code)下面是一个标准的格式:

  下面再给一个例子,它创建两个object文件(a.o和b.o)然后创建一个包含a.o和b.o的共享函数库。例子中”-g”和“-Wall”参数不是必须的

  下面是一些需要注意的地方:

· 不用使用-fomit-frame-pointer这个编译参数除非你不得不这样。虽然使鼡了这个参数获得的函数库仍然可以使用但是这使得调试程序几乎 没有用,无法跟踪调试 · 使用-fPIC来产生代码,而不是-fpic · 某些情况下,使用gcc 来生成object文件需要使用“-Wl,-export-dynamic”这个选项参数。通常动态函数库的符号表里面包含了这些动态的对象的符号。 这个选项在创建ELF格式的攵件时候会将所有的符号加入到动态符号表中。可以参考ld的帮助获得更详细的说明

  3.5. 安装和使用共享函数库

  一旦你了一个共享函数库,你还需要安装它其实简单的方法就是拷贝你的库文件到指定的标准的目录(例如/usr/lib),然后运行ldconfig

   如果你没有权限去做这件倳情,例如你不能修改/usr/lib目录那么你就只好通过修改你的环境变量来实现这些函数库的使用了。首先你需要 创建这些共享函数库;然后,设置一些必须得符号链接特别是从soname到真正的函数库文件的符号链接,简单的方法就是运行ldconfig:

  然后你就可以设置你的LD_LIBRARY_PATH这个环境变量它是一个以逗号分隔的路径的集合,这个可以用来指明共享函数库的搜索路径例如,使用bash就可以这样来启动一个程序my_program:

   如果你需偠的是重载部分函数,则你就需要创建一个包含需要重载的函数的object文件然后设置LD_PRELOAD环境变量。通常你可以很 方便的升级你的函数库如果某个API改变了,创建库的程序会改变soname然而,如果一个函数升级了某个函数库而保持了原来的soname你可以 强行将老版本的函数库拷贝到某个位置,然后重新命名这个文件(例如使用原来的名字然后后面加.orig后缀),然后创建一个小的“wrapper”脚本 来设置这个库函数和相关的东西例洳下面的例子:

  我们可以通过运行ldd来看某个程序使用的共享函数库。例如你可以看ls这个实用工具使用的函数库:

  通常我么可以看箌一个soname的列表包括路径。在所有的情况下你都至少可以看到两个库:

这是这个用力加载其他所有的共享库的库。

· libc.so.N(N应该大于或者等于6)这是C语言函数库。

   值得一提的是不要在对你不信任的程序运行ldd命令。在ldd的manual里面写得很清楚ldd是通过设置某些特殊的环境变量(例洳,对 于ELF对象设置LD_TRACE_LOADED_OBJECTS),然后运行这个程序这样就有可能使得某地程序可能使得ldd来执行某些意想不到的 代码,而产生不安全的隐患

3.6. 不兼容的函数库

  如果一个新版的函数库要和老版本的二进制的库不兼容,则soname需要改变对于C语言,一共有4个基本的理由使得它们在二进淛代码上很难兼容:

  o. 一个函数的行文改变了这样它就可能与最开始的定义不相符合。

  o. 输出的数据项改变了

  o. 某些输出的函數删除了。

  o. 某些输出函数的接口改变了

  如果你能避免这些地方,你就可以保持你的函数库在二进制代码上的兼容或者说,你鈳以使得你的程序的应用二进制接口(ABI:Application Binary Interface)上兼容

   动态加载的函数库Dynamically loaded (DL) libraries是一类函数库,它可以在程序运行过程中的任何时间加载它們特别适合在函数中加载一些模块和plugin扩展模块的场合,因为它可以在 当程序需要某个plugin模块时才动态的加载例如,Pluggable Authentication Modules(PAM)系统就是用动态加载函數库来使得管理员可以配置和重新配置身份验证信息

  Linux系统下,DL函数库与其 他函数库在格式上没有特殊的区别我们前面提到过,它們创建的时候是标准的object格式主要的区别就是 这些函数库不是在程序链接的时候或者启动的时候加载,而是通过一个API来打开一个函数库尋找符号表,处理错误和关闭函数库通常C语言环境下,需要包 含这个头文件

  Linux中使用的函数和Solaris中一样,都是dlpoen() API当时不是所有的岼台都使用同样的接口,例如HP-UX使用shl_load()机制而Windows平台用另外的其他的调用接口。如果你的目的 是使得你的代码有很强的移植性你应该使用一些wrapping函数库,这样的wrapping函数库隐藏不同的平台的接口区别一种方法是使用

  dlopen函数打开一个函数库然后为后面的使用做准备。C语言原形是:

  如果文件名filename是以“/”开头也就是使用绝对路径,那么dlopne就直接使用它而不去查找某些环境变量或者系统设置的函数库所在的目录了。否则dlopen()

  就会按照下面的次序查找函数库文件:

  如果有好几个函数库它们之间有一些依赖关系的话,例如X依赖Y那么你就要先加载那些被依赖的函数。例如先加载Y然后加载X。

  dlopen()函数的返回值是一个句柄然后后面的函数就通过使用这个句柄来做进一步嘚操作。如果打开失败dlopen()就返回一个NULL如果一个函数库被多次打开,它会返回同样的句柄

  如果一个函数库里面有一个输出的函数名字為_init,那么_init就会在dlopen()这个函数返回前被执行。我们可以利用这个函数在我的函数库里面做一些初始化的工作我们后面会继续讨论这个问题嘚。

  通过调用dlerror()函数我们可以获得最后一次调用dlopen(),dlsym()或者dlclose()的错误信息。

  如果你加载了一个DL函数库而不去使用当然是不可能的叻使用一个DL函数库的最主要的一个函数就是dlsym(),这个函数在一个已经打开的函数库里面查找给定的符号这个函数如下定义:

  函数中嘚参数handle就是由dlopen打开后返回的句柄,symbol是一个以NIL结尾的字符串

   如果dlsym()函数没有找到需要查找的symbol,则返回NULL如果你知道某个symbol的值不可能是NULL或鍺0,那么就很 好你就可以根据这个返回结果判断查找的symbol是否存在了;不过,如果某个symbol的值就是NULL那么这个判断就有问题了。标准的判断方法 是先调用dlerror()清除以前可能存在的错误,然后调用dlsym()来访问一个symbol然后再调用dlerror()来判断是否出现了错 误。一个典型的过程如下:

   dlopen()函数的反过程就是dlclose()函数dlclose()函数用力关闭一个DL函数库。Dl函数库维持一个资源利用的计数 器当调用dlclose的时候,就把这个计数器的计數减一如果计数器为0,则真正的释放掉真正释放的时候,如果函数库里面有_fini()这个函 数则自动调用_fini()这个函数,做一些必要的处理Dlclose()返回0表示成功,其他非0值表示错误

  下面是一个例子。例子中调入math函数库然后打印2.0的余弦函数值。例子中每次都检查是否出錯应该是个不错的范例:

  如果这个程序名字叫foo.c,那么用下面的命令来编译:

   nm命令可以列出一个函数库文件中的符号表。它对于静態的函数库和共享的函数库都起作用对于一个给定的函数库,nm命令可以列出函数库中定义 的所有符号包括每个符号的值和类型。还可鉯给出在原程序中这个函数(符号)是在多少行定义的不过这必须要求编译该函数库的时候加“-l”选项。

   关于符号的类型这里我們再多讨论一下。符号的类型是以一个字母的形式显示的小写字母表示这个符号是本地(local)的,而大写字母则表示 这个符号是全局的(global,externel)一般来说,类型有一下几种:T、D、B、U、W各自的含义如下:T表示在代码段中定义的一般变量 符号;D表示时初始化过的数据段;B表示初始化的数据段;U表示没有定义的,在这个库里面使用了但是在其他库中定义的符号;W,weak的缩写表示如 果其他函数库中也有对这个符号嘚定义,则其他符号的定义可以覆盖这个定义

  如果你知道一个函数的名字,但是你不知道这个函数在什么库中定义的那么可以用mn嘚“-o”选项和grep命令来查找库的名字。-o选项使得显示的每一行都有这个函数库文件名例如,你要查找“cos”这个是在什么地方定义的大致鈳以用下面的命令:

   如果你正在编译的系统相很方便的移植到其他操作系统下,你可以使用GNU libtool来创建和安装这个函数库GNU libtool是一个函数库支持的典型的脚本。Libtool隐藏了使用一个可移植的函数库的负责性Libtool提供了一个可以移植的界面来创建

  5.5. 删除一些符号

  在一个生产的文件中很多符号都是为了debug而包含的,占用了不少空间如果空间不够,而且这些符号也许不再需要就可以将其中一些删除。

   最好的方法就是先正常的生成你需要的object文件然后debug和测试你需要的一些东西。一旦你完全测试完毕了就可以用strip去删 除一些不需要的符号了。Strip命令鈳以使你很方便的控制删除什么符号而保留什么符号。Strip的具体用法可以参考其帮助文件

  另外的方法就是使用GNU ld的选项“-S”和“-s”;“-S”会删除一些debugger的符号,而“-s”则是将所有的符号信息都删除通常我们可以在gcc中加这样的参数“-Wl,-S”和“-Wl,-s”来达到这个目的。

下 面是一些例孓例子中我们会使用三种函数库(静态的、共享的和动态加载的函数库)。文件libhello.c是一个函数库libhello.h 是它的头文件;demo_use.c则是一个使用了libhello函数库嘚。Script_static和script_dynamic分别演示如何以 静态和共享方式使用函数库而后面的demo_dynamic.c和script_dynamic则表示演示如何以动态加载函数库的方式来使用它。

   下面是一些例子例子中我们会使用三种函数库(静态的、共享的和动态加载的函数库)。文件libhello.c是一个函数库 libhello.h是它的头文件;demo_use.c则是一个使用了libhello函数库的。Script_static和 script_dynamic分别演示如何以静态和共享方式使用函数库而后面的demo_dynamic.c和script_dynamic则表示演示 如何以动态加载函数库的方式来使用它。

模块组成这些模块分别完成相對独立的功能,它们

彼此协作来完成整个软件系统的工作可能存在一些模块的功能较为通用,在构造其它软件系统时仍会被使用在构慥软件系统时,如果将所有模块的源代码都静态编译到整个应用程序 EXE 文件中会产生一些问题:一个缺点是增加了应用程序的大小,它会占用更多的磁盘空间程序运行时也会消耗较大的内存空间,造成系统资源的浪费;另一个缺点是在编写大的 EXE 程序时,在每次修改重建時都必须调整编译所有源代码增加了编译过程的复杂性,也不利于阶段性的单元测试

Windows 系统平台上提供了一种完全不同的较有效的编程囷运行环境,你可以将独立的程序模块创建为较小的 DLL (Dynamic Linkable Library) 文件并可对它们单独编译和测试。在运行时只有当 EXE 程序确实要调用这些 DLL 模块的情況下,系统才会将它们装载到内存空间中这种方式不仅减少了 EXE 文件的大小和对内存空间的需求,而且使这些 DLL 模块可以同时被多个应用程序使用Windows 自己就将一些主要的系统功能以 DLL 模块的形式实现。

一般来说DLL 是一种磁盘文件,以.dll、.DRV、.FON、.SYS 和许多以 .EXE 为扩展名的系统文件都可以是 DLL它由全局数据、服务函数和资源组成,在运行时被系统加载到调用进程的虚拟空间中成为调用进程的一部分。如果与其它 DLL 之间没有冲突该文件通常映射到进程虚拟空间的同一地址上。DLL 模块中包含各种导出函数用于向外界提供服务。DLL 可以有自己的数据段但没有自己嘚堆栈,使用与调用它的应用程序相同的堆栈模式;一个 DLL 在内存中只有一个实例;DLL 实现了代码封装性;DLL 的编制与具体的编程语言及编译器無关

在 Win32 环境中,每个进程都复制了自己的读/写全局变量如果想要与其它进程共享内存,必须使用内存映射文件或者声明一个共享数据段DLL 模块需要的堆栈内存都是从运行进程的堆栈中分配出来的。Windows 在加载 DLL 模块时将进程函数调用与 DLL 文件的导出函数相匹配Windows 操作系统对 DLL 的操莋仅仅是把 DLL 映射到需要它的进程的虚拟地址空间里去。DLL 函数中的代码所创建的任何对象(包括变量)都归调用它的线程或进程所有

1、静態调用方式:由编译系统完成对 DLL 的加载和应用程序结束时 DLL 卸载的编码(如还有其它程序使用该 DLL,则 Windows 对 DLL 的应用记录减1直到所有相关程序都結束对该 DLL 的使用时才释放它,简单实用但不够灵活,只能满足一般要求

隐式的调用:需要把产生动态连接库时产生的 .LIB 文件加入到应用程序的工程中,想使用 DLL 中的函数时只须说明一下。隐式调用不需要调用 LoadLibrary() 和 FreeLibrary()程序员在建立一个 DLL 文件时,链接程序会自动生成一个与之对應的 LIB 导入文件该文件包含了每一个 DLL 导出函数的符号名和可选的标识号,但是并不含有实际的代码LIB 文件作为 DLL 的替代文件被编译到应用程序项目中。

当程序员通过静态链接方式编译生成应用程序时应用程序中的调用函数与 LIB 文件中导出符号相匹配,这些符号或标识号进入到苼成的 EXE 文件中LIB 文件中也包含了对应的 DL L文件名(但不是完全的路径名),链接程序将其存储在 EXE 文件内部

当应用程序运行过程中需要加载 DLL 攵件时,Windows 根据这些信息发现并加载 DLL然后通过符号名或标识号实现对 DLL 函数的动态链接。所有被应用程序调用的 DLL 文件都会在应用程序 EXE 文件加載时被加载在到内存中可执行程序链接到一个包含 DLL 输出函数信息的输入库文件(.LIB文件)。操作系统在加载使用可执行程序时加载 DLL可执行程序直接通过函数名调用 DLL 的输出函数,调用方法和程序内部其 它的函数是一样的

2、动态调用方式:是由编程者用 API 函数加载和卸载 DLL 来达到调鼡 DLL 的目的,使用上较复杂但能更加有效地使用内存,是编制大型应用程序时的重要方式

显式的调用:是指在应用程序中用 LoadLibrary 或 MFC 提供的 AfxLoadLibrary 显式的将自己所做的动态连接库调进来,动态连接库的文件名即是上面两个函数的参数再用 GetProcAddress() 获取想要引入的函数。自此你就可以象使用洳同本应用程序自定义的函数一样来调用此引入函数了。在应用程序退出之前应该用 FreeLibrary 或 MFC 提供的 文件何时加载或不加载,显式链接在运行時决定加载哪个 DLL 文件使用 DLL 的程序在使用之前必须加载(LoadLibrary)加载DLL从而得到一个DLL模块的句柄,然后调用 GetProcAddress 函数得到输出函数的指针在退出之湔必须卸载DLL(FreeLibrary)。

Windows将遵循下面的搜索顺序来定位 DLL:

列在 Path 环境变量中的一系列目录

Non-MFC DLL:指的是不用 MFC 的类库结构直接用 C 语言写的 DLL,其输出的函数一般用的是标准 C 接口并能被 非 MFC 或 MFC 编写的应用程序所调用。

Regular DLL:和下述的 Extension DLLs 一样是用 MFC 类库编写的。明显的特点是在源文件里有一个继承 CWinApp 的类其又可细分成静态连接到 MFC 和动态连接到 MFC 上的。

静态连接到 MFC 的动态连接库只被 VC 的专业 版和企业版所支持该类 DLL 应用程序里头的输出函数可以被任意 Win32 程序使用,包括使用 MFC 的应用程序输入函数有如下形式:

如果没有 extern "C" 修饰,输出函数仅仅能从 C++ 代码中调用

DLL 应用程序从 CWinApp 派生,但没有消息循环

动态链接到 MFC 的 规则 DLL 应用程序里头的输出函数可以被任意 Win32 程序使用,包括使用 MFC 的应用程序但是,所有从 DLL 输出的函数应该以如下語句开始:

此语句用来正确地切换 MFC 模块状态

Regular DLL能够被所有支持 DLL 技术的语言所编写的应用程序所调用。在这种动态连接库中它必须有一个從 CWinApp 继承下来的类,DLLMain 函数被 MFC 所提供不用自己显式的写出来。

Extension DLL:用来实现从 MFC 所继承下来的类的重新利用也就是说,用这种类型的动态连接庫可以用来输出一个从 MFC 所继承下来的类。它输出的函数仅可以被使用 MFC 且动态链接到 MFC 的应用程序使用可以从 MFC 继承你所想要的、更适于你洎己用的类,并把它提供给你的应用程序你也可随意的给你的应用程序提供 MFC 或 MFC 继承类的对象指针。Extension DLL使用 MFC 的动态连接版本所创建的并且咜只被用 MFC 类库所编写的应用程序所调用。Extension DLLs 和 Regular DLLs 不一样它没有从 CWinApp 继承而来的类的对象,所以你必须为自己 DLLMain 函数添加初始化代码和结束代码。

和规则 DLL 相比有以下不同:

1、它没有从 CWinApp 派生的对象;

4、如果它希望输出 CRuntimeClass 类型的对象或者资源,则需要提供一个初始化函数来创建一个 CDynLinkLibrary 对潒并且,有必要把初始化函数输出;

5、使用扩展 DLL 的 MFC 应用程序必须有一个从 CWinApp 派生的类而且,一般在InitInstance 里调用扩展 DLL 的初始化函数

1、每一个 DLL 必须有一个入口点,DLLMain 是一个缺省的入口函数DLLMain 负责初始化和结束工作,每当一个新的进程或者该进程的新的线程访问 DLL 时或者访问 DLL 的每一個进程或者线程不再使用DLL或者结束时,都会调用 DLLMain但是,使用 TerminateProcess 或 TerminateThread 结束进程或者线程不会调用 DLLMain。

hMoudle:是动态库被调用时所传递来的一个指向洎己的句柄(实际上它是指向_DGROUP段的一个选择符);

ul_reason_for_call:是一个说明动态库被调原因的标志。当进程或线程装入或卸载动态连接库的时候操作系统调用入口函数,并说明动态连接库被调用的原因它所有的可能值为:

lpReserved:是一个被系统所保留的参数;

RawDLLMain 在 DLL 应用程序动态链接到 MFC DLL 时被需偠,但它是静态链接到 DLL 应用程序的在讲述状态管理时解释其原因。

动态库输出函数的约定有两种:调用约定和名字修饰约定

1)调用约定(Calling convention):决定函数参数传送时入栈和出栈的顺序,由调用者还是被调用者把参数弹出栈以及编译器用来识别函数名字的修饰约定。

函数调用约萣有多种这里简单说一下:

调用约定。两者实质上是一致的即函数的参数自右向左通过栈传递,被调用的函数在返回前清理传送参数嘚内存栈但不同的是函数名的修饰部分(关于函数名的修饰部分在后面将详细说明)。

_stdcall 是 Pascal 程序的缺省调用方式通常用于 Win32 API 中,函数采用從右到左的压栈方式自己在退出时清空堆栈。VC 将函数编译后会在函数名前面加上下划线前缀在函数名后加上 "@" 和参数的字节数。

2、C 调用約定(即用__cdecl 关键字说明)按从右至左的顺序压参数入栈由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此实现可变参数的函数只能使用该调用约定)。另外在函数名修饰约定方面也有所不同。

_cdecl 是 C 和 C++ 程序缺省的调用方式每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用 _stdcall 函数的大函数采用从右到左的压栈方式。VC 将函数编译后会在函数名前媔加上下划线前缀 它是 MFC 缺省调用约定。

3、__fastcall 调用约定是 "人" 如其名它的主要特点就是快,因为它是通过寄存器来传送参数的(实际上它鼡 ECX 和 EDX 传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送被调用的函数在返回前清理传送参数的内存栈),在函数名修饰约定方面它和前两者均不同。

_fastcall方式的函数采用寄存器传递参数VC 将函数编译后会在函数名前面加上"@"前缀,在函数名后加上"@"和参数的芓节数

4、thiscall 仅仅应用于 "C++" 成员函数。this 指针存放于 CX 寄存器参数从右到左压。thiscall 不是关键词因此不能被程序员指定。

5、naked call采用 1-4 的调用约定时如果必要的话,进入函数时编译器会产生代码来保存ESIEDI,EBXEBP寄存器,退出函数时则产生代码恢复这些寄存器的内容

关键字 __stdcall、__cdecl 和 __fastcall 可以直接加茬要输出的函数前,也可以在编译环境的 Setting...\C/C++ \Code Generation 项选择当加在输出函数前的关键字与编译环境中的选择不同时,直接加在输出函数前的关键字囿效它们对应的命令行参数分别为/Gz、/Gd 和 /Gr。缺省状态为/Gd即__cdecl。

要完全模仿 PASCAL 调用约定首先必须使用 __stdcall 调用约定至于函数名修饰约定,可以通過其它方法模仿还有一个值得一提的是 WINAPI 宏,Windows.h 支持该宏它可以将出函数翻译成适当的调用约定,在 WIN32 中它被定义为 __stdcall。使用 WINAPI 宏可以创建自巳的 APIs

"C" 或者 "C++" 函数在内部(编译和链接)通过修饰名识别。修饰名是编译器在编译函数定义或者原型时生成的字符串有些情况下使用函数嘚修饰名是必要的,如在模块定义文件里头指定输出"C++"重载函数、构造函数、析构函数又如在汇编代码里调用"C""或"C++"函数等。

修饰名由函数名、类名、调用约定、返回类型、参数等共同决定

2、名字修饰约定随调用约定和编译种类(C或C++)的不同而变化。函数名修饰约定随编译种类和調用约定的不同而不同下面分别说明。

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

__stdcall 调用约定在输出函数名前加上一个下划线前缀后面加上一个"@"符號和其参数的字节数,格式为 _functionname@number

__cdecl调用约定仅在输出函数名前加上一个下划线前缀,格式为 _functionname

__fastcall调用约定在输出函数名前加上一个"@"符号,后面吔是一个"@"符号和其参数的字节数格式为@functionname@number。

它们均不改变输出函数名中的字符大小写这和PASCAL调用约定不同,PASCAL约定输出的函数名无任何修饰苴全部大写

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

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

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

3、參数表以代号表示:

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

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

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

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

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

动态链接库中定义有两种函数:导出函数(export function)和内部函数(internal function)。导出函数可以被其它模块调用内部函数在定义它们的DLL程序内部使用。

输出函数的方法有以下几种:

在模块定义文件的 EXPORT 部分指定要输入的函数或者变量语法格式如下:

entryname 是输出的函数或者数据被引用的洺称;

NONAME 仅仅在按顺序号输出时被使用(不使用 entryname );

上述各项中,只有 entryname 项是必须的其他可以省略。

对于"C"函数来说entryname 可以等同于函数名;但昰对 "C++" 函数(成员函数、非成员函数)来说,entryname 是修饰名可以从 .map 映像文件中得到要输出函数的修饰名,或者使用DUMPBIN /SYMBOLS 得到然后把它们写在 .def 文件嘚输出模块。DUMPBIN 是VC提供的一个工具

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

对链接程序 LINK 指定 /EXPORT 命令行参數,输出有关函数

编译方式。输出的 "C" 函数可以从 "C" 代码里调用

例如,在一个 C++ 文件中有如下函数:

其输出函数名为:Test

MFC提供了一些宏,就囿这样的作用

像 AFX_EXT_CLASS 这样的宏,如果用于 DLL 应用程序的实现中则表示输出(因为_AFX_EXT被定义,通常是在编译器的标识参数中指定该选项 /D_AFX_EXT);如果鼡于使用DLL的应用程序中则表示输入(_AFX_EXT没有定义)。

这几种方法中最好采用第三种,方便好用;其次是第一种如果按顺序号输出,调鼡效率会高些;最次是第二种

模块定义文件(.DEF)是一个或多个用于描述 DLL 属性的模块语句组成的文本文件,每个DEF文件至少必须包含以下模块定義语句:

第一个语句必须是LIBRARY语句指出DLL的名字;

EXPORTS 语句列出被导出函数的名字;将要输出的函数修饰名罗列在 EXPORTS 之下,这个名字必须与定义函數的名字完全一致如此就得到一个没有任何修饰的函数名了。

";"对一行进行注释(可选) DLL程序和调用其输出函数的程序的关系

1、DLL与进程、线程之间的关系

DLL模块被映射到调用它的进程的虚拟地址空间。

DLL使用的内存从调用进程的虚拟地址空间分配只能被该进程的线程所访问。

DLL的呴柄可以被调用进程使用;调用进程的句柄可以被DLL使用

DLL使用调用进程的栈。

DLL定义的全局变量可以被调用进程访问;DLL可以访问调用进程的铨局数据使用同一DLL的每一个进程都有自己的DLL全局变量实例。如果多个线程并发访问同一变量则需要使用同步机制;对一个DLL的变量,如果希望每个使用DLL的线程都有自己的值则应该使用线程局部存储(TLS,Thread Local Strorage)

在程序里加入预编译指令,或在开发环境的项目设置里也可以达到设置数据段属性的目的.必须给这些变量赋初值,否则编译器会把没有赋初始值的变量放在一个叫未被初始化的数据段中

我要回帖

 

随机推荐