背景: 其实我的这个进阶计划在大一下结束的那个暑假, 也就是2019.8月份的时候就制定了, 但由于一些原因(可能以后会写一篇与我编程经历有关的文章, 在那个里面再解释吧), 这个计划效果很不理想.
听过很多知乎上的大佬说过, 要想学透知识, 不仅得输入, 还得输出, 所以我打算在我的博客开启这样一个进阶之路系列的专栏, 第一个系列之所以取名为 重学操作系统, 是因为其实我已经啃过两遍 深入理解计算机系统这本书啦! 但我很不满意QwQ, 所以重学一遍, Let's Go !!!
1. 编译语言与解释语言
我们平时敲代码用的都是高级语言(汇编神犇除外), 计算机是不能够直接理解高级语言的, 所以我们要把高级语言翻译成计算机能够理解的语言, 也即机器语言, 这个翻译的时间不同就造成了编译语言和解释语言的根本差别.
编译语言在程序执行之前, 将源代码通过编译器, 然后生成一个目标文件, 下次执行的时候就不用再编译了, 直接执行这个可执行文件就好了, 就好比一大桌子菜做好了, 端上了桌子, 大家直接吃这个桌子上面的菜就行了.
而解释型语言是边编译边运行, 就好比吃火锅, 你边吃边往火锅里面烫菜, 继续吃, 继续烫菜.
再举点例子吧
编译型语言的代表是C,源代码被编译之后生成中间文件(.o和.obj),然后用连接器和汇编器生成机器码,也就是一系列基本操作的序列,机器码最后被执行生成最终动作.
解释型语言的代表是Python, 对于Python而言,python源码不需要编译成二进制代码,它可以直接从源代码运行程序.当我们运行python文件程序的时候, python解释器将源代码转换为字节码,然后再由python解释器来执行这些字节码. 在Python解释器的内部, 它又将这些字节码给翻译成对应平台上的机器码, 然后再运行, 可想而知, 它的效率很低.
还有一种比较特殊, 我也不太清楚该把它归为哪一类, 它编译了两次, 比如说Java, 大家肯定都知道 java一次编译, 到处运行, 但有没有想过是为什么呢? 喜欢用命令行的朋友肯定都知道, java的编译命令是javac, 它生成了一个.class字节码文件, 那它有什么用呢? 它的用处就是将java源代码翻译成所有java虚拟机(JVM)都认识的一种语言, 不同平台上的java虚拟机将java字节码翻译成不同平台上的机器码, 虚拟机内的编译器叫做JIT编译器, 大家感兴趣的可以自己去了解一下.
2. 静态类型
静态类型就是在编译时类型就可以确定, 与之相对应的有动态类型, 它的类型要等到程序运行时才能够确定, 所以很多在编译期间就能发现的错误, 动态语言比如Python要等到运行时才能抛出异常, 比如我之前学 python的时候, 经常就是某个库的第几千行抛出一个异常, 然后回溯了一大段报错信息, 我人都傻了(当然, 可能是由于我太菜了QwQ).
举个简单的例子
在python中, 你可以写这样一个函数
def add (x, y):
return a + b
与之对应的, 可以写这样一个c程序
int add (int x, int y) {
return x + y;
}
静态类型的优点:
- 由于存在类型声明, 所以在编译的过程中, 如果该函数在其他的地方被调用了, 那么编译器就会检查参数是否符合类型声明, 不符合的话就会报错, 更安全.
- 由于这种检查发生在编译期, 故运行时更快
- 编译时的类型声明可以节省空间. 这一点可能您不太理解, 因为对于动态类型, 比如
python. 而在编译语言中, 变量的名称只会在编译的过程中存在, 在运行时是不存在的. 编译器为每个变量选择一个位置, 并将这个位置作为所编译的变量的一部分, 这个位置就是地址, 在运行期间, 每个变量的值都储存在它的地址的地方, 但变量名不存储.
3. 编译的过程
我们应该知道编译器在编译的时候做了什么, 这样我们看到报错信息的时候就会更容易的知道哪里出了问题, 从计算机的角度思考, 我们也能写出更容易被编译器优化的代码, 程序的执行效率也就会更高.
- 预处理: 比如说c语言里面的预处理指令
#include, 这个预字代表的是在编译之前进行处理, 它就是将你所include的头文件的源码插入进来, 仅此而已. - 解析: 可以想象一下你用谷歌搜索时, 输入了一个很长的句子, 然后谷歌的搜索引擎做了什么? 是不是把这个句子解析出一个个的词条? 编译器也类似, 在解析过程中,编译器读取源代码,并构建程序的内部表示,称为“抽象语法树”(AST).这一阶段的错误检测通常为语法错误.
- 静态检查: 这一阶段的检查就是检查你的类型与值是否对应, 你的函数的参数的类型与个数是否对应, 返回值类型是否对应等等.这一阶段的错误称为"静态语义"错误.
- 代码生成: 将代码生成字节码或者机器码
- 链接: 如果程序使用了定义在库中的函数, 那么编译器会从相应的库中找到对应的函数并把它给链接进来
- 优化: 就是一些优化, 优化级别你可以自己选, 举个例子, 比如说
while (i > 0) {
...
}
它可能给你优化成
while(i != 0) {
...
}
这是我曾经学过深入理解计算机系统, 里面讲了一些汇编, 然后我看的反汇编代码发现它居然优化成这个鬼东西, 不过可能有别的优化, 比如说将一些比较短的函数优化成内联代码这种, 也别太靠编译器, 还是得靠咱程序员嘛.
通常你运行 gcc 这个命令的时候, 它会执行上面的所有步骤, 并生成一个可执行文件.
下面以我活到现在写过的最牛逼的一个程序举例
然后编译运行它
当然你也可以用 -o参数指定文件名, 这些不细谈.
4. 目标代码
gcc命令的-c参数 可以生成目标代码, 目标代码不是可执行代码, 但是可以连接到可执行文件中去
目标代码(英语:object code)指计算机科学中编译器或汇编器处理源代码后所生成的代码,它一般由机器代码或接近于机器语言的代码组成。[1]目标文件(英语:object file)即存放目标代码的计算机文件,它常被称作二进制文件(binaries)。 目标文件包含着机器代码(可直接被计算机中央处理器执行)以及代码在运行时使用的数据,如重定位信息,如用于链接或调试的程序符号(变量和函数的名字),此外还包括其他调试信息。[2]目标文件是从源代码文件产生程序文件这一过程的中间产物,链接器正是通过把目标文件链接在一起来生成可执行文件或库文件。目标文件中唯一的要素是机器代码 来源:维基百科
5. 汇编代码
gcc的-S参数生成汇编代码, 汇编代码是最接近机器码的, 对于神犇们来说, 也是能够看得懂的. 类似于下图所示
在不同的架构的机器上, 生成的汇编代码是不同的, 比如我现在是intel的机子, 它给我生成的就是x86指令集的汇编代码.
小声吐槽, x86巨难
6. 可执行文件
可执行的文件, 是通过加载器, 加载到内存之中, 供CPU调用执行.