📝前言:
这篇文章主要讲解一下C语言的编译和链接,帮我们更好的理解程序的执行过程,更好的理解计算机系统。
🎬个人简介:努力学习ing
📋个人专栏:C语言入门基础
🎀优快云主页 愚润求学
🌄每日鸡汤:山高路远,唯有逢山开路,遇水架桥
文章目录
一,编译和链接
1. 什么是编译和链接?
在编写C语言程序时,我们通常会写一个或多个.c
文件(源代码文件)。计算机并不能直接理解这些文本文件,需要将它们转换为机器可以执行的二进制文件。这个过程分为两个主要步骤:
- 编译:将源代码(
.c
文件)转换为目标文件(.o
或.obj
文件)。 - 链接:将多个目标文件和库文件合并,生成最终的可执行文件(如
.exe
或.out
文件)。
下面我们详细讲解这两个过程。
2. 编译过程
编译是将C语言源代码转换为机器代码的过程。它分为以下几个步骤:
2.1 预处理(Preprocessing)
预处理阶段,源文件和头文件会被处理成以.i
为后缀的文件。预处理主要处理源文件中以#
开头的预编译指令,比如:
- 宏定义处理:删除所有
#define
,并展开所有宏定义。假设代码中有#define PI 3.14159
,预处理后代码中所有的PI
都会被替换为3.14159
。 - 条件编译指令处理:处理
#if
、#ifdef
、#elif
、#else
、#endif
等条件编译指令,根据条件决定代码的取舍。 - 头文件包含处理:处理
#include
预编译指令,将包含的头文件内容插入到该指令的位置,这个过程是递归进行的。如果test.c
中#include "stdio.h"
,预处理时stdio.h
的内容会被插入到#include
所在的位置。 - 保留
#pragma
指令:保留#pragma
编译器指令,供后续编译器使用。 - 注释删除:删除所有注释,让代码更加简洁,便于后续处理。
在gcc环境下,可以使用gcc -E test.c -o test.i
命令查看预处理后的结果。
示例:
#include <stdio.h>
#define PI 3.14159
int main() {
printf("PI = %f\n", PI);
return 0;
}
预处理后,#include <stdio.h>
会被替换为stdio.h
文件的内容,PI
会被替换为3.14159
。
2.2 编译(Compilation)
编译过程是将预处理后的文件进行词法分析、语法分析、语义分析及优化,生成相应的汇编代码文件,使用gcc -S test.i -o test.s
命令。
汇编代码一般具备的信息:
- 指令信息:这些指令对应着计算机处理器的基本操作,如数据传送(
MOV
)、算术运算(ADD、SUB
)、逻辑运算(AND、OR
)、跳转(JMP、JE
)等 - 数据信息:定义了程序中使用的各种数据,包括变量、常量等。
- 控制信息:包含了程序的控制结构信息,如循环、条件判断等。这些控制结构通过跳转指令(如
JMP、JE、JNE
等)来实现。 - 内存和寄存器信息:明确了程序中使用的内存地址和寄存器。寄存器是处理器内部的高速存储单元,汇编代码会频繁地使用寄存器来进行数据处理和传递。
下面通过array[index] = (index + 4) * (2 + 6);
这段代码来看看编译过程:
-
词法分析:将源代码输入扫描器,扫描器把代码中的字符分割成一系列记号,如关键字、标识符、字面量、特殊字符等。上述代码经过词法分析后会得到
array
、[
、index
…6
、)
等16个记号。 -
语法分析:语法分析器对扫描产生的记号进行语法分析,生成以表达式为节点的语法树。在这个过程中,编译器会检查代码的语法结构是否正确,比如括号是否匹配、语句是否完整等。
-
语义分析:语义分析器完成语义分析,主要包括声明和类型的匹配、类型的转换等,同时报告错误的语法信息。比如,如果代码中把两个不同类型的变量进行不兼容的运算,语义分析阶段就会报错。
总结:语法错误(拼写,括号匹配),类型不匹配,使用未声明标识符,重复定义,作用域问题等容易引发编译成错误。
2.3 汇编(Assembly)
汇编器将汇编代码转变成机器可执行的指令,每一个汇编语句几乎都对应一条机器指令。使用gcc -c test.s -o test.o
命令完成汇编过程。汇编器会根据汇编指令和机器指令的对照表进行翻译,这个过程不做指令优化。
3. 链接过程
链接是将多个目标文件和库文件合并,生成最终可执行文件的过程。链接器的主要任务包括:
3.1 符号解析(Symbol Resolution)
在编译过程中,每个源文件会生成一个目标文件。如果多个文件之间有函数调用或变量引用,链接器需要解析这些符号(函数名、变量名等),确保它们能够正确关联。
示例:
main.c
中调用了math.c
中的add
函数。- 链接器会找到
add
函数的定义,并将其与main.c
中的调用关联起来。
3.2 重定位(Relocation)
目标文件中的地址通常是相对地址。链接器会将这些相对地址转换为最终可执行文件中的绝对地址。
假设有test.c
和add.c
两个文件,test.c
中使用了add.c
中的Add
函数和g_val
变量。每个源文件单独编译生成对应的目标文件,test.c
生成test.o
,add.c
生成add.o
。在编译test.c
时,并不知道Add
函数和g_val
变量的地址,所以暂时搁置调用Add
指令的目标地址和g_val
的地址。链接器会根据引用的符号Add
在其他模块中查找Add
函数的地址,然后将test.c
中所有引用到Add
的指令重新修正,让它们的目标地址为真正的Add
函数的地址,对于全局变量g_val
也采用类似方法修正地址,这个过程就是重定位。
3.3 生成可执行文件
链接器将所有目标文件和库文件合并,生成一个可执行文件(如a.out
或program.exe
)。这个文件可以直接在操作系统中运行。
4. 编译和链接的示意图
以下是一个简单的示意图,展示了从源代码到可执行文件的过程:
5. 实际使用中的编译和链接
在实际开发中,我们通常使用编译器(如gcc
)来自动完成编译和链接的过程。例如:
gcc main.c math.c -o program
这条命令会:
- 编译
main.c
和math.c
,生成目标文件。 - 链接目标文件,生成可执行文件
program
。
二, 翻译环境和运行环境
1. 翻译环境
翻译环境就是由上面提到的两个过程:编译和链接组成。而编译又可以进一步细分为预处理、编译、汇编三个子过程。
总结一下:
- 预处理:主要处理源文件中以
#
开头的预编译指令 - 编译:将预处理后的文件进行词法分析、语法分析、语义分析及优化,生成相应的汇编代码文件
- 汇编:汇编代码转变成机器可执行的指令,每一个汇编语句几乎都对应一条机器指令
- 链接:将多个目标文件和库文件合并,生成最终可执行文件的过程。
2. 运行环境
程序在翻译环境生成可执行文件后,就进入运行环境:
- 程序载入内存:在有操作系统的环境中,一般由操作系统完成程序的载入;在独立环境中,程序的载入可能需要手工安排,或者通过可执行代码置入只读内存来完成。
- 程序执行开始:程序载入内存后,开始执行,首先调用
main
函数。 - 执行程序代码:程序使用运行时堆栈存储函数的局部变量和返回地址,同时也可以使用静态内存,静态内存中的变量在程序整个执行过程中一直保留其值。
- 终止程序:程序执行结束,正常情况下是
main
函数执行完毕返回,也有可能因为意外情况终止。
🌈我的分享也就到此结束啦🌈
要是我的分享也能对你的学习起到帮助,那简直是太酷啦!
若有不足,还请大家多多指正,我们一起学习交流!
📢公主,王子:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!