学习思考
🀄链接
00 分钟
2023-8-5
2023-8-13
type
status
date
slug
summary
tags
category
icon
password
Property
Aug 13, 2023 04:20 AM

ELF文件

如何理解ELF文件,我们从jyy的“计算机就是一个状态机”的视角来进行思考:
  • 计算机就是一个状态机
  • ELF放在execve系统调用中被执行
  • exec类的系统调用实际上就是对状态机进行“reset”,从而改变状态机的执行流
  • 一个描述了状态机的初始状态+迁移数据结构
    • 寄存器
      • 大部分由ABI规定
      • 例如初始的pc
    • 地址空间
      • 二进制文件+ABI共同决定
      • 例如argv和envp(和其他信息)的存储
    • 其他有用的信息(例如便于调试的core dump的信息)
  • ELF:Executable Linkable Format
  • #!:一个偷换概念的execve,当shell发现以#!开头时,execve会讲第一个参数替换
  • 什么样的程序算是ELF文件,是用exec执行其他类型文件时,会发生什么?

解析可执行文件

二进制文件工具集:

  • 生成可执行文件
    • ld, as
    • ar, ranlib
  • 解析可执行文件
    • objdump/objcopy/readelf
    • addr2line,size,nm

可执行文件类型

ELF文件主要有三种类型,可以通过ELF Header中的e_type成员进行区分。
  • 可重定位文件(Relocatable File)ETL_REL。一般为.o文件。可以被链接成可执行文件或共享目标文件。静态链接库属于可重定位文件。
  • 可执行文件(Executable File)ET_EXEC。可以直接执行的程序。
  • 共享目标文件(Shared Object File)ET_DYN。一般为.so文件。有两种情况可以使用。
    • 链接器将其与其他可重定位文件、共享目标文件链接成新的目标文件;
    • 动态链接器将其与其他共享目标文件、结合一个可执行文件,创建进程映像。

可重定位目标文件

每个可重定位目标文件都可以大致分为三部分,分别是:
  • ELF header
  • 不同的section
  • 描述这些section的表
notion image
ELF Section Header Table
ELF 节头表是一个节头数组。每一个节头都描述了其所对应的节的信息,如节名、节大小、在文件中的偏移、读写权限等。编译器、链接器、装载器都是通过节头表来定位和访问各个节的属性的。
最开始的16个字节:
notion image
ELF文件结构示意图中定义的Elf_Shdr的各个成员的含义与readelf具有对应关系。如下表所示:
成员
含义
sh_name
节名
节名是一个字符串,保存在一个名为.shstrtab的字符串表(可通过Section Header索引到)。sh_name的值实际上是其节名字符串在.shstrtab中的偏移值
sh_type
节类型
sh_flags
节标志位
sh_addr
节地址:节的虚拟地址
如果该节可以被加载,则sh_addr为该节被加载后在进程地址空间中的虚拟地址;否则sh_addr为0
sh_offset
节偏移
如果该节存在于文件中,则表示该节在文件中的偏移;否则无意义,如sh_offset对于BSS 节来说是没有意义的
sh_size
节大小
sh_link、sh_info
节链接信息
sh_addralign
节地址对齐方式
sh_entsize
节项大小
有些节包含了一些固定大小的项,如符号表,其包含的每个符号所在的大小都一样的,对于这种节,sh_entsize表示每个项的大小。如果为0,则表示该节不包含固定大小的项。
ELF Sections

section的分类

上述ELF Section Header Table部分已经简单介绍了节类型。接下来我们来介绍详细一些比较重要的节。
notion image
notion image
notion image

符号表.symtab

对于每一个可重定位文件都有一个符号表,这个符号表包含该模块定义喝引用的符号信息。
三种不同的符号:
  • Global Symbols:由该模块定义并能被其他模块引用的全局符号,这些符号对应于非静态的c函数喝全局变量
  • External Symbols:由其他模块定义并被该模块引用的全局符号,这些符号称为外部符号,对应于在其他模块中定义的非静态c函数喝全局变量
  • Local Symbols:只被该模块定义和引用的局部符号。他们对应于带static属性的c函数和全局变量,这些符号在该模块中任意位置都可见,但是不能被其他模块引用
强符号和弱符号:
  • Strong Symbols:函数和已初始化的全局变量
  • Weak Symbols:未初始化的全局变量
 
一个符号表:
notion image
Section header table
描述section不同属性的表

链接

链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行
链接是由叫做链接器的程序自动执行的
链接器使得分离编译成为可能
链接可以执行于编译时,也可以执行于运行时

编译器驱动程序

大多数编译系统提供编译驱动程序(compiler driver),它代表用户在需要时调用语言预处理器、编译器、汇编器和链接器。
大家都知道的过程:
.c → .s(ASCII汇编语言文件) → .o(可重定位目标文件relocatable object file) → 运行ld将文件以及一些必要的系统目标文件组合起来,创建一个可执行目标文件
shell调用操作系统中一个叫做加载器loader的函数,它将可执行文件的代码和数据复制到内存,然后将控制转移到这个程序的开头

静态链接

像linux LD这样的静态链接器 static linker以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的、可以加载和运行的可执行目标文件作为输入。
输入的可重定位目标文件由各种不同的代码和数据节(section)组成,每一节都是一个连续的字节序列。
指令在一节中,初始化的全局变量在另一节中,而未初始化的变量又在另外一节中。
为了构造可执行文件,链接器必须完成两个主要任务:
  • 符号解析 symbol resolution:目的是将每个符号引用正好和一个符号定义关联起来
  • 重定位 relocation:编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使他们指向这个内存位置
notion image
具体过程:
  1. 在符号解析阶段,链接器从左到右按照命令行中出现的顺序来扫描可重定位文件和静态库文件,由于编译器驱动程序总是将libc.a传给链接器,所以命令中不必显示的引用libc.a
  1. 在扫描的过程中,链接器一共维护了三个集合
      • E:在链接器扫描的过程中发现了可重定位目标文件就会放在这个集合中,在链接即将完成的时候,这个集合中的文件最终会被合并起来形成可执行文件
      • U:链接器会把引用了但是尚未定义的符号放在这个集合里
      • D:存放输入文件中已经定义的符号
      链接最开始时,这三个集合均为空
  1. 对于命令行中每一个文件,链接器都会判断这是一个目标文件还是一个静态库文件
      • 如果是一个目标文件,那么链接器会把该文件放在集合E中,同时修改U和D来反映文件中的符号引用和定义。此时对于未定义的符号,编译器会认为它们在其他模块被定义,因此编译器不会报错。此外,该文件中已经定义的全局符号和函数会被放在集合D中
        • notion image
      • 如果是一个静态库文件,链接器会尝试在这个静态库文件中寻找集合U中未解析的符号,对于已经定义的符号,也要加入到集合D中
        • notion image
      对于静态库文件中的所有成员目标文件都要依次进行以上处理,直到集合U和D都不再变化
  1. 对于不在集合E中的成员目标文件都被简单的丢弃
  1. 最后链接器还要扫描libc.a文件,再删除集合U中的符号
  1. 最后,如果集合U是空的,那么链接器会合并集合E中的文件来生成可执行文件;如果链接器完成对命令行上所有输入文件的扫描后,集合U非空,那就说明程序中使用了未定义的符号,此时链接器输出一个错误并终止
不幸的是,这种算法会导致一些令人困扰的错误,在命令行中文件的顺序至关重要,同时,要留意两个静态库文件中函数相互引用的情况。

重定位

经过上边的过程,链接器就可以确定要将哪些文件进行合并了,同时,链接器也获得了这些目标文件的代码节和数据节的大小信息,接下来,开始进行重定位的操作。
在这个过程中,链接器合并输入模块,并为每个符号分配运行时地址,具体的重定位过程分为两步:
  1. 重定位节和符号定义 Relocating sections and symbol definitions
  1. 重定位节中的符号引用 Relocating symbol references within sections
首先我们需要明确的两个事实:
1)汇编器生成一个目标模块时从地址0开始生成代码和数据节 ,它并不知道数据和代码最终将放在内存的什么位置,也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。
2)链接器在重定位步骤中,合并输入模块并将运行时地址赋给输入模块定义的每个节、符号。当这一步完成时,程序中的每条指令和全局变量才拥有唯一的运行时内存地址。
现在我们引出一个关键问题。当一个模块引用一个外部定义的函数时,既然汇编器并不知道函数的最终内存位置,它要如何定位到这个函数?毕竟如下图所示,最终的函数调用是需要指出其具体地址的,接下来我们便针对这个例子展开叙述。

重定位相对引用

notion image
我们先粗略地了解汇编器解决上述问题的做法:
可重定位条目 Relocation Entries :用来告诉链接器在合成可执行文件时应该如何修改这个引用
当汇编器遇到一个最终位置未知的引用时,它简单地将立即数0x0放入引用处,并为这个引用生成一个重定位条目放在 .rel .text 中(代码放在.rel.text,已经初始化的数据放在.rel.data中),而这个重定位条目拥有足够多的信息指导链接器在链接时将0x0修改为正确的数值,剩下的事就都交给链接器处理了。
notion image
 
重定位条目有如下对应关系:
1)offset --> f,f指立即数0x0位置的偏移量,即要修改的引用位置离main的偏移量
2)type --> RX86_64_PC32重定位条目一共有32种type,R_X86_64_PC32表明重定位的引用使用32位PC相对地址
3)symbol --> sum,表明调用的符号是sum,在本例中是个函数
4)addend --> -0x4,这个addend可能会带来疑惑,它代表引用位置与下一条指令的相对位置关系,在此例中就是引用的偏移量 0xf 减去下一指令的偏移量 0x13 得到 -0x4,在链接器进行具体计算时我们需要用到它
 
链接器如何进行最后的运算?
我们知道在链接时,链接器已经赋予了main和sum运行时地址,现在我们要利用它们结合重定位条目的信息计算并修改引用位置的0x0。
首先,链接器需要根据重定位条目计算出引用的运行时地址 ,具体的计算方法是通过函数main的起始地址与重定位条目中的偏移量字段相加,这样,我们就得到了引用的运行时地址
然后更新这个符号引用,使它在运行时指向相关函数。用函数的起始地址减去刚才计算得到的运行时地址。运行时当前pc的值要加多少,才能使下一个正好是call的值
Relocation algorithm:
如上图和前文所示,refptr是指向引用的指针,refaddr是引用的运行时地址,ADDR(r.symbol)是sum的运行时地址,r.addend是引用和下一条指令的相对位置。注意,第二个if语句针对使用绝对地址的引用,不在我们这个例子的考虑范围内。
notion image
当CPU执行callq指令时,PC的值为0x4004e3,即callq的下一指令地址(在处理器中,callq进入执行阶段,下一指令进入取址阶段),为了执行callq指令,CPU将执行以下步骤:
1)将PC压入栈中
2)更新 PC <-- PC + 0x5 = 0x4004e3 + 0x5 = 0x4004e8
显然0x5是我们的重定位算法最后放在引用处的值,它对应于 sum 的运行时地址 减去 callq 下一指令的运行时地址,即:ADDR(r.symbol) - (refaddr - r.addend)

重定位绝对引用

notion image
针对上边的例子:当执行完重定位操作后,指令操作数部分就是ADDR(array),重定条目中offend字段值默认为0

可执行目标文件

notion image
其中有一项是程序的入口,也就是程序运行时要执行的第一条指令的地址,可执行文件的.init节定义了一个名为_init的函数,程序的初始化代码会调用这个函数进行初始化
.text、rodata、以及.data section与可重定位目标文件中的节是类似的,不过这些节以及被重定位到最终的运行时内存地址上,因此,执行目标文件中不再需要rel section
notion image
执行时,程序的代码段和数据段要被加载到内存执行,不过还有一部分内容不会被加载内存,例如符号表和调试信息等
  • 程序头部表 Program Header Table
    • 描述了代码段,数据段与内存的映射关系
      .bss section同样要被加载到内存空间,因此数据段在内存中的size会更大
  • 可执行文件是如何加载运行的
    • fork()execve() → 加载
      每个Linux程序都有一个运行时内存,如图所示
      notion image
      当加载器运行时,它为程序创造图中所示的内存镜像,根据程序头部表的内容,加载器将可执行文件的section复制到内存相应的位置
      接下来,加载器跳转到程序的入口处,也就是_start()函数(系统目标文件ctrl.o中定义)的地址
      接下来,函数_star()调用__libc_start_main() 位于libc.so中,作用是初始化执行环境
      然后,调用用户层的main函数
      当main函数执行完毕后,函数main的返回值还是由libc.so中的这个函数来处理,并且在需要的时候把控制权交还给操作系统
      notion image
      注:这个过程只是大概,没有结合进程与虚拟内存等内容

动态链接共享库

是一种特殊的可重定位目标文件,在linux系统中通常以.so的后缀来表示
 
创建过程
  • -share 指示编译器创建一个共享的目标文件
  • -fpic 告诉编译器生产位置无关的代码
 
notion image
链接过程
  • 首先将libc.so中的代码和数据重定位到某个内存段
  • 然后重定位制定动态库中的代码和数据到另一个内存段
  • 接下来重定位prog2中有libc.so和libvector.so定义的符号引用
  • 上述重定位的操作完成后,动态链接器把控制权交给应用程序prog2,从这个时刻开始,共享库的位置就固定了,并且在程序的执行过程中都不会改变

MIPS相关

参考

上一篇
Interrupt
下一篇
Traps

评论
Loading...