从编程语言到可执行程序

一、编译器是如何工作的?

        什么是编译器?

        编译器是一个将高级语言翻译为低级语言的程序,我们一定要意识到编译器就是一个普通程序,只不过从复杂程度上来讲,编译器这个程序的复杂度更高而已。

        世界上所有编程语言都是遵照特定语法来编写的,编译器根据该语言的语法将代码解析成语法树,遍历语法树先生成机器指令等,然后交给CPU(或者虚拟机)来执行。

        (1)编译器首先需要把每个符号切分出来,并把该符号与其所附带的信息打包起来。

        (2)编译器根据语法解析出来的“结构”用语法树来进行表达,这个过程被称为语法分析。

        (3)有了语法树之后,还需要去判断语法树是否合理。

        (4)根据语法树生成中间代码。语义分析之后,编译器遍历语法树并用另一种形式来表示。

        (5)代码生成。编译器将上述中间代码转换为汇编指令,最后,编译器将上述汇编指令转换为机器指令,就这样编译器把人类认识的一串被称为代码的字符转换成了CPU可以执行的指令。

 二、链接器是如何工作的?

        与编译器一样,链接器也是一个普通程序,它负责把编译器产生的一堆目标文件打包成最终的可执行文件,就像压缩程序把一堆文件打包成一个压缩包一样。

        假设有源文件fun.c,那么该文件被编译后会生成对应的fun.o,该文件保存的就是代码对应的机器指令,该文件就被称为目标文件。我们见到的所有应用程序,小到自己实现的“helloworld”程序,大到复杂的如浏览器、网络服务器等,这些应用程序都是由链接器通过将一个个所需用到的目标文件汇集起来最终形成的。

        代码中存在重定位问题,假设某个源代码引用了其他模块定义的print()函数,那么编译器在编译该源文件时根本不知道该函数到底会被放到哪个内存地址上,这时编译器也仅仅用'N’来代替,当链接器汇总合并生成可执行文件时就能知道该函数的确切地址了,这时再把N替换成真正的内存地址。

        以上就是链接器工作过程中的几个重要阶段:符号决议、生成可执行文件与重定位。

1、符号决议 

        符号是指什么呢?指的就是变量名,这包括全局变量名和函数名,由于局部变量是模块私有的,不可以被外部模块引用,因此链接器不关心局部变量。

        链接器在这一步需要做的工作就是确保所有目标文件引用的外部符号都有定义,该定义必须是唯一的。

        变量可以分为两部分:

       (1)局部变量。局部变量是函数私有的,外部不可见,你没有办法在代码中引用其他模块的局部变量,因此链接器对此类变量并不感兴趣。

       (2)全局变量:全局变量可以被其他模块引用。

        链接器真正感兴趣的是这里的全局变量,他必须知道这样两个信息:(1)该文件有几个符号可以供其他模块使用;(2)该文件引用了几个其他模块定义的符号。

 2、静态库、动态库与可执行文件

        静态库在Windows下是以.lib为后缀的文件,在Linux下是以.a为后缀的文件,可以将基础架构团队的代码单独编译打包,并对外提供一个包含所有实现函数的头文件。

        利用静态库,可以把一堆源文件提前单独编译链接成静态库,注意是提前且可以单独编译。

在生成可执行文件时只需要编译你自己的代码,并在链接过程中把需要的静态库复制到可执行文件中,这时就不需要编译项目依赖的外部代码了,从而加快项目编译速度,这个过程就是静态链接。

        可以简单地将静态链接理解为将目标文件集合进行拼装,并将各个目标文件中的数据区、代码区合并起来。

        缺陷:静态链接会将用到的库直接复制到可执行文件中,但对于printf和scanf这样的标准库,在静态链接下生成的所有可执行文件中都有一份一样的代码和数据,这是对硬盘和内存的极大浪费。

        动态库,又叫共享库、动态链接库等。在Windows下,就是我们常见的DLL文件,Windows系统大量使用了动态库,在Linux下动态库是以.so为后缀的文件,同时以lib为前缀。

        假如我们有两个源文件,a.c与b.c,希望打包成动态库foo,那么在Linux下可以通过如下命令生成动态库:

gcc -shared -fPIC -o libfoo.so a.c b.c

        从名字上我们知道动态库也是库,本质上动态库也同样包含我们已经熟悉的代码区、数据区等,只不过动态库的使用方式和使用时间与静态库不太一样。

        当使用静态库时,静态库的代码区和数据区都会被直接打包复制到可执行文件中。

        当使用动态库时,可执行文件中仅仅包含关于所引用动态库的一些必要信息,如所引用动态库的名字、符号表及重定位信息等,而不需要像静态库那样将该库的内容复制到可执行文件中,这一点尤其重要,与静态库相比这无疑将减小可执行文件的大小。

        动态链接有两种可能出现的场景:

        第一种场景,在程序加载时进行动态链接,这里的加载指的是可执行文件的加载,其实就是把可执行文件从磁盘搬到内存的过程,因为程序最终都是在内存中被执行的,系统中有一个特定的程序专门负责程序的加载,这个程序被称为加载器。

        加载时进行动态链接需要我们把可执行文件依赖哪些动态库这一信息明确地告诉编译器,如我们有一个源文件main.c,依赖了动态库libfoo.so,想生成一个叫做pro的可执行文件,就可以用下面这个命令达到目的。

gcc -o pro main.c /path/to/libfoo.so

        第二种场景,除了在加载期间进行动态链接,我们还可以在程序运行期间进行动态链接。运行时指的是从程序开始被CPU执行到程序执行完成退出的这段时间。

        由于在生成可执行文件的过程中没有提供所依赖的动态库信息,因此这项任务就留给了程序员。程序员可以在编写程序时使用特定的API来根据需求动态加载指定动态库,如在Linux下可以通过使用dlopen、dlsym、dlclose这样一组函数在运行时链接动态库。

        动态库还有另外的强大之处,那就是如果修改了动态库的代码,我们只需要重新编译动态库即可,而不需要重新编译依赖该动态库的程序,因为可执行文件当中仅仅保留了动态库的必要信息,只需要简单地用新的动态库替换原有动态库即可,下一次程序运行时就可以使用最新的动态库了。

        动态库的优点不止于此,我们知道动态链接可以出现在程序运行时,动态链接的这种特性可以方便地用于扩展程序能力,如何扩展呢?插件-》首先我们可以提前规定好几个函数,所有的插件只要实现这几个函数就行,然后这些插件以动态库的形式供主程序调用,只要提供新的动态库,主程序就会有新的能力。

        动态库在程序加载时或运行时才进行链接,同静态链接相比,使用动态链接的程序在性能上要削弱于静态链接。动态库中的代码是地址无关代码,之所以动态库中的代码是地址无关的,是因为动态库在内存中只有一份,但该动态库在内存中又可以被其他依赖此库的进程共享,因此动态库的代码不能依赖任何绝对地址,绝对地址是一个写定的数值,就像这条调用foo函数的指令:

call 0x4004d6 # 调用foo函数

        函数地址0x4004d6这个值就是绝对值,动态库中显然不能有这样的指令,因为动态库加载到不同的进程中其所在的地址空间是不同的。地址无关就是无论在哪个进程中调用foo函数我们都能找到该函数正确的运行时地址,这种地址无关的设计会导致在引用动态库的变量时会多一点“间接寻址”。

        如果没有提供所依赖的动态库或者所提供的动态库版本与可执行文件所依赖的不兼容,那么程序是无法启动的。

3、重定位:确定符号运行时地址

        此处略,前面有提到

你可能感兴趣的:(开发语言,c++)