4.4.4版本下Linker 详解
1. 简述
- 在 Android 系统中,Linker(链接器)是负责将编译后的目标文件(
.o
)和库文件(如.so
或.a
)合并为可执行文件的关键组件,其核心角色涉及 符号解析、重定位、动态链接 等核心流程; - 后续主要是分析安卓加载 so 的源码,源码的版本是 4.4.4;
- 在整体的分析流程中,主要的解释会写在代码块里以注释的形式表现;
2. 源码分析
- Java 有两种方式加载库文件,一种是 System.load(),另一种是 System.loadLibrary();前一种需要传入绝对路径,后一种只需要指出库文件名称即可,安卓一般采取后一种方式;在分析前,可以先看看图 1,直观感受调用链,不过下图并不完全契合我们的流程,因为版本会有所差异:
- 在安卓进行加载 so 的时候,主要入口就是如下代码,也是这里的重点,由它开始分析;
static {
System.loadLibrary("rc4");
}
- 分析的源码版本是 4.4,从 System.java 这个类开始看;
loadLibrary
- 它的位置是:libcore/luni/src/main/java/java/lang/System.java
- 按照上面说的,找到 loadLibrary 方法,定义如下:
public static void loadLibrary(String libName) {
Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
}
- 传递的参数是模块名,VMStack.getCallingClassLoader()拿到的则是调用者的 classloader,继续往下跟;
- 继续往下看,这里走的是 Runtime.getRuntime(),去看看它的实现;
public static Runtime getRuntime() {
return mRuntime;
}
- 这里返回的实际上是 mRuntime,代表一个运行时,实际上返回的是 mRuntime,它是这个类的一个实例;
private static final Runtime mRuntime = new Runtime();
- 实际上最后还是这个 Runtime 类下的 loadLibrary 方法,它的位置:libcore/luni/src/main/java/java/lang/Runtime.java,去看它的实现;
- 这里是有重载,但根据我们传递的参数,应该是下面的这个两个参数的方法;看看它的实现;
void loadLibrary(String libraryName, ClassLoader loader) {
if (loader != null) {
/*这里把 libraryName 传进去 findLibrary 实际上是找的 dexclassloder
也就是 DexPathList.java 里的 findLibrary 方法 获取指定库名的完整路径
*/
String filename = loader.findLibrary(libraryName);
if (filename == null) {
// 找不到就会抛异常
throw new UnsatisfiedLinkError("Couldn't load " + libraryName +
" from loader " + loader +
": findLibrary returned null");
}
// 调用 doLoad 方法
String error = doLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
String filename = System.mapLibraryName(libraryName);
List <String> candidates = new ArrayList <String>();
String lastError = null;
for (String directory : mLibPaths) {
String candidate = directory + filename;
candidates.add(candidate);
if (IoUtils.canOpenReadOnly(candidate)) {
// 调用 doLoad 方法
String error = doLoad(candidate, loader);
if (error == null) {
return; // We successfully loaded the library. Job done.
}
lastError = error;
}
}
if (lastError != null) {
throw new UnsatisfiedLinkError(lastError);
}
throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
}
- 把它拆分后开始分析,String error = doLoad(filename, loader),这里会调用这个 doLoad 方法,当源码里为玫红色的时候,则代表此方法的定义在本类中,可以直接点击跳转过去(2025-07-22 续:源码站点失效 无法直接跳转了);
doLoad
- 它的位置是:libcore/luni/src/main/java/java/lang/Runtime.java
private String doLoad(String name, ClassLoader loader) {
//有些注释
String ldLibraryPath = null;
//BaseDexClassLoader 是顶端的 classloder
if (loader != null && loader instanceof BaseDexClassLoader) {
ldLibraryPath = ((BaseDexClassLoader) loader).getLdLibraryPath();
}
// 上方的代码实际上就是在给 ldLibraryPath 赋值
// 有些注释
synchronized (this) {
// 随后又调用 nativeLoad 这个方法
return nativeLoad(name, loader, ldLibraryPath);
}
}
ldLibraryPath
是 Android 系统在 Java 层实现的 动态库搜索路径管理机制,用于替代传统的LD_LIBRARY_PATH
环境变量。它由BaseDexClassLoader
维护,包含应用私有库路径和系统库路径的集合;- 赋完值后又调用了 nativeLoad 这个方法,而它是这样的定义:
private static native String nativeLoad(String filename, ClassLoader loader, String ldLibraryPath);
nativeLoad
- 它的位置是:art/runtime/native/java_lang_Runtime.cc
- 可以看到它是一个 native 方法,那他就是 c 或者 cpp 写的,那我们需要找一下它的具体实现位置;可以去搜索一下,这里勾选上所有的文件;
- 找到它的实现位置,或者说根据拼接规则来,当前我们在 [Runtime.java] 文件下,那它的函数名应该就是 Runtime_nativeLoad;它的实现如下:
static jstring Runtime_nativeLoad(JNIEnv* env, jclass, jstring javaFilename, jobject javaLoader, jstring javaLdLibraryPath) {
ScopedObjectAccess soa(env);
ScopedUtfChars filename(env, javaFilename);
// 判断文件名是不是 null 这个没啥可说的
if (filename.c_str() == NULL) {
return NULL;
}
// 同样判断 javaLdLibraryPath
if (javaLdLibraryPath != NULL) {
// ScopedUtfChars 将 javaFilename 转换为 C 字符串
ScopedUtfChars ldLibraryPath(env, javaLdLibraryPath);
if (ldLibraryPath.c_str() == NULL) {
return NULL;
}
/*
通过 dlsym 查找 Android 动态链接器的私有符号 android_update_LD_LIBRARY_PATH
调用该函数临时修改当前线程的库搜索路径,确保后续 dlopen 能识别 APK 私有目录
其实就是确保程序能找到 so 文件到底在哪里
*/
void* sym = dlsym(RTLD_DEFAULT, "android_update_LD_LIBRARY_PATH");
if (sym != NULL) {
typedef void (*Fn)(const char*);
Fn android_update_LD_LIBRARY_PATH = reinterpret_cast <Fn>(sym);
(*android_update_LD_LIBRARY_PATH)(ldLibraryPath.c_str());
} else {
// 这里的错误信息也可以看出来是加载失败了的
LOG(ERROR) << "android_update_LD_LIBRARY_PATH not found; .so dependencies will not work!";
}
}
// 将 SO 与类加载器绑定,实现多模块(如宿主与插件)加载同名 SO 时的隔离
mirror:: ClassLoader* classLoader = soa.Decode <mirror::ClassLoader*>(javaLoader);
std:: string detail;
JavaVMExt* vm = Runtime:: Current()-> GetJavaVM();
// 主要就应该是 LoadNativeLibrary 这个函数了 传入文件名、类加载器和 detail 字符串
// LoadNativeLibrary 就是实际执行库加载的核心函数
bool success = vm-> LoadNativeLibrary(filename.c_str(), classLoader, detail);
if (success) {
return NULL;
}
// Don't let a pending exception from JNI_OnLoad cause a CheckJNI issue with NewStringUTF.
env-> ExceptionClear();
return env-> NewStringUTF(detail.c_str());
}
- 分析一下 Runtime_nativeLoad 这个函数,参数 1 是文件名,参数 2 是 classloader,参数 3 应该是路径;比较重要的点基本上都在代码块进行了解释,总体来说整个函数就是 LoadNativeLibrary 来执行库加载;
- 接下来继续分析 LoadNativeLibrary 这个函数;
LoadNativeLibrary
- 它的位置是:art/runtime/jni_internal.cc,中间有一些注释太占篇幅,在代码中被舍弃了;
bool JavaVMExt:: LoadNativeLibrary(const std:: string& path, ClassLoader* class_loader,
std:: string& detail) {
// 传入文件名、类加载器和 detail 字符串
detail.clear();
SharedLibrary* library;
Thread* self = Thread:: Current();
{
MutexLock mu(self, libraries_lock);
library = libraries-> Get(path);
// 关于锁的一些处理 通过 MutexLock 对共享库列表 libraries 加锁,防止多线程并发访问导致状态不一致
}
// 这里从 libraries 中获取 path 对应的库,如果存在的话,进行一些检查
// 如果 library 不存在,即需要真正加载的情况 所以直接看后面
if (library != NULL) {
if (library-> GetClassLoader() != class_loader) {
StringAppendF(&detail, "Shared library \"%s \" already opened by "
"ClassLoader %p; can't open in ClassLoader %p",
path.c_str(), library-> GetClassLoader(), class_loader);
LOG(WARNING) << detail;
return false;
}
VLOG(jni) << "[Shared library \" " << path << " \" already loaded in "
<< "ClassLoader " << class_loader << "]";
if (! library-> CheckOnLoadResult()) {
StringAppendF(&detail, "JNI_OnLoad failed on a previous attempt "
"to load \"%s \"", path.c_str());
return false;
}
return true;
}
// 通过 TransitionFromRunnableToSuspended 将线程标记为阻塞状态,防止 dlopen 耗时操作影响主线程调度
self-> TransitionFromRunnableToSuspended(kWaitingForJniOnLoad);
// 主要看这里 主要看这里 主要看这里 主要看这里 不能走上面
// 调用 dlopen 把传进来的 path 和 RTLD_LAZY 传进去 RTLD_LAZY 表示延迟绑定(仅解析当前使用符号)
// 往下看 dlopen 的解释 会单独分析
void* handle = dlopen(path.empty() ? NULL : path.c_str(), RTLD_LAZY);
self-> TransitionFromSuspendedToRunnable();
VLOG(jni) << "[Call to dlopen(\" " << path << " \", RTLD_LAZY) returned " << handle << "]";
// 抛异常
if (handle == NULL) {
detail = dlerror();
LOG(ERROR) << "dlopen(\"" << path << "\", RTLD_LAZY) failed: " << detail;
return false;
}
bool created_library = false;
{
MutexLock mu(self, libraries_lock);
library = libraries-> Get(path);
if (library == NULL) { // We won race to get libraries_lock
library = new SharedLibrary(path, handle, class_loader);
libraries-> Put(path, library);
created_library = true;
}
}
if (! created_library) {
LOG(INFO) << "WOW: we lost a race to add shared library: "
<< "\"" << path << "\" ClassLoader =" << class_loader;
return library-> CheckOnLoadResult();
}
VLOG(jni) << "[Added shared library \" " << path << " \" for ClassLoader " << class_loader << "]";
bool was_successful = false;
void* sym = dlsym(handle, "JNI_OnLoad");
if (sym == NULL) {
VLOG(jni) << "[No JNI_OnLoad found in \" " << path << " \"]";
was_successful = true;
} else {
typedef int (*JNI_OnLoadFn)(JavaVM*, void*);
JNI_OnLoadFn jni_on_load = reinterpret_cast <JNI_OnLoadFn>(sym);
ClassLoader* old_class_loader = self-> GetClassLoaderOverride();
self-> SetClassLoaderOverride(class_loader);
int version = ;
{
ScopedThreadStateChange tsc(self, kNative);
VLOG(jni) << "[Calling JNI_OnLoad in \" " << path << " \"]";
version = (*jni_on_load)(this, NULL);
}
self-> SetClassLoaderOverride(old_class_loader);
if (version == JNI_ERR) {
StringAppendF(&detail, "JNI_ERR returned from JNI_OnLoad in \"%s \"", path.c_str());
} else if (IsBadJniVersion(version)) {
StringAppendF(&detail, "Bad JNI version returned from JNI_OnLoad in \"%s \": %d",
path.c_str(), version);
} else {
was_successful = true;
}
VLOG(jni) << "[Returned " << (was_successful ? " successfully " : " failure ")
<< " from JNI_OnLoad in \"" << path << "\"]";
}
library-> SetResult(was_successful);
return was_successful;
}
dlopen
- 它的位置是:bionic/linker/dlfcn.cpp
- dlopen 整体代码量不多,核心就应该是 do_dlopen 这个函数,在继续跟之前这里需要提前知道 soinfo 这个结构体,让我们了解一下它吧;
void * dlopen(const char* filename, int flags) {
// 两个参数:filename(共享库的路径)和 flags(加载标志)
ScopedPthreadMutexLocker locker(&gDlMutex);
// do_dlopen 核心函数
soinfo* result = do_dlopen(filename, flags);
if (result == NULL) {
__bionic_format_dlerror("dlopen failed", linker_get_error_buffer());
return NULL;
}
return result;
}
soinfo结构体
- 它的位置是:bionic/linker/linker.h
- soinfo 应该是 Android 动态链接器中的一个核心结构体,用来表示加载的共享对象(.so 文件);每个加载到进程地址空间的共享库都有一个对应的 soinfo 实例;它的作用类似于 ELF 文件的运行时表示,保存了链接器在加载和解析库时所需的各种信息;
- 它的定义和解释如下:
struct soinfo {
public:
// 共享库名称,最大长度由 SOINFO_NAME_LEN 定义
char name [SOINFO_NAME_LEN];
// ELF 程序头表(Program Header Table)的指针
const Elf32_Phdr* phdr;
// 程序头表项的数量
size_t phnum;
// ELF 文件入口点地址(程序执行起点)
Elf32_Addr entry;
// 共享库在内存中的加载基地址
Elf32_Addr base;
// 共享库占用的总内存大小
unsigned size;
// 保留字段(兼容性用途,不可使用)
uint32_t unused1;
// 动态段(Dynamic Section)的指针,存储动态链接信息
Elf32_Dyn* dynamic;
// 保留字段(兼容性用途,不可使用)
uint32_t unused2;
uint32_t unused3;
// 指向下一个 soinfo 结构的指针(用于构建共享库链表)
soinfo* next;
// 标志位(可能包含加载状态、属性等)
unsigned flags;
// 字符串表(String Table)指针,用于符号名称解析
const char* strtab;
// 符号表(Symbol Table)指针
Elf32_Sym* symtab;
// 哈希表桶数量和链长度
size_t nbucket;
size_t nchain;
// 哈希表桶数组和链数组
unsigned* bucket;
unsigned* chain;
// PLT/GOT 表的地址(用于过程链接表/全局偏移表)
unsigned* plt_got;
// PLT 重定位信息
Elf32_Rel* plt_rel;
size_t plt_rel_count;
// 常规重定位信息
Elf32_Rel* rel;
size_t rel_count;
// 构造函数相关
linker_function_t* preinit_array; // 预初始化函数数组
size_t preinit_array_count;
linker_function_t* init_array; // 初始化函数数组
size_t init_array_count;
linker_function_t* fini_array; // 析构函数数组
size_t fini_array_count;
// 显式指定的初始化/终止函数
linker_function_t init_func;
linker_function_t fini_func;
#if defined(ANDROID_ARM_LINKER)
// ARM 架构的异常展开信息(用于栈回溯)
unsigned* ARM_exidx;
size_t ARM_exidx_count;
#elif defined(ANDROID_MIPS_LINKER)
// MIPS 架构相关字段
unsigned mips_symtabno; // 符号表条目数
unsigned mips_local_gotno; // 局部 GOT 项数量
unsigned mips_gotsym; // GOT 对应的符号索引
#endif
// 引用计数(表示该库被加载的次数)
size_t ref_count;
// ELF 链接映射信息(用于 dl_iterate_phdr 等接口)
link_map_t link_map;
// 构造函数是否已被调用的标志
bool constructors_called;
// 加载偏移量(ELF 虚拟地址 -> 内存实际地址的转换基值)
Elf32_Addr load_bias;
// 是否包含文本段重定位(修改代码段的重定位)
bool has_text_relocations;
// 是否设置了 DT_SYMBOLIC 标志(影响符号解析顺序)
bool has_DT_SYMBOLIC;
// 构造函数/析构函数调用接口
void CallConstructors();
void CallDestructors();
void CallPreInitConstructors();
private:
// 辅助函数:调用函数数组(reverse 为 true 时逆序调用)
void CallArray(const char * array_name, linker_function_t* functions, size_t count, bool reverse);
// 辅助函数:调用单个函数
void CallFunction(const char* function_name, linker_function_t function);
};
- 这里需要了解一下 elf 文件的格式,会对这个结构体有一些熟悉的认知,接下来继续分析上面提到的 do_dlopen 函数;
do_dlopen
- 它的位置是:bionic/linker/linker.cpp
- 此时基本上算是正式与 linker 接轨,do_dlopen 的定义如下:
//do_dlopen
soinfo * do_dlopen(const char* name, int flags) {
if ((flags & ~(RTLD_NOW|RTLD_LAZY|RTLD_LOCAL|RTLD_GLOBAL)) != ) {
DL_ERR("invalid flags to dlopen: %x", flags);
return NULL;
}
// 临时将 soinfo 池(存储动态库元数据的内存区域)的访问权限设置为可读写
// 在 Android 的 Linker 实现中,soinfo 池默认可能被设置为只读(PROT_READ)
// 修改权限是为了允许写入新的库加载信息(如依赖项解析结果)
set_soinfo_pool_protection(PROT_READ | PROT_WRITE);
// 通过 find_library 函数加载目标库及其依赖项
// find_library 这个方法,它会先在 solist(已经加载的动态链接库链表)里进行查找,如果找到了就返回对应的 soinfo 结构体指针。否则,就调用 load_library 进行加载。然后,调用 soinfo_link_image 方法,根据 soinfo 结构体解析相应的 Section
soinfo* si = find_library(name);
if (si != NULL) {
// 调用 CallConstructors
si-> CallConstructors();
}
// 将 soinfo 池恢复为只读
set_soinfo_pool_protection(PROT_READ);
return si;
}
- 所以这里需要先看 find_library 这个方法,看看具体做了什么,后面可能会跳来跳去,需要注意;
find_library_internal
- 它的位置是:bionic/linker/linker.cpp
- find_library 定义如下,实际上没有做什么额外的东西,还需要往 find_library_internal 里走,这里直接将两个方法放一起,他俩都在 linker.cpp 里;
// find_library
static soinfo * find_library(const char* name) {
soinfo* si = find_library_internal(name);
if (si != NULL) {
si-> ref_count++;
}
return si;
}
// find_library_internal
static soinfo * find_library_internal(const char* name) {
if (name == NULL) {
return somain;
}
/*
调用 find_loaded_library(name)来查找已经加载的库 如果找到了 si 不为 NULL
再检查 si 的 flags 是否有 FLAG_LINKED 标志。如果有,说明已经链接过了,直接返回这个 si
如果没找到已加载的库,就会进入加载流程
*/
soinfo* si = find_loaded_library(name);
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);
// 调用 load_library(name)来加载库,返回值是 soinfo 指针 si
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);
/*
接下来调用 soinfo_link_image(si),这个函数应该处理符号解析、重定位等链接步骤
如果 soinfo_link_image 返回 false,说明链接失败。这时候需要解除内存映射,
释放 soinfo 结构,返回 NULL。否则的话,返回这个 si
*/
if (! soinfo_link_image(si)) {
munmap(reinterpret_cast <void*>(si-> base), si-> size);
soinfo_free(si);
return NULL;
}
return si;
}
有三个函数是 find_library_internal 函数里比较重要的函数,这里稍作解释,分别如下:
- find_loaded_library:遍历已经加载的 so 列表,查找是否已经加载了名字匹配的库;
- load_library:负责打开文件,读取 ELF 头,进行内存映射等操作,创建 soinfo 结构,并初始化基本字段;
- soinfo_link_image:进行链接操作,比如解析符号、重定位、初始化 PLT/GOT 等;
- 这里就大致可以引出 linker 加载、链接等流程;那现在就应该去看 load_library 和 soinfo_link_image 这两个函数,先看看 load_library;
load_library
- 它的位置是:bionic/linker/linker.cpp
- 首先看它的定义:
static soinfo * load_library(const char* name) {
// Open the file. 说的很清楚了 打开 so 文件 失败返回
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;
si-> dynamic = NULL;
si-> phnum = elf_reader.phdr_count();
si-> phdr = elf_reader.loaded_phdr();
return si;
}
- 这个函数不是特别长,可以在这里说明,首先源码本身的注释已经说的很清楚了,打开 so 文件,失败返回空并打印日志;
- 接下来,创建了一个 ElfReader 对象 elf_reader,传入 name 和 fd 作为参数;然后调用 elf_reader.Load()函数,如果返回 false,说明加载 ELF 文件失败,返回 NULL;
- 调用 soinfo_alloc 函数分配一个 soinfo 结构体,并将相关信息填充进去,这里的字段都是 soinfo 结构体的,在前面的讲述可以找到对应字段的含义,最后返回 si 指针;
- 这里又要继续深入,看这个加载 so 的函数,elf_reader.Load();
elf_reader.Load
- 它的位置是:bionic/linker/linker_phdr.cpp
- 它的定义如下:
bool ElfReader:: Load() {
return ReadElfHeader() &&
VerifyElfHeader() &&
ReadProgramHeader() &&
ReserveAddressSpace() &&
LoadSegments() &&
FindPhdr();
}
看起来非常简单,实际上又要拆分成多个函数来说,从符号也大概可以看出来每个函数的作用,稍微列举一下:
- ReadElfHeader:从文件中读取 ELF 文件头;
- VerifyElfHeader:检查 ELF 文件的格式和兼容性;
- ReadProgramHeader:解析程序头表(Program Header Table);
- ReserveAddressSpace:为 ELF 段预留虚拟地址空间;
- LoadSegments:将 ELF 段从文件映射到内存;
- FindPhdr:定位程序头表在内存中的地址;
- 那么肯定是需要看这些函数做了什么,所以这种文章写起来比较麻烦,总是层层递进,后面还要慢慢抽出来,会有很强的突兀感;
ReadElfHeader
- 它的位置是:bionic/linker/linker_phdr.cpp
- 其实就在 Load 函数的下方,后续几个函数位置也都差不多,它的定义如下:
bool ElfReader:: ReadElfHeader() {
// 使用 read 函数从文件描述符 fd_中读取 ELF 头部到 header_ 变量中
ssize_t rc = TEMP_FAILURE_RETRY(read(fd_, &header_, sizeof(header_)));
if (rc < 0) {
DL_ERR("can't read file \"%s \": %s", name_, strerror(errno));
return false;
}
if (rc != sizeof(header_)) {
DL_ERR("\"%s \" is too small to be an ELF executable", name_);
return false;
}
return true;
}
- header_变量会存储我们的头部信息,函数的作用与我们预期是一致的,接下来看下一个函数;
VerifyElfHeader
- 它的位置是:bionic/linker/linker_phdr.cpp
- 这里直接看注释吧,代码段比较长,这一段最好是先熟悉一下ELF文件结构,否则会比较吃力;
bool ElfReader::VerifyElfHeader() {
/*
ELFMAG0-ELFMAG3实际上应该是ELF magic的意思
0123分别是 0x7f E L F 这里就是校验魔数
*/
if (header_.e_ident[EI_MAG0] != ELFMAG0 ||
header_.e_ident[EI_MAG1] != ELFMAG1 ||
header_.e_ident[EI_MAG2] != ELFMAG2 ||
header_.e_ident[EI_MAG3] != ELFMAG3) {
DL_ERR("\"%s\" has bad ELF magic", name_);
return false;
}
// 判断文件类别 是否是32位 这里只支持32位
if (header_.e_ident[EI_CLASS] != ELFCLASS32) {
DL_ERR("\"%s\" not 32-bit: %d", name_, header_.e_ident[EI_CLASS]);
return false;
}
// e_ident[EI_DATA]是否为ELFDATA2LSB,也就是小端序
if (header_.e_ident[EI_DATA] != ELFDATA2LSB) {
DL_ERR("\"%s\" not little-endian: %d", name_, header_.e_ident[EI_DATA]);
return false;
}
// 检查是否是共享目标文件 也就是检查是否是so文件
if (header_.e_type != ET_DYN) {
DL_ERR("\"%s\" has unexpected e_type: %d", name_, header_.e_type);
return false;
}
// 检查版本信息
if (header_.e_version != EV_CURRENT) {
DL_ERR("\"%s\" has unexpected e_version: %d", name_, header_.e_version);
return false;
}
// 检查运行平台
if (header_.e_machine !=
#ifdef ANDROID_ARM_LINKER
EM_ARM
#elif defined(ANDROID_MIPS_LINKER)
EM_MIPS
#elif defined(ANDROID_X86_LINKER)
EM_386
#endif
) {
DL_ERR("\"%s\" has unexpected e_machine: %d", name_, header_.e_machine);
return false;
}
return true;
}
- 总体来说,这个函数校验了elf文件头的一些字段,都是比较容易理解的;也没有什么需要深入理解的,看下一个函数;
ReadProgramHeader
- 它的位置是:bionic/linker/linker_phdr.cpp
bool ElfReader::ReadProgramHeader() {
// e_phnum表示程序头表中的条目数量 header_是前面 ReadElfHeader拿到的
phdr_num_ = header_.e_phnum;
// Like the kernel, we only accept program header tables that
// are smaller than 64KiB. 做了判断 这里可能是在检查程序头表的数量是否合理
if (phdr_num_ < 1 || phdr_num_ > 65536/sizeof(Elf32_Phdr)) {
DL_ERR("\"%s\" has invalid e_phnum: %d", name_, phdr_num_);
return false;
}
/*
page_min是程序头表起始位置对齐后的起始页地址
page_max是程序头表结束位置(起始偏移加上整个程序头表的大小)对齐后的结束页地址
page_offset则是e_phoff相对于页面起始的偏移量,也就是程序头表在映射后的内存中的偏移量
总体就是做一个页对齐
*/
Elf32_Addr page_min = PAGE_START(header_.e_phoff);
Elf32_Addr page_max = PAGE_END(header_.e_phoff + (phdr_num_ * sizeof(Elf32_Phdr)));
Elf32_Addr page_offset = PAGE_OFFSET(header_.e_phoff);
// 页对齐后的数据大小
phdr_size_ = page_max - page_min;
/*
使用mmap函数将对应的文件区域映射到内存中
参数:起始地址NULL表示由系统自动选择,大小phdr_size_,只读,标志是私有映射,文件描述符fd_,偏移量page_min
mmap需要按页对齐,所以实际映射的是从page_min开始的整个页面范围,这样就能覆盖整个程序头表所在的区域
*/
void* mmap_result = mmap(NULL, phdr_size_, PROT_READ, MAP_PRIVATE, fd_, page_min);
if (mmap_result == MAP_FAILED) {
DL_ERR("\"%s\" phdr mmap failed: %s", name_, strerror(errno));
return false;
}
phdr_mmap_ = mmap_result;
phdr_table_ = reinterpret_cast<Elf32_Phdr*>(reinterpret_cast<char*>(mmap_result) + page_offset);
return true;
}
- 总体这个函数的作用就是读程序表头,会涉及到一个mmap函数,它会将程序头表映射到内存,继续看下一个函数;
ReserveAddressSpace
- 它的位置是:bionic/linker/linker_phdr.cpp
bool ElfReader::ReserveAddressSpace() {
Elf32_Addr min_vaddr;
// 这个函数可能是从程序头表(PHDR)中获取需要加载的段的总大小,并且确定最小的虚拟地址min_vaddr
load_size_ = phdr_table_get_load_size(phdr_table_, phdr_num_, &min_vaddr);
if (load_size_ == 0) {
DL_ERR("\"%s\" has no loadable segments", name_);
return false;
}
uint8_t* addr = reinterpret_cast<uint8_t*>(min_vaddr);
int mmap_flags = MAP_PRIVATE | MAP_ANONYMOUS;
// 映射到内存 start是加载到内存的首地址 不一定是0开始
void* start = mmap(addr, load_size_, PROT_NONE, mmap_flags, -1, 0);
if (start == MAP_FAILED) {
DL_ERR("couldn't reserve %d bytes of address space for \"%s\"", load_size_, name_);
return false;
}
load_start_ = start;
// 内存实际加载的地址与虚拟地址的差值就是 load_bias_
load_bias_ = reinterpret_cast<uint8_t*>(start) - addr;
return true;
}
- 理解一下整个函数,在最开始的时候我们提到过这个函数可能是为 ELF 段预留虚拟地址空间,是因为ELF文件的程序头表中的各个段可能有不同的虚拟地址要求,所以在加载ELF时,需要先确定需要的总内存大小和最小虚拟地址,然后保留对应的地址空间;
- 这里的phdr_table_get_load_size作用是遍历程序头表(PHDR),计算所有可加载段(
PT_LOAD
)的总内存大小load_size_
,并确定最低虚拟地址min_vaddr
,这个函数就不去详细解析了;
LoadSegments
- 它的位置是:bionic/linker/linker_phdr.cpp
- 总的来说,ReserveAddressSpace进行了空间预留的操作,那么LoadSegments就用于在预留空间依次加载 p_type 为 PT_LOAD 的段;
bool ElfReader::LoadSegments() {
// 遍历程序头表中的所有段(phdr_num_个)
for (size_t i = 0; i < phdr_num_; ++i) {
// phdr_table_ 在读程序头的时候已经拿到了
const Elf32_Phdr* phdr = &phdr_table_[i];
// 检查段的类型是否为PT_LOAD,如果不是就跳过 明只处理可加载的段
if (phdr->p_type != PT_LOAD) {
continue;
}
// 计算段在内存中的起始地址seg_start,这里用了load_bias_加上段的虚拟地址p_vaddr
Elf32_Addr seg_start = phdr->p_vaddr + load_bias_;
// seg_end是段的结束地址,等于seg_start加上p_memsz,也就是段占用的内存大小
Elf32_Addr seg_end = seg_start + phdr->p_memsz;
/*
seg_page_start是seg_start所在页面的起始地址,而seg_page_end是seg_end后面的一个页面边界
这里用了PAGE_START和PAGE_END宏,这两个宏通常用于对齐到页大小
*/
Elf32_Addr seg_page_start = PAGE_START(seg_start);
Elf32_Addr seg_page_end = PAGE_END(seg_end);
// seg_start + p_filesz
Elf32_Addr seg_file_end = seg_start + phdr->p_filesz;
/*
file_start是段在文件中的起始偏移量,file_end是文件中的结束偏移量
file_page_start是该偏移量页对齐后的起始位置
file_length是文件中这个段占用的页数,即file_end减去file_page_start
*/
Elf32_Addr file_start = phdr->p_offset;
Elf32_Addr file_end = file_start + phdr->p_filesz;
Elf32_Addr file_page_start = PAGE_START(file_start);
Elf32_Addr file_length = file_end - file_page_start;
/*
如果file_length不为0,就用mmap将文件中的这段映射到内存中;这里使用了MAP_FIXED标志,意味着必须映射到指定的地址seg_page_start;
使用的保护标志是根据段的p_flags转换来的,比如PF_R、PF_W、PF_X对应PROT_READ、PROT_WRITE、PROT_EXEC;映射的文件是打开的ELF文件(fd_),偏移量是file_page_start
*/
if (file_length != 0) {
void* seg_addr = mmap((void*)seg_page_start,
file_length,
PFLAGS_TO_PROT(phdr->p_flags), // 从文件中读取flag
MAP_FIXED|MAP_PRIVATE,
fd_,
file_page_start);
if (seg_addr == MAP_FAILED) {
DL_ERR("couldn't map \"%s\" segment %d: %s", name_, i, strerror(errno));
return false;
}
}
// if the segment is writable, and does not end on a page boundary,
// zero-fill it until the page limit.
// 根据官方的注释也可以看出来 如果段是可写的,并且没有在页面边界结束,则零填充它直到页面限制
if ((phdr->p_flags & PF_W) != 0 && PAGE_OFFSET(seg_file_end) > 0) {
memset((void*)seg_file_end, 0, PAGE_SIZE - PAGE_OFFSET(seg_file_end));
}
// seg_file_end被调整到下一个页面的开始地址,也就是原来的seg_file_end经过PAGE_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.
/*
如果段在内存中的结束地址seg_page_end大于seg_file_end,说明内存中的段比文件中的大
需要将多出的部分用匿名映射来填充0
*/
if (seg_page_end > seg_file_end) {
void* zeromap = mmap((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;
}
总结一下,这个函数的主要步骤是:
- 遍历所有程序头;
- 对于每个可加载段,计算内存地址并进行页对齐;
- 将文件中的段映射到内存,可能包括多个页面;
- 对于段末尾未填满的页面部分,清零处理;
- 如果内存中的段比文件中的大,用匿名映射填充多出的部分;
- 所有操作失败时返回false,否则返回true;
- 这个函数比较长,解析起来也比较麻烦,记住总结的作用也行,稍作理解;看最后一个 FindPhdr 函数;
FindPhdr
- 它的位置是:bionic/linker/linker_phdr.cpp
bool ElfReader::FindPhdr() {
const Elf32_Phdr* phdr_limit = phdr_table_ + phdr_num_;
// If there is a PT_PHDR, use it directly.
for (const Elf32_Phdr* phdr = phdr_table_; phdr < phdr_limit; ++phdr) {
if (phdr->p_type == PT_PHDR) {
return CheckPhdr(load_bias_ + phdr->p_vaddr);
}
}
// Otherwise, check the first loadable segment. If its file offset
// is 0, it starts with the ELF header, and we can trivially find the
// loaded program header from it.
for (const Elf32_Phdr* phdr = phdr_table_; phdr < phdr_limit; ++phdr) {
if (phdr->p_type == PT_LOAD) {
if (phdr->p_offset == 0) {
Elf32_Addr elf_addr = load_bias_ + phdr->p_vaddr;
const Elf32_Ehdr* ehdr = (const Elf32_Ehdr*)(void*)elf_addr;
Elf32_Addr offset = ehdr->e_phoff;
return CheckPhdr((Elf32_Addr)ehdr + offset);
}
break;
}
}
DL_ERR("can't find loaded phdr for \"%s\"", name_);
return false;
}
这个函数不长,直接分析吧,关于它的作用可以参考以下观点:
- FindPhdr 的目的是在 ELF 文件加载到内存后,定位程序头表(Program Header Table)的内存地址;
- 程序头表描述了可执行文件的段(Segment)布局,是加载和运行 ELF 文件的关键数据结构;
- 总体 elf_reader.Load 做的事情大概就是这些,稍微总结一下,ReadElfHeader等函数读取了程序头、校验了程序头,但是并没有实际上对内存做一下处理,只是读出来了放在变量里,真正对内存做事的是后面的ReserveAddressSpace、LoadSegments函数;
- 现在需要回到最开始跟进来的位置,load_library这个函数,继续看他的定义,从我们跟进来的地方开始;
load_library续
static soinfo * load_library(const char* name) {
// Open the file. 说的很清楚了 打开 so 文件 失败返回
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);
// 从这里进去的 Load已经分析完毕
if (! elf_reader.Load()) {
return NULL;
}
const char* bname = strrchr(name, '/');
// 申请内存空间
soinfo* si = soinfo_alloc(bname ? bname + 1 : name);
if (si == NULL) {
return NULL;
}
// 填充 soinfo 结构体 这些内容在Load的分析中都有涉及
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(); // 找到的phdr
return si;
}
- 这个函数也差不多了,总体内容就这些,又需要跳出去,回到 find_library_internal 函数去分析;
find_library_internal续
- 回顾它的定义:
static soinfo * find_library_internal(const char* name) {
if (name == NULL) {
return somain;
}
/*
调用 find_loaded_library(name)来查找已经加载的库 如果找到了 si 不为 NULL
再检查 si 的 flags 是否有 FLAG_LINKED 标志。如果有,说明已经链接过了,直接返回这个 si
如果没找到已加载的库,就会进入加载流程
*/
soinfo* si = find_loaded_library(name);
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);
// 调用 load_library(name)来加载库,返回值是 soinfo 指针 si
// load_library已经分析完 接着往下走
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);
/*
接下来调用 soinfo_link_image(si),这个函数应该处理符号解析、重定位等链接步骤
如果 soinfo_link_image 返回 false,说明链接失败。这时候需要解除内存映射,
释放 soinfo 结构,返回 NULL。否则的话,返回这个 si
*/
if (!soinfo_link_image(si)) {
munmap(reinterpret_cast <void*>(si-> base), si-> size);
soinfo_free(si);
return NULL;
}
return si;
}
- 剩下的其实就剩 soinfo_link_image 这个函数了,看符号应该就是链接了,看看它的实现;
soinfo_link_image
- 它的位置是:bionic/linker/linker.cpp
- 这个函数非常长,所以重要的点都会写在注释里;
static bool soinfo_link_image(soinfo* si) {
// 这几个基本前面解析都遇到过
Elf32_Addr base = si->load_bias;
const Elf32_Phdr *phdr = si->phdr;
int phnum = si->phnum;
bool relocating_linker = (si->flags & FLAG_LINKER) != 0;
if (!relocating_linker) {
INFO("[ linking %s ]", si->name);
DEBUG("si->base = 0x%08x si->flags = 0x%08x", si->base, si->flags);
}
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;
} else {
if (!relocating_linker) {
DEBUG("dynamic = %p", si->dynamic);
}
}
//
#ifdef ANDROID_ARM_LINKER
(void) phdr_table_get_arm_exidx(phdr, phnum, base,
&si->ARM_exidx, &si->ARM_exidx_count);
#endif
// 这里有一个循环遍历动态节区的每个条目(Elf32_Dyn结构),
// 根据不同的d_tag(动态条目标签)来设置soinfo结构的不同字段
// 这段控制流总体来说就是在根据不同的d_tag去做一些事情
// 上面我们解析到了Dynamic段的地址跟数量,下面就开始遍历Dynamic信息
uint32_t needed_count = 0;
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<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:
// 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:
// 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<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(Elf32_Addr);
break;
case DT_PREINIT_ARRAY:
// 也是初始化函数,但是跟init.array不同,这个段大多只出现在可执行文件中,在So中我选择了忽略
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:
// 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
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
#if defined(ANDROID_MIPS_LINKER)
case DT_STRSZ:
case DT_SYMENT:
case DT_RELENT:
break;
case DT_MIPS_RLD_MAP:
{
r_debug** dp = (r_debug**) 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;
default:
DEBUG("Unused DT entry: type 0x%08x arg 0x%08x", d->d_tag, d->d_un.d_val);
break;
#endif
}
}
DEBUG("si->base = 0x%08x, si->strtab = %p, si->symtab = %p",
si->base, si->strtab, si->symtab);
if (relocating_linker && needed_count != 0) {
DL_ERR("linker cannot have DT_NEEDED dependencies on other libraries");
return false;
}
// 这几个判断为0的都是必须要有的
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;
}
// 这段代码是动态链接器(如Android的linker)中的一部分,用于处理可执行文件(而非共享库)启动时的预加载库
// so不用管
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 {
DL_WARN("could not load library \"%s\" from LD_PRELOAD for \"%s\"; caused by %s",
gLdPreloadNames[i], si->name, linker_get_error_buffer());
}
}
}
//至此,Dynamic段的信息就解析完毕了,其中想表达的信息也被处理后放到了soinfo中,后面直接就可以拿来用了
// 开辟依赖库的soinfo空间,准备处理依赖
soinfo** needed = (soinfo**) alloca((1 + needed_count) * sizeof(soinfo*));
soinfo** pneeded = needed;
// 这里和控制流一样 把DT_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->has_text_relocations) {
DL_WARN("%s has text relocations. This is wasting memory and is "
"a security risk. 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;
}
}
// 都是在判断 根据plt_rel、rel 重点是soinfo_relocate 应该是最后的内容了 单独分析
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;
}
}
#ifdef ANDROID_MIPS_LINKER
if (!mips_relocate_got(si, needed)) {
return false;
}
#endif
// 设置soinfo的LINKED标志,表示已进行链接
si->flags |= FLAG_LINKED;
DEBUG("[ finished linking %s ]", si->name);
if (si->has_text_relocations) {
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;
}
}
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;
}
在分析下一个函数之前先总结一下这个soinfo_link_image函数,他虽然很长,但是表达的意思真的很简单:
- 解析Dynamic段信息;
- 处理依赖;
- 准备进行重定位;
- 接下来就开始分析So重定位的逻辑;
soinfo_relocate
- 它的位置是:bionic/linker/linker.cpp
- 上一个函数里出现了 FLAG_LINKED ,表示链接完成,那其实整个linker的内容也差不多了,就剩下这个关键函数,它的定义如下:
- 我们看到它调用了两次,只不过入参不同,分别是我们的重定位表和PLT重定位表,函数很长,依旧在代码块里进行分析,但是分析之前需要了解一下这个结构体;
typedef struct {
Elf32_Word r_offset; /* where to do it */
Elf32_Word r_info; /* index & type of relocation */
} Elf32_Rel;
- 这个结构体比较简单,一个是偏移一个是重定位表类型和index;
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<Elf32_Addr>(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,说明重定位需要用到符号,先来找符号,拿到符号名 st_name
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. */
#elif defined(ANDROID_X86_LINKER)
case R_386_JMP_SLOT:
case R_386_GLOB_DAT:
case R_386_32:
case R_386_RELATIVE: /* Dont' care. */
#endif /* ANDROID_*_LINKER */
break;
#if defined(ANDROID_X86_LINKER)
case R_386_PC32:
sym_addr = reloc;
break;
#endif /* ANDROID_X86_LINKER */
#if defined(ANDROID_ARM_LINKER)
case R_ARM_COPY:
#endif /* ANDROID_ARM_LINKER */
default:
DL_ERR("unknown weak reloc type %d @ %p (%d)",
type, rel, (int) (rel - start));
return -1;
}
} else {
#if 0
if ((base == 0) && (si->base != 0)) {
DL_ERR("cannot locate \"%s\"...",
strtab + symtab[sym].st_name);
return -1;
}
#endif
// 如果我们找到了外部符号,取到外部符号的地址
sym_addr = static_cast<Elf32_Addr>(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<Elf32_Addr*>(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<Elf32_Addr*>(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<Elf32_Addr*>(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<Elf32_Addr*>(reloc) += sym_addr - rel->r_offset;
break;
#elif defined(ANDROID_X86_LINKER)
// X86 X86 X86 X86 X86 X86
case R_386_JMP_SLOT:
count_relocation(kRelocAbsolute);
MARK(rel->r_offset);
TRACE_TYPE(RELO, "RELO JMP_SLOT %08x <- %08x %s", reloc, sym_addr, sym_name);
*reinterpret_cast<Elf32_Addr*>(reloc) = sym_addr;
break;
case R_386_GLOB_DAT:
count_relocation(kRelocAbsolute);
MARK(rel->r_offset);
TRACE_TYPE(RELO, "RELO GLOB_DAT %08x <- %08x %s", reloc, sym_addr, sym_name);
*reinterpret_cast<Elf32_Addr*>(reloc) = sym_addr;
break;
#elif defined(ANDROID_MIPS_LINKER)
// MIPS也不用管
case R_MIPS_REL32:
count_relocation(kRelocAbsolute);
MARK(rel->r_offset);
TRACE_TYPE(RELO, "RELO REL32 %08x <- %08x %s",
reloc, sym_addr, (sym_name) ? sym_name : "*SECTIONHDR*");
if (s) {
*reinterpret_cast<Elf32_Addr*>(reloc) += sym_addr;
} else {
*reinterpret_cast<Elf32_Addr*>(reloc) += si->base;
}
break;
#endif /* ANDROID_*_LINKER */
#if defined(ANDROID_ARM_LINKER)
case R_ARM_RELATIVE:
#elif defined(ANDROID_X86_LINKER)
case R_386_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的基址相加,写入需要重定位的地方
// 这里加上上面的4个一共是5个需要处理
*reinterpret_cast<Elf32_Addr*>(reloc) += si->base;
break;
#if defined(ANDROID_X86_LINKER)
// X86 X86 X86 X86 X86
case R_386_32:
count_relocation(kRelocRelative);
MARK(rel->r_offset);
TRACE_TYPE(RELO, "RELO R_386_32 %08x <- +%08x %s", reloc, sym_addr, sym_name);
*reinterpret_cast<Elf32_Addr*>(reloc) += sym_addr;
break;
case R_386_PC32:
count_relocation(kRelocRelative);
MARK(rel->r_offset);
TRACE_TYPE(RELO, "RELO R_386_PC32 %08x <- +%08x (%08x - %08x) %s",
reloc, (sym_addr - reloc), sym_addr, reloc, sym_name);
*reinterpret_cast<Elf32_Addr*>(reloc) += (sym_addr - reloc);
break;
#endif /* ANDROID_X86_LINKER */
#ifdef ANDROID_ARM_LINKER
case R_ARM_COPY:
// 进行了一些错误处理
if ((si->flags & FLAG_EXE) == 0) {
DL_ERR("%s R_ARM_COPY relocations only supported for ET_EXEC", si->name);
return -1;
}
count_relocation(kRelocCopy);
MARK(rel->r_offset);
TRACE_TYPE(RELO, "RELO %08x <- %d @ %08x %s", reloc, s->st_size, sym_addr, sym_name);
if (reloc == sym_addr) {
Elf32_Sym *src = soinfo_do_lookup(NULL, sym_name, &lsi, needed);
if (src == NULL) {
DL_ERR("%s R_ARM_COPY relocation source cannot be resolved", si->name);
return -1;
}
if (lsi->has_DT_SYMBOLIC) {
DL_ERR("%s invalid R_ARM_COPY relocation against DT_SYMBOLIC shared "
"library %s (built with -Bsymbolic?)", si->name, lsi->name);
return -1;
}
if (s->st_size < src->st_size) {
DL_ERR("%s R_ARM_COPY relocation size mismatch (%d < %d)",
si->name, s->st_size, src->st_size);
return -1;
}
memcpy((void*)reloc, (void*)(src->st_value + lsi->load_bias), src->st_size);
} else {
DL_ERR("%s R_ARM_COPY relocation target cannot be resolved", si->name);
return -1;
}
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中,需要处理的重定位类型并不是很多,就5种类型需要处理,而且还有两种处理方式相同;
- 到目前为止,基本上的内容都分析完成了,还记得在do_dlopen函数说的,会跳来跳去,此时我们就需要回到do_dlopen函数接着分析;
do_dlopen续
- 回顾函数定义:
//do_dlopen
soinfo * do_dlopen(const char* name, int flags) {
if ((flags & ~(RTLD_NOW|RTLD_LAZY|RTLD_LOCAL|RTLD_GLOBAL)) != ) {
DL_ERR("invalid flags to dlopen: %x", flags);
return NULL;
}
// 临时将 soinfo 池(存储动态库元数据的内存区域)的访问权限设置为可读写
// 在 Android 的 Linker 实现中,soinfo 池默认可能被设置为只读(PROT_READ)
// 修改权限是为了允许写入新的库加载信息(如依赖项解析结果)
set_soinfo_pool_protection(PROT_READ | PROT_WRITE);
// 通过 find_library 函数加载目标库及其依赖项
// find_library 这个方法,它会先在 solist(已经加载的动态链接库链表)里进行查找,如果找到了就返回对应的 soinfo 结构体指针。否则,就调用 load_library 进行加载。然后,调用 soinfo_link_image 方法,根据 soinfo 结构体解析相应的 Section
soinfo* si = find_library(name);
if (si != NULL) {
// 最后会调用 CallConstructors
si-> CallConstructors();
}
// 将 soinfo 池恢复为只读
set_soinfo_pool_protection(PROT_READ);
return si;
}
- find_library这个函数就是我们进去的位置,此时So已经被装载且链接过了,现在出来分析CallConstructors这个函数;
CallConstructors
- 它的位置是:bionic/linker/linker.cpp
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);
}
- 关于CallFunction函数,也可以稍微看一下;
CallFunction
- 它的位置是:bionic/linker/linker.cpp
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就分析结束了;
3. 总结
- 在分析完整个加载过程后我们发现,其实最主要的就是处理了下面这两个段信息;
4. 参考资料
对以下大佬表示感谢:
- Linker加载so流程:https://yuuki.cool/2025/04/16/ReReadLinker/
- Android脱壳2-so文件加载过程剖析:https://www.youncyb.cn/?p=1152
- Android脱壳3-so文件链接过程剖析:https://www.youncyb.cn/?p=1160
- Android Linker详解(二):https://bbs.kanxue.com/thread-269891.htm
- 源码查看地址:https://cs.android.com/android/platform/superproject/+/android-4.4.4_r1: