如何做自己的公司网站,云seo,重庆招聘网,企业管理网站建设一个程序从编译到运行的全过程 一个程序从编译到运行的全过程编译预处理编译 汇编链接载入虚拟内存用户空间 总结 一个程序从编译到运行的全过程
每次用编译器写完一个程序后#xff0c;我们会进行调试和执行#xff0c;将代码的结果输出在我们的电脑屏幕上#xff0c;但是… 一个程序从编译到运行的全过程 一个程序从编译到运行的全过程编译预处理编译 汇编链接载入虚拟内存用户空间 总结 一个程序从编译到运行的全过程
每次用编译器写完一个程序后我们会进行调试和执行将代码的结果输出在我们的电脑屏幕上但是我们并不清楚为什么我们写的这些代码可以转为我们想要的结果呢
其实在ANSI C标准C的任何一种实现中都存在两个不同的环境即翻译环境和执行环境。
翻译环境下程序会经过两个阶段一个是编译一个是链接。翻译环境内代码被转换为可执行的机器指令。执行环境内它用于实际执行的代码。
那么我们的程序就是进行了这两个过程才最终将我们想要的结果输出在屏幕上。
一个程序从编写完代码到被计算机运行总共需要经历以下四步
编译编译器会将程序源代码编译成汇编代码。汇编汇编器会将汇编代码文件翻译成为二进制的机器码。链接链接器会将一个个目标文件和库文件链接在一起成为一个完整的可执行程序。载入加载器会将可执行文件的代码和数据从硬盘加载到内存中然后跳转到程序的第一条指令处开始运行。 链接器和加载器是由操作系统实现的程序。而编译器和汇编器则是由不同的编程语言自己实现的了。 这里需要展开来说一说我们常用的高级语言按照转化成机器码的方式不同可以分为编译型语言和解释型语言
编译型语言要求由编译器提前将源代码一次性转换成二进制指令即生成一个可执行程序后续的执行无需重新编译。比如我们常见的 C、Golang 等优点是执行效率高缺点是可执行程序不能跨平台不同的操作系统对不同的可执行文件的内部结构要求不同另外由于不同操作系统支持的函数等也可能不同所以部分源代码也不能跨平台。解释型语言不需要提前编译程序只在运行时才由解释器翻译成机器码每执行依次就要翻译一次。比如我们常见的 Python、PHP 等优点是较方便对编写用户而言省去了编译的步骤实时性高每次修改代码后都可直接运行能跨平台缺点是效率低。半编译半解释型语言还有一类比较特殊混合了两种方式。源代码需要先编译成一种中间文件字节码文件然后再将中间文件拿到虚拟机中解释执行。比如我们常见的 Java、C# 等。
所以要设计一门语言还必须为其编写相应的编译器和解释器将源代码转化为计算机可执行的机器码。由于不同的语言有不同的转化方式接下来将以最常见的 C 语言为例简单分析一下 编译→汇编→链接→载入 的过程。 总结不同的语言会使用不同的方式将源代码转化为机器码但是之后的链接和载入过程都是由操作系统完成的都是相同的。 编译
编译是读取源程序进行词法和语法分析将高级语言代码转换为汇编代码。整个编译过程可以分为两个阶段。
预处理
对其中的伪指令以 # 开头的指令进行处理。 #define 定义的符号替换。我们通常使用define定义标识符和宏。假设我们这里顶一个一个MAX为100#define MAX 100 那么在预编译阶段我们程序中的MAX会被直接替换成100。处理条件编译指令。如 #if、#elif、#else、endif 等处理头文件包含指令。如 #include将被包含的文件插入到该预编译指令的位置 删除所有的注释。添加行号和文件名标识。
编译
对预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后产生相应的汇编代码文件。
过程比较复杂主要包括四个步骤
词法分析。这一过程是将字符序列转为单词序列。这里的单词是一个字符串是构成源代码的最小单位。从输入字符流中生成单词的过程叫作单词化同时会对单词进行分类。比如sum 23; 将会拆成sum、、2、、3 这些单词。语法分析。这一过程是分析单词符号串是否形成符合语法规则的语法单位。如表达式、赋值、循环等最终是否构成一个符合要求的程序再按照对应语言的语法规则检查每条语句是否正确。语义分析。语义分析是对结构上正确的源程序进行上下文有关性质的审查进行类型审查。比如语义分析的一个工作是进行类型审查审查每个运算符是否具有语言规范允许的运算对象当不符合语言规范时编译程序 应报告错误。符号汇总。这个环节会将源文件的全局函数的函数名进行汇总。
汇编
将编译完的汇编代码文件翻译成机器指令保存在后缀为 .o 的目标文件Object File中。同时会给每个源文件汇总出来的符号分配一个地址然后生成一个符号表。
这个文件是一个 ELF 格式的文件Executable and Linkable Format可执行可链接文件格式包括可以被执行的文件和可以被链接的文件如目标文件 .o可执行文件 .exe共享目标文件 .so有其固定的格式。
链接
由汇编程序生成的目标文件并不能被立即执行还需要通过链接器Linker将有关的目标文件彼此相连接使得所有的目标文件成为一个能够被操作系统载入执行的统一整体。 例如在某个源文件的函数中调用了另一个源文件中的函数或者调用了库文件中的函数等等情况都需要经过链接才能使用。 在链接阶段编译器主要完成两个任务
合并段表。在汇编结束生成的obj文件内部其实会被划分为几个段在链接过程中会将这些段进行合并。符合表的合并和重定位。在链接过程中我们会对不同的符合分配一个相应的地址。而有的时候一些符号在它所处的文件中并不存在存在于另一个文件中这时候我们会提前分配一个无意义的地址。在合并之后会将合法的地址重新定位覆盖原来无意义的地址。
链接处理可以分为两种
静态链接直接在编译阶段就把静态库加入到可执行文件当中去。优点不用担心目标用户缺少库文件。缺点最终的可执行文件会较大且多个应用程序之间无法共享库文件会造成内存浪费。动态链接在链接阶段只加入一些描述信息等到程序执行时再从系统中把相应的动态库加载到内存中去。优点可执行文件小多个应用程序之间可以共享库文件。缺点需要保证目标用户有相应的库文件。
载入
加载器Loader会将可执行文件的代码和数据加载到内存虚拟内存中然后跳转到程序的第一条指令开始执行程序。
虚拟内存
首先为了避免进程所使用的内存地址相互影响操作系统会为每个进程分配一套独立的虚拟内存地址然后再提供一种机制将虚拟内存地址和物理内存地址进行映射。
我们程序所使用的内存地址叫做虚拟内存地址Virtual Memory Address实际存在硬件里面的空间地址叫物理内存地址Physical Memory Address。
用户空间
然后操作系统将整个内存空间分为用户空间和内核空间其中内核空间只有内核程序能够访问且所有进程共用一个内核空间而用户空间是专门给应用程序使用的每当创建了一个新的进程都要分配一个用户空间。
接下来以 32 位内存空间为例进行说明32 位内存空间大小为 4GB其中 1GB 为内核空间3GB 为用户空间。用户空间中按照数据类型不同划分为了不同的内存段各类数据会被存放到各自的内存段中。 用户空间内存从低到高分别是 6 种不同的内存段
程序文件段.text包括二进制可执行代码已初始化数据段.data包括静态常量未初始化数据段.bss包括未初始化的静态变量堆段包括动态分配的内存从低地址开始向上增长。当进程调用malloc等函数分配内存时新分配的内存就被动态添加到堆上堆被扩张当利用free等函数释放内存时被释放的内存从堆中被剔除堆被缩减文件映射段包括动态库、共享内存等从低地址开始向上增长栈段包括局部变量和函数调用的上下文等。栈的大小是固定的一般是 8 MB。当然系统也提供了参数以便我们自定义大小栈段可以通过系统调用自动地扩充空间但是不能回收空间所以栈段设置得太大会导致内存泄露。
至此可执行文件载入内存的过程可以概括为以下几步
给进程分配虚拟内存空间创建虚拟地址到物理地址的映射创建页表加载代码段和数据段等数据即将硬盘中的文件拷贝到物理内存页中并在页表中写入映射关系把可执行文件的入口地址写入到 CPU 的 指令寄存器PC中即可执行程序。
总结
本文简单的描述了一下一个程序从编写完代码到被计算机运行的过程其实当中的每一步都十分复杂深奥都值得深入学习尤其是最后一步载入内存的过程展开来说可以涉及到整个操作系统的内存管理像分段、分页、多级页表、TLB、内存分配、内存泄露、内存回收、页面置换算法等等。