编译链接的过程
编译就是把文本形式源代码翻译为机器语言形式的目标文件过程。
链接是把目标文件、操作系统的启动代码和用到的库文件进行组织最终形成可执行代码的过程。
对于GCC来说,编译源代码并最终形成可执行的二进制文件,分为以下四个步骤:
- 预处理。在该阶段,编译器将C源代码中的包含的头文件如stdio.h编译进来。使用GCC的选项”-E”,生成“.i”文件。
- 编译阶段。在这个阶段中,Gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,GCC把代码翻译成汇编语言。使用”-S”选项,该选项只进行编译而不进行汇编,生成汇编代码,即“.s”文件。
- 汇编阶段。汇编阶段是把编译阶段生成的”.s”文件转成二进制目标代码,生成“.o”文件。
- 链接阶段。将“.o”文件链接成最终可执行文件。
如图所示:
ELF文件格式
ELF(Executable and Linkable Format)即可执行和可链接的格式,是一个目标文件格式标准。
目标文件有三种:
可重定向文件
。文件保存着代码和适当的数据,用来和其他的目标文件一起来创建一个可执行文件或者是一个共享目标文件。(目标文件或者静态库文件,即linux通常后缀为.a和.o的文件)可执行文件
。文件保存着一个用来执行的程序,例如bash,gcc等,一般由多个可重定位文件结合生成。共享目标文件
。共享库。文件保存着代码和合适的数据,用来被下连接编辑器和动态链接器链接。(linux下后缀为.so的文件)
ELF格式的文件用于存储Linux程序,是一种对象文件的格式,用于定义不同类型的对象文件中都有什么内容、以什么样的格式放这些内容。一般的ELF文件包括三个索引表:
ELF header
。在文件的开始,保存了路线图,描述了该文件的组织情况。Program header table
。告诉系统如何创建进程映像。用来构造进程映像的目标文件必须具有程序头部表,可重定位文件不需要这个表。Section header table
。包含了描述文件节区的信息,每个节区在表中都有一项,每一项给出诸如节区名称、节区大小这类信息。用于链接的目标文件必须包含节区头部表,其他目标文件可以有,也可以没有这个表。
ELF文件结构如图所示:
可以使用readelf
命令相关选项查看文件的elf形式,如图所示:
动态链接
动态链接区别于静态链接,不会在编译链接时将需要执行的代码直接复制带最终可执行文件中,而是通过记录一系列符号和参数,在程序运行或加载时将这些信息传递给操作系统,由操作系统将需要的动态库加载到内存中。动态链接行程的可执行文件较小,但对系统环境依赖度高。
动态链接分为可执行程序装载时动态链接和运行时动态链接。
装载时动态链接
使用命令gcc -shared shlibexample.c -o libshlibexample.so -m32
编译装载时链接动态库libshlibexample.so。
运行时动态链接
使用命令gcc -shared dllibexample.c -o libdllibexample.so -m32
编译运行时链接动态库libdllibexample.so。
运行测试
使用命令gcc example.c -o example -L ./ -l shlibexample -ldl -m32
编译测试main函数,注意将当前目录加入库文件搜索目录。
cgdb跟踪分析execve
和之前跟踪调试系统调用的步骤一样,在menuOS中进行调试。断点设置如下:
首先停在sys_execve处:
然后是load_elf_binary,按s可以细致的看到程序内部的流程:
停在start_thread处,分析发现将new_ip设置成hello的入口地址,并将new_ip赋值给regs_ip:
总结
可以看到对于装载时动态链接和运行时动态链接的代码,运行时动态链接不需要包含头文件,但需要在代码中做一系列的处理,这些处理一方面显得有些繁琐,有些影响程序可读性;另一方面也增加了出错的可能。但是在装载时加载过多库又会造成程序启动缓慢。
我们在日常生活打开电脑应用时也经常遇到某个dll文件不存在而导致应用打不开的情况,这些dll文件都是装载时链接的吗?还是说应用在装载时也会对一些必要的、运行时链接的库做是否存在的检查?在计算机存储空间比较富足的现在,装载时链接相比静态链接似乎已经没什么优势了?