程序是如何运行的(一): 编译

312 阅读4分钟

程序是如何运行的(一): 编译

关于这个问题一直是我的心结, 一方面C++语言本身的规范和技巧我算是有所了解, 另一方面计算机四大基础课程也是我本科以来接续学习过的. 但是语言是怎么从人写的英文字符深入到os内部实施运行的, os又是怎么在管理茫茫多程序的? 我不理解.

<程序员的自我修养>一书大概也是从这个问题出发探索程序如何运行的同时将计算机基础知识与高级语言连贯起来. 讲的很细, 包含了Windows和Linux两个操作系统的实现, 也深入带着读者去梳理操作系统的源码或系统调用. 细节我不多谈, 也谈不好, 只是讲讲我是怎么用这本书来贯通程序和计算机基础知识的关系.

程序需要被计算机运行前需要经过以下大步骤

  1. 预编译
  2. 编译
  3. 汇编
  4. 链接
  5. 装载

预编译

一点点来谈, 从简单预编译开始: 如果把整个四步过程看作做一个大菜的话, 预编译这一步就相当于洗菜和切菜. 预编译处理的主要是 '#' 和 '//' 符号后的内容, 比如在这个阶段 #define 会被删除, 直接在代码里进行替换; 所有注释会在这个阶段删除; 所有类似 '#if', '#ifndef'的预编译指令都会在这个阶段处理

编译

接下来就是编译: 编译负责将高级语言加工成低级的汇编语言. 这其中又分为以下步骤:

  • 词法分析: "运用一种类似有限状态机的算法可以轻松地将源代码的字符序列分割成一系列记号". 意思是说, 在词法分析之前代码被计算机是为连续的字符串, 每一个字符并没有逻辑上的指示. 词法分析做的就是把整个连续的字符串分割, 提取出一个个独立的逻辑单元并标记. 比如:a = 1; 就会被分割成a, =, 1并打上标识符, 赋值, 数字的标签.

  • 语法分析: 上一步得到的是分割的字符和字符的标签, 但是在组织形式上仍然是线性的, 这并没有表达出字符与字符之间的关系. 而语法分析做的就是这个. 语法分析通过 "下推自动机"(这是计算理论的基础内容)将字符组织成树的: 两个子节点为操作树, 父节点为操作符.经过语法分析, 零散的字符表示就变成了有语法结构的语法树.

image.png

  • 语义分析: 零散的字符标识被组织成语法树后, 仍然缺少操作系统需要的类型标注 -- 这就是语义分析做的事情

  • 中间代码的生成和优化: 所谓中间语言指的是与操作系统类型无关的, 我愿意称之为 "逻辑上的汇编代码".语义分析后, 代码所表达的意思基本上可以被直接翻译成 "逻辑上的汇编代码". 但是编译所作的远不止于此. 除了能正确翻译成汇编语言之外, 编译器还做了代码执行效率上的优化.比如书中提到的 2 + 6 并不会等到运行的时候占用额外资源去计算, 而是在编译的时候直接被当作8.

  • 目标代码的生成和优化: 这里的目标代码就是汇编语言了. 中间代码可以很直接地更具操作系统的不同翻译成对应的汇编代码. 这个过程也是有前人智慧和经验积累的优化过程.

汇编

汇编过程就比较直观了, 只需要一一对应地翻译成机器指令即可

小结

编译过程大抵如此, 可以看到计算机编译代码的过程和人看代码的过程其实也是类似的. 首先代码只是连续的字符集合, 人第一步也是根据经验将字符分割开并打上标签; 其次分辨出字符与字符间的关系, 然后再根据字符的类型来理解每一行代码.

其实以上这些内容在之前多少也是有所耳闻, 而接下来的链接(尤其是动态链接), 装载和最后的运行才真是仅知其然.