程序运行之动态链接三
静态链接情况下,操作系统接着就把控制权交给可执行文件的入口地址,然后程序开始执行。但在动态链接情况下,操作系统还不能在装载完可执行文件后就把控制权交给程序。因为可执行文件依赖很多共享对象。这个时候可执行文件中对于很多外部符号的引用还处于无效地址的状态,也就是还没有跟相应的共享对象中的实际位置链接过来。所以在映射完可执行文件后,操作系统会先启动一个动态链接器。
在Linux下,动态链接器ld.so实际上是一个共享对象,操作系统通过映射的方式将它加载到进程的地址空间中。操作系统在加载动态链接器之后,就将控制权交给动态链接器的入口地址。当动态链接器得到控制权之后,它就开始自身的初始化操作,然后根据当前的环境参数,开始对可执行文件进行链接工作。当所有的动态链接都完成后,动态链接器会将控制权转交到可执行文件的入口地址,程序开始执行。
那么这个ld.so在什么位置呢。执行objdump -s program1可以看到在.interp段中有/lib64/ld-linux- x86-64.so.2
program1: 文件格式 elf64-x86-64
Contents of section .interp:
0238 2f6c6962 36342f6c 642d6c69 6e75782d /lib64/ld-linux-
0248 7838362d 36342e73 6f2e3200 x86-64.so.2
.interp段的内容其实就是保存一个字符串,这个字符串就是可执行文件所需要的动态链接器的路径。
.dynamic段
动态链接ELF中最重要的就是.dynamic段,这个段里面保存了动态链接器的所有信息,比如依赖哪些共享对象,动态链接符号表的位置,动态链接重定位表的位置,共享对象初始化代码的地址等等。结构定义在”elf.h”中
typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
}
} Elf32_Dyn
Elf32_Dyn结构由一个类型值加上 附加的数组和指针。对于不同的类型,后面附加的数值或者指针有着不同的含义。
使用readelf -d Lib.so可以查看.dynamic段的内容。
Dynamic section at offset 0xe20 contains 24 entries:
标记 类型 名称/值
0x0000000000000001 (NEEDED) 共享库:[libc.so.6]
0x000000000000000c (INIT) 0x520
0x000000000000000d (FINI) 0x690
0x0000000000000019 (INIT_ARRAY) 0x200e10
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x200e18
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x1f0
0x0000000000000005 (STRTAB) 0x368
0x0000000000000006 (SYMTAB) 0x230
0x000000000000000a (STRSZ) 163 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000003 (PLTGOT) 0x201000
0x0000000000000002 (PLTRELSZ) 48 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x4f0
0x0000000000000007 (RELA) 0x448
0x0000000000000008 (RELASZ) 168 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffe (VERNEED) 0x428
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x40c
0x000000006ffffff9 (RELACOUNT) 3
0x0000000000000000 (NULL) 0x0
另外linux还提供了ldd命令用来查看一个程序主模块或一个共享库依赖于哪些共享库:ldd pro1
linux-vdso.so.1 (0x00007fff3cef5000)
./Lib.so (0x00007fe6e267b000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe6e228a000)
/lib64/ld-linux-x86-64.so.2 (0x00007fe6e2a7f000)
动态符号表
为了完成动态链接,最关键的还是所依赖的符号和相关文件的信息。在静态链接中,有一个专门的段叫做.symtab,里面保存了所有关于该目标文件的符号的定义和引用。Pro1中也导入了foobar函数,并且提供给其他模块使用。为了表示动态链接这些模块之间的符号导入导出关系,ELF专门有一个叫做动态符号表的段用来保存这些信息。段名是.dynsym。里面只保存与动态链接相关的符号。为了加快符号的查找,往往还有辅助的符号哈希表(.hash)。我们可以用readelf工具来查看ELF文件的动态符号表及它的哈希表
readelf -sD Lib.so
Symbol table of `.gnu.hash' for image:
Num Buc: Value Size Type Bind Vis Ndx Name
7 0: 0000000000201030 0 NOTYPE GLOBAL DEFAULT 22 _edata
8 0: 0000000000201038 0 NOTYPE GLOBAL DEFAULT 23 _end
9 1: 0000000000201030 0 NOTYPE GLOBAL DEFAULT 23 __bss_start
10 1: 0000000000000520 0 FUNC GLOBAL DEFAULT 9 _init
11 2: 0000000000000690 0 FUNC GLOBAL DEFAULT 13 _fini
12 2: 000000000000065a 51 FUNC GLOBAL DEFAULT 12 foobar
动态链接重定位表:
动态链接下,无论是可执行文件还是共享对象,一旦它依赖于其他的共享对象。也是有导入的符号,那么代码或者数据中就有对导入符号的引用。动态链接的时候,导入符号的地址需要运行的时候才能确定,所以需要在运行时将这些导入符号的引用修正,也就是重定位。静态链接中,.rel.text表示代码段的重定位表, .rel.data表示数据段的重定位表。动态链接中,类似的重定位表分别是.rel.dyn和.rel.plt。相当于.rel.text和.rel.data。.rel.dyn修正的位置位于.got以及数据段,.rel.plt是对函数引用的修正,修正位置位于.got.plt。可以用readelf来查看动态链接的文件的重定位表
readelf -r Lib.so
重定位节 '.rela.dyn' at offset 0x448 contains 7 entries:
偏移量 信息 类型 符号值 符号名称 + 加数
000000200e10 000000000008 R_X86_64_RELATIVE 650
000000200e18 000000000008 R_X86_64_RELATIVE 610
000000201028 000000000008 R_X86_64_RELATIVE 201028
000000200fe0 000100000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTMClone + 0
000000200fe8 000300000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000200ff0 000400000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCloneTa + 0
000000200ff8 000600000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize@GLIBC_2.2.5 + 0
重定位节 '.rela.plt' at offset 0x4f0 contains 2 entries:
偏移量 信息 类型 符号值 符号名称 + 加数
000000201018 000200000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000201020 000500000007 R_X86_64_JUMP_SLO 0000000000000000 sleep@GLIBC_2.2.5 + 0
比如我们看printf的重定位入口,类型为R_X86_64_JUMP_SLOT,偏移为000000201018位于.got.plt中。当动态链接需要重定位的时候,先查找printf的地址,”printf”位于libc-2.6.1.so。假设链接在全局符号表里面找到printf的地址是0x01010101,那么连接器就将这个地址填入到.got.plt中的偏移为000000201018的位置中去实现地址重定位。
如果某个ELF文件是以PIC模式编译的,并调用了一个外部函数printf,则printf会出现在.rel.plt中。但是如果不以PIC模式编译,则printf将出现在.rel.dyn中。
动态链接时进程堆栈初始化信息
当操作系统把控制权交给动态链接器的时候,它将开始链接工作。但是需要知道关于可执行文件和本进程的一些信息。比如可执行文件有几个段,每个段的属性,程序的入口地址。这些信息都保存在堆栈里面,用一个结构数组来表示。
typedef struct
{
uint32_t a_type;
union
{
uint32_t a_val;
}a_un;
}Elf32_auxv_t;
字段定义如下:
在栈中的分布如下:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架