第一章,计算机系统漫游,很有意思。作者把一段程序从文本到执行,捏碎了讲。我这边不求甚解的搬运一遍。
入口-文本文件
首先,计算机被要求完成人所布置的任务。所以,计算机程序的输入理所当然就是人类的语言。下面是用人类的语言描述的,让计算机打印Hello World到标准输出的文本文件:
$ cat HelloWorld.c
#include <stdio.h>
int main() {
printf("%s", "HelloWorld");
return 0;
}
因为计算机只能表示0/1,所以这段文本在计算机中是以ASCII码表示的。现在我们来看一下这段文本文件的真身,顺便扔上鬼子电报员来照号码簿的翻译:
$ hexdump -C HelloWorld.c
00000000 23 69 6e 63 6c 75 64 65 20 3c 73 74 64 69 6f 2e |#include <stdio.|
00000010 68 3e 0a 69 6e 74 20 6d 61 69 6e 28 29 20 7b 0a |h>.int main() {.|
00000020 09 70 72 69 6e 74 66 28 22 25 73 22 2c 20 22 48 |.printf("%s", "H|
00000030 65 6c 6c 6f 57 6f 72 6c 64 22 29 3b 0a 09 72 65 |elloWorld");..re|
00000040 74 75 72 6e 20 30 3b 0a 7d 0a |turn 0;.}.|
0000004a
看到了吧,就是这么存的。其中把字符转化成ascii码的过程称为encode/编码,把ascii码转化为字符的过程称为decode/解码。
当然,计算机现在还是不知道我们到底要干什么,还得继续翻译。
第二阶段-翻译成机器码
翻译的过程非常冗长并且对我而言极其枯燥。甚至有一本厚厚的龙书来讲这个所谓的编译系统。我知其然就行:
┌───────┐ helloworld.c ┌─────┐ helloworld.i ┌─────┐ helloworld.s ┌────┐ helloworld.o ┌────┐ helloworld ┌────────┐
│ input │ ──────────────> │ cpp │ ──────────────> │ ccl │ ──────────────> │ as │ ──────────────> │ ld │ ────────────> │ output │
└───────┘ └─────┘ └─────┘ └────┘ └────┘ └────────┘
因为graph-easy对中文和我都不友好,所以图里面的关键组件都用了英文缩写名,具体含义以及工作如下:
cpp: 预处理器
因为我们的程序使用了外部代码printf,所以他需要将外部代码的头文件,也就是stdio.h插入到我们的文本文件中,以便后续翻译过程中能够定位并执行这个我们使用的外部函数printf。我们查看一下中间产物,helloworld.i,看看到底发生了什么变化:
$ cpp HelloWorld.c -o helloworld.i
$ less helloworld.i
ctrl+g直接滚到最后,可以看到自己写的代码安静地躺着,但是前面多了八百行又长又看不懂的代码。这就是cpp的工作,将我引用的头函数插入我的文本中。我们可以查看一下/usr/include/stdio.h文件,看看cpp是不是如实干了上面的任务。不过因为stdio又include了一大堆头文件,cdef/feature什么的,所以已经超过我的阅读能力了。
ccl: 编译器
编译就是将高级语言翻译到汇编语言的过程。在座的诸位应该都写过汇编,所以这里对汇编也没什么好介绍的,大家直接看汇编代码:
$ gcc -S HelloWorld.c -o helloworld.s
$ less helloworld.s
...
movl $.LC0, %esi
movl $.LC1, %edi
movl $0, %eax
call printf
...
可以看到在汇编程序中
- 首先定义了一个字符串HelloWorld
- 然后声明了一个function main
- 然后是function main的汇编代码。main函数中的汇编代码,就是放了一堆执行参数进寄存器,紧接着调用了一下printf函数。
有意思的是,在汇编的过程中,原本helloworld.i里面的各种声明,比如extern/typedef/struct,都不会被翻译为汇编代码。于是helloworld.s又变成了短短30行的文本文件helloworld.s。
as: 汇编器
汇编器的工作是将汇编代码转化为计算机看得懂的机器指令。举个十分不恰当的栗子,假如我这边定义一个新的cpu指令集ZISC,其中movl的机器码是0x110,立即数0就是0,寄存器寻址%eax的hex进制表示是0x233,那么movl $0, %eax这句汇编,按照ZISC翻译成机器码就是:
0x110 0x0 0x233
这种机器码就是处理器cpu看得懂的机器指令,他的指令集有很多种,比如RISC类和CISC类和我刚刚发明的ZISC,这两种架构的竞争发展很有意思,大家可以逛一下贴吧,当做茶余饭后的八卦RISC和CISC。
ld: 链接器
因为Helloworld这个程序还蛮复杂的,需要调用外部函数printf。而在编译阶段大家可以发现printf函数的汇编代码并不在helloworld.s中。其实printf函数已经被编译完,作为一个动态链接库的一部分安静地躺在/lib/x86_64-linux-gnu/libc.so.6里面。到底是怎么定位到libc.so内部的printf的相关机器指令的,我以后再深究。
$ gcc HelloWorld.c -o helloworld
# 看一下helloworld动用了哪几个动态链接库
$ ldd helloworld
...
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f807b809000)
...
# 看一下动态链接库libc里面有没有printf函数
$ nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep -w printf
0000000000055800 T printf
# 有的,就是决定是它了
这里如果include的是自己写的另一段程序的函数,那么到ld之前,汇编器应该已经产生了两个机器码文件,a.o和b.o。ld的任务就是把这两个球搓成一个球。
出口-可执行文件
由人类用英语写的文本文件,经过gcc一阵翻译,最终变成了一堆cpu指令集表示的机器码。到现在为止,计算机终于看得懂了,可以去执行人类的任务了。