Android和Linux动态加载机制及PLT/GOT作用介绍

来源:互联网 发布:网络编辑是做什么的 编辑:程序博客网 时间:2024/06/06 19:54

1. Android和Linux动态加载机制

首先回顾一下Linux平台上,一个模块甲需要调用另外一个模块乙中的函数时的动态链接机制:


    1、模块甲在编译期间,将要引用的模块乙的名字与函数名写入自身的符号表。
    2、运行期模块甲调用时,调用流程是从调用代码到PLT表到GOT表再跳入模块乙。

    而如何保证模块甲的代码能从其PLT/GOT跳到正确的模块乙入口,这就是链接器做的事情。
    标准Linux链接器是ld.so,支持懒绑定,也就是说,模块甲在编译期间生成的调用模块乙的原始代码,流程是从调用代码到PLT表到链接器。运行期第一次调模块乙时,首先进入链接器,链接器根据调用信息加载模块乙搜寻其符号并将找到的函数地址填入GOT表,之后的后续调用流程就直接走PLT/GOT表了。这种机制能减少加载时的开销,为Linux发行版等采用。

    Android虽然内核基于Linux,但其动态链接机制却不是ld.so而是自带的linker,不支持懒绑定。也就是说,上述模块甲乙如果在Android平台上,则是模块甲加载时,linker就会根据模块甲中的.rel.plt表和字符串表中的内容加载模块乙并搜索其所需函数地址并预先填入GOT表。之后调用流程每次都直接走PLT/GOT表,不再进linker,PLT表中也省去了跳至linker的代码,这种流程和“勤劳”绑定类似,倒是为拦截提供了一点方便。如果拦截懒绑定的入口时模块乙还没加载地址也没找到,拦截就没法进行了。

    要拦截模块甲对乙的调用,一般思路是通过ptrace远程注入并加载一新拦截模块至模块甲,并搜索模块甲的GOT表,找到对模块乙的调用地址,改成新模块内的某函数地址,然后新模块内的这个函数在进行了自己的处理后,再跳到模块乙中。

    Android和Linux的链接器不同导致了内存布局的差异,也导致了网上流行的Linux注入与HOOK的方法行不通。网上的方法是通过ptrace注入后,搜索dynamic的section中的PLTGOT区,去里头取link_map以遍历此进程所加载的模块来搜索需要hook的函数地址。但Android上,dynamic的section的PLTGOT区前几项都是空的,没有link_map这个数据结构,只能通过分析/proc/<pid>/maps来遍历模块。

2. PLT和GOT在动态链接过程中的作用

ELF 格式的共享库使用 PIC 技术使代码和数据的引用与地址无关,程序可以被加载到地址空间的任意位置。PIC 在代码中的跳转和分支指令不使用绝对地址。PIC 在 ELF 可执行映像的数据段中建立一个存放所有全局变量指针的全局偏移量表 GOT

对于模块外部引用的全局变量和全局函数,用 GOT 表的表项内容作为地址来间接寻址;对于本模块内的静态变量和静态函数,用 GOT 表的首地址作为一个基准,用相对于该基准的偏移量来引用,因为不论程序被加载到何种地址空间,模块内的静态变量和静态函数与 GOT 的距离是固定的,并且在链接阶段就可知晓其距离的大小。这样,PIC 使用 GOT 来引用变量和函数的绝对地址,把位置独立的引用重定向到绝对位置。

对于 PIC 代码,代码段内不存在重定位项,实际的重定位项只是在数据段的 GOT 表内。共享目标文件中的重定位类型有 R_386_RELATIVE、R_386_GLOB_DAT 和 R_386_JMP_SLOT,用于在动态链接器加载映射共享库或者模块运行的时候对指针类型的静态数据、全局变量符号地址和全局函数符号地址进行重定位。(这个地方与我另一篇转载的文章基于Android的PLT/GOT符号重定向过程有出入,我认为这里是正确的


PLT过程链接表用于把位置独立的函数调用重定向到绝对位置。通过 PLT 动态链接的程序支持惰性绑定模式。每个动态链接的程序和共享库都有一个 PLT,PLT 表的每一项都是一小段代码,对应于本运行模块要引用的一个全局函数。程序对某个函数的访问都被调整为对 PLT 入口的访问。

每个 PLT 入口项对应一个 GOT 项,执行函数实际上就是跳转到相应 GOT 项存储的地址,该 GOT 项初始值为 PLTn项中的 push 指令地址(即 jmp 的下一条指令,所以第 1 次跳转没有任何作用),待符号解析完成后存放符号的真正地址。动态链接器在装载映射共享库时在 GOT 里设置 2 个特殊值:在 GOT+4( 即 GOT[1]) 设置动态库映射信息数据结构link_map 地址;在 GOT+8(即 GOT[2])设置动态链接器符号解析函数的地址_dl_runtime_resolve。

PLT 的第 1 个入口 PLT0 是一段访问动态链接器的特殊代码。程序对 PLT 入口的第 1 次访问都转到了 PLT0,最后跳入 GOT[2]存储的地址执行符号解析函数。待完成符号解析后,将符号的实际地址存入相应的 GOT 项,这样以后调用函数时可直接跳到实际的函数地址,不必再执行符号解析函数。

操作系统运行程序时,首先将解释器程序即动态链接器ld.so 映射到一个合适的地址,然后启动 ld.so。ld.so 先完成自己的初始化工作,再从可执行文件的动态库依赖表中指定的路径名查找所需要的库,将其加载映射到内存。

Linux用一个全局的库映射信息结构 struct link_map链表来管理和控制所有动态库的加载,动态库的加载过程实际上是映射库文件到内存中,并填充库映射信息结构添加到链表中的过程。结构 struct link_map 描述共享目标文件的加载映射信息,是动态链接器在运行时内部使用的一个结构,通过它保持对已装载的库和库中符号的跟踪。
link_map 使用双向链接中间件“l_next”和“l_prev”链接进程中所有加载的共享库。当动态链接器需要去查找符号的时候,可以向前或向后遍历这个链表,通 过访问链表上的每一个库去搜索需要查找的符号。Link_map 链表的入口由每个可执行映像的全局偏移表的第 2 个入口(GOT[1])指向,查找符号时先从 GOT[1]读取 link_map 结点地址,然后沿着link-map 结点进行搜索。

动态库的加载映射过程主要分 3 步:
(1) 动态链接器调用 __mmap 函数对动态库的所有PT_LOAD 可加载段进行整体映射:

l_map_start=(ElfW(Addr))__mmap ((void *)0, maplength, prot,
MAP_COPY | MAP_FILE, fd, mapoff);

返回值 l_map_start 是实际映射的虚拟地址,和段结构成员 p_vaddr 指定的虚拟地址不一定相同,这对于位置无关代码不会产生影响。但是对于数据段和 link_map 结构中其它相关的位置描述信息还要进行修正。共享库映射的内存位置关系如图 1,l_addr 是实际映射地址和原来指定的映射地址的差值,用于其它位置信息的修正,即简单地将原来指定的虚拟地址加上 l_addr 就可以得到实际加载的虚拟地址

(2)共享文件映射完毕,动态链接器处理共享库的PT_DYNAMIC 动态段,将各项动态链接信息主要是哈希表、符号表、字符串表、重定位表、PLT 重定位项表等地址填写到 link_map 的 l_info 数组结构中。l_info 是 link_map 最重要的字段之一,几乎所有与动态链接管理相关的内容都与 l_info数组有关。动态链接器还要加载处理当前共享库的所有依赖库。

(3)由于实际的映射地址和指定的虚拟地址有可能不同,因此还要对动态库及其依赖库进行重定位。设置动态库的第1 个和第 2 个 GOT 表项:

Elf32_Addr *got =
(Elf32_Addr *) lmap->l_info[DT_PLTGOT].d_un.d_ptr;
got[1]=lmap;
got[2]=&_dl_runtime_resolve;

对动态库的所有重定位项进行重定位,在重定位项指定的偏移地址处加上修正值 l_addr。动态项 DT_REL 给出了重定位表的地址,DT_RELSZ 给出重定位表项的数目。
映射完毕后,动态链接器调用共享库(包括所有相关的依赖库)自备的初始化函数进行初始化。


0 0
原创粉丝点击