编程高手必学的内存知识
海纳
华为编译器高级专家,原 Huawei JDK 团队负责人
20674 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 33 讲
编程高手必学的内存知识
15
15
1.0x
00:00/00:00
登录|注册

07 | 动态链接(上):地址无关代码是如何生成的?

你好,我是海纳。
通过上节课的学习,我们了解到,链接器可以将不同的编译单元所生成的中间文件组合在一起,并且可以为各个编译单元中的变量和函数分配地址,然后将分配好的地址传给引用者。这个过程就是静态链接。
静态链接可以让开发者进行模块化的开发,大大的促进了程序开发的效率。但同时静态链接仍然存在一个比较大的问题,就是无法共享。例如程序 A 与程序 B 都需要调用函数 foo,在采用静态链接的情况下,只能分别将 foo 函数链接到 A 的二进制文件和 B 的二进制文件中,这样导致系统同时运行 A 和 B 两个进程的时候,内存中会装载两份 foo 的代码。那么如何消除这种浪费呢,这就是我们接下来两节课的主题:动态链接。
动态链接的重定位发生在加载期间或者运行期间,这节课我们将重点分析加载期间的重定位,它的实现依赖于地址无关代码。我们知道,深入地掌握动态链接库是开发底层基础设施必备的技能之一,如果你想要透彻地理解动态链接机制,就必须掌握地址无关代码技术。
在你掌握了地址无关代码技术后,你还将对程序员眼中的“风骚”操作,比如,如何通过重载动态库对系统进行热更新,如何对动态库里的函数进行 hook 操作,以便于调试和追踪问题等等,都会有更深入的理解。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

动态链接技术是解决静态链接无法共享代码的重要方法。通过将公共的库函数抽离出来,形成共享模块和私有模块,实现多个进程共享公共函数的目的。动态链接库文件在系统中只会被加载到内存中一次,无论有多少个进程使用它,这种文件在内存中只有一个副本。动态链接的实现依赖于地址无关代码技术,因为不同进程的内存地址不同,共享模块的代码必须是地址无关的。文章还介绍了LIBRARY_PATH和LD_LIBRARY_PATH两个环境变量的作用和区别,以及动态链接器的使用时机。通过本文的介绍,读者可以初步了解动态链接技术的原理和实现过程,以及动态链接技术如何节省内存。整体而言,本文深入浅出地介绍了动态链接技术的原理和实现细节,对读者快速了解动态链接技术具有重要参考价值。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《编程高手必学的内存知识》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(18)

  • 最新
  • 精选
  • keepgoing
    老师我在尝试把这节课的知识和上节课联系起来,梳理出几个小问题想请教一下: 1. 一个程序编译不管存不存在动态库一起编译,都会进行一遍编译->静态链接,我理解前两个阶段是运行时无关的,但如果存在动态库,静态编译时的动态库符号是怎么进行区分的呢,区分之后也是跟链接之前的符号一样先用0标记存在重定位表里吗? 2.老师在动态链接这一节举的动态库编译的例子讲到动态库编译期间本身也会生成GOT段,来解决本动态库依赖其他动态库中符号的固定地址偏移问题,但在动态库编译上一个例子执行进程A、B中举例道:A进程加载的libc在0x2000的位置,进程GOT表在0x5000的位置,B进程的libc在0x5000的位置,GOT表在0x8000的位置,所以运行无问题,这就产生了一些歧义我没理解透: - 地址无关技术到底是解决的动态库依赖动态库地址无法固定的问题;还是因为不同进程调用同一个A动态库中的方法,如果该方法依赖其他动态库B中的方法,动态库方法A和依赖方法B间的偏移在不同进程中必须固定? - 动态库中的GOT表信息会在和程序执行的时候一起被合并到进程的GOT表中吗? - 如果合并进入进程的GOT表里了,那么在编译动态库时将符号与GOT表作偏移这一步还有意义吗? 抱歉问题比较多,因为自己确实对这块很感兴趣,一直没了解清楚过,如果问题有冒犯或者让老师觉得鱼唇的地方请多多包涵,感谢!

    作者回复: 很好。看不懂的就大胆提问,不用不好意思,不存在愚蠢的问题。第一个大问题,在链接时,可执行程序的每一个符号都要被解决,有些是在其它中间文件中,这种就是静态链接;有些是在动态库里,这种就不需要解决了,只要在GOT表里留好位置就行了。所以编译一个依赖动态库的应用程序时,既有静态链接,也有动态链接。这不矛盾。第二个大问题的第1小问,两个因素都起作用的。首先,动态库之间的相对位移是在运行时才能知道的,如果整个程序中只有一个动态库,且这个动态库不再依赖其他库了,那显然不需要PIC。其次,如果不是多个进程需要共享代码的话,那我们把偏移放到代码里也没问题。这两个条件都要满足,这才是要使用PIC的原因。第2个问题,进程没有GOT表,GOT表都是跟着动态库的,你可以再读一个03,然后自己动手看看readelf -l的结果和运行起来以后smap的结果。所以第3个问题也就回答了。

    2021-11-08
    4
    6
  • kylin
    GOT是不同进程私有的,并且可以读写,应该在数据段

    作者回复: Good

    2021-11-09
    3
  • kylin
    老师,请问当两个进程仅仅共享同一个动态库的话,您文中说过: “正是虚拟地址技术让我们在进程间共享动态库变得容易,我们只需要在虚拟空间里设置一下到物理地址的映射即可完成共享。” 因为动态库在不同进程的虚拟内存不一样,所以每个进程只要知道动态库的起始地址就可以了,因为具体动态库的符号相对起始地址的偏移量都是一样的。 而地址无关代码是在有多个共享链接库,并且共享链接库之间有引用关系的时候才会用到,在这里还有一个疑问:如何保证不同进程的共享链接库中引用者与这个固定地址之间的相对偏移是固定的呢?

    作者回复: 注意看文章,我们讲了A.exe和B.exe的smap的结果,那里能看到动态库是独立的,它不会再和其他动态库合并了。这就意味着一个库的数据段和代码段在内存里也是靠在一起的。相对偏移和文件中的相对偏移是一样的。所以它就可以提前决定这个偏移是多少。

    2021-11-09
    3
  • 🐮
    老师,请教个问题啊,GOT这块之前也去学习搞过,但这个东西不用就忘记,原理类的有必要去了解吗,学习到那种程度,还是说知道有这个东西就可以了

    作者回复: 这节课的核心就是地址无关代码产生的动机是什么。我们学习计算机知识,一定不能死记硬背。你不必记忆got是什么,只要理解了pic产生的原因,got这种东西就是自然而然的。掌握了这个方案,你下次在工作中遇到类似的问题就知道往哪个方向去研究了。死记硬背的东西很快就忘了,那种学习方法我是非常反对的。

    2021-11-08
    3
  • 我是内存
    你好,我对文中说明使用GOT时候的例子有一个疑问。 文中的背景是: 1)进程1映射foo在0x1000处,调用foo的指令在0x2000,但是填入的是call 0x3000(%rip),GOT在0x5000处。 2)进程2映射foo在0x2000处,调用foo的指令在0x5000,但是填入的是call 0x3000(%rip),GOT在0x80000处。 然后,无论是在进程1还是在进程2,当调用foo时,都先偏移0x3000找到各自的GOT表,再去表里面查找各自的foo地址。 我疑惑的是,如果除了foo,还有其他的foo2,foo3,foo4....这些共享函数。 它们被调用的地方在进程1和进程2里面是随机的,两个进程不太可能在相同的偏移地点都调用foo2,foo3,foo4。这样的话,比如在进程1里面调用foo2的指令在0x2004,这个地方距离GOT的偏移是0x5000-0x2004。进程2里面调用foo2的指令在0x5100,这个地方距离进程2的GOT偏移是0x8000-0x5100。这种情况下,它们距离各自的GOT偏移又不相等了,这时该怎么处理呢?或是怎么样能避免这种情况呢?

    作者回复: 调用者和自己的GOT表的偏移是固定的。它们是同一个动态库,所以它们之间的偏移值就是确定的,不会因为进程的改变而改变。记住一点:调用的语句和GOT之间的偏移和进程无关,它们永远都和磁盘上的偏移是一样的;但是GOT里面的内容,却是和进程相关的,不同的进程里面放的值都不一样,这是因为被调用者的位置在不同的进程里各不相同。

    2021-11-18
    3
    2
  • 小时候可鲜啦
    原文:"call 指令处被填入了 0x3000,这是因为进程 1 的 GOT 与 call 指令之间的偏移是 0x5000-0x2000=0x3000,同时进程 2 的 GOT 与 call 指令之间的偏移是 0x8000-0x5000=0x3000" 疑问:这里GOT和call指令之间的0x3000的固定偏移是如何保证的?GOT在装载后被映射到了进程的数据段,call指令被映射到进程的代码段,这俩段介于不同进程的不同segment中,相对位置如何保持一致?

    作者回复: 我记得在原文中写过,动态库的代码段和数据段是靠在一起的,它们不会被分拆。也就是说,动态库被加载的时候,代码段不必与其他动态库合并。这是静态链接和动态链接比较重要的一个核心区别。

    2021-12-08
    1
  • 好吃不贵
    GOT表是存在数据段中的。 通过readelf的Segment sections的mapping可以看到。 03 .tdata .init_array .ctors .dtors .data.rel.ro .dynamic .got .got.plt .data .bss

    作者回复: Good,动手看一下最直接。不过,还是要稍想一下,为什么要这么做?

    2021-11-09
    1
  • overheat
    typo: linke to linker

    作者回复: OK,thanks

    2021-12-29
  • 坚定的抢手
    老师想咨询一个问题,加载到内存中的每个.so都是有各自独立的GOT表吗,还是所有的.so的GOT表合并到一起了?看精彩留言里,老师好像是说.so都是有各自独立的GOT表。但是并没有很明确的说出这一点来,所以对这个知识点有点迷糊。

    作者回复: .so文件的所有段都是自己独立的,不和其他文件的段合并。也就是说每个.so文件的GOT是独立的。正文中其实有讲,只是没有当做重点单独划出来。

    2021-12-16
    2
  • Geek_551fd5
    假设一个so占1KB,其中data占0.1KB,code占0.9KB。 一个进程使用so。 一个页表项4KB,对应一个4KB物理内存。 一个页表项放so。实际使用4KB物理内存,且私有。 理论上只需要0.1KB私有的物理内存。 这样分析对吗? 怎么实现理论值?

    作者回复: 不是。因为data和code的性质不同,loader在内存中不会把它们放到同一页的。所以这种情况也需要两个页,也就是8KB。

    2021-11-30
    3
收起评论
显示
设置
留言
18
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部