本系列文章是《程序员的自我修养–链接、装载与库》(电子工业出版社)一书的学习摘录笔记,本文是书中1.1至1.4部分。
基础概念
#include <stdio.h>
int main()
{
printf("Hello World\n");
return 0;
}
对于以上的程序,我们提出一下问题:
- 程序为什么要被编译器编译了之后才可以运行?
- 编译器在把C语言程序转换成可以执行的机器码的过程中做了什么,
怎么做的? - 最后编译出来的可执行文件里面是什么? 除了机器码还有什么? 它们
怎么存放的, 怎么组织的? - #include <stdio.h>是什么意思? 把stdio.h包含进来意味着什么? C语言
库又是什么? 它怎么实现的? - 不同的编译器(Microsoft VC、 GCC) 和不同的硬件平台(x86、
SPARC、 MIPS、 ARM) , 以及不同的操作系统(Windows、 Linux、
UNIX、 Solaris) , 最终编译出来的结果一样吗? 为什么? - Hello World程序是怎么运行起来的? 操作系统是怎么装载它的? 它从
哪儿开始执行, 到哪儿结束? main函数之前发生了什么? main函数结束
以后又发生了什么? - 如果没有操作系统, Hello World可以运行吗? 如果要在一台没有操作
系统的机器上运行Hello World需要什么? 应该怎么实现? - printf是怎么实现的? 它为什么可以有不定数量的参数? 为什么它能够
在终端上输出字符串? - Hello World程序在运行时, 它在内存中是什么样子的?
这些问题都需要我们对程序编译、静态链接、操作系统的动态链接及运行库和标准库的实现进行了解。
我们在开始正式学习前需要对计算机系统的一些基本概念进行一些回顾,这包括硬件部分和软件部分。
硬件
对于系统程序开发者来说, 计算机多如牛毛的硬件设备中, 有三个部件最为关键, 它们
分别是中央处理器CPU、 内存和I/O控制芯片, 这三个部件几乎就是计算机的核心了 。
早期的计算机没有很复杂的图形功能, CPU的核心频率也不高, 跟内存的频率一样, 它们都是直接连接在同一个总线(Bus) 上的。 由于I/O设备诸如显示设备、 键盘、 软盘和磁盘等速度与CPU和内存相比还是慢很多, 当时也没有复杂的图形设备, 显示设备大多是只能输出字符的终端。 为了协调I/O设备与总线之间的速度, 也为了能够让CPU能够和I/O设备进行通信, 一般每个设备都会有一个相应的I/O控制器。
后来由于CPU核心频率的提升, 导致内存跟不上CPU的速度, 于是产生了与内存频率一致的系统总线, 而CPU采用倍频的方式与系统总线进行通信。 接着随着图形化的操作系统普及, 特别是3D游戏和多媒体的发展, 使得图形芯片需要跟CPU和内存之间大量交换数据, 慢速的I/O总线已经无法满足图形设备的巨大需求。 为了协调CPU、 内存和高速的图形设备, 人们专门设计了一个高速的北桥芯片, 以便它们之间能够高速地交换数据。由于北桥运行的速度非常高, 所有相对低速的设备如果全都直接连接在北桥上, 北桥既须处理高速设备, 又须处理低速设备, 设计就会十分复杂。 于是人们又设计了专门处理低速设备的南桥(Southbridge) 芯片,磁盘、 USB、 键盘、 鼠标等设备都连接在南桥上, 由南桥将它们汇总后连接到北桥上。
20世纪90年代的PC机在系统总线上采用的是PCI结构,而在低速设备上采用的ISA总线, 采用PCI/ISA及南北桥设计的硬件构架如下图所示:
位于中间是连接所有高速芯片的北桥(Northbridge, PCI Bridge) , 它就像人的心脏, 连接并驱动身体的各个部位; 它的左边是CPU, 负责所有的控制和运算, 就像人的大脑。 北桥还连接着几个高速部件, 包括左边的内存和下面的PCI总线。
软件
系统软件这个概念其实比较模糊, 传统意义上一般将用于管理计算机本身的软件称为系统软件, 以区别普通的应用程序。 系统软件可以分成两块, 一块是平台性的, 比如操作系统内核、 驱动程序、 运行库和数以千计的系统工具; 另外一块是用于程序开发的, 比如编译器、 汇编器、 链接器等开发工具和开发库。 书中着重介绍了系统软件的一部分, 主要是链接器和库(包括运行库和开发库) 的相关内容。
计算机软件体系结构如上图所示。
每个层次之间都须要相互通信,既然需要通信就必须有一个通信的协议,我们一般将其称为接口( Interface),接口的下面那层是接口的提供者,由它定义接口;接口的上面那层是接口的使用者,它使用该接口来实现所需要的功能。在层次体系中,接口是被精心设计过的,尽量保持稳定不变,那么理论上层次之间只要遵循这个接口,任何一个层都可以被修改或被替换。
除了硬件和应用程序,其他都是所谓的中间层,每个中间层都是对它下面的那层的包装和扩展。正是这些中间层的存在,使得应用程序和硬件之间保持相对的独立,比如硬件和操作系统都日新月异地发展,但是最初为80386芯片和DOS系统设计的软件在最新的多核处理器和 WindowsVista下还是能够运行的,这方面归功于硬件和操作系统本身保持了向后兼容性,另一方面不得不归功于这种层次结构的设计方式。最近开始流行的虚拟机技术更是在硬件和操作系统之间增加了层虚拟层,使得一个计算机上可以同时运行多个操作系统,这也是层次结构带来的好处,在尽可能少改变甚至不改变其他层的情况下,新增加一个层次就可以提供前所未有的功能。
软件体系中最上层的是应用程序,比如我们用到的网络浏览器、多媒体播放器等,从整个结构层次上看,开发工具和用用程序是属于同一层次的,因为他们都使用一个接口,那就是操作系统应用程序编程接口(Application Programming Interface),应用程序接口的提供者是运行库,什么样的运行库提供什么样的API,Windows的运行库提供Windows API, 最常见的32位Windows提供的API又被称为Win32。
操作系统内核层对于硬件层来说是硬件接口的使用者,而硬件是接口的定义者,硬件的接口定义决定了操作系统内核,具体来讲就是驱动程序如何操作硬件,如何与硬件进行通信。这种接口往往被叫做硬件规格( Hardware Specification)。
硬件的生产厂商负责提供硬件规格,操作系统和驱动程序的开发者通过阅读硬件规格文档所规定的各种硬件编程接口标准来编写操作系统和驱动程序。
操作系统
操作系统的一个功能是提供抽象的接口, 另外一个主要功能是管理硬件资源。
-
操作系统和内存
现在主流的程序运行模式是多任务(Multi-tasking) 系统, 操作系统接管了所有的硬件资源, 并且本身运行在一个受硬件保护的级别。 所有的应用程序都以进程(Process) 的方式运行在比操作系统权限更低的级别, 每个
进程都有自己独立的地址空间, 使得进程之间的地址空间相互隔离。CPU由操作系统统一进行分配, 每个进程根据进程优先级的高低都有机会得到CPU, 但是, 如果运行时间超出了一定的时间, 操作系统会暂停该进程, 将CPU资源分配给其他等待运行的进程。 这种CPU的分配方式即所谓的抢占式(Preemptive) , 操作系统可以强制剥夺CPU资源并且分配给它认为目前最需要的进程。 如果操作系统分配给每个进程的时间都很短, 即CPU在多个进程间快速地切换, 从而造成了很多进程都在同时运行的假象。 目前几乎所有现代的操作系统都是采用这种方式。 -
操作系统和硬件
操作系统作为硬件层的上层, 它是对硬件的管理和抽象。
当成熟的操作系统出现以后, 硬件逐渐被抽象成了一系列概念。 在UNIX中, 硬件设备的访问形式跟访问普通的文件形式一样; 在Windows系统中, 图形硬件被抽象成了GDI, 声音和多媒体设备被抽象成了DirectX对象; 磁盘被抽象成了普通文件系统,等等。
繁琐的硬件细节全都交给了操作系统, 具体地讲是操作系统中的硬件驱动(Device Driver) 程序来完成。 驱动程序可以看作是操作系统的一部分, 它往往跟操作系统内核一起运行在特权级, 但它又与操作系统内核之间有一定的独立性, 使得驱动程序有比较好的灵活性。