Hello World 是如何跑起来的
下面的 hello world
是我们学习各个语言时都会首先尝试运行的第一个程序,因为它打开了新世界的大门,但是,计算机是如何将我们编写的程序在系统上运行,并在屏幕上输出信息的呢?所以我们需要了解一个程序的整个生命周期。
#include <stdio.h>
int main()
{
printf("hello, world\n");
return 0;
}
Hello World 源文件
创建源文件是我们编写程序第一步需要执行的操作,比如上面的代码被保存在 hello.c
中。
在计算机中,所有文件都是:由 0 和 1 组成的比特序列,每 8 位被组织成一组,被解释为 1 个字节。
而每个字节都会被计算机解析成不同的含义,而根据不同类型的文件,同一个字节数如35
可能会被解析成不同的字符含义。
在计算机中分为两种类型的文件:
- 文本文件:这一类文件中,每个字节都代表一个唯一的
ASCII
字符。 - 二进制文件:根据环境上下文,不同的字节数解析为不同的字符,例如
UTF-8
中,3
个字节确定一个中文字符 。
所以,我们可以知道:系统中所有的信息(磁盘文件、内存中的程序、内存中的用户数据、网络传输数据···)都是由一串比特序列组成,区分不同数据对象的唯一方法依赖于读取这些数据对象时的上下文。
在不同的上下文中,同样的一串字节序列可能代表不同含义:整数、浮点数、字符串、机器指令。
源文件被翻译成可被系统执行文件
为了能够在系统上运行源文件编写的程序,系统必须将源文件翻译成可执行文件,这一步需要:编译器。
在Unix中,C语言提供的编译器是GCC,它是一个驱动程序,可以读取C文件并翻译成可执行文件。
linux > gcc -o hello hello.c
这个翻译过程分为四步:
- 预处理:根据以
#
字符开头的命令,修改源文件,将头文件的内容直接插入到源文件文本中。例如#include<stdio.h>
会把stdio.h
的内容插入到hello.c
文件的头部。这一步过后,会生成hello.i
文件,代表经过预处理后的C程序。 - 编译:编译器将
hello.i
文件中的C命令翻译成汇编指令,生成汇编语言程序hello.s
,汇编语言是通用的输出语言,与机器无关,很好地消除了不同编程语言之间的差异性。 - 汇编:汇编器将
hello.s
文件中的汇编指令翻译成机器语言指令,生成可重定位目标程序hello.o
二进制文件。 - 链接:链接器负责将程序中所有外部函数所属的二进制文件都合并到源二进制文件中,目的是让程序可以调用其它库提供的函数。在
hello.c
程序中,调用了prinf
函数,这个函数是C标准库中的一个函数,已经提前编译好prinf.o
文件,我们想使用这个函数,必须将这个函数引入到该文件内部,所以prinf.o
文件必须合并到hello.o
程序中。
链接完成后,生成一个可执行文件hello
,可以被系统加载到内存中,由系统执行。
系统读取可执行文件运行
在Unix中,输入可执行文件名可以加载程序并运行:
linux > ./hello
hello, world
我们需要探讨和了解的是:可执行文件被系统加载到内存中,如何识别出不同的指令从而达到在内存中运行的效果。
程序运行的过程其实并不复杂,可以抽象为这样一段话:我们编写的源文件最初存放在硬盘上,被系统加载到内存中,各种逻辑运算都在CPU寄存器上进行,然后将结果写回到内存中、保存在硬盘中或者通过I/O回显在设备上,这就是一个程序运行时主要在做的事情。
其中,各个过程内部是怎样运行的,内部涉及到计算机的各个组成结构相互协作,所以我们必须了解计算机中的各个不同的硬件,还有它们的作用。
计算机的基本硬件组成结构
计算机硬件组成主要有4个部分:
- 总线:负责在各个部件之间传递信息,传递的信息的基本单位是字(word),字代表一定长度的字节数,是一个系统参数,现在的机器字长大部分都是4个字节(32位)或者是8个字节(64位),所以我们日常生活中也常常说:32位机或者是64位机。
- I/O设备:I/O设备是系统与真实世界的联系通道,常见的设备有:鼠标、键盘、屏幕和磁盘,还有如打印机等等的I/O设备。每个I/O设备都会通过控制器或适配器与I/O总线相连,与系统进行交互。
- 适配器:能够在主板上进行替换,只要满足硬件接口的性质即可,例如显卡。
- 控制器:I/O 设备本身或者系统的主板上默认含有的芯片组,例如USB接口。
- 主存储器:系统运行时的临时存储设备,俗称”内存“,主存储器是一个线性的字节数组,每个字节都有唯一的索引地址(类比数组索引),每条数据可能会占用多个索引,例如,在64位计算机中,
short
占用2个字节,int
和float
占用4个字节,double
占用8个字节。 - 处理器(CPU):处理器是计算机的大脑,它内部主要包含3个非常重要的组成:算术逻辑单元(ALU)、寄存器、程序计数器(PC)。
- 寄存器:存储从主存储器传递过来的数据(写入),或者将寄存器的值写回到主存中(刷新)。
- 算术逻辑单元:从两个寄存器中取出数据复制到ALU中,ALU对其进行算术运算,并将结果存入一个寄存器中,覆盖原来的内容,并计算新的指令地址,传递给程序计数器。
- 程序计数器:指向主存中的某一条机器指令,从系统开始运行时,处理器会一直执行PC指令地址指向的机器指令,然后执行操作后,更新PC,继续执行下一条指令,如此往复。
- 高速缓存:由于主存和CPU寄存器之间的运行速度差异近1000倍,CPU从主存读取到寄存器,从寄存器写入到主存时效率过于低下,限制了CPU的执行效率,所以必须引入高速缓存作为中间存储区域,它的处理速度可以将CPU和主存之间的矛盾降到最低。
了解了计算机的基本组成后,就可以知道hello
可执行文件是如何被系统执行了。
首先从键盘输入"hello"时,键盘通过USB控制器,由系统总线传递到CPU寄存器中保存,CPU寄存器将"hello"写入到主存储器中。
当我们按下回车键时,shell会执行一系列指令加载可执行的hello文件,将文件里的数据从磁盘中加载到主存中保存。
然后处理器会在合适的时机,开始执行hello
程序的机器语言指令,将表示hello, world\n
字符串的所有字节都从主存复制到寄存器,然后从寄存器复制到显示设备,回显在屏幕上。