Windows下的动态链接

Windows下的DLL文件和EXE文件实际上是一个概念,它们都是有PE格式的二进制文件,稍微有些不同的是PE文件头部有个符号位表示该文件是EXE或是DLL,而DLL文件的扩展名不一定是.dll,也可能是.ocx(OCX控件)或是.CPL(控制面板程序) 

ELF中,共享库中所有的全局函数和变量在默认情况下都可以被其他模块使用,也就是说ELF默认导出所有的全局符号。但是在DLL中情况有所不同,我们需要显示地“告诉”编译器我们需要导出某个符号,否则编译器默认所有符号都不导出。当我们在程序中使用DLL导出的符号时,这个过程被称为导入(Import)。

除了使用“__declspec”扩展关键字指定导入导出符号之外,我们也可以使用“.def”文件来声明导入导出符号。“.def”扩展名的文件是类似于ld链接器的链接脚本语言,可以被当作link链接器的输入文件,用于控制链接过程。“.def”文件中的IMPORT或者EXPORTS段可以用来声明导入导出符号,这个方法不仅对C/C++有效,对其他语言也有效。

DLL的简单例子

假设我们的一个DLL提供数学运算的函数,源代码如下(Math.c)

__declspec(dllexport) double Sub(double a, double b)

{

                Return a – b;

}

经过MSVC编译器cl进行编译,结果生成了“Math.dll”、“Math.obj”、“Math.exp”和“Math.lib”这4个文件.

Math.dll——就是生成的DLL文件;

Math.obj——是编译的目标文件;

Math.exp——EXP文件实际上是链接器在创建DLL时的临时文件。链接器在创建DLL时与静态链接时一样采用两遍扫描过程,DLL一般都有导出符号,链接器在第一遍时会遍历所有的目标文件并且收集所有导出符号信息并且创建DLL的导出表。为了方便起见,链接器把这个导出表放到一个临时的目标文件叫做“.edata”的段中,这个目标文件就是EXP文件,EXP文件实际上是一个标准的PE/COFF目标文件,只不过它的扩展名不是.obj而是.exp

Math.lib——在静态链接的时候,“.lib”文件是一组目标文件的集合。在动态链接的”Math.lib”里面的目标文件是什么呢?“Math.lib”中并不真正包含“Math.c”的代码和数据,它用来描述“Math.dll”的导出符号,它把包含了应用程序链接Math.dll时所需要的导入符号以及一部分“桩”代码,又被称作“胶水”代码,以便与将程序与DLL粘在一起。因此又被称为导入库(Import Library

DLL显示运行时链接

·         LoadLibrary(或者LoadLibraryEx),这个函数用来装载一个DLL到进程的地址空间。

·         GetProcAddress,用来查找某个符号的地址。

·         FreeLibrary,用来卸载某个已加载的模块。

导入函数的调用

编译器在产生导入库时,同一个导出函数会产生两个符号的定义,比如对于函数foo来说,它在导入库中有两个符号,一个是foo,另外一个是__imp__foo。这两个符号的区别是,foo这个符号指向foo函数的桩代码(来源于产生DLL文件时伴随的那个LIB文件,即导入库),而__imp__foo指向foo函数在IAT(导入地址数组Import Address Table)中的位置。程序员可以通过__declspec(dllimport)”来声明导入函数,也可以不使用。通过__declspec(dllimport)来声明导入函数,那么编译器就知道他是外部导入的,以便于产生相应的指令以确保跟导入库中的“__imp__foo”能够正确链接;如果不使用,链接器会把导入函数的目标地址导向一小段桩代码(Stub),由这个桩代码再将控制权交给IAT中的真正目标地址。因此,推荐使用“__declspec(dllimport)”,毕竟从性能上来讲,它比不使用该声明少了一次跳转指令。

使用C++编写共享库会经常遇到各种兼容性问题,这是因为C++的标准只规定了语言层面的规则,而对二进制级别却没有任何规定。为了解决这些问题,更大程度上使得程序能够有更好的重用性,微软公司提出了组件对象模型(COM, Component object model)。为了指导我们使用C++编写动态链接库,在Windows平台下(有些意见对Linux/ELF也有效),要尽量遵循以下指导意见:

·         所有的借口函数都应该是抽象的。所有的方法都应该是纯虚的。(或者是inline的方法也可以)

·         所有的全局函数都应该使用extern “C”来防止名字修饰的不兼容。并且导出函数都应该是__stdcall调用规范的(COMDLL都是用这样的规范)。

·         不要使用C++标准库STL

·         不要使用异常。

·         不要使用虚析构函数。可以创建一个destroy()方法并且重载delete操作符并且调用destroy()

·         不要在DLL里面申请内存,而在DLL外释放(或者相反)。不同的DLL和可执行文件可能使用不同的堆,在一个堆里面申请内存而在另外一个堆里面释放会导致错误。比如,对于内存分配相关的寒暑不应该是inline的,以防止它在编译时被展开到不同的DLL和可知性文件。

·         不要在接口中使用重载方法(Overloaded Methods,一个方法多重参数)。因为不同的编译器对于vtable的安排可能不同。

 

 

你可能感兴趣的:(windows,dll,编译器,import,扩展,语言)