目标文件格式
前言:在讲链接之前,我们先得说说,可重定位目标文件格式和可执行目标文件格式
采用的是 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 个字节为0x7F
、e
、l
、f
;Java的可执行文件格式的头 4 个字节为c
、a
、f
、e
;如果被执行的是 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
这里只是大概介绍了一些知识,具体怎么运用要看接下来的文章。