一、编译、链接和装载:拆解程序执行
C 语言代码,可编译成汇编代码,汇编器变成 CPU 可以理解机器码,CPU执行机器码。C 语言程序是如何变成可执行程序。
通过 gcc 生成的文件和 obj dump 获取到的汇编指令都有些小小的问题。我们先把前面的 add 函数示例,拆分成两个文件 add_lib.c 和link_example.c。
我们通过 gcc 来编译这两个文件,然后通过 obj dump 命令看看它们的汇编代码。
$ gcc -g -c add_lib.c link_example.c
$ obj dump -d -M intel -S add_lib.o
$ obj dump -d -M intel -S link_example.o
既然代码已经“编译”成了指令,运行一下./link_example.o。文件没有执行权限Permission denied 错误。赋予权限,./link_example.o仍 cannot execute binary file: Exec format error 的错误。
看obj dump 出来代码,两个程序的地址都是从 0 开始。如果地址一样,程序如果通过 call 指令调用函数的话,怎么知道应该跳转到哪一个文件里呢?
无论运行报错,还是地址重复,都因为 add_lib.o 以及link_example.o 并不是一个可执行文件(Executable Program),而是目标文件(Object File)。只有通过链接器(Linker)把多个目标文件以及调用的各种函数库链接起来,我们才能得到一个可执行文件。
gcc 的 -o 参数,生成对应可执行文件,对应执行之后,得函数的结果。
$ gcc -o link-example add_lib.o link_example.o
$ ./link_example
c = 15
二、“C 语言代码 - 汇编代码 - 机器码” 由两部分组成:
(1)编译(Compile)、汇编(Assemble)、链接(Link)生成可执行文件。
(2)通过装载器(Loader)把可执行文件装载(Load)到内存中。CPU 从内存中读取指令和数据,真正执行程序。
ELF格式和链接:理解链接过程
程序最终是通过装载器变成指令和数据,生成可执行代码不仅是一条条的指令。还是通过 ob jdump 指令,可执行文件的内容拿出来看:
可执行代码 dump 出来内容,之前的目标代码长得差不多,但长了很多。在 Linux 下,可执行文件和目标文件所使用的都是一种叫ELF(Execuatable and Linkable File Format)的文件格式,中文名字叫可执行与可链接文件格式,存汇编指令(编译成)和别的数据。
所有 obj dump 出来的代码里,可以看到对应的函数名称,像 add、main 等等,乃至你自己定义的全局可以访问的变量名称,都存在 ELF 里。名字和它们对应的地址,ELF 里存储符号表(Symbols Table)里(相当于地址簿,关联名字和地址)
add 的跳转地址,不再是下一条指令的地址了,是 add 函数入口地址,这就是 EFL 格式和链接器的功劳。
ELF 把信息,分成一个个 Section 存起来。文件头(File Header):文件的基本属性,是否是可执行文件,对应的 CPU、操作系统等。
1. text Section:代码段或者指令段(Code Section),保存代码和指令;
2. data Section:数据段(Data Section),初始化数据信息
3. rel.text Secion:重定位表(Relocation Table)。当前的文件里面,哪些跳转地址其实是我们不知道的。比如link_example.o调用 add 和 printf,链接发生之前,并不知道该跳转到哪里,这些信息就会存储在重定位表里;
4.symtab Section:符号表(Symbol Table)。当前文件里面定义的函数名称和对应地址的地址簿。
链接器:扫描所有输入的目标文件,收集符号表里信息,构成全局符号表。根据重定位表,把不确定跳转地址代码,根据符号表里面存储的地址修正。把目标文件合并,变成可执行代码。这也是为什么,可执行文件里面的函数调用的地址都是正确的。
装载器执行程序。不需要考虑地址跳转的问题,只需解析 ELF 文件,把对应的指令和数据,加载到内存里面CPU 执行。
总结延伸
为什么同样一个程序, Linux 下可执行 Windows 不能。格式不一样(可执行文件)。
(1)Linux 下ELF 文件格式, Windows 可执行文件格式叫作PE(Portable Executable Format)。Linux 下的装载器只能解析 ELF 格式不能解析 PE。
能解析 PE 格式装载器,就可 Linux 下运行 Windows 程序。例:Wine(Linux 下)兼容 PE 格式的装载器,Windows 里也提供了 WSL(Windows Subsystem for Linux) ,可解析加载 ELF 格式的文件。
(2)我们不仅是把代码放在文件里编译执行,可拆分成不同函数库,通过一个静态链接的机制,文件之间:有分工,有合作(通过静态链接),变成可执行程序。
ELF 文件,为了能够实现静态链接的机制,罗列程序所需要执行指令,包括链接所需要的重定位表和符号表。
课后思考
想要更深入了解程序的链接过程和 ELF 格式,我推荐你阅读《程序员的自我修养——链接、装载和库》的 1~4 章。
通过 readelf 读取出今天演示程序的符号表,看看符号表里都有哪些信息;然后通过 objdump 读取出今天演示程序的重定位表,看看里面又有哪些信息。