省流:android 不兼容 glibc,而是写了一套独立的 c 运行时库 (bionic libc),为移动设备和 google 自己推的东西做了大量优化。在这套工具链里,aosp 实现了一个兼容 bionic libc 的链接器,放到系统中代替 ld。
这个链接器叫 linker,二进制放在 /bin 和 /system/bin 目录下。
好像有点跑题了,我都忘了在哪看到的
linker才写到这里的。可能是 frida 用了 Interceptor hook 的 linker
扯回来。在 Java 的应用层代码里写 System.loadLibrary 后,会经历如下调用栈
// java
System.loadLibrary
Runtime.loadLibrary0
// jni
// c
Runtime_nativeLoad
JVM_NativeLoad
// cc
art::JavaVMExt::LoadNativeLibrary // 检查是否是要 load 已有 library,不是的话调用 OpenNativeLibrary(dlopen) 获取 handle、创建 library 对象、查找 JNI_Onload 的 sym,没有的话直接 return true.
android::OpenNativeLibrary
OpenSystemLibrary ...(android_dlopen_ext/dlopen)
其中,从 nativeLoad 开始,执行流通过 jni 跳转到了 c 函数 Runtime_nativeLoad。可以查看 Runtime.c - Android Code Search ,往里跟踪的其他 c 文件,最后调用到 android_dlopen_ext,然后转到 linker.cpp 的 find_libraries
详解 linker find_libraries 的 7 步实现
详解每一层调用的博客:
Android动态库的加载原理,真的理解了吗?一文带你游览动态库的加载流程,从 Java 到 C++,一步一步探索知识的 - 掘金
这篇把无关代码删了,很方便阅读。实际进行链接操作的核心函数是 /bionic/linker/linker.cpp 的 find_libraries 函数。不过上文作者删的太多了,有点难以理解中间变量的意义,让 deepseek 打点注释好理解多了。
命名空间(与 c 做区分,是安卓linker 的加载命名空间),用于动态库加载隔离,记录了开始搜索的 start_from_path。
以下是我理解的关键逻辑:
Step 0: Prepare - 创建一个 load_tasks 数组,用形参的 libnames 初始化,此时还未查找库文件
Step 1: Extend load_tasks:调用 load_libraries_internal 函数从命名空间查找待加载库文件,扩充库文件的 DT_NEEDED 依赖到 load_tasks,添加 soinfo 到 needed_by 的字节点和 so_infos 数组。(在这一步打开文件,保存 fd 到 task 但不 mmap)
比对库文件的最底层调用是 strcmp(ns_name, sonane),namespace 的路径是在 linker.cpp: static const char* KDefaultLdPaths[] 定义的字符串指针数组
Step 2: Random order load:随机顺序加载库。按顺序把 load_task 放到 load_list 里,剔除已链接项和重复项,然后 shuffle,用 ElfReader 加载到内存(mmap segments)
Step 3: bfs pre-link all libs:预链接。解析 .dynamic 段的元数据。通过 ELF 的程序头表 (PH) 获取 .dynamic section 的地址,解析段中各种元数据,包括 (strtab, symtab, rel) 表的地址, size 等,保存在 si 的成员变量里。for + switch, 最后注册 tls。
Step 4: 覆盖设置 LD_PRELOAD 库的 DF_1_GLOBAL 标志
Step 5: collect roots of local_groups. 按照依赖顺序构建动态库链接树,维护 vector<soinfo*> 记录所有的 local_group 的 root。按 DT_NEEDED 和 namespace 划分
Step 6: Link,链接动态库,进行符号解析与重定位。
- 首先构建 (global_group, local_group) 的
lookup_list,调用soinfo::relocate(looup_list) - 根据各个段的存在性,先后顺次执行重定位逻辑,支持的段包括:
.relr,"APS2"(android 压缩.rel,.rela),(typical) .rela | (plt) .rela/(typical) .rel | (plt) .rel - 对段名 switch,调用模板函数,遍历每一项 rel[i]。 0. 从
r_offset计算要修改的地址rel_target(要修改的 got entry) 1. 解析:从r_index识别当前符号 2. 从 symtab 里通过 id 找到 sym_name 3. 调用lookup_symbol从动态库里找到符号,获取实际地址 3. 写入*rel_target - 最后处理
TLSDESC - 对于
-Bsymbolic写了单独处理逻辑
Offset Type Symbol Index Addend
0x0040 R_386_32 0x0000010 0
r_offset
Step 7: 将所有 load_tasks 标记为已链接,并增加 load_groups 间的引用数;
// 该函数用于查找并加载多个共享库,处理库之间的依赖关系
// add_as_children - 是否将一级加载的库(library_names[]中的库,不包括它们的传递依赖)
// 作为start_with库的子节点。当为dlopen()调用时设为false,
// 此时新加载的库必须形成独立的树结构
bool find_libraries(android_namespace_t* ns, // 使用的命名空间
soinfo* start_with, // 起始库(父节点)
const char* const library_names[], // 要加载的库名数组
size_t library_names_count, // 库名数组长度
soinfo* soinfos[], // 输出加载的soinfo指针数组
std::vector<soinfo*>* ld_preloads, // 预加载库列表
size_t ld_preloads_count, // 预加载库数量
int rtld_flags, // 加载标志(如RTLD_LAZY等)
const android_dlextinfo* extinfo, // 扩展加载信息
bool add_as_children, // 是否作为子节点添加
std::vector<android_namespace_t*>* namespaces) { // 相关命名空间列表
// 步骤0:准备工作
std::unordered_map<const soinfo*, ElfReader> readers_map; // 用于缓存ELF读取器
LoadTaskList load_tasks; // 加载任务列表
// 为每个库名创建初始加载任务
for (size_t i = 0; i < library_names_count; ++i) {
const char* name = library_names[i];
load_tasks.push_back(LoadTask::create(name, start_with, ns, &readers_map));
}
// 如果soinfos数组为空,在栈上分配临时空间
// 需要这个数组来处理加载部分失败的情况(例如加载多个库时部分成功)
if (soinfos == nullptr) {
size_t soinfos_size = sizeof(soinfo*)*library_names_count;
soinfos = reinterpret_cast<soinfo**>(alloca(soinfos_size)); // 栈上分配临时内存
memset(soinfos, 0, soinfos_size); // 初始化为空指针
}
size_t soinfos_count = 0; // 已加载的库数量
// 使用范围守卫确保加载任务会被清理
auto scope_guard = android::base::make_scope_guard([&]() {
for (LoadTask* t : load_tasks) {
LoadTask::deleter(t); // 退出时清理加载任务
}
});
ZipArchiveCache zip_archive_cache; // 用于APK内文件的缓存
soinfo_list_t new_global_group_members; // 新的全局组成员
// 步骤1:扩展加载任务列表,包含所有DT_NEEDED依赖(暂时不实际加载)
for (size_t i = 0; i<load_tasks.size(); ++i) {
LoadTask* task = load_tasks[i];
soinfo* needed_by = task->get_needed_by(); // 获取依赖该库的父库
// 判断是否是DT_NEEDED依赖(非直接加载的依赖)
bool is_dt_needed = needed_by != nullptr && (needed_by != start_with || add_as_children);
task->set_extinfo(is_dt_needed ? nullptr : extinfo); // 设置扩展信息
task->set_dt_needed(is_dt_needed); // 标记是否为DT_NEEDED依赖
// 从任务指定的命名空间开始查找(可能不同于当前命名空间)
android_namespace_t* start_ns = const_cast<android_namespace_t*>(task->get_start_from());
// 实际查找库的实现
if (!find_library_internal(start_ns, task, &zip_archive_cache, &load_tasks, rtld_flags)) {
return false; // 查找失败直接返回
}
soinfo* si = task->get_soinfo(); // 获取找到的soinfo对象
// 如果是DT_NEEDED依赖,添加到父库的子节点
if (is_dt_needed) {
needed_by->add_child(si);
}
// 处理预加载库(LD_PRELOAD)
bool is_ld_preload = false;
if (ld_preloads != nullptr && soinfos_count < ld_preloads_count) {
ld_preloads->push_back(si);
is_ld_preload = true;
}
// 填充输出数组
if (soinfos_count < library_names_count) {
soinfos[soinfos_count++] = si;
}
// 处理全局符号可见性(DF_1_GLOBAL标志)
if (is_ld_preload || (si->get_dt_flags_1() & DF_1_GLOBAL) != 0) {
if (!si->is_linked() && namespaces != nullptr && !new_global_group_members.contains(si)) {
new_global_group_members.push_back(si);
// 将新全局成员添加到所有初始命名空间
for (auto linked_ns : *namespaces) {
if (si->get_primary_namespace() != linked_ns) {
linked_ns->add_soinfo(si); // 添加到命名空间
si->add_secondary_namespace(linked_ns); // 添加二级命名空间
}
}
}
}
}
// 步骤2:随机顺序加载库(防止地址预测攻击)
LoadTaskList load_list;
// 收集所有需要加载的库(过滤已链接的)
for (auto&& task : load_tasks) {
soinfo* si = task->get_soinfo();
auto pred = [&](const LoadTask* t) { return t->get_soinfo() == si; };
if (!si->is_linked() && std::find_if(load_list.begin(), load_list.end(), pred) == load_list.end()) {
load_list.push_back(task);
}
}
// 处理地址保留标志
bool reserved_address_recursive = false;
if (extinfo) {
reserved_address_recursive = extinfo->flags & ANDROID_DLEXT_RESERVED_ADDRESS_RECURSIVE;
}
if (!reserved_address_recursive) {
shuffle(&load_list); // 随机打乱加载顺序
}
// 设置地址空间参数
address_space_params extinfo_params, default_params;
size_t relro_fd_offset = 0;
if (extinfo) {
// 处理保留地址相关的标志
if (extinfo->flags & ANDROID_DLEXT_RESERVED_ADDRESS) {
extinfo_params.start_addr = extinfo->reserved_addr;
extinfo_params.reserved_size = extinfo->reserved_size;
extinfo_params.must_use_address = true;
} else if (extinfo->flags & ANDROID_DLEXT_RESERVED_ADDRESS_HINT) {
extinfo_params.start_addr = extinfo->reserved_addr;
extinfo_params.reserved_size = extinfo->reserved_size;
}
}
// 实际加载库到内存
for (auto&& task : load_list) {
address_space_params* address_space =
(reserved_address_recursive || !task->is_dt_needed()) ? &extinfo_params : &default_params;
if (!task->load(address_space)) {
return false;
}
}
// 步骤3:预链接所有依赖库(广度优先顺序)
bool any_memtag_stack = false;
for (auto&& task : load_tasks) {
soinfo* si = task->get_soinfo();
if (!si->is_linked() && !si->prelink_image()) { // 执行预链接
return false;
}
// 检查内存标签(MTE)相关设置
if (si->has_min_version(7) && si->memtag_stack()) {
any_memtag_stack = true;
}
register_soinfo_tls(si); // 注册TLS信息
}
// 处理内存标签回调
if (any_memtag_stack) {
if (auto* cb = __libc_shared_globals()->memtag_stack_dlopen_callback) {
cb();
} else {
__libc_shared_globals()->initial_memtag_stack = true;
}
}
// 步骤4:强制设置LD_PRELOAD库的DF_1_GLOBAL标志
if (ld_preloads != nullptr) {
for (auto&& si : *ld_preloads) {
si->set_dt_flags_1(si->get_dt_flags_1() | DF_1_GLOBAL);
}
}
// 步骤5:收集本地组的根节点(处理跨命名空间的情况)
std::vector<soinfo*> local_group_roots;
if (start_with != nullptr && add_as_children) {
local_group_roots.push_back(start_with); // 起始库作为根
} else {
CHECK(soinfos_count == 1);
local_group_roots.push_back(soinfos[0]); // 第一个加载的库作为根
}
// 遍历加载任务查找跨命名空间的边界
for (auto&& task : load_tasks) {
soinfo* si = task->get_soinfo();
soinfo* needed_by = task->get_needed_by();
bool is_dt_needed = needed_by != nullptr && (needed_by != start_with || add_as_children);
android_namespace_t* needed_by_ns = is_dt_needed ? needed_by->get_primary_namespace() : ns;
// 如果库未链接且跨命名空间
if (!si->is_linked() && si->get_primary_namespace() != needed_by_ns) {
auto it = std::find(local_group_roots.begin(), local_group_roots.end(), si);
if (it == local_group_roots.end()) {
local_group_roots.push_back(si); // 添加为新的本地组根
}
}
}
// 步骤6:链接所有本地组
for (auto root : local_group_roots) {
soinfo_list_t local_group;
android_namespace_t* local_group_ns = root->get_primary_namespace();
// 遍历依赖树收集本地组成员
walk_dependencies_tree(root,
[&] (soinfo* si) {
if (local_group_ns->is_accessible(si)) { // 检查访问权限
local_group.push_back(si);
return kWalkContinue;
} else {
return kWalkSkip;
}
});
// 准备符号查找列表(全局组 + 本地组)
soinfo_list_t global_group = local_group_ns->get_global_group();
SymbolLookupList lookup_list(global_group, local_group);
soinfo* local_group_root = local_group.front(); // 本地组根节点
// 遍历并链接本地组成员
bool linked = local_group.visit([&](soinfo* si) {
if (!si->is_linked() && si->get_primary_namespace() == local_group_ns) {
// 处理扩展信息
const android_dlextinfo* link_extinfo = nullptr;
if (si == soinfos[0] || reserved_address_recursive) {
link_extinfo = extinfo;
}
// 调用加载hook(如果有)
if (__libc_shared_globals()->load_hook) {
__libc_shared_globals()->load_hook(si->load_bias, si->phdr, si->phnum);
}
// 处理符号查找
lookup_list.set_dt_symbolic_lib(si->has_DT_SYMBOLIC ? si : nullptr);
if (!si->link_image(lookup_list, local_group_root, link_extinfo, &relro_fd_offset) ||
!get_cfi_shadow()->AfterLoad(si, solist_get_head())) {
return false;
}
}
return true;
});
if (!linked) {
return false;
}
}
// 步骤7:标记所有加载任务为已链接,并更新引用计数
if (start_with != nullptr && add_as_children) {
start_with->set_linked();
}
// 标记所有库为已链接状态
for (auto&& task : load_tasks) {
soinfo* si = task->get_soinfo();
si->set_linked();
}
// 更新跨组的引用计数
for (auto&& task : load_tasks) {
soinfo* si = task->get_soinfo();
soinfo* needed_by = task->get_needed_by();
if (needed_by != nullptr &&
needed_by != start_with &&
needed_by->get_local_group_root() != si->get_local_group_root()) {
si->increment_ref_count(); // 增加引用计数
}
}
return true; // 全部加载成功
}
符号解析与重定位
符号解析(Symbol Resolution)
符号解析是指动态链接器(如 Android 的 linker 或 GNU ld.so)在加载共享库(so 文件)时,查找和绑定程序所需的符号(函数或变量)的过程。
1. 符号表(Symbol Table)
ELF 可执行文件和共享库中包含 符号表,它存储了所有函数和全局变量的信息,每个符号都有:
- 符号名称(Name):如
printf - 符号地址(Address):符号在内存中的位置(或者偏移量)
- 符号类型(Type):函数、全局变量等
- 符号大小(Size):符号占用的字节数
- 符号绑定(Binding):
STB_LOCAL:本地符号,仅在当前共享库或可执行文件可见STB_GLOBAL:全局符号,可以被其他库引用STB_WEAK:弱符号,如果存在全局符号,则使用全局符号,否则使用弱符号
ELF 符号表通常存储在 DT_SYMTAB(符号表)和 DT_STRTAB(字符串表)中,前者包含符号信息,后者存储符号的名称。
2. 符号解析的步骤
-
查找符号:
- 动态链接器会遍历加载的所有共享库,并在符号表中查找所需符号。
- 现代 ELF 采用 GNU Hash Table (
DT_GNU_HASH) 或 传统哈希表 (DT_HASH) 加速查找。
-
符号作用域:
- 符号解析遵循 ELF 共享库加载顺序(即
DT_NEEDED的顺序)。 - 若启用了
DF_SYMBOLIC(DT_FLAGS中设置),则先在本共享库中查找,再去其他库查找。 - 如果启用了
RTLD_LOCAL(默认),则库内符号仅对自己可见;如果使用RTLD_GLOBAL,则符号可以被后续加载的库使用。
- 符号解析遵循 ELF 共享库加载顺序(即
-
符号冲突解决:
- 若两个库定义了同名全局符号,默认使用 先加载的库 中的符号(依赖 ELF 加载顺序)。
STB_WEAK符号如果有同名的STB_GLOBAL,则STB_GLOBAL具有更高优先级。
重定位(Relocation)
重定位是指当 ELF 可执行文件或共享库被加载到内存后,调整其中的符号地址,使其指向正确的内存位置的过程。
1. 为什么需要重定位?
-
共享库地址不固定:
- 共享库使用 位置无关代码(PIC,Position Independent Code),使其可以加载到任意地址,但某些符号仍然需要修正地址。
- Android 64 位系统禁止 TEXTREL(文本段重定位),要求所有数据都在数据段完成重定位。
-
符号地址动态解析:
- 动态链接库中的函数和变量地址在编译时无法确定,运行时需要根据加载地址调整。
2. 重定位表(Relocation Table)
ELF 提供 重定位表,存储需要调整的地址信息:
DT_RELA或DT_REL:存储所有需要重定位的项。DT_JMPREL:存储PLT/GOT相关的重定位信息(用于函数调用)。DT_PLTGOT:指向 PLT/GOT 表,帮助间接函数调用。
3. 重定位类型
ELF 体系结构定义了多种重定位方式,不同架构可能有所不同,常见的包括:
R_xxx_RELATIVE:- 直接调整地址,适用于全局变量。
- 例如
R_AARCH64_RELATIVE、R_386_RELATIVE。
R_xxx_GLOB_DAT:- 全局数据的符号地址需要在动态链接时解析,如
printf。
- 全局数据的符号地址需要在动态链接时解析,如
R_xxx_JUMP_SLOT:- 用于**延迟绑定(Lazy Binding)**的 PLT/GOT 机制,主要用于函数调用。
R_xxx_ABS:- 绝对地址重定位,一般用于静态编译的二进制。
4. 重定位过程
-
读取重定位表
DT_RELA/DT_REL:- 遍历重定位表中的所有项,每个项包含:
- 重定位类型(如
R_AARCH64_RELATIVE) - 需要修改的地址(Addend)
- 符号索引(如果涉及符号解析)
- 重定位类型(如
- 遍历重定位表中的所有项,每个项包含:
-
计算新地址:
- 如果是
R_xxx_RELATIVE,直接加上load_bias进行修正。 - 如果是
R_xxx_GLOB_DAT,则需要从符号表解析出地址。 - 如果是
R_xxx_JUMP_SLOT,则修正 GOT 表中存储的目标函数地址。
- 如果是
-
写入新地址:
- 动态链接器将计算后的地址写入相应的 GOT 或内存地址。
示例:函数调用的重定位
假设 main() 调用了 printf(),但 printf() 在 libc.so 里,编译时并不知道 printf 的确切地址,运行时需要修正。
1. PLT(Procedure Linkage Table)
- 当
main()调用printf()时,代码不会直接跳转到printf,而是跳转到 PLT(过程链接表)。 - PLT 里的第一条指令会间接跳转到 GOT(全局偏移表) 中存储的
printf入口地址。
2. GOT(Global Offset Table)
- 初始时,GOT 里
printf对应的项指向 PLT 自己的入口。 - 当第一次调用
printf()时,PLT 触发lazy binding(延迟绑定)。 - 动态链接器在
DT_JMPREL表里找到printf对应的R_xxx_JUMP_SLOT记录。 - 解析
printf的真实地址,并填充到 GOT 中。 - 之后的
printf调用就可以直接从 GOT 跳转到printf的真实地址。
总结
- 符号解析:查找 ELF 符号表,确定符号的真实地址。
- 重定位:修正共享库加载后的符号地址,使其指向正确的位置。
- 关键机制:
- 采用 ELF 符号表(SYMTAB, STRTAB) 存储符号信息。
- 使用 哈希表(GNU Hash, DT_HASH) 加速符号查找。
- 重定位表(DT_RELA, DT_REL, DT_JMPREL) 存储需要修正的地址信息。
- PLT/GOT 机制 进行延迟绑定,提高性能。
理解这些概念对于分析 Android 动态库加载机制、逆向分析 so 文件非常重要!你可以结合 objdump -D 或 readelf -r 查看具体的 ELF 重定位信息,以加深理解。
symtab 符号表条目
每个条目关联了一个地址(函数/变量)到一个字符串池 id
在 ELF (Executable and Linkable Format) 文件中,符号表(symtab)是一个保存符号信息的结构数组。每个符号条目(ElfN_Sym,N 表示 32 或 64 位架构)包含以下字段:
符号表条目结构定义
以 64 位架构为例,符号表条目定义如下(定义在 <elf.h> 中):
typedef struct {
Elf64_Word st_name; // 符号名称在字符串表中的偏移量
unsigned char st_info; // 符号类型和绑定信息
unsigned char st_other; // 符号的可见性(如默认/隐藏)
Elf64_Section st_shndx; // 关联的段索引(如 .text、.data)
Elf64_Addr st_value; // 符号的地址(内存中的偏移量)
Elf64_Xword st_size; // 符号的大小(如函数代码大小)
} Elf64_Sym;
字段解释及示例
1. st_name
- 作用:符号名称在字符串表(
.strtab)中的偏移量。 - 示例:
- 若
st_name = 0x10,表示符号名称存储在字符串表第 16 字节的位置。 - 若字符串表在
0x2000处,则符号名称地址为0x2000 + 0x10 = 0x2010。
- 若
2. st_info
- 作用:高 4 位表示符号绑定(全局/局部/弱引用),低 4 位表示符号类型(函数/变量/未定义等)。
- 解析方法:
- 绑定类型:
ELF64_ST_BIND(info)STB_GLOBAL(1):全局符号(如printf)STB_LOCAL(0):局部符号(如静态函数)STB_WEAK(2):弱引用符号(可被覆盖)
- 符号类型:
ELF64_ST_TYPE(info)STT_FUNC(2):函数(如main)STT_OBJECT(1):变量(如全局变量int global_var)STT_NOTYPE(0):未定义符号(如外部依赖的符号)
- 绑定类型:
示例:
st_info = 0x12(二进制0001_0010):- 高 4 位
0001:STB_GLOBAL(全局符号) - 低 4 位
0010:STT_FUNC(函数)
- 高 4 位
3. st_other
- 作用:符号的可见性(默认为 0,表示默认可见性)。
- 常见值:
STV_DEFAULT(0):符号可被其他模块访问。STV_HIDDEN(1):符号仅在本模块可见(静态链接时隐藏)。
4. st_shndx
- 作用:符号关联的段索引(如
.text段、.data段)。 - 特殊值:
SHN_UNDEF(0):未定义的符号(如外部依赖的malloc)。SHN_ABS(0xfff1):绝对地址符号(如编译时常量)。
示例:
st_shndx = 1:符号关联到.text段(段索引由段表定义)。
5. st_value
- 作用:符号的地址(在可执行文件中的逻辑偏移,加载时会被重定位)。
- 示例:
- 若符号是函数,
st_value是函数在代码段(.text)中的偏移量。 - 若符号是全局变量,
st_value是变量在数据段(.data)中的偏移量。
- 若符号是函数,
6. st_size
- 作用:符号的大小(如函数代码的大小或变量的字节数)。
- 示例:
- 函数
int foo() { ... }编译后代码大小为 20 字节,则st_size = 20。
- 函数
具体示例
假设我们有一个全局函数 void my_func() 和一个全局变量 int global_var,它们的符号表条目如下:
1. 函数符号 my_func
Elf64_Sym entry_func = {
.st_name = 0x20, // 字符串表中偏移 0x20 处是 "my_func"
.st_info = 0x12, // STB_GLOBAL | STT_FUNC
.st_other = 0, // 默认可见性
.st_shndx = 1, // 关联到 .text 段(段索引 1)
.st_value = 0x4000, // 函数在 .text 段的偏移地址
.st_size = 0x50, // 函数代码大小为 80 字节
};
2. 变量符号 global_var
Elf64_Sym entry_var = {
.st_name = 0x30, // 字符串表中偏移 0x30 处是 "global_var"
.st_info = 0x11, // STB_GLOBAL | STT_OBJECT
.st_other = 0, // 默认可见性
.st_shndx = 3, // 关联到 .data 段(段索引 3)
.st_value = 0x5000, // 变量在 .data 段的偏移地址
.st_size = 4, // int 类型占 4 字节
};
3. 未定义符号 malloc(外部依赖)
Elf64_Sym entry_undef = {
.st_name = 0x40, // 字符串表中偏移 0x40 处是 "malloc"
.st_info = 0x10, // STB_GLOBAL | STT_NOTYPE
.st_other = 0,
.st_shndx = SHN_UNDEF,// 未定义(需要动态链接)
.st_value = 0, // 未定义符号的地址为 0
.st_size = 0, // 大小未知
};
如何查看符号表?
使用 readelf 或 objdump 工具可以查看 ELF 文件的符号表:
readelf -s libexample.so # 查看符号表
objdump -t libexample.so # 显示符号表条目
输出示例:
Symbol table '.dynsym' contains 5 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000004000 80 FUNC GLOBAL DEFAULT 1 my_func
2: 0000000000005000 4 OBJECT GLOBAL DEFAULT 3 global_var
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND malloc@GLIBC_2.34
总结
- 符号表条目:每个条目描述了一个符号的名称、类型、绑定方式、所在段和地址。
- 关键字段:
st_info决定了符号的全局性(如是否可被其他模块访问)和类型(函数/变量)。 - 动态链接:未定义符号(
SHN_UNDEF)需要在运行时通过动态链接器解析。
理论讲解的 ref
.symtab 和 .strtab 是常规符号表,可以 strip 删了。.dynstr, .dynsym 是动态符号表,动态链接要用
链接器的工作:填充 GOT(global offset table) 表,更新外部符号的位置。动态库里,实现函数跳转的方法是,跳转到
.got[i]。汇编里写死 got 表的偏移间接寻址.
如果是 plt 模式,got 内填写 plt 表 (Procedure Linkage Table) 内容,再次间接跳转到_dl_runtime_resolve
1311



被折叠的 条评论
为什么被折叠?



