上一篇序章我谈了谈 程序员为啥要懂底层计算机结构 ,有人赞同也有人反对,但是这并不影响 LZ 对深入理解计算机系统研究的热情。这篇博客以案例驱动的模式,通过跟踪一个简单 Hello World 程序的生命周期开始系统的学习,包括它被程序员创建,到在系统上运行,输出简单的消息,然后终止。LZ 将沿着这个程序的声明周期,先简要的介绍一些逐步出现的关键概念、专业术语以及组成部分。后面将会详细展开。
1、计算机系统
我们知道计算机系统是由硬件和软件组成的。它们共同工作来运行应用程序。虽然系统的实现方式随着时间不断变化,但是系统内在的概念却没有改变。所有计算机系统都有相似的硬件和软件组件,它们执行这相似的功能,我们只有深入了解这些组件是如何工作的,以及这些组件是如何影响程序的正确性和性能的,才能写出高质量的代码。
2、万能程序大法----Hello World
1
2
3
4
5
6
7
|
#include <stdio.h> int main() { printf( "Hello World
" ); return 0 ; //c标准规定建议main函数返回值为int } |
这段代码不用多说,就是一个C语言的Hello World,程序的执行结果是打印 “Hello World”。
3、信息的表示
我们将上面的 Hello World 程序保存在一个 hello.c 的文件中,那么它是怎么存储在文件中的呢?实际上它是以字节序列的方式存储在文件中。
什么是字节?一个字节由8个位组成,而一个位是由值0和1组成。也就是说 hello.c 源程序是由值0和1组成的位序列。
大部分的现代系统都是用 ASCII 码构成,这种方式实际上就是用一个唯一的单字节大小的整数来表示每个字符。下面我们给出 hello.c 程序的 ASCII 码表示:
左边是文件对应的16进制代码,右边是我们的源程序,例如:第一个字符“#”的 ASCII 值是0x23。需要特别注意一下:每个文本行都以一个看不见的换行符‘ ’结束的。第2行中有2个连续的0x0D 0x0A ,这是windows中特有的“换行符 ” ,在linux中的是“换行符 ”。像hello.c文件这样只由 ASCII 码组成的文件叫做个“文本文件”,其他所有文件都叫“二进制文件”。
系统中所有的信息都是由位+上下文构成。
包括磁盘文件、存储器中的程序,存储器中存放的用户数据以及网络上传送的数据都是由一串位表示。而区分不同数据对象的唯一方法就是我们读到这些对象时的上下文。比如在不同的上下文中,一个同样的字节序列可能表示一个整数、浮点数、字符串或者机器指令。
作为程序员,我们需要了解数字的机器表示方式,因为它们与实际的整数和实数是不同的。它们是对真值的有限近视值,有时候会有意想不到的行为表现。这个后面我们会详细讲解。
4、程序的编译
hello 程序的生命周期是从一个高级 C 语言程序开始的,因为这种形式能被人读懂。然而,计算机系统是读不懂高级语言的。为了在系统上运行 hello.c 程序,每条 C 语句都必须要被其他程序转化为一系列的低级机器语言指令。
一般来说,要将 hello.c 变成一个可执行的目标程序,必须要经过 预处理器、编译器、汇编器和链接器 的处理。如下:
预处理器、编译器、汇编器和链接器 一起构成了编译系统,下面对每个步骤分别进行解析:
①、预处理阶段:预处理器 cpp 根据以字符 # 开头的命令,修改原始的 C 程序,比如 Hello.c 中第一行 #include<studio.h> 命令告诉预处理器读取系统文件 stdio.h 的内容,并把它直接插入到程序中。结果就得到另一个 C 程序,通常是以 .i 作为文件扩展名。
②、编译阶段:编译器 ccl 将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序,汇编语言程序中的每条语句都以一种标准的文本格式确切的描述一条低级机器语言指令。汇编语言能为不同高级语言的不同编译器提供通用的输出语言。
③、汇编阶段:汇编器 as 将hello.s 翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件 hello.o 中,hello.o 文件是一个二进制文件,它的字节编码是机器预言指令而不是字符。如果我们用文本编辑器打开 hello.o 文件,将会是一堆乱码。
④、链接阶段:在 hello.c 程序中,我们看到程序调用了 printf 函数,它是每个 C 编译器都会提供的标准 C 库中的一个函数。printf 函数存在于一个名为 printf.o 的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的 hello.o 程序中。链接器 ld 就是负责处理这种合并,结果就得到一个 hello 文件,它是一个可执行目标程序,可以被加载到内存中,由系统运行。
这里我做一下验证,我在 Linux 系统上创建 hello.c 程序,然后依次执行上面的步骤:
预处理:
1
|
gcc -E hello.c -o hello.i |
然后查看 hello.i
编译阶段:
1
|
gcc -S hello.i |
然后查看 hello.s
上面截图的是一个汇编程序
5、程序的运行
经过上面程序的编译,hello.c 源程序已经被编译成了可执行目标文件 hello,并存放在磁盘上,那么如何运行呢?
①、系统的硬件组成
为了理解运行 hello 程序时发生了什么,我们先要了解一个典型系统的硬件组织。如下图:
我们现在不需要对这张图有很深入的理解,后面会详细进行介绍。现在先简单的认识一下下面几个主要部件:
一、总线:贯穿整个系统的一组电子管道,通常被设计成用来传送定长的字节块,也就是字。字的大小与系统相关,比如在32位操作系统当中,一个字是4个字节。
二、I/O设备:输入/输出(I/O)设备是系统与外部世界联系通道,上图有4个I/O设备。作为用户输入的键盘和鼠标,作为用户输出的显示器,以及用于长期存储数据和程序的磁盘。每一个I/O设备都通过一个控制器或者适配器与I/O总线相连。控制器是置于I/O设备本身的或者系统的主印刷电路板(通常称为主板)上的芯片组,而适配器则是一块插在主板插槽上的卡。无论如何,它们的功能都是在 I/O 总线和 I/O 设备之间传递信息。
三、主存:它是计算机中的一个临时存储设备,在处理器执行程序的时候,用来存放程序和程序处理的数据。物理上来说,主存是由一组动态随机存取存储器(DRAM)组成的,逻辑上来说,它是一个线性的字节数组,每一个字节都有唯一的地址(即数组索引)。
四、处理器:全称中央处理器(CPU),是解释(或执行)存储在主存中指令的引擎。处理器的核心是一个字长的存储设备(或寄存器),简称程序计数器(PC),在任何时刻,它都会指向主存中的某条机器指令(即含有该条指令的地址)。从系统通电到断点,处理器一直在不断的执行程序计数器所指向指令,再更新程序计数器,使其指向下一条指令。处理器所做的操作是围绕主存、寄存器文件以及算术/逻辑单元(ALU)进行的,寄存器文件是一个小的存储设备,由一些1字长的寄存器组成,每个寄存器都有唯一的名字。ALU则计算新的数据和地址值。
CPU 在指令的要求下会做如下操作:
①、加载:把一个字节或者一个字从主存复制到寄存器,以覆盖寄存器原来的内容
②、存储:把一个字节或者一个字从寄存器复制到主存的某个位置,以覆盖这个位置上原来的内容
③、操作:把两个寄存器的内容复制到 ALU,ALU 对这两个字做算术操作,并把结果存放到一个寄存器中,以覆盖寄存器原来的内容
④、跳转:从指令本身中抽取一个字,并将这个字复制到程序计数器(PC)中,以覆盖PC中原来的内容。
处理器当中提到的是指令集结构的简单实现,不过实际上现代处理器使用了非常复杂的机制来加速程序的运行。我们可以这样去区分指令集机构以及微体系结构,指令集结构描述的是每条机器代码指令的效果,而微体系结构描述的是处理器实际上是如何实现的,类似于JAVA虚拟机与JAVA虚拟机实现的关系。
②、运行 Hello World 程序
前面简单的介绍了系统的硬件组成和操作,那么接下来介绍我们运行程序时到底发生了什么。
想要在 Linux 系统中运行该可执行程序,我们要将它的文件名输入到称为外壳(shell)的应用程序中,外壳是一个命令行解释器,它输出一个提示符,等待你输入一个命令,然后执行这个命令。如果该命令行的第一个单词不是一个内置的外壳命令,那么外壳就会假设这是一个可执行文件的名字,它将加载并运行这个文件。
初始时,外壳程序执行它的指令,等待我们输入一个命令。当我们在键盘上输入字符串"./hello"后,外壳程序将字符逐一读入到寄存器中,再把它放入到存储器中,如下图:
PS:为什么要输入“./hello”来执行,对于Linux系统有一定了解的人,可能知道这是运行命令的一种方法。
当我们在键盘上敲回车键的时候,外壳程序知道我们已经结束了命令的输入。然后外壳执行一系列指令来加载可执行的 hello 文件,将 hello 目标文件中的代码和数据从磁盘复制到主存。数据包括最终会被输出的字符串“Hello World ”,一旦目标文件中的代码和数据被加载到主存,处理器就开始执行 hello 程序的 main 程序中的机器语言指令。这些指令将“Hello World ” 字符串中的字节从主存复制到寄存器文件,再从寄存器文件中复制到显示设备,最终显示在屏幕上。
6、 本章总结
①、出现的名词解释:
位:"位(bit)"是电子计算机中最小的数据单位。每一位的状态只能是0或1。
字节:8个二进制位构成1个"字节(Byte)",它是存储空间的基本计量单位。1个字节可以储存1个英文字母或者半个汉字,换句话说,1个汉字占据2个字节的存储空间。
字:"字"由若干个字节构成,字的位数叫做字长,不同档次的机器有不同的字长。例如一台8位机,它的1个字就等于1个字节,字长为8位。如果是一台16位机,那么,它的1个字就由2个字节构成,字长为16位。在32位操作系统当中,一个字是4个字节,字是计算机进行数据处理和运算的单位。
ASCII:American Standard Code for Information Interchange,美国信息交换标准代码。注意不是ASCⅡ(罗马数字2),使用指定的7 位或8 位二进制数组合来表示128 或256 种可能的字符。标准ASCII 码也叫基础ASCII码,使用7 位二进制数(剩下的1位二进制为0)来表示所有的大写和小写字母,数字0 到9、标点符号, 以及在美式英语中使用的特殊控制字符。
文本文件和二进制文件:
文本文件是指以ASCII码方式(也称文本方式)存储的文件,后面基于 utf-8 编码的文本文件,utf-8是能够向后兼容ASCII,即相同的ASCII文本文件和UTF-8文本文件完全一致。它是一种典型的顺序文件,其文件的逻辑结构又属于流式文件。
二进制文件:是基于值编码的文件,你可以根据具体应用,指定某个值(可以看作是自定义编码)。
②、内容总结
计算机是由软件与硬件组成的,而硬件又包括了总线、I/O设备、主存以及处理器,其中信息是由位以及上下文表示的,而信息则是从I/O设备以位的形式通过总线进入主存,然后由处理器从主存将信息取出处理。
一个程序的执行,是经历了预处理器、编译器、汇编器以及链接器的处理之后,才最终成为可执行的文件。
PS:有人问我《深入理解计算机系统》这本书的PDF文档,这里给出下载链接:http://pan.baidu.com/s/1boOM3Tl 密码:kfe1