127 lines
8.5 KiB
Markdown
127 lines
8.5 KiB
Markdown
|
# 01 | 程序的运行过程:从代码到机器运行
|
|||
|
|
|||
|
你好,我是LMOS。
|
|||
|
|
|||
|
欢迎来到操作系统第一课。在真正打造操作系统前,有一条必经之路:你知道程序是如何运行的吗?
|
|||
|
|
|||
|
一个熟练的编程老手只需肉眼看着代码,就能对其运行的过程了如指掌。但对于初学者来说,这常常是很困难的事,这需要好几年的程序开发经验,和在长期的程序开发过程中对编程基本功的积累。
|
|||
|
|
|||
|
我记得自己最初学习操作系统的时候,面对逻辑稍微复杂的一些程序,在编写、调试代码时,就会陷入代码的迷宫,找不到东南西北。
|
|||
|
|
|||
|
不知道你现在处在什么阶段,是否曾有同样的感受?**我常常说,扎实的基本功就像手里的指南针,你可以一步步强大到不依赖它,但是不能没有。**
|
|||
|
|
|||
|
因此今天,我将带领你从“Hello World”起,扎实基本功,探索程序如何运行的所有细节和原理。这节课的配套代码,你可以从[这里](https://gitee.com/lmos/cosmos/tree/master/lesson01/HelloWorld)下载。
|
|||
|
|
|||
|
## 一切要从牛人做的牛逼事说起
|
|||
|
|
|||
|
**第一位牛人,是世界级计算机大佬的传奇——Unix之父Ken Thompson**。
|
|||
|
|
|||
|
在上世纪60年代的一个夏天,Ken Thompson的妻子要回娘家一个月。呆在贝尔实验室的他,竟然利用这极为孤独的一个月,开发出了UNiplexed Information and Computing System(UNICS)——即UNIX的雏形,一个全新的操作系统。
|
|||
|
|
|||
|
要知道,在当时C语言并没有诞生,从严格意义上说,他是用B语言和汇编语言在PDP-7的机器上完成的。
|
|||
|
|
|||
|
![](https://static001.geekbang.org/resource/image/41/56/418b94aed8aab2abf6538a103d9f2856.png?wh=953*633)
|
|||
|
|
|||
|
**牛人的朋友也是牛人,他的朋友Dennis Ritchie也随之加入其中,共同创造了大名鼎鼎的C语言,并用C语言写出了UNIX和后来的类UNIX体系的几十种操作系统,也写出了对后世影响深远的第一版“Hello World”**:
|
|||
|
|
|||
|
```
|
|||
|
#include "stdio.h"
|
|||
|
int main(int argc, char const *argv[])
|
|||
|
{
|
|||
|
printf("Hello World!\n");
|
|||
|
return 0;
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
计算机硬件是无法直接运行这个C语言文本程序代码的,需要C语言编译器,把这个代码编译成具体硬件平台的二进制代码。再由具体操作系统建立进程,把这个二进制文件装进其进程的内存空间中,才能运行。
|
|||
|
|
|||
|
听起来很复杂?别急,接着往下看。
|
|||
|
|
|||
|
## 程序编译过程
|
|||
|
|
|||
|
我们暂且不急着摸清操作系统所做的工作,先来研究一下编译过程和硬件执行程序的过程,约定使用GCC相关的工具链。
|
|||
|
|
|||
|
那么使用命令:gcc HelloWorld.c -o HelloWorld 或者 gcc ./HelloWorld.c -o ./HelloWorld ,就可以编译这段代码。其实,GCC只是完成编译工作的驱动程序,它会根据编译流程分别调用预处理程序、编译程序、汇编程序、链接程序来完成具体工作。
|
|||
|
|
|||
|
下图就是编译这段代码的过程:
|
|||
|
|
|||
|
![](https://static001.geekbang.org/resource/image/f2/4a/f2b10135ed52436888a793327e4d5a4a.jpg?wh=3015*2410 "HelloWorld编译流程")
|
|||
|
|
|||
|
其实,我们也可以手动控制以上这个编译流程,从而留下中间文件方便研究:
|
|||
|
|
|||
|
* gcc HelloWorld.c -E -o HelloWorld.i预处理:加入头文件,替换宏。
|
|||
|
* gcc HelloWorld.c -S -c -o HelloWorld.s编译:包含预处理,将C程序转换成汇编程序。
|
|||
|
* gcc HelloWorld.c -c -o HelloWorld.o汇编:包含预处理和编译,将汇编程序转换成可链接的二进制程序。
|
|||
|
* gcc HelloWorld.c -o HelloWorld链接:包含以上所有操作,将可链接的二进制程序和其它别的库链接在一起,形成可执行的程序文件。
|
|||
|
|
|||
|
## 程序装载执行
|
|||
|
|
|||
|
对运行内容有了了解后,我们开始程序的装载执行。
|
|||
|
|
|||
|
我们将请出**第三位牛人——大名鼎鼎的阿兰·图灵。在他的众多贡献中,很重要的一个就是提出了一种理想中的机器:图灵机。**
|
|||
|
|
|||
|
图灵机是一个抽象的模型,它是这样的:有一条无限长的纸带,纸带上有无限个小格子,小格子中写有相关的信息,纸带上有一个读头,读头能根据纸带小格子里的信息做相关的操作并能来回移动。
|
|||
|
|
|||
|
文字叙述还不够形象,我们来画一幅插图:
|
|||
|
|
|||
|
![](https://static001.geekbang.org/resource/image/69/7d/6914497643dbb0aaefffc32b865dcf7d.png?wh=517*199)
|
|||
|
|
|||
|
不理解?下面我再带你用图灵机执行一下“1+1=2”的计算,你就明白了。我们定义读头读到“+”之后,就依次移动读头两次并读取格子中的数据,最后读头计算把结果写入第二个数据的下一个格子里,整个过程如下图:
|
|||
|
|
|||
|
![](https://static001.geekbang.org/resource/image/43/87/43812abfe104d6885815825f07622e87.jpg?wh=2705*2755 "图灵机计算过程演示")
|
|||
|
|
|||
|
这个理想的模型是好,但是理想终归是理想,想要成为现实,我们得想其它办法。
|
|||
|
|
|||
|
**于是,第四位牛人来了,他提出了电子计算机使用二进制数制系统和储存程序,并按照程序顺序执行,他叫冯诺依曼,他的电子计算机理论叫冯诺依曼体系结构。**
|
|||
|
|
|||
|
根据冯诺依曼体系结构构成的计算机,必须具有如下功能:
|
|||
|
|
|||
|
* 把程序和数据装入到计算机中;
|
|||
|
* 必须具有长期记住程序、数据的中间结果及最终运算结果;
|
|||
|
* 完成各种算术、逻辑运算和数据传送等数据加工处理;
|
|||
|
* 根据需要控制程序走向,并能根据指令控制机器的各部件协调操作;
|
|||
|
* 能够按照要求将处理的数据结果显示给用户。
|
|||
|
|
|||
|
为了完成上述的功能,计算机必须具备五大基本组成部件:
|
|||
|
|
|||
|
* 装载数据和程序的输入设备;
|
|||
|
* 记住程序和数据的存储器;
|
|||
|
* 完成数据加工处理的运算器;
|
|||
|
* 控制程序执行的控制器;
|
|||
|
* 显示处理结果的输出设备。
|
|||
|
|
|||
|
根据冯诺依曼的理论,我们只要把图灵机的几个部件换成电子设备,就可以变成一个最小核心的电子计算机,如下图:
|
|||
|
|
|||
|
![](https://static001.geekbang.org/resource/image/bd/26/bde34df011c397yy42dc00fe6bd35226.jpg?wh=1386*1026)
|
|||
|
|
|||
|
是不是非常简单?这次我们发现读头不再来回移动了,而是靠地址总线寻找对应的“纸带格子”。读取写入数据由数据总线完成,而动作的控制就是控制总线的职责了。
|
|||
|
|
|||
|
## 更形象地将HelloWorld程序装入原型计算机
|
|||
|
|
|||
|
下面,我们尝试将HelloWorld程序装入这个原型计算机,在装入之前,我们先要搞清楚HelloWorld程序中有什么。
|
|||
|
|
|||
|
我们可以通过gcc -c -S HelloWorld得到(只能得到其汇编代码,而不能得到二进制数据)。我们用objdump -d HelloWorld程序,得到/lesson01/HelloWorld.dump,其中有很多库代码(只需关注main函数相关的代码),如下图:
|
|||
|
|
|||
|
![](https://static001.geekbang.org/resource/image/39/14/3991a042107b90612122b14596c65614.jpeg?wh=820*291)
|
|||
|
|
|||
|
以上图中,分成四列:第一列为地址;第二列为十六进制,表示真正装入机器中的代码数据;第三列是对应的汇编代码;第四列是相关代码的注释。这是x86\_64体系的代码,由此可以看出x86 CPU是变长指令集。
|
|||
|
|
|||
|
接下来,我们把这段代码数据装入最小电子计算机,状态如下图:
|
|||
|
|
|||
|
![](https://static001.geekbang.org/resource/image/5d/6e/5d4889e7bf20e670ee71cc9b6285c96e.jpg?wh=3810*1815 "PS:上图内存条中,一个小格子中只要一个字节,但是 [br] 图中放的字节数目不等,这是为了方便阅读,不然图要画得很大。")
|
|||
|
|
|||
|
## 重点回顾
|
|||
|
|
|||
|
以上,对应图中的伪代码你应该明白了:现代电子计算机正是通过内存中的信息(指令和数据)做出相应的操作,并通过内存地址的变化,达到程序读取数据,控制程序流程(顺序、跳转对应该图灵机的读头来回移动)的功能。
|
|||
|
|
|||
|
这和图灵机的核心思想相比,没有根本性的变化。只要配合一些I/O设备,让用户输入并显示计算结果给用户,就是一台现代意义的电子计算机。
|
|||
|
|
|||
|
到这里,我们理清了程序运行的所有细节和原理。还有一点,你可能有点疑惑,即printf对应的puts函数,到底做了什么?而这正是我们后面的课程要探索的!
|
|||
|
|
|||
|
## 思考题
|
|||
|
|
|||
|
为了实现C语言中函数的调用和返回功能,CPU实现了函数调用和返回指令,即上图汇编代码中的“call”,“ret”指令,请你思考一下:call和ret指令在逻辑上执行的操作是怎样的呢?
|
|||
|
|
|||
|
期待你在留言区跟我交流互动。如果这节课对你有所启发,也欢迎转发给你的朋友、同事,跟他们一起学习进步。
|
|||
|
|