文章目录
- hello world
- 程序源文件的本质是0和1
- hello world文件的ASCII表示
- 程序被其他程序翻译成不同的格式
- 预处理阶段
- 编译阶段
- 汇编阶段
- 链接阶段
- 为什么需要了解编译系统的工作原理?
- 优化程序性能
- 理解链接时出现的错误
- 避免安全漏洞
- 参考
hello world
程序源文件的本质是0和1
hello程序的生命周期是从一个源程序(或者说源文件)开始的,即程序员通过编辑器创建并保存的文本文件,文件名是 hello.c。源程序实际上就是一个由值0和1组成的位(又称为比特)序列,8个位被组织成一组,称为字节。每个字节表示程序中的某些文本字符。
hello world文件的ASCII表示
大部分的现代计算机系统都使用 ASCII标准来表示文本字符,这种方式实际上就是用一个唯一的单字节大小的整数值来表示每个字符。
hello.c 程序是以字节序列的方式储存在文件中的。
每个字节都有一个整数值,对应于某些字符。
例如,第一个字节的整数值是 35,它对应的就是字符“# ”。第二个字节的整数值为 105,它对应的字符是“i,依此类推。
每个文本行都是以一个看不见的换行符“\n’来结束的,它所对应的整数值为 10。
像 hello.c 这样只由ASCII 字符构成的文件称为文本文件,所有其他文件都称为二进制文件。
hello.c 的表示方法说明了一个基本思想:
系统中所有的信息一一包括磁盘文件、内存中的程序、内存中存放的用户数据以及网络上传送的数据,都是由一串bit表示的。区分不同数据对象的唯一方法是我们读到这些数据对象时的上下文。
比如,在不同的上下文中,一个同样的字节序列可能表示一个整数、浮点数、字符串或者机器指令。
作为程序员,我们需要了解数字的机器表示方式,因为它们与实际的整数和实数是不同的。它们是对真值的有限近似值,有时候会有意想不到的行为表现。
程序被其他程序翻译成不同的格式
hello程序的生命周期是从一个高级 C语言程序开始的,因为这种形式能够被人读懂。
然而,为了在系统上运行 hello.c 程序,每条 C语句都必须被其他程序转化为一系列的低级机器语言指令。
这些指令按照一种称为可执行目标程序的格式打好包,并以二进制磁盘文件的形式存放起来。
目标程序也称为可执行目标文件。
在 Unix 系统上,从源文件到目标文件的转化是由编译器驱动程序完成的:
linux> gcc -o hello hello.c
在这里,GCC 编译器驱动程序读取源程序文件 hello.c,并把它翻译成一个可执行目标文件 hello。
这个翻译过程可分为四个阶段完成。
执行这四个阶段的程序(预处理器、编译器、汇编器和链接器)
一起构成了编译系统(compilation system)。
预处理阶段
预处理器(cpp)根据以字符#开头的命令,修改原始的 C 程序。比如hello.c中第 1行的#include <stdio.h> 命令告诉预处理器读取系统头文件stdio.h 的内容,并把它直接插入程序文本中。结果就得到了另一个 C程序,通常是以.i 作为文件扩展名。
编译阶段
编译器(cc1)将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。该程序包含函数 main的定义,如下:
main:
subq $8,%rsp
movl $.LCO,%edi
call puts
movl $0,%eax
addq $8,%rsp
ret
定义中 2~7行的每条语句都以一种文本格式描述了一条低级机器语言指令.
汇编语言是非常有用的,因为它为不同高级语言的不同编译器提供了通用的输出语言。
汇编阶段
接下来,汇编器(as)将 hello.s 翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标文件 hello.o中。
hello.o文件是一个二进制文件,它包含的 17 个字节是函数 main的指令编码。
如果我们在文本编辑器中打开 hello.o文件,将看到一堆乱码。
链接阶段
请注意,hello程序调用了 printf 函数,它是每个C编译器都提供的标准C库中的一个函数。
printf 函数存在于一个名为 printf.o的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的 hello.o程序中。链接器(ld)就负责处理这种合并。
结果就得到 hello 文件,它是一个可执行目标文件(或者简称为可执行文件),可以被加载到内存中,由系统执行。
为什么需要了解编译系统的工作原理?
对于像 hello.c 这样简单的程序,我们可以依靠编译系统生成正确有效的机器代码。
但是,有一些重要的原因促使程序员必须知道编译系统是如何工作的。
优化程序性能
现代编译器都是成熟的工具,通常可以生成很好的代码。作为程序员,我们无须为了写出高效代码而去了解编译器的内部工作。但是,为了在 C程序中做出好的编码选择,我们确实需要了解一些机器代码以及编译器将不同的 C语句转化为机器代码的方式。
比如,一个 switch 语句是否总是比一系列的 if-else 语句高效得多?
一个函数调用的开销有多?
while循环比 for循环更有效吗?
指针引用比数组索引更有效吗?
为什么将循环求和的结果放到一个本地变量中,会比将其放到一个通过引用传递过来的参数中,运行起来快很多呢?
为什么我们只是简单地重新排列一下算术表达式中的括号就能让函数运行得更快?
理解链接时出现的错误
根据我们的经验,一些最令人困扰的程序错误往往都与链接器操作有关,尤其是当你试图构建大型的软件系统时。
比如,链接器报告说它无法解析一个引用,这是什么意思?
静态变量和全局变量的区别是什么?
如果你在不同的 C文件中定义了名字相同的两个全局变量会发生什么?
静态库和动态库的区别是什么?
我们在命令行上排列库的顺序有什么影响?
最严重的是,为什么有些链接错误直到运行时才会出现?
避免安全漏洞
多年来,缓冲区溢出错误是造成大多数网络和 Internet 服务器上安全漏洞的主要原因。
存在这些错误是因为很少有程序员能够理解需要限制从不受信任的源接收数据的数量和格式。
学习安全编程的第一步就是理解数据和控制信息存储在程序栈上的方式会引起的后果。
参考
《深入理解计算机系统》