so 文件 elf apk so文件怎么打开开

ELF文件格式解析
1. ELF文件简介
首先,你需要知道的是所谓对象文件(Object files)有三个种类:
可重定位的对象文件(Relocatable file)
这是由汇编器汇编生成的 .o 文件。后面的链接器(link editor)拿一个或一些 Relocatable object files 作为输入,经链接处理后,生成一个可执行的对象文件 (Executable file) 或者一个可被共享的对象文件(Shared object file)。我们可以使用 ar 工具将众多的 .o Relocatable object files 归档(archive)成 .a 静态库文件。如何产生 Relocatable file,你应该很熟悉了,请参见我们相关的基本概念文章和JulWiki。另外,可以预先告诉大家的是我们的内核可加载模块 .ko 文件也是 Relocatable object file。
可执行的对象文件(Executable file)
这我们见的多了。文本编辑器vi、调式用的工具gdb、播放mp3歌曲的软件mplayer等等都是Executable object file。你应该已经知道,在我们的
里面,存在两种可执行的东西。除了这里说的 Executable object file,另外一种就是可执行的脚本(如shell脚本)。注意这些脚本不是 Executable object file,它们只是文本文件,但是执行这些脚本所用的解释器就是 Executable object file,比如 bash shell 程序。
可被共享的对象文件(Shared object file)
这些就是所谓的动态库文件,也即 .so 文件。如果拿前面的静态库来生成可执行程序,那每个生成的可执行程序中都会有一份库代码的拷贝。如果在磁盘中存储这些可执行程序,那就会占用额外的磁盘空 间;另外如果拿它们放到Linux系统上一起运行,也会浪费掉宝贵的物理内存。如果将静态库换成动态库,那么这些问题都不会出现。动态库在发挥作用的过程 中,必须经过两个步骤:
a) 链接编辑器(link editor)拿它和其他Relocatable object file以及其他shared object file作为输入,经链接处理后,生存另外的 shared object file 或者 executable file。
b)在运行时,动态链接器(dynamic linker)拿它和一个Executable file以及另外一些 Shared object file 来一起处理,在Linux系统里面创建一个进程映像。
这里我们主要是以Shared Object File(.so)为重点分析对象,因为我们在逆向APK中会遇到的绝大部分都是此类文件。
2. ELF文件格式
首先,ELF文件格式提供了两种视图,分别是链接视图和执行视图。
链接视图是以节(section)为单位,执行视图是以段(segment)为单位。链接视图就是在链接时用到的视图,而执行视图则是在执行时用到的视图。上图左侧的视角是从链接来看的,右侧的视角是执行来看的。总个文件可以分为四个部分:
- ELF header: 描述整个文件的组织。
- Program Header Table: 描述文件中的各种segments,用来告诉系统如何创建进程映像的。
- sections 或者 segments:segments是从运行的角度来描述elf文件,sections是从链接的角度来描述elf文件,也就是说,在链接阶段,我们可以忽略program header table来处理此文件,在运行阶段可以忽略section header table来处理此程序(所以很多加固手段删除了section header table)。从图中我们也可以看出,segments与sections是包含的关系,一个segment包含若干个section。
- Section Header Table: 包含了文件各个segction的属性信息,我们都将结合例子来解释。
程序头部表(Program Header Table),如果存在的话,告诉系统如何创建进程映像。
节区头部表(Section Header Table)包含了描述文件节区的信息,比如大小、偏移等。
如下图,可以通过执行命令&readelf -S android_server&来查看该可执行文件中有哪些section。
通过执行命令readelf &segments android_server,可以查看该文件的执行视图。
这验证了第一张图中所述,segment是section的一个集合,sections按照一定规则映射到segment。那么为什么需要区分两种不同视图?
当ELF文件被加载到内存中后,系统会将多个具有相同权限(flg值)section合并一个segment。操作系统往往以页为基本单位来管理内存分配,一般页的大小为4096B,即4KB的大小。同时,内存的权限管理的粒度也是以页为单位,页内的内存是具有同样的权限等属性,并且操作系统对内存的管理往往追求高效和高利用率这样的目标。ELF文件在被映射时,是以系统的页长度为单位的,那么每个section在映射时的长度都是系统页长度的整数倍,如果section的长度不是其整数倍,则导致多余部分也将占用一个页。而我们从上面的例子中知道,一个ELF文件具有很多的section,那么会导致内存浪费严重。这样可以减少页面内部的碎片,节省了空间,显著提高内存利用率。
3. ELF Header
首先,我们先来看下32位ELF文件中常用的数据格式:
然后我们来观察一下ELF Header的结构体:
#define EI_NIDENT 16
typedef struct {
unsigned char e_ident[EI_NIDENT];
ELF32_Half e_
ELF32_Half e_
ELF32_Word e_
ELF32__Addr e_
ELF32_Off e_
ELF32_Off e_
ELF32_Word e_
ELF32_Half e_
ELF32_Half e_
ELF32_Half e_
ELF32_Half e_
ELF32_Half e_
ELF32_Half e_
接着运行readelf -h android_server命令,可以看到ELF Header结构的内容。
或者使用010Editor的ELF模板也可以看到ELF Header结构。对比以下三类ELF文件,我们得到了以下结论:
(1)e_type标识了文件类型
(2)Relocatable File(.o文件)不需要执行,因此e_entry字段为0,且没有Program Header Table等执行视图
(3)不同类型的ELF文件的Section也有较大区别,比如只有Relocatable File有.strtab节。
Shared Object File(.so文件)
Executable File(可执行文件android_server)
Relocatable File(.o文件)
在ELF Header中我们需要重点关注以下几个字段:
e_entry:程序入口地址
这 个sum.o的进入点是0x0(e_entry),这表面Relocatable objects不会有程序进入点。所谓程序进入点是指当程序真正执行起来的时候,其第一条要运行的指令的运行时地址。因为Relocatable objects file只是供再链接而已,所以它不存在进入点。而可执行文件test和动态库.so都存在所谓的进入点,且可执行文件的e_entry指向C库中的_start,而动态库.so中的进入点指向 call_gmon_start。
如上图中e_entry = 0xD8B0,我们用ida打开该文件看到确实是_start()函数的地址。
e_ehsize:ELF Header结构大小
e_phoff、e_phentsize、e_phnum:描述Program Header Table的偏移、大小、结构。
e_shoff、e_shentsize、e_shnum:描述Section Header Table的偏移、大小、结构。
e_shstrndx:这一项描述的是字符串表在Section Header Table中的索引,值25表示的是Section Header Table中第25项是字符串表(String Table)。
4. Section Header Table
一个ELF文件中到底有哪些具体的 sections,由包含在这个ELF文件中的 section head table(SHT)决定。在SHT中,针对每一个section,都设置有一个条目(entry),用来描述对应的这个section,其内容主要包括该 section 的名称、类型、大小以及在整个ELF文件中的字节偏移位置等等。我们也可以在TISCv1.2规范中找到SHT表中条目的C结构定义:
解析android_server 可执行ELF文件,我们可以看到Section Header Table中确实有23(17h)个条目,且索引为22(16h)确实为section header section string table。
打开条目,我们可以看到每个entry的具体字段,与上图的Elf32_Shdr结构一致。
需要注意的是,sh_name值实际上是.shstrtab中的索引,该string table中存储着所有section的名字。下图中蓝色部分是.shstrtab的数据,我们可以看到,sh_name实际上是从索引1开始的&.shstrtab&字符串,因此这里的sh_name值为1h。
5. Section
下面我们分析一些so文件中重要的Section,包括符号表、重定位表、GOT表等。
-符号表(.dynsym)
符号表包含用来定位、重定位程序中符号定义和引用的信息,简单的理解就是符号表记录了该文件中的所有符号,所谓的符号就是经过修饰了的函数名或者变量名,不同的编译器有不同的修饰规则。例如符号_ZL15global_static_a,就是由global_static_a变量名经过修饰而来。
符号表项的格式如下:
typedef struct {
Elf32_Word st_ //符号表项名称。如果该值非0,则表示符号名的字
//符串表索引(offset),否则符号表项没有名称。
Elf32_Addr st_ //符号的取值。依赖于具体的上下文,可能是一个绝对值、一个地址等等。
Elf32_Word st_ //符号的尺寸大小。例如一个数据对象的大小是对象中包含的字节数。
unsigned char st_ //符号的类型和绑定属性。
unsigned char st_ //未定义。
Elf32_Half st_ //每个符号表项都以和其他节区的关系的方式给出定义。
//此成员给出相关的节区头部表索引。
下面是通过010Editor解析出的符号表.dynsym的section header表项:
再来看一下符号表的具体内容:
-字符串表(.dynstr)
上面我们提到,符号表的st_name是符号名的字符串表中的索引,那么字符串表中肯定存放着所有符号的名称字符串。下面,我们先来看一看字符串表的section header表项:
再看一下下图中字符串表的具体内容,我们可以看出,.dynstr和.shstrtab结构完全相同,不过一个存储的是符号名称的字符串,而另一个是Section名称的字符串。
重定位表在ELF文件中扮演很重要的角色,首先我们得理解重定位的概念,程序从代码到可执行文件这个过程中,要经历编译器,汇编器和链接器对代码的处理。然而编译器和汇编器通常为每个文件创建程序地址从0开始的目标代码,但是几乎没有计算机会允许从地址0加载你的程序。如果一个程序是由多个子程序组成的,那么所有的子程序必需要加载到互不重叠的地址上。重定位就是为程序不同部分分配加载地址,调整程序中的数据和代码以反映所分配地址的过程。简单的言之,则是将程序中的各个部分映射到合理的地址上来。
换句话来说,重定位是将符号引用与符号定义进行连接的过程。例如,当程序调用了一个函数时,相关的调用指令必须把控制传输到适当的目标执行地址。
具体来说,就是把符号的value进行重新定位。
可重定位文件必须包含如何修改其节区内容的信息,从而允许可执行文件和共享目标文件保存进程的程序映象的正确信息。这就是重定位表项做的工作。重定位表项的格式如下:
typedef struct {
Elf32_Addr r_ //重定位动作所适用的位置(受影响的存储单位的第一个字节的偏移或者虚拟地址)
Elf32_Word r_ //要进行重定位的符号表索引,以及将实施的重定位类型(哪些位需要修改,以及如何计算它们的取值)
//其中 .rel.dyn 重定位类型一般为R_386_GLOB_DAT和R_386_COPY;.rel.plt为R_386_JUMP_SLOT
typedef struct {
Elf32_Addr r_
Elf32_Word r_
Elf32_Word r_
对 r_info 成员使用 ELF32_R_TYPE 宏运算可得到重定位类型,使用 ELF32_R_SYM 宏运算可得到符号在符号表里的索引值。 三种宏的具体定义如下:
#define ELF32_R_SYM(i) ((i)&&8)
#define ELF32_R_TYPE(i) ((unsigned char)(i))
#define ELF32_R_INFO(s, t) (((s)
再看一下重定位表中的内容。
以下是.rel.plt表的具体内容:
我们可以看到,每8个字节(s_entsize)一个表项。第一个表项中的r_offset值为0xc7660,r_info为0xa16。其中r_offset指向下图中GOT表中第一项__imp_clock_gettime外部函数地址。那么我们如何利用r_offset值来找到其对应的符号呢?如上所述,进行 ELF32_R_SYM宏运算实际上就是将r_info右移8位,0xa16右移8位得到0xa,因此这就是其在符号表中的索引。
从下图中可以看见符号表的s_entsize值为10h,即16个字节每条目。因此我们可以找到其索引为0xa的条目的st_name值为0x9ea。那么怎么证明我们确实找到的是clock_gettime函数的符号呢?我们再来看一下st_name值是不是正确的。
st_name值表示的是符号名字符串中的第一个字符在字符串表中的偏移量,因此我们用0x9ea加上符号表的起始位置(0x7548)就能得到该字符串在?0x7F32位置。如下图所示。
-常见的重定位表类型:
.rel.text:重定位的地方在.text段内,以offset指定具体要定位位置。在链接时候由链接器完成。.rel.text属于普通重定位辅助段 ,他由编译器编译产生,存在于obj文件内。连接器连接时,他用于最终可执行文件或者动态库的重定位。通过它修改原obj文件的.text段后,合并到最终可执行文件或者动态文件的.text段。其类型一般为R_386_32和R_386_PC32。
.rel.dyn:重定位的地方在.got段内。主要是针对外部数据变量符号。例如全局数据。重定位在程序运行时定位,一般是在.init段内。定位过程:获得符号对应value后,根据rel.dyn表中对应的offset,修改.got表对应位置的value。另外,.rel.dyn 含义是指和dyn有关,一般是指在程序运行时候,动态加载。区别于rel.plt,rel.plt是指和plt相关,具体是指在某个函数被调用时候加载。我个人理解这个Section的作用是,在重定位过程中,动态链接器根据r_offset找到.got对应表项,来完成对.got表项值的修改。
.rel.dyn和.rel.plt是动态定位辅助段。由连接器产生,存在于可执行文件或者动态库文件内。借助这两个辅助段可以动态修改对应.got和.got.plt段,从而实现运行时重定位。
.rel.plt:重定位的地方在.got.plt段内(注意也是.got内,具体区分而已)。 主要是针对外部函数符号。一般是函数首次被调用时候重定位。首次调用时会重定位函数地址,把最终函数地址放到.got内,以后读取该.got就直接得到最终函数地址。我个人理解这个Section的作用是,在重定位过程中,动态链接器根据r_offset找到.got对应表项,来完成对.got表项值的修改。
.plt段(过程链接表):所有外部函数调用都是经过一个对应桩函数,这些桩函数都在.plt段内。具体调用外部函数过程是:
调用对应桩函数&&桩函数取出.got表表内地址&&然后跳转到这个地址.如果是第一次,这个跳转地址默认是桩函数本身跳转处地址的下一个指令地址(目的是通过桩函数统一集中取地址和加载地址),后续接着把对应函数的真实地址加载进来放到.got表对应处,同时跳转执行该地址指令.以后桩函数从.got取得地址都是真实函数地址了。
下图是.plt某表项,它包含了取.got表地址和跳转执行两条指令。
.got(全局偏移表)
6. Program Header Table
程序头部(Program Header)描述与程序执行直接相关的目标文件结构信息。用来在文件中定位各个段的映像。同时包含其他一些用来为程序创建映像所必须的信息。
可执行文件或者共享目标文件的程序头部是一个结构数组,每个结构描述了一个段或者系统准备程序执行所必须的其他信息。目标文件的&段&包含一个或者多个&节区&,也就是&段内容(Segment Contents)&。程序头部仅对可执行文件和共享目标文件有意义。
程序头部的数据结构如下:
typedef struct {
Elf32_Word p_ //此数组元素描述的段的类型,或者如何解释此数组元素的信息。
Elf32_Off p_ //此成员给出从文件头到该段第一个字节的偏移
Elf32_Addr p_ //此成员给出段的第一个字节将被放到内存中的虚拟地址
Elf32_Addr p_ //此成员仅用于与物理地址相关的系统中。System V忽略所有应用程序的物理地址信息。
Elf32_Word p_ //此成员给出段在文件映像中所占的字节数。可以为0。
Elf32_Word p_ //此成员给出段在内存映像中占用的字节数。可以为0。
Elf32_Word p_ //此成员给出与段相关的标志。
Elf32_Word p_ //此成员给出段在文件中和内存中如何对齐。
我们看到,以下两个工具确实是照此格式解析的。
(window.slotbydup=window.slotbydup || []).push({
id: '2467140',
container: s,
size: '1000,90',
display: 'inlay-fix'
(window.slotbydup=window.slotbydup || []).push({
id: '2467141',
container: s,
size: '1000,90',
display: 'inlay-fix'
(window.slotbydup=window.slotbydup || []).push({
id: '2467142',
container: s,
size: '1000,90',
display: 'inlay-fix'
(window.slotbydup=window.slotbydup || []).push({
id: '2467143',
container: s,
size: '1000,90',
display: 'inlay-fix'
(window.slotbydup=window.slotbydup || []).push({
id: '2467148',
container: s,
size: '1000,90',
display: 'inlay-fix'新手园地& & & 硬件问题Linux系统管理Linux网络问题Linux环境编程Linux桌面系统国产LinuxBSD& & & BSD文档中心AIX& & & 新手入门& & & AIX文档中心& & & 资源下载& & & Power高级应用& & & IBM存储AS400Solaris& & & Solaris文档中心HP-UX& & & HP文档中心SCO UNIX& & & SCO文档中心互操作专区IRIXTru64 UNIXMac OS X门户网站运维集群和高可用服务器应用监控和防护虚拟化技术架构设计行业应用和管理服务器及硬件技术& & & 服务器资源下载云计算& & & 云计算文档中心& & & 云计算业界& & & 云计算资源下载存储备份& & & 存储文档中心& & & 存储业界& & & 存储资源下载& & & Symantec技术交流区安全技术网络技术& & & 网络技术文档中心C/C++& & & GUI编程& & & Functional编程内核源码& & & 内核问题移动开发& & & 移动开发技术资料ShellPerlJava& & & Java文档中心PHP& & & php文档中心Python& & & Python文档中心RubyCPU与编译器嵌入式开发驱动开发Web开发VoIP开发技术MySQL& & & MySQL文档中心SybaseOraclePostgreSQLDB2Informix数据仓库与数据挖掘NoSQL技术IT业界新闻与评论IT职业生涯& & & 猎头招聘IT图书与评论& & & CU技术图书大系& & & Linux书友会二手交易下载共享Linux文档专区IT培训与认证& & & 培训交流& & & 认证培训清茶斋投资理财运动地带快乐数码摄影& & & 摄影器材& & & 摄影比赛专区IT爱车族旅游天下站务交流版主会议室博客SNS站务交流区CU活动专区& & & Power活动专区& & & 拍卖交流区频道交流区
白手起家, 积分 3, 距离下一级还需 197 积分
论坛徽章:0
小弟最近在做Android开发,遇到一个问题需要把Android应用反编译,请问如何把里面的.so文件反编译成汇编?因为Android也是linux的内核的,所以应该和linux中的反汇编差不多吧?还望高手指点,如何将linux中的.so文件反汇编???最好有个小例子,谢谢
&&nbsp|&&nbsp&&nbsp|&&nbsp&&nbsp|&&nbsp&&nbsp|&&nbsp
论坛徽章:92
& & .so 文件一般就是個 ELF 文件。
白手起家, 积分 3, 距离下一级还需 197 积分
论坛徽章:0
& & 那要怎样才可以将其反汇编呢?谢谢
论坛徽章:92
回复&&MMMIX
& & 那要怎样才可以将其反汇编呢?谢谢
wdsr90 发表于
& & 和反汇编其他程序一样,用 objdump 或其他你喜欢的反汇编工具。
论坛徽章:1
objdump 吧,在 linux 下用这个比较顺手。
富足长乐, 积分 5503, 距离下一级还需 2497 积分
论坛徽章:2
objdump -S
北京皓辰网域网络信息技术有限公司. 版权所有 京ICP证:060528号 北京市公安局海淀分局网监中心备案编号:
广播电视节目制作经营许可证(京) 字第1234号
中国互联网协会会员&&联系我们:
感谢所有关心和支持过ChinaUnix的朋友们
转载本站内容请注明原作者名及出处developerWorks 社区
动态链接,一个经常被人提起的话题。但在这方面很少有文章来阐明这个重要的软件运行机制,只有一些关于动态链接库编程的文章。本系列文章就是要从源代码的层次来探讨这个问题。
(), linux爱好者
王瑞川 linux爱好者,愿与志同道合者一起探讨,联系方式
当然从文章的题目就可以看出,intel平台下的linux ELF文件的动态链接。一则是因为这一方面的资料查找比较方便,二则也是这个讨论的意思比其它的动态链接要更为重要(毕竟现在是intel的天下)。当然,有了这么一个例子,其它的平台下的ELF文件的动态链接也就大同小异。你可以在阅读完了本文之后"举一隅,而反三隅"了。由于这是一个系列的文章,我计划分三部分来写,第一部分主要分析加载,涉及dl_open这个函数的内容,但由于这个函数所包含的内容实在太多。这里主要是它的_dl_map_object与_dl_init这两个部分,因为这里是把动态链接文件通过在ELF文件中的得到信息映射到内存空间中,而_dl_init中是一个特殊的初始化。这是对面向对象的函数实现的。第二部分我将分析函数解析与卸载,这里要讲的内容会比较多,但每一个内容都不会多。首先是在前一篇中没有说完的dl_open中的涉及的_dl_map_object_deps和_dl_relocate_object两个函数内容,因为这些都与函数解析的内容直接相关,所以安排在这里。而下面的函数解析过程_dl_runtime_resolve是在程序运行中的动态解析过程。这里从本质上来讲没有太多的代码,但它的精巧程度却是最多的(正是我这三篇文章的核心之处)。最后是一个dl_close的实现。这里是一个结尾的工作,顺带一下是_dl_signal_cerror,与_dl_catch_error的错误例外处理。第三部将给出injectso实例分析与应用,会介绍一个应用了动态链接的实例,并可以在日后的程序调试过程中使用的injectso实例,它不仅可以让我们对前面所说的动态链接原理有一个更感性的认识,而且就这个实例而言,还可以在以后的代码开发过程中来作为一种动态打补丁的工具,甚至有可能,我会在以后的文章中会用这个工具来介绍新的技术。一、历史问题关于动态链接,可以说由来已久。如果追溯,最早的思想就在五十年代就有了,那时就想把一些公用的代码放在内存中的一个地方上,在别的地址用call便是了。到后来又发展到了 loading overlays(就是把在程序运行生命期不同的代码在不同的时间段被加入内存),这是在六十年代的事。但这只能算是"滥觞"时期。接近于我们现在所说的动态链接是在unix操作系统之后,因为从unix的设计结构而言,本身就是分成模块来实现一个复杂的功能的操作系统。但这些还不是现代意义上的动态链接,原因是现代意义上的动态链接要符合两个特点:1、
动态的加载,就是当这个运行的模块在需要的时候才被映射入运行模块的虚拟内存空间中,如一个模块在运行中要用到mylib.so中的myget函数,而在没有调用mylib.so这个模块中的其它函数之前,是不会把这个模块加载到你的程序中(也就是内存映射),这些内容在内核中实现,用的是页面异常机制(我可能在另一篇文章中提到这个问题)。2、
动态的解析,就是当要调用的函数被调用的时候,才会去把这个函数在虚拟内存空间的起始地址解析出来,再写到专门在调用模块中的储存地址内,如前面所说的你已经调用了myget,所以mylib.so模块肯定已经被映射到了程序虚拟内存之中,而如果你再调用mylib.so中的myput函数,那它的函数地址就在调用的时候才会被解析出来。(注:这里用的程序就是一般所说的进程process,而模块既可能是你的程序的二进制代码,也可能是被你的程序所依赖的别的共享链接文件-------同样ELF格式。)在这两点中很有点像现在的操作系统中对内存的操作,也就是只有当要用到一个内存空间中的时候才会进行虚拟空间映射,而不是过早的把所有的空间映射好,而只有当要从这个内存空间读的时候才分配物理空间。这有点像第一条。而只有当对这个内存空间进行写的时候产生一个COW(copy on write)。这就有点像第二条。这样的好处就是充分避免不必要的开销。因为任何一个程序在运行的时候,大部分情况下,不可能用到所有的调用函数。这样的思想方法提出与实现都是在八十年代的sun公司的SunOS的系统上。关于这一段历史,请你参见资料[1]。ELF二进制格式文件与现代的动态链接思想大致是在同一时段形成的,它的来源是AT&T公司的最早的unix中的a.out二进行文件格式。Bell labs的工作人员为了使这种在unix的早期主要的文件格式适应当时新的软件与操作系统的要求(如aix,SunOS,HP-UX这样的unix变种,对更广泛的应用程序的扩展要求,对面向对象的支持等等),就发明了ELF文件格式。我在这里并不详细讨论ELF文件的具体细节,这本来就可以写一篇很长的文章,你可以参看资料[2]来得到关于它的ABI(application binary interface的规范)。但在ELF文件所采用的那种分层的管理方式却不仅在动态链接中起着重要的作用,而且这一思想可以说是我们计算机中的最古老,也是最经典的思想。对每个ELF文件,都有一个ELF header,在这里的每个header有两个数据成员,就是Elf32_Off
e_它们分别代表了program header 与section header 在ELF文件中的偏移量。Program header 是总纲,而section header 则是第一个小目。Elf32_Addr
sh_Sh_addr这个section 在内存中的映射地址(对动态链接库而言,这是一个相对量,它与整个ELF文件被加载的l_addr形成绝对地址)。Sh_offset是这个section header在文件中的偏移量。用一图来表示就是这样的,它就是用elf header 来管理了整个ELF文件:举个例子,如果要从一个ELF动态链接库文件中,根据已知的函数名称,找到相应的函数起始地址,那么过程是这样的。先从前面的ELF 的ehdr中找到文件的偏移e_phoff处,在这其中找到为PT_DYNAMIC 的d_tag的phdr,从这个地址开始处找到DT_DYNAMIC的节,最后从其中找到这样一个Elf32_Sym结构,它的st_name所指的字符串与给定的名称相符,就用st_value便是了。这种的管理模式,可以说很复杂,有时会看起来是繁琐。如找一个function 的起始地址就要从 elf header &&program header &&symbol section &&function address 这样的四个步骤。但这里的根本的原因是我们的计算机是线性寻址的,并且冯*诺依曼提出的计算机体系结构相关,所以在前面说这是一个古老的思想。但同样也是由于这样的一个ELF文件结构,很有利于ELF文件的扩充。我们可以设想,如果有一天,我们的ELF文件为了某种原因,对它进行加密。这时如果要在ELF文件中保存密钥,这时候可以在ELF文件中开辟一个专门的section
encrypt ,这个section 的type 就是ST_ENCRYPT,那不就是可以了吗?这一点就可以看出ELF文件格式设计者当初的苦心了(现在这个真的有这么一个节了)。二、代码举例讲了这么多,还没有真正讲到在intel 32平台下linux动态链接库的加载与调用。在一般的情况下,我们所编写的程序是由编译器与ld.so这个动态链接库来完成的。而如果要显式的调用某一个动态链接库中的程序,则下面是一个例子。#include &dlfcn.h&
#include &stdio.h&
void (*printf_call)();
char* error_
if(libc=dlopen("/lib/libc.so.5",RTLD_LAZY))
printf_call=dlsym(libc,"printf");
(*printf_call)("hello, world\n");
dlclose(libc);
error_text= dlerror();
printf(error_test);
return -2;
}在这里先用dlopen来打开一个动态链接库文件,而这个过程比我们这里看到的内容多的多,我会在下面用很大的篇幅来说明这一点,而它返回的参数是一个指针,确切的说是struct link_map*,而dlsym就是在这个struct link_map* 与函数名称一起决定这个函数在这个进程中的地址,这个过程用术语来说就是函数解析(function resolution)。而最后的dlclose就是释放刚才在dlopen中得到的资源,这个过程与我们在加载的share object file module,内核中的程序是大概相同的,只不过这里是在用户态,而那个是在内核态。从函数的复杂性而言这里还要复杂一些(最后有一点要说明,如果你想编译上面的文件-------文件名如果是test那就不能用一般的gcc -o test test.c ,而应该是gcc -c test test.c -ldl这样才能编译通过,因为不这样编译器会找不到dlopen 与dlsym dlclose这些特别函数的库文件libdl.so.2, -ldl 就是加载它的标志的)。三、_dl_open加载过程分析本文以及以后的两篇文章将都以上面的程序所展示的而讲解。也就是以dlopen && dlsym
&& dlclose 的方式 来讲解这个过程,但有几点先要说明: 我在这里所展示的源代码来自glibc 2.3.2版本。但由于原来的代码,从代码的移植与健壮的考虑,而有许多的防止出错,与关于不同平台的代码,在这里大部分是出错处理代码,我把这些的代码都删除。并且只以intel 32平台下的代码为准。还有,在这里的还考虑到了多线程情况下的动态链接库加载,这里也不予以包括在内(因为现在的linux内核中没有对内核线程的支持)。所以你所看到的代码,在尽量保证说明动态链接加载与函数解析的情况作了多数的删减,代码量大概只有原来的四分之一左右,同时最大程度保持了原来代码的风格,突出核心功能。尽管如此,还是有高达2000行以上的代码,请大家耐心的解读。我也会对其中可能的难解之处作出详细的说明。让大家真正体会到代码设计与动态解析的真谛。第一个函数在dl-open.c中
void* internal_function
_dl_open (const char *file, int mode, const void *caller)
struct dl_open_
__rtld_lock_lock_recursive (GL(dl_load_lock));
args.file =
args.mode =
args.caller =
args.map = NULL;
dl_open_worker(&args);
__rtld_lock_unlock_recursive (GL(dl_load_lock));
}这里的internal_function是表明这个函数从寄存器中传递参数,而它的定义在configure.in中得到的。# define internal_function __attribute__ ((regparm (3), stdcall))这其中的regparm就是gcc的编译选项是从寄存器传递3个参数,而stdcall表明这个函数是由调用函数来清栈,而一般的函数是由调用者来负责清栈,用的是cdecl。
__rtld_lock_lock_recursive (GL(dl_load_lock));与__rtld_lock_unlock_recursive (GL(dl_load_lock));在现在还没有完全定义,至少在linux中是没有的,但可以参考在linux/kmod.c 中的request_module中为了防止过度嵌套而加的一个锁。而其它的内容就是一个封装了。dl_open_worker是真正做动态链接库映射并构造一个struct link_map而这是一个绝对重要的数据结构它的定义由于太长,我会放在第二篇文章结束的附录中介绍,因为那时你可以回头再理解动态链接库加载与解析的过程,而在下面的具体函数中出现了作实用性的解释,下面我们分段来看:_dl_open() && dl_open_worker()
static void
dl_open_worker (void *a)
……………………..
args-&map = new = _dl_map_object (NULL, file, 0, lt_loaded, 0, mode);这里就是调用_dl_map_object 来把文件映射到内存中。原来的函数要从不同的路径搜索动态链接库文件,还要与SONAME(这是动态链接库文件在运行时的别名)比较,这些内容我在这里都删除了。_dl_open() && dl_open_worker() && _dl_map_object()
struct link_map *
internal_function
_dl_map_object (struct link_map *loader, const char *name, int preloaded,
int type, int trace_mode, int mode)
char *name_
struct link_map *l;
/* Look for this name among those already loaded.
for (l = GL(dl_loaded); l = l-&l_next)
if (!_dl_name_match_p (name, l))
…………….
fd = open_path (name, namelen, preloaded, &env_path_list,
&realname, &fb);
l = _dl_new_object (name_copy, name, type, loader);
return _dl_map_object_from_fd (name, fd, &fb, realname, loader, type, mode);
}/*end of _dl_map_object*/这里先在已经被加载的一个动态链接库的链中搜索,在行中就是作这一件事。想起来也很简单,因为可能在一个可执行文件依赖好几个动态链接库。而其中有几个动态链接库或许都依赖于同一个动态链接文件,可能早就加载了这样一个动态链接库,就是这样的情况了。下面open_path是一个关键,这里要指出的是env_path_list得到的方式有几种,一是在系统环境变量,二就是DT_RUNPATH所指的节中的字符串(参见下面的
),还有更复杂的,是从其它要加载这个动态链接库文件的动态链接库中得到的环境变量-------这些问题我们都不说明了。
_dl_open() && dl_open_worker() && _dl_map_object() && open_path()
static int open_path (const char *name, size_t namelen, int preloaded,
struct r_search_path_struct *sps, char **realname,
struct filebuf *fbp)
struct r_search_path_elem **dirs = sps-&
int fd = -1;
const char *current_what = NULL;
int any = 0;
buf = alloca (max_dirnamelen + max_capstrlen + namelen);
struct r_search_path_elem *this_dir = *
size_t buflen = 0;
………………
struct stat64
edp = (char *) __mempcpy (buf, this_dir-&dirname, this_dir-&dirnamelen);
for (cnt = 0; fd == -1 && cnt & ++cnt)
/* Skip this directory if we know it does not exist.
if (this_dir-&status[cnt] == nonexisting)
buflen = ((char *) __mempcpy (__mempcpy (edp, capstr[cnt].str,
capstr[cnt].len), name, namelen)- buf);
fd = open_verify (buf, fbp);
__xstat64 (_STAT_VER, buf, &st);
…………….
}在这上面的alloc是在栈上分配空间的函数,这样就不用担心在函数结束的时候出现内存泄漏的情况(好的程序员真的要对内存的分配熟谙于心)。1313行就是把r_search_path_elem的dirname copy过来,而在行的内容就是为这个路径加上最后的'/'路径分隔号,而capstr就是根据不同的操作系统与体系得到的路径分隔号。这其实是一个很好的例子,因为__memcpy返回的参数是dest string所copy的最后的一个字节的地址,所以每copy之后就会得到新的地址,如果用strncpy来写的话,就要用这样的方法strncpy(edp, capstr[cnt].str, capstr[cnt].len);
edp+=capstr[cnt].
strncpy(edp,name, namelen);
buflen=edp-这就要用四句,而这里用了一句就可以了。下面的open_verify是打开这个buf所指的文件名,fbp是从这个文件得到的文件开时1024字节的内容,并对文件的有效性进行检查,这里最主要的是ELF_IMAGIC核对。如果成功,就返回一个大于-1的文件描述符。整个open_path就这样完成了打开文件的方法。_dl_new_object是一个分配struct link_map* 数据结构并填充一些最基本的参数。
_dl_open() && dl_open_worker() && _dl_map_object() && _dl_new_object()
struct link_map *
internal_function
_dl_new_object (char *realname, const char *libname, int type,
struct link_map *loader)
struct link_map *l;
size_t libname_len = strlen (libname) + 1;
struct link_map *
struct libname_list *
new = (struct link_map *) calloc (sizeof (*new) + sizeof (*newname)
+ libname_len, 1);
………………..
new-&l_name =
new-&l_type =
new-&l_loader =
new-&l_scope = new-&l_scope_
new-&l_scope_max = sizeof (new-&l_scope_mem) / sizeof (new-&l_scope_mem[0]);
if (GL(dl_loaded) != NULL)
l = GL(dl_loaded);
while (l-&l_next != NULL)
new-&l_prev =
/* new-&l_next = NULL;
Would be necessary but we use calloc.
l-&l_next =
/* Add the global scope.
new-&l_scope[idx++] = &GL(dl_loaded)-&l_
GL(dl_loaded) =
++GL(dl_nloaded);
}在2039行的内存分配是一个把libname 与name的数据结构也一同分配,是一种零用整取的策略。从行都是为struct link_map 的成员数据赋值。从行则是把新的struct link_map* 加入到一个单链中,这是在以后是很有用的,因为这样在一个执行文件中如果要整体管理它相关的动态链接库,就可以以单链遍历。如果要加载的动态链接库还没有被映射到进程的虚拟内存空间的话,那只是准备工作,真正的要点在_dl_map_object_from_fd()这个函数开始的。因为这之后,每一步都有关动态链接库在进程中发挥它的作用而必须的条件。这上段比较长,所以分段来看,_dl_open() && dl_open_worker() && _dl_map_object() && _dl_map_from_fd()
struct link_map *
_dl_map_object_from_fd (const char *name, int fd, struct filebuf *fbp,
char *realname, struct link_map *loader, int l_type,
struct link_map *l = NULL;
const ElfW(Ehdr) *
const ElfW(Phdr) *
const ElfW(Phdr) *
struct stat64
__fxstat64 (_STAT_VER, fd, &st);
for (l = GL(dl_loaded); l = l-&l_next)
if (l-&l_ino == st.st_ino && l-&l_dev == st.st_dev)
__close (fd);
……………
free (realname);
add_name_to_object (l, name);
}这里先开始就要从再找一遍,如果找到了已经有的struct link_map* 要加载的libname(的而比较的依据是它的与st_ino,这是物理文件在内存中编号,且文件的设备号st_dev相同,这是从比较底层来比较文件,具体的原因,你可以参看我将要发表的《从linux的内存管理看文件共享的实现》)。之所以采取这样再查一遍,因为如果进程从要开始打开动态链接库文件,走到这里可能要经过很长的时间(据我作的实验来看,对第一次打开的文件大概也就在200毫秒左右---------主要的时间是硬盘的寻道与读盘,但这对于计算机的进程而言已经是很长的时间了。)所以,有可能别的线程已经读入了这个动态链接库,这样就没有必要再做下去了。这与内核在文件的打开文件所用的思想是一致的。
_dl_open() && dl_open_worker() && _dl_map_object() && _dl_map_from_fd()
/* This is the ELF header.
We read it in `open_verify'.
header = (void *) fbp-&
l-&l_entry = header-&e_
type = header-&e_
l-&l_phnum = header-&e_
maplength = header-&e_phnum * sizeof (ElfW(Phdr));
1436这一段所作的为下面的ELF文件的分节映射入内存做一点准备(要读写phdr的数组)。_dl_open() && dl_open_worker() && _dl_map_object() && _dl_map_from_fd()
/* Scan the program header table, collecting its load commands.
struct loadcmd
ElfW(Addr) mapstart, mapend, dataend,
} loadcmds[l-&l_phnum], *c;
size_t nloadcmds = 0;这里把数据结构定义在函数内部,能保证这是一个局部变量定义,与面向对象中的private的效果是一样的。_dl_open() && dl_open_worker() && _dl_map_object() && _dl_map_from_fd()
for (ph = ph & &phdr[l-&l_phnum]; ++ph)
switch (ph-&p_type)
case PT_DYNAMIC:
l-&l_ld = (void *) ph-&p_
l-&l_ldnum = ph-&p_memsz / sizeof (ElfW(Dyn));
case PT_PHDR:
l-&l_phdr = (void *) ph-&p_
case PT_LOAD:
…………..
c = &loadcmds[nloadcmds++];
c-&mapstart = ph-&p_vaddr & ~(ph-&p_align - 1);
c-&mapend = ((ph-&p_vaddr + ph-&p_filesz + GL(dl_pagesize) - 1)
& ~(GL(dl_pagesize) - 1));
c-&dataend = ph-&p_vaddr + ph-&p_
c-&allocend = ph-&p_vaddr + ph-&p_
c-&mapoff = ph-&p_offset & ~(ph-&p_align - 1);
…………..
c-&prot = 0;
if (ph-&p_flags & PF_R)
c-&prot |= PROT_READ;
if (ph-&p_flags & PF_W)
c-&prot |= PROT_WRITE;
if (ph-&p_flags & PF_X)
c-&prot |= PROT_EXEC;
}在ELF文件的规范中,根据不同的program header 不同,要实现不同的功能,采用不同的处理策略,具体的内容请参看
中的说明。这里没有出现一般的default 但实际运行与下面的语句是等价的:
真是达到程序简洁的特点。但有一个特别要指出的是PT_LOAD的那些,把所有的可以加载的节都在加载的数据结构中loadcmds中构建完成,是一个好的想法。特别是指针的妙用,值得学习(1467
c = &loadcmds[nloadcmds++];)。_dl_open() && dl_open_worker() && _dl_map_object() && _dl_map_from_fd()
maplength = loadcmds[nloadcmds - 1].allocend - c-&
if (__builtin_expect (type, ET_DYN) == ET_DYN)
…………….
l-&l_map_start = (ElfW(Addr)) __mmap ((void *)0, maplength,
c-&prot, MAP_COPY | MAP_FILE,
fd, c-&mapoff);
l-&l_map_end = l-&l_map_start +
l-&l_addr = l-&l_map_start - c-&
__mprotect ((caddr_t) (l-&l_addr + c-&mapend),
loadcmds[nloadcmds - 1].allocend - c-&mapend,
PROT_NONE);
}在行之间就是把整个文件都进行了映射,妙处在1498行与1501行,是把头与尾的两个PT_LOAD program header 的内容都计算在内了。而1503行就是我们这里的情景,因为这是动态链接库的加载。而1535行的修改虚拟内存的属性,就是把映射在最高地址的空白失效。这是一种保护。为了防止有人利用这里大做文章。_dl_open() && dl_open_worker() && _dl_map_object() && _dl_map_from_fd()
while (c & &loadcmds[nloadcmds])
if (l-&l_phdr == 0
&& (ElfW(Off)) c-&mapoff &= header-&e_phoff
&& ((size_t) (c-&mapend - c-&mapstart + c-&mapoff)
&= header-&e_phoff + header-&e_phnum * sizeof (ElfW(Phdr))))
l-&l_phdr = (void *) (c-&mapstart + header-&e_phoff - c-&mapoff);
if (c-&allocend & c-&dataend)
ElfW(Addr) zero, zeroend,
zero = l-&l_addr + c-&
zeroend = l-&l_addr + c-&
zeropage = ((zero + GL(dl_pagesize) - 1)
& ~(GL(dl_pagesize) - 1));
if (zeroend & zeropage)
zeropage =
if (zeropage & zero)
if ((c-&prot & PROT_WRITE) == 0)
/* Dag nab it.
__mprotect ((caddr_t) (zero & ~(GL(dl_pagesize)
GL(dl_pagesize),
c-&prot|PROT_WRITE) & 0);
memset ((void *) zero, '\0', zeropage - zero);
if ((c-&prot & PROT_WRITE) == 0)
__mprotect ((caddr_t) (zero & ~(GL(dl_pagesize) - 1)),
GL(dl_pagesize), c-&prot);
if (zeroend & zeropage)
mapat = __mmap ((caddr_t) zeropage, zeroend - zeropage,
c-&prot, MAP_ANON|MAP_PRIVATE|MAP_FIXED,
ANONFD, 0);
}这里所作的与上面的相类似,根据在前面从PT_LOAD program header 得到的文件映射的操作属性进行修改,但在zeroend&zerorpage的时候不同,把它映射成为进程独享的数据空间。这也就是一般的初始化数据区BSS的地方。因为zeroend是在文件中的映射的页面对齐尾地址,而zeropage是文件中的内容映射的页面对齐尾地址,这其中的差就是为未初始化数据准备的,这在行之间体现,要把它的属性改成可写的,且全为0。_dl_open() && dl_open_worker() && _dl_map_object() && _dl_map_from_fd()
if (l-&l_phdr == NULL)
ElfW(Phdr) *newp = (ElfW(Phdr) *) malloc (header-&e_phnum
* sizeof (ElfW(Phdr)));
l-&l_phdr = memcpy (newp, phdr,
(header-&e_phnum * sizeof (ElfW(Phdr))));
l-&l_phdr_allocated = 1;
/* Adjust the PT_PHDR value by the runtime load address.
(ElfW(Addr)) l-&l_phdr += l-&l_把phdr 就是program header 也纳入struct link_map的管理之中,一般的情况是不会有的,所以要copy过来。
_dl_open() && dl_open_worker() && _dl_map_object() && _dl_map_from_fd()
elf_get_dynamic_info (l);这里调用的函数elf_get_dynamic_info是在加载过程中最重要的一个之一,因为在这之后的几乎所有的对动态链接管理的内容都要用要与这里的l_info数据组相关。 _dl_open() && dl_open_worker() && _dl_map_object() &&
_dl_map_from_fd() && elf_get_dynamic_info()
static inline void __attribute__ ((unused, always_inline))
elf_get_dynamic_info (struct link_map *l)
ElfW(Dyn) *dyn = l-&l_
ElfW(Dyn) **
info = l-&l_
while (dyn-&d_tag != DT_NULL)
if (dyn-&d_tag & DT_NUM)
info[dyn-&d_tag] =
……………
if (l-&l_addr != 0)
ElfW(Addr) l_addr = l-&l_
if (info[DT_HASH] != NULL)
info[DT_HASH]-&d_un.d_ptr += l_
if (info[DT_PLTGOT] != NULL)
info[DT_PLTGOT]-&d_un.d_ptr += l_
if (info[DT_STRTAB] != NULL)
info[DT_STRTAB]-&d_un.d_ptr += l_
if (info[DT_SYMTAB] != NULL)
info[DT_SYMTAB]-&d_un.d_ptr += l_
……………….
if (info[DT_REL] != NULL)
info[DT_REL]-&d_un.d_ptr += l_
if (info[DT_JMPREL] != NULL)
info[DT_JMPREL]-&d_un.d_ptr += l_
if (info[VERSYMIDX (DT_VERSYM)] != NULL)
info[VERSYMIDX (DT_VERSYM)]-&d_un.d_ptr += l_
}上面的__attribute__ 中的unused 是为了消除编译器在-Wall 情况下对于其中可能没有用到在函数中的局部变量发出警告,而alwayse_inline,很好解释,就是内联函数的强制标志。2829行的l-&l_ld是在前面的__dl_map_object_from_fd中的1455被给定的。也就是所有关于动态链接节的所在地址(参看
中的解释)。
很明显在行之间的循环就是把l_info的内容都填充好。 这为之后有很大的作用,因为这些节是可以找到如函数名与定位信息的,这里的的妙处是把数组的偏移量与d_tag相关联,代码简洁。便是对动态链接库的调整过程(这里调整的每一个节都是与函数解析有重要关系的,详细内容可参看
),如果我们考虑的更远一点,在前面的函数中的1521行一开始把整个文件连续的映射入内存,在这里就很好的得到解释,如果不是连续的,就没有办法在这里作一个统一的调整了。
_dl_open() && dl_open_worker() && _dl_map_object() && _dl_map_from_fd()
/* Finally the file information.
l-&l_dev = st.st_
l-&l_ino = st.st_
}最后就是把设备号与节点号加入就完成了最后的dl_map_object就行了,回头看1414行中对已经加载的文件的搜索,就可以明白这里的作用了。再回到dl_open_worker中_dl_open() && dl_open_worker()
/* It was already open.
if (new-&l_searchlist.r_list != NULL)
if ((mode & RTLD_GLOBAL) && new-&l_global == 0)
(void) add_to_global (new);
/* Increment just the reference counter of the object.
}这就是对已经被打开了的,就对l_opencount加一返回了。但为什么要在2551行之后作出这一判断呢,那是在下面的代码有关,_dl_map_object_deps会把l_searchlist加载入。
_dl_open() && dl_open_worker()
/* Load that object's dependencies.
_dl_map_object_deps (new, NULL, 0, 0, mode & __RTLD_DLOPEN);
……………
while (l-&l_next)
if (! l-&l_relocated)
_dl_relocate_object (l, l-&l_scope, lazy, 0);
if (l == new)
}在这里的_dl_map_object_deps会填充l_searchlist.r_list,对于这个函数与下面的_dl_relocate_object由于与函数的解析关系比较大,所以我放在《Intel平台下linux中ELF文件动态链接的加载、解析及实例分析(中)-----------函数解析与卸载篇》讲解。但可以把这个当作这个新加载的动态链接库的所依赖的动态链接库的struct link_map* 放入这个指针的列表中(就是l_search_list中),_dl_relocate_object是对这个动态链接库中的函数重定位,而这里用的,这里之所以用的是while (1) 2576行,是因为在前面用的_dl_map_object_deps会把这个动态链接库所依赖的动态链接库也加载进来,这其中就会有没有重定位的。_dl_open() && dl_open_worker()
for (i = 0; i & new-&l_searchlist.r_ ++i)
if (++new-&l_searchlist.r_list[i]-&l_opencount & 1
&& new-&l_searchlist.r_list[i]-&l_type == lt_loaded)
struct link_map *imap = new-&l_searchlist.r_list[i];
struct r_scope_elem **runp = imap-&l_
size_t cnt = 0;
while (*runp != NULL)
if (*runp == &new-&l_searchlist)
if (*runp != NULL)
/* Avoid duplicates.
imap-&l_scope[cnt++] = &new-&l_
imap-&l_scope[cnt] = NULL;
}这段代码如果从实现功能上来讲是很简单的,就是在我们刚新加入的动态链接库new中的l_searchlist中(这些都是在前面被dl_object_deps加载入的被依赖的动态链接库数组)imap-&l_scope查找,如果里面runp有&new-&l_searchlist,就不用对原来的imap-&l_scope扩充了,但如果没有就要完成行的扩充工作。但在这之后的背景原因,却是&new-&l_searchlist其实就是new本身。在一般情况下,如果这个依赖的动态链接库在new被加载之前已经加载(具体的原因会在下一篇文章关于动态链接库函数解析中说明),那就会遇到这种情况。而我们又不能保证两个动态链接库之间的互相依赖情况的发生,如下图,那这里的解决办法便是一个补救措施了。_dl_open() && dl_open_worker()
_dl_init (new, __libc_argc, __libc_argv, __environ);这是要调用动态链接库自备的初始函数。这有点类似与insmod时调用的init_module的内容。至于这其中所传递的__libc_argc, __libc_argv, __environ三个参数是在你的可执行文件被运行的时候由bash引入的输入参数与环境变量,一般的动态链接库是没有什么用处了。_dl_open() && dl_open_worker()
_dl_init()
internal_function
_dl_init (struct link_map *main_map, int argc, char **argv, char **env)
ElfW(Dyn) *preinit_array = main_map-&l_info[DT_PREINIT_ARRAY];
ElfW(Dyn) *preinit_array_size = main_map-&l_info[DT_PREINIT_ARRAYSZ];
ElfW(Addr) *
addrs = (ElfW(Addr) *) (preinit_array-&d_un.d_ptr + main_map-&l_addr);
for (cnt = 0; cnt & ++cnt)
(init_t) addrs[cnt]) (argc, argv, env);
i = main_map-&l_searchlist.r_
while (i-- & 0)
call_init (main_map-&l_initfini[i], argc, argv, env);
}先是调用 DT_PREINIT的内容,这是在init之的init方法。我想这个之所以要实现,不光是为让动态链接库的开发者有更好的开发接口,而且还是在以它所依赖的动态链接库之前进行一些初始化工作,借鉴于面向对象的构造函数。
_dl_open() && dl_open_worker()
_dl_init()
&& call_init()
static void
call_init (struct link_map *l, int argc, char **argv, char **env)
if (l-&l_init_called)
l-&l_init_called = 1;
if (l-&l_info[DT_INIT] != NULL)
init_t init = (init_t) DL_DT_INIT_ADDRESS(l, l-&l_addr +
l-&l_info[DT_INIT]-&d_un.d_ptr);
/* Call the function.
init (argc, argv, env);
ElfW(Dyn) *init_array = l-&l_info[DT_INIT_ARRAY];
if (init_array != NULL)
ElfW(Addr) *
jm = l-&l_info[DT_INIT_ARRAYSZ]-&d_un.d_val / sizeof (ElfW(Addr));
addrs = (ElfW(Addr) *) (init_array-&d_un.d_ptr + l-&l_addr);
for (j = 0; j & ++j)
((init_t) addrs[j]) (argc, argv, env);
}行的内容一看便知,是防止两次初始化。下面是对DT_INIT与DT_INIT_ARRAY的函数调用,值得注意的是,前面调用call_init时是对l_initfine的数组进行的,这里就包括了这个新的动态链接库所依赖的。就这样完成了dl_open_worker()这个过程。到此,我们至少大致上已经把动态链接库的过程说了一遍(当然,除了_dl_map_object_deps和_dl_relocate_object)到现在我们已经明白了以下几点:1、
动态链接库的struct link_map* 的产生与组织过程(这个在_dl_new_object中实现)2、
动态链接库是如何被提取信息入struct link_map*中的,并被加载的(这个在open_verify 与dl_map_object_from_fd,elf_get_dynamic_info这三个函数中实现)3、
动态链接库本身的初始化过程(这个在_dl_init中实现)总体上函数调用结构在下图中一个示意图。但还有几个问题没有被提到1、
可执行文件中的函数被如何定位到动态链接库的函数体中的。2、
一个动态链接库与依赖的动态链接库之间是什么关系,它们之间是如何联系。3、
一个函数是怎样被动态解析,它又是使函数调用方与实现方成为一体的。这些问题我会在《Intel平台下linux中ELF文件动态链接的加载、解析及实例分析(中)-----------函数解析与卸载篇》进行阐明,敬请期待。附录A:动态链接section 类型及说明类型数值d_un所指EXEC可选性DYN可选性说明DT_NULL0不用必须必须这个表示动态链接section的结束标志DT_NEEDED1d_val可选可选这个节d_val是包含了以null结尾的字符串,这些字符串是这个动态链接文件或可执行文件的依赖文件名称与路径的节的开始地址DT_PLTRELSZ2d_val可选可选这里的d_val是过程链接表(procedure linkage table)的大小,它与DT_JMPREL结合使用DT_PLTGOT3d_ptr可选可选这里的d_ptr是过程链接表或全局偏移量表的起始地址。DT_HASH4d_ptr必须必须这里的d_val是符号哈希表的起始地址。DT_STRTAB5d_ptr必须必须这里d_ptr所给出的是符号名称字符串表的起始地址。DT_SYMTAB6d_ptr必须必须这里的d_ptr是Elf32_sym数据结构在的节表中的起始地址。DT_STRSZ10d_val必须必须这d_val是上面的DT_STRTAB节的大小。DT_SYMENT11d_val必须必须这里的d_val是DT_SYMTAB中的每个Elf32_Sym数据结构的大小DT_INIT12d_ptr可选可选这里的d_ptr是一个动态链接库被加载时调用的初始函数所在节的起始地址。DT_FINI13d_ptr可选可选这里的d_ptr是一个动态链接库被卸载时,调用解构函数所在节的起始地址。DT_REL17d_ptr必须可选这里的d_ptr与上面的DT_RELA相似,是Elf32_Rel数据结构所在节的起始地址,它在intel平台下用。DT_RELSZ18d_val必须可选这d_val与上面的DT_REL上面的相对应,表明上面的那个节的大小。DT_RELENT19d_val必须可选这里的d_val是DT_REL中的一个Elf32_Rel的数据结构的大小。DT_PLTREL20d_val可选可选这里的d_val是与过程链接表(procedure linkage table)有关的,就是DT_REL 或DT_RELA的值,也就是这个ELF文件用的是DT_REL的话那d_val就是17,而如果是DT_RELA的话就是7DT_JMPREL23d_ptr可选可选这是我们这里最重要的Elf_Dyn,因为d_ptr所指的就是GOT(global object table)全局对象表,这其实是一个导入函数与全局变量的地址表。DT_INIT_ARRAY25d_ptr可选可选这里的d_ptr是要初始化函数跳转表起始相对地址。DT_FINI_ARRAY26d_ptr可选可选这里的d_ptr是要解构时调用的函数跳转表起始相对地址。DT_INIT_ARRAYSZ27d_val可选可选这里的d_val表明前面的DT_INIT_ARRAY的大小。DT_FINI_ARRAYSZ28d_val可选可选这里的d_val是前面的DT_FINI_ARRAY的大小。DT_ENCODING32d_val或d_ptr没有规定没有规定现在这个节还没有规定,但很明显就是为以后的加密而准备的。DT_PREINIT_ARRAY32d_ptr可选不用这里d_ptr是在调用main函数之前的调用初始函数跳转表的起始地址。DT_PREINIT_ARRAYSZ33d_val可选不用这里的d_val是前面的DT_PREINIT_ARRAY的大小上面只列出了在我们这里要用到的项目,而ELF文件规范的设计者还为它留下了可以在不同的系统与平台中独自享用的项目,这里不列出了。附录B:动态链接库program header 类型的说明名称值说明PT_NULL0这是program header 数组的分界标志符。PT_LOAD1这个标志说明它所指的文件内容要被加载到内存单元,加载的内容由p_offset(在ELF文件中的偏移量)p_filesz(被加载的内容在文件中的大小)。而加载的要求是p_vaddr(被建议的加载的开始地址)p_memsz(被加载的建议内存大小)PT_DYNAMIC2表示它所对应的dynamic section 内容,也就是在
中所有的Elf32_Dyn数据结构所在的program heaer
PT_INTERP3这里所指的是一个字符串,它指的是为加载可执行文件而用的动态链接库名称,在linux下,这是/lib/ld-linux.so.2PT_NOTE4为软件开发商加入标识而用的,表明软件的开发说明。PT_SHLIB5这是为日后的扩充面预留。PT_PHDR6表示program header
array自身在内存中的映射地址与大小。
参考资料 John Levine
"Linkers and Loaders" (是对动态链接的一般性理论作了一个概观介绍)可以在以下的网址上看到它的网络版
Executable and Linkable Format (ELF) (这专门介绍ELF文件格式的ABI的好文章,网络版在
可以得到)
glibc2-3-2版本 本文的源代码来源。可以在
中下载而得。
developerWorks: 登录
标有星(*)号的字段是必填字段。
保持登录。
单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件。
在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。
所有提交的信息确保安全。
选择您的昵称
当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。昵称长度在 3 至 31 个字符之间。
您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。
标有星(*)号的字段是必填字段。
(昵称长度在 3 至 31 个字符之间)
单击提交则表示您同意developerWorks 的条款和条件。 .
所有提交的信息确保安全。
文章、教程、演示,帮助您构建、部署和管理云应用。
立即加入来自 IBM 的专业 IT 社交网络。
免费下载、试用软件产品,构建应用并提升技能。
static.content.url=/developerworks/js/artrating/SITE_ID=10Zone=LinuxArticleID=22121ArticleTitle=Intel平台下Linux中ELF文件动态链接的加载、解析及实例分析(一): 加载publish-date=

我要回帖

更多关于 打开so文件 的文章

 

随机推荐