Android Linker詳解
簡介
接上篇Linker源碼詳解(一),本文繼續來分析Linker的鏈接過程。為了更好的理解Unidbg的原理,我們需要了解很多細節。雖然一個模擬二進制執行框架的弊端很多,但也是未來二進制分析的一個很好的思路。
上篇文章我們講解了Linker的裝載,將So文件按PT_LOAD段的指示來將So加載到內存,那么我們這篇文章就來分析一下加載完之后又干了什么呢?
So的鏈接
http://androidxref.com/4.4.4_r1/xref/bionic/linker/linker.cpp#702
static soinfo* load_library(const char* name) { //... ElfReader elf_reader(name, fd); if (!elf_reader.Load()) { return NULL; } const char* bname = strrchr(name, '/'); soinfo* si = soinfo_alloc(bname ? bname + 1 : name); if (si == NULL) { return NULL; } si->base = elf_reader.load_start(); si->size = elf_reader.load_size(); si->load_bias = elf_reader.load_bias(); si->flags = 0; si->entry = 0; si->dynamic = NULL; si->phnum = elf_reader.phdr_count(); si->phdr = elf_reader.loaded_phdr(); return si;}
上篇我們進入了elf_reader.Load()函數,閱讀了Linker的裝載源碼,當裝載結束后,對soinfo結構體進行賦值(So文件的頭信息/裝載的結果),并插入到鏈表,接著我們回到上層函數繼續看。
http://androidxref.com/4.4.4_r1/xref/bionic/linker/linker.cpp#751
static soinfo* find_library_internal(const char* name) { //... si = load_library(name); if (si == NULL) { return NULL; } // At this point we know that whatever is loaded @ base is a valid ELF // shared library whose segments are properly mapped in. TRACE("[ init_library base=0x%08x sz=0x%08x name='%s' ]", si->base, si->size, si->name); if (!soinfo_link_image(si)) { munmap(reinterpret_cast<void*>(si->base), si->size); soinfo_free(si); return NULL; } return si;}
我們從上面這個函數中看到,當調用了load_library函數之后,又調用了soinfo_link_image這個函數。這個函數也就是我們今天分析的一個主要入口--鏈接。
下面的這個函數很長,我給大家把不相關的代碼去掉,大家先通過注釋來看一遍這個函數在干什么。
http://androidxref.com/4.4.4_r1/xref/bionic/linker/linker.cpp#1303
static bool soinfo_link_image(soinfo* si) { //拿到地址、段表指針、段表數 Elf32_Addr base = si->load_bias; const Elf32_Phdr *phdr = si->phdr; int phnum = si->phnum; //... size_t dynamic_count; Elf32_Word dynamic_flags; //這個函數很簡單,就是遍歷段表,找到類型為PT_DYNAMIC的段 phdr_table_get_dynamic_section(phdr, phnum, base, &si->dynamic, &dynamic_count, &dynamic_flags); if (si->dynamic == NULL) { if (!relocating_linker) { DL_ERR("missing PT_DYNAMIC in \"%s\"", si->name); } return false; } #ifdef ANDROID_ARM_LINKER //異常相關,有興趣的同學可以看看 (void) phdr_table_get_arm_exidx(phdr, phnum, base, &si->ARM_exidx, &si->ARM_exidx_count);#endif //上面我們解析到了Dynamic段的地址跟數量,下面就開始遍歷Dynamic信息 uint32_t needed_count = 0; //DT_NULL表示結束 for (Elf32_Dyn* d = si->dynamic; d->d_tag != DT_NULL; ++d) { DEBUG("d = %p, d[0](tag) = 0x%08x d[1](val) = 0x%08x", d, d->d_tag, d->d_un.d_val); switch(d->d_tag){ case DT_HASH: //哈希表 si->nbucket = ((unsigned *) (base + d->d_un.d_ptr))[0]; si->nchain = ((unsigned *) (base + d->d_un.d_ptr))[1]; si->bucket = (unsigned *) (base + d->d_un.d_ptr + 8); si->chain = (unsigned *) (base + d->d_un.d_ptr + 8 + si->nbucket * 4); break; case DT_STRTAB: //字符串表 si->strtab = (const char *) (base + d->d_un.d_ptr); break; case DT_SYMTAB: //符號表 si->symtab = (Elf32_Sym *) (base + d->d_un.d_ptr); break; case DT_PLTREL: //未處理 if (d->d_un.d_val != DT_REL) { DL_ERR("unsupported DT_RELA in \"%s\"", si->name); return false; } break; case DT_JMPREL: //PLT重定位表 si->plt_rel = (Elf32_Rel*) (base + d->d_un.d_ptr); break; case DT_PLTRELSZ: //PLT重定位表大小 si->plt_rel_count = d->d_un.d_val / sizeof(Elf32_Rel); break; case DT_REL: //重定位表 si->rel = (Elf32_Rel*) (base + d->d_un.d_ptr); break; case DT_RELSZ: //重定位表大小 si->rel_count = d->d_un.d_val / sizeof(Elf32_Rel); break; case DT_PLTGOT: //GOT全局偏移表,跟PLT延時綁定相關,此處未處理,在Unidbg中也沒有處理此項 si->plt_got = (unsigned *)(base + d->d_un.d_ptr); break; case DT_DEBUG: //調試相關, Unidbg未處理,不必理會 if ((dynamic_flags & PF_W) != 0) { d->d_un.d_val = (int) &_r_debug; } break; case DT_RELA: //RELA表跟REL表在Unidbg中的處理方案是相同的,這兩個值有哪個就用哪個,RELA只是比REL表多了一個adden常量 DL_ERR("unsupported DT_RELA in \"%s\"", si->name); return false; case DT_INIT: //初始化函數 si->init_func = reinterpret_cast(base + d->d_un.d_ptr); DEBUG("%s constructors (DT_INIT) found at %p", si->name, si->init_func); break; case DT_FINI: //析構函數 si->fini_func = reinterpret_cast(base + d->d_un.d_ptr); DEBUG("%s destructors (DT_FINI) found at %p", si->name, si->fini_func); break; case DT_INIT_ARRAY: //init.array 初始化函數列表,后面我們會看到這些初始化函數的調用順序 si->init_array = reinterpret_cast(base + d->d_un.d_ptr); DEBUG("%s constructors (DT_INIT_ARRAY) found at %p", si->name, si->init_array); break; case DT_INIT_ARRAYSZ: //init.array 大小 si->init_array_count = ((unsigned)d->d_un.d_val) / sizeof(Elf32_Addr); break; case DT_FINI_ARRAY: //析構函數列表 si->fini_array = reinterpret_cast(base + d->d_un.d_ptr); DEBUG("%s destructors (DT_FINI_ARRAY) found at %p", si->name, si->fini_array); break; case DT_FINI_ARRAYSZ: //fini.array 大小 si->fini_array_count = ((unsigned)d->d_un.d_val) / sizeof(Elf32_Addr); break; case DT_PREINIT_ARRAY: //也是初始化函數,但是跟init.array不同,這個段大多只出現在可執行文件中,在So中我選擇了忽略 si->preinit_array = reinterpret_cast(base + d->d_un.d_ptr); DEBUG("%s constructors (DT_PREINIT_ARRAY) found at %p", si->name, si->preinit_array); break; case DT_PREINIT_ARRAYSZ: //preinit 列表大小 si->preinit_array_count = ((unsigned)d->d_un.d_val) / sizeof(Elf32_Addr); break; case DT_TEXTREL: si->has_text_relocations = true; break; case DT_SYMBOLIC: si->has_DT_SYMBOLIC = true; break; case DT_NEEDED: //當前So的依賴 ++needed_count; break;#if defined DT_FLAGS // TODO: why is DT_FLAGS not defined? case DT_FLAGS: if (d->d_un.d_val & DF_TEXTREL) { si->has_text_relocations = true; } if (d->d_un.d_val & DF_SYMBOLIC) { si->has_DT_SYMBOLIC = true; } break;#endif } } //... Sanity checks. //至此,Dynamic段的信息就解析完畢了,其中想表達的信息也被處理后放到了soinfo中,后面直接就可以拿來用了 // 開辟依賴庫的soinfo空間,準備處理依賴 soinfo** needed = (soinfo**) alloca((1 + needed_count) * sizeof(soinfo*)); soinfo** pneeded = needed; //再次遍歷Dynamic段 for (Elf32_Dyn* d = si->dynamic; d->d_tag != DT_NULL; ++d) { if (d->d_tag == DT_NEEDED) { //查找DT_NEEDED項 const char* library_name = si->strtab + d->d_un.d_val; DEBUG("%s needs %s", si->name, library_name); //進行依賴處理,跟加載so一樣的路線,還是已加載直接返回,未加載進行查找加載 soinfo* lsi = find_library(library_name); if (lsi == NULL) { strlcpy(tmp_err_buf, linker_get_error_buffer(), sizeof(tmp_err_buf)); DL_ERR("could not load library \"%s\" needed by \"%s\"; caused by %s", library_name, si->name, tmp_err_buf); return false; } *pneeded++ = lsi; } } *pneeded = NULL; //至此依賴庫也已經加載完畢 //處理重定位 if (si->plt_rel != NULL) { DEBUG("[ relocating %s plt ]", si->name ); if (soinfo_relocate(si, si->plt_rel, si->plt_rel_count, needed)) { return false; } } if (si->rel != NULL) { DEBUG("[ relocating %s ]", si->name ); if (soinfo_relocate(si, si->rel, si->rel_count, needed)) { return false; } } //設置soinfo的LINKED標志,表示已進行鏈接 si->flags |= FLAG_LINKED; DEBUG("[ finished linking %s ]", si->name); //... return true;}
上面的函數雖然很長,但是它想表達的意思很簡單,我們再來回顧下它干了什么事情:
- 解析Dynamic段信息
- 處理依賴
- 準備進行重定位
So重定位
下面我們就來分析它的soinfo_relocate函數,我們看到它調用了兩次,只不過入參不同,分別是我們的重定位表和PLT重定位表。
http://androidxref.com/4.4.4_r1/xref/bionic/linker/linker.cpp#848
static int soinfo_relocate(soinfo* si, Elf32_Rel* rel, unsigned count, soinfo* needed[]){ //拿到符號表和字符串表,定義一些變量 Elf32_Sym* symtab = si->symtab; const char* strtab = si->strtab; Elf32_Sym* s; Elf32_Rel* start = rel; soinfo* lsi; //遍歷重定位表 for (size_t idx = 0; idx < count; ++idx, ++rel) { //拿到重定位類型 unsigned type = ELF32_R_TYPE(rel->r_info); //拿到重定位符號 unsigned sym = ELF32_R_SYM(rel->r_info); //計算需要重定位的地址 Elf32_Addr reloc = static_cast(rel->r_offset + si->load_bias); Elf32_Addr sym_addr = 0; char* sym_name = NULL; DEBUG("Processing '%s' relocation at index %d", si->name, idx); if (type == 0) { // R_*_NONE continue; } if (sym != 0) { //如果sym不為0,說明重定位需要用到符號,先來找符號,拿到符號名 sym_name = (char *)(strtab + symtab[sym].st_name); //下面這個函數大家有興趣的可以看一下,就是根據符號名來從依賴so中查找所需要的符號 s = soinfo_do_lookup(si, sym_name, &lsi, needed); if (s == NULL) { //如果沒找到,就用本身So的符號 s = &symtab[sym]; if (ELF32_ST_BIND(s->st_info) != STB_WEAK) { DL_ERR("cannot locate symbol \"%s\" referenced by \"%s\"...", sym_name, si->name); return -1; } switch (type) { //下面是如果符號不為外部符號,就只能為以下幾種類型#if defined(ANDROID_ARM_LINKER) case R_ARM_JUMP_SLOT: case R_ARM_GLOB_DAT: case R_ARM_ABS32: case R_ARM_RELATIVE: /* Don't care. */#endif /* ANDROID_*_LINKER */ /* sym_addr was initialized to be zero above or relocation code below does not care about value of sym_addr. No need to do anything. */ break; #if defined(ANDROID_ARM_LINKER) case R_ARM_COPY: /* Fall through. Can't really copy if weak symbol is not found in run-time. */#endif /* ANDROID_ARM_LINKER */ default: DL_ERR("unknown weak reloc type %d @ %p (%d)", type, rel, (int) (rel - start)); return -1; } } else { //如果我們找到了外部符號,取到外部符號的地址 sym_addr = static_cast(s->st_value + lsi->load_bias); } count_relocation(kRelocSymbol); } else { //如果sym為0,就說明當前重定位用不到符號 s = NULL; } //下面根據重定位類型來處理重定位 switch(type){#if defined(ANDROID_ARM_LINKER) case R_ARM_JUMP_SLOT: count_relocation(kRelocAbsolute); MARK(rel->r_offset); TRACE_TYPE(RELO, "RELO JMP_SLOT %08x <- %08x %s", reloc, sym_addr, sym_name); //直接將需要重定位的地方,寫入獲取到的符號地址 *reinterpret_cast(reloc) = sym_addr; break; case R_ARM_GLOB_DAT: count_relocation(kRelocAbsolute); MARK(rel->r_offset); TRACE_TYPE(RELO, "RELO GLOB_DAT %08x <- %08x %s", reloc, sym_addr, sym_name); //直接將需要重定位的地方,寫入獲取到的符號地址,與R_ARM_JUMP_SLOT相同 *reinterpret_cast(reloc) = sym_addr; break; case R_ARM_ABS32: count_relocation(kRelocAbsolute); MARK(rel->r_offset); TRACE_TYPE(RELO, "RELO ABS %08x <- %08x %s", reloc, sym_addr, sym_name); //先讀出需要重定位地方的數據,將其和符號地址相加,寫入需要重定位的地方 *reinterpret_cast(reloc) += sym_addr; break; case R_ARM_REL32: count_relocation(kRelocRelative); MARK(rel->r_offset); TRACE_TYPE(RELO, "RELO REL32 %08x <- %08x - %08x %s", reloc, sym_addr, rel->r_offset, sym_name); //先讀出需要重定位地方的數據,將其和符號地址相加,再與重定位的地址相減,重定位的寫入需要重定位的地方。此處Unidbg并未處理,也可忽略,應該是用不到的 *reinterpret_cast(reloc) += sym_addr - rel->r_offset; break;#endif /* ANDROID_*_LINKER */ #if defined(ANDROID_ARM_LINKER) case R_ARM_RELATIVE:#endif /* ANDROID_*_LINKER */ count_relocation(kRelocRelative); MARK(rel->r_offset); if (sym) { DL_ERR("odd RELATIVE form..."); return -1; } TRACE_TYPE(RELO, "RELO RELATIVE %08x <- +%08x", reloc, si->base); //先讀出需要重定位地方的數據,將其和So的基址相加,寫入需要重定位的地方 *reinterpret_cast(reloc) += si->base; break; #ifdef ANDROID_ARM_LINKER case R_ARM_COPY: //.. 進行了一些錯誤處理 break;#endif /* ANDROID_ARM_LINKER */ default: DL_ERR("unknown reloc type %d @ %p (%d)", type, rel, (int) (rel - start)); return -1; } } return 0;}
上面這個函數就是在處理重定位相關的信息了,我們看到從Dynamic段中拿到的跟重定位相關的表,會經過這個函數來處理,將So本身的地址引用進行重定位,使其可以正常運行。其實在32位So中,需要處理的重定位類型并不是很多,就4種類型需要處理,而且還有兩種處理方式相同。
現在So就重定位完成了,現在So已經就可以跑起來了,下面我們就來看看從Dynamic段中拿到的各種初始化函數是怎么處理的,還記得吧。
我們回到do_dlopen函數。
http://androidxref.com/4.4.4_r1/xref/bionic/linker/linker.cpp#823
soinfo* do_dlopen(const char* name, int flags) { if ((flags & ~(RTLD_NOW|RTLD_LAZY|RTLD_LOCAL|RTLD_GLOBAL)) != 0) { DL_ERR("invalid flags to dlopen: %x", flags); return NULL; } set_soinfo_pool_protection(PROT_READ | PROT_WRITE); soinfo* si = find_library(name); if (si != NULL) { si->CallConstructors(); } set_soinfo_pool_protection(PROT_READ); return si;}
此時我們的find_library函數已經處理完了,So已經被裝載且鏈接過了,最后一步它調用了soinfo的CallConstructors函數,我們來看看這個函數處理了什么。
http://androidxref.com/4.4.4_r1/xref/bionic/linker/linker.cpp#1192
void soinfo::CallConstructors() { if (constructors_called) { return; } constructors_called = true; if ((flags & FLAG_EXE) == 0 && preinit_array != NULL) { // The GNU dynamic linker silently ignores these, but we warn the developer. PRINT("\"%s\": ignoring %d-entry DT_PREINIT_ARRAY in shared library!", name, preinit_array_count); } //如果Dynamic段不為空,先處理依賴庫的初始化 if (dynamic != NULL) { for (Elf32_Dyn* d = dynamic; d->d_tag != DT_NULL; ++d) { if (d->d_tag == DT_NEEDED) { const char* library_name = strtab + d->d_un.d_val; TRACE("\"%s\": calling constructors in DT_NEEDED \"%s\"", name, library_name); find_loaded_library(library_name)->CallConstructors(); } } } TRACE("\"%s\": calling constructors", name); //我們來看下面一句英文注釋,非常重要。他說如果DT_INIT和DT_INIT_ARRAY都存在,DT_INIT應該在DT_INIT_ARRAY之前被調用 // DT_INIT should be called before DT_INIT_ARRAY if both are present. //下面就是在調用兩者,CallArray只是在循環調用CallFunction,我們看一下CallFunction CallFunction("DT_INIT", init_func); CallArray("DT_INIT_ARRAY", init_array, init_array_count, false);}
http://androidxref.com/4.4.4_r1/xref/bionic/linker/linker.cpp#1172
void soinfo::CallFunction(const char* function_name UNUSED, linker_function_t function) { if (function == NULL || reinterpret_cast<uintptr_t>(function) == static_cast<uintptr_t>(-1)) { return; } TRACE("[ Calling %s @ %p for '%s' ]", function_name, function, name); //在這里被調用了,其他沒啥好說的 function(); TRACE("[ Done calling %s @ %p for '%s' ]", function_name, function, name); // The function may have called dlopen(3) or dlclose(3), so we need to ensure our data structures // are still writable. This happens with our debug malloc (see http://b/7941716). set_soinfo_pool_protection(PROT_READ | PROT_WRITE);}
至此,Linker就分析結束了。
總結
我們在最后說一個Unidbg細節的bug,但是現在已經被修復了,就是作為一個擴展吧。我們來看下面一段Unidbg加載So的代碼。
if (elfFile.file_type == ElfFile.FT_DYN) { // not executable int init = dynamicStructure.getInit(); if (init != 0) { initFunctionList.add(new LinuxInitFunction(load_base, soName, init)); //new LinuxInitFunction(load_base, soName, init).call(emulator); } int initArraySize = dynamicStructure.getInitArraySize(); int count = initArraySize / emulator.getPointerSize(); if (count > 0) { Pointer pointer = UnidbgPointer.pointer(emulator, load_base + dynamicStructure.getInitArrayOffset()); if (pointer == null) { throw new IllegalStateException("DT_INIT_ARRAY is null"); } for (int i = 0; i < count; i++) { Pointer func = pointer.getPointer((long) i * emulator.getPointerSize()); if (func != null) { initFunctionList.add(new AbsoluteInitFunction(load_base, soName, ((UnidbgPointer) func).peer)); } } }}
如果我們細心的閱讀Linker的源碼,就會發現Unidbg這里處理的是不恰當的。在本文的最后,我們看到了初始化函數的調用,是DT_INIT函數先被執行,后面再處理DT_INIT_ARRAY,而Unidbg這里就是將他們都添加到一個List,再一起調用。
這樣就會產生一個問題,在某些加殼的So中,它的DT_INIT_ARRAY是在DT_INIT函數執行之后,才會有值的(進行修復),所以按照Unidbg這個寫法就無法執行INIT_ARRAY或部分INIT_ARRAY無法執行。處理方法也很簡單,注釋在上面了,只需要讓DT_INIT先執行就可以了。