ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

【Linux指南】动静态库系列(七):符号表与重定位:链接器如何把函数调用接起来

【Linux指南】动静态库系列(七):符号表与重定位:链接器如何把函数调用接起来 文章目录一、先看一个跨文件调用二、什么是符号三、用 nm 查看符号四、用 readelf -s 查看符号表五、undefined reference 的本质六、什么是重定位七、查看重定位表八、用 objdump 观察代码里的占位九、静态链接到底做了什么十、静态库为什么更新后程序必须重新链接十一、静态库链接顺序问题十二、符号表、重定位和库的关系十三、常用命令总结十四、总结上一篇我们认识了 ELF知道.o、.so、可执行文件都不是普通文件而是带有结构的二进制文件。ELF 中有代码、有数据、有 section、有 segment还有一个对链接非常重要的东西符号表。这篇文章要解决的问题是当main.c调用code.c里的run函数时编译器单独编译main.c时明明看不到run的实现为什么最后程序还能正确跳转到run答案就在符号解析和重定位中。一、先看一个跨文件调用准备两个文件。hello.c#includestdio.hexternvoidrun(void);intmain(){printf(hello\n);run();return0;}code.c#includestdio.hvoidrun(void){printf(run\n);}单独编译gcc-chello.c-ohello.o gcc-ccode.c-ocode.o此时hello.o中调用了run但run的实现并不在hello.o里。问题来了hello.o 如何知道 run 最终在哪里答案是它暂时不知道。它只是留下一个“未解决的符号引用”等链接阶段再解决。二、什么是符号在链接视角下函数名和全局变量名都可以看作符号。例如voidrun(void){}intg_val10;这里的run和g_val都是符号。符号大致可以分为两类定义符号当前文件提供了实现或实体 未定义符号当前文件用到了它但实现不在当前文件在上面的例子中hello.o 中run 是未定义符号 code.o 中run 是已定义符号链接器的工作之一就是把这些引用和定义匹配起来。三、用 nm 查看符号nm可以查看目标文件中的符号。查看hello.onm hello.o可看到0000000000000000 T main U printf U run这里的关键是T main U runT表示符号定义在代码段.text中说明main在hello.o里有实现。U表示 undefined说明run在hello.o中只是被引用还没有定义。再看code.onm code.o可能看到0000000000000000 T run U printf这里run是T说明code.o提供了run的函数实现。链接器看到hello.o 需要 run code.o 提供 run于是就可以把它们连接起来。四、用 readelf -s 查看符号表readelf -s也可以查看符号表readelf-shello.o你会看到符号的更多信息例如Num: Value Size Type Bind Vis Ndx Name ... FUNC GLOBAL DEFAULT UND run FUNC GLOBAL DEFAULT ... main其中UND表示 undefined也就是未定义符号。符号表告诉链接器这个目标文件定义了哪些符号 引用了哪些外部符号 每个符号大致属于什么类型 符号将来需要如何参与链接。五、undefined reference 的本质如果只链接hello.ogcc hello.o-omain会报错undefined reference to run这不是编译错误而是链接错误。因为编译hello.c时编译器只需要知道externvoidrun(void);它就可以相信run这个函数将来会存在。但链接时链接器必须真的找到run的实现。如果所有输入文件和库里都没有run链接器就无法生成最终可执行程序。所以undefined reference 链接器找不到某个符号的定义常见原因忘记链接某个.o文件。忘记写-lxxx。-L路径不对库没找到。库里根本没有这个函数实现。C 名字修饰导致符号名不匹配。六、什么是重定位符号解析解决了“函数在哪里定义”的问题但还不够。链接器还要解决一个更具体的问题调用指令中的地址应该填多少编译hello.c时编译器看到run();但它不知道run最终在可执行程序中的地址。于是它只能先留下一个需要修正的位置并记录到重定位表里。等链接器把hello.o、code.o合并成最终程序时它就知道run的最终地址了于是回头修正调用位置。这个过程就是重定位。通俗理解编译阶段先留坑 链接阶段填地址七、查看重定位表可以使用readelf-rhello.o你可能看到类似Relocation section .rela.text Offset Info Type Sym. Name ... ... ... ... run ... ... ... printf这说明hello.o的.text代码段中有一些位置需要在链接时被修正。重定位表记录的信息大致包括哪里需要修正 修正和哪个符号有关 用什么方式修正链接器根据这些信息把未确定的地址修正为最终地址。八、用 objdump 观察代码里的占位可以反汇编目标文件objdump-dhello.o你会看到call指令但目标地址可能还不是最终地址。因为在.o阶段run的最终位置还没确定。链接后再反汇编最终程序gcc hello.o code.o-omain objdump-dmain这时call run的地址就已经被链接器修正好了。这就是静态链接中非常核心的一步。九、静态链接到底做了什么现在我们可以重新理解静态链接。静态链接不是简单地把文件拼接到一起而是包含多个工作收集所有输入目标文件。扫描符号表。匹配未定义符号和已定义符号。合并同类 section例如.text、.data。分配最终地址。根据重定位表修正代码和数据中的地址。生成最终可执行文件。如果涉及静态库.a链接器还会从库中抽取需要的目标文件参与链接。所以静态库中的 .o本质上也会参与上述链接过程。十、静态库为什么更新后程序必须重新链接假设你用libmyc.a生成了程序main。静态链接时库中相关代码已经进入main。后来你修改my_string.c重新生成新的libmyc.a但不重新链接main。此时运行旧main行为不会变化。原因是旧 main 里已经包含旧版本库代码 新 libmyc.a 不会自动影响旧 main。只有重新链接gcc main.c -I./include -L./lib-lmyc-omain新库实现才会进入新的可执行程序。十一、静态库链接顺序问题在某些情况下静态库链接顺序也会影响结果。一般建议把依赖库放在使用它的目标文件后面gcc main.o -L.-lmyc-omain而不要写成gcc -L.-lmycmain.o-omain原因是传统链接器通常从左到右扫描输入。当它扫描到库时如果前面还没有未解决的符号引用可能不会从库中抽取目标文件。动态库场景下这个问题不一定表现得同样明显但学习静态链接时应该养成正确顺序习惯。十二、符号表、重定位和库的关系现在把它们串起来.c 源文件 - 编译成 .o - .o 中包含符号表和重定位表 - 多个 .o 或静态库参与链接 - 链接器解析符号并修正地址 - 生成可执行文件静态库.a只是把多个.o管理起来。真正起作用的仍然是里面.o的符号表和重定位信息。动态库.so的符号解析和重定位更复杂因为很多工作会推迟到加载和运行阶段。后面讲动态链接时会继续深入。十三、常用命令总结查看符号nm hello.o readelf-shello.o查看重定位信息readelf-rhello.o查看反汇编objdump-dhello.o objdump-dmain链接目标文件gcc hello.o code.o-omain查看未定义符号nm hello.o|grep U 十四、总结链接器最核心的工作可以概括为两件事符号解析找到每个外部符号的定义 重定位把代码和数据中暂时未知的地址修正为最终地址这也是undefined reference、静态库更新必须重新链接、链接顺序可能影响结果的根本原因。下一篇我们从链接继续走向加载一个 ELF 可执行文件还没运行时为什么里面已经记录了地址操作系统又是如何根据 ELF 的 segment 初始化进程地址空间的
返回列表