《程序员的自我修养》笔记1——目标文件

一、前言

在一般的开发过程中,我们往往离不开 编译 这个操作。有时候遇到一些编译错误的时候,第一反应就是查找百度。但如果我们对编译的整体有个比较清晰的认知,很多常见的错误我们是可以通过错误提示直接分析出错误的原因的。那么这个系列就是简单的记录下学习此书的知识,也希望对各位读者有所帮助。
注意:此系列仅讲述Linux系统下的C语言编译

二、目标文件

2.1 ELF格式

我们可以都知道一个程序是加载到系统中去执行,那么这个程序文件我们通常称呼为 可执行文件。比如windows 下 的 exe文件。而 Linux 下则没有后缀,每种系统下的 可执行文件 都有自己的格式,Linux 下的文件为 ELF格式

目标文件 是代码 编译 后但未进行 链接中间文件Linux.o文件 即为 目标文件。从广义上看,目标文件可执行文件 的格式几乎一致,可以看成一种类型的文件。

Linux 使用 ELF格式 存储的文件包括:

  • 可执行文件
  • 静态链接库
  • 动态链接库
  • 目标文件(可定位文件)
  • 核心转储文件(core Dump file)
ELF文件类型 说明 文件
可定位文件 文件包含了 代码数据 ,可被用来链接为 可执行文件 或者 共享目标文件 .a 文件和 .o文件
可执行文件 可以直接被执行,在 Linux 中此类文件没有拓展名 无拓展名
共享目标文件 包含了 代码和数据, 一般分 2种情况。一是与其他 可定位文件共享目标文件 进行链接产生 新的目标文件。二是将多个 共享目标文件可执行文件 结合,作为进程的一部分来运行 .so文件
核心转储文件 当进程意外终止时,系统可以将此进程的地址信息及其他信息存储到该文件中 core dump

2.2 目标文件内容

目标文件 往往包含了许多内容,包括 数据代码链接信息 等,其中 链接信息 包括 符号表调试信息字符串表等等。,根据这些内容组织形式的不同可以分为 2 种形式:

  • section:按照笔者理解,sectionELF文件 的基本单位
  • segment:由多个类型相同的 section 组成
ELF文件视图

在上面的图中,链接视图 有个 节区头部表(section talbe) ,在链接中也称为 段表,存储了所有 section 的信息。而 执行视图 有个 程序头部表(program header),存储了所有 segment 的信息,按照笔者的理解,它也可以称为 段表。两者的区别是在不同视图的不同表现。在下文中都简称为 段表(英语注释),读者可以根据语境判断使用的是哪种段表。

通常情况下,目标文件 包含以下几个常见的 section

  • 代码段(code section):存放编译后的 机器指令
  • 数据段(data section):存放 初始化全局变量局部静态变量
  • bss段(bss section):存放 未初始化全局变量局部静态变量,值得一提的是bss段 并不像 数据段 一样占据硬盘空间。由于 bss段 中变量的默认值为 0,所以 bss仅 记录这些变量的属性,不为这些变量开辟空间。只有等程序运行的时候,就会会 bss段 中的数据分配内存。

一般情况下,会将 数据段bss段 统称为 数据段

代码段数据段 为什么要分开存储?

  • 增加程序安全性
  • 使用cache提高访问速度
  • 节省内存

2.2 ELF文件头

/usr/lib/elf.h 中,我们可以看到下面的数据结构,它描述的就是 ELF文件 的头部。

typedef struct
{
  unsigned char e_ident[EI_NIDENT];     /* Magic number and other info */
  Elf32_Half    e_type;                 /* Object file type */
  Elf32_Half    e_machine;              /* Architecture */
  Elf32_Word    e_version;              /* Object file version */
  Elf32_Addr    e_entry;                /* Entry point virtual address */
  Elf32_Off     e_phoff;                /* Program header table file offset */
  Elf32_Off     e_shoff;                /* Section header table file offset */
  Elf32_Word    e_flags;                /* Processor-specific flags */
  Elf32_Half    e_ehsize;               /* ELF header size in bytes */
  Elf32_Half    e_phentsize;            /* Program header table entry size */
  Elf32_Half    e_phnum;                /* Program header table entry count */
  Elf32_Half    e_shentsize;            /* Section header table entry size */
  Elf32_Half    e_shnum;                /* Section header table entry count */
  Elf32_Half    e_shstrndx;             /* Section header string table index */
} Elf32_Ehdr;

举个例子,读取 ELF头 后输出如下:

ELF头

图片中的输出与结构体是一一对应的:

  • e_ident:对应上图中的红圈内的 5个 属性:
    • MagicELF魔数,用于表示 ELF文件平台属性
      • 1-4字节(7f 45 4c 46)ELF魔数,每个 文件 开头都为这 4个字节
      • 5字节:表示 ELF文件位数0132位0264位
      • 6字节:表示 大小端01小端02大端
      • 7字节:表示 ELF主版本号。一般为 1
  • e_typeELF文件类型,即 可定位文件可执行文件共享目标文件
  • e_machineELF文件 所于宁的机器平台,在图中可以看到为 ARM
  • e_versionELF版本号,一般为 1
  • e_entry:如果为可执行文件,则表示 入口地址,否则该成员为 0
  • e_phoffprogram headers 在文件中的 偏移
  • e_shoff段表(section table headers) 在文件中的 偏移
  • e_flags:用于标识 ELF文件平台 的相关属性
  • e_ehsizeELF文件头 大小
  • e_phentsize:一个 program header 的大小
  • e_phnumprogram header 的数量,一般就是指 segment 的数量
  • e_shentsize:一个 section header 的大小
  • e_shnumsection header 的数量,也就是 section 的数量。
  • e_shstrndx:描述 section talbe字符串段表section table 中的下标。该字段一般指的是 .shstrtab 在段表中的下标

2.3 段表

段表(section table) 用于保存程序各个段的 基本属性段表ELF文件 中的位置由 ELF头e_shoff 成员决定。

段表 本质是一个 Elf32_Shdr 结构体的数组,数组长度等于段的个数。每个 Elf32_Shdr 又被称为 段描述符(Section Descriptor)。一般来说,第一个元素的无效的段描述符,其类型为 NULL。如下图所示:

无效段

该结构体的代码如下:

typedef struct
{
  Elf32_Word    sh_name;                /* Section name (string tbl index) */
  Elf32_Word    sh_type;                /* Section type */
  Elf32_Word    sh_flags;               /* Section flags */
  Elf32_Addr    sh_addr;                /* Section virtual addr at execution */
  Elf32_Off     sh_offset;              /* Section file offset */
  Elf32_Word    sh_size;                /* Section size in bytes */
  Elf32_Word    sh_link;                /* Link to another section */
  Elf32_Word    sh_info;                /* Additional section information */
  Elf32_Word    sh_addralign;           /* Section alignment */
  Elf32_Word    sh_entsize;             /* Entry size if section holds table */
} Elf32_Shdr;
  • sh_name:该成员是一个 下标,标识该段的段名.shstrtab(字符串表) 中的偏移。
  • sh_type段类型,一般有以下的类型:
类型 含义
SHT_NULL 0 无效段
SHT_PROGBITS 1 程序段( 代码段数据段 都是此类型)
SHT_SYMTAB 2 符号表段
SH_STRTAB 3 字符串表段
SHT_RELA 4 重定位段,该段包含了 重定位信息
SHT_HASH 5 符号表段的哈希表段
SHT_DYNAMIC 6 动态链接信息段
SH_NOTE 7 提示性信息
SH_NOBITS 8 该段在文件中无内容,比如bss段
SHT_REL 9 该段也包含了 重定位信息
SHT_SHLIB 10 保留
SHT_DNYSYM 11 动态链接符号表段
  • sh_flags:段标志,一般有以下标志:
标志 含义
SHF_WRITE 1 该段在进程空间中可写
SHF_ALLOC 2 该段在进程空间中需要分配内存空间,一般 代码段、数据段和bss段 都有这种标志
SHF_EXECINSTR 4 标识该段在进程空间中可以被执行,一般是 代码段
  • sh_addr段虚拟地址。如果该段能被加载到内存,则表示该段在内存中的起始虚拟地址。
  • sh_offset段偏移。如果该段存在于程序文件中,则表示该段在文件中的偏移。一般来说 BSS段 的该属性没有意义,因为其不占据文件空间
  • sh_size:段长度
  • sh_link | sh_info:段链接信息,对于需要链接的段(比如重定位段),这2个属性意义如下。如果段不需要进行链接则这2个属性没有意义。
段类型 sh_link sh_info
SHT_DYNAMIC 该段所使用的的 字符串表 在段表中的 下标
SHT_HASH 该段所使用的的 符号表 在段表中的 下标
SHT_REL
SHT_RELA
该段所使用的的 符号表 在段表中的 下标 重定位表 所作用的段在段表中的下标
其他 未定义
  • sh_addralign:表示段是否有 地址对齐 要求,0无要求,1有要求
  • sh_entsize:该段中每一个固定项的长度,该属性为0则表示该段不包含大小固定的项。比如 符号表 中的每一个符号所占大小都一样,那么此时就需要 sh_entsize 来表示每一个符号(项)的大小。

2.4 重定位段

链接器 在处理目标文件时,必须要对目标文件中的某些部位进行 重定位, 即 代码段数据段 使用 绝对地址 的地址。重定位信息记录在 重定位表 中,对于每个要重定位的 代码段数据段 ,都有一个相应的 重定位表。比如代码段 .txt 对应 .rel.txt 这个重定位表。

每个 重定位表 就是 ELF文件 的一个段。后续文章会对 重定位表 进行讲解。

2.5 字符串表段

ELF文件 会使用到很多字符串,比如 段名变量名 等。在 ELF文件 中,引用字符串则给出该字符串在 字符串表 中的偏移即可。
字符串表ELF文件 中常常以 的形式存在。一般有以下2个:

  • .strtab(字符串表):用于保存普通的字符串。比如 符号名
  • .shstrtab(段表字符串标):用于保存段表中用到的字符串,最常见的就是 段名

2.6 符号表

2.6.1 符号

链接 时,要解决的问题就是 目标文件 之间的 地址引用 问题,即对 函数变量 地址的引用。每个函数和变量都有自己独一无二的名字,这样才能避免链接时混淆不同变量和函数。在链接中,将 函数变量 统称为 符号,而 函数名变量名 则是 符号名

整个链接过程都是基于符号来完成,所以对于符号管理是链接时很关键的一个环节。每个 目标文件 都有自己的 符号表符号表 记录了 目标文件 中所用到的所有符号。每个定义的符号都有一个值,即 符号值。一般来说,函数变量符号值 就是地址。

一般情况下,符号有以下几类:

  • 定义本目标文件 中的全局符号
  • 在本目标文件中引用到的 全局符号,却没有定义在 本目标文件中,这一般称为 外部符号
  • 段名,其一般为编译器产生,其值为对应段的 起始地址
  • 局部符号,一般只在编译单元内部可见。其在链接过程中也没有作用,一般链接过程也会忽略掉
  • 行号信息,即 目标文件源代码 中代码行的对应关系。

2.6.2 符号表

符号表ELF文件 中就是一个 ,段名为 .symtab。在代码表现为 Elf32_Sym 结构体数组。 其代码结构体如下:

typedef struct
{
  Elf32_Word    st_name;                /* Symbol name (string tbl index) */
  Elf32_Addr    st_value;               /* Symbol value */
  Elf32_Word    st_size;                /* Symbol size */
  unsigned char st_info;                /* Symbol type and binding */
  unsigned char st_other;               /* Symbol visibility */
  Elf32_Section st_shndx;               /* Section index */
} Elf32_Sym;
  • st_name符号名,该成员表示该符号名在字符串表中的 下标

  • st_value:符号对应的值,一般有如下几种情况:

    • 非COMMON块 中,表示 符号其所在段 中的偏移。
    • COMMON块 中,则表示该符号的 对齐属性
    • 可执行文件 中,表示符号的 虚拟地址
  • st_info:表示 符号类型(低4位)绑定信息(高28位)

    • 绑定信息
    宏定义名 说明
    STB_LOCAL 0 局部符号,对于目标文件的外部不可见
    STB_GLOBAL 1 全局符号,外部可见
    STB_WEAK 2 弱引用
    • 符号类型
    宏定义名 说明
    STT_NOTYPE 0 未知类型符号
    STT_OBJECT 1 该符号为 数据对象,比如变量数组
    ST__FUNC 2 符号是个函数或其他可执行代码
    STT_SECTION 3 该符号表示一个段,此类符号必须是 STB_LOCAL
    STT_FILE 4 该符号表示文件名,一般都是目标文件所对应的 源文件名,一定是 STB_LOACL,且 符号所在段 必须为 SHN_AVS
  • st_shndx:如果 符号 定义在本目标文件中,那么该成员表示 符号所在段段表 中的下标。如果 符号 不是定义在本目标文件中或者其他情况,则如下表:

宏定义名 说明
SHN_ABS 0xfff1 符号包含一个绝对的值,比如 文件名符号
SHN_COMMON 0xfff2 该符号表示 COMMON块 类型,比如 为初始化的全局变量
SHN_UNDEF 0 表示符号未定义,该符号被本文件引用但定义在其他文件中

2.6.3 弱符号和强符号

在编程中常常碰到 符号重复定义,即多个 目标文件 中含有相同名字全局符号的定义。
一般情况下分为 弱符号强符号

  • 弱符号:编译器默认 函数初始化的全局变量强符号
  • 强符号未初始化的全局变量 为弱符号。

一般 GCC编译器 可以使用 attribute_((weak)) 来定义任意的强符号为 弱符号强符号弱符号 都是针对 定义 来说,不是针对符号的 引用

一般链接器在处理 强符号弱符号 时有如下规则:

  • 不允许 强符号 被多次定义。
  • 如果一个符号在某个目标文件中的 强符号,在其他文件中都是 弱符号,那么就选择 强符号
  • 如果一个符号在所有目标中都是 弱符号,那么选择占用空间最大的一个。

2.6.4 弱引用和强引用

  • 强引用:如果没找到符号定义,链接器就会报符号 未定义错误
  • 弱引用:如果该符号有定义,则链接器将该符号的引用进行决议。如果该符号为定义,则链接器不报错。一般对于未定义的弱引用,链接器默认其为0,或者是一个特殊的值。

强引用弱引用 对于库十分有用,如下:

  • 库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数
  • 程序可以对某些扩展工鞥模块的引用定义为弱引用,当我们将扩展模块与程序链接在一起时,功能模块就可以正常使用
  • 如果我们去掉了某些功能模块,那么程序也可以正常链接,只是缺少了相应的功能

你可能感兴趣的:(《程序员的自我修养》笔记1——目标文件)