android linker 淺析

Android 的加載/鏈接器linker 主要用於實現共享庫的加載與鏈接。它支持應用程序對庫函數的隱式和顯式調用。對於隱式調用,應用程序的編譯與靜態庫大致相同,只是在靜態鏈接的時候通過--dynamic-linker /system/bin/linker 指定動態鏈接器,(該信息將被存放在ELF文件的.interp節中,內核執行目標映像文件前將通過該信息加載並運行相應的解釋器程序linker.)並鏈接相應的共享庫。與ld.so不同的是,Linker目前沒有提供Lazy Binding機制,所有外部過程引用都在映像執行之前解析。對於顯式調用,可以同過linker中提供的接口dlopen,dlsym,dlerror和dlclose來動態加載和鏈接共享庫。

Android中的共享庫和可執行映像都默認採用ELF格式的文件,其基本格式如下:


       每個ELF文件的開始部分都包含一個ELF頭,其中包含了整個文件的基本信息,包括目標代碼的格式,體系結構,各程序頭或節頭的偏移和大小,組織結構和訪問權限等信息。

       程序頭表包含了加載到內存中的各種段的索引及屬性信息,它將告訴加載器如何加載映像。每個段中有包含了一個或幾個節區,每個節區應是唯一的。無論是可執行程序還是共享庫都包含以下幾個的節區:

1. GOT表和PLT表:

       不同映像間的函數和數據引用都是通過它們實現的。GOT(全局偏移表)給出了映像中所有被引用符號(函數或變量)的值。每個普通PLT表項相當於一個函數的樁函數(stub),支持懶綁定的情況下,當發生對外部函數的調用時,程序會通過PLT表將控制交給動態連接器,後者解析出函數的絕對地址,修改GOT中相應的值,之後的調用將不再需要連接器的綁定。由於linker是不支持懶綁定的,所以在進程初始化時,動態鏈接器首先解析出外部過程引用的絕對地址,一次性的修改所有相應的GOT表項。對共享對象來說,由於GOT,PLT節以及代碼段和數據段之間的相對位置是固定的,所有引用都是基於一個固定地址(GOT)的偏移量,所以實現了PIC代碼,重定位時只需要修改可寫段中的GOT表。而可執行程序在連接過程中則可能發生對不可寫段的修改。如果只讀段和可寫段不是以固定的相對位置加載的,那麼在重定位是還需要修改所有指向GOT的指針。   

                                       

2. dynamic節:

       與重定位有關的基本目錄結構,例如:

Dynamic section at offset 0x61014 contains 20 entries:

  Tag        Type                         Name/Value

 0x00000001 (NEEDED)                     Shared library: [libc.so.6]

 0x0000000c (INIT)                             0xb8a8

 0x0000000d (FINI)                           0x555c4

 0x00000004 (HASH)                       0x8128

 0x00000005 (STRTAB)                       0xa004

 0x00000006 (SYMTAB)                     0x8aa4

 0x0000000a (STRSZ)                         2902 (bytes)

 0x0000000b (SYMENT)                     16 (bytes)

 0x00000015 (DEBUG)                                0x0

 0x00000003 (PLTGOT)                     0x710dc

 0x00000002 (PLTRELSZ)                     2464 (bytes)

 0x00000014 (PLTREL)                       REL

 0x00000017 (JMPREL)                       0xaf08

 0x00000011 (REL)                           0xae98

 0x00000012 (RELSZ)                        112 (bytes)

 0x00000013 (RELENT)                      8 (bytes)

3. dynsym和dynstr節:

       與重定位有關的符號表和字符串表:

Symbol table '.dynsym' contains 69 entries:

   Num:    Value   Size  Type    Bind       Vis        Ndx   Name

     0:   00000000   0  NOTYPE  LOCAL   DEFAULT  UND

     ……

     6:  00002568    28  FUNC    GLOBAL  DEFAULT    7   __ashldi3

     7:  00000001    58  FUNC    GLOBAL  DEFAULT  UND  _ZNK7android7RefBase9decS

     8:  00000001    32  FUNC    GLOBAL  DEFAULT  UND  ioctl

     9:  00000001    18  FUNC    GLOBAL  DEFAULT  UND  _ZN7android7String8D1Ev

    10:  00000001    16  FUNC    GLOBAL  DEFAULT  UND  _ZNK7android8EventHub16ge

    11:  00000001    32  FUNC    GLOBAL  DEFAULT  UND  strerror

    12:  00003024     0  NOTYPE  GLOBAL  DEFAULT  ABS  __exidx_end

4. .rel.dyn和.rel.plt節:

       .rel.dyn節的表項對應了出外部過程調用的符號以外的所有重定位對象,.rel.plt則對應所有外部過程調用的重定位信息。每個重定位項記錄了符號的符號表索引,重定位的操作地址,重定位類型的信息(見3.3節)。重定位所在的節區往往與重定位類型有關,例如:

Relocation section '.rel.plt' at offset 0x2f08 contains 308 entries:

 Offset     Info      Type                  Sym.Value  Sym. Name

000710e8  00000116   R_ARM_JUMP_SLOT   0000b8d0   fileno

000710ec  00000216   R_ARM_JUMP_SLOT   0000b8dc   getpagesize

000710f0  00000316   R_ARM_JUMP_SLOT   0000b8e8   fputs

000710f4  00000416   R_ARM_JUMP_SLOT   0000b8f4   abort

000710f8  00000516   R_ARM_JUMP_SLOT   0000b900   __errno_location

 

Relocation section '.rel.dyn' at offset 0x2e98 contains 14 entries:

 Offset     Info      Type            Sym.Value  Sym. Name

000715b8  00001e15  R_ARM_GLOB_DAT    00071000   __fini_array_end

000715bc  00002f15  R_ARM_GLOB_DAT    00000000   __gmon_start__

000715c8  0000f515  R_ARM_GLOB_DAT    00071000   __fini_array_start

000715cc  00010015  R_ARM_GLOB_DAT    00071000   __init_array_end

000715d0  00012e15  R_ARM_GLOB_DAT    00071000   __init_array_start

00072a00  00002714  R_ARM_COPY         00072a00   __timezone

00072a04  00005514  R_ARM_COPY         00072a04   __daylight

 

       R_ARM_JUMP_SLOT和R_ARM_GLOB_DAT屬性的重定位地址一般位於GOT表,R_ARM_COPY和R_ARM_ABS32屬性的重定位一般位於.data節或.text節中。

Linker的加載與啓動

       Linker是共享庫的加載/鏈接器,也可以稱爲解釋器(interpreter)。共享庫以ELF文件的形式保存在文件系統中,核心的load_elf_binary會首先將其映像文件映射到內存,然後映射並執行其解釋器也就是linker的代碼。linker的代碼段是進程間共享的,但數據段爲各進程私有。

linker執行完後會自動跳轉到目標映像的入口地址。

       /*in sys_execve->do_execve->search_binary_handler->load_elf_binary*/

       elf_entry = load_elf_interp(&loc->interp_elf_ex,interpreter,&interp_map_addr, load_bias);

       ………..

       start_thread(regs, elf_entry, bprm->p);       //start to execute linker

       在android中,linker代碼的運行域由地址0xb0000100開始(see /bionic/linker/Android.mk),直接從_start開始執行。do_execve會預先將應用程序參數(argc,argv[],envc和envp[]還有一些"輔助向量(Auxiliary Vector)"等(see load_elf_binary>create_elf_tables))存放在分配好的用戶空間堆棧中,通過堆棧將這些參數和指針(位於linux_binprm結構體bprm中)傳遞給用戶空間的目標進程。

       Linker會提取出它所需要的信息,例如目標映像中程序頭表在用戶空間的地址以及應用程序入口等。

       /*in __linker_init()*/

               /* extract information passed from the kernel */

    while(vecs[0] != 0){

        switch(vecs[0]){

        case AT_PHDR:

            si->phdr = (Elf32_Phdr*) vecs[1];

            break;

        case AT_PHNUM:

            si->phnum = (int) vecs[1];

            break;

        case AT_ENTRY:

            si->entry = vecs[1];                /*entry of the executable image.*/

            break;

        }

        vecs += 2;

}

加載依賴的共享庫

Linker會首先調用__linker_init執行一段自舉代碼,完成其自身的初始化,初始化與目標映像相關的數據結構。Linker會首先調用alloc_info爲目標映像分配一個soinfo結構體,它用於存放與映像文件有關的所有信息,這樣可以使可執行映像與共享對象共享連接與重定位函數,後面的程序將通過soinfoflags域判斷目標映像是共享庫還是可執行程序。

    si = alloc_info(argv[0]);                  /*name of exe */

    if(si == 0) {

        exit(-1);

    }

       ………

    si->flags |= FLAG_EXE;                 /*exe not share library*/

與共享庫的鏈接操作通過函數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對齊,庫的代碼段和數據段都是頁對齊的。

    for(d = si->dynamic; *d; d += 2) {

          f(d[0] == DT_NEEDED){             //it ‘s a needed share library.

              soinfo *lsi = find_library(si->strtab + d[1]);  //get soinfo by name         

              if(lsi == 0) {

                   goto fail;

            }

        lsi->refcount++;                  //  Increment it’s referenced count

        }

    }

load_library的具體加載過程是:

1.       讀取共享庫的文件頭和程序頭表到指定的頁中。

2.       調用get_lib_extents分析ELF頭表,並獲取文件中的地址信息。如果該庫不是prelink的,則庫加載的起始地址爲零。同時計算出加載該庫所需總的內存空間大小。

3.       根據已獲得的起始地址和總空間大小,調用alloc_mem_region預先爲共享庫分配一段內存空間。這段空間是通過系統調用mmap實現的,其訪問屬性是PROT_READ | PROT_EXEC MAP_PRIVATE | MAP_ANONYMOUS,文件描述符爲-1,匿名私有的內存映射意味着,內核將爲該庫分配虛擬線性區,保留給後邊進行內存映射文件的操作。

           while(libbase < LIBLAST) {  //LIBLAST=0x90000000

        base = mmap((void*) libbase, sz, PROT_READ | PROT_EXEC,

                    MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

        if(((unsigned)base) == libbase) {

            return base;      

        }

        if(base != MAP_FAILED)

               munmap(base, sz);

        libbase += LIBINC;    // LIBINC = 0x00100000,再進行一次映射。

    }

4.       調用alloc_info爲該庫在共享庫鏈表中分配一個soinfo節點,初始化其數據   結構。

5.       調用load_segments將所有的PT_LOAD屬性的段加載至合適的地址空間,代碼段與數據段的相對位置與文件的運行域一致。  

       pbase = mmap(tmp, len, PFLAGS_TO_PROT(phdr->p_flags),MAP_PRIVATE |    MAP_FIXED, fd,phdr->p_offset & (~PAGE_MASK));

       如果該段是隻讀的,則核心將其映射致內存中唯一的拷貝,如果該段是可寫的,    NAP_PRIVATE意味着該段是寫時拷貝的,只有在寫操作時核心纔會將相應的    頁面拷貝至內存。由於需要對可執行映像中位於只讀段的代碼進行重定位,所       以調用mprotect    將只讀段的屬性暫時更改爲R/W/E的。 

if (si->flags & FLAG_EXE) {

       ……        

                    if (!(phdr->p_flags & PF_W)) {

                if ((unsigned)pbase < si->wrprotect_start)

                    si->wrprotect_start = (unsigned)pbase;

                if (((unsigned)pbase + len) > si->wrprotect_end)

                    si->wrprotect_end = (unsigned)pbase + len;

                         mprotect(pbase, len,PFLAGS_TO_PROT(phdr->p_flags) |                                                  PROT_WRITE);

            }

                    ……

             }

                    共享庫代碼由於是位置無關的,所以只需要可寫段中的.got段(COW的),                 所以不需要只讀段的內存保護。

                    如果bss段的區間中包含頁邊界,則對超出的部分作另外的匿名映射,                          private anonymous mappings意味着當這些內存映射被取消映射時,內存會真的                     將其釋放給系統。

       if (tmp < (base + phdr->p_vaddr + phdr->p_memsz)) {

       extra_base = mmap((void *)tmp, extra_lenPFLAGS_TO_PROT(phdr->p_flags), MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS, -1, 0);

加載完成後load_library會調用init_library進行庫的初始化操作,init_library又會調用link_image,鏈接共享庫的映像文件,共享對象的鏈接與重定位過程參考3.3節。

可執行映像的重定位

加載目標映像的所有依賴庫後,link_image調用reloc_library函數根據DYNAMIC段中的DT_RELDT_JMPREL屬性節區對目標映像進行重訂位,兩種屬性的節區都是Elf32_Rel結構體的鏈表。

typedef struct elf32_rel {

  Elf32_Addr       r_offset;           //重定位偏移量,以目標文件加載地址爲基準

  Elf32_Word      r_info;                    //包含了重定位類型和在符號表中的索引    

} Elf32_Rel;

DT_JMPREL包含了與PLT表相關的重定位信息,把它與主重定位表分離是爲了支持懶綁定,以便讓連接器在進程初始化時跳過這些重定位,而在運行時發生外部過程調用時通過PLT表調用連接器函數實現綁定。由於linker不支持懶綁定,所以對該表的重定位也需要提前到進程初始化時進行。

/*in myandroid/build/core/armelf.xsc*/

  .rel.dyn        :

    {

      *(.rel.init)

      *(.rel.text .rel.text.* .rel.gnu.linkonce.t.*)

      *(.rel.fini)

      *(.rel.rodata .rel.rodata.* .rel.gnu.linkonce.r.*)

      *(.rel.data.rel.ro* .rel.gnu.linkonce.d.rel.ro.*)

      *(.rel.data .rel.data.* .rel.gnu.linkonce.d.*)

      *(.rel.tdata .rel.tdata.* .rel.gnu.linkonce.td.*)

      *(.rel.tbss .rel.tbss.* .rel.gnu.linkonce.tb.*)

      *(.rel.ctors)

      *(.rel.dtors)

      *(.rel.got)

      *(.rel.bss .rel.bss.* .rel.gnu.linkonce.b.*)

    }

  .rel.plt        : { *(.rel.plt) }

屬性在重定位的過程中,linker會調用_do_lookupsoinfo結構體鏈表中的所有映像文件的符號表中查找該符號的實際地址,然後修改.rel.plt表項所指向的該符號在映像(位於.got段)中的地址。對.rel.plt段的重定位將解析所有外部過程引用(符號屬性st_shndxSTN_UNDEF),完成與共享庫的鏈接。

/*in link_image()*/

    if(si->plt_rel) {                          /*it ‘s a .rel.plt (DT_JMPREL) section*/

      if(reloc_library(si, si->plt_rel, si->plt_rel_count))//binding all the external func

            goto fail;

    }

    if(si->rel) {                             /*it ‘s a .rel.dyn (DT_REL)section*/

        if(reloc_library(si, si->rel, si->rel_count))

            goto fail;

    }

重定位的過程也是解析和綁定符號的過程,主要要解決的兩個問題是:

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鏈表對應的共享庫中查找該符號。

   /*in reloc_library ()*/

    for (idx = 0; idx < count; ++idx) {          //count = si->plt_rel_count.

        unsigned type = ELF32_R_TYPE(rel->r_info);

        unsigned sym = ELF32_R_SYM(rel->r_info);

        unsigned reloc = (unsigned)(rel->r_offset + si->base);

        if(sym != 0) {

            /*search symbol within solist*/

                    s = _do_lookup(si, strtab + symtab[sym].st_name, &base);

            if ((s->st_shndx == SHN_UNDEF) && (s->st_value != 0)) {return -1;}

}

_do_lookup首先會調用_do_lookup_in_so在目標映像本地的符號表中查找該符號,這個過程會解析出本地的重定位符號,對於未定義的外部符號,_do_lookup_in_so會返回0,然後

_do_lookup開始進入一個for循環,遍歷整個soinfo鏈表。爲每個soinfo調用一次_do_lookup_in_so

       /* in _do_lookup()*/

    for(si = solist; (s == NULL) && (si != NULL); si = si->next)

    {

        if((si->flags & FLAG_ERROR) || (si == user_si))

            continue;

        s = _do_lookup_in_so(si, name, &elf_hash);

        if (s != NULL) {

            *base = si->base;

            break;

        }

}

_do_lookup_in_so首先會調用elfhash計算出未定義字符串的hash值,將該值作爲參數傳遞給_elf_lookup,它將最終返回符號對應的值。

static Elf32_Sym *_elf_lookup(soinfo *si, unsigned hash, const char *name)

{

    Elf32_Sym *s;

    Elf32_Sym *symtab = si->symtab;

    const char *strtab = si->strtab;

    unsigned n;

    n = hash % si->nbucket;

    for(n = si->bucket[hash % si->nbucket]; n != 0; n = si->chain[n]){

        s = symtab + n;

        if(strcmp(strtab + s->st_name, name)) continue; /* only concern ourselves with                    global symbols */     

        switch(ELF32_ST_BIND(s->st_info)){

        case STB_GLOBAL:

            if(s->st_shndx == 0) continue;  /* no section == undefined */

        case STB_WEAK:

            return s;

        }

    }

    return 0;

}

程序中的參數hash是針對目標符號字符串計算出的hash值,bucket[hash % si->nbucket]

對應於符號表中的一個索引,根據這個索引找到相應的符號,與目標符號比較,相同則返回該符號的值(s->st_shndx == 0除外,說明該符號不在本文件定義),否則繼續查找,n = si->chain[n]將給出相同hash值的另一個符號索引。

       符號若找到,其對應的地址被返回給reloc_library.reloc_library會根據之前得到的重定位類型,用該值進行相應的重定位操作。

       /*in reloc_library*/

                          ……..

            sym_addr = (unsigned)(s->st_value + base);   /*get the actual address.*/

            sym_name = (char *)(strtab + symtab[sym].st_name);

        }

        switch(type){

        case R_ARM_JUMP_SLOT:

        case R_ARM_GLOB_DAT:

        case R_ARM_ABS32:

            *((unsigned*)reloc) = sym_addr;

            break;

        case R_ARM_RELATIVE:

            if(sym){return -1; }

            *((unsigned*)reloc) += si->base;

            break;

        case R_ARM_COPY:

            memcpy((void*)reloc, (void*)sym_addr, s->st_size);  /*object in RW seg*/

            break;

        default: return -1;

        }

        rel++;

    }

    return 0;

}

 

整個加載與鏈接的過程通過link_image遞歸進行,最終所有相關映像文件均會被加載並連接,爲避免遞歸式的加載與重定位過程導致進程啓動的時間開銷過大,可以使用mklibs工具控制共享庫的數量。

可執行映像的重定位完成後link_image之後會調用mprotect將代碼段的權限改回可讀可執行。

    if (si->wrprotect_start != 0xffffffff && si->wrprotect_end != 0) {

        mprotect((void *)si->wrprotect_start, si->wrprotect_end -                                        si->wrprotect_start, PROT_READ | PROT_EXEC);

    }

然後調用call_destructors,執行映像的初始化隊列,最後返回映像的入口地址。Linker將直接跳入可執行映像並開始執行。

/* begin.S */

_start:

      ……….

      bl    __linker_init   /* linker init returns the _entry address in the main image */

      mov pc, r0

 

linker中定義了dl_unwind_find_exidx函數,該函數將通過dl.so導出給libc.so的__gnu_Unwind_Find_exidx函數,該函數可以根據PC計數器的值返回相應共享庫中指向ARM_EXIDX段的指針(位於soinfo結構中),該段用於棧退回(stack unwinding)機制,確保C++在異常被拋出、捕獲並處理後,所有生命期已結束的對象都會被正確地析構,它們所佔用的空間會被正確地回收。可執行映像及共享庫的soinfo結構用於棧退回的數據結構分別在link_image和load_library中被賦值。

Unload_library用於卸載指定的共享庫並卸載其依賴庫中可以被卸載的庫。

       如果採用顯式調用的方法動態鏈接使用共享庫的例程,應用程序需要使用linker提供的外部接口,libdl.so中包含了所有這些接口,鏈接時在命令行加入-ldl,這樣可執行映像的依賴庫中將只包含libdl.sodlopen是加載共享庫的接口,它會調用find_library找到並加載共享庫。dlsym會調用_do_lookup返回符號地址,dlerror用於錯誤檢查,dlclose調用unload_library動態卸載共享庫(進程退出時不會自動卸載不用的共享庫)。用戶也可以通過系統調用sys_uselib在覈心態加載共享庫,但該函數只支持固定地址加載。

       生成共享庫的基本方法與linux相同。在編譯鏈接時在命令行加入

 -shared 和 –fPIC,android共享庫鏈接腳本爲armelf.xcs。例如:

       $ arm-none-linux-gnueabi-gcc -fpic -nostdlib -Wl,-T,armelf.xsc, -shared, -Bsymbolic -o libhello.so  hello.c     /*create shared object*/

      在編譯動態鏈接的可執行文件時使用--dynamic-linker ,-nostdlib, -rpath ,–L…. 指定解釋器,共享庫及搜索路徑。例如:

      $ arm-none-linux-gnueabi-gcc -c start.c
       $ arm-none-linux-gnueabi-gcc -c main.c
      $ arm-none-linux-gnueabi-ld --dynamic-linker /system/bin/linker -nostdlib /
   -rpath /system/lib -rpath ~/tmp/android/system/lib -L . /

      -L ~/tmp/android/system/lib -lc -lhello -o hello2 start.o main.o

      另外有兩個特別的工具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++異常機制的實現方式和開銷分析

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章