编译与链接导论
函数如何工作?
函数名转化为地址后 call (调用),计算机处理流自动跳转到对应地址,取指令开始执行代码。
预处理,编译,链接,执行
预处理
源代码向源代码的转换,比如 #define 展开和 #if 条件选择编译。
编译
编写 C 语言无非是:声明和 实现 。
- 声明 :告诉编译器:这里有个东西 (X),是什么,值是多少?不知道。
- 实现 :把声明和声明的实现关联起来,就是实现。
- 对于全局变量,实现是一个数据;对于函数,实现是我们可执行的代码。
- (实现)将会分配空间。
// demo.c
int un_g_initialized_var;
int g_initialized_var = 1;
extern int extern_var;
static int un_init_local_var;
static int init_local_var = 1;
static int local_func() {
return 1;
}
int func() {
return 2;
}
extern int extern_func();
int main() {
return extern_var + extern_func();
}
| 符号 (Symbol) | 类别 | 存储类别 | 链接性 (Linkage) | 上CPU后运行时所在的内存区域 (Typical Segment) | 作用 (Function) |
|---|---|---|---|---|---|
un_g_initialized_var | 变量定义 | 全局 | 外部 (External) | BSS (Block Started by Symbol) | 未初始化的全局变量,运行时初始化为 0。 |
g_initialized_var | 变量定义 | 全局 | 外部 (External) | Data (Initialized Data) | 已初始化的全局变量。 |
extern_var | 变量声明 | N/A (引用) | 外部 (External) | N/A (期望在其他文件定义) | 引用其他编译单元中定义的全局变量。 |
un_init_local_var | 变量定义 | 全局 | 内部 (Internal) | BSS | 具有文件作用域的静态变量,未初始化,运行时初始化为 0。 |
init_local_var | 变量定义 | 全局 | 内部 (Internal) | Data | 具有文件作用域的静态变量,已初始化。 |
local_func | 函数定义 | 函数 | 内部 (Internal) | Code (.text) | 静态函数,只能在当前文件内被调用。 |
func | 函数定义 | 函数 | 外部 (External) | Code (.text) | 普通函数,可供其他文件调用。 |
extern_func | 函数声明 | 函数 | 外部 (External) | N/A (期望在其他文件定义) | 引用其他编译单元中定义的函数。 |
- 临时变量是不存在的,它以指令的方式在
code里。 - 运行时存在栈(stack)里。
编译器怎么编译
- 将
C文件.o或.obj - 分为机器代码 :0 和 1 组成的特定指令。
- 全局变量演化出的数据 :C 文件中的全局变量。
只能一份一份的编译,文件之间的互相配合需要链接器
链接器
解决最小可执行文件(为什么是最小的呢?我们之后继续讨论)的符号未定义问题。任何那些 你没提供对应信息告知定义的具体内容(那些用了的函数的源代码漏写) 的链接都会失败!最后当链接器搜寻一圈后,只要存在未定义符号(也就是nm或者dumpbin中Class是U的符号),链接器就会拉起报错:告诉你所有那些没有定义的符号。 这个时候你的解决方案非常简单——找到这些符号的可重定位文件(一般构建系统的源代码文件名和可重定位文件名相同,只有后缀不同),然后链接的时候提供! 这是所有无动态库的编译场景下解决 undefined reference的 唯一办法 。
https://www.lurklurk.org/linkers/linkers.html
库和接口编程
静态库:
我们可以早就准备好一系列的可重定位文件和一组符号的声明文件,然后我们编程的时候就不用重复造轮子了,直接在 编程的时候利用这些声明文件告知编译器我担保这些符号存在 ,编译的时候 通过编译生成咱们自己的可重定位文件 ,然后 链接的时候把这些早就准备好的可重定位文件和我们自己的重定位文件组合起来构成一个可执行文件.
我们以在微控制器上进行嵌入式开发为例,来看看这套流程是如何让你“偷懒”的:
假设你正在写一个水流发电的控制程序,你需要用到一个复杂的数学运算:求平方根 sqrt()。
- 供应商的准备(造轮子):
ARM 官方或者交叉编译工具链的开发者,早就写好了极其高效的求平方根的C代码
sqrt.c。他们把它编译成了sqrt.o,并和其他数学函数(sin.o,cos.o等)一起打包成了一个静态库文件:libm.a(Math Library)。同时,他们提供了一个包含所有这些数学函数声明的声明文件:math.h。 - 你的编码阶段(担保):
你在你的
main.c里写下了#include <math.h>,然后直接调用了sqrt(4.0)。 - 你的编译阶段(生成自己的
.o): 编译器处理main.c时,看到#include <math.h>里的声明,心想:“行,你担保了sqrt这个符号存在,我先放行。” 于是顺利编译出main.o。但在main.o内部,sqrt的内存地址是空的。 - 最终的链接阶段(组合):
你敲下编译命令,链接器开始工作。它拿到你的
main.o,发现缺少sqrt的具体实现。这时候,你告诉链接器:“去libm.a里找!”。链接器解开libm.a,精准地把里面的sqrt.o揪出来,和你的main.o拼合在一起。最后生成了烧录到单片机里的可执行固件。
.a是打包(用归档管理器ar)后的.o,当代码中使用到相应的.o时,会从.a中检索出来对应的.o然后链接到程序里,但是.o里如果还是有只声明未找到定义的,要继续检索.a直到全部定义
动态库/共享库:
MCU+RTOS几乎用不到,Linux里常用
如果链接器发现某个符号的定义存在于共享库中,它就不会在最终的可执行文件中包含该符号的定义。相反,链接器会在可执行文件中记录符号的名称以及它应该来自哪个库.
也就是可执行文件里缺少这部分定义,到时候去动态库里运行,可执行文件里只给个声明
还有一点,共享库会整个映射到地址中去
C++的编译与链接
为了支持 函数重载 、 命名空间 、类成员函数等 C 语言没有的特性,C++ 编译器会对源代码中的函数名进行复杂的编码,这一过程称为名称修饰(Name Mangling)
但是静态库.a是由C编辑器编译.c生成的,C编辑器不进行名称修饰,
那么当编译好C++后,只声明未定义的变量从静态库中是找不到的,因为变量名被名称修饰了
使用extern “C”

.h文件
1.有了.a但是怎么用呢,这里需要.h文件去使用
2.自己的项目把要用的文件声明都放到一个.H文件里,可以避免重复声明
有什么?:
- 函数声明 :
void LED_Init(void);(告诉编译器有这么个函数)。 - 宏定义和常量 :
#define MAX_BUFFER_SIZE 1024。 - 结构体和类型定义 :
typedef struct { ... } GPIO_Config;