android 动态库加载机制

HarmonyOS Next开发学习手册——C/C++标准库机制 fdsan针对的操作对象是文件描述符,主要用于检测不同使用者对相同文件描述符的错误操作,包括多次关闭(double-close)和关闭后使用(use-after-close)。这些文件描述符可以是操作系统中的文件、目录、网络套接字和其他I/O设备等,在程序中,打开文件或套接字会生成一个文件描述符,如果此文件描述符在使用后出现反复关闭、或者关闭后使用等场景,就会造成内存泄露、文件句柄泄露等安全隐患问题。 阅读详情

省流:android 不兼容 glibc,而是写了一套独立的 c 运行时库 (bionic libc),为移动设备和 google 自己推的东西做了大量优化。在这套工具链里,aosp 实现了一个兼容 bionic libc 的链接器,放到系统中代替 ld。

这个链接器叫 linker,二进制放在 /bin 和 /system/bin 目录下。

好像有点跑题了,我都忘了在哪看到的 linker 才写到这里的。可能是 frida 用了 Interceptor hook 的 linker

扯回来。在 Java 的应用层代码里写 System.loadLibrary 后,会经历如下调用栈

// java
System.loadLibrary
Runtime.loadLibrary0
// jni
// c
Runtime_nativeLoad
JVM_NativeLoad
// cc
art::JavaVMExt::LoadNativeLibrary // 检查是否是要 load 已有 library,不是的话调用 OpenNativeLibrary(dlopen) 获取 handle、创建 library 对象、查找 JNI_Onload 的 sym,没有的话直接 return true.
android::OpenNativeLibrary
OpenSystemLibrary ...(android_dlopen_ext/dlopen)

其中,从 nativeLoad 开始,执行流通过 jni 跳转到了 c 函数 Runtime_nativeLoad。可以查看 Runtime.c - Android Code Search ,往里跟踪的其他 c 文件,最后调用到 android_dlopen_ext,然后转到 linker.cpp 的 find_libraries

详解 linker find_libraries 的 7 步实现

详解每一层调用的博客:

​​​Android动态库的加载原理,真的理解了吗?一文带你游览动态库的加载流程,从 Java 到 C++,一步一步探索知识的 - 掘金

这篇把无关代码删了,很方便阅读。实际进行链接操作的核心函数是 /bionic/linker/linker.cpp 的 find_libraries 函数。不过上文作者删的太多了,有点难以理解中间变量的意义,让 deepseek 打点注释好理解多了。

命名空间(与 c 做区分,是安卓linker 的加载命名空间),用于动态库加载隔离,记录了开始搜索的 start_from_path。

以下是我理解的关键逻辑:

Step 0: Prepare - 创建一个 load_tasks 数组,用形参的 libnames 初始化,此时还未查找库文件

Step 1: Extend load_tasks:调用 load_libraries_internal 函数从命名空间查找待加载库文件,扩充库文件的 DT_NEEDED 依赖到 load_tasks,添加 soinfo 到 needed_by 的字节点和 so_infos 数组。(在这一步打开文件,保存 fd 到 task 但不 mmap)

比对库文件的最底层调用是 strcmp(ns_name, sonane),namespace 的路径是在 linker.cpp: static const char* KDefaultLdPaths[] 定义的字符串指针数组

Step 2: Random order load:随机顺序加载库。按顺序把 load_task 放到 load_list 里,剔除已链接项和重复项,然后 shuffle,用 ElfReader 加载到内存(mmap segments)

Step 3: bfs pre-link all libs:预链接。解析 .dynamic 段的元数据。通过 ELF 的程序头表 (PH) 获取 .dynamic section 的地址,解析段中各种元数据,包括 (strtab, symtab, rel) 表的地址, size 等,保存在 si 的成员变量里。for + switch, 最后注册 tls。

Step 4: 覆盖设置 LD_PRELOAD 库的 DF_1_GLOBAL 标志

Step 5: collect roots of local_groups. 按照依赖顺序构建动态库链接树,维护 vector<soinfo*> 记录所有的 local_group 的 root。按 DT_NEEDED 和 namespace 划分

Step 6: Link,链接动态库,进行符号解析与重定位。

  • 首先构建 (global_group, local_group) 的 lookup_list,调用 soinfo::relocate(looup_list)
  • 根据各个段的存在性,先后顺次执行重定位逻辑,支持的段包括: .relr, "APS2"(android 压缩 .rel, .rela), (typical) .rela | (plt) .rela / (typical) .rel | (plt) .rel
  • 对段名 switch,调用模板函数,遍历每一项 rel[i]。 0. 从 r_offset 计算要修改的地址 rel_target (要修改的 got entry) 1. 解析:从 r_index 识别当前符号 2. 从 symtab 里通过 id 找到 sym_name 3. 调用 lookup_symbol 从动态库里找到符号,获取实际地址 3. 写入 *rel_target
  • 最后处理 TLSDESC
  • 对于 -Bsymbolic 写了单独处理逻辑
Offset   Type     Symbol Index  Addend
0x0040   R_386_32  0x0000010   0
r_offset

Step 7: 将所有 load_tasks 标记为已链接,并增加 load_groups 间的引用数;

// 该函数用于查找并加载多个共享库,处理库之间的依赖关系
// add_as_children - 是否将一级加载的库(library_names[]中的库,不包括它们的传递依赖)
//                   作为start_with库的子节点。当为dlopen()调用时设为false,
//                   此时新加载的库必须形成独立的树结构
bool find_libraries(android_namespace_t* ns,          // 使用的命名空间
                    soinfo* start_with,                // 起始库(父节点)
                    const char* const library_names[], // 要加载的库名数组
                    size_t library_names_count,        // 库名数组长度
                    soinfo* soinfos[],                 // 输出加载的soinfo指针数组
                    std::vector<soinfo*>* ld_preloads, // 预加载库列表
                    size_t ld_preloads_count,          // 预加载库数量
                    int rtld_flags,                    // 加载标志(如RTLD_LAZY等)
                    const android_dlextinfo* extinfo,  // 扩展加载信息
                    bool add_as_children,              // 是否作为子节点添加
                    std::vector<android_namespace_t*>* namespaces) { // 相关命名空间列表
  // 步骤0:准备工作
  std::unordered_map<const soinfo*, ElfReader> readers_map; // 用于缓存ELF读取器
  LoadTaskList load_tasks; // 加载任务列表

  // 为每个库名创建初始加载任务
  for (size_t i = 0; i < library_names_count; ++i) {
    const char* name = library_names[i];
    load_tasks.push_back(LoadTask::create(name, start_with, ns, &readers_map));
  }

  // 如果soinfos数组为空,在栈上分配临时空间
  // 需要这个数组来处理加载部分失败的情况(例如加载多个库时部分成功)
  if (soinfos == nullptr) {
    size_t soinfos_size = sizeof(soinfo*)*library_names_count;
    soinfos = reinterpret_cast<soinfo**>(alloca(soinfos_size)); // 栈上分配临时内存
    memset(soinfos, 0, soinfos_size); // 初始化为空指针
  }

  size_t soinfos_count = 0; // 已加载的库数量

  // 使用范围守卫确保加载任务会被清理
  auto scope_guard = android::base::make_scope_guard([&]() {
    for (LoadTask* t : load_tasks) {
      LoadTask::deleter(t); // 退出时清理加载任务
    }
  });

  ZipArchiveCache zip_archive_cache; // 用于APK内文件的缓存
  soinfo_list_t new_global_group_members; // 新的全局组成员

  // 步骤1:扩展加载任务列表,包含所有DT_NEEDED依赖(暂时不实际加载)
  for (size_t i = 0; i<load_tasks.size(); ++i) {
    LoadTask* task = load_tasks[i];
    soinfo* needed_by = task->get_needed_by(); // 获取依赖该库的父库

    // 判断是否是DT_NEEDED依赖(非直接加载的依赖)
    bool is_dt_needed = needed_by != nullptr && (needed_by != start_with || add_as_children);
    task->set_extinfo(is_dt_needed ? nullptr : extinfo); // 设置扩展信息
    task->set_dt_needed(is_dt_needed); // 标记是否为DT_NEEDED依赖

    // 从任务指定的命名空间开始查找(可能不同于当前命名空间)
    android_namespace_t* start_ns = const_cast<android_namespace_t*>(task->get_start_from());

    // 实际查找库的实现
    if (!find_library_internal(start_ns, task, &zip_archive_cache, &load_tasks, rtld_flags)) {
      return false; // 查找失败直接返回
    }

    soinfo* si = task->get_soinfo(); // 获取找到的soinfo对象

    // 如果是DT_NEEDED依赖,添加到父库的子节点
    if (is_dt_needed) {
      needed_by->add_child(si);
    }

    // 处理预加载库(LD_PRELOAD)
    bool is_ld_preload = false;
    if (ld_preloads != nullptr && soinfos_count < ld_preloads_count) {
      ld_preloads->push_back(si);
      is_ld_preload = true;
    }

    // 填充输出数组
    if (soinfos_count < library_names_count) {
      soinfos[soinfos_count++] = si;
    }

    // 处理全局符号可见性(DF_1_GLOBAL标志)
    if (is_ld_preload || (si->get_dt_flags_1() & DF_1_GLOBAL) != 0) {
      if (!si->is_linked() && namespaces != nullptr && !new_global_group_members.contains(si)) {
        new_global_group_members.push_back(si);
        // 将新全局成员添加到所有初始命名空间
        for (auto linked_ns : *namespaces) {
          if (si->get_primary_namespace() != linked_ns) {
            linked_ns->add_soinfo(si);     // 添加到命名空间
            si->add_secondary_namespace(linked_ns); // 添加二级命名空间
          }
        }
      }
    }
  }

  // 步骤2:随机顺序加载库(防止地址预测攻击)
  LoadTaskList load_list;
  // 收集所有需要加载的库(过滤已链接的)
  for (auto&& task : load_tasks) {
    soinfo* si = task->get_soinfo();
    auto pred = [&](const LoadTask* t) { return t->get_soinfo() == si; };
    if (!si->is_linked() && std::find_if(load_list.begin(), load_list.end(), pred) == load_list.end()) {
      load_list.push_back(task);
    }
  }

  // 处理地址保留标志
  bool reserved_address_recursive = false;
  if (extinfo) {
    reserved_address_recursive = extinfo->flags & ANDROID_DLEXT_RESERVED_ADDRESS_RECURSIVE;
  }
  if (!reserved_address_recursive) {
    shuffle(&load_list); // 随机打乱加载顺序
  }

  // 设置地址空间参数
  address_space_params extinfo_params, default_params;
  size_t relro_fd_offset = 0;
  if (extinfo) {
    // 处理保留地址相关的标志
    if (extinfo->flags & ANDROID_DLEXT_RESERVED_ADDRESS) {
      extinfo_params.start_addr = extinfo->reserved_addr;
      extinfo_params.reserved_size = extinfo->reserved_size;
      extinfo_params.must_use_address = true;
    } else if (extinfo->flags & ANDROID_DLEXT_RESERVED_ADDRESS_HINT) {
      extinfo_params.start_addr = extinfo->reserved_addr;
      extinfo_params.reserved_size = extinfo->reserved_size;
    }
  }

  // 实际加载库到内存
  for (auto&& task : load_list) {
    address_space_params* address_space =
        (reserved_address_recursive || !task->is_dt_needed()) ? &extinfo_params : &default_params;
    if (!task->load(address_space)) {
      return false;
    }
  }

  // 步骤3:预链接所有依赖库(广度优先顺序)
  bool any_memtag_stack = false;
  for (auto&& task : load_tasks) {
    soinfo* si = task->get_soinfo();
    if (!si->is_linked() && !si->prelink_image()) { // 执行预链接
      return false;
    }
    // 检查内存标签(MTE)相关设置
    if (si->has_min_version(7) && si->memtag_stack()) {
      any_memtag_stack = true;
    }
    register_soinfo_tls(si); // 注册TLS信息
  }

  // 处理内存标签回调
  if (any_memtag_stack) {
    if (auto* cb = __libc_shared_globals()->memtag_stack_dlopen_callback) {
      cb();
    } else {
      __libc_shared_globals()->initial_memtag_stack = true;
    }
  }

  // 步骤4:强制设置LD_PRELOAD库的DF_1_GLOBAL标志
  if (ld_preloads != nullptr) {
    for (auto&& si : *ld_preloads) {
      si->set_dt_flags_1(si->get_dt_flags_1() | DF_1_GLOBAL);
    }
  }

  // 步骤5:收集本地组的根节点(处理跨命名空间的情况)
  std::vector<soinfo*> local_group_roots;
  if (start_with != nullptr && add_as_children) {
    local_group_roots.push_back(start_with); // 起始库作为根
  } else {
    CHECK(soinfos_count == 1);
    local_group_roots.push_back(soinfos[0]); // 第一个加载的库作为根
  }

  // 遍历加载任务查找跨命名空间的边界
  for (auto&& task : load_tasks) {
    soinfo* si = task->get_soinfo();
    soinfo* needed_by = task->get_needed_by();
    bool is_dt_needed = needed_by != nullptr && (needed_by != start_with || add_as_children);
    android_namespace_t* needed_by_ns = is_dt_needed ? needed_by->get_primary_namespace() : ns;

    // 如果库未链接且跨命名空间
    if (!si->is_linked() && si->get_primary_namespace() != needed_by_ns) {
      auto it = std::find(local_group_roots.begin(), local_group_roots.end(), si);
      if (it == local_group_roots.end()) {
        local_group_roots.push_back(si); // 添加为新的本地组根
      }
    }
  }

  // 步骤6:链接所有本地组
  for (auto root : local_group_roots) {
    soinfo_list_t local_group;
    android_namespace_t* local_group_ns = root->get_primary_namespace();

    // 遍历依赖树收集本地组成员
    walk_dependencies_tree(root,
      [&] (soinfo* si) {
        if (local_group_ns->is_accessible(si)) { // 检查访问权限
          local_group.push_back(si);
          return kWalkContinue;
        } else {
          return kWalkSkip;
        }
      });

    // 准备符号查找列表(全局组 + 本地组)
    soinfo_list_t global_group = local_group_ns->get_global_group();
    SymbolLookupList lookup_list(global_group, local_group);
    soinfo* local_group_root = local_group.front(); // 本地组根节点

    // 遍历并链接本地组成员
    bool linked = local_group.visit([&](soinfo* si) {
      if (!si->is_linked() && si->get_primary_namespace() == local_group_ns) {
        // 处理扩展信息
        const android_dlextinfo* link_extinfo = nullptr;
        if (si == soinfos[0] || reserved_address_recursive) {
          link_extinfo = extinfo;
        }
        // 调用加载hook(如果有)
        if (__libc_shared_globals()->load_hook) {
          __libc_shared_globals()->load_hook(si->load_bias, si->phdr, si->phnum);
        }
        // 处理符号查找
        lookup_list.set_dt_symbolic_lib(si->has_DT_SYMBOLIC ? si : nullptr);
        if (!si->link_image(lookup_list, local_group_root, link_extinfo, &relro_fd_offset) ||
            !get_cfi_shadow()->AfterLoad(si, solist_get_head())) {
          return false;
        }
      }
      return true;
    });

    if (!linked) {
      return false;
    }
  }

  // 步骤7:标记所有加载任务为已链接,并更新引用计数
  if (start_with != nullptr && add_as_children) {
    start_with->set_linked();
  }

  // 标记所有库为已链接状态
  for (auto&& task : load_tasks) {
    soinfo* si = task->get_soinfo();
    si->set_linked();
  }

  // 更新跨组的引用计数
  for (auto&& task : load_tasks) {
    soinfo* si = task->get_soinfo();
    soinfo* needed_by = task->get_needed_by();
    if (needed_by != nullptr &&
        needed_by != start_with &&
        needed_by->get_local_group_root() != si->get_local_group_root()) {
      si->increment_ref_count(); // 增加引用计数
    }
  }

  return true; // 全部加载成功
}

符号解析与重定位

符号解析(Symbol Resolution)

符号解析是指动态链接器(如 Android 的 linker 或 GNU ld.so)在加载共享库(so 文件)时,查找和绑定程序所需的符号(函数或变量)的过程。

1. 符号表(Symbol Table)

ELF 可执行文件和共享库中包含 符号表,它存储了所有函数和全局变量的信息,每个符号都有:

  • 符号名称(Name):如 printf
  • 符号地址(Address):符号在内存中的位置(或者偏移量)
  • 符号类型(Type):函数、全局变量等
  • 符号大小(Size):符号占用的字节数
  • 符号绑定(Binding):
    • STB_LOCAL:本地符号,仅在当前共享库或可执行文件可见
    • STB_GLOBAL:全局符号,可以被其他库引用
    • STB_WEAK:弱符号,如果存在全局符号,则使用全局符号,否则使用弱符号

ELF 符号表通常存储在 DT_SYMTAB(符号表)和 DT_STRTAB(字符串表)中,前者包含符号信息,后者存储符号的名称。

2. 符号解析的步骤
  1. 查找符号:

    • 动态链接器会遍历加载的所有共享库,并在符号表中查找所需符号。
    • 现代 ELF 采用 GNU Hash Table (DT_GNU_HASH) 或 传统哈希表 (DT_HASH) 加速查找。
  2. 符号作用域:

    • 符号解析遵循 ELF 共享库加载顺序(即 DT_NEEDED 的顺序)。
    • 若启用了 DF_SYMBOLIC(DT_FLAGS 中设置),则先在本共享库中查找,再去其他库查找。
    • 如果启用了 RTLD_LOCAL(默认),则库内符号仅对自己可见;如果使用 RTLD_GLOBAL,则符号可以被后续加载的库使用。
  3. 符号冲突解决:

    • 若两个库定义了同名全局符号,默认使用 先加载的库 中的符号(依赖 ELF 加载顺序)。
    • STB_WEAK 符号如果有同名的 STB_GLOBAL,则 STB_GLOBAL 具有更高优先级。

重定位(Relocation)

重定位是指当 ELF 可执行文件或共享库被加载到内存后,调整其中的符号地址,使其指向正确的内存位置的过程。

1. 为什么需要重定位?
  • 共享库地址不固定:

    • 共享库使用 位置无关代码(PIC,Position Independent Code),使其可以加载到任意地址,但某些符号仍然需要修正地址。
    • Android 64 位系统禁止 TEXTREL(文本段重定位),要求所有数据都在数据段完成重定位。
  • 符号地址动态解析:

    • 动态链接库中的函数和变量地址在编译时无法确定,运行时需要根据加载地址调整。
2. 重定位表(Relocation Table)

ELF 提供 重定位表,存储需要调整的地址信息:

  • DT_RELA 或 DT_REL:存储所有需要重定位的项。
  • DT_JMPREL:存储 PLT/GOT 相关的重定位信息(用于函数调用)。
  • DT_PLTGOT:指向 PLT/GOT 表,帮助间接函数调用。
3. 重定位类型

ELF 体系结构定义了多种重定位方式,不同架构可能有所不同,常见的包括:

  • R_xxx_RELATIVE:
    • 直接调整地址,适用于全局变量。
    • 例如 R_AARCH64_RELATIVE、R_386_RELATIVE。
  • R_xxx_GLOB_DAT:
    • 全局数据的符号地址需要在动态链接时解析,如 printf。
  • R_xxx_JUMP_SLOT:
    • 用于**延迟绑定(Lazy Binding)**的 PLT/GOT 机制,主要用于函数调用。
  • R_xxx_ABS:
    • 绝对地址重定位,一般用于静态编译的二进制。
4. 重定位过程
  1. 读取重定位表 DT_RELA/DT_REL:

    • 遍历重定位表中的所有项,每个项包含:
      • 重定位类型(如 R_AARCH64_RELATIVE)
      • 需要修改的地址(Addend)
      • 符号索引(如果涉及符号解析)
  2. 计算新地址:

    • 如果是 R_xxx_RELATIVE,直接加上 load_bias 进行修正。
    • 如果是 R_xxx_GLOB_DAT,则需要从符号表解析出地址。
    • 如果是 R_xxx_JUMP_SLOT,则修正 GOT 表中存储的目标函数地址。
  3. 写入新地址:

    • 动态链接器将计算后的地址写入相应的 GOT 或内存地址。

示例:函数调用的重定位

假设 main() 调用了 printf(),但 printf() 在 libc.so 里,编译时并不知道 printf 的确切地址,运行时需要修正。

1. PLT(Procedure Linkage Table)
  • 当 main() 调用 printf() 时,代码不会直接跳转到 printf,而是跳转到 PLT(过程链接表)。
  • PLT 里的第一条指令会间接跳转到 GOT(全局偏移表) 中存储的 printf 入口地址。
2. GOT(Global Offset Table)
  • 初始时,GOT 里 printf 对应的项指向 PLT 自己的入口。
  • 当第一次调用 printf() 时,PLT 触发 lazy binding(延迟绑定)。
  • 动态链接器在 DT_JMPREL 表里找到 printf 对应的 R_xxx_JUMP_SLOT 记录。
  • 解析 printf 的真实地址,并填充到 GOT 中。
  • 之后的 printf 调用就可以直接从 GOT 跳转到 printf 的真实地址。

总结

  • 符号解析:查找 ELF 符号表,确定符号的真实地址。
  • 重定位:修正共享库加载后的符号地址,使其指向正确的位置。
  • 关键机制:
    • 采用 ELF 符号表(SYMTAB, STRTAB) 存储符号信息。
    • 使用 哈希表(GNU Hash, DT_HASH) 加速符号查找。
    • 重定位表(DT_RELA, DT_REL, DT_JMPREL) 存储需要修正的地址信息。
    • PLT/GOT 机制 进行延迟绑定,提高性能。

理解这些概念对于分析 Android 动态库加载机制、逆向分析 so 文件非常重要!你可以结合 objdump -D 或 readelf -r 查看具体的 ELF 重定位信息,以加深理解。

symtab 符号表条目

每个条目关联了一个地址(函数/变量)到一个字符串池 id

在 ELF (Executable and Linkable Format) 文件中,符号表(symtab)是一个保存符号信息的结构数组。每个符号条目(ElfN_Sym,N 表示 32 或 64 位架构)包含以下字段:


符号表条目结构定义

以 64 位架构为例,符号表条目定义如下(定义在 <elf.h> 中):

typedef struct {
    Elf64_Word    st_name;   // 符号名称在字符串表中的偏移量
    unsigned char st_info;   // 符号类型和绑定信息
    unsigned char st_other;  // 符号的可见性(如默认/隐藏)
    Elf64_Section st_shndx;  // 关联的段索引(如 .text、.data)
    Elf64_Addr    st_value;  // 符号的地址(内存中的偏移量)
    Elf64_Xword   st_size;   // 符号的大小(如函数代码大小)
} Elf64_Sym;

字段解释及示例

1. st_name
  • 作用:符号名称在字符串表(.strtab)中的偏移量。
  • 示例:
    • 若 st_name = 0x10,表示符号名称存储在字符串表第 16 字节的位置。
    • 若字符串表在 0x2000 处,则符号名称地址为 0x2000 + 0x10 = 0x2010。
2. st_info
  • 作用:高 4 位表示符号绑定(全局/局部/弱引用),低 4 位表示符号类型(函数/变量/未定义等)。
  • 解析方法:
    • 绑定类型:ELF64_ST_BIND(info)
      • STB_GLOBAL (1):全局符号(如 printf)
      • STB_LOCAL (0):局部符号(如静态函数)
      • STB_WEAK (2):弱引用符号(可被覆盖)
    • 符号类型:ELF64_ST_TYPE(info)
      • STT_FUNC (2):函数(如 main)
      • STT_OBJECT (1):变量(如全局变量 int global_var)
      • STT_NOTYPE (0):未定义符号(如外部依赖的符号)

示例:

  • st_info = 0x12(二进制 0001_0010):
    • 高 4 位 0001:STB_GLOBAL(全局符号)
    • 低 4 位 0010:STT_FUNC(函数)
3. st_other
  • 作用:符号的可见性(默认为 0,表示默认可见性)。
  • 常见值:
    • STV_DEFAULT (0):符号可被其他模块访问。
    • STV_HIDDEN (1):符号仅在本模块可见(静态链接时隐藏)。
4. st_shndx
  • 作用:符号关联的段索引(如 .text 段、.data 段)。
  • 特殊值:
    • SHN_UNDEF (0):未定义的符号(如外部依赖的 malloc)。
    • SHN_ABS (0xfff1):绝对地址符号(如编译时常量)。

示例:

  • st_shndx = 1:符号关联到 .text 段(段索引由段表定义)。
5. st_value
  • 作用:符号的地址(在可执行文件中的逻辑偏移,加载时会被重定位)。
  • 示例:
    • 若符号是函数,st_value 是函数在代码段(.text)中的偏移量。
    • 若符号是全局变量,st_value 是变量在数据段(.data)中的偏移量。
6. st_size
  • 作用:符号的大小(如函数代码的大小或变量的字节数)。
  • 示例:
    • 函数 int foo() { ... } 编译后代码大小为 20 字节,则 st_size = 20。

具体示例

假设我们有一个全局函数 void my_func() 和一个全局变量 int global_var,它们的符号表条目如下:

1. 函数符号 my_func
Elf64_Sym entry_func = {
    .st_name   = 0x20,     // 字符串表中偏移 0x20 处是 "my_func"
    .st_info   = 0x12,     // STB_GLOBAL | STT_FUNC
    .st_other  = 0,        // 默认可见性
    .st_shndx  = 1,        // 关联到 .text 段(段索引 1)
    .st_value  = 0x4000,   // 函数在 .text 段的偏移地址
    .st_size   = 0x50,     // 函数代码大小为 80 字节
};
2. 变量符号 global_var
Elf64_Sym entry_var = {
    .st_name   = 0x30,     // 字符串表中偏移 0x30 处是 "global_var"
    .st_info   = 0x11,     // STB_GLOBAL | STT_OBJECT
    .st_other  = 0,        // 默认可见性
    .st_shndx  = 3,        // 关联到 .data 段(段索引 3)
    .st_value  = 0x5000,   // 变量在 .data 段的偏移地址
    .st_size   = 4,        // int 类型占 4 字节
};
3. 未定义符号 malloc(外部依赖)
Elf64_Sym entry_undef = {
    .st_name   = 0x40,     // 字符串表中偏移 0x40 处是 "malloc"
    .st_info   = 0x10,     // STB_GLOBAL | STT_NOTYPE
    .st_other  = 0,
    .st_shndx  = SHN_UNDEF,// 未定义(需要动态链接)
    .st_value  = 0,        // 未定义符号的地址为 0
    .st_size   = 0,        // 大小未知
};

如何查看符号表?

使用 readelf 或 objdump 工具可以查看 ELF 文件的符号表:

readelf -s libexample.so   # 查看符号表
objdump -t libexample.so   # 显示符号表条目

输出示例:

Symbol table '.dynsym' contains 5 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000004000    80 FUNC    GLOBAL DEFAULT    1 my_func
     2: 0000000000005000     4 OBJECT  GLOBAL DEFAULT    3 global_var
     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND malloc@GLIBC_2.34

总结

  • 符号表条目:每个条目描述了一个符号的名称、类型、绑定方式、所在段和地址。
  • 关键字段:st_info 决定了符号的全局性(如是否可被其他模块访问)和类型(函数/变量)。
  • 动态链接:未定义符号(SHN_UNDEF)需要在运行时通过动态链接器解析。

理论讲解的 ref

深入了解GOT,PLT和动态链接

.symtab 和 .strtab 是常规符号表,可以 strip 删了。.dynstr, .dynsym 是动态符号表,动态链接要用

链接器的工作:填充 GOT(global offset table) 表,更新外部符号的位置。动态库里,实现函数跳转的方法是,跳转到 .got[i]。汇编里写死 got 表的偏移间接寻址.
如果是 plt 模式,got 内填写 plt 表 (Procedure Linkage Table) 内容,再次间接跳转到 _dl_runtime_resolve

linker_relocate::process_relocation_impl

Android音频学习(十四)——加载音频设备 loadHwModule 是 Android 音频系统的核心初始化函数,负责加载音频硬件抽象层(HAL)的模块实现。它建立 AudioPolicyService 与底层硬件的桥梁,将配置文件(audio_policy_config.xml)中的标签<module>定义转化为可操作的硬件对象,加载的是音频硬件模块(HAL实现),每个模块对应一种音频设备类型,以动态库的形式存在,负责与底层音频硬件交互。 阅读详情

相关推荐

ARMv7l架构Android系统动态库文件深入解析

ARMv7架构是ARM公司设计的一系列32位RISC处理器架构的代号。这一架构自从2000年代初期开始,逐渐成为移动设备处理器架构的事实标准。ARMv7架构在保持低功耗的基础上,加强了计算性能和多媒体处理能力,这使得搭载ARMv7处理器的移动设备在处理速度上有了质的飞跃。ARMv7架构的主要特点包括:高性能与能效比:通过对处理器的优化设计,ARMv7在提供高性能的同时,保证了低功耗的运行。丰富的指令集:ARMv7拥有丰富的指令集,支持多种编程语言和操作系统。

weixin_35757531的博客 1311

详解Android动态库的加载原理

源码版本:android-13.0.0_r41先通过相关的命名空间查找动态库;找到后打开动态库;把动态库加载到内存中,返回 handler;构建SharedLibrary,存有 3 中的 handler,然后保存到 libraries_ 中;检查是否有 JNI_OnLoad 函数,有就执行;4 中 SharedLibrary 析构的时候,调用 dlclose(handle) 关闭动态库;对动态库加载流程有了整体的认知;对于 ELF 文件结构,认识了一些常见的表和节区;

金戈鐡馬 2489

波士顿咨询最新发布《2025年全球支付报告:未来不稳定》:到2029 年,支付收入将预计达到 2.4 万亿美元

波士顿咨询最新发布《2025年全球支付报告:未来不稳定》:到2029 年,支付收入将预计达到 2.4 万亿美元

face light~ 917

Android APK调用JNI加载so动态库

AndroidN加载私有库时报错:xxx is not accessible for the namespace

特立独行的博客 6185

离散数学 --- 图论基础 --- 图的同构,通路与回路,可达性与最短通路

同一个图(这里的图是抽象的数学定义)可以有不同的图形表示方法1.重数:两点之间的平行边的个数1.得到 n!的过程,一个图中的一个结点在另一个图中对应的结点有n种可能(黄框中定义的图来讨论),这个对应好后下一个结点有 n - 1 种可能,再下一个有n-2种,直到最后一个为1,所有可能的结果就等于 n * (n - 1) *(n - 2)*....*1 = n!2.虽然说找对应结点很困难,但不是没有规律可循的。比如1.两个相互对应的结点的度数要相同;

qq_51947882的博客 7439

iSLIP算法实战:如何用Python模拟交换机优先级匹配(附完整代码)

本文详细介绍了如何使用Python实现iSLIP算法,模拟交换机优先级匹配过程。通过核心的指针滑动逻辑和迭代循环策略,构建了一个完整的iSLIP算法模拟器,并加入可视化模块以直观展示匹配过程。文章还提供了完整的代码框架和性能优化建议,适合网络工程师和Python开发者直接集成到测试环境中。

weixin_29313239的博客 249

Android 项目调用第三方库so动态库

在Android NDK开发中,可以通过自己编写C/C++代码来构建so动态库进行调用之外,还可以把so动态库提供给第三方使用;接下来,我将介绍在新创建的Android 项目(或已存在的Android 项目)中如何调用第三方so动态库。关于如何进行NDK开发,实现JNI,以及编写C++与Java交互(Java调用本地函数)并编译出本地so动态库,请查看。

UrasakiNakajima的博客 1万+

MobaXterm-SSH远程连接Ubuntu

我们常常在公司的服务器上面的虚拟机进行交叉编译,所以才需要我们用远程终端工具MobaXterm去接入虚拟机。然后把我们在window用VScode编辑好的代码拖拽上传到服务器,等待服务器编译完把生成的可执行文件从服务器拖拽下来,烧录到板子运行。

weixin_50257954的博客 3470

编译适配安卓的动态库

编译适配安卓的动态库

wanglei_11的博客 566

斑马打印机状态监控进阶:不用SDK也能获取打印状态的3种野路子

本文介绍了三种不依赖官方SDK的斑马打印机状态监控方法,包括SGD命令、TCP端口嗅探和共享打印机日志分析。这些方案适用于老旧系统或网络受限环境,帮助运维人员实时掌握打印状态,提升设备管理效率。特别适合工业自动化和物流仓储场景中的应急处理。

weixin_42628688的博客 426

YOLOv11改进 | 特殊场景改进篇 | 2024最新改进CPA-Enhancer链式思考网络(适用低照度、图像去雾、雨天、雪天)

本文给大家带来的改进机制是2024.3月份最新改进,由CPA-Enhancer: Chain-of-Thought Prompted Adaptive Enhancer for Object Detection under Unknown Degradations论文提出的CPA-Enhancer链式思考网络CPA-Enhancer通过引入链式思考提示机制,实现了对未知退化条件下图像的自适应增强。该方法的核心在于能够利用CoT提示对图像退化进行动态分析和适应,从而显著提升物体检测性能。

Snu77的博客 2013

android 动态库 加载机制,Android的JNI_OnLoad简介与应用

#include //jni的主要头文件//函数名字可以随便取,不过参数一定要和javah生成的函数的参数一致,包括返回值static void JNICALL func2 (JNIEnv *env, jobject jobj){printf("--- func2 called in version 2\n");}//定义批量注册的数组,是注册的关键部分static const JNINativ...

weixin_35998791的博客 893

从零开始用 C# 打造工业级上位机系统:完整项目实战

本文介绍了一个基于C#开发的工业级上位机系统项目。该系统采用分层架构设计,包含通信、数据采集、UI展示、告警日志等模块,支持Modbus协议和串口通信。文章详细阐述了系统架构设计、关键模块实现(如Modbus通信封装、实时数据采集、告警系统等)以及性能优化策略。该项目已在实际工控场景中稳定运行超过1年,具备良好的可扩展性和稳定性,适合有一定C#基础的开发者参考学习工业软件开发实践。

这里只有干货:从 Modbus 通信到 MES 对接,从 YOLO 训练到工控机部署,带你搞定工业软件开发全流程。 1366

android动态库存放路径问题

首先来看下我们是如何使用自己的动态库: public class MyActivity extends Activity { /** Called when the activity is first created. */ //WebView webView; public native int sum(int i, int j); static{

wuhengde的专栏 8210

基于MATLAB Simulink的电动汽车整车控制仿真

在本文中,我们选择了power_ElectricVehicle示例模型,该模型已经包含了电动汽车的基本组成部分,包括电机、变速器、驱动轮、电池等。例如,使用PID控制器来控制电机的转速,使用ABS控制器来控制车轮的制动力,使用转向控制器来控制车辆的转向角度,使用悬挂控制器来控制车轮的姿态等。通过建立模型和实现控制器,我们可以比较直观地了解电动汽车的工作原理和性能特点,并验证设计的控制系统在不同工况下的效果。而在电动汽车的开发中,整车控制系统是至关重要的一个组成部分,直接影响着电动汽车的性能和性价比。

qq_39605374的博客 1800

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

android系统动态库加载分析

最近开发的应用apk出现了运行时加载so库失败问题,所以顺带研究下Android查找和加载so库的过程. android加载动态库使用java.lang.System类(在libcore/luni/src/main/java/java/lang/System.java这个文件中),...

chunqingtai2922的博客 729

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

ZygiskFrida架构解析:突破性安卓反调试绕过技术实战指南

在安卓逆向工程领域,传统Frida工具面临的最大挑战是如何绕过日益严格的反调试检测机制。ZygiskFrida作为一项创新性技术方案,通过Zygisk注入机制实现了Frida gadget的无痕加载,从根本上解决了ptrace检测、完整性校验等安全防护措施的检测问题。这项技术不仅改变了安卓应用动态分析的游戏规则,更为安全研究人员提供了全新的逆向工程视角。 ## 技术挑战分析:安卓逆向工程的瓶颈与

gitblog_00268的博客 422

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

Android ----Ijkplayer源码阅读native层之动态库的加载和初始化(三)

在Android ----- ijkplayer源码阅读Java层(二)中欠了一篇细说加载Ijkplayer动态库时到底干了写啥???那我们就来聊聊。 VideoActivity.onCreate中调用 native_profileBegin这个是加载----性能分析工具android-ndk-profiler的动态库。这方面没用过,不说。 loadLibrariesOnce: 加载三个动态库 ffmpeg:音视频编解码的核心库 ijksdl:在native层创建一些java类对象,并存

silently_frog的博客 1337

Android Linker学习笔记

原文地址: http://drops.wooyun.org/tips/12122 0x00 知识预备 Linker是Android系统动态库so的加载器/链接器,要想轻松地理解Android linker的运行机制,我们需要先熟悉ELF的文件结构,再了解ELF文件的装入/启动,最后学习Linker的加载和启动原理。 鉴于ELF文件结构网上有很多资料,这里就不做累述了。

Omnictech的专栏 6879

java 动态库 加载_Android JAVA层加载动态库原理分析

Android JAVA层加载动态库原理分析Author : Aoyousatuo Zhaohttp://blog.sina.com.cn/aoyousatuo在Android应用的开发中我们往往需要加载一些系统的或者自己的或者第三方的动态库来提高代码的运行开发效率。在JAVA层面,Android主要提供了两种加载本地库的方式:System.loadLibrary以及System.load,两者最...

weixin_30154537的博客 376

Android OpenGL库加载过程源码分析

Android系统采用OpenGL绘制3D图形,使用skia来绘制二维图形;OpenGL源码位于: frameworks/native/opengl frameworks/base/opengl 本文简单介绍OpenGL库的加载过程。OpenGL以动态库的方式提供,因此在使用OpenGL的接口函数绘图前,需要加载OpenGL库,并得到接口函数指针。函数EGLBoolean egl_...

weixin_34409741的博客 100

Android so 加载原理分析

1.loadLibrary动态库加载过程分析 http://gityuan.com/2017/03/26/load_library/ 2.深入理解 System.loadLibrary https://pqpo.me/2017/05/31/system-loadlibrary/ 查看Android源码: https://cs.android.com/ 防止原文丢失,备份,推荐...

along 1829

基于android2.3.5系统:Android动态库链接

跟普通的linux基本一样,不过android是采用了自己的glibc,也就是在bionic这个文件中,在这个文件夹中它有libc、libdl、libm、libstdc++、libthread_db以及linker

嵌入式与人工智能天地 3345

【Android14】Android系统内置第三方应用时OpenSSL版本冲突的深度解析与解决方案优化

Android N(7.0)及更高版本对/system目录下的应用施加严格限制,强制使用AOSP原生库(如BoringSSL),禁止加载第三方自定义库。在预置应用时,将libcrypto.so 和libssl.so 重命名为libcrypts.so 和libsss.so。的解决方案,先通过脚本将APP拷贝到某一目录下,然后再开机的时候使用脚本install 进去。目录,此分区的动态库加载策略更为宽松,允许应用使用自带的OpenSSL库。),而系统自带的OpenSSL库版本较旧(如1.0.x),缺乏。

xuanyulevel6的博客 1160

VS之动态库实现和调用

程序开发中,经常会使用到动态库,那么动态库该如何实现和调用呢?本文通过一个简单的例子实现。

冰点蓝欣的专栏 2820
上一篇: 理解链接动态库:二进制的调库
下一篇: got, plt
dilvx
博客等级 码龄5年 892粉丝 73原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值