一段源代码的旅行——程序运行背后的机制和由来

本文探讨了源代码从编译到执行的全过程,包括预处理、编译、汇编和链接四个阶段。通过Hello World程序为例,详细解释了每个阶段的功能和重要性,强调了理解这些机制对解决程序问题和提升性能的重要性。推荐了《Linkers and Loaders》、《深入理解计算机系统》以及《程序员的自我修养——链接、装载与库》作为深入学习的参考资料。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【自省】
自学习编码以来,Coder本人很少去认真去研究一段代码的运行过程,关注更多的是代码的产生结果。但随着知识越学越深,Coder深觉,我们往往会被复杂的集成工具所提供的强大功能所蒙蔽,很多系统软件的运行机制被埋藏,其程序的很多莫名其妙的错误让我们不知所措,面对程序运行时种种性能瓶颈我们望天扶额。
能看得到的是问题的现象,看不到的是问题的本质。所有问题的本质就是软件背后的机理及支撑软件运行的各种平台和工具,如果能够深入了解这些机制,那么解决问题就有了方向,期望能够达到游刃有余、收放自如的境界。

【关于9 3/4车站的提问】
Hello World程序是我们进入计算机世界的九又四分之三车站。但是,面对这样一个简单程序,我们可以清晰明确地回答出一些问题吗:

  • 编译器在C程序转换成可执行代码的过程中发生了什么?
  • 编译器是怎么做的?
  • 程序为什么要被编译器编译了以后才能执行?

今天我们就初步来研究以上问题。Coder这是一个现学现卖的过程,我一边学习着新的知识,一边把新知识组织整理用文字表现出来。今天我们只是简单来了解编译链接的过程,后续我们会继续来探讨装载与库的过程。

【共勉】
俞甲子有句话很受用,“我始终认为对于一个问题比较好的描述方式,是由一个很小很简单的问题或示例入手,层层剥开深入挖掘。”听大师的话,我们先从Hello World着手。
C语言下,Hello World程序是程序入门,是开发环境测试的默认标准,编译运行一气呵成。

#include <stdio.h>

int main()
{
    printf("Hello World\n");
    return 0;
}

在Linux下,我们使用GCC来编译此程序时,使用简单命令即可,指定源代码文件名为hello.c,则:

$gcc hello.c
$./a.out
Hello World

教科书上说,上述过程分为四个步骤:预处理、编译、汇编和链接。通常的IDE一般将编译和链接的过程一步完成,称之为构建。下面我们来分别看一下这四部分。

【预处理阶段】
概括来讲,预处理阶段发生三件事:

  1. 添加行号和文件名标识;
  2. 删除所有注释,以空格替换之;
  3. 处理所有以“#”开始的预编译指令:
    保留#pragma编译器指令;
    将所有“#define”删除并展开宏定义;
    处理#include预编译指令,引入头文件包含的内容;
    处理所有条件编译指令,e.g.#if、#endif、#ifdef等;

预编译过程相当于如下命令(源代码文件 hello.c 和相关头文件等被预编译器预编译成一个 .i 文件):

$gcc -E hello.c -o hello.i

【编译过程】
编译过程就是把预处理后的文件进行一系列词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件。会变过程是整个程序构建的核心部分。上述的编译过程相当于如下命令:

$gcc -S hello.i -o hello.s

编译是核心,我们用一张图来清晰地看看这四部分的关系:

这里写图片描述

【汇编】
汇编是将汇编代码转变成机器可以执行的指令,每一条汇编语句几乎都对应一条机器指令。上述汇编过程我们可以这样写:

$gcc -c hello.s -o hello.o

经过预编译、编译和汇编后,会直接输出目标文件。

【链接】
链接压轴出场,它是一个很费解的过程,为什么汇编器不直接输出可执行文件而是输出一个目标文件?为神秘要走链接的过程?链接的背后到底包含了什么内容?听说我们得需将一大堆文件链接起来才可以得到最终的可执行文件?预知详情,我们需得关注一个工具——编译器。
编译器是将高级语言翻译成机器语言的工具,比起早期使用机器指令或汇编语言编写程序,编译器使得程序开发变得高效。编译器的编译过程一般可以分为六步:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化。

[词法分析]
词法分析的过程是将源代码被输入到扫描器,扫描器进行简单词法分析,它将源代码的字符序列分割成一系列记号,它们分别是关键字、标识符、字面量(数字和字符串)和特殊符号。在识别记号的同时,扫描器将标识符存放在符号表,将数字和字符串常量存放在文字表中,以备不时之需。

[语法分析]
承上,语法分析器将对扫描器产生的记号进行语法分析,生成语法树。此处涉及很多编译原理相关知识,简单来讲,语法树就是以表达式为节点的树,将整个语句拆分为最小单位表达式,以树的形式呈现出。

[语义分析]
承上,语法分析仅仅完成对表达式语法层面的分析,但语句是否有意义不得而知。语义分析器接棒后,会对声明和类型的匹配做检测、对符号表里的符号类型做更新,同时还可能进行类型的转换等。经过语义分析以后,语法树上的表达式都被标识了类型。

[中间语言生成]
编译器会在源代码级别有优化过程,如果某表达式的值在编译期间是可以确定的,源代码级优化器会在源代码级别对其进行优化,将整个语法树转换成中间代码,它是语法树的顺序表示,目标代码近在咫尺。中间代码有多种类型,常见的有我们常说的三地址码和P-Code。

[目标代码生成与优化]
这个过程属于编辑器后端,编辑器后端包括代码生成器和目标代码优化器。代码生成器将中间代码转换成目标机器代码,之后目标代码优化器对目标代码进行优化,比如选择合适的寻址方式、使用位移代替乘法运算、删除多余的指令等。

经过以上这些繁琐的过程,源代码终于被编译成了目标代码,但是,有一个很重要的问题:如果目标代码中有变量定义在其他模块该怎么办?带着这个问题,我们来认识链接器。

[链接器]
链接器是比编译器存在更久远的机器。
我们把每个源代码模块独立地编译,然后按需将它们组装,这个组装模块的过程就是链接。链接过程主要包括了地址和空间分配、符号决议和重定位等这些步骤。

【结语】
源代码到最终可执行文件的四步骤就这样相互作用地完成了,通常我们很少关注这些步骤,但毋庸置疑,这些都是很重要很繁琐的过程,深入了解计算机编码世界,我们还有很长的路要走。

【推荐丛书】
首先,介绍链接、装载与库原理等的资料非常少,大师们对于这方面推荐的丛书有:
《Linkers and Loaders》,John R.Levine。这本书基本上是链接和装载方面最为完整和权威的理论著作,但是内容偏旧,且有晦涩难懂。
《深入理解计算机系统》,这本书对整个计算机软硬件体系结构进行了深入浅出的介绍,对于理解系统底层来说是本不可多得的好书。
Coder本人深觉大师境界目前高度难及,所以推荐一本我在学习这部分时的参考书:
《程序员的自我修养——链接、装载与库》(俞甲子 石凡 潘爱民 著)。这本书主要介绍的就是系统软件的运行机制和原理。
同时,文章中提到的一些知识点涉及编译原理的内容,Coder参考的书目是《编译原理》(第2版 )(张素芹,清华大学出版社 )。欲知更多,可自行研究,米呐桑,我们下期再见。

评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值