【C】编译与链接

server/2025/1/12 7:03:40/

  在本文章里面,我们讲会讲解C语言程序是如何从我们写的代码一步步变成计算机可以执行的二进制指令,并最终执行的。C语言程序运行主要包括两大步骤 -- 编译和链接,接下来我们就来一一讲解。


目录

1  翻译环境和运行环境

2  翻译环境 

 1) 预处理(预编译)

2) 编译

(1) 词法分析

(2) 语法分析

(3) 语义分析 

3) 汇编 

4) 链接


1  翻译环境和运行环境

  在ANSI C(标准C)的任何一种实现里面,都会包含两个不同的环境:

  1) 翻译环境:会将源代码转变为可执行的二进制的机器指令。

  2) 运行环境:用于执行实际的代码。

  两个环境之间的关系可以用图片来描述一下:

用语言简单改过一下就是,各种源文件通过翻译环境中的编译和链接两步,会将我们写的C代码转变成机器能够执行的二进制指令,然后生成可执行程序(.exe为后缀的文件),在运行环境中就可以运行了。

   所以可执行程序中存放的都是二进制指令,如果直接用记事本打开会看到是一堆乱码:


2  翻译环境 

  翻译环境主要由编译和链接两步组成,而编译又由预处理(预编译)、编译、汇编三个过程,所以翻译环境也可以说成是由预处理、编译、汇编、链接四小部分组成

  一个C语言的项目一般会由多个源文件共同构成,每一个源文件会先经过编译器(在vs中为cl.exe)生成对应的目标文件(Windows环境下后缀为.obj,Linux环境下后缀为.o),然后在经过链接器(vs中为link.exe)把各个目标文件和链接库(运行时库(支持程序运行的基本函数的集合)和第三方库)一起链接为可执行程序

 1) 预处理(预编译)

  在预处理阶段,源文件和头文件会被处理成为后缀为 .i 的文件。 

  预处理阶段主要会处理一些以 # 开头的预处理指令,具体规则如下: 

预处理(预编译)处理规则
1将所有的 #define 删除,并展开所有的宏定义
2处理所有的预编译指令,如 #if 、#endif、#ifdef、#elif、#else 等等
3处理 #include 预编译指令,将包含的头文件的内容插入到预编译指令的位置
4删除所有注释
5添加行号和文件名标识,方便后续编译器生成调试信息等
6

保留所有的 #pragma 的编译器指令,编译器后续使用

  第6条我们曾在结构体内存对齐中曾经使用过,如:

//修改默认对齐数为1
#pragma pack(1)//还原为默认对齐数
#pragma pack()

2) 编译

  编译的主要作用将C语言代码转变为汇编代码,会对文件进行一系列的:词法分析,语法分析,语义分析及优化,生成相应的汇编代码文件

  如以下的这个代码:

arr[index] = (index + 5) * (3 * 8); 

(1) 词法分析

  将原代码程序输入扫描器,然后由扫描器进行词法分析,将代码中的字符分割成一系列的记号(关键词,标识符,特殊字符等)。上述代码会在扫描器中会分割成以下记号:
  

(2) 语法分析

  经过词法分析之后,语法分析器会对产生的记号进行语法分析,产生语法树(以表达式为结点的树):

(3) 语义分析 

  接下来就是由语义分析器来完成语义分析,即对表达式进行语法层面分析。编译器的分析是语义分析的静态分析,通常包括声明和类型的匹配,类型转换等等。在这个阶段会报告错误的语法信息。

  语义标识后的语法树如图所示:

3) 汇编 

  经过了预处理和编译之后,就到了汇编阶段,在该阶段会用汇编器将汇编代码转变为机器执行的指令,也就是二进制指令,不做指令优化。

  经过了以上三个阶段,就生成了目标文件。

4) 链接

  链接会将一堆文件链接在一起,生成一个可执行程序。

  链接过程主要包括:地址和空间分配,符号决议和重定位等。其实链接解决的是一个项目中多文件、多模块之间的相互调用问题。

比如:

//add.c
int Add(int x, int y)
{return x + y;
}//test.c
#include<stdio.h>//extern声明外部函数
extern int Add(int, int);int main()
{int a = Add(3, 5);printf("%d\n", a);return 0;
}

在add.c源文件中有一个Add函数。而在test.c源文件中,用extern关键字声明了外部函数并在main函数中使用了Add函数。

  其实在链接的过程中,每一个源文件中分别会对函数和全局变量来生成一个符号表,这个符号表里面存储了函数和全局变量的地址,对于定义了的函数和全局变量会在符号表中存储真正的地址,而对于文件中没有定义只是声明的函数和全局变量的地址会先在符号表中存储一个无效的地址,然后链接的时候,会将符号表合并,在其他模块里面寻找真正定义过相同函数和全局变量的地址,来修正文件中引用到该函数的地址,若符号表中仍存在无效地址,咋会报链接错误。

  比如在以上例子中,假设test.c 生成的符号表为:

test.c 文件的符号表
Add函数0x00000000(无效地址)
main函数0x1187f3c0

add.c 文件的符号表
Add函数0x12f2d7c4

  在链接时,会将两个符号表合并生成一个新的符号表:

合成的符号表
Add函数0x12f2d7c4
main函数0x1187f3c0

  再比如以下这个例子:

//add.c
int add(int x, int y)
{return x + y;
}//test.c
#include<stdio.h>extern int Add(int x, int y);int main()
{int ret = Add(3, 5);return 0;
}
test.c 文件的符号表
Add函数0x00000000(无效地址)
main函数0x1187f3c0

add.c 文件的符号表
add函数0x12f2d7c4

合成的符号表
Add函数0x00000000(无效地址)
main函数0x1187f3c0
add函数0x12f2d7c4

合成的符号表中出现了无效地址,所以运行时会报链接错误:


http://www.ppmy.cn/server/157242.html

相关文章

2025.01.15docker

韩梅梅是某新兴技术公司的新进实习生&#xff0c;该公司专注于提供基于容器的云原生解决方案。为了提升公司的服务效 率和响应速度&#xff0c;公司决定采用Docker容器技术来管理和部署应用。韩梅梅被分配到了Docker运维团队&#xff0c;她的主要任务是 通过Docker CLI和Dock…

Unity3D Huatuo热更环境安装与示例项目详解

前言 Unity3D作为一款强大的游戏开发引擎&#xff0c;广泛应用于各类游戏和应用程序的开发中。然而&#xff0c;随着游戏版本的迭代和功能的增加&#xff0c;热更新技术变得越来越重要。Huatuo是一款基于Unity3D的IL2CPP解释执行框架&#xff0c;可以实现对游戏代码的热更新&a…

torch.gather(input_tensor, dim=1, index=index_tensor)

torch.gather(input_tensor, dim1, indexindex_tensor) dim0代表按着行的顺序取&#xff0c;即列方向上取&#xff1b; dim1代表按着列的顺序取&#xff0c;即行方向上取。 import torch # 示例输入张量 (2D) input_tensor torch.tensor([[10, 20, 30], …

农业信息化、智慧农业领域工作实践总结以及展望

该篇为目录页&#xff0c;结合自身的项目经验进行梳理。详细信息参考目录链接下的具体文章。 农业是一个很宽泛的称呼&#xff0c;大体分为种植业与养殖业两部分&#xff0c;还有一些算是农村范畴业会有所涉及。种植业又可分大田农业、设施农业、风景园林、中草药等。养殖业分畜…

React中的useMemo 和 useEffect 哪个先执行?

在 React 组件的渲染过程中&#xff0c;useMemo 和 useEffect 的执行顺序是不同的。具体来说&#xff1a; useMemo 先执行&#xff1a;useMemo 是在 渲染阶段 执行的&#xff0c;它的作用是缓存计算结果&#xff0c;确保在渲染过程中可以直接使用缓存的值。 useEffect 后执行&…

字节小米等后端岗位C++面试题

C 基础 引用和指针之间的区别&#xff1f;堆栈和堆中的内存分配有何区别&#xff1f;存在哪些类型的智能指针&#xff1f;unique_ptr 是如何实现的&#xff1f;我们如何强制在 unique_ptr 中仅存在一个对象所有者&#xff1f;shared_ptr 如何工作&#xff1f;对象之间如何同步…

【第04阶段-机器学习深度学习篇-1-深度学习基础-深度学习介绍】

1 深度学习概念 深度学习是基于机器学习延伸出来的一个新的领域&#xff0c;由以人大脑结构为启发的神经网络算法为起源加之模型结构深度的增加发展&#xff0c;并伴随大数据和计算能力的提高而产生的一系列新的算法。 2 深度学习发展 其概念由著名科学家Geoffrey Hinton等人…

深入Android架构(从线程到AIDL)_19 IPC的Proxy-Stub设计模式01

1、 复习&#xff1a; IBinder 接口 onTransact()就是EIT造形里的<I>这是标准的EIT造形&#xff0c;其<I>是支持<基类/子类>之间IoC调用的接口。运用曹操(Stub)类&#xff0c;形成两层EIT(两层框架)。