一个程序从编写到最后执行究竟发生了什么

一直以来,感觉学的很多课程,都只研究了程序的的某个部分。比如程序设计课程关注程序的编写阶段,如何编写。编译原理关注程序的编译阶段。操作系统则关心程序的执行阶段以及加载进内存后的事情。因此想写一篇博客将它们串起来。

编写

程序的第一个阶段肯定就是编写了,由程序员编写特定格式的文本也就是源代码写入文件中,从而让编译器能够识别并且编译。本节从编辑器的选择讲起,然后到编写规范,最后简单介绍一下软件工程

编辑器

编写代码肯定需要一个编辑器来帮助我们编写代码。各个编辑器有它们的优势。从Vim开始,它是纯文本的编辑器,也就是不会有自动补全,高亮,提示语法错误等功能的编辑器。Vim可以进行纯键盘的操作,如果快捷键用的熟练的话可以达到极致的效率,但是它的学习曲线非常的陡峭,很多人都无法接受。

然后说一说稍微轻量级一些的vscode吧, vscode有现代化的图形界面,提供丰富的可视化功能,并且它的插件生态非常的好,丰富多样并且点击下载就可以安装,不需要Vim那样去费心思去配置。配置好的 vscode可以集成git,docker管理,远程ssh等丰富的功能。个人感觉 vscode最大的优势是它的丰富的插件,并且相比于大型的IDE可以自定义定制一些内容。

最后是一些大型的IDE如,大型的IDE一般都是有一些专攻。比如VS一般用来开发 .NET 程序。IDEA来开发Java程序。这些大型的IDE集成了更加丰富的功能,包括编码、编译、调试、构建、部署等一系列软件开发相关的工具,更适合大型项目的开发。

编写规范

解决了编译器的问题,下一个要注意的就是编写格式的规范了,这方面包括很多的规范,比如代码的缩进,命名的风格等等。不同的语言的代码有不同的规范,不同的规范之间可能并不适用。比如缩进相关的规范对于一些缩紧本就是语法的语言如Python可能就不适用,命名的一些规范对于一些命名敏感的语言如Go可能就不适用。因此具体的编写格式规范需要具体语言具体分析,在此就不再多赘述了。

软件工程

软件工程可能是计算机科学中最像文科的一个部分了,但是它却十分的重要,很多的事情如果从代码编写的时候开始关注,那么后面就没有那么多事情了。软件工程涉及到软件声明周期的方方面面,在编写代码的方面我认为主要是设计模式,和一些类的编写规则。如设计模式中的创造模式和行为模式,类的一些编写规则如SOLID等。这方面的内容非常多,在这里也不再赘述了

编译

编译就是从源代码到最终目标程序的过程。根据这个过程的不同可以分为解释型语言如Python,编译型语言如C/C++。

编译器

编译型的语言编写完成后需要经过编译器编译才能成为可执行的程序,以C语言为例。

29be0b9e4395c94ba70fa5fb75c1b6b5.png

那么编译器都干了什么呢?

下图即为一个编译的通常的结构,分为前端和后端。

flowchart LR
subgraph 编译器前端
    词法分析--词法单元流-->语法分析
    语法分析--抽象语法树AST-->语义分析
    语义分析--抽象语法树AST-->中间代码生成
  end
subgraph 编译器后端
    代码优化-->目标代码生成
  end

中间代码生成-->中间代码
中间代码-->代码优化
编译器前端

前端先对源代码进行词法分析,产生词法单元流或词法单元序列然后,然后进行语法分析生成抽象语法树(Abstract Syntax Tree, AST),然后进行语义分析生成已标记的抽象语法树(Annotated Abstract Syntax Tree),在这个阶段会检查源代码的语义是否正确,并将相关的语义信息附加到AST节点上。然后由中间代码生成器生成中间代码IR(Intermediate Representation)。这里的IR已经很接近汇编代码了但是和汇编代码是不同的。
这里不同的编译器前端生成的IR会有不同。这里也不再详细介绍了。

编译器后端

编译器后端接收中间代码后会对代码进行优化,经过优化后根据具体的目标代码的架构(x86,ARM)生成对应的汇编代码。编译器后端会将汇编代码格式化为特定的目标文件格式,如ELF文件。目标文件包含了经过汇编和优化的机器代码,以及符号表、重定位信息等。ELF具体的解析可以看博客的[这篇文章](ELF文件详细学习 - Jty’s Blog)。可以看到这时候具体的程序执行需要的信息,如堆栈信息和一些符号等的信息已经确定下来了。

解释器

解释型语言就是不需要经过编译这个过程,由解释器一行一行将高级语言代码变为对应架构的机器码来执行。Python就是典型的解释型语言。

程序运行

源代码经过编译器的编译之后成了一堆二进制的数据,在硬盘上呆着。

装载

在程序的真正运行之前,操作系统会进行装载。

操作系统会根据程序创建一个进程控制块PCB,PCB上有进程标识符,进程状态信息,进程调度信息和控制信息等。

装载器不会把代码装载到物理内存中,而是用一个页表把代码在硬盘上的位置记录下来,只有在真正运行的时候才会加载到内存里面

最后,装载器会找到程序的入口地址,执行的时候,从入口地址开始读第一条指令。

运行

操作系统进行进程的调度,当轮到这个进程来的时候,才从装载器返回的入口点开始执行。

CPU从程序入口处取出指令,但是这是一个虚拟地址。然后依据操作系统的内存管理模式,来将虚拟地址转换为程序的物理地址。随着程序的执行,越来越多的数据和代码被加载到物理内存,但是这些程序也不是连续的,以碎片的形式安插在内存页中,(这里也涉及到操作系统的复杂的内存管理算法)。

CPU会不断地按照调度算法执行进程,更细分的话是调度线程。最后进程运行结束,内存中的数据会清理,覆盖。


一个程序从编写到最后执行究竟发生了什么
http://jty-123.github.io/2024/07/23/2024-07-23-一个程序从编写到最后执行究竟发生了什么/
作者
Jty
发布于
2024年7月23日
许可协议