Linux和Windows符号导入导出的对比分析1、符号导入导出概念2、导入导出实践(Windows)3、导入导出实践(Linux)4、提取共用头文件 |
您所在的位置:网站首页 › 头文件的定义和使用 › Linux和Windows符号导入导出的对比分析1、符号导入导出概念2、导入导出实践(Windows)3、导入导出实践(Linux)4、提取共用头文件 |
1、符号导入导出概念 1.1 符号导出 Windows下,当一个PE文件需要将一些函数或变量提供给其他PE文件使用时,我们把这种行为叫做符号导出。 Linux下,ELF也是同样的概念,将导出的符号保存在”.dynsym”段中,供动态链接器查找和使用。 1.2 导出表所有导出的符号都被集中存放在了被称作导出表的结构中。 从结构上来看,它提供了一个符号名与符号地址的映射关系,即可以通过某个符号查找相应的地址。 1.3 符号导入如果我们在某个程序中使用了来自于其他动态库的函数或者变量,那么我们就把这种行为叫做符号导入。 1.4 导入表ELF中,“rel.dyn”和”rel.plt”两个段中分别保存了该模块所需要导入的变量和函数的符号以及所在的模块等信息。而”.got”和”.got.plt”则保存着这些变量和函数的真正地址。 Windows中也有类似的机制,它的名字更加直接,叫做导入表,当某个PE文件被加载,windows加载器的其中一个任务就是将所有需要导入的函数地址确定并且将导入表中的元素调整到正确的地址,以实现动态链接的过程。 2、导入导出实践(Windows) 2.1 导出符号Windows下,我们一般通过”__declspec”属性关键字来修饰某个函数或者变量,当我们使用”__declspec(dllexport)”时表示该符号是从本DLL导出的符号,”__declspec(dllimport)”表示该符号是从别的DLL导入的符号。 注:在C++中,如果你希望导入或者导出符号符合C语言的符号修饰规范,那么必须在这个符号的定义之前加上external“C”,以便使编译器按照C语言的风格进行符号修饰。 // SymbolExport.cpp // #include "stdafx.h" #ifdef TEST_MODULE_EXPORTS #define TEST_EXPORTS __declspec(dllexport) #else #define TEST_EXPORTS__declspec(dllimport) #endif double TEST_EXPORTS Add(double a,double b) { return a +b; } double TEST_EXPORTS Sub(double a,double b) { return a -b; } double Mul(double a,double b) { return a *b; } double Divide(double a,double b) { return a /b; } 如上代码,我们定义了一个宏TEST_EXPORTS,在TEST_MODULE_EXPORTS被定义的情况下,定义TEST_EXPORTS的值为__declspec(dllexport),在TEST_MODULE_EXPORTS没有被定义的情况下,定义TEST_EXPORTS的值为__declspec(dllimport)。 注:TEST_MODULE_EXPORTS宏可以在属性->C/C++->预处理器中加 然后,我们把该工程编译为一个DLL,然后使用dumpbin工具来看到生成的DLL中导出的符号: ![]() 如上,dumpbin /exports SymbolExport.dll 即可查看(直接使用dumpbin命令,请搜索到dumpbin.exe的目录后,将该目录加到系统环境变量path里,然后重启) 如上图,dumpbin得到的结果里,只有Add和Sub函数,没有Mul和Divide,结果符合预期,因为,Add和Sub函数前加了__declspec(dllexport)的宏定义TEST_EXPORTS,而Mul和Divide没有。 2.2 导入符号为了验证导入导出的原理,我决定把上面的导出代码稍微改一下,把函数声明放到头文件中,同时导出直接使用__declspec(dllexport),如下: double __declspec(dllexport) Add(double a,double b); double __declspec(dllexport) Sub(double a,double b); double Mul(double a,double b); double Divide(double a,double b); 然后,新加一个导入的工程SymbolImport,并引入上面的头文件,调用Add函数, // SymbolImport.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include "stdio.h" #include "../SymbolExportDLL/SymbolExportDLL.h" int _tmain(int argc,_TCHAR* argv[]) { double result = Add(3.0,2.0); printf("Result = %f\n",result); return 0; } 对上面的工程进行编译,该工程带main函数,生成EXE文件。 原本预想,不加__declspec(dllimport) double Add(double a,double b); 这行代码会链接不过,但是用VS验证了一下,加不加都能正常编译通过,且运行结果都正常。 经查阅资料确定,现在的MSVC编译器对于以上两种导入方式(加__declspec(dllimport)或者不加)都支持。差别在于: 不加__declspec(dllimport)的情况下, 编译器并不区分导入函数和本地函数,它统一地产生直接调用的指令,它统一地产生直接调用的指令。但是链接器在链接时会将导入函数的目标地址导入一小段桩代码(Stub),由这个桩代码再将控制权交给IAT中的真正目标地址,实现如下: CALL 目标地址 ... JMP DWORD PTR [目标地址] 但是从效率和性能的角度来考虑,还是建议使用__declspec(dllimport),因为这样可以减少一条跳转指令。 那__declspec(dllexport)可不可以不用呢? 当然,如果不用__declspec(dllexport)就会有问题了,因为链接的库没有把符号导出,使用的库是链接不通过的。如下: 如截图所示,Mul没有使用__declspec(dllexport)修饰,其他库想要使用,是不行的。 导入符号查看 在刚才的cpp文件中随便加入一个函数test_print来做区分,如下: 编译生成EXE文件,使用dumpbin来查看导入的函数: 从截图中我们也可以看到,除了导入SymbolExportDLL中的函数外,SymbolImport.exe还导入了许多标准基础库的符号。 2.3 导入导出特殊用法 2.3.1 使用宏定义如截图所示,使用宏定义,用工程是否定义了TEST_MODULE_EXPORTS了来区分是导出函数还是导入函数。当别的工程引用SymbolExportDLL.h时,可以不用将需要引用的符号,再使用__declspec(dllimport)来特殊声明,因为通过宏定义,已经自动区分了,该工程是导出该符号,还是需要导入该符号。 2.3.2 序号一个DLL中每一个导出函数都有一个对应的序号(Ordinal Number)。一个导出函数甚至可以没有函数名,但它必须有一个唯一的序号。序号标示被导出函数地址在DLL导出表中的位置。 一般来说,那些仅供内部使用的导出函数,它只有序号没有函数名,这样外部使用者就无法推测它的含义和使用方法,以防止误用。 2.3.2.1 使用def文件定义导出函数序号 接着用上面的导出函数,定义一个.def的文件SymbolExport.def,内容如下: LIBRARY SymbolExport EXPORTS Add @3 Sub @4 Mul @5 Divide @6 NONAME 定义上述几个函数的导出序号,同时将Divide函数导出时去掉函数名。(这里使用3、4、5、6和默认的从1开始区分) 如下,为了避免找C++编译修饰后的函数名称的麻烦,这里改成用c编译。 Cl /c SymbolExport.c Link /DLL /DEF:SymbolExport.def SymbolExport.obj Dumpbin /EXPORTS SymbolExport.dll 如上,可以通过定义def文件的方式来设定动态库导出符号的序号,其他模块在使用这些符号的时候也可以直接使用序号,而不用符号名称。 注:一般情况不建议使用序号作为导入导出的手段,因为真实生产环境中代码变更的频繁度可能比较高。 3、导入导出实践(Linux) 3.1 导出符号 3.1.1 默认符号全部导出和windows不同,windows是默认所有符号不导出,而Linux是默认所有符号都导出,这和gcc的编译参数有关系。 gcc中有一个参数-fvisibility,在不设置的情况下,默认值是default,默认情况下是所有符号都导出。因此正常情况下,Linux是不用关注符号导入导出的问题的。 3.1.2 符号按需导出的好处但是设置符号按需导出,也是有好处的: 1)空间占用:全部符号导出时需要占用资源的,减少符号的导出量,编译生成的动态库的大小会比全部导出小不少。 2)效率:程序启动的时候,在加载动态库时,需要解析和查找本动态库引用的所有符号,当程序加载的动态库较多,且动态库较大的情况下,符号解析也是很耗时的。当符号按需导出后,符号查找的总量大幅缩减,程序的启动效率也会加快。 3.1.3 Linux符号导出的方法前面提到,Linux下gcc默认是导出所有符号的,如果要符号按需导出,则gcc编译时需要加上-fvisibility=hidden参数,使所有符号默认全部不导出,也就是对外不可见。 和windows下__declspec(dllexport)对应的,Linux下可以通过__attribute__ ((visibility("default")))来设置符号导出。 则之前的符号导出头文件可以定义为如下形式: #ifdef TEST_MODULE_EXPORTS #define TEST_EXPORTS __attribute__((visibility("default"))) #else #define TEST_EXPORTS __attribute__((visibility("default"))) #endif double TEST_EXPORTS Add(double a,double b); double TEST_EXPORTS Sub(double a,double b); double Mul(double a,double b); double Divide(double a,double b) 如截图所示,我们分别使用gcc -fPIC -shared -o SymbolExport.so SymbolExport.c -fvisibility=hidden和gcc -fPIC -shared -o SymbolExportOld.so SymbolExport.c各编译一个动态库,然后使用nm查询动态库符号做对比。 如上截图,使用了-fvisibility=hidden参数的情况下,按需导出的符号是T标识的,而不使用-fvisibility=hidden参数的情况下,代码中定义的所有函数符号都是T标识的。 而根据nm的man的官方解释: The symbol type. At least the following types are used; others are, as well, depending on the object file format. If lowercase, the symbol is usuallylocal; if uppercase, the symbol isglobal (external). There are however a few lowercase symbols that are shown for special global symbols ("u", "v" and "w"). 因此,可以通过nm看出,做了符号按需导出的情况下,只有导出的符号才是全局可见的,否则就只是本动态库内的。 3.2 导入符号 3.2.1 GOT简介我们知道ELF的结构如上,...部分里其实还有一部分占比比较小的段,没有一一列出来,其中有一个段,叫做GOT,也被称为全局偏移表,它是一个指向引用的外部模块的全局符号的指针数组。 对于模块间调用和跳转,GOT中相应的向保存的是目标函数的地址,当模块要调用目标函数时,可以通过GOT中的项进行间接跳转。 3.2.2 GOT详情ELF将GOT拆分成了两个表,分别是”.got”和”.got.plt”,其中”.got”用来保存全局变量引用地址,而”.got.plt”用来保存函数引用地址。也就是说,所有对于外部函数的引用全部被分离到了”.got.plt”中。 “.got.plt”还有一个特殊的地方是它的前三项是有特殊意义的: 第一项保存的是”.dynamic”端地址,这段描述了本模块动态链接相关的信息 第二项保存的是被模块的ID 第三项保存的是_dl_runtime_resolve()的地址(用于进行延迟绑定)。 3.2.3 符号导入实践根据上面的理论我们知道Linux下,ELF文件需要导入的外部符号信息都会保存在”.got.plt”段中。下面我们实践一下。 #include "SymbolExport.h" #include "stdio.h" int main() { int result = Add(2,3); int result2 = Sub(2,3); printf("The Result is : %d",result); } 如上,定义一个SymbolImport.c的文件,简单实现如上,然后在linux下编译: gcc -o symbol SymbolImport.c SymbolExport.h SymbolExport.so 编译生成symbol可执行文件,然后使用objdump -x symbol查看。 如上,由于不方便全部内容显示,部分截图,从截图信息中可以看到,symbol中确实存在”.got.plt”的段,它最终指向的位置,指向了两个本模块未定义却使用到了的两个外部函数Sub和Add。 跟前面部分的理论保持一致。 注:Linux下没有和windows下__declspec(dllimport)一样的机制,Linux下只要引用的符号在其他模块是全局可见(也就是导出)的,同时在链接的时候会依赖该模块,那它就是可以直接使用的。 4、提取共用头文件#ifdef TEST_MODULE_EXPORTS #ifdef _WIN32 #define TEST_EXPORTS __declspec(dllexport) #elif _LINUX #define TEST_EXPORTS __attribute__((visibility("default"))) #endif #else #ifdef _WIN32 #define TEST_EXPORTS__declspec(dllimport) #elif _LINUX #define TEST_EXPORTS __attribute__((visibility("default"))) #endif #endif 如上,可以通过宏定义,定义统一的导入导出宏,通过这个宏在不同条件的下的含义来自动修饰要导出或者导入的函数。避免写很多重复代码。而且能做到对需要导出的符号屏蔽系统差异。 |
今日新闻 |
推荐新闻 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |