简单编译原理#
1 hello, world在计算机的表示#
hello 程序的生命周期是从一个源程序(或者说源文件)开始的,即程序员通过编辑器创建并保存的文本文件,文件名是 hello.c。源程序实际上就是一个由值 0 和 1组成的位(又称为比特)序列,8 个位被组织成一组,称为字节。每个字节表示程序中的某些文本字符
大部分计算机使用 ASCII 标准来表示文本字符
- 用一个唯一的单字节大小的整数值息来表示每个字符
- hello.c 程序是以字节序列的方式储存在文件中的
hello.c 的表示方法说明了一个基本思想∶ 系统中所有的信息——包括磁盘文件、内存中的程序、内存中存放的用户数据以及网络上传送的数据,都是由一串比特表示的
2 编译过程#
hello 程序的生命周期从一个高级 C 语言程序开始
为了在系统上运行 hello.c 程序,每条 C 语句都必须被其他程序转化为一系列的低级机器语言指令
然后这些指令按照一种称为可执行目标程序的格式打好包,并以二进制磁盘文件的形式存放起来
GCC 编译器读取源程序文件 hello.c,并把它翻译成一个可执行目标文件 hello。这个翻译过程可分为四个阶段完成,如下图所示
执行这四个阶段的程序(预处理器、编译器、汇编器和链接器)一起构成了编译系统(compilation system)
2.1 预处理阶段#
预处理器(cpp)
根据以字符#开头的命令,修改原始的 C 程序。比如 hello.c中第 1行的#include < stdio.h>命令告诉预处理器读取系统头文件 stdio.h 的内容,并把它直接插入程序文本中。结果就得到了另一个 C程序,通常是以.i作为文件扩展名。
2.2 编译阶段#
编译器(ccl)
将文本文件 hello.i翻译成文本文件 hello.s,它包含一
个汇编语言程序。该程序包含函数 main 的定义,如下所示:
main:
subq $8, %rsp
mov1 $.LCO,%edi
call puts
mov1 $0,%eax
addq $8,%rsp
ret
每条语句都以一种文本格式描述了一条低级机器语言指令。汇编语言非常有用,它为不同高级语言的不同编译器提供了通用的输出语言
2.3 汇编阶段#
汇编器(as)
将 hello.s 翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标文件 hello.o中。
hello.o 文件是一个二进制文件,它包含的17 个字节是函数 main的指令编码。如果我们在文本编辑器中打开 hello.o文件,将看到一堆乱码。
2.4 链接阶段#
注意,hello程序调用了 printf 函数,它是每个 C 编译器都提供的标准 C 库中的一个函数。printf 函数存在于一个名为 printf.o 的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的 hello.o 程序中。
链接器(ld)
就负责处理这种合并。结果就得到 hello 文件,它是一个可执行目标文件(或者简称为可执行文件),可以被加载到内存中,由系统执行。
3 hello, world的执行过程#
第一步#
- shell 等待我们输入一个命令
- 当我们在键盘上输入字符串"./hello"(注意这里是编译好的可执行目标文件)后
- shell 程序将字符逐一读入寄存器
- 再把它存放到内存中
第二步#
- 当我们在键盘上敲回车键时,shell 程序就知道我们已经结束了命令的输人
- 然后 shell 执行一系列指令来加载可执行的 hello 文件
- 这些指令将 hello 目标文件中的代码和数据从磁盘复制到主存
- 数据包括最终会被输出的字符串"hello,world\n"。
第三步#
- 一旦目标文件 hello 中的代码和数据被加载到主存
- 处理器就开始执行 hello 程序的 main 程序中的机器语言指令
- 这些指令将 "hello,world\n" 字符串中的字节从主存复制到寄存器文件
- 再从寄存器文件中复制到显示设备,最终显示在屏幕上
4 程序在计算机内的存储#
上面的例子揭示了一个重要的问题,即系统“似乎”花费了大量的时间和步骤把信息从一个地方挪到另一个地方
- hello程序的机器指令最初是存放在磁盘上
- 当程序加载时,它们被复制到主存
- 当处理器运行程序时,指令又从主存复制到处理器
相似地,数据串 "hello,world\n"开始时在磁盘上,然后被复制到主存,最后从主存上复制到显示设备