hello 程序执行背后的故事 博客分类: 操作系统 helloWorld编译系统系统硬件组成
程序员文章站
2024-03-18 14:09:40
...
源文件 hello.c 的代码如下:
要运行该程序,需要编译器驱动程序将其翻译成可执行的目标文件 hello,这个过程可分为如下图所示的四个阶段,执行这四个阶段的程序(预处理器、编译器、汇编器和链接器)一起构成了编译系统。
其中各阶段所做的事情如下。
* 预处理阶段:预处理器 cpp 会根据以字符“#”开头的命令来修改 C 程序,比如“#include <stdio.h>”命令会告诉预处理器读取头文件 stdio.h 的内容,并把它直接插入到程序文本中,于是得到一个通常以“.i”作为扩展名的 C 程序。
* 编译阶段:编译器 ccl 将文本文件 hello.i 翻译成汇编语言程序文本文件 hello.s。每条汇编语句都以一种标准的文本格式确切地描述了一条低级机器语言指令。
* 汇编阶段:汇编器 as 将 hello.s 翻译成机器语言指令,并将这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,最后将结果保存在二进制的目标文件 hello.o 中。
* 链接阶段:链接器 ld 负责将使用到的其他目标文件(如 hello.c 中调用了 printf 函数,而该函数位于另一个目标文件 printf.o 中)合并到 hello.o 中,之后才能得到可执行的目标文件 hello,它可被系统加载到内存中执行。
翻译后的可执行文件 hello 存放于磁盘上,为理解运行该程序时发生了什么,需要先了解下图所示的一个典型系统的硬件组织。
在此需要明确图中的几个概念。
* 总线:贯穿整个系统的一组电子管道,用于在各个部件间传递字节信息。一般被设计成传送定长的字节块,也就是字。字中的字节数(即字长)是一个基本的系统参数,多为 4 个字节或是 8 个字节。
* I/O 设备:输入/输出(I/O)设备是系统与外界的联系通道,如图中作为用户输入的键盘和鼠标,作为用户输出的显示器,以及用于长期存储数据的磁盘等。每个 I/O 设备都通过一个控制器或适配器与 I/O 总线相连,两者的区别主要在于封装方式的不同。控制器是置于 I/O 设备本身的或者系统的主印制电路板(通常也称主板)上的芯片组,而适配器则是一块插在主板插槽上的卡。但无论如何,它们的功能都是在 I/O 总线和 I/O 设备之间传递信息。
* 主存:处理器执行程序时用来存放程序和数据的一个临时存储设备。从物理上来说,它是由一组动态随机存取存储器(DRAM)芯片组成的。从逻辑上来说,它是一个线性的字节数组,每个字节都有唯一的从零开始的地址(即数组索引)。一般组成程序的每条机器指令都由不同数量的字节所构成。
* 处理器:即*处理单元(CPU),是解释或执行存储在主存中指令的引擎。它的核心是一个字长的存储设备(或寄存器),称为程序计数器(PC),PC 在任何时刻都指向主存中的某条机器语言指令。
从系统通电开始,直到系统断电,处理器一直在不断地执行 PC 指向的指令,然后再更新 PC,使其指向下一条指令,而这条指令并不一定与刚执行的那条相邻。这样的操作是围绕着主存、寄存器文件和算术/逻辑单元(ALU)进行的。寄存器文件是一个小的存储设备,由一些 1 字长的寄存器组成,每个寄存器都有唯一的名字。ALU 计算新的数据和地址值。
CPU 在指令的要求下可能会执行以下操作:
* 加载:把一个字节或者一个字从主存复制到寄存器。
* 存储:把一个字节或者一个字从寄存器复制到主存的某个位置。
* 操作:把两个寄存器的内容复制到 ALU,以对这两个字做算术操作,并将结果存放到一个寄存器中。
* 跳转:从指令本身中抽取一个字,并将其复制到 PC 中,以覆盖 PC 中原来的值。
了解这些后,再来看一下执行 hello 程序时所发生的事情。
当在 Shell 中使用键盘输入“./hello”时,Shell 会逐一将字符读入寄存器,再把它存放到存储器中。这个过程如下图所示:
Shell 会在用户敲了回车键后结束命令的输入,然后执行一系列指令来加载可执行的 hello 文件,并将其中的代码和数据从磁盘复制到主存。这里的数据包括最终会被输出的字符串“hello, world\n”。利用直接存储器存取(DMA)技术,数据可以不通过处理器而直接从磁盘到达主存。这个过程如下图:
再将目标文件 hello 中的代码和数据加载到主存后,处理器就开始执行 main 函数中的机器语言指令,这些指令将“hello, world\n”从主存复制到寄存器文件,再从寄存器文件中复制到显示设备,最终显示在屏幕上。如下图所示:
从这个例子中还可以看出,系统花了大量的时间把信息从一个地方复制到另一个地方。从程序员的角度来看,这些复制就是开销,减缓了程序“真正”的工作。再加上处理器访问寄存器、主存和磁盘之间存在巨大的速率差异,因此很有必要加快这些复制操作的完成。这也是高速缓存存储器(简称高速缓存)得以发展的原因。
高速缓存一般是用一种叫做静态随机访问存储器(SRAM)的硬件技术实现的。常用的高速缓存一般分为三级:L1、L2 和 L3,它们的大小逐级递增,但处理器访问它们的速度却逐级递减。系统可以获得一个很大的存储器,同时访问速度也很快,就是利用了高速缓存的局部性原理,即程序具有访问局部区域里的数据和代码的趋势。通过在高速缓存里存放经常访问的数据的方法,大部分的存储器操作都能在快速的高速缓存中完成。下图就是一个典型的存储器层次结构。其中,从上到下,设备的访问速度变得越来越慢、容量越来越大,价格也越来越便宜。每一层都可作为低一层的高速缓存,比如,寄存器文件作为 L1 的高速缓存,L1 作为 L2 的高速缓存,依次类推。
参考书籍:
1、《深入理解计算机系统》第一章——计算机系统漫游。
#include <stdio.h> int main(){ printf("hello world!"); return 0; }
要运行该程序,需要编译器驱动程序将其翻译成可执行的目标文件 hello,这个过程可分为如下图所示的四个阶段,执行这四个阶段的程序(预处理器、编译器、汇编器和链接器)一起构成了编译系统。
其中各阶段所做的事情如下。
* 预处理阶段:预处理器 cpp 会根据以字符“#”开头的命令来修改 C 程序,比如“#include <stdio.h>”命令会告诉预处理器读取头文件 stdio.h 的内容,并把它直接插入到程序文本中,于是得到一个通常以“.i”作为扩展名的 C 程序。
* 编译阶段:编译器 ccl 将文本文件 hello.i 翻译成汇编语言程序文本文件 hello.s。每条汇编语句都以一种标准的文本格式确切地描述了一条低级机器语言指令。
* 汇编阶段:汇编器 as 将 hello.s 翻译成机器语言指令,并将这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,最后将结果保存在二进制的目标文件 hello.o 中。
* 链接阶段:链接器 ld 负责将使用到的其他目标文件(如 hello.c 中调用了 printf 函数,而该函数位于另一个目标文件 printf.o 中)合并到 hello.o 中,之后才能得到可执行的目标文件 hello,它可被系统加载到内存中执行。
翻译后的可执行文件 hello 存放于磁盘上,为理解运行该程序时发生了什么,需要先了解下图所示的一个典型系统的硬件组织。
在此需要明确图中的几个概念。
* 总线:贯穿整个系统的一组电子管道,用于在各个部件间传递字节信息。一般被设计成传送定长的字节块,也就是字。字中的字节数(即字长)是一个基本的系统参数,多为 4 个字节或是 8 个字节。
* I/O 设备:输入/输出(I/O)设备是系统与外界的联系通道,如图中作为用户输入的键盘和鼠标,作为用户输出的显示器,以及用于长期存储数据的磁盘等。每个 I/O 设备都通过一个控制器或适配器与 I/O 总线相连,两者的区别主要在于封装方式的不同。控制器是置于 I/O 设备本身的或者系统的主印制电路板(通常也称主板)上的芯片组,而适配器则是一块插在主板插槽上的卡。但无论如何,它们的功能都是在 I/O 总线和 I/O 设备之间传递信息。
* 主存:处理器执行程序时用来存放程序和数据的一个临时存储设备。从物理上来说,它是由一组动态随机存取存储器(DRAM)芯片组成的。从逻辑上来说,它是一个线性的字节数组,每个字节都有唯一的从零开始的地址(即数组索引)。一般组成程序的每条机器指令都由不同数量的字节所构成。
* 处理器:即*处理单元(CPU),是解释或执行存储在主存中指令的引擎。它的核心是一个字长的存储设备(或寄存器),称为程序计数器(PC),PC 在任何时刻都指向主存中的某条机器语言指令。
从系统通电开始,直到系统断电,处理器一直在不断地执行 PC 指向的指令,然后再更新 PC,使其指向下一条指令,而这条指令并不一定与刚执行的那条相邻。这样的操作是围绕着主存、寄存器文件和算术/逻辑单元(ALU)进行的。寄存器文件是一个小的存储设备,由一些 1 字长的寄存器组成,每个寄存器都有唯一的名字。ALU 计算新的数据和地址值。
CPU 在指令的要求下可能会执行以下操作:
* 加载:把一个字节或者一个字从主存复制到寄存器。
* 存储:把一个字节或者一个字从寄存器复制到主存的某个位置。
* 操作:把两个寄存器的内容复制到 ALU,以对这两个字做算术操作,并将结果存放到一个寄存器中。
* 跳转:从指令本身中抽取一个字,并将其复制到 PC 中,以覆盖 PC 中原来的值。
了解这些后,再来看一下执行 hello 程序时所发生的事情。
当在 Shell 中使用键盘输入“./hello”时,Shell 会逐一将字符读入寄存器,再把它存放到存储器中。这个过程如下图所示:
Shell 会在用户敲了回车键后结束命令的输入,然后执行一系列指令来加载可执行的 hello 文件,并将其中的代码和数据从磁盘复制到主存。这里的数据包括最终会被输出的字符串“hello, world\n”。利用直接存储器存取(DMA)技术,数据可以不通过处理器而直接从磁盘到达主存。这个过程如下图:
再将目标文件 hello 中的代码和数据加载到主存后,处理器就开始执行 main 函数中的机器语言指令,这些指令将“hello, world\n”从主存复制到寄存器文件,再从寄存器文件中复制到显示设备,最终显示在屏幕上。如下图所示:
从这个例子中还可以看出,系统花了大量的时间把信息从一个地方复制到另一个地方。从程序员的角度来看,这些复制就是开销,减缓了程序“真正”的工作。再加上处理器访问寄存器、主存和磁盘之间存在巨大的速率差异,因此很有必要加快这些复制操作的完成。这也是高速缓存存储器(简称高速缓存)得以发展的原因。
高速缓存一般是用一种叫做静态随机访问存储器(SRAM)的硬件技术实现的。常用的高速缓存一般分为三级:L1、L2 和 L3,它们的大小逐级递增,但处理器访问它们的速度却逐级递减。系统可以获得一个很大的存储器,同时访问速度也很快,就是利用了高速缓存的局部性原理,即程序具有访问局部区域里的数据和代码的趋势。通过在高速缓存里存放经常访问的数据的方法,大部分的存储器操作都能在快速的高速缓存中完成。下图就是一个典型的存储器层次结构。其中,从上到下,设备的访问速度变得越来越慢、容量越来越大,价格也越来越便宜。每一层都可作为低一层的高速缓存,比如,寄存器文件作为 L1 的高速缓存,L1 作为 L2 的高速缓存,依次类推。
参考书籍:
1、《深入理解计算机系统》第一章——计算机系统漫游。