下面是我再看一遍链接过程的与 AI 的对话,对我帮助挺大的,贴上来吧

在动态链接过程中,程序通过以下机制实现外部符号的调用和重定位,涉及 .dynamic 段、PLT/GOT 表和符号解析机制:


一、外部符号调用的初始状态

1. 调用点的原始代码

当程序调用共享库中的函数(如 printf)时,编译器会生成跳转到 PLT(Procedure Linkage Table) 的指令:

; x86_64示例
call printf@plt

对应的 PLT 条目初始内容为:

printf@plt:
    jmp *GOT[1]   ; 初始时跳转到动态链接器的解析逻辑
    push index     ; 符号在重定位表中的索引
    jmp resolve    ; 跳转到动态链接器符号解析代码

2. GOT(Global Offset Table)的初始状态

  • GOT[0]:动态链接器内部数据(link_map 结构地址)。
  • GOT[1]:动态链接器的符号解析函数(_dl_runtime_resolve 地址)。
  • GOT[2..n]:外部函数的地址槽(初始指向 PLT 解析逻辑)。

二、动态链接器的重定位流程

1. 加载时重定位

动态链接器(ld.so)在程序启动时处理以下步骤:

  1. 解析 .dynamic: 获取动态链接所需的关键信息:

    • DT_NEEDED:依赖的共享库列表。
    • DT_SYMTAB:动态符号表(.dynsym)地址。
    • DT_RELA/DT_REL:重定位表(.rela.plt)地址。
    • DT_JMPREL:PLT 重定位表(通常与 .rela.plt 相同)。
    • DT_HASH/DT_GNU_HASH:符号哈希表地址。
  2. 加载依赖库: 递归加载所有依赖库到内存,构建 link_map 链表。

  3. 符号解析与重定位: 遍历重定位表(如 .rela.plt),根据符号名和哈希表查找目标地址,填充 GOT 条目。

2. 重定位表示例(.rela.plt

typedef struct {
    Elf64_Addr r_offset;  // GOT条目地址(需修改的位置)
    Elf64_Xword r_info;   // 符号索引和重定位类型(如R_X86_64_JUMP_SLOT)
    Elf64_Sxword r_addend;
} Elf64_Rela;
  • r_offset:指向 GOT 中需要修改的地址槽。
  • r_info:高 32 位为符号在 .dynsym 中的索引,低 32 位为重定位类型(如 R_X86_64_JUMP_SLOT)。

3. 符号解析过程

动态链接器通过以下步骤解析符号:

  1. 哈希查找: 使用 .hash.gnu.hash 计算符号名的哈希值,快速定位符号在 .dynsym 中的位置。

    // 伪代码(glibc/dlcache.c)
    Sym* find_symbol(const char* name) {
        uint32_t hash = elf_gnu_hash(name);
        uint32_t bucket = hash % hashtable->nbuckets;
        for (int i = hashtable->buckets[bucket]; i != 0; i = hashtable->chains[i]) {
            if (strcmp(name, &dynstr[shdr->dynsym[i].st_name]) == 0)
                return &dynsym[i];
        }
        return NULL;
    }
  2. 符号表(.dynsym)匹配: 遍历哈希链,匹配符号名和版本信息,最终获取符号的虚拟地址。

  3. 填充 GOT: 将符号地址写入重定位表指定的 GOT 条目:

    *(Elf64_Addr*)r_offset = sym->st_value;  // 修改GOT条目为实际地址

三、运行时延迟绑定(Lazy Binding)

1. 首次调用触发解析

当程序首次调用外部函数时,PLT 的初始跳转会进入动态链接器的解析逻辑:

  1. _dl_runtime_resolve: 根据 PLT 压入的符号索引,找到重定位条目,解析符号地址。
  2. 更新 GOT: 将解析后的函数地址写入 GOT,后续调用直接跳转到目标函数。

2. 性能优化

  • 哈希加速.gnu.hash 比传统 .hash 更快,减少哈希冲突。
  • PLT 延迟绑定:避免启动时解析所有符号,按需解析提升加载速度。

四、关键数据结构与工具验证

1. 查看动态段信息

readelf -d libexample.so

输出示例:

Dynamic section at offset 0x2e40 contains 24 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x0000000000000005 (STRTAB)             0x7d0
 0x0000000000000006 (SYMTAB)             0x3d0
 0x0000000000000017 (JMPREL)             0x1a20
 0x0000000000000007 (RELA)               0x1000

2. 查看重定位表

readelf -r libexample.so

输出示例:

Relocation section '.rela.plt' at offset 0x1a20 contains 3 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000200018  000300000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0

3. 调试符号解析

使用 LD_DEBUG 环境变量观察动态链接过程:

LD_DEBUG=symbols,bindings ./program

输出示例:

symbol=printf;  lookup in file=./libexample.so [0]
symbol=printf;  lookup in file=/lib/x86_64-linux-gnu/libc.so.6 [0]
binding file ./libexample.so [0] to /lib/x86_64-linux-gnu/libc.so.6 [0]: normal symbol 'printf' [GLIBC_2.2.5]

五、总结

  • 调用点原始代码:PLT 跳转到未初始化的 GOT 条目,触发动态解析。
  • 重定位时机:加载时由 ld.so 处理,或运行时首次调用时延迟绑定。
  • 符号定位依据:通过 .dynsym.hash/.gnu.hash 和重定位表协作完成。
  • 动态链接器依赖.dynamic.dynsym.rela.plt.hashld.so 解析的核心结构。

通过这一机制,程序能够高效地调用外部符号,同时支持动态库的灵活加载和更新。


我还是不明白。Elf64_Xword r_info ; 符号索引是什么时候建立的。整个过程我还是挺混乱的,有没有什么清晰一点的讲解思路


动态链接过程中的符号解析和重定位机制确实容易令人困惑,下面用分阶段、模块化的方式重新梳理整个过程,确保逻辑清晰。

一、符号重定位的核心问题

程序在编译时无法确定外部函数(如 printf)的地址,因此需要在运行时动态解析。这需要 两个关键步骤

  1. 记录哪些位置需要修正(重定位条目)。
  2. 告诉动态链接器如何修正这些位置(符号名称和规则)。

二、分阶段流程解析

阶段 1:编译源代码(生成目标文件 .o

  • 外部符号标记 编译器发现未定义的符号(如 printf),生成一个 未解决的重定位条目(Relocation Entry)。

  • 示例: 假设在 main.c 中调用 printf,编译器生成:

    ; main.o 的反汇编片段
    call 0x0  ; 占位符地址(未解析的printf)

    对应的重定位条目:

    // .rela.text 中的条目
    {
      r_offset = 0x10,  // call指令中地址的偏移(在.text中)
      r_info   = (index << 32) | R_X86_64_PLT32,  // 符号索引 + 重定位类型
      r_addend = -4
    }

阶段 2:静态链接(生成可执行文件或 .so

  • 合并重定位条目 静态链接器(如 ld)将所有 .o 文件的重定位条目合并到 .rela.plt(PLT 重定位表)中。

  • 生成符号索引 静态链接器在 .dynsym(动态符号表)中为每个外部符号分配索引,并在重定位条目中记录该索引。

    // 符号表.dynsym中的条目(假设printf是第3个符号)
    dynsym[3] = {
      st_name  = 0x20,  // 在.dynstr中的偏移,指向字符串"printf"
      st_value = 0x0,   // 初始地址(运行时由动态链接器填充)
      st_info  = (STB_GLOBAL << 4) | STT_FUNC
    };
    // .rela.plt中的重定位条目
    {
      r_offset = 0x2000,  // GOT中的槽位地址
      r_info   = (3 << 32) | R_X86_64_JUMP_SLOT,  // 符号索引=3
      r_addend = 0
    }

阶段 3:动态链接器(ld.so)加载程序

  • 加载依赖库 ld.so 读取 .dynamic 段,加载所有依赖的共享库(如 libc.so.6)。
  • 构建符号全局视图 通过 .dynsym.hash/.gnu.hash 和依赖库的符号表,建立所有符号的全局哈希表。

阶段 4:符号解析与重定位

  1. 遍历重定位表(.rela.plt 对于每个条目(如 r_info 中符号索引=3):

    • 查找符号名:通过 .dynsym[3].st_name 找到 .dynstr 中的字符串 "printf"

    • 哈希查找符号:用 .gnu.hash 快速定位 libc.so.6 中的 printf 函数地址。

    • 修正 GOT:将地址写入 r_offset 指定的位置(如 GOT[2])。

      *(uint64_t*)0x2000 = 0x7f8a5d400520;  // printf在libc中的地址
  2. 延迟绑定(Lazy Binding) 若启用延迟绑定,首次调用 printf@plt 时触发解析:

    printf@plt:
        jmp *GOT[2]        ; 第一次跳转到解析逻辑
        push 3             ; 符号索引=3
        jmp _dl_runtime_resolve

三、关键数据结构与工具验证

1. 查看符号索引

readelf -s libexample.so | grep printf

输出示例:

Num:    Value          Size Type    Bind   Vis      Ndx Name
 3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5
  • 符号索引:此处 printf 的索引为 3。

2. 查看重定位表

readelf -r libexample.so

输出示例:

Relocation section '.rela.plt' at offset 0x1a20:
  Offset          Info           Type           Sym. Value  Sym. Name + Addend
000000200018  000300000007 R_X86_64_JUMP_SLO 0000000000000000 printf + 0
  • Info 字段000300000007 → 符号索引=3(高 32 位),类型=7(R_X86_64_JUMP_SLOT)。

3. 调试符号解析

LD_DEBUG=symbols,bindings ./program

输出示例:

symbol=printf;  lookup in file=./libexample.so [0]
symbol=printf;  lookup in file=/lib/x86_64-linux-gnu/libc.so.6 [0]
binding file ./libexample.so [0] to /lib/x86_64-linux-gnu/libc.so.6 [0]: normal symbol 'printf'

四、总结:符号索引与重定位流程

阶段操作生成的关键数据
编译生成未解析的重定位条目,标记外部符号的调用位置。.rela.text 中的条目(符号索引占位)
静态链接合并重定位条目到 .rela.plt,分配符号索引。.dynsym.rela.plt.gnu.hash
动态链接根据符号索引查找符号名,解析实际地址,填充 GOT。修正后的 GOT 条目(指向真实函数地址)
  • 符号索引的建立:静态链接时确定,存储在 .rela.pltr_info 字段。
  • 动态链接器的角色:利用 .dynsym.hash 等 Section 解析符号,修正地址。

通过这一流程,程序在运行时能动态绑定外部符号,无需在编译时硬编码地址。


编译阶段我理解了,就是把 call 一个未定义函数的位置记下来,类型什么的都记下来。但是这里函数的“符号”放在哪里?我想是有一个符号表,前面那个结构里存了符号表的偏移,这样就解释的通了。对,这个符号表是 .dynsym,同时搞定了那种外部定义,自己声明的情况;也可以表示对外定义的情况。

然后是静态链接阶段。 把前面的结构统一放在.rela.plt 中这个可以。然后符号表也统一放在一起,偏移什么的都容易调整。

然后是动态链接阶段。这里的意思是如果采用延迟绑定的策略,就在静态链接的地方先把 call 的地方都换成 call 这个 printf@plt: 之类的东西是吧,然后在第一次有调用外部链接的函数的时候开始整个符号解析过程。因为 call 已经有一个地址了,这里说是 printf@plt 第一个指令转到 GOT[2] 里的位置,好,GOT[2] 指向的是 PLT 解析逻辑,所以这里就开始遍历整个重定位表是吧?哦我懂了,这里不去修改.text 里的东西,而是去修改 printf@plt 里面指向的 GOT[2] 里的东西,所以 call 的东西在静态链接的时候替换了之后就不变了是吧。(其实也是文件偏移,不过刚好偏移到 got 表里了而已)遍历整个重定位表,重定位表里有 got 的槽位,符号在符号表里的索引,符号表里的存的又是一个符号的字符串,真正的函数地址还不知道在哪。这个时候怎么建立从字符串到函数地址这条路呢?前面漏掉了导出函数的工作吧。应该有一个从字符串哈希到一个数,从数映射到一个地址的工作,然后动态链接的时候把这些集合起来?是涉及到哈希表的合并了吗?并没有,动态链接器为每个库维护独立的哈希表,查找时按依赖顺序遍历所有库的哈希表,直到找到符号。

分步详解动态链接过程


一、符号存储与符号表(.dynsym

1. 符号的存储位置
  • 符号表 .dynsym: 动态链接所需的符号(如外部函数名 printf)存储在 .dynsym(动态符号表)中。每个条目包含符号名在 .dynstr 中的偏移、类型、绑定属性等。

    typedef struct {
        Elf64_Word    st_name;  // 符号名在.dynstr中的偏移
        unsigned char st_info;  // 符号类型(如函数)和绑定属性(如全局)
        // ... 其他字段(如大小、地址等)
    } Elf64_Sym;
  • .dynstr: 字符串表,存储符号名的实际字符串(如 "printf")。

2. 符号类型与绑定属性
  • st_info 字段: 由 符号绑定类型(高 4 位)和 符号类型(低 4 位)组成:

    • 绑定类型STB_GLOBAL(全局符号)、STB_LOCAL(局部符号)、STB_WEAK(弱符号)。
    • 符号类型STT_FUNC(函数)、STT_OBJECT(全局变量)、STT_NOTYPE(未定义类型)。

    示例:(STB_GLOBAL << 4) | STT_FUNC 表示一个全局函数符号。

3. 示例验证

查看符号表:

readelf -s libexample.so

输出示例:

Symbol table '.dynsym' contains 5 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5
  • printf.dynsym 中的第 3 个符号,类型为 FUNC,绑定属性为 GLOBAL

二、静态链接阶段:重定位表(.rela.plt)与 GOT/PLT 机制

1. 重定位表的真实作用
  • .rela.plt 的作用: 记录 GOT 条目需要被修正的位置,而非直接修正 call 指令的地址。
    • r_offset:指向 GOT 中的槽位(而非 call 指令的地址)。
    • 原因:直接修改 call 指令需要写代码段(r-x 权限),而修改 GOT 只需写数据段(rw- 权限),更安全高效。
2. GOT 与 PLT 的分工
  • GOT(Global Offset Table): 存储外部函数的 实际地址(动态链接后填充)。
  • PLT(Procedure Linkage Table): 提供跳转到 GOT 的 跳板代码,首次调用时触发动态解析。
3. 示例验证

查看重定位表和 GOT:

readelf -r libexample.so

输出示例:

Relocation section '.rela.plt' at offset 0x1a20 contains 3 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000200018  000300000007 R_X86_64_JUMP_SLO 0000000000000000 printf + 0
  • r_offset = 0x200018:GOT 中 printf 的槽位地址。
  • r_info:符号索引=3,类型=R_X86_64_JUMP_SLOT(PLT 重定位)。

三、动态链接阶段:符号解析与延迟绑定

1. 延迟绑定(Lazy Binding)
  • 首次调用触发解析: 程序首次调用外部函数时,PLT 代码跳转到动态链接器的解析逻辑。

    printf@plt:
        jmp *GOT[2]        ; 第一次跳转指向解析逻辑
        push 3              ; 符号索引=3
        jmp _dl_runtime_resolve
  • 解析后填充 GOT: 动态链接器解析符号地址后,将结果写入 GOT。后续调用直接跳转到目标函数。

2. 符号解析的核心步骤
  1. 符号名 → 符号索引: 通过 .rela.plt 中的条目获取符号索引(如 3)。
  2. 符号索引 → 符号名: 通过 .dynsym[3].st_name 找到 .dynstr 中的字符串 "printf"
  3. 符号名 → 地址: 动态链接器通过 哈希表(.gnu.hash 在所有依赖库中查找 "printf" 的地址。
3. 哈希表的合并与查找
  • 哈希表的作用: 避免遍历所有符号,通过哈希值快速定位符号所在的哈希桶(Bucket)。

  • .gnu.hash 结构

    struct gnu_hash {
        uint32_t nbuckets;     // 哈希桶数量
        uint32_t symoffset;    // 符号表起始索引
        uint32_t bloom_size;   // 布隆过滤器大小
        uint32_t bloom_shift;  // 布隆过滤器位移
        // 布隆过滤器数组、哈希桶数组、哈希链数组
    };
  • 查找步骤

    1. 计算符号名的哈希值。
    2. 使用布隆过滤器快速排除不存在的符号。
    3. 遍历哈希桶中的链,匹配符号名。
4. 示例验证

查看哈希表:

readelf -S libexample.so | grep .gnu.hash

输出示例:

  [ 5] .gnu.hash         GNU_HASH         00000000000001b8

四、总结与补充

1. 完整流程
  1. 编译阶段: 记录外部符号的调用位置,生成未解决的重定位条目。
  2. 静态链接阶段: 合并重定位条目到 .rela.plt,设置符号索引,生成 PLT/GOT。
  3. 动态链接阶段
    • 加载依赖库,构建全局符号视图。
    • 按需(首次调用)或立即解析符号,修正 GOT 条目。
2. 关键设计思想
  • PLT/GOT 解耦: 将代码段(不可写)和数据段(可写)分离,保证安全性和灵活性。
  • 哈希加速: 通过 .gnu.hash 和布隆过滤器大幅提升符号查找效率。
3. 工具验证命令
# 查看符号表
readelf -s libexample.so
 
# 查看重定位表
readelf -r libexample.so
 
# 查看哈希表
readelf -S libexample.so | grep .gnu.hash
 
# 调试动态链接过程
LD_DEBUG=symbols,bindings ./program

五、答疑

  • 为什么 r_offset 指向 GOT 而非 call 指令? 直接修改代码段需要 r-xrwx 权限切换,存在安全风险且效率低。修改 GOT 只需操作数据段,更安全高效。

  • 哈希表如何合并多个库的符号? 动态链接器为每个库维护独立的哈希表,查找时按依赖顺序遍历所有库的哈希表,直到找到符号。

  • 延迟绑定的代价是什么? 首次调用会有解析开销,但避免了启动时解析所有符号,适合大型程序。可通过 LD_BIND_NOW=1 强制立即绑定。