目录
-
问题描述
-
初步排查
-
_Unwind_resume
-
ndk bug
-
根因分析
-
解决方案
-
启示
-
附录1:Android虚拟机so加载流程
-
dlopen之前的函数调用流转过程:
-
dlopen之后的函数调用流转过程:
-
附录2:ELF文件格式基础
-
链接视图和执行视图
-
dynsym构成
问题描述
9.3.10灰度阶段收到大量Android 5.1设备崩溃,被秋实大佬@薛秋实拉入相关调查群,报错如下:
java.lang.UnsatisfiedLinkError: dlopen failed: cannot locate symbol "_Unwind_Resume" referenced by "/data/data/com.kuaishou.nebula/app_lib/libwesteros.so
看报错是System.load加载拍摄sdk的so westeros时失败了,具体的错误是"_Unwind_Resume"符号查找失败了。
P.S.:虚拟机加载so的更多源码细节,请参考附录1。
初步排查
此类问题,根据过往的问题排查经验,推测是ndk工具链不统一导致的ABI兼容性bug,立即让拍摄sdk同学@王文峰查看一下westeros的编译工具链版本有无发生变化:
readelf libwesteros.so --string-dump=.note.android.ident
显示最近几个版本没变化,都是ndk r20:
String dump of section ‘.note.android.ident‘:
[ c] Android
[ 18] r20
[ 58] 5594570
于是又让拍摄同学看了一下westeros的符号依赖有没有变化:
readelf -s libwesteros.so | grep _Unwind_Resume
依然没有变化,都是:
0000000000000000 0 FUNC GLOBAL DEFAULT UND _Unwind_Resume
UND代表undefined,最近几个版本都是UND,问题可能出现在了westeros动态依赖的库上,首先怀疑的是“惯犯”libc++。因为不同的ndk版本,会对应不同版本的libc++_shared.so,libc++的ABI并不稳定,不一致时也会导致一些ABI兼容问题,于是解压查看主站apk里最近两个版本的libc++,却发现仍然没有变化,都是:
String dump of section ‘.note.android.ident‘:
[ c] Android
[ 18] r20
[ 58] 5594570
而且,此版本的libc++并不存在_Unwind_Resume符号。
问题复杂了起来,看来并不是简单的westeros切换了编译环境导致的问题,灰度版本不能delay。
这时,拍摄同学发现回退audioprocesslib能解决问题,测试同学发现32位没问题,只有64位才会崩。两个奇怪的现象,都不能从原理层面解释,只能先回退代码临时解决问题。
P.S.:ELF文件格式的更多信息请参考附录2
_Unwind_resume
需要找到根因,首先研究一下_Unwind_resume的原理。
_Unwind_resume是用于处理c++异常的,已经被写入了C++ ABI规范。
prebuilts/gcc/darwin-x86/aarch64/aarch64-linux-android-4.9/lib/gcc/aarch64-linux-android/4.9.x/include/unwind.h 代码片段:
/* Resume propagation of an existing exception. This is used after
e.g. executing cleanup code, and not to implement rethrowing. */
extern void LIBGCC2_UNWIND_ATTRIBUTE
_Unwind_Resume (struct _Unwind_Exception *);
以ndk _Unwind_Resume为关键字搜索,很容易得到以下信息:
Common Issues
Unwinding
The NDK uses LLVM‘s libunwind. libunwind is needed to provide C++ exception handling support and C’s __attribute__((cleanup)). The unwinder is linked automatically by Clang, and is built with hidden visibility to avoid shared libraries re-exporting the unwind interface.
Until NDK r23, libgcc was the unwinder for all architectures other than 32-bit ARM, and even 32-bit ARM used libgcc to provide compiler runtime support. Libraries built with NDKs older than r23 by build systems that did not follow the advice in this document may re-export that incompatible unwinder. In this case those libraries can prevent the correct unwinder from being used by your build, resulting in crashes or incorrect behavior at runtime.
The best way to avoid this problem is to ensure all libraries in the application were built with NDK r23 or newer.
信息量挺大的,看上去像是个经典问题:
- ndk r23以前,ndk的unwinder都是使用的libgcc,即在ndk r23以前_Unwind_Resume的实现都是使用的libgcc.a(后文会讲,实际上需要是libgcc_real.a)
- 如果有错误的依赖顺序,导致unwinder符号重复导出,将会带来运行时错误
- ndk已经做了unwinder符号隐藏,避免了unwinder符号重复导出
看上去都正常,而且ndk已经做了规避(不过比较扯的是建议大家都切换到ndk 23重新编一遍),那么问题出在哪呢?
ndk bug
ndk没有使用github作为代码托管工具进行持续集成开发,但是使用了github作为bug追踪反馈平台,仔细查阅过往的ndk issue,终于发现几条相关内容:
- [BUG] unwinder symbols are no longer hidden with ndk-build #1092 : DanAlbert也就是官方ndk维护者发现,unwinder符号在ndk r19下并不会隐藏(潜台词过去一直是隐藏的),原因是-Wl,--exclude-libs,libgcc.a不再生效,libgcc.a从ndk r19开始只是一个脚本(汗??),需要-Wl,--exclude-libs,libgcc_real.a,并自己提了一个patch。(P.S.: -Wl,--exclude-libs的作用是避免静态库导入的符号在编译产物里重复导出),libgcc.a脚本截图:
- [BUG] libc++_shared.so exposes libgcc _Unwind_* symbols #1166:有人发现ndk r21自带的arm64的libc++_shared.so暴露了unwiner符号、armeabi-v7a没问题,这其实就是上一个bug的受害者,只是由于libc++的普遍使用再加上下文描述的第3个ndk bug,会使得这个问题极具传染性,具体符号对比:
r21
$ for lib in /x/ndk-release-r21/out/android-ndk-r21/sources/cxx-stl/llvm-libc++/libs/*/libc++_shared.so; do printf ‘\n%s\n‘ $lib; readelf --dyn-syms -W $lib | grep ‘ _Unwind_Resume$‘; done
/x/ndk-release-r21/out/android-ndk-r21/sources/cxx-stl/llvm-libc++/libs/arm64-v8a/libc++_shared.so
1930: 00000000000b5e40 252 FUNC GLOBAL DEFAULT 11 _Unwind_Resume
/x/ndk-release-r21/out/android-ndk-r21/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_shared.so
r20
$ for lib in /x/android-ndk-r20/sources/cxx-stl/llvm-libc++/libs/*/libc++_shared.so; do printf ‘\n%s\n‘ $lib; readelf --dyn-syms -W $lib | grep ‘ _Unwind_Resume$‘; done
/x/android-ndk-r20/sources/cxx-stl/llvm-libc++/libs/arm64-v8a/libc++_shared.so
/x/android-ndk-r20/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_shared.so
上述第二个libc++_shared.so重复导出unwind符号导出的可怕之处,请看第三个“bug”:
- 前文描述的ndk common issue的最后,有写建议的link顺序是:
The following link order will protect against incorrectly built dependencies:
- crtbegin
- object files
- static libraries
- libunwind
- shared libraries
- crtend
可以看到静态库是建议优先于动态库的,但是让我们给link flags加上-Wl,-trace(跟宝哥@王连宝学的)看下实际上的默认link顺序(以koom为例):
/llvm-sysroot/usr/lib/aarch64-linux-android/21/crtbegin_so.o
CMakeFiles/koom-java.dir/src/main/cpp/hprof_dump.cpp.o
/xxx-path/arm64-v8a/libxhook_lib.so
/xxx-path/arm64-v8a/libkwai-linker.so
/llvm-sysroot/usr/lib/aarch64-linux-android/21/liblog.so
/llvm-sysroot/usr/lib/aarch64-linux-android/21/libm.so
/xxx-path/arm64-v8a/libc++_shared.so
/llvm-sysroot/usr/lib/aarch64-linux-android/21/libm.so
/llvm-prebuilt/darwin-x86_64/lib/gcc/aarch64-linux-android/4.9.x/libgcc_real.a(unwind-dw2.o)
/llvm-prebuilt/darwin-x86_64/lib/gcc/aarch64-linux-android/4.9.x/libgcc_real.a(unwind-dw2-fde-dip.o)
/llvm-prebuilt/darwin-x86_64/sysroot/usr/lib/aarch64-linux-android/21/libdl.so
/llvm-prebuilt/darwin-x86_64/sysroot/usr/lib/aarch64-linux-android/21/libc.so
/llvm-prebuilt/darwin-x86_64/sysroot/usr/lib/aarch64-linux-android/21/libdl.so
/llvm-prebuilt/darwin-x86_64/sysroot/usr/lib/aarch64-linux-android/21/crtend_so.o
实际的顺序是:crtbegin > object files > 项目自身shared libraries > ndk 部分shared libraries > libgcc静态库 > crtend。这个顺序和建议的顺序完全不一样-_-||。
这个“bug”会导致,假设任意一个动态库重复导出了unwind符号,后续动态link依赖此so的库编译时,会优先选择此动态库里的unwind符号,这显然是不合理的,先不说稳定性、性能开销就变大了(plt调用多了一次跳转)。
貌似离真相很近了,但直接回答我们遇到的问题,还有些距离。
根因分析
回顾下前面的两个现象:
- 回退audioprocesslib能解决问题
- 测试同学发现32位没问题
先查看westeros的所有依赖库:
aarch64-linux-android-readelf libwesteros.so -d
Dynamic section at offset 0x123ed0 contains 39 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libprotobuf-lite.so]
0x0000000000000001 (NEEDED) Shared library: [libdaenerys.so]
0x0000000000000001 (NEEDED) Shared library: [libandroid.so]
0x0000000000000001 (NEEDED) Shared library: [liblog.so]
0x0000000000000001 (NEEDED) Shared library: [libEGL.so]
0x0000000000000001 (NEEDED) Shared library: [libjnigraphics.so]
0x0000000000000001 (NEEDED) Shared library: [libOpenSLES.so]
0x0000000000000001 (NEEDED) Shared library: [libGLESv2.so]
0x0000000000000001 (NEEDED) Shared library: [libffmpeg.so]
0x0000000000000001 (NEEDED) Shared library: [libz.so]
0x0000000000000001 (NEEDED) Shared library: [libm.so]
0x0000000000000001 (NEEDED) Shared library: [libc++_shared.so]
0x0000000000000001 (NEEDED) Shared library: [libdl.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so]
0x000000000000000e (SONAME) Library soname: [libwesteros.so]
可以看出westeros并没有直接依赖audioprocesslib,于是依次排查每一个so和audioprocesslib的依赖情况以及unwind符号导出情况,终于发现问题出在了daenerys:
有问题的版本:
aarch64-linux-android-readelf libdaenerys.so -aW | grep _Un
000000000026ea48 000000ad00000402 R_AARCH64_JUMP_SLOT 0000000000000000 _Unwind_Resume + 0173: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _Unwind_Resume
正常的版本:
aarch64-linux-android-readelf libdaenerys.so -aW | grep _Unwind_R
0000000000272548 0000095e00000402 R_AARCH64_JUMP_SLOT 00000000001ca06c _Unwind_RaiseException + 0
0000000000272a00 000008c600000402 R_AARCH64_JUMP_SLOT 00000000001ca2d8 _Unwind_Resume + 02246: 00000000001ca2d8 252 FUNC GLOBAL DEFAULT 11 _Unwind_Resume
可以看到从正常到有问题,_Unwind_resume的Ndx(elf section index)从11变成了UND(undefined)。
实际上正是daenerys依赖了audioprocesslib,再去看audioprocesslib的_Unwind_resume符号依赖情况变化:果然,audioprocesslib的导出状态也发生了变化,旧版本的audioprocesslib是没有导出_Unwind_Resume的,但是新版本也就是有问题的版本变为了导出状态也就是.dynsym里搜索的结果是GLOBAL DEFAULT Ndx不是UND。
这时,一起定位的@陈翔宇同学也反馈发现audioprocesslib最近确实编译工具链从ndk r17升级了ndk版本到ndk r20,终于,所有的隐藏信息都齐了。
现在,我们基本可以推断出:首先ndk bug导致audioprocesslib升级工具链后audioprocesslib编译产物unwind符号导出状态变化,进而导致连锁反应daenerys的符号导出产生变化。
另外,雪上加霜的是,我发现虽然apk包里的daenerys是新版本,但是westeros动态link的是旧版本的daenerys。
然后,32位没问题的原因,参考ndk issue链接,32位没有使用libgcc.a,使用的libunwind.a始终是第一个link的;android高版本没问题的原因,是高版本系统库也意外导出了unwind符号,刚好可以查找的到。
这一系列阴差阳错的连锁反应,导致了前面所述的奇怪现象。
一图胜千言。--鲁迅
用一副图来总结这个bug的产生:
上图黑色代表原有逻辑,红色代表变更后引入bug的逻辑。
解决方案
一个最稳妥的解决方案:和ndk官方的修复方案类似,在编译选项link flags里面加上-Wl,--exclude-libs,libgcc_real.a,手动排除libgcc的unwind相关符号的重复导出,就可以规避ndk的这个bug,还有减少符号导出带来的包大小和性能收益。
其他方案:修改link的顺序,保证libgcc静态库在动态库link的顺序前面。
启示
随着工具链的升级普及,可能越来越多团队会类似甚至完全一样的问题,既有库一行代码不改只是重编了一下就会导致这个bug,难免会使得首次遇到这个问题时手忙脚乱,事实上这也是写这篇经验分享文章的理由之一。如前段时间在大群里看到的__aeabi_ldiv0的查找失败问题:
java.lang.UnsatisfiedLinkError: dlopen failed: cannot locate symbol "__aeabi_ldiv0" referenced by "xxx.so"
- 广告时间:集成Android Native Plugin,每次打包时产出工具链及编译选项检查,遇到类似问题不再慌
- 普通的依赖头文件执行的abi检查来尝试规避abi兼容性问题的方案,会有漏网之鱼
- 保证编译环境的一致性:1.link版本和apk实际版本保持一致 2.各个so的ndk工具链保持一致
- ndk并不完全靠谱,自建工具链,研究linker script,对link阶段更多掌控,控制crtbegin/crtend等
附录1:Android虚拟机so加载流程
Java虚拟机的JNI规范,以动态链接库即so的形式提供从java到native侧的调用,和传统Linux应用一样,动态库需要通过dlopen来动态加载,android java虚拟机so的加载如果以dlopen为界限的话,可以分为两个阶段,第一个阶段在虚拟机测由System.load发起,第二个阶段在dynamic linker侧由android_dlopen_ext发起(android_dlopen_ext是dlopen的android扩展实现,在原有的dlopen(3)上增加了一个const android_dlextinfo *__info参数)。额外提一句,当这两个过程完成后,虚拟机会使用dlsym查询so的JNI_OnLoad方法并调用。
以Android 11 Art虚拟机的so加载为例,分析具体源码。
dlopen之前的函数调用流转过程:
- libcore/ojluni/src/main/java/java/lang/System.java,load
- libcore/ojluni/src/main/java/java/lang/Runtime.java load0
- libcore/ojluni/src/main/native/Runtime.c Runtime_nativeLoad
- art/openjdkjvm/OpenjdkJvm.cc JVM_NativeLoad
- art/runtime/jni/java_vm_ext.cc JavaVMExt::LoadNativeLibrary
- art/libnativeloader/native_loader.cpp OpenNativeLibrary
最终调用:
void* handle = android_dlopen_ext(path, RTLD_NOW, &dlextinfo);
这一过程主要为虚拟机侧处理so的加载路径,调用方classloader对于的namespace权限,so重复加载判断等逻辑。且可以看到dlopen的flags传参为RTLD_NOW,代表所有未定义符号(UND symbol)都需要在加载阶段就被解析。
dlopen之后的函数调用流转过程:
android和dynamic linker有关的文件有两个,libdl.so和/system/bin/linker(或/system/bin/linker64),libdl只是一些的桩函数,实际的实现在linker,从dlopen调用开始的源码调用流程为:
- bionic/linker/dlfcn.cpp __loader_android_dlopen_ext -> dlopen_ext
- bionic/linker/linker.cpp do_dlopen -> find_library -> find_library_internal -> load_library
我们这里关心的so加载的主要工作,就在find_library完成了so的装载(load)和链接(link),按照随机(load会调用shuffle为了安全)的策略将DT_NEED间接依赖的so执行load,按照广度优先的策略将DT_NEED间接依赖的so执行link,具体单次某个so的load和link具体流程为:
- 装载load:读取program header(elf文件格式参考附录2),并根据program header的信息,计算so需要的内存空间大小并通过mmap分配相应大小内存,用mmap映射的方式以segment为单位将PT_LOAD类型的所有segment装载到内存,代码:
//bionic/linker/linker_phdr.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) {
int prot = PFLAGS_TO_PROT(phdr->p_flags);
if ((prot & (PROT_EXEC | PROT_WRITE)) == (PROT_EXEC | PROT_WRITE)) {
// W + E PT_LOAD segments are not allowed in O.if (get_application_target_sdk_version() >= 26) {
DL_ERR_AND_LOG("\"%s\": W+E load segments are not allowed", name_.c_str());
return false;
}
DL_WARN_documented_change(26,
"writable-and-executable-segments-enforced-for-api-level-26",
"\"%s\" has load segments that are both writable and executable",
name_.c_str());
add_dlwarning(name_.c_str(), "W+E load segments");
}
void* seg_addr = mmap64(reinterpret_cast<void*>(seg_page_start),
file_length,
prot,
MAP_FIXED|MAP_PRIVATE,
fd_,
file_offset_ + file_page_start);
...
}
...
}
...
}
- 链接link:link阶段会首先解析.dynamic section的内容,将解析到的如strtab/symtab等存入linker保存的soinfo中;然后再将需要重定位的地址修改为绝对地址。解析dynamic section的代码:
//bionic/linker/linker_phdr.cpp
bool soinfo::prelink_image() {
...
/* Extract dynamic section */
ElfW(Word) dynamic_flags = 0;
phdr_table_get_dynamic_section(phdr, phnum, load_bias, &dynamic, &dynamic_flags);
...
for (ElfW(Dyn)* d = dynamic; d->d_tag != DT_NULL; ++d) {
...
switch (d->d_tag) {
...
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_GNU_HASH:
gnu_nbucket_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[0];
// skip symndx
gnu_maskwords_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[2];
gnu_shift2_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[3];
gnu_bloom_filter_ = reinterpret_cast<ElfW(Addr)*>(load_bias + d->d_un.d_ptr + 16);
gnu_bucket_ = reinterpret_cast<uint32_t*>(gnu_bloom_filter_ + gnu_maskwords_);
// amend chain for symndx = header[1]
gnu_chain_ = gnu_bucket_ + gnu_nbucket_ -
reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[1];
...
--gnu_maskwords_;
flags_ |= FLAG_GNU_HASH;
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;
...
}
...
}
...
}
重定位主要针对的是导入符号即调用的外部符号,这些符号在编译时的地址是未知的所以需要重定位,具体重定位的内容为附录2中的.rel.dyn和.rel.plt section中的符号,二者的区别为前者是数据的重定位,后者为函数调用的重定位,基本流程代码:
//bionic/linker/linker_relocate.cpp
bool soinfo::relocate(const SymbolLookupList& lookup_list) {
...
Relocator relocator(version_tracker, lookup_list);
...
if (relr_ != nullptr) {
DEBUG("[ relocating %s relr ]", get_realpath());
if (!relocate_relr()) {
return false;
}
}
#if defined(USE_RELA)if (rela_ != nullptr) {
DEBUG("[ relocating %s rela ]", get_realpath());
if (!plain_relocate<RelocMode::Typical>(relocator, rela_, rela_count_)) {
return false;
}
}
if (plt_rela_ != nullptr) {
DEBUG("[ relocating %s plt rela ]", get_realpath());
if (!plain_relocate<RelocMode::JumpTable>(relocator, plt_rela_, plt_rela_count_)) {
return false;
}
}
#elseif (rel_ != nullptr) {
DEBUG("[ relocating %s rel ]", get_realpath());
if (!plain_relocate<RelocMode::Typical>(relocator, rel_, rel_count_)) {
return false;
}
}
if (plt_rel_ != nullptr) {
DEBUG("[ relocating %s plt rel ]", get_realpath());
if (!plain_relocate<RelocMode::JumpTable>(relocator, plt_rel_, plt_rel_count_)) {
return false;
}
}
#endif
可以看到分别执行了.rel.dyn和.rel.plt的重定位,限于篇幅的原因,上述代码relocator的具体实现不再展开。大体原理为遍历每个重定位项,根据每个符号的字符串名称去依赖的so中查找此符号,查找成功后将地址修正为重定位后的地址,如查找失败即报错,本文这个案例,正是"_Unwind_resume"符号,没有在依赖的so中查找到。
附录2:ELF文件格式基础
链接视图和执行视图
ELF非常巧妙的一个地方在于,同一个ELF文件可以同时分为链接视图和执行视图来处理,分别的:
- 链接视图:用于编译时链接或运行时动态链接,以section为单位进行管理,section信息放在section header。
- 执行视图:用于运行时执行elf文件中的代码和数据,以segment为单位进行管理,segment信息放在program header。
具体,如下图:
例如,查看westeros的链接视图section header:
aarch64-linux-android-readelf libwesteros.so -SW There are 26 section headers, starting at offset 0x145568: Section Headers: [Nr] Name Type Address Off Size ES Flg Lk Inf Al [ 0] NULL 0000000000000000 000000 000000 00 0 0 0 [ 1] .note.gnu.build-id NOTE 0000000000000200 000200 000024 00 A 0 0 4 [ 2] .hash HASH 0000000000000228 000228 0016fc 04 A 4 0 8 [ 3] .gnu.hash GNU_HASH 0000000000001928 001928 001448 00 A 4 0 8 [ 4] .dynsym DYNSYM 0000000000002d70 002d70 0058e0 18 A 5 3 8 [ 5] .dynstr STRTAB 0000000000008650 008650 00cbb3 00 A 0 0 1 [ 6] .gnu.version VERSYM 0000000000015204 015204 000768 02 A 4 0 2 [ 7] .gnu.version_r VERNEED 0000000000015970 015970 000040 00 A 5 2 8 [ 8] .rela.dyn RELA 00000000000159b0 0159b0 019ff8 18 A 4 0 8 [ 9] .rela.plt RELA 000000000002f9a8 02f9a8 002100 18 AI 4 21 8 [10] .plt PROGBITS 0000000000031ab0 031ab0 001620 10 AX 0 0 16 [11] .text PROGBITS 00000000000330d0 0330d0 09bcbc 00 AX 0 0 4 [12] .rodata PROGBITS 00000000000ced90 0ced90 022ede 00 A 0 0 16 [13] .eh_frame_hdr PROGBITS 00000000000f1c70 0f1c70 0061a4 00 A 0 0 4 [14] .eh_frame PROGBITS 00000000000f7e18 0f7e18 019930 00 A 0 0 8 [15] .gcc_except_table PROGBITS 0000000000111748 111748 008c78 00 A 0 0 4 [16] .note.android.ident NOTE 000000000011a3c0 11a3c0 000098 00 A 0 0 4 [17] .init_array INIT_ARRAY 000000000012a868 11a868 0000c0 08 WA 0 0 8 [18] .fini_array FINI_ARRAY 000000000012a928 11a928 000010 08 WA 0 0 8 [19] .data.rel.ro PROGBITS 000000000012a938 11a938 009598 00 WA 0 0 8 [20] .dynamic DYNAMIC 0000000000133ed0 123ed0 0002b0 10 WA 5 0 8 [21] .got PROGBITS 0000000000134180 124180 000e80 08 WA 0 0 8 [22] .data PROGBITS 0000000000135000 125000 020360 00 WA 0 0 8 [23] .bss NOBITS 0000000000155360 145360 001078 00 WA 0 0 16 [24] .comment PROGBITS 0000000000000000 145360 000108 01 MS 0 0 1 [25] .shstrtab STRTAB 0000000000000000 145468 0000fb 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), p (processor specific)
关键的section,如:.dynsym代表所有动态链接相关的(导入和导出)的符号,.rela.dyn和.rel.plt代表需要需要重定位的符号项,.got代表全局偏移表存储的是PIC(Position Independent Code)的符号与地址对照表,.plt也是为了实现PIC功能相关,存储的是导入符号即调用外部符号的对照表。
dynsym构成
展开来看.dynsym(也是本文我们查找"_Unwind_Resume"符号时使用的命令):
aarch64-linux-android-readelf libwesteros.so -sW
Symbol table ‘.dynsym‘ contains 948 entries:
Num: Value Size Type Bind Vis Ndx Name
12: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _ZN6google8protobuf5Arena11AddListNodeEPvPFvS2_E
439: 00000000000a8290 16 FUNC GLOBAL DEFAULT 11 _ZN8kuaishou8westeros10VideoFrame16frame_number_keyEv
440: 00000000000acfc0 4 FUNC WEAK DEFAULT 11 _ZN8kuaishou8westeros8internal11PublishableINSt6__ndk110shared_ptrINS0_10VideoFrameEEEE16SetProcessorNameERKNS3_12basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEE
599: 00000000000ef600 44 OBJECT GLOBAL DEFAULT 12 _ZTSN8kuaishou8westeros21WesterosTextureDrawerE
这里选择了几个有代表性的符号,可以看到他们有以下几个不同点:
- Type:FUNC/OBJECT. 两种符号类型,一个代表函数,一个代表数据。
- Bind:GLOBAL/WEAK. 强弱符号,同时存在时优先选择强符号,参考编译器自带宏__attribute__((weak))。
- Vis:DEFAULT/LOCAL. 符号可见性,DEFAULT是全局导出符号,LOCAL是内部符号,参考编译器自带宏__attribute((visibility))。
- Ndx:UND/具体数值. Ndx是number index的缩写,具体的含义是符号所在的section index。对于内部符号,为一个具体数值,对应一个类型为PROGBITS的section,代表符号在程序内的具体位置;对于外部动态链接的导入符号,因为符号的实现并不在当前elf文件内,所以的是UND,代表undefined。
再来看执行视图:
aarch64-linux-android-readelf /Users/lirui/Downloads/nebula-GENERIC-gifmakerrelease-9.0.50.1014_x64/lib/arm64-v8a/libwesteros.so -lW
Elf file type is DYN (Shared object file)
Entry point 0x330d0
There are 8 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x11a458 0x11a458 R E 0x10000
LOAD 0x11a868 0x000000000012a868 0x000000000012a868 0x02aaf8 0x02bb70 RW 0x10000
DYNAMIC 0x123ed0 0x0000000000133ed0 0x0000000000133ed0 0x0002b0 0x0002b0 RW 0x8
NOTE 0x000200 0x0000000000000200 0x0000000000000200 0x000024 0x000024 R 0x4
NOTE 0x11a3c0 0x000000000011a3c0 0x000000000011a3c0 0x000098 0x000098 R 0x4
GNU_EH_FRAME 0x0f1c70 0x00000000000f1c70 0x00000000000f1c70 0x0061a4 0x0061a4 R 0x4
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
GNU_RELRO 0x11a868 0x000000000012a868 0x000000000012a868 0x00a798 0x00a798 R 0x1
Section to Segment mapping:
Segment Sections...
00 .note.gnu.build-id .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .plt .text .rodata .eh_frame_hdr .eh_frame .gcc_except_table .note.android.ident
01 .init_array .fini_array .data.rel.ro .dynamic .got .data .bss
02 .dynamic
03 .note.gnu.build-id
04 .note.android.ident
05 .eh_frame_hdr
06
07 .init_array .fini_array .data.rel.ro .dynamic .got
可以看到segment的类型有LOAD、DYNAMIC、NOTE、GNU_EH_FRAME、GNU_STACK、GNU_RELRO几种,我们主要关注LOAD类型,附录1里so装载的过程有介绍segment通过mmap分配内存的细节。program header里还有segment和section的index对应关系。