Android Linker浅析

原文出处:http://blog.csdn.net/dinuliang/archive/2010/04/20/5509009.aspx

android linker 浅析

Android 的加载/链接器linker 主要用于实现共享库的加载与链接。它支持应用程序对库函数的隐式和显式调用。对于隐式调用,应用程序的编译与静态库大致相同,只是在静态链接的时候通过--dynamic-linker /system/bin/linker 指定动态链接器,(该信息将被存放在ELF文件的.interp节中,内核执行目标映像文件前将通过该信息加载并运行相应的解释器程序linker.)并链接相应的共享库。与ld.so不同的是,Linker目前没有提供Lazy Binding机制,所有外部过程引用都在映像执行之前解析。对于显式调用,可以同过linker中提供的接口dlopendlsym,dlerrordlclose来动态加载和链接共享库。

Android中的共享库和可执行映像都默认采用ELF格式的文件。(可以通过readelf命令查看可执行文件或共享库的elf信息)

 

1.ELF文件(Exectuable Linkabale Format)

      每个ELF文件的开始部分都包含一个ELF头,其中包含了整个文件的基本信息,包括目标代码的格式,体系结构,各程序头或节头的偏移和大小,组织结构和访问权限等信息。

       程序头表包含了加载到内存中的各种段的索引及属性信息,它将告诉加载器如何加载映像。每个段中有包含了一个或几个节区,每个节区应是唯一的。无论是可执行程序还是共享库都包含以下几个的节区:


1.1 GOTPLT:(全局偏移量表:Global Offset Table   --  过程链接表:Procedure Linkage Table)

       不同映像间的函数和数据引用都是通过它们实现的。GOT(全局偏移表)给出了映像中所有被引用符号(函数或变量)的值。每个普通PLT表项相当于一个函数的桩函数(stub),支持懒绑定的情况下,当发生对外部函数的调用时,程序会通过PLT表将控制交给动态连接器,后者解析出函数的绝对地址,修改GOT中相应的值,之后的调用将不再需要连接器的绑定。由于linker是不支持懒绑定的,所以在进程初始化时,动态链接器首先解析出外部过程引用的绝对地址,一次性的修改所有相应的GOT表项。对共享对象来说,由于GOTPLT节以及代码段和数据段之间的相对位置是固定的,所有引用都是基于一个固定地址(GOT)的偏移量,所以实现了PIC代码,重定位时只需要修改可写段中的GOT表。而可执行程序在连接过程中则可能发生对不可写段的修改。如果只读段和可写段不是以固定的相对位置加载的,那么在重定位是还需要修改所有指向GOT的指针。   

                                      

1.2  dynamic节:

       与重定位有关的基本目录结构,例如:

 

1.3  dynsymdynstr节:

       与重定位有关的符号表和字符串表:

 

1.4  .rel.dyn.rel.plt节:

       .rel.dyn节的表项对应了出外部过程调用的符号以外的所有重定位对象,.rel.plt则对应所有外部过程调用的重定位信息。每个重定位项记录了符号的符号表索引,重定位的操作地址,重定位类型的信息(见3.3节)。重定位所在的节区往往与重定位类型有关,例如:

 

       R_ARM_JUMP_SLOTR_ARM_GLOB_DAT属性的重定位地址一般位于GOT表,R_ARM_COPYR_ARM_ABS32属性的重定位一般位于.data节或.text节中。

2 Linker的加载与启动

       Linker是共享库的加载/链接器,也可以称为解释器(interpreter)。共享库以ELF文件的形式保存在文件系统中,核心的load_elf_binary会首先将其映像文件映射到内存,然后映射并执行其解释器也就是linker的代码。linker的代码段是进程间共享的,但数据段为各进程私有。

linker执行完后会自动跳转到目标映像的入口地址。

       

       android,linker代码的运行域由地址0xb0000100开始(see /bionic/linker/Android.mk),直接从_start开始执行。do_execve会预先将应用程序参数(argc,argv[],envcenvp[]还有一些"辅助向量(Auxiliary Vector)"等(see load_elf_binary>create_elf_tables)存放在分配好的用户空间堆栈中,通过堆栈将这些参数和指针(位于linux_binprm结构体bprm)传递给用户空间的目标进程。

       Linker会提取出它所需要的信息,例如目标映像中程序头表在用户空间的地址以及应用程序入口等。

       

3 加载依赖的共享库

Linker会首先调用__linker_init执行一段自举代码,完成其自身的初始化,初始化与目标映像相关的数据结构。Linker会首先调用alloc_info为目标映像分配一个soinfo结构体,它用于存放与映像文件有关的所有信息,这样可以使可执行映像与共享对象共享连接与重定位函数,后面的程序将通过soinfoflags域判断目标映像是共享库还是可执行程序。

 

与共享库的链接操作通过函数link_image调用其它函数执行。Link_image会对ELF文件进行解析,根据DYNAMIC段确定目标映像(可能是可执行程序或共享库)依赖的共享库,调用find_library函数在soinfo链表中搜索并加载这些共享库。Soinfo链表是进程私有的全局变量,无论其它进程是否已将某一共享库加载至内存,依赖它的进程都需要调用mmap来建立其虚拟内存到实际物理内存的映射,这是因为每个进程都有它自己的mm_struct内存描述符和vm_area_struct结构体链表(每个vm_area_struct对应了该进程虚拟地址空间的一个区域(VMA),同一个物理内存中的映射文件在不同的进程中会被映射到不同的虚拟地址空间。在linux下可以使用pmap(pid)cat /proc/(pid)/maps查看相应进程的地址空间分布,会发现同一个库(如libc.so)被放到了不同的地址上。实际的从文件到内存页的拷贝发生在程序对相应的虚拟内存进行读写操作的时候,系统发生缺页异常,从而产生一次调页请求,内核根据操作的不同创建后援文件页或COW页。

如果在搜索链表的过程中发现该库已经存在,则find_library直接返回该库的soinfo结构,以防止发生重复的加载甚至进入无限递归,否则会调用load_library进行实际的加载操作,库的加载地址均位于0x800000000x90000000之间(prelink的库除外),库与库之间以1MB对齐,库的代码段和数据段都是页对齐的。

 

load_library的具体加载过程是:

1.       读取共享库的文件头和程序头表到指定的页中。

2.       调用get_lib_extents分析ELF头表,并获取文件中的地址信息。如果该库不是prelink的,则库加载的起始地址为零。同时计算出加载该库所需总的内存空间大小。

3.       根据已获得的起始地址和总空间大小,调用alloc_mem_region预先为共享库分配一段内存空间。这段空间是通过系统调用mmap实现的,其访问属性是PROT_READ | PROT_EXEC MAP_PRIVATE | MAP_ANONYMOUS,文件描述符为-1,匿名私有的内存映射意味着,内核将为该库分配虚拟线性区,保留给后边进行内存映射文件的操作。

 

4.       调用alloc_info为该库在共享库链表中分配一个soinfo节点,初始化其数据   结构。

5.       调用load_segments将所有的PT_LOAD属性的段加载至合适的地址空间,代码段与数据段的相对位置与文件的运行域一致。  

 

       如果该段是只读的,则核心将其映射致内存中唯一的拷贝,如果该段是可写的,    NAP_PRIVATE意味着该段是写时拷贝的,只有在写操作时核心才会将相应的    页面拷贝至内存。由于需要对可执行映像中位于只读段的代码进行重定位,所       以调用mprotect    将只读段的属性暂时更改为R/W/E的。 

  

共享库代码由于是位置无关的,所以只需要可写段中的.got段(COW的),                 所以不需要只读段的内存保护。

                    如果bss段的区间中包含页边界,则对超出的部分作另外的匿名映射,                          private anonymous mappings意味着当这些内存映射被取消映射时,内存会真的                     将其释放给系统。

        

加载完成后load_library会调用init_library进行库的初始化操作,init_library又会调用link_image,链接共享库的映像文件,共享对象的链接与重定位过程参考3.3节。

4 可执行映像的重定位

加载目标映像的所有依赖库后,link_image调用reloc_library函数根据DYNAMIC段中的DT_RELDT_JMPREL属性节区对目标映像进行重订位,两种属性的节区都是Elf32_Rel结构体的链表。

 

DT_JMPREL包含了与PLT表相关的重定位信息,把它与主重定位表分离是为了支持懒绑定,以便让连接器在进程初始化时跳过这些重定位,而在运行时发生外部过程调用时通过PLT表调用连接器函数实现绑定。由于linker不支持懒绑定,所以对该表的重定位也需要提前到进程初始化时进行。

 

属性在重定位的过程中,linker会调用_do_lookupsoinfo结构体链表中的所有映像文件的符号表中查找该符号的实际地址,然后修改.rel.plt表项所指向的该符号在映像(位于.got段)中的地址。对.rel.plt段的重定位将解析所有外部过程引用(符号属性st_shndxSTN_UNDEF),完成与共享库的链接。

 

重定位的过程也是解析和绑定符号的过程,主要要解决的两个问题是:

1.  如何找出有哪些符号需要重定位。

2.  这些符号的重定位类型(R_ARM_GLOB_DAT,R_ARM_JUMP_SLOT…)及相应的重定位操作。

    

 

上图给出了linker进行符号绑定过程涉及到的主要数据对象及其关系,Dyn段中给出了所有与重定位有关的数据结构的组成分布。Rel代表了所有重定位表项。每个重定位表项对应一个Symtab表项和一个strtab表项。Symtabstrtab也一一对应,但strtab中只包含了字符串,并不能指向其它的表。Hash表与symtab表中的索引项相对应,通过它可以加快符号查找的速度。同一个符号在“对象层可能出现多次。

 

       Linker中符号的绑定过程大致如下:

首先在reloc_library中队重定位节的每个Rel表项的rel->r_info成员依次进行解析,得到该重定位符号在本地符号表中的索引,该符号的重定位类型以及应进行修改的位置。根据符号索引在字符串表中找到相应的符号字符串,调用_do_lookup函数在soinfo链表对应的共享库中查找该符号。

 

_do_lookup首先会调用_do_lookup_in_so在目标映像本地的符号表中查找该符号,这个过程会解析出本地的重定位符号,对于未定义的外部符号,_do_lookup_in_so会返回0,然后

_do_lookup开始进入一个for循环,遍历整个soinfo链表。为每个soinfo调用一次_do_lookup_in_so

 

_do_lookup_in_so首先会调用elfhash计算出未定义字符串的hash值,将该值作为参数传递给_elf_lookup,它将最终返回符号对应的值。

 

程序中的参数hash是针对目标符号字符串计算出的hash值,bucket[hash % si->nbucket]

对应于符号表中的一个索引,根据这个索引找到相应的符号,与目标符号比较,相同则返回该符号的值(s->st_shndx == 0除外,说明该符号不在本文件定义),否则继续查找,n = si->chain[n]将给出相同hash值的另一个符号索引。

       符号若找到,其对应的地址被返回给reloc_library.reloc_library会根据之前得到的重定位类型,用该值进行相应的重定位操作。

 

 

整个加载与链接的过程通过link_image递归进行,最终所有相关映像文件均会被加载并连接,为避免递归式的加载与重定位过程导致进程启动的时间开销过大,可以使用mklibs工具控制共享库的数量。

可执行映像的重定位完成后link_image之后会调用mprotect将代码段的权限改回可读可执行。

 

然后调用call_destructors,执行映像的初始化队列,最后返回映像的入口地址。Linker将直接跳入可执行映像并开始执行。

 

linker中定义了dl_unwind_find_exidx函数,该函数将通过dl.so导出给libc.so__gnu_Unwind_Find_exidx函数,该函数可以根据PC计数器的值返回相应共享库中指向ARM_EXIDX段的指针(位于soinfo结构中),该段用于栈退回(stack unwinding)机制,确保C++在异常被抛出、捕获并处理后,所有生命期已结束的对象都会被正确地析构,它们所占用的空间会被正确地回收。可执行映像及共享库的soinfo结构用于栈退回的数据结构分别在link_imageload_library中被赋值。

Unload_library用于卸载指定的共享库并卸载其依赖库中可以被卸载的库。

       如果采用显式调用的方法动态链接使用共享库的例程,应用程序需要使用linker提供的外部接口,libdl.so中包含了所有这些接口,链接时在命令行加入-ldl,这样可执行映像的依赖库中将只包含libdl.sodlopen是加载共享库的接口,它会调用find_library找到并加载共享库。dlsym会调用_do_lookup返回符号地址,dlerror用于错误检查,dlclose调用unload_library动态卸载共享库(进程退出时不会自动卸载不用的共享库)。用户也可以通过系统调用sys_uselib在核心态加载共享库,但该函数只支持固定地址加载。

       生成共享库的基本方法与linux相同。在编译链接时在命令行加入

 -shared 和 –fPICandroid共享库链接脚本为armelf.xcs。例如:

 

      在编译动态链接的可执行文件时使用--dynamic-linker ,-nostdlib, -rpath ,–L…. 指定解释器,共享库及搜索路径。例如:

 

      另外有两个特别的工具mklibsapriorimklib可用于查找并复制程序用到的最小的共享库集,apriori可以预先为若干共享库确定加载地址,并为有依赖关系的共享库做静态重定位和连接,解释器会在共享库加载时(see load_library)调用is_prelinked查看该库是否时prelink的并在alloc_mem_region中检查目的地址是否被占用。

Reference

[1]  漫谈兼容内核之八: ELF映像的装入 毛德操

[2]  Linkers and loaders.

[3]  How to write shared libraries.

[4]  C++异常机制的实现方式和开销分析

 

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值