可执行程序的生成过程
可执行文件的生成要经过预处理、编译、汇编和链接四个步骤。
其中,对于链接过程,其主要任务是完成符号解析(symbol resolution) 和 重定位(relocation)
其本质就是将程序中具有相同特征的节合并:
ELF文件类型
在Linux中,上面提到的可重定位文件和可执行文件使用的文件格式是ELF。ELF指的是Executable and Linkable Format,可执行链接格式,是一种二进制文件格式。
ELF有几种目标文件类型:
- 可重定位文件(relocatable file):就是通常说的目标文件,属于源文件编译后但还未完成链接的半成品,例如
gcc -c
选项编译出来的.o文件,其代码和数据可和其他可重定位文件合并成可执行文件,且代码和数据的都从0开始 - 共享目标文件(shared object file):特殊的可重定位目标文件,能在装载或运行时加载到内存并连接,linux下文件后缀为so,windows下文件后缀为dll。
- 可执行文件(executle file):经过编译连接,代码和数据可以直接装载到内存并被执行,代码和数据地址为虚拟地址空间中的地址。
- 核心转储文件(Core Dump File):在程序意外终止时,系统可以将该进程的地址空间的内容及终止时的一些其他信息转储到核心转储文件,比如linux下的core dump。
段和节
程序中最重要的部分就是段(segment)和节(section),它们是真正的程序体。程序中有很多段,如代码段和数据段,同样也有很多节,段是由节来组成的,多个节经过链接之后就被合并成一个段了。
段和节分别通过节头表和程序头表引用:
ELF文件的两种视图
ELF文件分为链接视图和执行视图,链接视图中以节为处理单位,执行视图中会将具有相同特征的节合并成一个段。
ELF文件头
先看ELF文件头的结构,ELF文件头定义了ELF魔数、版本、小端/大端、操作系统平台、目标文件的类型、机器结构类型、程序执行的入口地址、程序头表(段头表)的起始位置和长度、节头表的起始位置和长度等。
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* 魔数和一些其他信息 */
Elf64_Half e_type; /* 目标文件类型:可重定位目标文件/可执行文件/共享目标文件 */
Elf64_Half e_machine; /* 目标文件所属的体系结构 */
Elf64_Word e_version; /* 目标文件版本 */
Elf64_Addr e_entry; /* 程序的入口虚拟地址 */
Elf64_Off e_phoff; /* 程序头表在文件内的字节偏移量 */
Elf64_Off e_shoff; /* 节头表在文件内的字节偏移量 */
Elf64_Word e_flags; /* 与处理器相关的标志 */
Elf64_Half e_ehsize; /* 指明elf头的字节大小 */
Elf64_Half e_phentsize; /* 程序头表中每个条目的字节大小 */
Elf64_Half e_phnum; /* 程序头表中条目的数量,实际就是段的个数 */
Elf64_Half e_shentsize; /* 节头表中每个条目的字节大小 */
Elf64_Half e_shnum; /* 节头表中条目的数量,实际就是节的个数 */
Elf64_Half e_shstrndx; /* 指明字符串表在节头表中的索引 */
} Elf64_Ehdr;
下面是一个可重定位ELF头信息的解析示例(32位的目标文件):
可重定位目标文件
一个典型的可重定位目标文件的格式如下:
其中节头表定义如下:
typedef struct
{
Elf64_Word sh_name; /* 节名字符串在字符串表.strtab的索引 */
Elf64_Word sh_type; /* 节类型:代码或数据/符号/字符串 */
Elf64_Xword sh_flags; /* 节标志:访问属性 */
Elf64_Addr sh_addr; /* 节的虚拟地址,若不可加载则为0 */
Elf64_Off sh_offset; /* 节在文件中的偏移量,对.bss节来说无意义 */
Elf64_Xword sh_size; /* 节在文件中占的大小 */
Elf64_Word sh_link; /* sh_link和sh_info用于和链接相关的节(如.rel.text节、.rel.data节、.symtab节等) */
Elf64_Word sh_info;
Elf64_Xword sh_addralign; /* 节的对齐要求 */
Elf64_Xword sh_entsize; /* 节中每个表项的长度,0表示无固定长度表项 */
} Elf64_Shdr;
下面是节头表的信息举例(32位目标文件):
以下是一些比较重要的节:
.text
:已编译程序的机器代码。
.rodata
:只读数据,比如 printf
语句中的格式串和开关语句的跳转表。
.data
:已初始化的全局和静态 C 变量。局部 C 变量在运行时被保存在栈中,既不岀现在 .data
节中,也不岀现在 .bss
节中。
.bss
:未初始化的全局和静态 C 变量,以及所有被初始化为 0 的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分已初始化和未初始化变量是为了空间效率:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。运行时,在内存中分配这些变量,初始值为 0。
.symtab
:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。一些程序员错误地认为必须通过 -g
选项来编译一个程序,才能得到符号表信息。实际上,每个可重定位目标文件在 .symtab
中都有一张符号表(除非程序员特意用 STRIP
命令去掉它)。然而,和编译器中的符号表不同,.symtab
符号表不包含局部变量的条目。
.rel.text
:一个 .text
节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。注意,可执行目标文件中并不需要重定位信息,因此通常省略,除非用户显式地指示链接器包含这些信息。
.rel.data
:被模块引用或定义的所有全局变量的重定位信息。一般而言,任何已初始化的全局变量,如果它的初始值是一个全局变量地址或者外部定义函数的地址,都需要被修改。
.debug
:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的 C 源文件。只有以 -g
选项调用编译器驱动程序时,才 会得到这张表。
.line
:原始 C 源程序中的行号和 .text
节中机器指令之间的映射。只有以 -g
选项调用编译器驱动程序时,才会得到这张表。
.strtab
:一个字符串表,其内容包括 .symtab
和 .debug
节中的符号表,以及节头部中的节名字。字符串表就是以 null
结尾的字符串的序列。
符号与符号表
每个可重定位目标模块 m
都有一个符号表,它包含 m
定义和引用的符号的信息。在链接器的上下文中,有三种不同的符号:
在ELF文件中,.symtab
节记录了符号表的相关信息:
每个符号都被分配到目标文件的某个节,由 st_shndx
字段表示,该字段也是一个到节头部表的索引。有三个特殊的伪节(pseudosection),它们的st_shndx
字段在节头部表中没有对应条目:
ABS
:代表不该被重定位的符号;UNDEF
:代表未定义的符号,也就是在本目标模块中引用,但是却在其他地方定义的符号;COMMON
表示还未被分配位置的未初始化的数据目标。对于COMMON
符号,value
字段给出对齐要求,而size
给出最小的大小。注意,只有可重定位目标文件中才有这些伪节,可执行目标文件中是没有的。
其中COMMON
和.bss
的区别很细微,现在GCC按如下规则将可重定位目标文件中的分配到COMMON
和.bss
中:COMMON
:未初始化的全局变量.bss
:未初始化的静态变量,以及初始化为0的全局或静态变量
下面是一个符号表的信息解读示例:
可执行目标文件
一个典型的可执行文件的目标格式如下:
其中段头表的定义如下:
typedef struct
{
Elf64_Word p_type; /* 段类型 */
Elf64_Word p_flags; /* 段的权限属性 */
Elf64_Off p_offset; /* 段在文件中的偏移量 */
Elf64_Addr p_vaddr; /* 段在虚拟地址空间中的起始位置 */
Elf64_Addr p_paddr; /* 段的装载地址一般与vaddr一致 */
Elf64_Xword p_filesz; /* 段在文件中的大小 */
Elf64_Xword p_memsz; /* 段在进程虚拟地址中大小 */
Elf64_Xword p_align; /* 段的对齐属性 */
} Elf64_Phdr;
一个可执行文件的程序头解析示例如下:
参考资料
计算机系统基础(二):程序的执行和存储访问_南京大学_中国大学MOOC(慕课) (icourse163.org)
第 3 章:程序的机器级表示 | 深入理解计算机系统(CSAPP) (gitbook.io)
Linux二进制分析-ELF格式分析及符号重定位 - bunner - 博客园 (cnblogs.com)
《程序员的自我修养——装载链接与库》