本系列文章以我的个人博客的搭建为线索(GitHub 仓库:Evian-Zhang/evian-blog),记录我在现代化程序设计中的一些笔记。在本篇文章中,我想就编译方法作一些讨论。
运行平台简介
我们的计算机执行一个程序,其本质的过程如下:
- 我们告诉操作系统,要执行某个程序
- 操作系统查看该程序,并告诉CPU如何执行这个程序
- CPU开始执行程序
在这个过程中,我们可以发现,影响一个程序执行的因素有两个:操作系统决定这个程序该如何执行,以及CPU根据程序执行相应的指令。
“操作系统决定这个程序该如何运行”究竟是指什么呢?以可执行文件a.exe
为例,它实际上就是一串二进制数组成的序列。一个可执行文件实际上并不全是需要CPU执行的指令,也就是说,操作系统不会告诉CPU,直接从这串二进制串的第一个数位开始,一直执行到结束。相反,一个可执行程序是具有精妙的结构的。这是因为一个可执行程序除了可执行的代码,还有许多别的东西需要保存。比如说,C语言中的一个静态全局变量:
static int a = 114514;
在大多数操作系统平台中,其并不会位于可执行文件的代码区,而是单独存储在一个数据区。通过这样的做法,可以让处于代码区的任何一个指令读取这个数据。当然,可执行文件内部分区的作用还有很多,这里就不再介绍了。
但是,可执行文件分区并没有一个统一的标准,Windows的PE文件结构,macOS的mach-O文件结构,Linux的elf文件结构等等,有各种各样对可执行文件的分区。因此,一个可执行文件只有在特定的操作系统中才能被正确执行,也就是说只有特定的操作系统,才能识别这个可执行文件到底应该让CPU执行哪一部分程序,而把哪一部分数据载入内存。
此外,除了特别简单的程序以外,大部分程序还必须链接操作系统提供的系统库。这是因为有许多必要的功能都是只有操作系统才能提供的,比如说打开文件、打开套接字等等功能。这些功能,不同的操作系统中有不同的实现。所以,链接了某个操作系统的系统库的程序也只能在相应的操作系统中运行。
至于CPU执行程序,也有其特殊性。就像刚刚说的,CPU执行的实际上是一串二进制数组成的序列。那么,它怎么知道这串二进制序列对应哪一个指令呢?这一过程也是没有统一标准的。因此,Intel的x86_64的CPU有一套二进制串对应指令的字典,AMD的CPU也有一套,PowerPC的CPU也有一套。那么,我们编写的可执行程序也必须有关于CPU的特异性,也就是说,能在某个CPU上运行的可执行程序,未必能使用另一种CPU运行。
根据上面的讨论,我们可以发现,如果我们的程序需要在m个操作系统的n个CPU上运行,那么总共需要产生mn种不同的可执行程序。因此,一个操作系统+一个CPU,就构成了程序运行的一个平台。
编译型语言与解释型语言
最早的开发者,就是纯手打CPU指令序列,或者其对应的汇编语言,来生成一个又一个可执行文件。那么,就像上面所说的,如果要适应m个操作系统的n个CPU,就要写mn种不同的程序,但它实现的功能却是相同的。这显然是一种非人的行径。它的开发方式如图所示
因此,人们就开始思考有什么解决方法。