返回首页  设为首页  加入收藏  今天是:
网站首页电脑主板电脑cpu电脑内存电脑硬盘电脑显卡电脑电源显示器电脑配件电脑维修
相关文章
 探索64位linux下C++编译链接…
 谷歌首台折叠机Pixel Fold上…
 记录世间瑰奇OPPO超影像大赛…
 英特尔GPU路线图曝光:Celes…
 要求超高!《赛博朋克2077》…
 gpu - ZAKER
 克服阻力所做功的功率怎么求
 你家的电费这里教你查明明白…
 双积分政策调整:新能源积分…
 14亿人用电煤矿功不可没但太…
 特斯拉充电功率
 每GB不到五毛钱!毁灭者HD70…
 惠普m2固态硬盘1TB特惠:279…
 组装电脑必看:初次 DIY 电脑…
 GDP增速稳坐全国第一方阵——…
 电脑配置不低为什么用Creo就…
 【超薄显示屏】_超薄显示屏品…
 双十二即将来袭!这几款显示…
 显示器图片
 亏出血的CPU一哥 何时才能翻…
 办公台式电脑买整机好还是买…
 丰富接口高性能“千元级”迷…
 【硬件资讯】加速推动中!in…
 第一次购买电脑喜欢玩游戏是…
 开机显示器检测不到信号
 昂达推出629元的H610白色ITX…
 准备换13700kf求问目前b660m…
 锐龙7000系装机便宜了!微星…
 【装机帮扶站】第1040期:捡…
 时钟系统时钟同步显示屏(子…
 清华紫光电脑怎样(清华紫光电…
 TCL 华星展示最新156 英寸超…
 颠覆设计 惊现AOC全球最薄液…
 清华电脑台式
 CPU处理器价格越来越贵 笔记…
 为什么越来越多的手机厂商开…
 蓝思科技:公司目前主营业务…
 喝到断片后的事真的不记得吗…
 春秋电子:笔记本电脑结构件…
 银欣推出ALTA F2机箱:2023旗…
 台式电脑主板排行_台式电脑主…
 联想刃7000K 2022台式机主板…
 鲁大师怎么看主板型号 查看电…
 电脑硬件科普-主板
 友达70面板 27寸4K电竞显示器…
 三星玄龙骑士电竞显示器亮相…
 五一宅家指南:电竞游戏显示…
 OLED不适合当显示器?文本的…
 提供一年质保!山水24英寸显…
 CPU是如何被「瓜分」的
专题栏目
网络
您现在的位置: 电脑评测网 >> 电脑内存 >> 正文
高级搜索
探索64位linux下C++编译链接的那些事
作者:佚名 文章来源:本站原创 点击数: 更新时间:2023/5/4 11:40:38 | 【字体:

  御夫网【导读】:编译与链接对C&既熟悉又陌生,熟悉在于每份代码都要经历编译与链接过程,陌生在于大部分人并不会刻意关注编译与链接的原理。本文通过开发过程中碰到的四个典型问题来探索64位

  将如下最简单的C++程序(main.cpp)编译成可执行目标程序,实际上可以分为四个步骤:预处理、编译、汇编、链接,可以通过

  g++ main.cpp –v看到详细的过程,不过现在编译器已经把预处理和编译过程合并。

  预处理:g++ -E main.cpp -o main.ii,-E表示只进行预处理。预处理主要是处理各种宏展开;添加行号和文件标识符,为编译器产生调试信息提供便利;删除注释;保留编译器用到的编译器指令等。

  编译:g++ -S main.ii –o main.s,-S表示只编译。编译是在预处理文件基础上经过一系列词法分析、语法分析及优化后生成汇编代码。

  汇编:g++ -c main.s –o main.o。汇编是将汇编代码转化为机器可以执行的指令。

  链接:g++ main.o。链接生成可执行程序,之所以需要链接是因为我们代码不可能像main.cpp这么简单,现代软件动则成百上千万行,如果写在一个main.cpp既不利于分工合作,也无法维护,因此通常是由一堆cpp文件组成,编译器分别编译每个cpp,这些cpp里会引用别的模块中的函数或全局变量,在编译单个cpp的时候是没法知道它们的准确地址,因此在编译结束后,需要链接器将各种还没有准确地址的符号(函数、变量等)设置为正确的值,这样组装在一起就可以形成一个完整的可执行程序。

  在编译过程中最诡异的问题莫过于头文件遮挡,如下代码中main.cpp包含头文件common.h,真正想用的头文件是图中最右边那个包含name

  成员的文件(所在目录为./include),但在编译过程中中间的common.h(所在目录为./include1)抢先被发现,导致编译器报错:Test结构没有name成员,对程序员来讲,自己明明定义了name成员,居然说没有name这个成员,如果第一次碰到这种情况可能会怀疑人生。应对这种诡异的问题,我们可以用-E参数看下编译器预处理后的输出,如下图。

  预处理文件格式如下:# linenum filename flag,表示之后的内容是从文件名为filaname的文件中第linenum行展开的,flag的取值可以是1,2,3,4,可以是用空格分开的多值,1表示接下来要展开一个新文件;2表示一个文件展开完毕;3表示接下来内容来自一个系统头文件;4表示接下来的内容应该看做是extern C形式引入的。

  从展开后的输出我们可以清楚地看到Test结构确实没有定义name这个成员,并且Test这个结构是在./include1中的common.h中定义的,到此真相大白,编译器压根就没用我们定义的Test结构,而是被别的同名头文件截胡了。我们可以通过调整-I或者在头文件中带上部分路径更详细制定头文件位置来解决。

  编译链接最终会生成各种目标文件,Linux下目标文件格式为ELF(Executable Linkable Format),详细定义见/usr/include/elf.h头文件,常见的目标文件有:可重定位目标文件,也即.o结尾的目标文件,当然静态库也归为此类;可执行文件,比如默认编译出的a.out文件;共享目标文件.so;核心转储文件,也就是core dump后产出的文件。Linux文件格式可以通过file命令查看。

  一个典型的ELF文件格式如下图所示,文件有两种视角:编译视角,以section头部表为核心组织程序;运行视角,程序头部表以segment为核心组织程序。这么做主要是为了节约存储,很多细碎的section在运行时由于对齐要求会导致很大的内存浪费,运行时通常会将权限类似的section组织成segment一起加载。

  链接器会为对外部符号的引用修改为正确的被引用符号的地址,当无法为引用的外部符号找到对应的定义时,链接器会报undefined reference to XXXX的错误。另外一种情况是,找到了多个符号的定义,这种情况链接器有一套规则。在描述规则前需要了解强符号和弱符号的概念,简单讲函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。

  针对符号的多重定义链接器处理规则如下(作者在gcc 7.3.0上貌似规则2,3都按1处理):

  2. 链接器维护一个目标文件的集合E,一个未解析符号集合U,以及E中已定义的符号集合D,初始状态E、U、D都为空

  3. 对命令行上每个文件f,链接器会判断f是否是一个目标文件还是静态库,如果是目标文件,则f加入到E,f中未定义的符号加入到U中,已定义符号加入到D中,继续下一文件

  4. 如果是静态库,链接器尝试到静态库目标文件中匹配U中未定义的符号,如果m中匹配U中的一个符号,那么m就和上步中文件f一样处理,对每个成员文件都依次处理,直到U、D无变化,不包含在E中的成员文件简单丢弃

  5. 所有输入文件处理完后,如果U中还有符号,则出错,否则链接正常,输出可执行文件

  如下图所示,main.cpp依赖liba.a,liba.a又依赖libb.a,根据静态链接算法,如果用g++ main.cpp liba.a libb.a的顺序能正常链接,因为解析liba.a时未定义符号FunB会加入到上述算法的U中,然后在libb.a中找到定义,如果用g++ main.cpp libb.a liba.a的顺序编译,则无法找到FunB的定义,因为根据静态链接算法,在解析libb.a的时候U为空,所以不需要做任何解析,简单抛弃libb.a,但在解析liba.a的时候又发现FunB没有定义,导致U最终不为空,链接错误,因此在做静态链接时,需要特别注意库的顺序安排,引用别的库的静态库需要放在前面,碰到链接很多库的时候,可能需要做一些库的调整,从而使依赖关系更清晰。

  之前大部分内容都是静态链接相关,但静态链接有很多不足:不利于更新,只要有一个库有变动,都需要重新编译;不利于共享,每个可执行程序都单独保留一份,对内存和磁盘是极大的浪费。

  要生成动态链接库需要用到参数“-shared -fPIC”表示要生成位置无关PIC(Position Independent Code)的共享目标文件。对静态链接,在生成可执行目标文件时整个链接过程就完成了,但要想实现动态链接的效果,就需要把程序按照模块拆分成相对独立的部分,在程序运行时将他们链接成一个完整的程序,同时为了实现代码在不同程序间共享要保证代码是和位置无关的(因为共享目标文件在每个程序中被加载的虚拟地址都不一样,要保证它不管被加载在哪都能工作),而为了实现位置无关又依赖一个前提:数据段和代码段的距离总是保持不变。

  由于不管在内存中如何加载一个目标模块,数据段和代码段间的距离是不变的,编译器在数据段前面引入了一个全局偏移表GOT(Global Offset Table),被引用的全局变量或者函数在GOT中都有一条记录,同时编译器为GOT中每个条目生成一个重定位记录,因为数据段是可以修改的,动态链接器在加载时会重定位GOT中的每个条目,这样就实现了PIC。

  大体原理基本就这样,但具体实现时,对函数的处理和全局变量有所不同。由于大型程序函数成千上万,而程序很可能只会用到其中的一小部分,因此没必要加载的时候把所有的函数都做重定位,只有在用到的时候才对地址做修订,为此编译器引入了过程链接表PLT(Procedure Linkage Table)来实现延时绑定。PLT在代码段中,它指向了GOT中函数对应的地址,第一次调用时候,GOT存放的不是函数的实际地址,而是PLT跳转到GOT代码的后一条指令地址,这样第一次通过PLT跳转到GOT,然后通过GOT又调回到PLT的下一条指令,相当于什么也没做,紧接着PLT后面的代码会将动态链接需要的参数入栈,然后调用动态链接器修正GOT中的地址,从这以后,PLT中代码跳转到GOT的地址就是函数真正的地址,从而实现了所谓的延时绑定。

  2. 动态链接器自举通过GOT、.dynamic信息完成自身的重定位工作

  3. 装载共享目标文件:将可执行文件和链接器本身符号合并入全局符号表,依次广度优先遍历共享目标文件,它们的符号表会不断合并到全局符号表中,如果多个共享对象有相同的符号,则优先载入的共享目标文件会屏蔽掉后面的符号

  动态链接过程中最关键的第3步可以看到,当多个共享目标文件中包含一个相同的符号,那么会导致先被加载的符号占住全局符号表,后续共享目标文件中相同符号被忽略。当我们代码中没有很好的处理命名的话,会导致非常奇怪的错误,幸运的话立刻core dump,不幸的话直到程序运行很久以后才莫名其妙的core dump,甚至永远不会core dump但是结果不正确。

  有了动态链接和共享目标文件的加持,Linux提供了一种更加灵活的模块加载方式:通过提供dlopen,dlsym,dlclose,dlerror几个API,可以实现在运行的时候动态加载模块,从而实现插件的功能。

  在全面了解了动态链接相关知识后,我们来看一个静态全局变量和动态库纠结在一起引发的问题,代码如下,foo.cpp中有一个静态全局对象foo_,foo.cpp会编译成一个libfoo.a,bar.cpp依赖libfoo.a库,它本身会编译成libbar.so,main.cpp既依赖于libfoo.a又依赖libbar.so。

  运行a.out会导致double free的错误。这是由于在一个位置上调用了两次析构函数造成的。之所以会这样是因为链接的时候先链接的静态库,将foo_的符号解析为静态库中的全局变量,当动态链接libbar.so时,由于全局已经有符号foo_,因此根据全局符号介入,动态库中对foo_的引用会指向静态库中版本,导致最后在同一个对象上析构了两次。

  2. 编译时候调换库的顺序,动态库放在前面,这样全局只会有一个foo_对象

  通过四个编译链接中碰到的问题,基本把编译链接的这些事覆盖了一遍,有了这些基础,在日常工作中应对一般的编译链接问题应该可以做到游刃有余。

电脑内存录入:admin    责任编辑:admin 
  • 上一个电脑内存:

  • 下一个电脑内存: 没有了
  •  
     栏目文章
    普通电脑内存 探索64位linux下C++编译链接的那些事 (05-04)
    普通电脑内存 利润暴跌96%之后 三大巨头表态:最坏的日子很… (05-02)
    普通电脑内存 DIY海选导购 (05-02)
    普通电脑内存 微信为何占用手机的内存越来越大?网友们纷纷… (05-02)
    普通电脑内存 16GB大内存OPPO拍照手机80W闪充+120Hz+5000… (05-02)
    普通电脑内存 立功科技 (05-02)
    普通电脑内存 两年跌了96% 内存SSD不再降价 (05-01)
    普通电脑内存 三星Galaxy S23 Ultra内存多大?_三星Galaxy … (05-01)
    普通电脑内存 升级显卡和升级内存哪个可以让电脑性能提升更… (05-01)
    普通电脑内存 内存继续狂跌DDR4内存不值钱!PC现在升级换代… (05-01)
    普通电脑内存 微软电脑管家如何清理电脑内存_微软管家清理内… (05-01)
    普通电脑内存 电脑的内存容量和显存容量到底是什么意思啊?… (04-30)
    普通电脑内存 内存是什么(电脑内存一般是多少) (04-30)
    普通电脑内存 运行内存(运行内存是什么意思) (04-30)
    普通电脑内存 虚拟内存是什么_虚拟内存有什么用 (04-30)
    普通电脑内存 支持内存双通道是什么意思? (04-30)
    普通电脑内存 苹果iPhone14Pro有什么新功能 苹果Pro运行内存… (04-30)
    普通电脑内存 内存一般分为哪三种 (04-29)
    普通电脑内存 手机运行内存RAM是什么 (04-28)
    普通电脑内存 运行内存是什么意思 什么是运行内存 (04-28)