编译与链接

编译与链接

代码运行背后的过程

经典C语言程序:


#include<stdio.h>

int main()
{
    printf("hello world!\n");
    return 0;
}

Linux下,使用gcc来编译这个程序,进入命令行输入:


gcc hello.c

可以看到文件下面多出了一个a.out文件:

Linux的.out是由gcc编译生成的二进制格式文件,但有可能是执行程序也可能是链接库文件,因为在linux中扩展名除了一些特殊的约定,一般情况下是无意义的。在使用gcc编程时,没有指定输入可执行文件名,默认生成可执行文件a.out文件

图1.png

然后接着执行指令:


./a.out

输出:

图2.png

当然你也可以指定自己想要的输出文件的文件名字

使用如下指令:


gcc hello.c -o hello.out

结果:

图3.png

上面的过程可以分解为4个步骤:

  • 预处理

  • 编译

  • 汇编

  • 链接

GCC编译过程分解图示

图4.png

预编译

源代码文件hello.c与头文件,如stdio.h等被预编译器cpp预编译成一个.i文件

对应指令:


gcc -E hello.c -o hello.i

执行后:

图5.png

处理规则:

  • 处理关于 “#” 的指令

  • 删除#define,展开所有宏定义。例#define portnumber 3333

  • 处理条件预编译 #if, #ifdef, #if, #elif,#endif

  • 处理“#include”预编译指令,将包含的“.h”文件插入对应位置。这可是递归进行的,文件内可能包含其他“.h”文件。

  • 删除所有注释。/**/,//。

  • 添加行号和文件标识符。用于显示调试信息:错误或警告的位置。

  • 保留#pragma编译器指令。(1)设定编译器状态,(2)指示编译器完成一些特定的动作

经过预编译后的.i文件不包含任何宏定义,因为所有的宏已经被展开了,并且包含的文件也已经被插入到.i文件

编译

编译就是把预处理完的文件进行一系列词法分析,语法分析,语义分析以及优化后生成相应的汇编代码文件

执行编译过程的指令:


gcc -S hello.i -o hello.s

执行后:

图6.png

得到汇编文件hello.s

汇编

汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应着一条机器指令

执行汇编过程的命令:


gcc -c hello.s -o hello.o

执行后:

图7.png

得到目标文件hello.o

链接

为何要链接?汇编器为何不直接输出可执行文件而是输出一个目标文件?此处留下疑问

编译器做了哪些工作

编译过程是整个程序构建的核心部分,编译成功,会将源代码由文本形式转换成机器语言,编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件。

编译过程可以分6步:

  • 扫描

  • 语法分析

  • 语义分析

  • 源代码优化

  • 代码生成

  • 目标代码优化

图示:

图8.png

词法分析

词法分析是使用一种叫做lex的程序实现词法扫描,它会按照用户之前描述好的词法规则将输入的字符串分割成一个个记号。产生的记号一般分为:关键字、标识符、字面量(包含数字、字符串等)和特殊符号(运算符、等号等),然后他们放到对应的表中。

语法分析

语法分析器根据用户给定的语法规则,将词法分析产生的记号序列进行解析,然后将它们构成一棵语法树。对于不同的语言,只是其语法规则不一样。用于语法分析也有一个现成的工具,叫做:yacc。

语义分析

语法分析完成了对表达式语法层面的分析,但是它不了解这个语句是否真正有意义。有的语句在语法上是合法的,但是却是没有实际的意义,比如说两个指针的做乘法运算,这个时候就需要进行语义分析,但是编译器能分析的语义也只有静态语义。

静态语义:在编译期就可以确定的语义。通常包括声明与类型的匹配、类型的转换。比如当一个浮点型的表达式赋值给一个整型的表达式时,其中隐含一个从浮点型到整型的转换,而语义分析就需要完成这个转换,再比如,将一个浮点型的表达式赋值给一个指针,这肯定是不行的,语义分析的时候就会发现两者类型不匹配,编译器就会报错。

动态语义:只有在运行期才能确定的语义。比如说两个整数做除法,语法上没问题,类型也匹配,听着好像没毛病,但是,如果除数是0的话,这就有问题了,而这个问题事先是不知道的,只有在运行的时候才能发现他是有问题的,这就是动态语义。

中间代码生成

我们的代码是可以进行优化的,对于一些在编译期间就能确定的值,是会将它进行优化的,比如说上边例子中的 2+6,在编译期间就可以确定他的值为8了,但是直接在语法上进行优化的话比较困难,这时优化器会先将语法树转成中间代码。中间代码一般与目标机器和运行环境无关。(不包含数据的尺寸、变量地址和寄存器的名字等)。中间代码在不同的编译器中有着不同的形式,比较常见的有三地址码和P-代码。

中间代码使得编译器可以分为前端和后端。编译器前端负责产生于机器无关的中间代码,编译器后端将中间代码换成机器代码。

目标代码生成与优化

代码生成器将中间代码转成机器代码,这个过程是依赖于目标机器的,因为不同的机器有着不同的字长、寄存器、数据类型等。

最后目标代码优化器对目标代码进行优化,比如选择合适的寻址方式、使用唯一来代替乘除法、删除出多余的指令等。

到现在,经历了这么多步,源代码终于翻译成了目标代码,但此时出现了一个问题:

假设目标代码中我们定义了变量,如:


int index;

int[] array;

那么index与array的地址从哪里得到?

如果目标代码中有变量定义在其他模块,那怎么确定?

最终,定义在其他模块的全局变量以及函数在最终运行时的绝对地址都要在最终链接的时候才能确定,所以现代的编译器可以将一个源代码文件编译成一个未链接的目标文件,然后由链接器最终将这些目标文件链接起来形成可执行文件。

初识链接

计算机发展初期,写程序都是使用机器语言,一条指令一条指令的书写。

比如:


0 0001 0100

1 ........

2 ........

3 ........

4 1000 0111

5 .......

上面是一段程序,但是程序写好之后可能很大概率还是要修改的,如在上面第一条指令之后,第五条指令之前插入n条指令,那么第五条指令以及后面的指令的位置需要相应的后移,那么可能就需要重新计算子程序或者其他涉及的目标地址。

这种重新计算各个目标的地址过程就叫重定位

但是这种方法太低效了,所以就出现了汇编语言,使用各种接近人类的符号和标记来帮助记忆。使用符号的方法可以使得人们从具体的指令地址中解放出来

假设上面的二进制指令中,第1条指令的作用是跳转到第5条指令,那么使用汇编可以把第五条指令开始的子程序命名为“foo”,那么第一条指令对应的汇编就是:

jmp foo

使用这种符号的方式后,不管这个“foo”之前插入或者减少了多少条指令导致“foo”目标地址发生了任何变化,汇编器每次汇编程序的时候会重新计算“foo”这个符号的地址,然后把所有引用到“foo”的指令修正到正确地址。无需人工参与。

随着软件开发规模的庞大,我们开始将代码按照功能或性质进行划分,形成不同的功能模块,不同模块之间按照层次结构或者其他来组织。比如C语言中,最小的单位是变量和函数,若干个变量和函数组成一个模块,存放在一个“.c”的源代码文件里面,然后这些源代码文件按照目录结构来组织

一个程序被分割成多个模块之后,模块之间最后如何组合形成一个单一的程序是一个问题。模块之间如何组合的问题归结为模块之间的如何通信的问题,最常见的属于静态语言的C/C++模块之间通信有两种方式:

  • 模块间的函数调用

  • 模块间的变量访问

函数访问需要知道目标函数的地址,变量访问也需要知道目标变量的地址,所以这两种方式都可以归结为一种方式:模块间符号的引用

模块间依靠符号来通信类似于拼图版,定义符号的模块多出了一块区域,引用该符号的模块刚好少了那一块区域,两者以拼接刚好完美组合。这个模块的拼接过程就是—链接

模块拼装—静态链接

对于复杂软件,人们把每个源代码模块独立的编译,然后按照需要将它们“组装”起来,这个组装模块的过程就是链接

链接的过程主要包括:

  • 地址和空间分配

  • 符号决议

  • 重定位

图示:

图9.png

每个模块的源代码文件(如.c)文件经过编译器编译成目标文件(后缀为.o或者.obj),目标文件和库(Library)一起链接形成最终可执行文件

链接过程

比如在程序模块main.c中使用了另外一个模块func.c中的函数foo(),在main.c模块中每一处调用foo的时候都必须确切的知道foo这个函数的地址,但是由于每个模块都是单独编译的,在编译器编译main.c的时候它并不知道foo函数的地址,所以它暂时把这些调用foo的指令的目标地址搁置,等待最后链接的时候由链接器去将这些指令的目标地址修正。

假设没有链接器,那么我们就需要手工的把每个调用foo的指令进行修正,再填入正确的foo函数地址,当func.c函数被重新编译时,foo函数的地址可能会发生改变,那么在main.c中所有使用到foo的地址的指令将要全部重新调整,这无疑将成为噩梦。

有了链接器,可以直接引用其他模块的函数和全局变量而无需知道它们的地址,因为链接器在链接的时候会根据你所引用的符号foo,自动的去相应的func.c模块查找foo的地址,然后将main.c模型所有引用到foo的指令重新修正,让它们的目标地址为真正的foo函数的地址,这就是静态链接最基本的过程和作用

参考资料

<<程序员的自我修养—链接、装载与库>>

https://blog.csdn.net/guaiguaihenguai/article/details/81160310

发布了227 篇原创文章 · 获赞 62 · 访问量 19万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览