『operating system-1』static link

静态链接

一、编译与链接

  • GCC编译过程分解:

    编译过程图示


  • 预编译:处理源代码文件中以“#”开头的预编译指令

    1. 展开所有的宏定义 #define
    2. 处理所有的条件预编译指令,如 #if、#ifdef、#elif、#endif 等
    3. 递归处理预编译指令 #include,将被包含文件插入到预编译指令的位置
    4. 删除所有注释
    5. 添加行号信息文件名标识,便于编译器产生调试所用的行号信息
    6. 保留所有的编译器指令 #pragma(编译器之后会使用这些指令)
  • 编译:对预处理的文件进行词法分析、语法分析、语义分析、指令优化等操作,转化为汇编代码

    Linux下C语言的编译程序是cc1、C++是cc1plus、Java是jc1

    注意:gcc只是多个后台命令的包装,会根据传入参数调用编译程序cc1,汇编器as或链接器ld

  • 汇编:将汇编代码转变为机器可执行的命令(即翻译)

  • 链接:将每个源代码模块独立地编译,再将各模块正确地衔接


  • 静态链接:地址和空间分配 + 符号决议 + 重定位

    • 重定位:链接时将跨模块调用的地址补充完整(填补之前地址一般为0)

二、目标文件

  • 可执行文件格式:Portable Executable (Windows) + Executable Linkable Format (ELF)

    动态链接库(DLL)和静态链接库(SLL)也可以按可执行文件格式存储

    ELF文件格式 描述 实例
    可重定位文件 包含代码和数据,可链接成可执行文件 Linux的.o & Windows的.obj
    可执行文件 可以直接执行的程序 Linux的/bin/bash & Windows的.exe
    共享目标文件 能与其它可重定位文件链接成新的目标文件,或与其它可执行文件链接成新的进程映像文件 Linux的.so & Windows的DLL
    核心转储文件 进程意外终止时可将其地址空间内容转储到核心转储文件 Linux下的core dump
  • 目标文件格式:按文件信息的不同属性分为“段”或“节”(表示一定长度)

    • ELF文件头:描述了整个文件的信息,以及段表

    • 代码段(.code & .text):存放编译后的机器指令

    • 数据段(.data):存放初始化全局变量局部静态变量

    • 只读数据段(.rodata):存放只读数据(如const修饰的变量或字符串常量

    • .bss段:预留未初始化全局变量局部静态变量的位置(本身无数据)

    • .comment段:存放编译器版本信息

    • .dynamic段:动态链接信息

      ...

      注意:段名都以“.”作为前缀,表示系统保留字;应用程序的自定义段名不能用“.”作为前缀

  • 文件分段的好处:为何要将指令(.text + .code)和数据段(.data)分离?

    • 指令一般是只读的,数据一般是可写的,便于分别设置权限
    • 现代CPU一般都有指令cache数据cache,将指令和数据分离有助于提高缓存命中率
    • 作为只读区域,指令便于共享,可以节省空间
  • 自定义段:可以指定变量所处的段,如利用gcc扩展机制,可将变量var放入自定义的name段

    1
    2
    __attribute__((section("name"))) int var = 1;
    __attribute__((section("func"))) void foo() {}

  • ELF文件结构:可使用binutils中的readelf命令查看ELF文件

    • 文件头(Elf32_Ehdr):包括ELF魔数、机器字节长度、运行平台,程序入口与长度等信息

      • ELF魔数(16位):前四个字节相同,为0x7f、0x45、0x4c、0x46,分别表示DEL、E、L、F

        第5个字节标识文件类型:0x01表示32位、0x02表示64位

        第6个字节标识字节序:0x01表示小端、0x02表示大端

      • 文件类型(e_type):ET_REL指可重定位、ET_EXEC指可执行、ET_DTN指共享目标文件

      • 机器类型(e_machine):如EM_386表示文件可以在Intel x86机器上运行

        注意:不同机器上的ELF文件都遵循同一套ELF标准

      • 程序虚拟入口(e_entry):进程开始执行的指令虚拟地址;可重定位文件的入口一般设0

        ...

      注意:ELF头中使用了typedef定义了自定义变量体系,使得在不同编译环境下都有相同的字段长度

      如Elf32_Addr类型表示原始的uint32_t类型(4字节);Elf32_Half表示原始的uint16_t类型(2字节)

    • 段表:保存各段的基本属性(段名、段长、段偏移等),ELF头的e_shoff指明了段表在ELF文件的位置

      段表本质上是一个以Elf32_Shdr结构体为元素的数组;Elf32_Shdr结构称作段描述符对应了一个段

      ELF头的e_shnum表示ELF中拥有的段数量,即段表内的元素个数

    • 段描述符:即Elf32_Shdr结构体

      • 段名(sh_name):一个指向“.shstrtab”字符串表的偏移量(注意是段表字符串表)

      • 段类型(sh_type):以关键字“SHT”开头,如代码段和数据段的类型都是SHT_PROGBITS .hash段的段类型为SHT_HASH;.bss段的段类型为SHT_NOBITS等

        注意:段名(像.text)并不能真正表示段的类型

      • 段的标志位(sh_flag):以关键字“SHF”开头,表示段在进程虚拟地址空间中的属性 SHF_WRITE指该段可写、SHF_ALLOC指需要在进程地址空间中为该段分配空间(如.data、.text、.bss)、SHF_EXECINSTR指可执行代码(如.text)

        注意:只有段类型段标志位对操作系统是有意义的,段名只对编译器和链接器有意义

      • 段的链接信息(sh_link、sh_info):标明与链接相关的段的信息,如SH_REL段(重定位表)

      • 段偏移(sh_offset)与段地址(sh_addr):分别表示该段在ELF文件中位置的偏移;段在进程虚拟地址空间中的虚拟地址

      • 段地址对齐(sh_addralign):对齐的段地址需满足 sh_addr % (2 ^ addralign) = 0 若无需对齐,sh_addralign取0或1

    • 重定位表(.rel.xxx):包含对应段xxx链接过程中的重定位信息,用于填补目标文件中的绝对地址引用;一张重定位表也是ELF文件中的一个段

    • 字符串表(.strtab)与段表字符串表(.shstrtab):保存ELF文件中符号的集合

      • 字符串表:保存ELF文件中普通的字符串

      • 段表字符串表:保存段表中用到的字符串(如段名)

        注意:ELF头中的e_shoff表示段表位置;e_shstrndx表示.shstrtab段在段表中对应的下标
        我们使用字符串在字符串表中的下标来引用对应的符号


  • 符号:链接的接口

    • 定义与引用:设目标文件B使用了目标文件A中的函数foo 目标文件A定义了函数foo 目标文件B引用了函数foo

    • 符号与符号值:“符号”指函数或变量,“符号值”一般指函数或变量的地址

      • 全局符号定义在本目标文件的全局符号,可被其它目标文件引用
      • 外部符号在本目标文件中引用的全局符号,但没定义在本目标文件
      • 段名:其值是段的起始地址(如.text)
      • 局部符号:不可被外部链接的符号(如static修饰的符号)
      • 行号信息:目标文件指令源代码行的对应关系
    • ELF符号表结构:符号表本身是一个,段名为 .symtab

      符号表是一个元素为结构体Elf32_Sym的数组(其首元素总是未定义的)

    • 符号结构体:对应于ELF文件中的符号

      • 符号名(st_name):一个指向该符号在字符串表中的下标

      • 符号所在段(st_shndx):对于变量或函数的定义,其表示符号所在段在段表中的下标,特殊情况下:

        • SHN_ABS:绝对的值,如ELF文件名
        • SHN_COMMON:表示COMMON块类型的符号,如未初始化的全局变量
        • SHN_UNDEF(0):表示该符号未定义,如被引用的符号
      • 符号值(st_value):对于变量和函数的定义,其值是该符号的地址,具体讨论如下:

        • 在本目标文件定义的非COMMON块符号,其值是st_shndx指定段中的偏移量
        • 目标文件中的COMMON块符号,st_value表示该符号的对齐属性
        • 在可执行文件中,st_value是虚拟地址
      • 符号类型与绑定信息(st_info):低4位表符号类型,高28位表绑定信息

        • 符号类型:以“STT”关键字开头,如: STT_OBJECT:指变量或数组 STT_FUNC:指函数或可执行代码 STT_SECTION:表示一个段(其符号总是STB_LOCAL) STT_FILE:表示源文件名(其符号总是STB_LOCAL的,其st_shndx总是SHN_ABS)
        • 符号绑定信息:以“STB”关键字开头,如:
          STB_LOCAL:指外部不可见的局部变量
          STB_GLOBAL:指外部可见的全局变量

    • 特殊符号:被定义在ld链接器的链接脚本中,可在程序中直接声明引用;以下特殊符号均表示虚拟地址

      • __executable_start:程序起始地址(而非入口地址)
      • __extext:代码段结束地址
      • _edata:数据段结束地址
      • _end:程序结束地址

    • 符号修饰与函数签名:

      • 符号修饰:修饰原始的符号名,防止同名函数冲突(_Z)

      • 函数签名:函数名 + 参数类型 + 所在命名空间(或类)

        注意:函数签名指向了唯一的函数

      • extern "C":在C++中声明一个代码块,将代码块中的代码按C语言代码处理


    • 弱符号与强符号:仅讨论定义(而非引用)的符号 初始化的全局变量称为强符号,未初始化的全局变量为弱符号 链接时选择多次定义的强弱符号遵循以下规则:

      • 不同的目标文件中不能有同名的强符号(否则链接器会报重复定义错误)
      • 若某个符号在某个目标文件中是强符号,在其它文件中都是弱符号,则链接时选择强符号
      • 若某个符号在所有目标文件中都是弱符号,则选择占用空间最大的弱符号

      注意:可以使用__attribute__((weak)) var = 1 将强符号var转化为弱符号

    • 弱引用与强引用:链接器需要针对所有的外部引用符号找到其符号定义

      • 强引用:引用的外部符号在链接时未找到定义则报错
      • 弱引用:引用的外部符号若被定义则根据其符号规则决议;若未被定义也不报错,为其分配一个特殊值
    • 弱符号与弱引用的用途:

      • 用户自定义的强符号可以覆盖标准库的弱符号
      • 将对扩展模块的引用声明为弱引用,便于扩展模块的装卸

      注意:可以使用 __attribute__((weakref)) void foo(); 将外部引用的函数转为弱引用


  • 调试信息:包括源代码映射、堆栈跟踪信息、函数参数或局部变量等

    • 调试:设置断点、监视变量变化、单步行进等

    • 调试前提:编译器记录了源代码目标代码间的关系

    • 在目标文件中添加调试信息:“-g”参数;删除目标文件中的调试信息:“strip”命令

      注意:调试信息占用空间很大且对用户无用处,应该在发布前被删除


三、静态链接

  • 空间与地址分配:将多个目标文件合并为一个输出文件,并为其分配空间

    • 按序叠加:直接将多个目标文件按次序合并,非常浪费空间

    • 相似段合并:将各目标文件中相同的段合并为一个段,一般分两步:

      1. 空间与地址分配:获得各输入文件的段长度,合并后计算各段合并后的长度与地址
      2. 符号解析与重定位:读取输入文件中的重定位信息,解析符号、调整代码地址、进行重定位

      注意:各段合并后(链接后)所使用的地址是进程中的虚拟地址


    • 确定符号地址:这里指加载到进程地址空间中的虚拟地址

      1. 第一步:确定合并后各段的起始虚拟地址
      2. 第二步:根据各符号于其所在段的偏移量,通过叠加段始址计算各符号的虚拟地址

  • 符号解析与重定位:

    • 重定位:填补本目标文件中外部引用的地址

    • 重定位表:保存有关重定位信息的表,专门占有一个段“.rel.xxx” 重定位表项是元素类型为Elf32_Rel结构的数组,一个结构体元素对应一个重定位入口

      • 重定位入口:指代码中需要被重定位的位置,其包含的外部引用地址将会被填补
      • 重定位入口的偏移(r_offset):0x04: call <addr> EC 00 00 00 00 \(\rightarrow\) r_offset = 0x05
        1. 可重定位文件:代表待修正地址首字节相对段始址的偏移数
        2. 可执行文件:代表待修正地址首字节虚拟地址
      • 重定位入口的类型与符号(r_info):
        1. 低8位:表示重定位入口类型
        2. 高24位:表示重定位入口的符号在符号表中的下标

    • 符号解析:全局undefined的符号(即外部引用)应当于链接后在全局符号表中找到,否则会报未定义错误


    • 指令修正方式:链接完成后,修正重定位入口中包含的地址,以x86_32为例:

      • 绝对寻址(mov):直接根据实际虚拟地址取值

      • 绝对寻址修正(R_386_32):符号实际虚拟地址 + 待修正位置的值


      • 相对寻址(call):实际虚拟地址(即跳转地址) = call的下一条指令地址 + 相对地址

      • 相对寻址修正(R_386_PC32):符号实际虚拟地址 + 待修正位置的值 - 被修正位置的虚址

        注意绝对修正后的地址即为实际地址相对修正后的地址为符号位置离被修正位置的地址差

        特别注意“被修正位置”指的是待填写地址首字节的位置,而不是所在指令的首地址

    各表间关系图示


  • COMMON块

    • 未初始化的全局变量:链接前不分配段空间,并标记为SHN_COMMON

      不把未初始化的全局变量直接分配在目标文件.bss段的原因:

      未初始化的全局变量是弱符号,链接前不知道各目标文件同名弱符号的相对大小;链接器选出了最大的弱符号后,才可将其分配在输出文件的.bss段中

      注意:链接时不允许有弱符号的大小大于强符号,否则会报错


  • API与ABI:

    • API:源代码级别的接口,遵循相同API标准的系统拥有相同的接口函数原型

    • ABI:二进制层面的接口,涉及内存分布、函数调用方式、寄存器使用约定等问题,比API更严格

      注意:即便API相同,ABI也不一定相同;硬件、编程语言、编译器、操作系统都可能影响ABI


  • 静态库链接

    • 静态库:一组目标文件的压缩集合,一个目标文件包含一个函数(节省空间、便于管理)

    • glibc :GNU发布的libc库,glibc是Linux系统中最底层的API

    • libc.a :Linux中最常用的C语言静态库文件,是glibc项目的一部分,共包含1400个目标文件

      如何使用静态库函数?将源目标文件libc.a链接即可


  • 链接过程控制:

    • 如何控制连接过程?

      • 为链接器ld传递参数
      • 将链接指令放在目标文件中(如COFF中的.drectve段)
      • 使用链接控制脚本
    • 链接脚本:默认链接脚本存放在/usr/lib/ldscripts路径下,负责指示链接器ld如何进行链接

      ld会根据命令行要求使用相应的链接脚本文件控制连接过程:

      • elf_i386.x:生成可执行文件的链接脚本
      • elf_i386.xs:生成共享目标文件的链接脚本
      • 自定义脚本:使用ld命令的 “-T” 参数指定自定义的 .lds
    • ld链接脚本的使用:输入目标文件和库文件,输出可执行文件

      • 链接脚本扩展名:.lds

      • 链接脚本语法:由一系列命令语句 + 赋值语句组成

        • 赋值语句:点符号 . 表示当前虚拟地址,指定后续输出段的始址

        • ENTRY(symbol):指定符号symbol的值作为入口虚拟地址,指定入口的优先级:

          1. ld命令行的 -e 选项
          2. 链接脚本中存在命令语句ENTRY(symbol)
          3. 若定义了**_start** 符号,则使用符号 _start 的值
          4. 若存在 .text 段,则使用.text段的首字节地址
          5. 使用默认值0
        • STARTUP(filename):将文件filename作为链接中的第一个输入文件

        • SEARCH_DIR(path):将路径path加入到ld链接器中的库查找目录(以寻找对应库)

        • INPUT(file1, file2, ...):指定若干文件作为链接输入

        • INCLUDE filename:将文件filename包含入本链接脚本

        • PROVIDE(symbol):在链接脚本中定义符号,如特殊符号__executable_start


        • SECTIONS命令语句:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        SECTIONS
        {
        ...
        /* secname 表示输出段的段名 */
        /* contents 表示若干个输出段应符合的条件 */
        secname : { contents }
        ...
        /* DISCARD 段表示丢弃符合条件的输入文件,不作链接 */
        /DISCARD/ : { contents }
        ...
        }

        下面是规则contents的语法示例:将符合条件的所有段合并为输出段secname

        • file1.o(.data):输入文件file1.o中的.data段符合条件
        • file1.o(.data, .rodata):输入文件file1.o中的.data 或 .rodata段符合条件
        • file1.o:输入文件file1.o的所有段都符合条件
        • *(.data):表示所有输入文件的.data段都符合条件

      注意:ld链接脚本的语法风格类似C语言,且支持正则表达式匹配


    • BFD库(Binary File Descriptor Library):通过统一的接口处理不同的目标文件格式

      • BFD库的目的:解决不同硬件/软件平台下目标文件格式的差异
      • BFD库的优势:将编译器和链接器本身与目标文件隔离,便于扩展支持新的目标文件格式
      • BFD库的使用:安装BFD开发库后,#include "BFD.h"

四、Windows PE/COFF

  • PE与COFF:

    "PE"(Portable Executable):微软引入的一种可执行文件格式(Win32平台)

    "COFF"(Common Object File Format):PE格式(Windows)和ELF格式(Linux格式)的共同起源

    注意:PE/COFF格式同样以的格式组织,代码段为.code、数据段为.data

  • Microsoft Visual C++编译环境:编译器cl(compiler)、链接器(link),可执行文件查看器(dumpbin)


  • COFF文件结构:文件头 + 若干个段

    • 文件头:映像头 + 段表

      • 映像头(IMAGE_FILE_HEADER):与Elf32_Ehdr的作用类似

        • Machine:目标机器类型,如x86、ARM等
        • NumberOfSections:目标文件中段的数量
        • TimeDateStamp :PE文件的创建时间
        • PointerToSymbolTable:符号表在PE文件中的偏移

      • 段表:记录每个段的信息,是一个IMAGE_SECTION_HEADER结构的数组

      • 段表项(IMAGE_SECTION_HEADER):一个段表项对应一个

        • VirtualSize:该段被加载到内存后的大小
        • VirtualAddress:该段被加载到内存后的虚拟地址
        • SizeOfRawData:该段在文件中的大小(由于.bss段的存在,SizeOfRawData不一定与VirtualSize相等)
        • Characteristics:段的属性,如段类型、对齐方式、读写权限等

    • 段:COFF各段的存储方式和内容与ELF几乎相同,新增了“.drectve”段和“.debug$...”段

    • .drectve段:包含了编译器向链接器传递的链接指令

    • .debug$段:以 .debug\$ 关键字开头的段,如:

      • .debug$S:表示符号相关的调试信息段
      • .debug$P;表示预编译头文件的调试信息段
      • .debug$T:表示包含类型相关的调试信息段

      注意:.drectve段和.debug$段的文件数据都位于 SECTION HEADER 下方的 RAW DATA 中


    • 符号表:与ELF文件符号表类似,包含符号名、符号类型、是否可外部链接等信息


  • PE文件结构:基于COFF的扩展

    • DOS MZ可执行文件格式的文件头和桩代码:Windows为适应传统的DOS而扩展的文件头 DOS桩代码的唯一功能是向终端输出“当前文件无法在DOS上运行”的信息

    • IMAGE_NT_HEADERS :PE文件头,在COFF的IMAGE_FILE_HEADER基础上新增了PE扩展头部

      • 标记:常量值0x00004550(即'P'、'E'、'\0'、'\0')
      • 映像头(IMAGE_FILE_HEADER):即原先COFF中的映像头
      • PE扩展头部(IMAGE_OPTIONAL_HEADER):包括数据目录、导入导出表、重定位表等
    • PE数据目录(IMAGE_DATA_DIRECTORY[ ]):PE扩展头部的成员结构,名称为DateDirectory的数组

      • 虚拟地址(VirtualAddress):PE文件的装载虚拟地址
      • 长度(Size):PE文件装载所占内存大小

      注意:DateDirectory元素中的各表项对应一个表的地址和大小信息(如导出表、重定位表等)


『operating system-1』static link
http://larry0454.github.io/2023/07/20/operating_system/static-link/
Author
WangLe
Posted on
July 20, 2023
Licensed under