安卓逆向-Linker详解

sana
7月4日发布 /正在检测是否收录...

4.4.4版本下Linker 详解

1. 简述

  • 在 Android 系统中,Linker(链接器)是负责将编译后的目标文件(.o)和库文件(如 .so.a)合并为可执行文件的关键组件,其核心角色涉及 符号解析、重定位、动态链接 等核心流程;
  • 后续主要是分析安卓加载 so 的源码,源码的版本是 4.4.4;
  • 在整体的分析流程中,主要的解释会写在代码块里以注释的形式表现;

2. 源码分析

  • Java 有两种方式加载库文件,一种是 System.load(),另一种是 System.loadLibrary();前一种需要传入绝对路径,后一种只需要指出库文件名称即可,安卓一般采取后一种方式;在分析前,可以先看看图 1,直观感受调用链,不过下图并不完全契合我们的流程,因为版本会有所差异:

image-20250702163927958

  • 在安卓进行加载 so 的时候,主要入口就是如下代码,也是这里的重点,由它开始分析;
static {
    System.loadLibrary("rc4");
}
  • 分析的源码版本是 4.4,从 System.java 这个类开始看;

image-20250318211157357

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,去看它的实现;

image-20250318212309628

  • 这里是有重载,但根据我们传递的参数,应该是下面的这个两个参数的方法;看看它的实现;
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 写的,那我们需要找一下它的具体实现位置;可以去搜索一下,这里勾选上所有的文件;

image-20250319222059793

  • 找到它的实现位置,或者说根据拼接规则来,当前我们在 [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;
}
  • 总结一下,这个函数的主要步骤是:

    1. 遍历所有程序头;
    2. 对于每个可加载段,计算内存地址并进行页对齐;
    3. 将文件中的段映射到内存,可能包括多个页面;
    4. 对于段末尾未填满的页面部分,清零处理;
    5. 如果内存中的段比文件中的大,用匿名映射填充多出的部分;
    6. 所有操作失败时返回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. 总结

  • 在分析完整个加载过程后我们发现,其实最主要的就是处理了下面这两个段信息;

image-20250704102637669

4. 参考资料

© 版权声明
THE END
喜欢就支持一下吧
点赞 1 分享 收藏
评论 抢沙发
OωO
取消