在学习链接的具体过程前,有必要好好了解一下ELF目标文件。
ELF的目标文件分为三类:
- 可重定位目标文件(.o)
- 其代码和数据可和其他可重定位文件合并为可执行文件
- 每个 .o 文件由对应的 .c 文件生成
- 每个 .o 文件的代码和数据地址都是从0开始的偏移
- 可执行目标文件(默认为a.out)
- 包含的代码和数据可以被直接复制到内存并执行
- 代码和数据的地址是虚拟地址空间中的地址
- 共享的目标文件(.so 共享库)
- 特殊的可重定位目标文件,能在装载到内存或运行时自动被链接,称为共享库文件
可通过objdump命令对比可重定位目标文件和可执行目标文件的不同:
可以看到,确实可重定位目标文件中的地址是从0开始的,而可执行目标文件中的地址是虚拟地址空间中的地址。
接下来介绍ELF文件的两种视图:
- 链接视图:可重定位文件(Relocatable object files)
-
执行视图:可执行目标文件(Executable object files)
image.png
链接视图 —— 可重定位目标文件
来看一个简单的C代码及其所生成的可重定位目标文件的关系图:
如上图,编译后的代码部分放到 .text节,已初始化的全局变量和已初始化的静态变量会放到 .data节,未初始化的全局变量和未初始化的静态变量会放到 .bss节。
实际上,为了进行链接,可重定位目标文件还需要许多其他信息,如符号表、重定位信息等。这些后面会陆续介绍。
在这里要特别说明一下 .bss节。该节在可重定位目标文件中并不占用空间,只是在节头表相应的表项中说明要为 .bss节预留多大的空间。
可重定位目标文件中包含有很多的节,格式如下图:
其中:
- ELF头
包括16字节的标识信息、文件类型(.o,exec,.so)、机器类型(如Intel 80386)、节头表的偏移、节头表的表项大小及表项个数。
- .text节
编译后的代码部分。
- .rodata节
只读数据,如 printf用到的格式串、switch跳转表等。
- .data节
已初始化的全局变量和静态变量。
- .bss节
未初始化全局变量和静态变量,仅是占位符,不占据任何磁盘空间。区分初始化和非初始化是为了空间效率。
- .symtab节
存放函数和全局变量(符号表)的信息,它不包括局部变量。
- .rel.text节
.text节的重定位信息,用于重新修改代码段的指令中的地址信息。
- .rel.data节
.data节的重定位信息,用于对被模块使用或定义的全局变量进行重定位的信息。
- .debug节
调试用的符号表(gcc -g)
- .strtab节
包含 .symtab节和 .debug节中的符号及节名
- 节头表(Section header table)
包含每个节的节名在.strtab节中的偏移、节的偏移和节的大小.
下边分别举例讲一下ELF头和节头表:
-------------------------------------- ELF头 (ELF Header)------------------------------
ELF头位于ELF文件的开始,其包含了文件结构的说明信息。其结构体定义如下:
#define EI_NIDENT 16
typedef struct {
unsigned char e_ident[EI_NIDENT];
uint16_t e_type;
uint16_t e_machine;
uint32_t e_version;
ElfN_Addr e_entry;
ElfN_Off e_phoff;
ElfN_Off e_shoff;
uint32_t e_flags;
uint16_t e_ehsize;
uint16_t e_phentsize;
uint16_t e_phnum;
uint16_t e_shentsize;
uint16_t e_shnum;
uint16_t e_shstrndx;
} ElfN_Ehdr;
用readelf -h 查看ELF头。下表是ELF头中各个成员的含义与readelf输出结果的对照表:
成员 | readelf输出结果及含义 |
---|---|
e_ident | Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 |
e_type | Type: REL(Relocatable file) ELF文件类型 |
e_machine | Machine: Intel 80386 ELF文件的CPU平台属性,相关常量以EM_开头 |
e_version | Version: 0x1 ELF版本号。一般为常数1 |
e_entry | Entry point address: 0x0 入口地址,规定ELF程序的入口虚拟地址,操作系统在加载完该程序后从这个地址开始执行进程的指令。可重定位目标文件一般没有入口地址,则这个值为0 |
e_phoff | Start of program header: 0 (bytes into file) 程序头表在文件中的偏移,可重定位目标文件不存在程序头表,故该值为0 |
e_shoff | Start of section header: 280(bytes into file) 节头表在文件中的偏移 |
e_flags | Flags: 0x0 ELF标志位,用来标识一些ELF文件平台相关的属性,相关常量的格式一般为EF_machine_flag,machine为平台,flag为标志 |
e_ehsize | Size of this header: 52(bytes) 即ELF文件头本身的大小 |
e_phentsize | Size of program headers: 0(bytes) 程序头表表项的大小 |
e_phnum | Number of program headers: 0 程序头表表项的数目 |
e_shentsize | Size of section headers: 40(bytes) 节表表项的大小 |
e_shnum | Number of section headers: 11 节表表项的数目 |
e_shstrndx | Section header string table index: 8 节表字符串表在节头表中的下标 |
-------------------------------------- 节头表(Section Header Table) ----------------------
除了ELF头之外,节头表是ELF可重定位目标文件中最重要的部分内容。
它描述了每个节的节名、在文件中的偏移、大小、访问属性、对齐方式等。其32位结构定义如下(每项占40字节):
typedef struct {
Elf32_Word sh_name; //节名字符串在.strtab中的偏移
Elf32_Word sh_type; //节类型:无效/代码或数据/符号/字符串/…
Elf32_Word sh_flags; //节标志:该节在虚拟空间中的访问属性
Elf32_Addr sh_addr; //虚拟地址:若可被加载,则对应虚拟地址
Elf32_Off sh_offset; //在文件中的偏移地址,对.bss节而言则无意义
Elf32_Word sh_size; //节在文件中所占的长度
Elf32_Word sh_link; //sh_link和sh_info用于与链接相关的节(如
Elf32_Word sh_info; // .rel.text节、.rel.data节、.symtab节等)
Elf32_Word sh_addralign; //节的对齐要求
Elf32_Word sh_entsize; //节中每个表项的长度,0表示无固定长度表项
} Elf32_Shdr;
使用 readelf -S命令查看节头表,示例如下:
A(alloc)标志表示该节将进程空间中必须要分配空间。可以看到,.text、.data、.bss、.rodata节都有这个标志。
执行视图 —— 可执行目标文件
再来看ELF的执行视图,也就是可执行目标文件的格式:
它与可重定位目标文件稍有不同:
- ELF文件头中的字段e_entry给出了执行程序时第一条指令的地址,而在可重定位目标文件中,此字段为0;程序头表的偏移e_poff和大小e_phentsiz和程序头表项的个数e_phnum不为0;
- 多了一个程序头表,也称为段头表(segment header table),是一个结构体数组;
- 多了一个 .init节,用于定义 _init函数,该函数用于在可执行目标文件开始执行时的初始化工作。
- 少了两个 .rel节(.rel.text和.rel.data),因为可执行目标文件已经在链接的过程中完成了重定位,已无须重定位。
使用readelf -h 来看可执行目标文件的ELF头,示例如下
程序头表描述的可执行文件中的节(section)与虚拟地址空间中的存储段(segment)之间的映射关系。
程序头表的结构定义如下:
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; //段的标志位,表示访问权限(Read|Write|Exec)
Elf32_Word p_align; //段在内存中的对齐要求
} Elf32_Phdr;
下图是用readelf -l 查看到的某可执行目标文件的程序头表:
Type为Load表示可装入内存的段。
第一个可装入段:第0x00000~0x004d3字节(包括ELF头、程序头表、.init节、.text节和.rodata节),映射到虚拟地址0x8048000开始长度为0x4d4字节的区域,按0x1000=4KB对齐,具有只读/执行权限(Flg=RE),是只读代码段。
第二个可装入段:第0x000f0c~开始长度为0x108字节的 .data节,映射到虚拟地址0x8049f0c开始长度为0x110字节的存储区域,在0x110=272B存储区中,前0x108=264B用 .data节内容初始化,后面272-264=8B对应.bss节,初始化为0,按0x1000=64K对齐,具有可读可写权限(Flg=RW),是可读写数据段。
映射到虚拟地址空间中如图: