计算机操作系统——编译、链接和加载

我是荔园微风,作为一名在IT界整整25年的老兵,今天给大家讲讲操作系统。这可能是全社区讲解编译、链接和加载最好的文章了,我还专门摘录了从网上找来的最好的配图并给予说明。

在执行用高级语言编写的程序之前,必须先将程序翻译成机器语言,与它所依赖的各种其他机器语言进行链接,并加载到内存中。在这里,我们要考虑用高级语言所编写的程序是如何编译(compile)成机器语言代码的,同时也要描述链接器和加载器如何为执行编译过的代码做准备。

编译

虽然每一种计算机只能理解它自己的机器语言,但几乎所有的程序都是用高级语言编写的。在生成可执行程序的过程中,第一阶段是将用高级程序设计语言所编写的代码编译成机器语言。编译器接收源代码(source code),源代码是用高级语言编写的,这作为输入,并返回目标码(object code),目标码包含要执行的机器语言指令,这作为输出。几乎所有商业上可以使用的应用程序都只交付目标码,有些分发(即开源软件)提供源代码。

编译过程可以分成几个阶段,图示意了编译的每个阶段。每一个阶段都对程序进行修改,以便能被下一阶段解释,一直到程序被翻译成机器代码。首先,源代码被传递到词法分析器(lexer 或者 lexical analyzer),也称为扫描器(scanner),它将程序源代码的字符分离成记号(token),包括关键字(例如,if,else 和int)、标识符(例如,有名称的变量和常量)、运算符(例如,-、+、*和/)和标点符号(例如,分号)。

词法分析器将这些记号流传递给解析器(parser),也称为语法分析器(syntax analyzer),它将这些记号分成语法上正确的语句组。中间代码生成器(intermediate code generator)将这一语法结构转换成与汇编语言类似的简单指令流(虽然它并没有指定每一个操作所使用的寄存器)。优化器(optimizer)力图提高代码的执行效率,并减少程序的内存需求。在最后阶段,代码生成器(code  generator)生成包含机器语言指令的目标文件。

链接

程序常常由若干独立的开发好的子程序组成,这些子程序称为模块(module)。执行常见计算机例程(例如,I/O操纵或者随机数生成)的函数被封装到预编译模块中,这些预编译模块称为库(library)。链接(linking)是将程序引用的各种模块集成为一个单一的可执行单元的过程。

当程序被编译时,它相应的目标模块包含从程序的源文件中检索出来的程序数据和指令。如果此程序引用另一个模块的函数或者数据,则编译器将它们翻译为外部引用( (externalreference)。而且,如果一个程序要使其他程序可以使用它的函数和数据,则其中每一个函数和数据都要表示为外部名(external name)。目标模块将这些外部引用和外部名存储在一个数据结构中,我们将此数据结构称为符号表(symbol table)。链接器产生的集成模块称为加载模块(load module)。到链接器的输入可以包括目标模块、加载模块和控制语句,例如,被引用的库文件的位置。

经常会提供给链接器若干个目标文件,这些文件形成一个单一的程序。一般情况下,这些目标文件指定数据和指令的位置时,所采用的是相对于每一个文件开始处的地址,这样的地址称为相对地址(relative address)。

在图中,目标模块A中的符号X和目标模块B的符号Y在它们各自的模块中具有相同的相对地址。链接器必须修改这些地址,以便在将这些模块组合起来形成一个链接程序时,它们不会引用无效的数据或者指令。重定位(relocating)地址确保每一条语句都由文件中唯一的一个地址来识别。当一个地址被修改时,到它的所有引用也必须更新为新的相对地址。在最终的加载模块中,X和Y被重定位成新的相对地址,这些地址在加载模块中是唯一的。链接器也经常提供加载模块中的相对地址,但是,分配这些地址时,它们全都是相对于整个加载模块开始处的地址。

链接器也完成符号解析(symbol resou tion) , 将一个模块中的外部引用转换为另一个模 块中相应的外部名。 在下图中,在目标模块2中对符号C的外部引用被解析为目标 模块1的外部名C。在一个独立的模块中,一旦外部引用与它相应的名称匹配时,就必须 修改外部引用的地址,以反映这种集成情况。 

链接经常需要两次传递。第一次传递确定每一个模块的大小,并构造一个符号表。符 号表将每一个符号(例如,变量名)与一个地址关联在一起,这样链接器就可以定位引用。 在第二次传递中,链接器将地址分配给不同的指令和数据单元,并解析外部符号引用。 因为一个加载模块可以成为另一个链接传递的输入,所以加载模块包含一个符号表,在这张表中所有符号都是外部名。在上图中需要注意的是,到符号Y的外部引用并没有列在加载模块的符号表中,因为它已经被解析。

一个程序被链接的时间取决于环境。如果程序员将所有必需的代码都包含在源文件中,这样并没有引用外部名,则程序就可以在编译时被链接。通过在源代码中搜索任何外部引用符号,并将这些符号放入最终的目标文件中,可以完成这一过程。一般情况下,这种方法是行不通的,因为许多程序都依赖于共享库(shared library),共享库是不同进程之间可以共享的函数的集合。许多程序可以引用相同的函数(例如,操纵输入流和输出流的库函数),而不用将这些函数包括在它们的目标码中。一般情况下,这种链接是在编译完成之后,但在加载开始之前完成的。在“小型案例分析:Mach 系统”中讨论到,共享库使得Mach微内核系统能够模拟多个操作系统。

小型案例分析:Mach系统

Mach系统是在1985—1994年由卡内基·梅隆大学(CMU)开发出来的,它是基于CMU 早期的 Accent 研究操作系统。48此项目由 Richard Rashid 直接负责,他现在是Microsoft Research 的高级副总裁。49Mach 是一流的和有名的微内核操作系统(请参见第1.13.3小节)之一。它已经被整合到了后来的系统中,包括 Mac OS X, NeXT和OSF/1,并对 Windows NT 最终也对 Windows XP(请参见第21章)产生了很大的影响。开源实现GNU Mach 被用作当前正在开发的GNU Hurd 操作系统的内核。

Mach 微内核系统的一个强大的功能是它能够模仿其他操作系统,Mach是使用“透明的共享库”来实现这一点的。透明的共享库实现了它所模仿的操作系统的系统调用的动作,然后截取程序发出的系统调用(编写这些程序本来是要运行在被模仿的那种操作系统上)。接着将截取的系统调用翻译为Mach 系统调用,所有的结果都被再翻译成被模仿系统的格式。因此,用户的程序并非一定要运行在一个运行 Mach 的系统上。此外,存储器中可以存储任何数量的这种透明库,所以Mach 能够同时模仿多个操作系统。

这一相同的过程也可以在加载时完成。链接和加载有时是由同一个应用程序来完成的,这一应用程序称为链接加载器(linking loader)。链接也可以发生在运行时,这一过程称为动态链接(dynamic linking)。在这种情况下,一直到进程被加载到内存中或者发出函数调用时,才解析对外部函数的引用。有些大型程序中使用到了由另一方控制的程序,那么动态链接就很有用,因为当它所使用的库被修改时,被动态链接的程序不需要再重新链接。而且,因为动态链接程序一直到加载到主存储器中时才进行链接,所以共享库代码可以与其他程序代码分开来存储。因为对于使用共享库的任意多个程序来说,只需要存储一份拷贝,所以动态链接也节省了辅助存储器中的空间。

加载

链接器创建了加载模块后,就将它传递给加载器(loader)程序。加载器负责将每一个指令和数据单元放置在内存地址中,这一过程称为地址绑定(address binding)。将程序加载到主存储器中有几种技术,其中大多数技术只对不支持虚拟内存的系统来说是重要的。如果加载模块已经指定了内存中的物理地址,那么加载器就简单地将指令和数据单元放置在由程序员或者编译器指定的地址(假定内存地址是有效的),这种技术称为绝对加载(absoluteloading)。如果加载模块包含相对地址,就需要将地址转换为实际的内存地址,此时就要执行重定位加载(relocatable loading)。加载器负责请求一块内存空间,用来存放程序,然后将此程序的地址重定位到内存中的相应位置。

在下图中,操作系统已经分配了一个内存块,其起始内存地址是10000,当加载程序时,加载器必须将加载模块中的每一个地址增加10000。在下图中,加载器将变量Example 的内存地址从最初的相对地址450更新为10450。

动态加载(dynamic loading)是一种在初次使用程序模块时才加载这些模块的技术。在许多虚拟内存系统中,为每一个进程指定它自己的虚拟地址集都以0作为起始地址,因此,加载器负责将程序加载到一个有效的内存区域。

在下图中我们回顾从源代码到执行的整个编译、链接和加载过程(使用加载时地址绑定)。程序员开始用某种高级语言(在此例中是用C语言)编写源代码,接下来,编译器将foo.c和bar.c源代码文件转换成机器语言,创建目标模块foo.o和bar.o。在此代码中,程序员已经在foo.c中定义了变量X,在bar.c中定义了变量Y,这两个变量在它们各自的目标模块中的位置都是相对地址100。用户或者另一个进程请求时,模块则必须被链接,在此之前,目标模块一直存放在辅助存储器中。

第二步,链接器将两个模块集成为一个单一的加载模块。链接器完成这一任务是通过在第一次传递时收集模块大小和外部符号的有关信息,并在第二次传递时将文件链接在一起。需要注意的是,链接器将变量Y重定位到相对地址400。

第三步,加载器为程序请求一个内存块。操作系统提供的地址范围是4000~5050,因此加载器将变量X重定位到绝对地址4100,将变量Y重定位到绝对地址4400。

作者简介:荔园微风,高级工程师,浙大工学硕士,软件工程项目主管,做过程序员、软件设计师、系统架构师,早期的Windows程序员,Visual Studio忠实用户,C/C++使用者,是一位在计算机界学习、拼搏、奋斗了25年的老将,经历了UNIX时代、桌面WIN32时代、Web应用时代、云计算时代、手机安卓时代、大数据时代、ICT时代、AI深度学习时代、智能机器时代,我不知道未来还会有什么时代,只记得这一路走来,充满着艰辛与收获,愿同大家一起走下去,充满希望的走下去。

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值