该文档深入分析了Android系统中
.so
文件的链接过程,揭示了加载过程中链接符号的流程、符号解析机制、以及相关的动态库依赖关系。
1. 简介
上文说到,通过 so 的加载,安卓将 so 的相关信息放在了 task 对象,包含了以下重要信息:phdr_table 指针、shdr_table 指针、动态节区指针、动态字符串节区指针。
本文将剖析 so 的链接过程,so 的动态链接信息位于动态节区(.dynamic),动态节区的包含了多个格式相同的结构体,这些结构体类型如下:
d_tag 控制着结构体的类型;
d_val 不同类型的结构体有着不同的意思;
d_ptr 结构体的偏移地址。
typedef struct dynamic {
Elf32_Sword d_tag;
union {
Elf32_Sword d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;
2. 填充 soinfo 结构体
在 task.read()
和 task.load()
阶段,就已经对 soinfo 结构体进行了部分赋值,此时已有的信息包括:
phdr
phnum
load_bias
有了 load_bias,只需要知道.dynamic 节区所在,便可填充 soinfo 结构体的剩余部分。
for (auto&& task : load_tasks) {
soinfo* si = task->get_soinfo();
if (!si->is_linked() && !si->prelink_image()) {
return false;
}
register_soinfo_tls(si);
}
2.1 prelink_image
首先调用 phdr_table_get_dynamic_section()
找到.dynamic 节区地址,具体方式:
- 通过 phdr 找到 $p_type=PT_DYNAMIC$ 的条目;
- 计算 $.dynamic_addr=load_bias+p_vaddr$
bool soinfo::prelink_image() {
/* Extract dynamic section */
ElfW(Word) dynamic_flags = 0;
phdr_table_get_dynamic_section(phdr, phnum, load_bias, &dynamic, &dynamic_flags);
......
}
/////////////////////////////////////
void phdr_table_get_dynamic_section(const ElfW(Phdr)* phdr_table, size_t phdr_count,
ElfW(Addr) load_bias, ElfW(Dyn)** dynamic,
ElfW(Word)* dynamic_flags) {
*dynamic = nullptr;
for (size_t i = 0; i<phdr_count; ++i) {
const ElfW(Phdr)& phdr = phdr_table[i];
if (phdr.p_type == PT_DYNAMIC) {
*dynamic = reinterpret_cast<ElfW(Dyn)*>(load_bias + phdr.p_vaddr);
if (dynamic_flags) {
*dynamic_flags = phdr.p_flags;
}
return;
}
}
}
遍历动态节区的每个条目,复制给 soinfo 结构体。可以注意到,在解析关于 SONAME 时,直接跳过了,原因是 SONAME 只是偏移量(动态字符串表),所以,需要先解析动态字符串表,才能解析 SONAME。
uint32_t needed_count = 0;
for (ElfW(Dyn)* d = dynamic; d->d_tag != DT_NULL; ++d) {
DEBUG("d = %p, d[0](tag) = %p d[1](val) = %p",
d, reinterpret_cast<void*>(d->d_tag), reinterpret_cast<void*>(d->d_un.d_val));
switch (d->d_tag) {
case DT_SONAME:
// this is parsed after we have strtab initialized (see below).
break;
case DT_HASH:
nbucket_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[0];
nchain_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[1];
bucket_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr + 8);
chain_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr + 8 + nbucket_ * 4);
break;
... ...
case DT_STRTAB:
strtab_ = reinterpret_cast<const char*>(load_bias + d->d_un.d_ptr);
break;
case DT_STRSZ:
strtab_size_ = d->d_un.d_val;
break;
case DT_SYMTAB:
symtab_ = reinterpret_cast<ElfW(Sym)*>(load_bias + d->d_un.d_ptr);
break;
... ...
for (ElfW(Dyn)* d = dynamic; d->d_tag != DT_NULL; ++d) {
switch (d->d_tag) {
case DT_SONAME:
set_soname(get_string(d->d_un.d_val));
break;
case DT_RUNPATH:
set_dt_runpath(get_string(d->d_un.d_val));
break;
}
}
当 prelink_image()
函数执行完毕,soinfo 结构体也完成了填充,此时动态链接的所有信息也都知道了。
3. 重定位
3.1 重定位简介
3.1.1 含义
重定义是指将 “导入符号” 的引用和符号的真实地址绑定的过程。简单来说,当调用函数或者全局变量、静态变量时,操作系统通过重定位生成的约定,去某张表获取符号的真实地址,如果是函数,则跳转到该地址执行,如果是变量则从该地址取值。
3.1.2 原因
一个程序由多个子程序构成,当程序被编译后,总是被设置从 0 地址开始加载,但是,每个子程序都从 0 地址开始加载,那么便乱套了。所以需要设置这些子程序从不同的地址加载,如:A 从 0x100 加载,B 从 0x200 加载。
3.1.3 原理
-
GOT 表 GOT 表是指.got 段和.got.plt 段,它们的区别是.got.plt 是给外部函数使用。GOT 表在链接时,会被修改为 “导入符号” 的真实地址。
-
PLT 表 PLT 表是指.plt 段,是一个中间跳转表。PLT 表在链接时,会被修改为如何跳转到 GOT 表的函数所在位置。
-
如何寻找符号真实地址
如图 1,当调用外部函数 strcmp 时,首先会跳转到.plt 段。通过
LDR PC, xxx
会修改 PC 寄存器地址,而这个地址指向 GOT 表中 strcmp 函数,如图 2 所示。而 GOT 表中则指定了 strcmp 的真实地址,如图 3 所示。图 1
图 2
图 3
-
依据什么修改 GOT 表?
在第 3 点中说到,在运行时,会根据某种规则修改 got 表。这种规则就是重定位表,重定位表常见的是:
.rel.dyn 和.rel.plt,常用于 Android 32 位;
还有.rela.dyn 和.rela.plt,和.rel 区别在于其数据结构多了一个 append 加数,Android 64 位一般是这种格式;
还有一种在 Android 6.0 之后支持的格式:.rel.dyn.aps2,.rela.plt.aps2,aps2 是 sleb128 编码格式的数据,读取时需要特别的解码逻辑。
上述 3 种格式都是同一种数据,32 位重定位表的表项结构如下:
typedef struct elf32_rela { Elf32_Addr r_offset; Elf32_Word r_info; Elf32_Sword r_addend; } Elf32_Rela; typedef struct elf32_rel { Elf32_Addr r_offset; Elf32_Word r_info; } Elf32_Rel;
r_offset:表示该符号在 got 表中的位置;
r_info:R_SYM 表示符号在符号表的索引。
#define ELF32_R_SYM(info) ((info)>>8) #define ELF32_R_TYPE(info) ((unsigned char)(info)) #define ELF32_R_INFO(sym, type) (((sym)<<8)+(unsigned char)(type)) #define ELF64_R_SYM(info) ((info)>>32) #define ELF64_R_TYPE(info) ((Elf64_Word)(info)) #define ELF64_R_INFO(sym, type) (((Elf64_Xword)(sym)<<32)+ \ (Elf64_Xword)(type))
r_addend:地址加数,当存在时,符号真实地址还需要+r_addend。
3.2 代码实例
介绍完重定位原理,接着看 Android 源码,前面系统通过 prelink_image
函数,为 soinfo 结构体赋值了符号表地址、动态字符串表地址、哈希表地址、重定位表地址等等。
但通过了解重定位的原因,赋值 soinfo 结构体是不够的,还需要将导入的外部符号进行重新定位。重定位部分位于 link_image
函数。
bool linked = local_group.visit([&](soinfo* si) {
// Even though local group may contain accessible soinfos from other namespaces
// we should avoid linking them (because if they are not linked -> they
// are in the local_group_roots and will be linked later).
if (!si->is_linked() && si->get_primary_namespace() == local_group_ns) {
const android_dlextinfo* link_extinfo = nullptr;
if (si == soinfos[0] || reserved_address_recursive) {
// Only forward extinfo for the first library unless the recursive
// flag is set.
link_extinfo = extinfo;
}
if (!si->link_image(global_group, local_group, link_extinfo, &relro_fd_offset) ||
!get_cfi_shadow()->AfterLoad(si, solist_get_head())) {
return false;
}
}
return true;
});
如下代码所示,APS2 格式的重定位表,会使用 sleb128_decoder
函数进行解码,然后再调用 relocate
函数进行重定位。紧接着判断是否存在 rela,如果存在,则使用 rela 进行重定位,否则使用 rel 进行重定位。
bool soinfo::link_image(const soinfo_list_t& global_group, const soinfo_list_t& local_group,
const android_dlextinfo* extinfo, size_t* relro_fd_offset) {
// APS2格式编码的重定位表会进行特殊解码
if (android_relocs_ != nullptr) {
// check signature
if (android_relocs_size_ > 3 &&
android_relocs_[0] == 'A' &&
android_relocs_[1] == 'P' &&
android_relocs_[2] == 'S' &&
android_relocs_[3] == '2') {
DEBUG("[ android relocating %s ]", get_realpath());
bool relocated = false;
const uint8_t* packed_relocs = android_relocs_ + 4;
const size_t packed_relocs_size = android_relocs_size_ - 4;
relocated = relocate(
version_tracker,
packed_reloc_iterator<sleb128_decoder>(
sleb128_decoder(packed_relocs, packed_relocs_size)),
global_group, local_group);
if (!relocated) {
return false;
}
} else {
DL_ERR("bad android relocation header.");
return false;
}
}
......
#if defined(USE_RELA)
if (rela_ != nullptr) {
DEBUG("[ relocating %s rela ]", get_realpath());
if (!relocate(version_tracker,
plain_reloc_iterator(rela_, rela_count_), global_group, local_group)) {
return false;
}
}
if (plt_rela_ != nullptr) {
DEBUG("[ relocating %s plt rela ]", get_realpath());
if (!relocate(version_tracker,
plain_reloc_iterator(plt_rela_, plt_rela_count_), global_group, local_group)) {
return false;
}
}
#else
if (rel_ != nullptr) {
DEBUG("[ relocating %s rel ]", get_realpath());
if (!relocate(version_tracker,
plain_reloc_iterator(rel_, rel_count_), global_group, local_group)) {
return false;
}
}
if (plt_rel_ != nullptr) {
DEBUG("[ relocating %s plt rel ]", get_realpath());
if (!relocate(version_tracker,
plain_reloc_iterator(plt_rel_, plt_rel_count_), global_group, local_group)) {
return false;
}
}
#endif
relocate
函数通过 for 循环遍历重定位表,获取重定位表包含的 type(类型)、sym(符号表索引)、reloc(GOT 表的符号地址)、addend(rela 具有的额外加数)。
bool soinfo::relocate(const VersionTracker& version_tracker, ElfRelIteratorT&& rel_iterator,
const soinfo_list_t& global_group, const soinfo_list_t& local_group) {
const size_t tls_tp_base = __libc_shared_globals()->static_tls_layout.offset_thread_pointer();
std::vector<std::pair<TlsDescriptor*, size_t>> deferred_tlsdesc_relocs;
for (size_t idx = 0; rel_iterator.has_next(); ++idx) {
const auto rel = rel_iterator.next();
if (rel == nullptr) {
return false;
}
ElfW(Word) type = ELFW(R_TYPE)(rel->r_info);
ElfW(Word) sym = ELFW(R_SYM)(rel->r_info);
ElfW(Addr) reloc = static_cast<ElfW(Addr)>(rel->r_offset + load_bias);
ElfW(Addr) sym_addr = 0;
const char* sym_name = nullptr;
ElfW(Addr) addend = get_addend(rel, reloc);
DEBUG("Processing \"%s\" relocation at index %zd", get_realpath(), idx);
if (type == R_GENERIC_NONE) {
continue;
}
....
第 7-15 行,通过 sym 索引获取符号名,然后通过 soinfo_do_lookup
函数获取符号信息。soinfo_do_lookup
函数通过哈希表快速定位符号,节省顺序寻找符号的时间。
需要解释一下,为何要用 soinfo_do_lookup
函数寻找符号。首先已经知道 sym,那么通过 symtab_[sym] 即可获取符号,但是这种方式,只是针对本模块的符号,对于 “导入符号”,本模块的符号表只会存储符号的 sym_name 和 sym_info 信息,如 strcmp,其他的值全为 0,图 4 所示。
所以,需要在外部模块进行寻找,而 soinfo_do_lookup
函数就是干这事的。
第 40 行,对于外部符号,通过 resolve_symbol_address
函数获取符号具体地址;
第 42-50 行,如果修改的段处于被保护状态,还需要进行保护解除操作;
第 55 行-x,根据 type,选择不同的赋值方式,如:R_GENERIC_JUMP_SLOT,则.got.plt 的指向的符号地址被修改 sym_addr + addend。
if (sym == 0) {
......
} else if (ELF_ST_BIND(symtab_[sym].st_info) == STB_LOCAL && is_tls_reloc(type)) {
......
} else {
sym_name = get_string(symtab_[sym].st_name);
const version_info* vi = nullptr;
if (!lookup_version_info(version_tracker, sym, sym_name, &vi)) {
return false;
}
if (!soinfo_do_lookup(this, sym_name, vi, &lsi, global_group, local_group, &s)) {
return false;
}
if (s == nullptr) {
......
} else { // We got a definition.
......
if (is_tls_reloc(type)) {
if (ELF_ST_TYPE(s->st_info) != STT_TLS) {
DL_ERR("reference to non-TLS symbol \"%s\" from TLS relocation in \"%s\"",
sym_name, get_realpath());
return false;
}
if (lsi->get_tls() == nullptr) {
DL_ERR("TLS relocation refers to symbol \"%s\" in solib \"%s\" with no TLS segment",
sym_name, lsi->get_realpath());
return false;
}
sym_addr = s->st_value;
} else {
if (ELF_ST_TYPE(s->st_info) == STT_TLS) {
DL_ERR("reference to TLS symbol \"%s\" from non-TLS relocation in \"%s\"",
sym_name, get_realpath());
return false;
}
sym_addr = lsi->resolve_symbol_address(s);
}
#if !defined(__LP64__)
if (protect_segments) {
if (phdr_table_unprotect_segments(phdr, phnum, load_bias) < 0) {
DL_ERR("can't unprotect loadable segments for \"%s\": %s",
get_realpath(), strerror(errno));
return false;
}
}
#endif
}
count_relocation(kRelocSymbol);
}
switch (type) {
case R_GENERIC_JUMP_SLOT:
count_relocation(kRelocAbsolute);
MARK(rel->r_offset);
TRACE_TYPE(RELO, "RELO JMP_SLOT %16p <- %16p %s\n",
reinterpret_cast<void*>(reloc),
reinterpret_cast<void*>(sym_addr + addend), sym_name);
*reinterpret_cast<ElfW(Addr)*>(reloc) = (sym_addr + addend);
break;
当重定位执行完成后,soinfo 结构体被设置为已链接状态。如果有被其他的 soinfo 结构体引用,则将 “引用计数” +1。至此,整个链接过程完成。返回到 do_dlopen
函数。
4. 执行构造函数
do_dlopen
函数中执行完 find_library
函数后,会执行当前共享对象的构造函数。
soinfo* si = find_library(ns, translated_name, flags, extinfo, caller);
loading_trace.End();
if (si != nullptr) {
void* handle = si->to_handle();
LD_LOG(kLogDlopen,
"... dlopen calling constructors: realpath=\"%s\", soname=\"%s\", handle=%p",
si->get_realpath(), si->get_soname(), handle);
si->call_constructors(); // 调用构造函数,实际上也是.init段
failure_guard.Disable();
LD_LOG(kLogDlopen,
"... dlopen successful: realpath=\"%s\", soname=\"%s\", handle=%p",
si->get_realpath(), si->get_soname(), handle);
return handle;
}
具体是去执行 DT_INIT 和 DT_INIT_ARRAY 包含的函数。
void soinfo::call_constructors() {
if (constructors_called) {
return;
}
constructors_called = true;
if (!is_main_executable() && preinit_array_ != nullptr) {
// The GNU dynamic linker silently ignores these, but we warn the developer.
PRINT("\"%s\": ignoring DT_PREINIT_ARRAY in shared library!", get_realpath());
}
get_children().for_each([] (soinfo* si) {
si->call_constructors();
});
if (!is_linker()) {
bionic_trace_begin((std::string("calling constructors: ") + get_realpath()).c_str());
}
// DT_INIT should be called before DT_INIT_ARRAY if both are present.
call_function("DT_INIT", init_func_, get_realpath());
call_array("DT_INIT_ARRAY", init_array_, init_array_count_, false, get_realpath());
if (!is_linker()) {
bionic_trace_end();
}
}
Comments NOTHING