C 语言执行流程
C 语言程序成为高级语言的原因是它能够读取并理解人们的思想。然而,为了能够在系统中运行 程序,则各个 C 语句必须由其他程序转换为一系列低级机器语言指令。这些指令被打包作为,存储在二进制磁盘文件中。目标程序也称为可执行目标文件。
在 UNIX 系统中,从源文件到对象文件的转换是由执行完成的。
gcc 编译器驱动从源文件读取 ,并把它翻译成一个可执行文件 。这个翻译过程可用下图来表示:
这就是一个完整的 hello world 程序执行过程,会涉及几个核心组件:
预处理器、编译器、汇编器、连接器
,下面我们逐个击破。
,预处理器会根据开始的 字符,修改源 C 程序。
#include
命令就会告诉预处理器去读系统头文件 中的内容,并把它插入到程序作为文本。然后就得到了另外一个 C 程序,这个程序通常是以 为结尾。
然后是 ,编译器会把文本文件 翻译成文本,它包括一段。
编译完成之后是,这一步,会把 hello.s 翻译成机器指令,把这些指令打包成放在 hello.o文件中。它包含的 17 个字节是函数 main 的指令编码,如果我们在文本编辑器中打开 hello.o 将会看到一堆乱码。
最后一个是,我们的 hello 程序会调用 函数,它是 C 编译器提供的 C 标准库中的一部分。printf 函数位于一个叫做 文件中,它是一个单独的预编译好的目标文件,而这个文件必须要和我们的 hello.o 进行链接, 会处理这个合并操作。结果是,hello 文件,它是一个可执行的目标文件(或称为可执行文件),已准备好加载到内存中并由系统执行。
你需要理解编译系统做了什么
对于上面这种简单的 hello 程序来说,我们可以依赖来提供一个正确和有效的机器代码。然而,对于我们上面讲的程序员来说,编译器有几大特征你需要知道:
,现代编译器是一种高效的用来生成良好代码的工具。对于程序员来说,你无需为了编写高质量的代码而去理解编译器内部做了什么工作。然而,为了编写出高效的 C 语言程序,我们需要了解一些基本的机器码以及编译器将不同的 C 语句转化为机器代码的过程。
,在我们的经验中,一些非常复杂的错误大多是由链接阶段引起的,特别是当你想要构建大型软件项目时。
,近些年来,是造成网络和 Internet 服务的罪魁祸首,所以我们有必要去规避这种问题。
系统硬件组成
为了理解 hello 程序在运行时发生了什么,我们需要首先对系统的硬件有一个认识。下面这是一张 Intel 系统产品的模型,我们来对其进行解释。
:在整个系统中运行的是称为总线的电气管道的集合,这些总线在组件之间来回传输字节信息。通常总线被设计成传送定长的字节块,也就是 。字中的字节数(字长)是一个基本的系统参数,各个系统中都不尽相同。现在大部分的字都是 4 个字节(32 位)或者 8 个字节(64 位)。
:Input/Output 设备是系统和外部世界的连接。上图中有四类 I/O 设备:用于用户输入的键盘和鼠标,用于用户输出的显示器,一个磁盘驱动用来长时间地保存数据和程序。刚开始的时候,可执行程序就保存在磁盘上。
每个I/O 设备连接 I/O 总线都被称为 或者是 。控制器和适配器之间的主要区别在于封装方式。控制器是 I/O 设备本身或者系统的主印制板电路(通常称作主板)上的芯片组。而适配器则是一块插在主板插槽上的卡。无论组织形式如何,它们的最终目的都是彼此交换信息。
,主存是一个,而不是永久性存储,磁盘是 的设备。主存既保存程序,又保存处理器执行流程所处理的数据。从物理组成上说,主存是由一系列 动态随机存储构成的集合。逻辑上说,内存就是一个线性的字节数组,有它唯一的地址编号,从 0 开始。一般来说,组成程序的每条机器指令都由不同数量的字节构成,C 程序变量相对应的数据项的大小根据类型进行变化。比如,在 Linux 的 x86-64 机器上,short 类型的数据需要 2 个字节,int 和 float 需要 4 个字节,而 long 和 double 需要 8 个字节。
, 或者简单的处理器,是解释(并执行)存储在主存储器中的指令的引擎。处理器的核心大小为一个字的存储设备(或寄存器),称为。在任何时刻,PC 都指向主存中的某条机器语言指令(即含有该条指令的地址)。
从系统通电开始,直到系统断电,处理器一直在不断地执行程序计数器指向的指令,再更新程序计数器,使其指向下一条指令。处理器根据其指令集体系结构定义的指令模型进行操作。在这个模型中,指令按照严格的顺序执行,执行一条指令涉及执行一系列的步骤。处理器从程序计数器指向的内存中读取指令,解释指令中的位,执行该指令指示的一些简单操作,然后更新程序计数器以指向下一条指令。指令与指令之间可能连续,可能不连续(比如 jmp 指令就不会顺序读取)。
下面是 CPU 可能执行简单操作的几个步骤:
:从主存中拷贝一个字节或者一个字到内存中,覆盖寄存器先前的内容。
:将寄存器中的字节或字复制到主存储器中的某个位置,从而覆盖该位置的先前内容。
:把两个寄存器的内容复制到 。把两个字进行算术运算,并把结果存储在寄存器中,重写寄存器先前的内容。
算术逻辑单元(ALU)是对数字二进制数执行算术和按位运算的组合数字电子电路。
领取专属 10元无门槛券
私享最新 技术干货