编译过程和elf文件
本文介绍二进制文件编译过程和elf文件。
编译过程
编译过程:可分为五个阶段:
- 1.词法分析
- 2.语法分析
- 3.语义分析
- 4.中间代码生成和优化
- 5.代码生成和优化
gcc编译过程
gcc编译过程主要包括四个:
- 1.预处理
- 2.编译
- 3.汇编
- 4.链接
使用了cc1、as、collect2三个工具,最后生成的是(静态|动态)的可执行文件
- 生成的汇编代码中函数printf()被替换成puts(),因为当printf()只有单一参数时与puts()十分相似,gcc的优化策略酒将其替换以便提高性能。
ELF(可执行可链接)文件格式
ELF文件可以分为三种类型:
- 可执行文件(.exec)
- 可重定位文件(.rel)
- 共享目标文件(.dyn)
1.可执行文件(executable file):经过链接、可执行的目标文件,通常也被称为程序。
2.可重定向文件(relocatable file):由原文件编译而成且尚未链接的目标文件,通常以”.o”作为扩展名。用于与其他目标文件进行链接以构成可执行文件或动态链接库,通常是一段位置独立的代码(Position Independent Code,PIC)。
3.共享目标文件(shared object flie):动态链接库文件。用于在链接过程中与其他动态链接库或可重定向文件一起构建新的目标文件,或者在可执行文件加载时,链接到进程中作为运行代码的一部分。
除了上面说的三种类型,核心转储文件(Core Dmp file)作为进程意外终止时进程地址空间的转储,也是ELF文件的一种。
使用gdb读取这类文件可以辅助调试和查找程序崩溃的原因。
ELF文件结构
ELF文件被统称为Object file。后面提到目标文件时,指各种ELF文件。
(.o)文件直接称为可重定向文件,由于这类文件包含了代码和饿数据,可以被用于链接成可执行文件或者共享目标文件。
查看目标文件时候有两种视角可以选择:
- 链接视角,通过节(Section)来划分
- 运行视角,通过段(Segment)来划分
链接视角
通常目标文件都会包含:代码(.text)、数据(.data)、BSS(.bss)这3个节,其中代码节用于保存可执行的机器指令,数据节用于已初始化的全局变量和局部静态变量,BSS节则用于保存未初始化的全局变量和局部静态变量。
除了上述的三个节,简化的目标文件应包含一个文件头(ELF header)。
将程序指令和数据分开的好处是:从安全的角度上,当程序加载后,数据和指令分别被映射到两个虚拟区域,由于数据区域对进程来说是可读可写的,而指令区域对进程来说是只读的,所以这两个虚存区域的权限可以被分别设置成可读写和只读,方杂志程序的指令被改写和利用。
ELF文件头
ELF文件头(ELF header)位于目标文件最开始的位置,包含描述整个文件的一些基本信息:ELF文件类型、版本/ABI版本、目标机器、程序入口、段表和节表的位置和长度等。
文件头部存在魔术字符(7f 45 4c 46),即字符串”\177ELF”,当文件被映射到内存是,可以通过搜索该字符确定映射地址。
节头表
一个目标文件包含了许多节,这些信息都保存在节头表中,表的每一项都是一个Elf64_Shdr(也称节描述符),记录了节的名字、长度、偏移、读写权限等信息。节头表的位置记录在文件头的e_shoff域中。节头表对于程序运行不是必须的,应为它与程序内存布局无关,是程序头表的任务,所以常用程序去除节头表,以便增加反编译的难度。
代码节
代码节(.text)
objdump -x -s -d xxxx(文件名)
Content of section .text部分是.text数据的十六进制形式。
- 3、Size表示总共有多少个字节。
- 1、Idx表示偏移量
- 2、Name表示节的名称
- 4、VMA和LMA在中间表示内容
Disassembly of section .text部分则是反编译的结果。
数据节
数据节(.data) 只读数据节(.rodata)
数据节(.data)可以看到已经保存的初始化的全局变量和局部静态变量
Content of section .data
global_init_var(0a000000)和local_static_init_var(14000000),每一个变量四个字节
只读数据节(.rodata)可以看到已经保存的只读数据,包括只读变量和字符串常量。
源代码调用printf()函数时,用到了一个”%d\n”,他是一种只读数据。
Content of section .rodata
可以看到字符串常量的ASCII形式,以’\0’结尾。
BSS节
用于保存未初始化的群居变量和局部静态变量。
常用的表
- 字符串表:包含以null结尾的字符序列用来表示符号名和节名,引用字符串时只需要给出字符序列在表中的便宜偏移即可,字符串表的第一个字符和最后一个字符都是null字符,以确保所有字符串都有开始和终止。
- 符号表:记录了目标文件中所用到的所有符号信息,通常为.dynsym和.symtab,前者是后者的子集。在.dynsym中保存了引用自外部文件的符号,只能在运行是被解析,而在.symtab中还保存了本地符号,用于调试和链接。目标文件通过一个符号在表中的索引值来使用该符号,索引值从0开始计数,但值为0的表项不具有实际的意义,他表示为定义的符号。每个符号都有一个符号值(symbol value)。对于常量和函数,该值就是符号的地址。
- 重定位是链接符号定义和符号引用的过程。可重定位文件在构建可执行文件或共享目标文件时,需要把节中的符号引用换成这些符号在进程中虚拟地址,包含这些转换信息的数据就是重定位项(relocation entries)
在Elf64_Rel和Elf64_Rela结构体中:
- r_offset是在重定向时需要被修改的符号偏移。
- r_info分为两个部分:type指示如何修改引用,symbol指示应该修改引用为那个符号。
- r_addend用于对被修改的引用做偏移调整。
运行视角
当执行一个可执行文件时,首先需要将该文件盒动态链接库装载到进程空间中,形成一个进程镜像,每个进程都有独立的虚拟地址空间,这个空间如何布局是由记录在段头表中的程序头决定的。
常用的段
通常一个可执行文件至少有一个PT_LOAD类型的段,用于描述可装载的段,而动态链接的可执行文件则包含两个,将.data 和.text分开存放。
动态段PT_DYNAMIC包含了一些动态链接器所必须段信息,如共享库列表、GOT表和重定向表等。
PT_NOTE类型的段保存了系统相关的附加信息,虽然程序运行并不需要这些。
PT_INTERP段将位置信息和大小信息存放在一个字符串中,是对程序解释器位置的描述。
PT_PHDR段保存了程序头表本身的位置和大小。
静态链接
编译时链接、加载时链接、运行时链接
将两个目标文件链接成一个可执行文件时使用两种方法:按序叠加、相似节合并
为了构造可执行文件,链接器必须完成两个工作:符号解析(symbol resolution)和重定向(relocation)
- 符号解析就是将每个符号(函数、全局变量、静态变量)的引用于其定义进行关联。
- 重定向就是将每个符号的定义与一个内存地址进行关联,然后修改这些符号的引用,使其指向这个内存地址。
1.使用objdump可以查看文件各个节的详细信息:
objdump -h main.o
objdump -h func.ELF
VMA(Virtual Memory Address)是虚拟地址,LMA(Load Memoey Address)是加载地址。
2.使用odjdump可以查看文件的反汇编代码,参数”-mi386:intel”表示以Intel格式输出。
odjdump -d -M intel –section=.text main.o
3.可重定向文件中最重要的就是包含重定位表,用于告诉链接器如何修改节的内容,每一个重定位表对应一个需要被重定位的节
odjdump -r main.o
名为.rel.text的节用于保存.text节的重定位表,同时.rel.text包含两个重定位入口。
shared的类型R_X86_64_32C用于绝对地址,CPU将直接使用在指令中编码的32位值作为有效地址。
func的类型R_X86_64_PC32用于相对地址,CPU将指令中编码的32位值加上PC(下一个指令地址)的值得到有效地址。
func-0x000000000000004中的-0x4是r_addend域中的地址。
4.后缀名为.a的文件是静态链接库文件,常见的有libc.a
一个静态链接库可以视为一组目标文件经过打包后形成的集合。执行后编译任务时,需要许多不同目标文件,比如输入输出有printf.o、scanf.o,内存管理malloc.o等。使用ar工具将这些目标文件进行了压缩、编号和索引,形成了libc.a
命令:ar -t libc.a
静态链接等缺点
- 随着系统中的可执行文件的增加,静态链接带来的磁盘和内存空间浪费问题越发严重
- 如果对标准函数做了一点微小的改动,都需要重新编译整个源文件,使得开发和维护很难
动态链接
把系统库和自己编写代码分割成两个独立的模块,等程序真正运行时,再把泽两个模块链接,就可以节省磁盘空间,并且内存中等一个系统库可以被多个程序共同使用,还节省物理内存空间,这种在运行或加载时在内存中完成链接等过程叫做动态链接,这些用于动态链接等系统库称为共享库或者共享对象,整个过程由动态链接器完成。
gcc参数:-shared表示生成共享库,-fpic表示生成与位置无关的代码
位置无关代码
可以加载而无须重定向的代码称为位置无关代码(Position-Independent Code,PIC),他是共享库必须具有的属性,通过给GCC传递-fpic参数可以生成。
通过位置无关代码,一个共享库的代码可以被无限多个进程所共享,从而节约内存资源。
由于一个程序或者共享库的数据段和代码段段相对距离总是不变的,因此指令和变量之间的距离是一个运行时常量,与绝对内存地址无关,于是就有了全局偏移量表(Global Offset Table,GOT),他位于数据段的开头,用于保存全局变量和库文件的引用,每个条目占8个字节,在加载时会进行重定向并填入符号的绝对位置。
为了引入RELRO保护机制,GOT被拆分为.got节和.got.plt节两个部分:
不需要延迟绑定的前者(.got)用于保存全局变量引用,加载到内存后被标记为只读。
需要延迟绑定的后着(.got.plt)用于保存函数引用,具有读写权限。
延迟绑定
由于动态链接时由于动态链接器在程序加载时,当需要重定向的符号(库函数)多了之后会影响性能,延迟绑定(lazy binding)就是为了这个问题,基本思想时当函数第一次被调用时,动态链接器材进行符号查找、重定位操作,如果未被调用则不进行绑定。
ELF文件通过过程链接表(Procedure Linkage Table,PLT)和GOT当配合来实现延迟绑定,每一个被调用的库函数都有一组对应的PLT和GOT。