【链接】深入理解PLT表和GOT表

系列综述:
目的:本系列是个人整理为了ELF文件的理解,整理期间苛求每个知识点,平衡理解简易度与深入程度。
来源:材料主要源于各大佬博客和深入理解计算机系统进行的,每个知识点的修正和深入主要参考各平台大佬的文章,其中也可能含有少量的个人实验自证。
结语:如果有帮到你的地方,就点个赞关注一下呗,谢谢!!!
【C++】秋招&实习面经汇总篇


文章目录

    • 概述
      • 基本知识
      • 源代码从静态翻译到动态执行
    • 参考博客


点此到文末惊喜↩︎


概述

基本知识

  1. 共享库(shared library)
    • 作用:所有进程共享代码段而各自拥有数据段的私有副本
    • 优点:节省了内存资源,增加了灵活性
  2. 位置无关代码PIC(position-Independent Code)
    • 作用:将程序编译成不同的目标模块, 模块内部的指令和数据。其余指令部分保持不变,而数据部分则在每个进程拥有一个副本。
    • 原理
      • 目标模块内部的指令和数据相对位置是固定的,加载到内存中时可以不需要重定位,通过指令的相对自己的绝对地址进行跳转即可。
      • 目标模块外的指令和数据的引用与重定位到内存的地址有关,需要在运行时进行动态链接。
  3. 进程的加载
    • 综述:Linux系统中的每个进程都是硬盘中的静态程序加载到内存中的动态执行,其中加载的是进程的内存和寄存器映像。
    • 过程:
      • 操作系统的父进程已通过execve("/bin/sh", argc_rc, envp_rv);,将shell程序加载到内存并运行。
      • 当shell运行一个目标程序文件时,父shell进程会fork一个子进程(fork是对父进程状态的一个复制)
      • 子进程通过execve系统调用启动加载器,加载器使用目标程序文件重置子进程的寄存器和虚拟内存映像(Reset子进程的状态机)
      • 最后加载器跳转到_start,开始执行应用程序的main函数
      • 加载器只进行虚拟内存和寄存器映像的Reset,不进行磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会通过页面调度机制自动从磁盘传送到内存
  4. elf是存储在磁盘中的静态文件,程序执行时所需要的指令和数据必需在内存中才能够正常运行
  5. 如果一个目标模块调用定义在共享库中的任何函数,它就有自己的GOT和PLT,GOT是数据段的一部分,而PLT是代码段的一部分
  6. 延迟绑定
    • 原因:因为一个程序模块通常只会用到共享库中极少量的函数,所以将解析外部模块中的函数内存地址留到实际调用时才进行。
    • 首次调用:跳转到函数对应的PLT项,再跳转到PLT对应的GOT项,GOT在首次调用指向PLT项的第二条指令,再跳回到PLT的第二条指令处开始执行,先将调用函数的ID压入栈中,再将GOT[1]压入栈中,最后通过GOT[2]间接跳转到动态链接器中。动态链接器通过栈中的函数ID和GOT[1]重写函数调用对应的GOT项
    • 再次调用:跳转函数对应的PLT项,再跳转到PLT项对应的GOT项,再通过GOT项跳转到对应的代码段函数
  7. 延迟绑定的实现
    • 全局偏移量表(Global Offset Table/GOT/符号表):
      • 位置:创建数据段开始处(运行中可修改)
      • 内容:前三项存放动态链接器解析相关地址,其他各项均存储本运行模块要引用的一个全局变量或函数的地址
      • GOT表的首地址作为一个基准,用相对于该基准的偏移量来引用静态变量、静态函数
    • 过程链接表(Procedure Linkage Table/PLT)
      • 位置:在代码段中(运行中不可修改)
      • 内容:PLT[0]会将GOT[1](传递给动态链接器的一个参数)压入栈中,再通过GOT[2]间接跳转到动态链接器中。PLT[2]会调用系统启动函数。其余项负责调用一个具体的函数。
  8. 如果一个模块调用其他共享模块中的函数,通常原模块中的函数调用会生成一条重定位记录,然后动态链接器在程序加载时再解析。
  9. 符号表GOT和PLT:统一静态与动态链接,静态链接时模块内部的相对地址,动态链接是通过延迟绑定的间接引用实现的
  10. C++20中的modules进行了模块隔离,不同模块间的函数调用,需要export具体的被调用函数即可,从而增加模块的独立性和编译速度。
  11. 如果系统开启了内存布局随机化ASLR,程序每次运行动态链接库的加载位置都是随机的,就很难通过调试工具直接确定函数的地址

源代码从静态翻译到动态执行

  1. 预处理阶段(Preprocessing)
    • 处理所有预编译指令(以#开头)
      • 删除#inlucde和#define并展开
      • 处理条件预编译指令,比如#if、#ifdef、#elif、#else、#endif等
    • 删除所有注释
    • 添加行号和文件标识
    • 预处理命令:gcc -E hello.c -o hello.i
  2. 编译(Compilation)
    • 词法分析:输入组成源程序的字符流,通过处理输出分解后成为词法单元组成的符号流
    • 语法分析:利用词法单元流构建一颗语法树
    • 语义分析:使用语法树和符号表中的数据,进行类型检查和运算符匹配检查
    • 中间代码生成:生成易于翻译到各种不同平台的机器无关语言
    • 机器无关代码优化:期望使用更长的编译时间换取更高效的目标程序运行
    • 代码生成:使用中间代码生成目标语言
    • 机器相关代码优化:生成更高效的目标机器语言(通常是汇编语言)
  3. 汇编(Assembly)
    • 将汇编语言文件生成可重定位的二进制目标程序
  4. 链接方式
    • 静态链接:将要链接的目标文件生成副本拷贝到可执行文件中,就算把静态库删除也不会影响可执行程序的执行。
      • 优点:运行速度快,删除静态库不影响代码执行。
      • 缺点:库函数修改需要重新链接,同一个链接文件可能在多个可执行程序中都由副本,浪费空间
    • 动态链接:将程序分成独立模块,在执行时再链接形成可执行文件
      • 优点:所有程序共享一个库文件,减少空间浪费。单个文件修改不影响整体执行
      • 缺点:每次执行时进行链接会产生性能损耗
  5. 静态链接器(ld)
    • 原因:现代操作系统通常以动态链接共享库(DLL)的形式进行程序加载,4可以忽略
      • 可重定位的目标文件.o共享库.so进行部分链接生成运行时再完全链接的可执行目标文件
      • 将模块内的位置无关代码进行静态链接,将模块间的符号引用
  6. 加载器(通过execve调用)
    • 部分链接的可执行目标文件共享库重定位到相应内存段,即Reset进程的初始状态
  7. 动态链接器(dynamic linking)
    • 对于模块外部引用的全局变量和全局函数,用 GOT 表的表项内容作为地址来间接寻址 ;对于本模块内的静态变量和静态函数,用GOT 表的首地址作为一个基准,用相对于该基准的偏移量来引用, 因为不论程序被加载到何种地址空间,模块内的静态变量和静态函数与 GOT 的距离是固定的,并且在链接阶段就可知晓其距离的大小。

其他:

  • 将多个目标文件中的相同段进行合并,为新的段分配虚拟内存地址,记录新的段在新文件中的偏移量,即VMA和File off。同时,计算并记录所有的符号的虚拟内存地址。
  • 根据各个目标文件中的重定位段信息,对合并后的text段中涉及到符号引用的位置进行重定位。


少年,我观你骨骼清奇,颖悟绝伦,必成人中龙凤。
不如点赞·收藏·关注一波


点此跳转到首行↩︎

参考博客

  1. 动态链接库中与地址无关代码(PIC)对于地址引用的处理
  2. 深入理解计算机系统
  3. 待定引用
  4. 待定引用
  5. 待定引用
  6. 待定引用
  7. 待定引用
  8. 待定引用

你可能感兴趣的:(深入理解操作系统,后端,linux)