Linux 动态库剖析Word文档格式.docx
《Linux 动态库剖析Word文档格式.docx》由会员分享,可在线阅读,更多相关《Linux 动态库剖析Word文档格式.docx(12页珍藏版)》请在冰点文库上搜索。
使用动态加载过程,程序可以先加载一个特定的库(已加载则不必),然后调用该库中的某一特定函数(图2展示了这两种方法)。
这是构建支持插件的应用程序的一个普遍的方法。
我稍候将在本文探讨并示范该应用程序编程接口(API)。
图2.静态链接与动态链接
用Linux进行动态链接
现在,让我们深入探讨一下使用Linux中的动态链接的共享库的过程。
当用户启动一个应用程序时,它们正在调用一个可执行和链接格式(ExecutableandLinkingFormat,ELF)映像。
内核首先将ELF映像加载到用户空间虚拟内存中。
然后内核会注意到一个称为.interp的ELF部分,它指明了将要被使用的动态链接器(/lib/ld-linux.so),如清单1所示。
这与UNIX®
中的脚本文件的解释器定义(#!
/bin/sh)很相似:
只是用在了不同的上下文中。
清单1.使用readelf来显示程序标题
mtj@camus:
~/dl$readelf-ldl
ElffiletypeisEXEC(Executablefile)
Entrypoint0x8048618
Thereare7programheaders,startingatoffset52
ProgramHeaders:
TypeOffsetVirtAddrPhysAddrFileSizMemSizFlgAlign
PHDR0x0000340x080480340x080480340x000e00x000e0RE0x4
INTERP0x0001140x080481140x080481140x000130x00013R0x1
[Requestingprograminterpreter:
/lib/ld-linux.so.2]
LOAD0x0000000x080480000x080480000x009580x00958RE0x1000
LOAD0x0009580x080499580x080499580x001200x00128RW0x1000
DYNAMIC0x00096c0x0804996c0x0804996c0x000d00x000d0RW0x4
NOTE0x0001280x080481280x080481280x000200x00020R0x4
GNU_STACK0x0000000x000000000x000000000x000000x00000RW0x4
...
~dl$
注意,ld-linux.so本身就是一个ELF共享库,但它是静态编译的并且不具备共享库依赖项。
当需要动态链接时,内核会引导动态链接(ELF解释器),该链接首先会初始化自身,然后加载指定的共享对象(已加载则不必)。
接着它会执行必要的再定位,包括目标共享对象所使用的共享对象。
LD_LIBRARY_PATH环境变量定义查找可用共享对象的位置。
定义完成后,控制权会被传回到初始程序以开始执行。
再定位是通过一个称为GlobalOffsetTable(GOT)和ProcedureLinkageTable(PLT)的间接机制来处理的。
这些表格提供了ld-linux.so在再定位过程中加载的外部函数和数据的地址。
这意味着无需改动需要间接机制(即,使用这些表格)的代码:
只需要调整这些表格。
一旦进行加载,或者只要需要给定的函数,就可以发生再定位(稍候在用Linux进行动态加载小节中会看到更多的差别)。
再定位完成后,动态链接器就会允许任何加载的共享程序来执行可选的初始化代码。
该函数允许库来初始化内部数据并备之待用。
这个代码是在上述ELF映像的.init部分中定义的。
在卸载库时,它还可以调用一个终止函数(定义为映像的.fini部分)。
当初始化函数被调用时,动态链接器会把控制权转让给加载的原始映像。
用Linux进行动态加载
Linux并不会自动为给定程序加载和链接库,而是与应用程序本身共享该控制权。
这个过程就称为动态加载。
使用动态加载,应用程序能够先指定要加载的库,然后将该库作为一个可执行文件来使用(即调用其中的函数)。
但是正如您在前面所了解到的,用于动态加载的共享库与标准共享库(ELF共享对象)无异。
事实上,ld-linux动态链接器作为ELF加载器和解释器,仍然会参与到这个过程中。
动态加载(DynamicLoading,DL)API就是为了动态加载而存在的,它允许共享库对用户空间程序可用。
尽管非常小,但是这个API提供了所有需要的东西,而且很多困难的工作是在后台完成的。
表1展示了这个完整的API。
表1.DlAPI
函数
描述
dlopen
使对象文件可被程序访问
dlsym
获取执行了dlopen函数的对象文件中的符号的地址
dlerror
返回上一次出现错误的字符串错误
dlclose
关闭目标文件
该过程首先是调用dlopen,提供要访问的文件对象和模式。
调用dlopen的结果是稍候要使用的对象的句柄。
mode参数通知动态链接器何时执行再定位。
有两个可能的值。
第一个是RTLD_NOW,它表明动态链接器将会在调用dlopen时完成所有必要的再定位。
第二个可选的模式是RTLD_LAZY,它只在需要时执行再定位。
这是通过在内部使用动态链接器重定向所有尚未再定位的请求来完成的。
这样,动态链接器就能够在请求时知晓何时发生了新的引用,而且再定位可以正常进行。
后面的调用无需重复再定位过程。
还可以选择另外两种模式,它们可以按位OR到mode参数中。
RTLD_LOCAL表明其他任何对象都无法使加载的共享对象的符号用于再定位过程。
如果这正是您想要的的话(例如,为了让共享的对象能够调用原始进程映像中的符号),那就使用RTLD_GLOBAL吧。
dlopen函数还会自动解析共享库中的依赖项。
这样,如果您打开了一个依赖于其他共享库的对象,它就会自动加载它们。
函数返回一个句柄,该句柄用于后续的API调用。
dlopen的原型为:
#include<
dlfcn.h>
void*dlopen(constchar*file,intmode);
有了ELF对象的句柄,就可以通过调用dlsym来识别这个对象内的符号的地址了。
该函数采用一个符号名称,如对象内的一个函数的名称。
返回值为对象符号的解析地址:
void*dlsym(void*restricthandle,constchar*restrictname);
如果调用该API时发生了错误,可以使用dlerror函数返回一个表示此错误的人类可读的字符串。
该函数没有参数,它会在发生前面的错误时返回一个字符串,在没有错误发生时返回NULL:
char*dlerror();
最后,如果无需再调用共享对象的话,应用程序可以调用dlclose来通知操作系统不再需要句柄和对象引用了。
它完全是按引用来计数的,所以同一个共享对象的多个用户相互间不会发生冲突(只要还有一个用户在使用它,它就会待在内存中)。
任何通过已关闭的对象的dlsym解析的符号都将不再可用。
char*dlclose(void*handle);
动态加载示例
了解了API之后,下面让我们来看一看DLAPI的例子。
在这个应用程序中,您主要实现了一个shell,它允许操作员来指定库、函数和参数。
换句话说,也就是用户能够指定一个库并调用该库(先前未链接于该应用程序的)内的任意一个函数。
首先使用DLAPI来解析该库中的函数,然后使用用户定义的参数(用来发送结果)来调用它。
清单2展示了完整的应用程序。
清单2.使用DLAPI的Shell
stdio.h>
string.h>
#defineMAX_STRING80
voidinvoke_method(char*lib,char*method,floatargument)
{
void*dl_handle;
float(*func)(float);
char*error;
/*Openthesharedobject*/
dl_handle=dlopen(lib,RTLD_LAZY);
if(!
dl_handle){
printf("
!
%s\n"
dlerror());
return;
}
/*Resolvethesymbol(method)fromtheobject*/
func=dlsym(dl_handle,method);
error=dlerror();
if(error!
=NULL){
error);
/*Calltheresolvedmethodandprinttheresult*/
printf("
%f\n"
(*func)(argument));
/*Closetheobject*/
dlclose(dl_handle);
}
intmain(intargc,char*argv[])
charline[MAX_STRING+1];
charlib[MAX_STRING+1];
charmethod[MAX_STRING+1];
floatargument;
while
(1){
>
"
);
line[0]=0;
fgets(line,MAX_STRING,stdin);
strncmp(line,"
bye"
3))break;
sscanf(line,"
%s%s%f"
lib,method,&
argument);
invoke_method(lib,method,argument);
要构建这个应用程序,需要通过GNUCompilerCollection(GCC)使用如下的编译行。
选项-rdynamic用来通知链接器将所有符号添加到动态符号表中(目的是能够通过使用dlopen来实现向后跟踪)。
-ldl表明一定要将dllib链接于该程序。
gcc-rdynamic-odldl.c-ldl
再回到清单2,main函数仅充当解释器,解析来自输入行的三个参数(库名、函数名和浮点参数)。
如果出现bye的话,应用程序就会退出。
否则的话,这三个参数就会传递给使用DLAPI的invoke_method函数。
首先调用dlopen来访问目标文件。
如果返回NULL句柄,表示无法找到对象,过程结束。
否则的话,将会得到对象的一个句柄,可以进一步询问对象。
然后使用dlsymAPI函数,尝试解析新打开的对象文件中的符号。
您将会得到一个有效的指向该符号的指针,或者是得到一个NULL并返回一个错误。
在ELF对象中解析了符号后,下一步就只需要调用函数。
要注意一下这个代码和前面讨论的动态链接的差别。
在这个例子中,您强行将目标文件中的符号地址用作函数指针,然后调用它。
而在前面的例子是将对象名作为函数,由动态链接器来确保符号指向正确的位置。
虽然动态链接器能够为您做所有麻烦的工作,但这个方法会让您构建出极其动态的应用程序,它们可以再运行时被扩展。
调用ELF对象中的目标函数后,通过调用dlclose来关闭对它的访问。
清单3展示了一个如何使用这个测试程序的例子。
在这个例子中,首先编译程序而后执行它。
接着调用了math库(libm.so)中的几个函数。
完成演示后,程序现在能够用动态加载来调用共享对象(库)中的任意函数了。
这是一个很强大的功能,通过它还能够给程序扩充新的功能。
清单3.使用简单的程序来调用库函数
~/dl$gcc-rdynamic-odldl.c-ldl
~/dl$./dl
libm.socosf0.0
1.000000
libm.sosinf0.0
0.000000
libm.sotanf1.0
1.557408
bye
~/dl$
工具
Linux提供了很多种查看和解析ELF对象(包括共享库)的工具。
其中最有用的一个当属ldd命令,您可以使用它来发送共享库依赖项。
例如,在dl应用程序上使用ldd命令会显示如下内容:
~/dl$ldddl
linux-gate.so.1=>
(0xffffe000)
libdl.so.2=>
/lib/tls/i686/cmov/libdl.so.2(0xb7fdb000)
libc.so.6=>
/lib/tls/i686/cmov/libc.so.6(0xb7eac000)
/lib/ld-linux.so.2(0xb7fe7000)
ldd所告诉您的是:
该ELF映像依赖于linux-gate.so(一个特殊的共享对象,它处理系统调用,它在文件系统中无关联文件)、libdl.so(DLAPI)、GNUC库(libc.so)以及Linux动态加载器(因为它里面有共享库依赖项)。
readelf命令是一个有很多特性的实用程序,它让您能够解析和读取ELF对象。
readelf有一个有趣的用途,就是用来识别对象内可再定位的项。
对于我们这个简单的程序来说(清单2展示的程序),您可以看到需要再定位的符号为:
~/dl$readelf-rdl
Relocationsection'
.rel.dyn'
atoffset0x520contains2entries:
OffsetInfoTypeSym.ValueSym.Name
08049a3c00001806R_386_GLOB_DAT00000000__gmon_start__
08049a7800001405R_386_COPY08049a78stdin
.rel.plt'
atoffset0x530contains8entries:
08049a4c00000207R_386_JUMP_SLOT00000000dlsym
08049a5000000607R_386_JUMP_SLOT00000000fgets
08049a5400000b07R_386_JUMP_SLOT00000000dlerror
08049a5800000c07R_386_JUMP_SLOT00000000__libc_start_main
08049a5c00000e07R_386_JUMP_SLOT00000000printf
08049a6000001007R_386_JUMP_SLOT00000000dlclose
08049a6400001107R_386_JUMP_SLOT00000000sscanf
08049a6800001907R_386_JUMP_SLOT00000000dlopen
从这个列表中,您可以看到各种各样的需要再定位(到libc.so)的C库调用,包括对DLAPI(libdl.so)的调用。
函数__libc_start_main是一个C库函数,它优先于程序的main函数(一个提供必要初始化的shell)而被调用。
其他操作对象文件的实用程序包括:
objdump,它展示了关于对象文件的信息;
nm,它列出来自对象文件(包括调试信息)的符号。
还可以将EFL程序作为参数,直接调用Linux动态链接器,从而手动启动映像:
~/dl$/lib/ld-linux.so.2./dl
libm.soexpf0.0
另外,可以使用ld-linux.so的--list选项来罗列ELF映像的依赖项(ldd命令也如此)。
切记,它仅仅是一个用户空间程序,是由内核在需要时引导的。
结束语
本文只涉及到了动态链接器功能的皮毛而已。
在下面的参考资料中,您可以找到对ELF映像格式和过程或符号再定位的更详细的介绍。
而且和Linux其他所有工具一样,你也可以下载动态链接器的源代码(参见参考资料)来深入研究它的内部。
参考资料
学习
∙您可以参阅本文在developerWorks全球站点上的英文原文。
∙查看PeterSeebach所著的文章“剖析共享库”(developerWorks,2005年1月),了解共享库,学习如何构建共享库,并了解可以用于剖析共享库的各种工具。
∙SkyFree.org提供了对ELF的简介(PDF),涵盖了对对象文件、程序加载和C库的介绍。
Wikipedia还提供了对ELF的简短描述以及很多介绍ELF其他信息(很多处理器架构的规格和接口)的资源链接。
∙ChrisRohlf的EM_386博客详细描述了ELF符号解析和所有具体细节。
它解释了GOT和PLT表格以及如何通过Linux动态链接器来操作它们。
∙Wikipedia有很多关于库和静态库的资源。
在那里,您还可以学习到有关链接器和加载器的知识,以及它们与库的关系。
∙阅读“Standardsandspecs:
Anunsunghero:
ThehardworkingELF”(developerWorks,2005年12月),这是一篇很好的对ELF的介绍。
ELF是Linux的标准对象格式。
ELF是一个灵活的文件格式,它涵盖了有关可执行映像、对象、共享库乃至内核转储的信息。
在格式参考和ELF规范文档中查找更详细的信息。
∙LinuxJournal文章“链接器和加载器”(2002年11月)很好地介绍了链接器和加载器使用ELF文件(包括符号解析和再定位)的背后意图。
∙在developerWorksLinux专区中可以找到为Linux开发人员准备的更多参考资料,还可以查阅最受欢迎的文章和教程。
∙在developerWorks上查阅所有Linux技巧和Linux教程。
∙随时关注developerWorks技术活动和网络广播。