sudo apt install gcc2.1 从源代码到可执行文件
编译原理
编译过程可大致分为下面5个步骤,如图2-1所示。
(1)Lexical analysis:输入源程序的字符流,输出为有意义的Lexeme
(2)Syntax analysis:根据各个词法单元的第一个分量来创建树型的中间表示形式,通常Syntax tree
(3)Semantic analysis:使用语法树和符号表中的信息,检测源程序是否满足语言定义的语义约束,同时收集类型信息,用于代码生成、类型检查和类型转换。
(4)中间代码生成和优化:根据语义分析输出,生成类机器语言的中间表示,如三地址码。然后生成的中间代码进行分析和优化
(5)代码生成和优化:把中间表示形式映射到目标机器语言
gcc编译过程
➜ 2 cat hello.c
#include <stdio.h>
int main() {
printf("hello, world\n");
}然后使用如下的编译命令
gcc hello.c -o hello -save-temps --verbose
// -save-temps: 于将编译过程中生成的中间文件 保存下来
// --verbose: 查看GCC编译的详细工作流程GCC 的编译主要包括四个阶段,即预处理(Preprocess)、编译(Compile)、汇编 (Assemble)和链接(Link),编译的过程中分别使用了cc1, as, collect2三个工具。
// prepocess hello.c -> hello.i
/usr/libexec/gcc/x86_64-linux-gnu/13/cc1 ... hello.c ... -o hello.i
// Compile hello.i -> hello.s
/usr/libexec/gcc/x86_64-linux-gnu/13/cc1 ... hello.i ... -o hello.s
// Assemble hello.s -> hello.o
as -v --64 -o hello.o hello.s
// Link hello.o -> hello
/usr/libexec/gcc/x86_64-linux-gnu/13/collect2 ... hello.s -o helloELF文件格式
ELF文件分为三种类型,可执行文件(.exec)、 可重定位文件(.rel)和共享目标文件(.dyn)
➜ elf_file vim elfDemo.c
➜ elf_file gcc elfDemo.c -o elfDemo.exec
➜ elf_file gcc -static elfDemo.c -o elfDemo_static.exec
➜ elf_file gcc -c elfDemo.c -o elfDemo.rel
➜ elf_file gcc -c -fPIC elfDemo.c -o elfDemo_pic.rel && gcc -shared elfDemo_pic.rel -o elfDemo.dyn
➜ elf_file file elfDemo*
elfDemo.c: C source, ASCII text
elfDemo.dyn: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=38817e43eefbff82f8214521c3758ee0b3d2e0aa, not stripped
elfDemo.exec: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=8ee4c2bb0d039bd9ab9a3eeb8753312945e3596f, for GNU/Linux 3.2.0, not stripped
elfDemo_pic.rel: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
elfDemo.rel: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
elfDemo_static.exec: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=b258bdce62a64b9dbcf8addef70d72fe38fd47ed, for GNU/Linux 3.2.0, not stripped可执行文件(executable file):经过链接的、可执行的目标文件,通常也被称为程序。
可重定位文件(relocatable file):由源文件编译而成且尚未链接的目标文件,通常以“.o”作 为扩展名。用于与其他目标文件进行链接以构成可执行文件或动态链接库,通常是一段位置 独立的代码(Position Independent Code, PIC)。
共享目标文件(shared object file):动态链接库文件。用于在链接过程中与其他动态链接库或 可重定位文件一起构建新的目标文件,或者在可执行文件加载时,链接到进程中作为运行代 码的一部分。
除了上面三种主要类型,核心转储文件(Core Dump file)作为进程意外终止时进程地址空间的 转储,也是ELF文件的一种。使用gdb读取这类文件可以辅助调试和查找程序崩溃的原因。
File Header
审视一个目标文件,有两种视角可供选择:
一种是链接视角,通过Section来进行规分,Program header table optional
一种是运行视角,通过Segment来进行划分,Section header table optional
代码节用于保存可执行的机器指令
数据节用于保存已初始化的全局变量和局部静态变量
BSS节则用于保存未初始化的全局变量和局部静态变量
00 2e \x00 .
位置无关代码
可以加载而无须重定位的代码称为位置无关代码(Position-Independent Code, PIC),它是共享库 必须具有的属性,通过给GCC传递-fpic参数可以生成PIC。通过PIC,一个共享库的代码可以被无 限多个进程所共享,从而节约内存资源。
由于一个程序(或者共享库)的数据段和代码段的相对距离总是保持不变的,因此,指令和变 量之间的距离是一个运行时常量,与绝对内存地址无关。于是就有了全局偏移量表(Global Offset Table, GOT),它位于数据段的开头,用于保存全局变量和库函数的引用,每个条目占8个字节,在 加载时会进行重定位并填入符号的绝对地址。
实际上,为了引入RELRO保护机制,GOT被拆分为.got节和.got.plt节两个部分,不需要延迟 绑定的前者用于保存全局变量引用,加载到内存后被标记为只读;需要延迟绑定的后者则用于保存 函数引用,具有读写权限(详情请查看4.6节)。
我们看一下func.so的情况,可以看到全局变量tmp位于GOT上,R_X86_64_GLOB_DAT表示 需要动态链接器找到tmp的值并填充到0x200fd8。在func()函数需要取出tmp时,计算符号相对PC 的偏移rip+0x20090f,也就是0x6c9+0x20090f=0x200fd8。
延迟绑定
由于动态链接是由动态链接器在程序加载时进行的,当需要重定位的符号(库函数)多了之后,
势必会影响性能。延迟绑定(lazy binding)就是为了解决这一问题,其基本思想是当函数第一次被调用时,动态链接器才进行符号查找、重定位等操作,如果未被调用则不进行绑定。
ELF 文件通过过程链接表(Procedure Linkage Table, PLT)和 GOT 的配合来实现延迟绑定,每
个被调用的库函数都有一组对应的PLT和GOT。
位于代码段.plt节的PLT是一个数组,每个条目占16个字节。其中PLT[0]用于跳转到动态链接
器,PLT[1]用于调用系统启动函数__libc_start_main(),我们熟悉的 main()函数就是在这里面调用的,从PLT[2]开始就是被调用的各个函数条目。
位于数据段.got.plt 节的GOT也是一个数组,每个条目占8个字节。其中GOT[0]和GOT[1]包含动态链接器在解析函数地址时所需要的两个地址(.dynamic 和 relor 条目),GOT[2]是动态链接器ld-linux.so的入口点,从GOT[3]开始就是被调用的各个函数条目,这些条目默认指向对应PLT条目的第二条指令,完成绑定后才会被修改为函数的实际地址。
以func.ELF2 调用库函数func()为例。可以看到,执行call指令会进入func@plt,第一条jmp指
令找到对应的GOT条目,这时该位置保存的还是第二条指令的地址,于是执行第二条指令push,将
对应的0x1(func 在.rel.plt 中的下标)压栈,然后进入 PLT[0]。PLT[0]先将GOT[1]压栈,然后调用GOT[2],也就是动态链接器的_dl_runtime_resolve()函数,完成符号解析和重定位工作,并将 func()的真实地址填入func@got.plt,也就是GOT[4],最后才把控制权交给func()。延迟绑定完成后,如果再调用func(),就可以由func@plt的第一条指令直接跳转到func@got.plt,将控制权交给func()。
评论(0)
暂无评论