尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

深入浅出理解计算机核心知识系列【C++语言特性合集-编译与链接篇】

深入浅出理解计算机核心知识系列【C++语言特性合集-编译与链接篇】 本人志在持续更新计算机系统、计算机网络、C语言的核心知识点的系列合集以易懂、全面的方式讲解底层知识。对于正在准备面试八股的朋友来说本系列涵盖了本人面试中遇到的所有考点以及许多相关拓展知识读完后能帮助你从容面对大部分面试拷打对于想要深入学习计算机知识的朋友来说本系列比较系统地介绍了操作系统和网络等重点内容也举了不少例子大大有助于你从底层的视角去理解计算机系统。先说明本系列恐怕不是计算机小白或是想速通期末的朋友们的目标它需要一定系统和语言基础也并不是面向教材和考试要求去讲解所以更适合那些实操过代码、了解一些计算机系统知识、并且想要深入底层和扎实基础的朋友们去耐心学习。如果你是这样的人欢迎阅读该系列文章并分享自己的理解或提出文章中的模糊、错误的地方不排除有。想要阅读系列中其他内容或想要持续关注本系列更新可移步https://github.com/feiyangyang11/Cpp-Core-CS-Interview-Guide.git。C 编译编译主流程预处理处理#include、#define宏展开、条件编译#ifdef、去掉注释生成.i文件编译把预处理后的代码做词法分析、语法分析、语义分析生成中间表示再做优化最终生成汇编代码.s汇编把汇编代码转成机器码目标文件.o这一步基本是一一对应链接把多个目标文件和库合并成一个可执行文件或动态库。预处理本质是文本替换没有太多值得分析的地方理解编译与汇编要先学习编译原理中的词法分析、汇编语言等知识且更多属于编译器和其他底层开发者的工作在此不再深入讲述而链接与我们编写 C 代码的方式息息相关更值得我们广大应用层开发者去研究所以本文讲解将聚焦在链接这一过程当然也有部分原因是编译原理这门课实在晦涩难懂本人学的不好于是没法在此很好地解释清楚链接目的是把多个目标文件.o和库文件里的代码/数据拼成一个最终可执行文件或共享库核心机制符号解析Symbol Resolution、重定位Relocation这里举一个简单的例子后面的分析都是基于这个例子讲解并深入分析假设有两个文件 main.cpp、math.cpp// main.cppexternintg;//全局变量声明intadd(int,int);//函数声明intmain(){returnadd(g,10);}// math.cppintg5;intadd(inta,intb){returnab;}到链接这一步前这两个文件会各自生成main.o和math.o文件此时 main 中虽然知道有 g 这个变量和 add 这个函数但并不知道它们的实际地址在哪只能在此留下重定位项。而链接要解决的就是建立重定位项和实际定义之间的关系.o文件是什么.text 和 .data 等都属于进程虚拟地址逻辑分区详解参考操作系统合集-进程篇.o 是 relocatable file即可被重定位的文件。它包含很多 section大致结构如下main.o.text//本文件中定义的机器指令.data//本文件中定义的已初始化全局/静态变量.bss//本文件中定义的未初始化或零初始化全局/静态变量.rodata//本文件中定义的常量字符串等.symtab//本文件自带的符号表.rela.text//重定位表定位定义在其他代码文件中的机器指令.rela.data//重定位表定位定义在其他代码文件中的已初始化全局/静态变量...符号与符号表在进入到链接之前代码文件中g、add()这类变量 / 函数会被编译成符号g、add每个符号在符号表中有自己对应的表项表项中保存符号名类型属于哪个 section在 section 内的偏移量全局/局部是否已定义对于g变量//在 math.o 中可能是Symbol:g Section:.data Offset:0Size:4Global:yes//在 main.o 中可能是Symbol:g Section:UND//undefined当前 .o 只知道这个名字但不知道定义在哪链接器要去其他 .o 或库里找//因为只声明 extern int g;重定位表生成main.o时add()的最终地址还不知道于是该文件的.text机器码里只能暂时留一个占位值。同时.rela.text记录此处是占位值后续需要重定位重定位表只保存需要重定位的符号信息每个表项大概长这样structElf64_Rela{Elf64_Addr r_offset;// 在机器码中要修改的位置Elf64_Xword r_info;// 在本地符号表中的编号 重定位规则Elf64_Sxword r_addend;// 附加常数};r_offset表示要重定位的符号add()在机器码中的地址相对本文件 .text 的偏移量r_info保存待重定位符号在本地符号表中的编号symbol index。还保存重定位规则比如决定符号地址究竟如何计算——如果此处的机器码是call add(...)那么应该填入add实际地址相对于当前指令的地址偏移量如果此处代码类似long* p g;那么应该填入g的绝对地址r_addend是重定位计算中的附加常数用于表达符号地址的额外偏移或者适配具体机器指令的寻址规则。以 x86-64 的call rel32为例链接器最终需要在call的 4 字节位移字段中填入disp。设目标符号add的最终地址为S该位移字段的地址为P附加值为A则对应重定位可表示为disp S A - P。由于 CPU 执行call rel32时使用的是“下一条指令地址 disp”而P指向 4 字节位移字段的起始位置下一条指令地址为P 4因此需要令A -4使得disp S - (P 4)最终 CPU 恰好跳转到地址 S。用简单的话讲r_offset决定哪个地方原来填的是临时值需要重定向r_info决定要重定向的是哪个符号、要算相对地址还是绝对地址r_addend为算重定向的符号值时提供一个修正量强符号和弱符号符号有两种属性强和弱如果一个 UND 的符号引用链接时在其他文件符号表中匹配到多个同名符号定义遵循如下规则强 强冲突报错multiple definition of g重复定义强 弱选择强符号的定义弱 弱选其中一个符号的定义强符号用户代码定义的函数 / 变量弱符号显式声明为 weak或者某些编译器/链接器机制生成的可被强符号覆盖的符号链接步骤1.符号解析链接器将符号引用匹配到符号定义比如//两个文件的符号表情况main.o add → undefined g → undefined math.o add → defined g → defined//链接匹配得到main.o:add-math.o:add main.o:g-math.o:g如果未匹配到符号的定义就会报链接错误如undefined reference to add()如果出现强符号冲突就会报重定义错误如multiple definition of g链接器顺序读取每个 .o处理每个 .o 的符号表时候会把遍历到的需要跨文件解析的符号加入到全局符号表避免每遇到一个 UND 都扫描其他所有.o文件如果读到 UND 符号会检查全局符号表是否已有此符号的匹配关系有则直接利用否则加入全局符号表并标记为unresolved如果读到符号定义会检查全局符号表中是否已有此符号的匹配关系有则根据符号强弱关系决定是否更新否则在全局符号表中新添对应的g → math.o::g这样一个映射表项而对于 .o 文件自己的符号表它不会在全局符号表中匹配成功时改掉本地符号的 UND而是在本地另建立一套区别于本地符号表的上下文在其中添加本地符号 - 全局符号表表项 - 符号定义的一条路径注这里指的本地符号表不是指存放局部变量 / 函数符号的表局部变量不会参与跨文件解析而是管理本文件所有符号的表2.section 合并和地址布局进入链接之前每个 .o 文件中各自都分好了 section如main.o .text 100B .data 20B math.o .text 80B .data 16B链接器在这一步会把它们合起来变成//最终可执行文件.text ┌─────────────────┐ │ main.o.text │ │ math.o.text │ └─────────────────┘.data ┌─────────────────┐ │ main.o.data │ │ math.o.data │ └─────────────────┘//然后进行内存对齐、决定最终虚拟地址.text →0x401000.data →0x404000//直到这一步符号终于有了最终地址main 函数 →0x401000add 函数 →0x401080g →0x404010//这些结果会写入全局符号表 entry 里更新这个全局符号的最终 value3.重定位在编译main.o时由于add()地址未知所以会在自己的.text中先放一个占位值进入该阶段后链接器遍历每个 .o 文件的重定位表把要重定位的符号一个个重定位把原来写在机器码里的占位值替换成真实的地址值可能是偏移地址也可能是绝对地址根据重定位表表项配置决定详细计算方式在介绍重定位表中有讲解以某个符号的重定向为例如.rela.text记录r_offset:0x20r_info:r_Symbol:add//符号在本文件符号表中的编号r_Type:R_X86_64_PLT32 r_addend:...//遍历到上面这一项时这样计算//通过 r_Symbol - 到本文件符号表找到符号名 - 到全局符号表中找到符号真实地址 - 根据 r_Type、r_addend 计算得到最终地址这一步后机器码已经成为完全体(⊙﹏⊙)静态库与静态链接静态库.a本质上是很多.o文件打包成的归档文件。静态链接时链接器只从.a中按需加载.o文件再进行链接可以理解为静态库.a就是一坨还没有经历过符号解析、section 合并和重定位的.o文件以及一张符号索引表的集合符号索引表静态库中有一张符号索引表准确说是一套体系它由三部分组成符号个数 N数组 AN 个整型保存该符号所在的.o文件头在整个.a文件中的字节偏移数组 BN 个以\0为结尾的字符串即符号名成员顺序和数组 A 一一对应这张表用来做什么主要是为了避免加载整个静态库做到按需加载当外部要链接该静态库时会先做符号解析。查到 UND 符号时如果想要搜索静态库直接遍历数组 B 比对符号名。如果查到对应符号就找到数组 A 中相同索引的元素获取.o文件的地址将它纳入本次链接即所谓按需加载静态库如何组成的静态库就是先把一堆.o打包在一起然后遍历到所有可跨文件访问的符号建立符号索引表组成了.a文件静态库链接顺序很重要# -lxxx 表示静态链接 xxx.agcc-lmathmain.o gcc main.o-lmath这两种写法可能造成不一样的链接结果因为链接器通常从左到右处理输入。如果解析main.o中标记add为 UND然后在解析math.a时发现add可以在符号索引表中查到那么就会将对应.o文件从math.a中抽出并参与链接如果链接顺序反过来那么main.o就找不到add的定义了会报错所以链接顺序要做到依赖别人的对象放前面被依赖的静态库放后面静态库循环依赖链接了两个静态库 libA、libB彼此都有 UND 符号在对方之中——解析 libA 时产生B1 UND然后去 libB 中查出 B1又产生 A2 UND但 libA 已经过去拥有 A2 的.o没有被链接导致 A2 链接失败解决方法让链接器反复扫描这一组静态库直到没有新的.o需要抽取更建议的做法是减少这种循环依赖写法动态库与动态链接动态库.soWindows 上是.dll和静态库最本质的区别是静态链接把代码复制进可执行文件动态链接则是运行时才把代码加载进来、让多个程序共享同一份正因为不是链接时一次性拷贝动态链接要额外解决两个静态链接没有的问题代码加载到内存的哪个地址是运行时才决定的编译期不知道所以机器码里那些引用全局变量、调用函数的地址没法在链接时填死函数什么时候才真正解析出地址如果程序启动就全部解析启动会很慢下面按「地址怎么解决」和「解析时机怎么优化」两条线讲1.位置无关代码PIC静态链接时每个符号的最终地址在链接阶段就定了所以机器码里可以直接写绝对地址。但动态库是共享的可能被加载到任意地址如果代码里写死了绝对地址一换加载地址就全错解决办法是编译动态库时加-fPIC生成位置无关代码代码段里不出现绝对地址凡是引用全局变量、调用外部函数的地方都改成「先查表、再跳转」这个表就是下面要讲的 GOT。2.GOT 和 PLTGOT全局偏移表可以理解成一张「地址表」每一项存一个符号的真实地址。代码引用全局变量g时不直接写g的地址而是去 GOT 里查g那一项拿到地址再访问。这样只要运行时把 GOT 表项填对代码段本身就不用改天然支持被加载到任意位置。PLT过程链接表用来配合「延迟绑定」实现用到才解析。程序启动时不解析所有函数的地址而是给每个外部函数在 PLT 里留一个桩stub。第一次调用add()时跳到 PLT 里的addplt桩桩先去 GOT 查发现还没解析过就调用动态链接器去把add的真实地址找出来填回 GOT再跳到真正的add执行之后第二次再调用add()GOT 里已经有地址了直接跳就行不再走动态链接器。这就是「延迟绑定」好处是没被用到的函数根本不会去解析启动更快3.动态链接器的工作流程.so里同样有自己的符号表和重定位表。当程序启动时内核加载可执行文件后会先交给动态链接器ld.so它做这么几件事加载程序依赖的所有.so进内存记录在可执行文件的动态段里做符号解析把可执行文件和各个.so里的 UND 符号在彼此之间匹配上定义做重定位因为编译动态库时地址未知所以重定位表里留的项要在这个阶段真正填值——结合 GOT/PLT把符号真实地址写进 GOT 表项这一步做完程序才开始执行main
返回列表