【C语言】C程序的编译+链接
写在前面
在学习了众多C语言语法和特性后,我们需要认识C语言是这么从一行行代码变成一个可执行程序的。关于本文图片标题:光标移动到对应的图片中会显示标题
。
文章目录
- 写在前面
- 一、程序的翻译环境和执行环境
- 二、编译环境
- 三、详解编译环境中的编译、连接
- 3.1编译
- 3.1.1**预编译(预处理)阶段**:
- 3.1.2**编译阶段**:
- 3.1.3、汇编阶段
- 3.2连接
- 四、编译环境总结
- 五、运行环境
一、程序的翻译环境和执行环境
只要是在ANSI规定的C中的任何一种实现中,都一定存在翻译环境和执行环境。
- 翻译环境:在这个环境中源代码被转换为可执行的机器指令
- 执行环境:它用于实际执行代码。
本篇笔记重点详解编译环境。
二、编译环境
在编译环境中又分为:
编译
与连接
。
我们编写C语言时候使用的VS2022或者DevC++工具都是编译工具,充当着编译环境的角色。这里不才使用VS2022与gcc进行详解。
我们在VS2022中每次进行运行代码时候,VS2022都会自动的帮我们进行两个环境的连续运行(如点击下图按钮)。
所以每次我们只要点击后(不报错前提),我们就会得到运行界面,但是我们可以打开文件夹查看,当未运行前,文件中只有.c
文件(图一)。
在运行后,可以看到多出了Debug
文件,文件中有一个.exe
文件(图二、三),
我们可以得到如下结论:
- 在翻译环境运行后,会把原来的
.c
文件生成一个.exe
文件 - 在成功生成新的
.exe
文件后,VS2022会自动运行.exe
文件,即自动进入执行环境中。 - 结合结论1、2可以得出,编译环境中的:
编译
与连接
是在.c
文件到.exe
文件过程中完成的操作。即可以得到下图:
三、详解编译环境中的编译、连接
在编译中又细分为:
预编译
、编译
、汇编
。如下图
3.1编译
为了更直观感受编译的过程,不才举个简单的栗子🫡🫡
我们先编写一个test.h
和test.c
文件
test.h
文件
struct S{
int a;
char c;
};
test.c
文件
#include "test.h"
#define M 100
int main(){
struct S s;//这个结构体是在test.h头文件内包含的!
int max = M;
return 0;
}
3.1.1预编译(预处理)阶段:
预编译阶段生成的文件后缀为
.i
的文件。
手动生成一个.i
文件, 需要程序猿手动生成,因为在整个编译阶段不需要每个阶段都生成对应的文件。gcc中对应的是 -E
指令搭配 -o
指令。
-E
指令是运行预编译指令-O
指令是完成预编译后把结果生成一个新文件,如果不搭配-o
指令就无法看到预编译后程序的结果。- 在上例中即为:
gcc -E test.c -o test.i
。运行后得到下图
-
打开
test.i
文件,预处理中处理了C语言中的预处理指令。把所有的预处理指令都替换成对应的代码(如下图)。(把标识符常量M 全部替换成100)
-
预编译也会把全部引用的头文件的全部内容拷贝放置在编译后生成的
.i
文件中如下图。 -
在
test.c
文件中的结构体变量s
后原有注释,解释结构体定义了在哪里,但是经过预编译后,注释被删除掉了(如上图中struct S s;后没有了注释)。
3.1.2编译阶段:
编译阶段会对预编译阶段生成的
.i
文件进行操作,生成的文件后缀为.s
的文件。
- 程序猿手动编译的话,只能对
.i
预编译文件进行编译,不能对.c
和.h
等文件进行编译。
在gcc中,编译指令是-S
。
-S
指令是运行编译指令,在执行编译指令后,会自动生成编译文件(.s
文件),当然也可以使用-o
重命名。- 如在我们上面已经生成
.i
文件后,我们只需要gcc -S test.i
就把test.i文件进行编译了。如下图
我们打开.s
文件查看,发现里面所有的代码都转换成汇编代码(如下图)
说明在编译阶段是把C语言代码转换为汇编代码的过程,在转换过程中会把C代码中的所有的 语法、词法和语义 进行分析,保证程序可以运行。并且会把所有的符号(包含全局变量名、函数名等等)进行汇总,为汇编阶段做前期准备。
3.1.3、汇编阶段
汇编阶段会对编译阶段生成的
.s
文件进行操作,生成的文件后缀为.o
的文件,名为:目标文件。
在gcc中,编译指令是-c
。使用-c
不一定是要拥有.s
文件,就算是普通的.c
文件使用-c
指令都会进行完成。
-c
指令是运行汇编指令,在执行汇编指令后,会自动生成目标文件(.o
文件),当然也可以使用-o
重命名。(在vs2022中是生成.obj
文件)
- 如在我们上面已经生成
.s
文件后,我们只需要gcc -c test.s
就把test.s文件进行汇编了。如下图
如果我们正常使用文本查看器查看.o
文件,会发现全部乱码(如下图)。在汇编处理中,其实是把汇编代码全部转换成二进制代码,说要我们在正常的文本查看器中是查看不了的。
在汇编阶段中,最主要的是把.s
文件中汇总的符号生成符号表
在Linux环境下
,gcc编译生成的.o
文件和可以执行程序都是使用elf
文件格式来组织的。我们可以使用readelf
指令来查看gcc编译生成的.o
文件或者可以执行程序,我们使用readelf
中的-s
指令来查看生成的符号表(readelf test.o -s
)如下图。
上图中把test.i
中的符号汇聚成表,符号表重点在于编译阶段的最后阶段连接做准备。
为了更直观感受符号表。我们再举个栗子:
int sum = 0;//全局变量
int add(int a, int b){
return a + b;
}
int main(){
int a = 10;
int b = 20;
sum = add(a, b);
return 0;
}
我们编译后查看符号表:如下图
上图中可以清晰看到全局变量sum、add函数与main函数在符号表中形成序号。
3.2连接
接下来讲的编译
都代指编译环境中的编译
、连接
都代指编译环境中的连接
。
连接运行后就会生成可执行程序,所以在gcc中只需要生成可执行程序即完成了连接步骤
- 连接的主要作用是把每个
.c
文件编译后(汇编后)形成的符号表进行连接 - 在
1.c
文件中定义了全局变量或者函数。同一工程中,在2.c
文件中我们要使用1.c
文件定义的全局变量或者函数可以使用关键字extern
标识,
为了更直观感受连接的过程,不才举个简单的栗子🫡🫡
创建test.c
与add.c
文件
add.c
文件:
int Add(int a, int b) {
return a + b;
}
test.c
文件:
extern Add(int a, int b);
int main() {
int a = 10;
int b = 20;
int c = Add(a, b);
return 0;
}
我们先把test.c
与add.c
文件编译生成.o
文件,并且查看他们的符号表
test.o
文件:
add.c
文件:
在上面两图中,我们可以看到在未连接生成可执行程序前,我们test.o
中的Add函数的Size
值为0
,反add.o
文件的Add
函数的Size
值为20
,可以看到即使我们在test.c
文件中的Add
函数使用了extern
关键字也没起到作用。
我们连接生成的可执行程序(gcc -o test add.c test.c
),在生成可执行程序test
后使用readelf
查看一下生成的连接表。如下图
其中我们可以看到,在生成的可执行程序的连接表中只看到只有一个Add
函数,并且Size
的值是20
。
说明连接是处理在同一工程中的每个
.c
文件产生的符号表,把每个符号表相对应的符号进行连接,保留有效值
我们画个简图示例。
当然,成功连接的前提是名字相同,若是名字不同,在连接结束后发现Size
为0
则报错。如下图
在VS2022编译器中,报错内容是无法解析的外部命令,说明在text.c文件中的add
函数的值为0
,在连接中也找不到所对应有值的add
函数。
四、编译环境总结
程序在编译运行过程中的简化流程图如下图:
我们也可以得到一个小结论图:
五、运行环境
程序执行的过程:
- 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
- 程序的执行便开始。接着便调用main函数。
- 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
- 终止程序。正常终止main函数;也有可能是意外终止。
想要继续提升内功可以看:《程序员的自我修养》
以上就是本章所有内容。若有勘误请私信不才。万分感激💖💖 如果对大家有用的话,就请多多为我点赞收藏吧~~~💖💖
ps:表情包来自网络,侵删🌹