程序的链接——目标文件格式

目标文件格式

前言:在讲链接之前,我们先得说说,可重定位目标文件格式和可执行目标文件格式

采用的是 ELF 标准二进制文件格式进行说明

可重定位目标文件格式

整个文件格式如下:

名称
ELF 头
.text 节
.rodata 节
.data 节
.bss 节
.symtab 节
.rel.text 节
.rel.data 节
.debug 节
.line 节
.strtab 节
节头表
  • ELF 头
#define EI_NIDENT 16

typedef struct {
        unsigned char   e_ident[EI_NIDENT];
        Elf32_Half      e_type;
        Elf32_Half      e_machine;
        Elf32_Word      e_version;
        Elf32_Addr      e_entry;
        Elf32_Off       e_phoff;
        Elf32_Off       e_shoff;
        Elf32_Word      e_flags;
        Elf32_Half      e_ehsize;
        Elf32_Half      e_phentsize;
        Elf32_Half      e_phnum;
        Elf32_Half      e_shentsize;
        Elf32_Half      e_shnum;
        Elf32_Half      e_shstrndx;
} Elf32_Ehdr;

一共 52 个字节。咋算的呢?Elf32_Half 开头的就是 16 位两个字节

也就是:

16 + 2 + 2 + 4 + 4 + 4 + 4 + 4 + 6 * 2 = 52

每个的作用如下

成员 含义
e_ident[EI_NIDENT] 前 4 个字节成为魔数字,通常用来确定文件的类型和格式
e_type 文件类型:可重定位、可执行、共享库
e_machine 机器结构类型:如 IA-32
e_version 文件版本
e_entry 程序起始虚拟地址,如可重定位文件就是 0
e_phoff 程序头表的偏移(在可执行目标文件中才有)
e_shoff 节头表的偏移(字节为单位)
e_flags
e_ehsize ELF 头大小(字节为单位)
e_phentsize 程序头表大小(在可执行目标文件中才有)
e_phnum 程序头表项数(在可执行目标文件中才有)
e_shentsize 节头表中一个表项的大小(每个表项大小一致,字节为单位)
e_shnum 节头表的项数
e_shstrndx .strlab 节在节头表的索引

每种可执行文件的格式的开头几个字节都是很特殊的,特别是开头 4 个字节,通常被称为魔数(Magic Number)。通过对魔数的判断可以确定文件的格式和类型。如:ELF 的可执行文件格式的头 4 个字节为0x7Felf;Java的可执行文件格式的头 4 个字节为cafe;如果被执行的是 Shell 脚本或 perl、python 等解释型语言的脚本,那么它的第一行往往是 #!/bin/sh#!/usr/bin/perl#!/usr/bin/python,此时前两个字节 #! 就构成了魔数,系统一旦判断到这两个字节,就对后面的字符串进行解析,以确定具体的解释程序路径。

假设你有这样一个 main.o 的文件,可以用 readelf 看它的 ELF 头

readelf -h main.o

节是 ELF 文件中的主体信息,包含了链接过程中所用的目标代码信息,包括指令、数据、符号表和重定位信息。

节名称 用途
.text 节 目标代码部分(二进制)
.rodata 节 只读数据,如 printf 中的格式串、开关语句的跳转表
.data 节 已经初始化的全局变量
.bss 节 未初始化的全局变量。由于是未初始化,所以无需在当前目标文件中分配用与保存值的空间。而对于局部变量来说,运行时被分配在栈中所以既不出现在 .bss 节中也不会出现在 .data 节中
.symtab 节 符号表,程序中定义的函数名和全局静态变量名都属于符号,与这些符号相关的信息都保存在符号表中。每个可重定位目标文件都有一个 .symtab 节
.rel.text 节 .text 节相关的重定位信息。通常,调用外部函数或者引用全局变量的指令中的地址字段需要修改
.rel.data 节 .data 节相关的可重定位信息。
.debug 节 调试用符号表
.line 节 源程序中的行号和 .text 节中的机器指令之间的映射
.strtab 节 字符串表,包括 .symtab 和 .debug 节中的符号以及节头表中的节名。
  • 节头表
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 shdebugging sym_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;

大小 40B = 4 × 10B。

命令:readelf -S main.o

可执行目标文件格式

链接器将相互关联的可重定位目标文件中的相同代码和数据节(.text 节,.rodata 节,.data 节,.bss 节)合并。因为合并后,所有的虚假地址都能被计算出来。也就能算出每个符号的地址。

可执行目标文件格式包括:

  • ELF 头
  • 程序头表
  • 节头表
名称
ELF 头
程序头表
.init、.fini 节
.text 节
.rodata 节
.data 节
.bss 节
.symtab 节
.debug 节
.line 节
.strtab 节
节头表

与可重定义目标文件格式类似,主要不同点有:

  • ELF 头中 e_entry 不再是 0。而是执行代码的第一条指令的地址
  • 多了 .init 节和 .fini 节,其中 .init 节中定义了一个 _init 函数,用于可执行目标文件执行时初始化工作。.fini 包含进程终止时要执行的指令代码
  • 少了 .rel.text 和 .rel.data 节等重定位信息节。
  • 多了一个程序头表也叫作段头表

整个文件有两个重要的段

  • 只读代码段:(ELF 头 + 程序头表 + .init .fini 节 + .text 节 + .rodata 节 )
  • 可读写数据段:(.data 节 + .bss 节),由于在执行文件时这两个段必须分配空间所以又可以叫做 可装入段

下面隆重介绍程序头表:

为了在可执行文件执行时能够访在内存中访问到代码和数据,必须将可执行文件中这些连续的,具有相同访问属性的代码和数据段映射到存储空间(通常是虚拟地址)。程序头表就用于描述这种映射关系,一个表项对应一个连续的存储段或特殊节。

typedef struct {
        Elf32_Word      p_type;
        Elf32_Off       p_offset;
        Elf32_Addr      p_vaddr;
        Elf32_Addr      p_paddr;
        Elf32_Word      p_filesz;
        Elf32_Word      p_memsz;
        Elf32_Word      p_flags;
        Elf32_Word      p_align;
} Elf32_Phdr;

成员 含义
p_type 短的类型或者节的类型,列入是否为可装入段
p_offset 本段的首字节在文件中的偏移地址
p_vaddr 本字段的虚拟地址
p_paddr 本段首字节的物理地址
p_filesz 本段所占字节数
p_memsz 在存储器中所占字节数
p_flags 存取权限
p_align 对齐方式

输入 readelf -l main

程序头:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x00000034 0x00000034 0x00120 0x00120 R   0x4
  INTERP         0x000154 0x00000154 0x00000154 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x00000000 0x00000000 0x00730 0x00730 R E 0x1000
  LOAD           0x000edc 0x00001edc 0x00001edc 0x0012c 0x00130 RW  0x1000
  DYNAMIC        0x000ee4 0x00001ee4 0x00001ee4 0x000f8 0x000f8 RW  0x4
  NOTE           0x000168 0x00000168 0x00000168 0x00044 0x00044 R   0x4
  GNU_EH_FRAME   0x0005d0 0x000005d0 0x000005d0 0x00044 0x00044 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10
  GNU_RELRO      0x000edc 0x00001edc 0x00001edc 0x00124 0x00124 R   0x1

这里只是大概介绍了一些知识,具体怎么运用要看接下来的文章。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容