在这里,我想任何人做编程相关的人都应该至少接触过某种编程语言,接触过程序的编译,执行过自己源代码产生的可执行文件。对于可执行文件我想不得不提需要关心的应该直接首先是两个:

1.可执行文件的产生

2.可执行文件的运行

可执行文件的产生:

可执行文件是什么?

目标机器可执行的一段指令流和所用到的数据流。更确说,其实应该还有一些额外的信息(我们暂时称它为文件信息),但这些在不同的情况下有所差异,我们可以暂时不考虑,比如在裸机下我们往特定地址烧录的.bin(不包含额外的信息),cpu是可执行的。在linux系统下的elf文件(包含很多系统运行时所需的文件相应信息),也是可执行的。还有我们大学时期往8位51单片机烧写的.hex(包含特定的地址信息),总的说来,文件信息都是更好地为指令流在特定环境下顺利执行服务的。

什么是目标机器可执行

简单来说,就是目标机器cpu能够正确地把一串0和1解析成正确的指令,再简单来说就是译码单元可解析。实际我们cpu完成了从机器码到自身可执行具体指令的转换(这是需要对应的具体硬件电路有相应实现支持的)。

可执行文件的详细产生过程

这里以C语言为例,这里简单阐述高级语言到可执行文件的整个过程。

预处理:

比如文件名叫a.c,首先我们会将.c文件进行预处理,预处理是将很多宏做展开,条件编译的处理,去掉注释信息,头文件的插入。这里不多赘述。

编译:

我们会根据C语言规则做词法分析,语法分析,语义分析,机器无关中间代码产生与优化,机器相关目标代码的产生与优化。这里我都把每个流程用一句话去简单说明。

词法分析:

把程序语句拆分成一个一个token。比如 a=b+5;   token: a ,= , b, +, 5, ;,就把一个句子拆成一个一个词汇

语法分析:

比如a=+;

这就过不了语法分析,这里 =赋值符两边的token ,a +的性质不同。

语义分析:

float a =1.1111;

int *b = a;

语法分析只完成了表达式层次的语法检查,但是并不知道表达式是否有意义。这里把int类型的指针赋值做浮点的强制转换,这就是符合语法分析,但是不符合语义分析的。

机器无关中间代码的产生与优化:

这里是将语法树转换为中间代码,比如三地址码,其实就是将树形结构转换为顺序的一条一条表达式结构。但是又没有具体到具体的寄存器和指令,这里是可优化但是又机器无关的。这也是常说的编译器的前端。

机器相关目标代码的产生与优化:

这里是把中间代码转换到汇编,优化之后,转换为目标机器代码,具体到具体的寄存器,具体的硬件支持。比如不同机器的寄存器数量不同,有的有乘法运算单元有的没有,等等。

链接:

链接我想用一个比喻来说明,假设可执行文件的产生是一个拼积木图的过程。比如我们拼一个老虎,我们编译实际就是区组装这个老虎的各个部分,比如它的头,它的尾巴.....当我们拿到已经拼好的各个部位的时候,这其实并不是一副完整的拼图也不是一只完整的老虎,我们还需要最后一步,去把每个部分与其他部分衔接拼成一只完整的老虎,这种衔接页跟积木拼接一样需要严丝合缝,不是简单的把老虎身子放在头的下面就能草草了事,这个严丝合缝的衔接过程就是链接,(图纸就是链接脚本)由此也可以看出链接的本质——把所有的东西放在它该放的位置。编译阶段最多只能在某个模块的内部,把它放在相对这个模块的正确位置,比如眼睛相对头的位置肯定是可以确定的,但是头在整副图中的绝对位置,眼睛在整副图中绝对位置在编译阶段是无法确定的。

上面打了个比喻,个人感觉很贴切,这里还是比较客观的给出链接阶段所完成的事情:

符号决议

简单来说,符号决议简单来说,就是若干个中间文件根据各自符号表去他人表内找到唯一存在的自己所需的外部符号的定义(当然定义肯定不是定义在表内,只是记录在案)。当我第一次看到符号表的时候,我感觉豁然开朗,因为我突然明白,a文件怎么在b文件去找到它要的东西,符号表是在中间文件生成的,也就是编译时生成,虽然符号决议没有在编译时完成,但是这个符号表却是链接阶段能够完成符号决议的关键因素,实在是太巧妙了,这也回答了static 修饰的变量为什么能够做到外部不可访问,该变量在符号表中的属性可以去做限制。也看到别人总结的很好:

本质上整个符号表只是想表达两件事:

我能提供给其它文件使用的符号

我需要其它文件提供给我使用的符号

段整合和代码的重定位

将各个中间文件的对应内容根据链接脚本放在对应的位置(对应的段中),也就是说可执行文件的各个段是怎么由中间文件组成的。确定程序运行时的地址,因为在中间文件中所使用的函数和全局变量地址在运行时的最终地址我们在编译时是无法确定的,通常链接时会给出,通过参数或者链接脚本指定,我们一般会在中间文件中先暂时把地址设置为0,然后把这些需要重定位的函数和变量记录在某些段中(实际上是两个rl.txt rl.data),最后我们会去修正这些地址(在指令机器码中修正)。

地址和空间分配

这里我感觉是指的是划分每段的分布位置还有大小,就经常在链接脚本里看见的那样,全局变量,函数的地址分配。只有分配了,才能有确定的地址,有了确定的地址才能去做修正。

总结:

在总结完这些后,我更加惊讶于计算机前辈们的无处不在的睿智,而且一直以来,我自己觉得计算机问题,应该是先问自己,再参考别人。就是当我们需要做到某个功能时,你会怎么去做,所有的解决方法和理论都是伴随问题诞生的。这里只是简单讨论了一个可执行程序的编译链接过程,程其中很多重要的细节没有讲到。我会在后续的文章中继续讨论,这也是在博客园的第一篇博客,希望自己能一直坚持写下去。

09-28 10:58