程序简史
–程序的编译、链接、加载和运行
目标
对于程序员而言,我们将一堆写满代码的文本文件通过IDE或者编译链,最终形成一个可执行文件,而后丢到运行平台上让其行使其最终的使命。那么?程序如何从那一堆文本文件,变为最终的可执行文件?又如何在某平台开始运行?本文旨在从相对宏观的角度,忽略具体实现细节,描述一下程序的诞生以及运行的流程。
概要
以最常用的c语言为例,我们将整个程序的生命周期分为两个部分:一、程序的产生 二、程序的运行。
程序的产生
如何将一堆h文件和c文件最终编译成可执行文件,主要经历以下阶段。
step1:预处理阶段:预处理阶段主要处理预处理指令,主要行为包括头文件展开、宏展开、条件编译、删除注释、添加行号和文件名标识以及保留#pragma预处理命令。
step2:编译阶段:编译阶段主要将各个c文件编译为汇编文件。
step3:汇编阶段:该阶段主要将汇编指令翻译为机器码以及完成一些伪指令的翻译。
step4:链接阶段:该阶段主要将c文件生成的各个二进制文件组装为最终的可执行文件。
具体的实现流程下文将详细介绍。
程序的运行
可执行文件生成后,只是一堆二进制的数据,要想要真正发挥作用,则需要加载器将其加载到内存(或可以地址随机访问的flash)中,然后指定pc指针和sp指针,cpu才会开始取指,译码和运行。
程序的编译
无论是汇编阶段之后的二进制文件还是最终经过链接器的可执行文件,其格式类似,都为elf文件。elf文件格式如下:
程序的链接
程序的链接过程主要完成以下三件事
1.分段组装:elf文件和汇编文件有些类似,都有段的概念,包括代码段、数据段、只读数据段和bss段等,该阶段主要将各个二进制文件的各个段组装在一起。
2.符号决议:多个c文件有时候会出现函数名或者全局变量名冲突的情况,为解决该矛盾,引入强符号和若符号的概念。
强符号:函数名以及初始化的全局变量为强符号
弱符号:未初始化的全局变量为弱符号。
强弱不可并存,也就是说同一符号的函数名和全局变量不可同时定义。需要注意的是,编译器提供了__attribute__关键字在定义时主动将函数名或全局变量名设置为若符号的方式。
多个弱符号可以并存,存在多个弱符号时,认定定义内存最大的弱符号有效。
对于强弱符号的使用,比如通过全局变量的符号去访问其地址中的值,或通过函数符号去访问其地址中指向的指令,该过程称之为应用。与强弱符号相对应,变有强引用与弱引用。对于强引用,链接过程中找不到对应的强符号,就会报未定义错误,对于弱引用,链接过程找不到弱符号,则不会报错,该特性常用于三方库的制作。
3.重定位:由于汇编过程是对各个c文件进行的,每个c文件中的全局变量和函数对应的地址都是相对于零地址的偏移,连接器将各个c文件的目标文件分段重组后各个段的起始地址都发生了偏移,对应的全局变量和函数地址也应当发生偏移,由于所有的符号地址均存在于符号表中,所以该过程也称之为修表,也就是符号的重定位,至于重定位的具体实现过程,在此不做赘述。
程序的加载和运行
对于程序员来说,程序的加载和运行才是和我们平时工作息息相关的,所以也将是本文的重点介绍内容。
1.对于带操作系统的平台例如linux:程序原本存储在掉电非易失的存储介质flash中,需要运行时则先由加载器将其从flash中加载到内存中,然后封装成进程,从而开始参与到系统的调度,大概的加载流程大家可参考下图
那么:加载器怎么知道如何加载该程序?如果这个可执行程序的指令集为x86而非arm指令集,那么是否可以运行?
我们前面已经知道,可执行文件是一个elf文件,elf文件中携带了一部分校验信息和说明信息用于帮助加载器将程序加载到对应的内存并开始运行。
2.对于裸机系统,加载过程稍微复杂一些。一些常见的嵌入式设备比如stm32更是有着分散加载的概念。也就是将代码段留在flash中直接运行,而将数据段放在ram中。具体加载配置可以参考ide或者工具链的链接脚本。
注意:flash的类型有很多种,比如norflash、nandflash、emmc、ssd等等。但只有部分flash满足地址随机访问要求的才能直接将代码段放在flash中直接运行。