Android中的共享庫和可執行映像都默認採用ELF格式的文件,其基本格式以下: linux
每一個ELF文件的開始部分都包含一個ELF頭,其中包含了整個文件的基本信息,包括目標代碼的格式,體系結構,各程序頭或節頭的偏移和大小,組織結構和訪問權限等信息。 android
程序頭表包含了加載到內存中的各類段的索引及屬性信息,它將告訴加載器如何加載映像。每一個段中有包含了一個或幾個節區,每一個節區應是惟一的。不管是可執行程序仍是共享庫都包含如下幾個的節區: 數據結構
1. GOT表和PLT表: app
不一樣映像間的函數和數據引用都是經過它們實現的。GOT(全局偏移表)給出了映像中全部被引用符號(函數或變量)的值。每一個普通PLT表項至關於一個函數的樁函數(stub),支持懶綁定的狀況下,當發生對外部函數的調用時,程序會經過PLT表將控制交給動態鏈接器,後者解析出函數的絕對地址,修改GOT中相應的值,以後的調用將再也不須要鏈接器的綁定。因爲linker是不支持懶綁定的,因此在進程初始化時,動態連接器首先解析出外部過程引用的絕對地址,一次性的修改全部相應的GOT表項。對共享對象來講,因爲GOT,PLT節以及代碼段和數據段之間的相對位置是固定的,全部引用都是基於一個固定地址(GOT)的偏移量,因此實現了PIC代碼,重定位時只須要修改可寫段中的GOT表。而可執行程序在鏈接過程當中則可能發生對不可寫段的修改。若是隻讀段和可寫段不是以固定的相對位置加載的,那麼在重定位是還須要修改全部指向GOT的指針。 ionic
2. dynamic節: 函數
與重定位有關的基本目錄結構,例如: 工具
Dynamic section at offset 0x61014 contains 20 entries: ui
Tag Type Name/Value spa
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是共享庫的加載/連接器,也能夠稱爲解釋器(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結構體,它用於存放與映像文件有關的全部信息,這樣可使可執行映像與共享對象共享鏈接與重定位函數,後面的程序將經過soinfo的flags域判斷目標映像是共享庫仍是可執行程序。
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進行實際的加載操做,庫的加載地址均位於0x80000000到0x90000000之間(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_len,PFLAGS_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_REL和DT_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_lookup在soinfo結構體鏈表中的全部映像文件的符號表中查找該符號的實際地址,而後修改.rel.plt表項所指向的該符號在映像(位於.got段)中的地址。對.rel.plt段的重定位將解析全部外部過程引用(符號屬性st_shndx爲STN_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表項。Symtab和strtab也一一對應,但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.so。dlopen是加載共享庫的接口,它會調用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
另外有兩個特別的工具mklibs和apriori。mklib可用於查找並複製程序用到的最小的共享庫集,apriori能夠預先爲若干共享庫肯定加載地址,併爲有依賴關係的共享庫作靜態重定位和鏈接,解釋器會在共享庫加載時(see load_library)調用is_prelinked查看該庫是否時prelink的並在alloc_mem_region中檢查目的地址是否被佔用。
[1] 漫談兼容內核之八: ELF映像的裝入 毛德操
[2] Linkers and loaders.
[3] How to write shared libraries.
[4] C++異常機制的實現方式和開銷分析