Android Linker學習筆記
0x00 知識預備
Linker是Android系統動態庫so的載入器/連結器,要想輕鬆地理解Android linker的執行機制,我們需要先熟悉ELF的檔案結構,再瞭解ELF檔案的裝入/啟動,最後學習Linker的載入和啟動原理。
鑑於ELF檔案結構網上有很多資料,這裡就不做累述了。
0x01 so的載入和啟動
我們知道如果一個APP需要使用某一共享庫so的話,它會在JAVA層宣告程式碼:
#!java
Static{
System.loadLibrary(“name”);
}
此程式碼完成library的載入工作。翻看system.loadLibrary的原始碼,可以發現:
System.loadLibrary也是一個native方法,它的呼叫的過程是:
#!cpp
Dalvik/vm/native/java_lang_Runtime.cpp:
Dalvik_java_lang_Runtime_nativeLoad ->Dalvik/vm/Native.cpp:dvmLoadNativeCode
dvmLoadNativeCode
開啟函式dvmLoadNativeCode,可以找到以下程式碼:
#!bash
……..
handle = dlopen(pathName, RTLD_LAZY); //獲得指定庫檔案的控制程式碼,這個handle是soinfo*
//這個庫檔案就是System.loadLibrary(pathName)傳遞的引數
…..
vonLoad = dlsym(handle, "JNI_OnLoad"); //獲取該檔案的JNI_OnLoad函式的地址
if (vonLoad == NULL) { //如果找不到JNI_OnLoad,就說明這是用javah風格的程式碼了,那麼就推遲解析
LOGD("No JNI_OnLoad found in %s %p, skipping init",pathName, classLoader); //這句話我們在logcat中經常看見!
}else{
….
}
從上面的程式碼可以看出Android系統載入共享庫的關鍵程式碼為dlopen函式。這個dlopen函式的程式碼在bionic/linker/dlfcn.c中:
#!cpp
void* dlopen(const char* filename, int flags) {
ScopedPthreadMutexLocker locker(&gDlMutex);
soinfo* result = do_dlopen(filename, flags);
if (result == NULL) {
__bionic_format_dlerror("dlopen failed", linker_get_error_buffer());
return NULL;
}
return result;
}
此函式主要透過呼叫do_dlopen
函式來返回一個動態連結庫的控制程式碼,該控制程式碼為一個soinfo結構體。Soinfo結構體的具體定義在bionic/linker/linker.h
中。
繼續檢視do_dlopen
函式,程式碼在linker.cpp中:
#!cpp
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
函式。此函式程式碼如下:
#!cpp
static soinfo* find_library(const char* name) {
soinfo* si = find_library_internal(name);
if (si != NULL) {
si->ref_count++;
}
return si;
}
繼續往下深入:
#!cpp
static soinfo* find_library_internal(const char* name) {
……..
soinfo* si = find_loaded_library(name); //首先檢視這個so是否已經載入,如果已經載入,就返回該so的soinfo
if (si != NULL) {
if (si->flags & FLAG_LINKED) {
return si;
}
DL_ERR("OOPS: recursive link to \"%s\"", si->name);
return NULL;
}
TRACE("[ '%s' has not been loaded yet. Locating...]", name);
si = load_library(name); //說明該so沒有被載入,就呼叫此函式進行載入
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("[ find_library_internal base=%p size=%zu name='%s' ]",
reinterpret_cast<void*>(si->base), si->size, si->name);
if (!soinfo_link_image(si)) { //載入完so後,根據si的反饋進行連結。會在第3節進行詳細分析
munmap(reinterpret_cast<void*>(si->base), si->size);
soinfo_free(si);
return NULL;
}
return si;
}
先不去關心那些錯誤處理資訊,我們假設各個函式的返回值均在預期範圍內,這個函式的執行流程為:
- 使用find_loaded_library函式在已經載入的動態連結庫連結串列裡面查詢該動態庫。如果找到了,就返回該動態庫的soinfo,否則執行第②步;
- 此時,說明指定的動態連結庫還沒有被載入,就使用load_library函式來載入該動態庫。
load_library
函式是整個so載入過程的重中之重!它建立了動態連結庫的控制程式碼,程式碼如下:
#!cpp
static soinfo* load_library(const char* name) {
// Open the file.
int fd = open_library(name);
if (fd == -1) {
DL_ERR("library \"%s\" not found", name);
return NULL;
}
// Read the ELF header and load the segments.
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; //入口函式設為null
si->dynamic = NULL;
si->phnum = elf_reader.phdr_count();
si->phdr = elf_reader.loaded_phdr();
return si;
}
load_library
函式的執行過程可以概括如下:
- 使用open_library函式開啟指定so檔案;
- 建立ElfReader類物件,並透過該物件的load方法,讀取Elf檔案頭,然後透過分析Elf檔案來載入各個segments;
- 使用soinfo_alloc函式分配一個soinfo結構體,併為這個結構體中的各個成員賦值。
下面對步驟二
加以詳細介紹。
1.1 SO檔案的讀取與載入工作
Linker使用ElfRead類的load函式完成so檔案的分析工作。該類的原始碼在linker_phdr.cpp
中。Load函式程式碼如下:
#!cpp
bool ElfReader::Load() {
return ReadElfHeader() &&
VerifyElfHeader() &&
ReadProgramHeader() &&
ReserveAddressSpace() &&
LoadSegments() &&
FindPhdr();
}
顯然此函式依次呼叫ReadElfHeader、ReadProgramHeader等函式。
首先,我們需要知道Android系統載入segments的機制:
一個ELF檔案的程式頭表包含一個或多個PT_LOAD segments
,這些segments標誌ELF檔案中需要被對映到程式空間的區域。每一個可以載入的segment都含有如下重要屬性:
p_offset
: 段在檔案的偏移地址p_filesz
:段的大小p_memsz
:段在記憶體中佔據的大小(通常大於p_filesz)。p_vaddr
: 段的虛擬地址p_flags
:段的標記(可讀,可寫,可執行)
當前,我們忽略p_paddr
和p_align
成員。
可以載入的segments能在虛擬地址範圍[p_vaddr…p_vaddr+p_memsz)
以列表的形式展現。其中有如下幾個規則:
- 各個segments的虛擬地址範圍不可重疊;
- 如果一個segment的
p_filesz
小於p_memsz
,那麼兩者之間的額外資料將被初始化為0; - segment的虛擬地址範圍的起、始地址不是必須在某一頁的邊界。兩個不同的segments的起、始地址可以在同一頁,在這種情況,該頁繼承後一segment的對映標記(mapping flags)
- 每一個segment實際載入的地址並非p
_vaddr
。而是由載入器決定將第一個segment載入到記憶體中的哪個位置,然後剩下的segments就以第一個segment為參照物,進行載入。比如:
下面是兩個loadable segments的資訊:
#!bash
[ offset:0, filesz:0x4000, memsz:0x4000, vaddr:0x30000 ],
[ offset:0x4000, filesz:0x2000, memsz:0x8000, vaddr:0x40000 ],
相當於這兩個segments的虛擬地址範圍分別為:
#!bash
0x30000...0x34000
0x40000...0x48000
如果載入器決定將第一個segment載入到0xa0000000的話(透過後面的分析會知道,這個載入地址是在載入程式頭部表的時候由系統確定的),那麼它們的實際虛擬地址範圍就是:
#!bash
0xa0030000...0xa0034000
0xa0040000...0xa0048000
換句話說,所有的segments的實際載入開始地址與其vaddr的偏差值是固定的(0xa0030000 – 0x30000 = 0xa0040000 – 0x40000)。
但是,在實際情況下,segments的地址並不是在每一頁的邊界出開始的。考慮到我們只能在頁面邊界進行記憶體對映,因此,這就意味著載入地址的偏差bias應當按照如下方法進行計算:
#!bash
load_bias = phdr0_load_address - PAGE_START(phdr0->p_vaddr)
(#define PAGE_START(x) ((x) & PAGE_MASK)
PAGE_MASK的值一般為0xfffff000。)
所以第一個segment的load_bias
= 0xa0030000 – 0x30000&0xfffff000 = 0xa00000000。
這裡phdr0_load_address
必須以某一頁的邊界為起始地址,所以該segments的真正內容的開始地址為:
#!bash
phdr0_load_address + PAGE_OFFSET(phdr0->p_vaddr)
(#define PAGE_OFFSET(x) ((x) & ~PAGE_MASK) 就是x & 0xfff)
注意:ELF要求如下條件,以滿足mmap正常工作:
#!bash
PAGE_OFFSET(phdr0->p_vaddr) == PAGE_OFFSET(phdr0->p_offset)
每一個loadable segments的p_vaddr
都必須加上load_bias
,其和就是該segments在記憶體中的實際開始地址。
1.1.1 ReadProgramHeader
理清了Android載入segments的機制,我們就來看linker中的實際程式碼,先看ReadProgramHeader:
#!cpp
bool ElfReader::ReadProgramHeader() {
phdr_num_ = header_.e_phnum;
……..
ElfW(Addr) page_min = PAGE_START(header_.e_phoff);
ElfW(Addr) page_max = PAGE_END(header_.e_phoff + (phdr_num_ * sizeof(ElfW(Phdr))));
ElfW(Addr) page_offset = PAGE_OFFSET(header_.e_phoff);
phdr_size_ = page_max - page_min;
void* mmap_result = mmap(NULL, phdr_size_, PROT_READ, MAP_PRIVATE, fd_, page_min);
……..
phdr_mmap_ = mmap_result;
phdr_table_ = reinterpret_cast<ElfW(Phdr)*>(reinterpret_cast<char*>(mmap_result) + page_offset);
return true;
}
- 首先讀取elf檔案的程式頭部表項數目
phdr_num
; - 然後分別獲取程式頭部表在頁邊界對齊後的起始地址
page_min
、結束地址page_max
和偏移地址page_offset
。並根據page_max
與page_start
計算出程式頭部表佔據的頁面大小phdr_size
; - 再以只讀模式建立一個私有對映,該對映將elf檔案中偏移值為
page_min
,大小為phdr_size
的區域對映到記憶體中。將對映後的記憶體地址賦給phdr_mmap_
,簡單一句話:將程式頭部表對映到記憶體中,並將記憶體地址賦值; reinterpret_cast<new_type>(expression)
,這是c++中的強制型別轉換符,類似於(new_type*)(expression)
。這裡我們對上面紅色部分程式碼加以解釋:
(注:紅色程式碼為倒數第三句)
首先reinterpret_cast<char*>(mmap_result)
:經void*
型指標mmap_result
強制轉換成char*
型;
然後reinterpret_cast<char*>(mmap_result) + page_offset
:char*
型指標+page_offset
,表示指向程式頭部表真正開始的地方;
最後再將其轉換成ElfW(Phdr)*
型指標,顯然phdr_table_
指向程式頭部表開始地址。
1.1.2 ReserveAddressSpace
再來看ReserveAddressSpace:
#!cpp
/*預備一塊足夠大的虛擬地址範圍,用來載入所有可載入的segments.我們可以透過mmap建立一個帶有PROT_NONE屬性的私有匿名記憶體對映。PROT_NONE表示頁不可訪問,匿名對映表示對映區不與任何檔案關聯(要求fd為-1),私有對映表示對該對映區域的寫入操作會產生一個對映檔案的複製,對此區域做的任何修改夠不會寫會原來的檔案*/
bool ElfReader::ReserveAddressSpace() {
ElfW(Addr) min_vaddr;
load_size_ = phdr_table_get_load_size(phdr_table_, phdr_num_, &min_vaddr);
……..
uint8_t* addr = reinterpret_cast<uint8_t*>(min_vaddr);
int mmap_flags = MAP_PRIVATE | MAP_ANONYMOUS;
void* start = mmap(addr, load_size_, PROT_NONE, mmap_flags, -1, 0);
……..
load_start_ = start;
load_bias_ = reinterpret_cast<uint8_t*>(start) - addr;
return true;
}
這裡有一個關鍵函式phdr_table_get_load_siz
:
#!cpp
/*返回ELF檔案程式頭部表中所指定的所有可載入segments(這些segments可能是非連續的)的區間大小,如果沒有可載入的segments,就返回0
如果out_min_vaddr 或 out_max_vadd是非空的,它們就會被設定成將被儲存的頁的最小/大地址(如果沒有可載入segments的話,就設為0) */
size_t phdr_table_get_load_size(const ElfW(Phdr)* phdr_table, size_t phdr_count,
ElfW(Addr)* out_min_vaddr,
ElfW(Addr)* out_max_vaddr) {
ElfW(Addr) min_vaddr = UINTPTR_MAX;
ElfW(Addr) max_vaddr = 0;
bool found_pt_load = false;
for (size_t i = 0; i < phdr_count; ++i) {
const ElfW(Phdr)* phdr = &phdr_table[i];
if (phdr->p_type != PT_LOAD) {
continue;
}
found_pt_load = true;
if (phdr->p_vaddr < min_vaddr) {
min_vaddr = phdr->p_vaddr;
}
if (phdr->p_vaddr + phdr->p_memsz > max_vaddr) {
max_vaddr = phdr->p_vaddr + phdr->p_memsz;
}
}
if (!found_pt_load) {
min_vaddr = 0;
}
min_vaddr = PAGE_START(min_vaddr);
max_vaddr = PAGE_END(max_vaddr);
if (out_min_vaddr != NULL) {
*out_min_vaddr = min_vaddr;
}
if (out_max_vaddr != NULL) {
*out_max_vaddr = max_vaddr;
}
return max_vaddr - min_vaddr;
}
通俗點講,此函式就是返回ELF檔案中包含的可載入segments總共需要佔用的空間大小,並設定其最小虛擬地址的值(是頁對齊的)。值得注意的是,原函式有4個引數,但是在ReserveAddressSpace中呼叫該函式時卻只傳遞了3個引數,忽略了out_max_vaddr
。在我個人看來是因為已知了out_min_vaddr
及兩者的差值load_size
,所以可以透過out_min_vaddr + load_size
來求得out_max_vaddr
。
現在回到ReserveAddressSpace函式。求得load_size
之後,就需要為這些segments分配足夠的記憶體空間。這裡需要注意的是mmap的第一個引數並非為Null,而是addr。這就表示將對映區間的開始地址放在程式的addr地址處(一般不會成功,而是由系統自動分配,所以可以看作是Null),mmap返回實際對映後的記憶體開始地址start。顯然load_bias_ = start – addr
就是實際對映記憶體地址同linker期望的對映地址的誤差值。後面的操作中,linker就可以透過p_vaddr + load_bias_
來獲取某一segments在記憶體中的開始地址了。
1.1.3 LoadSegments
現在就開始載入ELF檔案中的可載入segments了:
#!cpp
bool ElfReader::LoadSegments() {
for (size_t i = 0; i < phdr_num_; ++i) {
const ElfW(Phdr)* phdr = &phdr_table_[i];
if (phdr->p_type != PT_LOAD) {
continue;
}
// Segment addresses in memory.
ElfW(Addr) seg_start = phdr->p_vaddr + load_bias_;
ElfW(Addr) seg_end = seg_start + phdr->p_memsz;
ElfW(Addr) seg_page_start = PAGE_START(seg_start);
ElfW(Addr) seg_page_end = PAGE_END(seg_end);
ElfW(Addr) seg_file_end = seg_start + phdr->p_filesz;
// File offsets.
ElfW(Addr) file_start = phdr->p_offset;
ElfW(Addr) file_end = file_start + phdr->p_filesz;
ElfW(Addr) file_page_start = PAGE_START(file_start);
ElfW(Addr) file_length = file_end - file_page_start;
if (file_length != 0) {
void* seg_addr = mmap(reinterpret_cast<void*>(seg_page_start),
file_length, //是以檔案大小為參照,而非記憶體大小
PFLAGS_TO_PROT(phdr->p_flags),
MAP_FIXED|MAP_PRIVATE,
fd_,
file_page_start);
if (seg_addr == MAP_FAILED) {
DL_ERR("couldn't map \"%s\" segment %zd: %s", name_, i, strerror(errno));
return false;
}
}
/*如果segments可寫,並且該segments的實際結束地址不在某一頁的邊界的話,就將該segments實際結束地址到此頁的邊界之間的記憶體全置為0*/
if ((phdr->p_flags & PF_W) != 0 && PAGE_OFFSET(seg_file_end) > 0) {
memset(reinterpret_cast<void*>(seg_file_end), 0, PAGE_SIZE - PAGE_OFFSET(seg_file_end));
}
seg_file_end = PAGE_END(seg_file_end);
// seg_file_end is now the first page address after the file
// content. If seg_end is larger, we need to zero anything
// between them. This is done by using a private anonymous
// map for all extra pages.
if (seg_page_end > seg_file_end) {
void* zeromap = mmap(reinterpret_cast<void*>(seg_file_end),
seg_page_end - seg_file_end,
PFLAGS_TO_PROT(phdr->p_flags),
MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE,
-1,
0);
if (zeromap == MAP_FAILED) {
DL_ERR("couldn't zero fill \"%s\" gap: %s", name_, strerror(errno));
return false;
}
}
}
return true;
}
此部分功能很簡單:就是將ELF中的可載入segments依次對映到記憶體中,並進行一些輔助掃尾工作。
1.1.4 FindPhdr
返回程式頭部表在記憶體中地址。這與phdr_table_
是不同的,後者是一個臨時的、在so被重定位之前會為釋放的變數:
#!cpp
bool ElfReader::FindPhdr() {
const ElfW(Phdr)* phdr_limit = phdr_table_ + phdr_num_;
//如果段型別是 PT_PHDR, 那麼我們就直接使用該段的地址.
for (const ElfW(Phdr)* phdr = phdr_table_; phdr < phdr_limit; ++phdr) {
if (phdr->p_type == PT_PHDR) {
return CheckPhdr(load_bias_ + phdr->p_vaddr);
}
}
//否則,我們就檢查第一個可載入段。如果該段的檔案偏移值為0,那麼就表示它是以ELF頭開始的,我們就可以透過它來找到程式頭表載入到記憶體的地址(雖然過程有點繁瑣)。
for (const ElfW(Phdr)* phdr = phdr_table_; phdr < phdr_limit; ++phdr) {
if (phdr->p_type == PT_LOAD) {
if (phdr->p_offset == 0) {
ElfW(Addr) elf_addr = load_bias_ + phdr->p_vaddr;
const ElfW(Ehdr)* ehdr = reinterpret_cast<const ElfW(Ehdr)*>(elf_addr);
ElfW(Addr) offset = ehdr->e_phoff;
return CheckPhdr((ElfW(Addr))ehdr + offset);
}
break;
}
}
DL_ERR("can't find loaded phdr for \"%s\"", name_);
return false;
}
要理解這段程式碼,我們需要知道段型別PT_PHDR所表示的意義:指定程式頭表在檔案及程式記憶體映像中的位置和大小。此段型別不能在一個檔案中多次出現。此外,僅當程式頭表是程式記憶體映像的一部分時,才可以出現此段。此型別(如果存在)必須位於任何可裝入段的各項的前面。有關詳細資訊,請參見程式的解釋程式。
至此so檔案的讀取、載入工作就分析完畢了。我們可以發現,Android對so的載入操作只是以段為單位,跟section完全沒有關係。另外,透過檢視VerifyElfHeader的程式碼,我們還可以發現,Android系統僅僅對ELF檔案頭的e_ident
、e_type
、e_version
、e_machine
進行驗證(當然,e_phnum
也是不能錯的),所以,這就解釋了為什麼有些加殼so檔案頭的section相關欄位可以任意修改,系統也不會報錯了。
1.2 so的連結機制
在1.1我們詳細分析了Android so的載入機制,現在就開始分析so的連結機制。在分析linker的關於連結的原始碼之前,我們需要學習ELF檔案關於動態連結方面的知識。
1.2.1 動態節區
如果一個目標檔案參與動態連結,它的程式頭部表將包含型別為 PT_DYNAMIC
的元素。此“段”包含.dynamic
節區(這個節區是一個陣列)。該節區採用一個特殊符號_DYNAMIC
來標記,其中包含如下結構的陣列:
#!cpp
typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;
extern Elf32_Dyn _DYNAMIC[]; //注意這裡是一個陣列
/*注意:
對每個這種型別的物件,d_tag控制d_un的解釋含義:
d_val 此 Elf32_Word 物件表示一個整數值,可以有多種解釋。
d_ptr 此 Elf32_Addr 物件代表程式的虛擬地址。
關於d_tag的值、該值的意義,及其與d_un的關係,可檢視ELF.PDF p24。 */
該Elf32_Dyn
陣列就是soinfo結構體中的dynamic成員,我們在第2節介紹的load_library
函式中發現,si->dynamic
被賦值為null,這就說明,在載入階段是不需要此值的,只有在連結階段才需要。Android的動態庫的連結工作還是由linker完成,主要程式碼就是在linker.cpp的soinfo_link_image
(find_library_internal
方法中呼叫)中,此函式的程式碼相當多,我們來分塊分析:
首先,我們需要從程式頭部表中獲取dynamic節區資訊:
#!cpp
/*in function soinfo_link_image */
/*抽取動態節區*/
size_t dynamic_count;
ElfW(Word) dynamic_flags;
/*這裡的si->dynamic 為ElfW(Dyn)指標,就是上面提到的Elf32_Dyn _DYNAMIC[]*/
phdr_table_get_dynamic_section(phdr, phnum, base, &si->dynamic,
&dynamic_count, &dynamic_flags);
此函式很簡單:
#!cpp
/*返回ELF檔案中的dynamic節區在記憶體中的地址和大小,如果沒有該節區就返回null
* Input:
* phdr_table -> program header table
* phdr_count -> number of entries in tables
* load_bias -> load bias
* Output:
* dynamic -> address of table in memory (NULL on failure).
* dynamic_count -> number of items in table (0 on failure).
* dynamic_flags -> protection flags for section (unset on failure)
*/
void phdr_table_get_dynamic_section(const ElfW(Phdr)* phdr_table, size_t phdr_count,
ElfW(Addr) load_bias,
ElfW(Dyn)** dynamic, size_t* dynamic_count, ElfW(Word)* dynamic_flags) {
const ElfW(Phdr)* phdr = phdr_table;
const ElfW(Phdr)* phdr_limit = phdr + phdr_count;
for (phdr = phdr_table; phdr < phdr_limit; phdr++) {
if (phdr->p_type != PT_DYNAMIC) {
continue;
}
*dynamic = reinterpret_cast<ElfW(Dyn)*>(load_bias + phdr->p_vaddr);
if (dynamic_count) {
*dynamic_count = (unsigned)(phdr->p_memsz / 8);
//這裡需要解釋下,在2.2.1中我們介紹了Elf32_Dyn的結構,它佔8位元組。而PT_DYNAMIC段就是存放著Elf32_Dyn陣列,所以dynamic_count的值就是該段的memsz/8。
}
if (dynamic_flags) {
*dynamic_flags = phdr->p_flags;
}
return;
}
*dynamic = NULL;
if (dynamic_count) {
*dynamic_count = 0;
}
}
成功獲取了dynamic節區資訊,我們就可以根據該節區中的Elf32_Dyn
陣列來進行so連結操作了。我們需要從dynamic節區中抽取有用的資訊,linker採用遍歷dynamic陣列的方式,根據每個元素的flags()進行相應的處理:
#!cpp
/*in function soinfo_link_image */
// 從動態dynamic節區中抽取有用資訊
uint32_t needed_count = 0;
//開始從頭遍歷dyn陣列,根據陣列中個元素的標記進行相應的處理
for (ElfW(Dyn)* d = si->dynamic; d->d_tag != DT_NULL; ++d) { //標記為 DT_NULL 的專案標註了整個 _DYNAMIC 陣列的末端,因此以它為結尾標誌。
........
switch (d->d_tag) {
case DT_HASH:
........
break;
case DT_STRTAB:
si->strtab = reinterpret_cast<const char*>(base + d->d_un.d_ptr);
break;
case DT_SYMTAB:
si->symtab = reinterpret_cast<ElfW(Sym)*>(base + d->d_un.d_ptr);
break;
case DT_JMPREL:
#if defined(USE_RELA)
si->plt_rela = reinterpret_cast<ElfW(Rela)*>(base + d->d_un.d_ptr);
#else
si->plt_rel = reinterpret_cast<ElfW(Rel)*>(base + d->d_un.d_ptr);
#endif
break;
case DT_PLTRELSZ:
#if defined(USE_RELA)
si->plt_rela_count = d->d_un.d_val / sizeof(ElfW(Rela));
#else
si->plt_rel_count = d->d_un.d_val / sizeof(ElfW(Rel));
#endif
break;
#if defined(__mips__)
case DT_PLTGOT:
// Used by mips and mips64.
si->plt_got = reinterpret_cast<ElfW(Addr)**>(base + d->d_un.d_ptr);
break;
#endif
........
#if defined(USE_RELA)
case DT_RELA:
si->rela = reinterpret_cast<ElfW(Rela)*>(base + d->d_un.d_ptr);
break;
case DT_RELASZ:
si->rela_count = d->d_un.d_val / sizeof(ElfW(Rela));
break;
case DT_REL:
DL_ERR("unsupported DT_REL in \"%s\"", si->name);
return false;
case DT_RELSZ:
DL_ERR("unsupported DT_RELSZ in \"%s\"", si->name);
return false;
#else
case DT_REL:
si->rel = reinterpret_cast<ElfW(Rel)*>(base + d->d_un.d_ptr);
break;
case DT_RELSZ:
si->rel_count = d->d_un.d_val / sizeof(ElfW(Rel));
break;
case DT_RELA:
DL_ERR("unsupported DT_RELA in \"%s\"", si->name);
return false;
#endif
case DT_INIT: //只有可執行檔案才有此節區
si->init_func = reinterpret_cast<linker_function_t>(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<linker_function_t>(base + d->d_un.d_ptr);
DEBUG("%s destructors (DT_FINI) found at %p", si->name, si->fini_func);
break;
case DT_INIT_ARRAY:
si->init_array = reinterpret_cast<linker_function_t*>(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:
si->init_array_count = ((unsigned)d->d_un.d_val) / sizeof(ElfW(Addr));
break;
case DT_FINI_ARRAY:
si->fini_array = reinterpret_cast<linker_function_t*>(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:
si->fini_array_count = ((unsigned)d->d_un.d_val) / sizeof(ElfW(Addr));
break;
case DT_PREINIT_ARRAY:
si->preinit_array = reinterpret_cast<linker_function_t*>(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:
si->preinit_array_count = ((unsigned)d->d_un.d_val) / sizeof(ElfW(Addr));
break;
case DT_TEXTREL:
#if defined(__LP64__)
DL_ERR("text relocations (DT_TEXTREL) found in 64-bit ELF file \"%s\"", si->name);
return false;
#else
si->has_text_relocations = true;
break;
#endif
case DT_SYMBOLIC:
si->has_DT_SYMBOLIC = true;
break;
case DT_NEEDED:
++needed_count;
break;
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;
#if defined(__mips__)
case DT_STRSZ:
case DT_SYMENT:
case DT_RELENT:
break;
case DT_MIPS_RLD_MAP:
// Set the DT_MIPS_RLD_MAP entry to the address of _r_debug for GDB.
{
r_debug** dp = reinterpret_cast<r_debug**>(base + d->d_un.d_ptr);
*dp = &_r_debug;
}
break;
case DT_MIPS_RLD_VERSION:
case DT_MIPS_FLAGS:
case DT_MIPS_BASE_ADDRESS:
case DT_MIPS_UNREFEXTNO:
break;
case DT_MIPS_SYMTABNO:
si->mips_symtabno = d->d_un.d_val;
break;
case DT_MIPS_LOCAL_GOTNO:
si->mips_local_gotno = d->d_un.d_val;
break;
case DT_MIPS_GOTSYM:
si->mips_gotsym = d->d_un.d_val;
break;
#endif
default:
DEBUG("Unused DT entry: type %p arg %p",
reinterpret_cast<void*>(d->d_tag), reinterpret_cast<void*>(d->d_un.d_val));
break;
}
}
完成dynamic陣列的遍歷後,就說明我們已經獲取了其中的有用資訊了,那麼現在就需要根據這些資訊進行處理:
#!cpp
/*in function soinfo_link_image */
//再檢測一遍,這種做法總是明智的
if (relocating_linker && needed_count != 0) {
DL_ERR("linker cannot have DT_NEEDED dependencies on other libraries");
return false;
}
if (si->nbucket == 0) {
DL_ERR("empty/missing DT_HASH in \"%s\" (built with --hash-style=gnu?)", si->name);
return false;
}
if (si->strtab == 0) {
DL_ERR("empty/missing DT_STRTAB in \"%s\"", si->name);
return false;
}
if (si->symtab == 0) {
DL_ERR("empty/missing DT_SYMTAB in \"%s\"", si->name);
return false;
}
// If this is the main executable, then load all of the libraries from LD_PRELOAD now.
//如果是main可執行檔案,那麼就根據LD_PRELOAD資訊來載入所有相關的庫
//這裡面涉及到的gLdPreloadNames變數,我們知道在前面的整個分析過程中均沒有涉及,這是因為,對於可執行檔案而言,它的起始函式並不是dlopen,而是系統核心的execv函式,透過層層呼叫之後才會執行到linker的linker_init_post_ralocation函式,在這個函式中呼叫parse_LD_PRELOAD函式完成 gLdPreloadNames變數的賦值
if (si->flags & FLAG_EXE) {
memset(gLdPreloads, 0, sizeof(gLdPreloads));
size_t preload_count = 0;
for (size_t i = 0; gLdPreloadNames[i] != NULL; i++) {
soinfo* lsi = find_library(gLdPreloadNames[i]);
if (lsi != NULL) {
gLdPreloads[preload_count++] = lsi;
} else {
........
}
}
}
//分配一個soinfo*[]指標陣列,用於存放本so庫需要的外部so庫的soinfo指標
soinfo** needed = reinterpret_cast<soinfo**>(alloca((1 + needed_count) * sizeof(soinfo*)));
soinfo** pneeded = needed;
//依次獲取dynamic陣列中定義的每一個外部so庫soinfo
for (ElfW(Dyn)* d = si->dynamic; d->d_tag != DT_NULL; ++d) {
if (d->d_tag == DT_NEEDED) {
const char* library_name = si->strtab + d->d_un.d_val; //根據index值獲取所需庫的名字
DEBUG("%s needs %s", si->name, library_name);
soinfo* lsi = find_library(library_name); //獲取該庫的soinfo
if (lsi == NULL) {
........
}
*pneeded++ = lsi;
}
}
*pneeded = NULL;
#if !defined(__LP64__)
if (si->has_text_relocations) {
// Make segments writable to allow text relocations to work properly. We will later call
// phdr_table_protect_segments() after all of them are applied and all constructors are run.
DL_WARN("%s has text relocations. This is wasting memory and prevents "
"security hardening. Please fix.", si->name);
if (phdr_table_unprotect_segments(si->phdr, si->phnum, si->load_bias) < 0) {
DL_ERR("can't unprotect loadable segments for \"%s\": %s",
si->name, strerror(errno));
return false;
}
}
#endif
#if defined(USE_RELA)
if (si->plt_rela != NULL) {
DEBUG("[ relocating %s plt ]\n", si->name);
if (soinfo_relocate(si, si->plt_rela, si->plt_rela_count, needed)) {
return false;
}
}
if (si->rela != NULL) {
DEBUG("[ relocating %s ]\n", si->name);
if (soinfo_relocate(si, si->rela, si->rela_count, needed)) {
return false;
}
}
#else
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;
}
}
#endif
#if defined(__mips__)
if (!mips_relocate_got(si, needed)) {
return false;
}
#endif
si->flags |= FLAG_LINKED;
DEBUG("[ finished linking %s ]", si->name);
#if !defined(__LP64__)
if (si->has_text_relocations) {
// All relocations are done, we can protect our segments back to read-only.
if (phdr_table_protect_segments(si->phdr, si->phnum, si->load_bias) < 0) {
DL_ERR("can't protect segments for \"%s\": %s",
si->name, strerror(errno));
return false;
}
}
#endif
/* We can also turn on GNU RELRO protection */
if (phdr_table_protect_gnu_relro(si->phdr, si->phnum, si->load_bias) < 0) {
DL_ERR("can't enable GNU RELRO protection for \"%s\": %s",
si->name, strerror(errno));
return false;
}
notify_gdb_of_load(si);
return true;
}
0x02 開始執行so檔案
上面的find_library_internal
函式中的soinfo_link_image
函式執行完後就返回到上層函式find_library
中,然後進一步返回到do_dlopen
函式:
#!cpp
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;
}
如果獲取的si不為空,就說明so的載入和連結操作正確完成,那麼就可以執行so的初始化建構函式了:
#!cpp
void soinfo::CallConstructors() {
........
// DT_INIT should be called before DT_INIT_ARRAY if both are present.
//如果檔案含有.init和.init_array節區的話,就先執行.init節區的程式碼再執行.init_array節區的程式碼
CallFunction("DT_INIT", init_func);
CallArray("DT_INIT_ARRAY", init_array, init_array_count, false);
}
由於我們只分析so庫,所以只需要關心CallArray("DT_INIT_ARRAY", init_array, init_array_count, false)
函式即可:
#!cpp
void soinfo::CallArray(const char* array_name UNUSED, linker_function_t* functions, size_t count, bool reverse) {
........
//這裡的recerse變數用於指定.init_array中的函式是由前到後執行還是由後到前執行。預設是由前到後
int begin = reverse ? (count - 1) : 0;
int end = reverse ? -1 : count;
int step = reverse ? -1 : 1;
for (int i = begin; i != end; i += step) {
TRACE("[ %s[%d] == %p ]", array_name, i, functions[i]);
CallFunction("function", functions[i]); //依次呼叫init_array中的函式。
}
........
}
這裡需要對init_array
節區的結構和作用加以說明。
首先是init_array
節區的資料結構。該節中包含指標,這些指標指向了一些初始化程式碼。這些初始化程式碼一般是在main函式之前執行的。在C++程式中,這些程式碼用來執行靜態建構函式。另外一個用途就是有時候用來初始化C庫中的一些IO系統。使用IDA檢視具有init_array
節區的so庫檔案就可以找到如下資料:
這裡共三個函式指標,每個指標指向一個函式地址。值得注意的是,上圖中每個函式指標的值都加了1,這是因為地址的最後1位置1表明需要使得處理器由ARM轉為Thumb狀態來處理Thumb指令。將目標地址處的程式碼解釋為Thumb程式碼來執行。
然後再來看CallFunction的具體實現:
#!cpp
void soinfo::CallFunction(const char* function_name UNUSED, linker_function_t function) {
//如果函式地址為空或者為-1就直接退出。
if (function == NULL || reinterpret_cast<uintptr_t>(function) == static_cast<uintptr_t>(-1)) {
return;
}
........
function(); //執行該指標所指定的函式
// 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);
}
至此,整個Android so的linker機制就分析完畢了!
相關文章
- Android 學習筆記雜記2018-08-01Android筆記
- Android學習筆記·ANR2019-03-08Android筆記
- Android學習筆記·Handler2019-03-11Android筆記
- Android學習筆記·ADB2019-03-14Android筆記
- Android學習筆記一2018-07-31Android筆記
- Android SQLite學習筆記2018-09-01AndroidSQLite筆記
- Android Studio學習筆記2024-09-18Android筆記
- Android 學習筆記核心篇2019-01-22Android筆記
- Android Gradle 學習筆記整理2019-09-18AndroidGradle筆記
- Android 學習筆記思考篇2019-06-28Android筆記
- 2018.03.06 Android Handler學習筆記2018-03-06Android筆記
- Android 開發學習筆記2024-07-17Android筆記
- Android 學習筆記架構篇2019-02-22Android筆記架構
- React Native Android學習筆記 - 20152018-08-17React NativeAndroid筆記
- 2018.03.16、Android-IntentService學習筆記2018-03-15AndroidIntent筆記
- Android環境搭建學習筆記2020-12-12Android筆記
- Android菜鳥學習js筆記一2018-06-27AndroidJS筆記
- Android卡頓優化學習筆記2020-09-24Android優化筆記
- 2018.03.30、Android-ObjectBox學習筆記12018-03-30AndroidObject筆記
- Android 快取工具 DiskLruCache 學習筆記2021-09-09Android快取筆記
- Android學習筆記(建立Menu,Intent的使用)2018-08-04Android筆記Intent
- Android學習筆記-Activity的啟動模式2020-10-01Android筆記模式
- numpy的學習筆記\pandas學習筆記2018-03-18筆記
- Android學習筆記之build.gradle檔案2018-08-08Android筆記UIGradle
- 學習筆記2024-04-14筆記
- 《Android原始碼設計模式》學習筆記之ImageLoader2019-01-16Android原始碼設計模式筆記
- [Android學習筆記]雜碎知識(持續更新)2020-10-18Android筆記
- 【學習筆記】數學2024-07-15筆記
- 《JAVA學習指南》學習筆記2019-03-24Java筆記
- 機器學習學習筆記2021-06-01機器學習筆記
- Android學習筆記--基於XMPP的即時通訊2018-11-12Android筆記
- Android Utils 之 Vector 學習筆記(一)—— VectorImpl 程式碼分析2018-07-11Android筆記
- Android學習過程的Cursor遊標填坑筆記2018-09-04Android筆記
- 學習筆記-粉筆9802024-10-14筆記
- 學習筆記(3.29)2019-03-31筆記
- 學習筆記(4.1)2019-04-01筆記
- 學習筆記(3.25)2019-03-25筆記
- 學習筆記(3.26)2019-03-26筆記