1. 从源文件到可执行程序的过程
这里以最简单的例子 helloworld 为例,新建一个 hello.c 的源文件,添加如下代码
#include "stdio.h"
int main()
{
printf("hello world \n");
return 0;
}
使用 gcc -o hello.c hello 指令可以将源文件转换为可执行程序,-o 选项可以指定可执行程序的名称,不指定该选项时默认为输出一个 a.out 的可执行程序。
在执行上述指令的过程中,gcc 读取目标源文件,并将其翻译成可执行文件,期间可分为四个阶段:
(1). 预处理阶段
程序根据 # 开头的指令,修改原始的 c 程序,如 #include "stdio.h" ,这里告诉预处理器读取对应系统头文件 stdio.h 的内容,并直接插入到程序文本中,得到一个 .i 为后缀的文件,可以通过
gcc -E hello.c 得到该文件,但此时只会获得该预处理后文件的输出,如果想要保存该文件则需使用 gcc -E hello.c -o hello.i 。
(2). 编译阶段
程序会将 hello.i 文件翻译成 hello.s 文件,其包含一个汇编语言程序。可以通过 gcc -S hello.c 来输出该汇编文件。
(3). 汇编阶段
接下来将 hello.s 翻译成机器语言指令,把这些指令打包成一个叫做可重定位目标程序的格式,并将结果保存在一个叫做 hello.o 的文件中,这是一个二进制文件,是 hello.s 的指令编码。可以通过 gcc -c hello.c 来生成 hello.o 文件。
(4). 链接阶段
在 hello.c 文件中,可以看到其调用了 printf 函数,其存在于标准 C 库中,printf 函数存在于一个名为 printf.o 的单独预编译好了的目标文件中,而我们调用了这个函数,因此需要将其合入到我们的 hello 程序中,链接器就负责处理这种合并,将所有 .o 文件合并结果得到一个 hello 程序,可以被加载到内存当中被系统执行。
2. 系统的硬件组成
(1). 总线
总线贯穿了整个系统,是一组电子管道,主要负责信息的传输,通常总线设计为传送定长的字节块,即字,字的字节数(也叫字长)是一个基本的系统参数,不同系统不尽相同,目前大多数机器字长多为 4 个字节(32位)或 8 个字节(64位)。
(2). I/O 设备
I/O 设备是系统与外部连接的通道,这里常用的 I/O 设备主要有键盘鼠标,显示器,磁盘等。每个 I/O 设备都通过一个 控制器 或者 适配器 与 I/O 总线相连接。控制器和适配器的区别主要在于封装方式上的不同,控制器通常为 I/O 设备本身或者系统的主板上的芯片组,而适配器通常为一块插在主板插槽上的卡,但其功能基本上是一致的,即为 I/O 总线和 I/O 设备之间传递信息。
以上为一个典型系统的硬件组成。
(3). 主存
主存是一个临时存储设备,即我们常说的内存,主要用来存放程序和程序相关的数据,从物理的层面来说,主存是一组动态随机存储器(DRAM)芯片组成,从逻辑上说,主存是一个线性的字节数组,每个字节都有其索引(地址),地址从零开始。
(4). 处理器
中央处理单元(CPU),简称处理器,是解释(或执行)存储在主存中指令的引擎,处理器的核心是一个大小为一个字节的存储设备(或寄存器),称为程序计数器(PC),在任何时刻,PC 都指向主存中的一条机器指令。从系统通电开始到断电,处理器一直在不断执行程序计数器指向的指令,并且不断更新该程序计数器,使用指向下一条指令。处理器从程序计数器指向的内存处读取指令,解释该指令,执行指令的操作,然后更新程序计数器,使其指向下一条指令,而下条指令未必与本次执行的指令相邻(地址上),如函数调用。