从静态库、动态库到 ELF 加载与 GOT 机制一、为什么需要库现实中每个程序都要依赖很多基础的底层库不可能每个人的代码都从零开始。库本质上是一种可执行代码的二进制形式可以被操作系统载入内存执行。Linux 下主要有两种库类型Linux 后缀Windows 后缀链接时机静态库.a.lib编译链接时动态库.so.dll程序运行时二、静态库2.1 特点静态库在编译链接时把库代码链接到可执行文件中程序运行时不再需要静态库。删除.a文件后程序照样可以运行。注意gcc 默认优先使用动态库只有找不到.so时才采用同名静态库。也可以使用-static强制静态链接。2.2 生成静态库使用arGNU 归档工具打包.o文件libmystdio.a: my_stdio.o my_string.o ar -rc $ $^ %.o: %.c gcc -c $ar的常用参数rreplace替换或添加ccreate创建t列出库中文件vverbose显示详细信息$ ar-tvlibmystdio.a rw-rw-r--1000/10002848Oct2914:352024my_stdio.o rw-rw-r--1000/10001272Oct2914:352024my_string.o2.3 使用静态库# 场景1头文件和库文件安装到系统路径gcc main.c-lmystdio# 场景2库文件在当前目录gcc main.c -L.-lmystdio# 场景3有独立路径gcc main.c -I头文件路径 -L库文件路径-lmystdio关键选项-I指定头文件搜索路径-L指定库文件搜索路径-l指定库名去掉lib前缀和.a/.so后缀三、动态库3.1 特点动态库在程序运行时才被链接多个程序可以共享同一份库代码因此可执行文件更小节省磁盘空间物理内存中一份动态库可被多个进程共用便于更新维护实现二进制级别代码复用3.2 生成动态库libmystdio.so: my_stdio.o my_string.o gcc -o $ $^ -shared %.o: %.c gcc -fPIC -c $两个关键参数-shared生成共享库格式-fPIC产生位置无关码Position Independent Code3.3 使用动态库gcc main.c -L.-lmystdio./a.out但此时直接运行可能会失败$ ldd a.out linux-vdso.so.1(0x00007fff4d396000)libmystdio.sonot found libc.so.6/lib64/libc.so.6(0x00007fa2aef30000)3.4 动态库运行搜索路径解决方法有四种拷贝到系统库路径/usr/lib、/usr/local/lib、/lib64等建立软链接到系统库路径修改环境变量LD_LIBRARY_PATH修改配置文件/etc/ld.so.conf.d/xxx.conf然后执行ldconfig重新加载# /etc/ld.so.conf.d/bit.conf/root/tools/linux# 生效ldconfig四、ELF 文件格式4.1 四种 ELF 文件类型说明可重定位文件.o文件可与其他目标文件链接可执行文件可运行的程序共享目标文件.so文件内核转储core dumps进程执行上下文4.2 ELF 的四大组成ELF 头描述文件主要特性定位其他部分程序头表列举所有有效段segments节头表包含对节sections的描述节基本组成单位如代码节、数据节等4.3 常见节Section节名作用.text机器指令代码.data已初始化的全局变量、静态变量.bss未初始化的全局变量、静态变量预留位置.rodata只读数据如字符串常量.symtab符号表记录函数名、变量名.got全局偏移表.plt过程链接表4.4 链接视图 vs 执行视图ELF 提供两种视角链接视图对应节头表粒度更细静态链接分析时关注执行视图对应程序头表告诉操作系统如何加载运行时关注为什么要把 section 合并成 segment为了减少页面碎片提高内存使用效率。例如.text为 4097 字节、.init为 512 字节若不合并占用 3 个页面每页 4KB合并后只需 2 个页面。同时相同属性的 section 合并成一个 segment 后可以实现统一的访问权限控制。五、ELF 形成与加载5.1 ELF 形成可执行文件的两个步骤编译将 C/C 源代码翻译成.o目标文件链接将多份.o文件的 section 进行合并5.2 静态链接研究静态链接本质就是研究.o如何链接。看一个例子// hello.c#includestdio.hvoidrun();intmain(){printf(hello world!\n);run();return0;}$ objdump-dhello.o 0000000000000000main:0: f3 0f 1e fa endbr644:55push %rbp... f: e8 00 00 00 00 callq14main0x14# printf 地址为 0...19: e8 00 00 00 00 callq 1emain0x1e# run 地址为 0可以看到call指令的跳转地址都被设为 0。这是因为编译hello.o时编译器完全不知道printf和run的地址。这个地址会在链接时被修正通过readelf -s code.o可以看到符号表中run是GLOBAL DEFAULT 1已定义puts是GLOBAL DEFAULT UND未定义链接时链接器根据.o文件中的重定位表找到需要重定位的函数和全局变量修正它们的地址。这个过程叫做静态重定位。链接后反汇编main.exe可以看到$ objdump-dmain.exe 000000000001149run:...1158: e8 f3 fe ff ff callq1050putsplt# 地址已被修正000000000001160main:... 116f: e8dcfe ff ff callq1050putsplt1179: e8 cb ff ff ff callq1149run# run 地址已被修正5.3 虚拟地址与进程地址空间关键问题一个 ELF 程序在没有被加载到内存时有没有地址答案有当代计算机采用平坦模式工作ELF 对自己的代码和数据进行统一编址。所以虚拟地址机制不光 OS 要支持编译器也要支持。ELF 在编译好后会把程序入口地址记录在 ELF Header 的Entry字段中。进程mm_struct、vm_area_struct在进程创建时的初始化数据就是从 ELF 各个 segment 来的——每个 segment 有自己的起始地址和长度用来初始化内核结构中的[start, end]范围数据并填充页表。六、动态链接原理6.1 动态链接的工作流程动态链接实际上将链接的整个过程推迟到了程序加载的时候。操作系统将程序的数据代码连同用到的动态库先加载到内存每个动态库的加载地址都不固定由 OS 根据地址空间使用情况动态分配库加载后修正库中的函数跳转地址6.2 程序启动时的流程C/C 程序开始执行时首先跳转到的不是main而是_start由 glibc 或 ld 提供的特殊函数设置堆栈为程序创建初始堆栈环境初始化数据段复制初始化数据、清零未初始化数据动态链接调用动态链接器解析并加载依赖的动态库进行符号解析和重定位调用__libc_start_main执行信号处理、线程库初始化等调用main函数处理返回值main返回后调用_exit终止程序6.3 动态链接器动态链接器如ld-linux.so负责在程序运行时加载动态库。它通过以下方式查找动态库环境变量LD_LIBRARY_PATH配置文件/etc/ld.so.conf及其子配置缓存文件/etc/ld.so.cache提高加载效率6.4 全局偏移表 GOT核心问题代码段是只读的但动态库加载地址不固定如何修改函数跳转地址解决方案在.data可读写中预留一片区域存放跳转地址——全局偏移表 GOTGlobal Offset Table。GOT 中每一项都是本运行模块要引用的一个全局变量或函数的地址。[24] .got PROGBITS 0000000000003fb8 00002fb8 0000000000000048 0000000000000008 WA 0 0 8关键点代码段只读不能直接修改代码段。有了 GOT代码可以被所有进程共享不同进程的地址空间中动态库的绝对地址不同所以每个进程的每个动态库都有独立的 GOT 表在单个.so中GOT 与.text的相对位置是固定的可以用相对寻址找到 GOT调用函数时首先查表然后根据表中的地址跳转这种方式实现的动态链接叫做PIC位置无关代码。所以编译动态库时必须加-fPIC参数。PIC 相对编址 GOT6.5 PLT 与延迟绑定由于动态链接在程序加载时需要对大量函数进行重定位这一步非常耗时。为了降低开销操作系统引入了延迟绑定Lazy Binding也叫PLTProcedure Linkage Table过程链接表。核心思想与其在程序一开始就对所有函数重定位不如将这个过程推迟到函数第一次被调用时。GOT 中的跳转地址默认指向一段辅助代码桩代码/stub第一次调用桩代码负责查询真正函数的跳转地址并更新 GOT 表后续调用直接跳转到动态库中真正的函数实现看一个 PLT 的例子0000000000001050 putsplt: 1050: f3 0f 1e fa endbr64 1054: f2 ff 25 75 2f 00 00 bnd jmpq *0x2f75(%rip) # 3fd0 putsGLIBC_2.2.56.6 库间依赖不仅可执行程序调用库库也会调用其他库库之间是有依赖的。库中也有.got和可执行文件一样——这就是为什么大家都是 ELF 格式的原因。解析依赖关系时就是加载并完善互相之间 GOT 表的过程。七、总结对比特性静态链接动态链接链接时机编译链接时程序加载时重定位类型编译重定位静态重定位运行重定位动态地址重定位可执行文件大小大包含库代码小只包含函数入口地址表内存占用每个进程一份多进程共享一份更新维护需重新编译整个程序只更新库文件运行速度快略慢需动态解析依赖无依赖动态库存在动态链接的代价牺牲了一定的性能和程序加载时间。动态链接的价值更高效利用磁盘空间和内存资源方便代码更新维护实现了二进制级别的代码复用。八、常用命令速查命令作用ar -rc libxxx.a *.o创建静态库ar -tv libxxx.a列出静态库内容gcc -shared -fPIC -o libxxx.so *.o创建动态库gcc main.c -L. -lxxx链接库ldd a.out查看依赖库readelf -h a.out查看 ELF 头readelf -S a.out查看节头表readelf -l a.out查看程序头表readelf -s a.out查看符号表objdump -d a.out反汇编代码段objdump -S a.out反汇编并显示源码ldconfig重新加载动态库搜索路径file hello.o查看文件类型
