代码从 “ctrl+F5” 到 “屏幕输出” 所经历的过程详解——翻译环境(编译+链接)+ 执行环境

前言

我们用编译器写完代码后,只需Crtl+F5即可让代码跑起来,最后将结果输出到屏幕上,那么你知道在这个过程中都经过了哪些步骤吗?

其实,在ANSI C(标准C)的任何一种实现中,都存在两个不同的环境,即翻译环境执行环境。翻译环境,在这个环境中,代码被转换为可执行的机器指令。执行环境,它用于实际执行代码。

我们运行代码时都将经历这两个过程,最后才将运行结果呈现在屏幕上。
在这里插入图片描述

翻译环境

在翻译环境下,要经历两个过程,即编译链接。我们写的程序中可能包含多个源文件(以.c为后缀),编译过程中这些源文件会单独经过编译器处理,最后生成对应的目标文件(以.obj为后缀),而在链接过程中,链接器会把这些目标文件和链接库(存放的是库函数信息)链接起来,最后生成可执行程序。
在这里插入图片描述
总结:

  • 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
  • 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
  • 链接器同时也会引入标准C库函数中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。

例如,我们写一个简单的加法函数在源文件Add.c中,再写一个源文件test.c用于调用Add函数,当程序执行完毕后,我们可以在文件中找到这些文件:
在这里插入图片描述

接下来,我们再深入认识一下编译和链接中每一步具体干了些什么事。

编译 = 预编译 + 编译 + 汇编

编译过程其实又被细分为三个环节,即预编译,编译和汇编。
一、预编译
在预编译这个环节,编译器主要会对C代码做这三件事:

  1. 头文件的包含。 在预编译阶段,编译器会将代码中所包含的头文件都替换成头文件的内容。例如,#include <stdio.h>这句代码会被替换成stdio.h这个头文件中的全部代码。
  2. 注释的删除。 在预编译阶段,编译器会将代码中所有的注释都删去,这样可以减少代码量,毕竟运行代码的时候编译器也不用看你的注释。
  3. #define定义的符号的替换。 我们都知道#define定义的标识符也好,定义的宏也罢,都是起到替换的作用,而真正进行替换的时刻便是预编译阶段。

二、编译
编译阶段的主要任务就是将C代码翻译成汇编指令,这个过程主要包括这四个步骤:

  1. 词法分析。 词法分析的任务是对由字符组成的单词进行处理,从左至右逐个字符地对源程序进行扫描,产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序。
  2. 语法分析。 编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,如表达式、赋值、循环等,最后看是否构成一个符合要求的程序,按该语言使用的语法规则分析检查每条语句是否有正确的逻辑结构,程序是最终的一个语法单位。
  3. 语义分析。 语义分析是编译过程的一个逻辑阶段, 语义分析的任务是对结构上正确的源程序进行上下文有关性质的审查,进行类型审查。语义分析是审查源程序有无语义错误,为代码生成阶段收集类型信息。
  4. 符号汇总。 在这个环节中,会将每个源文件的全局范围的变量符号进行汇总。
    在这里插入图片描述

三、汇编
汇编的时候会将汇编指令转换成二进制指令存放在相应的文件中(.obj为后缀),同时会给每个源文件汇总出来的符号分配一个地址,然后分别生成一个符号表。
在这里插入图片描述
注:因为test.c文件中提取的符号Add只是Add函数的声明,并不是定义,无法判断Add函数是否真正存在,所以test.c生成符号表时分配给Add符号的地址是一个无意义(非法)的地址。

链接

在链接过程中,编译器主要完成以下两个任务:

  1. 合并段表。 其实,汇编结束后所生成的obj文件内部会被划分为几个段,在链接过程中就会把每个obj文件对应的段通过某种规则合并起来,最后形成可执行程序(.exe为后缀)。
    在这里插入图片描述

  2. 符号表的合并和重定位。 在链接期间会将每个源文件的符号表进行合并,若不同源文件的符号表中出现了相同的符号,则取合法的地址为合并后的地址(重定位)。
    在这里插入图片描述
    注:符号表并非无意义。例如,当要调用某一函数时,编译器会在符号表中查找该符号,若有,则调用成功,否则调用失败。

执行环境

exe程序执行的过程大概可以分为四个步骤:

  1. 程序首先要载入内存中。 在有操作系统的环境中,该操作一般由操作系统来完成。在独立的环境中,程序的载入可以由手工完成,也可以通过可执行代码置入只读内存来完成。
  2. 程序的执行开始。 接着便调用main函数。
  3. 开始执行程序代码。 这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留它们的值。
  4. 终止程序。 正常终止main函数,也可能是以外终止。

猜你喜欢

转载自blog.csdn.net/chenlong_cxy/article/details/115198888