《程序员的自我修养:链接、装载与库》

  • 程序为什么要被编译之后才可以运行?
  • 编译器在将源代码转换成可以执行的机器码的过程中做了什么?
  • 编译出来的可执行文件里面是什么?
  • include 是什么意思?
  • 不同的编译器、不同的操作系统,最终编译出来的结果一样吗?
  • 如果没有操作系统,Hello World可以运行吗?
  • …………

这本书的内容就是为了回答上面这些问题,不同于教科书式的排版,个人感觉写的很流畅,有对程序运行整体流程的描述,细节的讲解也很深入。如果在大学时,能够阅读一些这样的非教科书类的书籍,我想我对计算机的兴趣以及对计算机的理解,都会有相当大的提高。也推荐对这方面有兴趣或有疑问的同学买来一读,因为代码不多,所以我看的是kindle版。

这篇文章是我在第一次不是特别细致的阅读完这本书后,对之前有些模糊或者我觉着有必要的地方,做的一个简单的整理,不然读过之后虽然当时感觉清晰了许多,但据我以往的经验,即使再明白,长时间不用,也会忘记大部分。

书中的话

我们经常在理解一些内容的时候,虽然大致能明白这个过程,但是总觉得似乎还有那么一层迷雾阻隔着,一旦涉及细节总是有一些模糊。

这是在读这本书时,里面不经意提到的一句话,和我最近的想法一模一样,所以把它记录下来,要时常提醒自己:有时候你感觉好像懂了,但是很虚,只要一涉及到细节问题就会模糊不清。我觉得这和人的大脑遵守的“能不用则不用”的原则有关,思想和整体框架总是最容易理解的,难点就在如何实现等细节上,所以还是要克服自己的懒惰,迎难而上。

万能的“中间层”

有人说过一句名言:计算机科学领域的任何问题,都可以通过增加一个间接的中间层来解决。但是出处无从考证。

除了硬件和应用程序,其它都是所谓的中间层,每个中间层都是对它下面的那层的包装和扩展。正是这些中间层的存在,使得应用程序和硬件之间保持相对独立。比如硬件和操作系统都在日新月异的发展,但是最初为80386芯片和DOS系统设计的软件,在多核处理器和Windows Vista下还是能够运行,这方面归功于硬件和操作系统本身保持了向后兼容性,另一方面不得不归功于这种层次结构的设计。

“虚拟地址”也是一种增加中间层的解决方案。因为程序每次运行时都需要装入内存,但每次程序运行的地址都是不确定的,这就给程序的编写造成了麻烦因为它访问数据和指令跳转时的目标地址很多都是固定的。解决这个问题的思路就是增加中间层,我们把程序给出的地址看成是一种虚拟地址,然后通过某种映射的方法,将这个虚拟地址转换成实际的物理地址。这样,每个进程都有自己独立的虚拟空间,只要我们妥善的控制这个虚拟地址到物理地址的映射过程,就可以保证任何一个程序所能够访问的物理内存区域跟另一个程序互相不重叠,以达到地址空间隔离的效果,也就是进程的隔离。

从操作系统的角度,一个进程最关键的特征是它拥有独立的虚拟地址空间,这使得它有别于其它的进程。所以,在一个进程创建之初,首要做的就是创建一个独立的虚拟地址空间。而创这句话听起来本身就很虚,什么叫创建空间?怎么创建?实际上,创建空间其实是创建映射函数所需要的相应的数据结构。

被隐藏的过程

1)预处理:预处理也叫预编译,主要处理那些源代码文件中的以“#”开始的预编译指令,比如“include”、“#define”等。

2)编译:编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析以及优化后产生相应的汇编代码,这个过程是程序构建的核心部分,也是最复杂的部分。

3)汇编:就是将汇编代码转变成机器指令,一条汇编语句对应一条机器指令。这个过程相对编译器来讲比较简单,只是根据汇编指令和机器指令的对照表一一翻译就可以了。“汇编”这个名字也来源于此。

4)链接:链接的主要内容就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确的衔接,它的工作无非就是把一些指令对其它符号地址的引用加以修正。在链接中,目标文件之间的相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。链接过程关键的一部分就是对符号的管理,每个目标文件都会有相应的符号表。

为什么要分段

程序源代码被被编译以后主要分成两种段:指令和数据。那为什么要这么麻烦,把程序的指令和数据分开存放呢?混杂的放在一起不是更简单?好处主要是如下几个方面:

1)当系统中运行着多个该程序的副本时,它们的指令都是一样的,所以内存中只需保存一份该指令部分。对于指令这种只读的区域来说是这样,对于其它的一些图片、文本、图表等资源也是这样,当然每个进程的数据区域是进程私有的。

2)数据区域对程序来说是可读写的,而指令区域对于进程来说是只读的,所以在映射到内存后,可以分别设置读写权限,防止程序的指令被改写。

3)缓存在现代计算机中的地位非常重要,所以程序必须尽量提高缓存的命中率,指令区和数据区的分离有利于提高程序的局部性,现在CPU缓存都被设置成数据缓存和程序缓存相分离,所以程序的指令和数据分开存放对CPU的缓存命中率提高有好处。

系统调用、API、库

系统调用是应用程序(运行库也是应用程序的一部分)与操作系统内核之间的接
口,无论程序是直接进行系统调用,还是通过运行库,最终还是会到达系统调用这个层面。系统调用涵盖的功能很广,例如进程/线程的创建与销毁、内存管理、访问网络、文件、其它硬件资源,也有对图形界面的操作支持。

而系统调用实际上不是系统与应用程序最终的接口,而是API,API是对系统调用进行了包装。为什么要对系统调用进行包装呢?首先是不同的操作系统,系统调用不兼容,将不同的操作系统的系统调用包装为统一固定的接口,使得同样的代码,在不同的系统下都可以直接编译,并产生同样的效果。缺陷就是为了保证多个平台之间能够互相通用,于是它只能取各个平台之间功能的交集。

运行时库:API是操作系统提供给用户方便设计应用程序的函数,而运行时库又是在API之上,进行的一层封装,因为API所提供的接口还是比较原始的,比如网络相关的接口仅仅是socket级别的,如果用户要通过API访问HTTP资源,还需要自己实现HTTP协议,所以直接使用API开发往往效率较低。

Windows中,系统调用又称作系统服务。

其它

1、操作系统的一个功能是提供抽象的接口,另一个主要功能时管理硬件资源。这样来看,操作系统就是底层硬件和上层应用程序的中间层。

2、通常将程序的编译和链接合并到一起的过程称为构建(Build),之前一直没有想过“build”的意思,现在明白了。

你可能感兴趣的:(《程序员的自我修养:链接、装载与库》)