漫游-翻译阶段

第一章,计算机系统漫游,很有意思。作者把一段程序从文本到执行,捏碎了讲。我这边不求甚解的搬运一遍。

入口-文本文件

首先,计算机被要求完成人所布置的任务。所以,计算机程序的输入理所当然就是人类的语言。下面是用人类的语言描述的,让计算机打印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
...

可以看到在汇编程序中

  1. 首先定义了一个字符串HelloWorld
  2. 然后声明了一个function main
  3. 然后是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指令集表示的机器码。到现在为止,计算机终于看得懂了,可以去执行人类的任务了。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容