注:由于VS2019是集成开发环境,不方便观察细节。我们使用Linxu gcc来演示编译和链接
目录
预处理阶段会把预处理指令转换,例如#include、define(完成替换)是一个预处理指令,同时删除注释
在链接期间,把跨多个文件的符号整合,这也是为什么在一个源文件中写的符号再另一个源文件中也可以使用。
1. 程序的翻译环境和执行环境
在C语言中,存在两个环境:
1.翻译环境,在这个环境中源代码被转换为可执行的机器指令(二进制指令)。
2.执行环境,它用于实际执行代码。
2. 编译和链接
编译器
编译器,是一个根据源代码生成机器码的程序。
厂商 |
C | C++ |
GNU | gcc |
g++ |
LLVM |
clang | clang++ |
2. 编译的几个阶段
在编译期间还分为了三个阶段,分别是预编译-->编译-->汇编
2.1 预编译(预处理):
我们首先在gcc底下创建一个test.c源文件和一个Add.c源文件
#include <stdio.h>
extern int Add(int, int);//申明外部符号
int main()
{
int a = 10;
int b = 20;
int c = Add(a, b);
printf("%d\n", c);//30
return 0;
}
#define _CRT_SECURE_NO_WARNINGS
int Add(int x, int y)
{
return x + y;
}
通过指令:gcc test.c -E -o test.i
-E让test.c源文件程序在预编译阶段停下来,-o把程序运行结果放进test.i(预处理后生成的文件)文件中。
在test.i中你会发现屏幕上多出800多行代码,extern和main函数都和源文件一样,唯独#include <stdio.h>变成了800行代码
当我们打开头文件时,会发现和预编译期间多出来的代码相同
把头文件的相关内容包含到test.i中
预处理阶段首先会进行头文件的包含
测试预处理阶段还会做什么
#define Max 100 //定义Max值为100 #include <stdio.h> extern int Add(int, int);//申明外部符号 int main() { int z = Max; int a = 10; int b = 20; int c = Add(a, b); printf("%d\n", c);//30 return 0; }
重复操作,打开test.i后发现注释和#define不见了,而且z直接被赋值成100
预处理阶段会把预处理指令转换,例如#include、define(完成替换)是一个预处理指令,同时删除注释
2.2 编译
让程序在编译期间停下来指令:gcc test.i -S后生成test.s编译文件,同样对Add.i文件处理
打开test.s文件,会发现这其实是汇编代码
编译期间把C语言代码翻译成了汇编代码
在其中进行了1.语法分析 2.词法分析 3.语义分析4.符号汇总等操作 --《编译原理》
符号汇总只会整理出全局符号,在main函数中整理出Add,main,在Add.c中会中整理出Add
2.3 汇编
gcc test.s -c --> 让程序在汇编期间停下生成了test.o文件 gcc Add.s -c -->同理
注意:windos环境下目标文件后缀为XXX.obj ,在Linxu环境下目标文件为XXX.o
目标文件是二进制
汇编这个过程把汇编指令转换成二进制指令
当我们通过一定方法去解读二进制指令后,打开符号表
在test.i中汇总了Add,main符号,在Add.i中汇总了Add符号
符号汇总后,形成符号表,符号表同时记录了地址,如果我们屏蔽Add函数,在main函数中找不到有效的Add函数地址,便会在符号表里放入无意义的值,无法找到有效地址,main函数则获得了有效地址
2.4 链接
链接需要做的:
1.合并段表(.o程序和.exe可执行程序都是同一种类型格式文件,需要合并相同段上的内容)
2.符号表的合并和重定位(Add符号出现了两次,其中main中的Add地址是无意义的,Add函数中的Add是有地址的,既然符号名相同,我们便合并成为一个符号,此时Add地址有意义,当我们需要找到对应函数通过地址寻找即可。)
同理,如果该符号地址无意义,无法通过地址寻找到有效的代码,程序便会在链接期间报错
如果我们在VS屏蔽了Add函数,可以发现程序在.obj链接阶段报错,或者写错了名字导致了无法把符号整理在一起,该符号还是无意义
在链接期间,把跨多个文件的符号整合,这也是为什么在一个源文件中写的符号再另一个源文件中也可以使用。
2.5 运行环境
程序执行的过程:
1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序 的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2. 程序的执行便开始。接着便调用main函数。
3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回 地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程 一直保留他们的值。
4. 终止程序。正常终止main函数;也有可能是意外终止。
2.6 头文件和库文件的区别
头文件:
在编程过程中,程序代码往往被拆成很多部分,每部分放在一个独立的源文件中,而不是将所有的代码放在一个源文件中运行时,编译器不知道代码用法是否正确,只有借助头文件中的函数声明来判断。
库文件:
有时候我们会有多个可执行文件,他们之间用到的某些功能是相同的,我们想把这些共用的功能做成一个库,方便大家一起共享(规范化代码)。库中的函数可以被可执行文件调用,也可以被其他库文件调用。库文件又分为静态库文件和动态库文件。
3. 预处理详解
3.1 预定义符号
这些预定义符号都是语言内置的
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
想获得当前程序执行的信息,便可以使用预定义符号 将信息计入到文件中,方便以后查看
int main()
{
int i = 0;
FILE* pf = fopen("log.txt", "a");
if (pf == NULL)
{
return 1;
}