正片开始!
1.翻译环境和运行环境
前言:
在ANSI C(制定的C语言标准)的任意种实现中,存在两个不用的环境。
1.翻译环境,在这个环境中源代码被转换为可执行的机器指令(二进制指令)。
2.执行环境,它用于实际执行代码。
2.翻译环境:预编译+编译+汇编+链接
编译和链接是构建环境的两个主要过程,而编译过程又可以细分为预处理(有时也称为预编译)、编译和汇编三个步骤。
2.1 预处理(预编译)
预处理阶段是编译过程中的第一步,它的主要目的是处理源文件中的预处理指令,如#include
、#define
等,并展开宏定义,最终生成一个.i为后缀的预处理后的文件。下面是在gcc环境下查看test.c文件预处理后的test.i文件的命令示例:
gcc -E test.c -o test.i
预处理阶段的处理规则包括:
- 删除所有的
#define
指令,并展开所有的宏定义。- 处理所有的条件编译指令,如
#if
、#ifdef
、#elif
、#else
、#endif
。- 处理
#include
预处理指令,将包含的头文件的内容插入到该预处理指令的位置。这个过程是递归进行的,也就是说被包含的头文件也可能包含其他文件。- 删除所有的注释。
- 添加行号和文件名标识,以便后续编译器生成调试信息等。
- 或保留所有的
#pragma
编译器指令,编译器后续可能使用。预处理后的.i文件中不再包含宏定义,因为宏已经被展开。而包含的头文件都被插入到.i文件中。因此,当我们无法确定宏定义或头文件是否正确时,可以查看预处理后的.i文件来确认。
2.2 编译
编译过程可以被概括为将预处理后的文件进行一系列的步骤,包括词法分析、语法分析、语义分析以及优化,最终生成相应的汇编代码文件。
对下面代码进行编译的时候,会怎么做呢?假设有下面的代码
array[index] = (index+4)*(2+6);
2.2.1 词法分析:
在进行词法分析后得到的16个记号可能代表了源代码中的关键字、标识符、字面量和特殊字符等。这些记号是词法分析器从源代码中识别出来的基本单元,用于构建后续的语法树或执行其他编译器任务。
上面程序进行词法分析后得到了16个记号:
2.2.2 语法分析
在语法分析阶段,语法分析器将对词法分析器产生的记号进行语法分析,以生成语法树。语法树是一种以表达式为节点的树形结构,用于表示源代码的语法结构。
例如:
2.2.3 语义分析
在语义分析阶段,语义分析器会对语法分析器生成的语法树进行分析,以进行语义层面的分析。编译器在这个阶段进行的分析被称为静态语义分析。
静态语义分析通常涉及以下几个方面的检查:
声明和类型的匹配:检查变量、函数和其他标识符的声明是否与其使用的上下文相匹配。例如,检查变量是否已经在使用之前进行了声明,检查函数调用时参数的类型是否与函数声明或定义中的参数类型匹配。
类型的转换:检查在表达式中是否存在类型不匹配的情况,例如将一个字符串与一个整数相加。在这种情况下,语义分析器可能会执行类型转换操作,将其中一个操作数转换为另一个操作数的类型,以使它们具有相同的类型。
常量表达式的求值:在编译时对常量表达式进行求值,以便进行优化或者在编译时报告错误。
作用域的检查:确保标识符的使用在其作用域内有效,并且没有重复定义的情况发生。
其他静态规则的检查:根据语言的规范,执行其他静态规则的检查,例如对数组访问的边界检查等。
在静态语义分析阶段,如果发现了语义错误,编译器会生成相应的错误信息,通常包括错误的位置和描述,以帮助程序员更容易地发现和修复问题。
2.3 汇编
汇编器是将汇编代码转换成机器可执行的指令的工具。在汇编过程中,每一条汇编语句几乎都对应着一条机器指令。汇编器的主要任务是根据汇编指令和机器指令的对照表一一进行翻译,将汇编语句转换为等效的机器指令。
汇编的命令:
gcc -c test.s -o test.o
2.4 链接
链接是编译过程中的重要步骤,它将多个目标文件和库文件链接在一起,生成最终的可执行程序。链接过程是一个复杂的过程,涉及到地址和空间分配、符号决议和重定位等多个步骤。
链接过程的主要步骤包括:
地址和空间分配:链接器负责将各个目标文件和库文件中的代码段、数据段分配到内存中的合适位置。它确定每个符号在内存中的地址,并分配足够的空间以容纳所有的目标文件和库文件。
符号决议:链接器解析目标文件中的符号引用,并将其与符号定义进行匹配。如果找到了对应的符号定义,则将引用替换为定义的地址或偏移量。如果找不到符号定义,则会报链接错误。
重定位:在将各个目标文件和库文件组合成最终的可执行程序时,需要对相对地址进行调整,以解决符号引用的问题。链接器会根据符号的地址信息和重定位表,将相对地址转换为绝对地址,以确保程序能够正确地执行。
重定位的解释:(由于每个文件是单独编译的,比如在当下的test.c文件引用了Add函数,但函数实际是在另外一个源文件add.c中定义的,这时编译器编译test.c的时候并不知道Add函数的地址,所以暂时把调用Add函数的指定搁置,等待链接器根据引用的符号Add在其他模块中查找Add函数的地址,然后将test.c中引用到Add的指令重新修改,让它们的目标地址为真正的Add函数的地址,对于全局变量也是类似的方法来修正地址,这个过程就是:重定位)
链接的主要作用是解决一个项目中多个文件、多个模块之间的相互调用和依赖关系。通过链接,各个模块之间的函数调用和数据访问得以正确地连接,最终形成一个完整的可执行程序。
3. 运行环境
在运行环境中,程序的执行经历以下步骤:
程序载入内存:
- 在有操作系统的环境中,程序的载入通常由操作系统完成。操作系统会将程序的可执行文件从存储设备加载到内存中。
- 在独立的环境中,程序的载入可能需要手动安排,或者通过将可执行代码加载到只读内存中来完成。
程序执行开始:
- 一旦程序载入到内存中,执行便开始。通常,操作系统会从程序的入口点开始执行,即调用
main
函数。程序代码执行:
- 在程序开始执行时,它会使用一个运行时堆栈(stack),用于存储函数的局部变量和返回地址。
- 程序也可以使用静态内存,用于存储静态变量。这些变量的值在程序整个执行过程中保持不变。
程序终止:
- 程序的终止可以是正常的,即
main
函数执行完毕并返回。- 也可能是意外的,例如发生了错误导致程序异常终止。
在程序执行过程中,操作系统负责管理程序的运行环境,包括内存分配、调度等。程序的执行过程中可能涉及到系统调用、异常处理等操作系统提供的功能。
这次分享就到这里啦,感谢观看o(* ̄▽ ̄*)ブ。