写在前面: 该文章一部分参考xxx大学计算机老师的文章, 如有侵犯版权, 请联系删除
编译过程
源代码到可执行程序经历的过程.
(源代码(hello.c) + 头文件(stdio.h))
↓
预处理(cpp) -> hello.i
↓
编译(ccl) -> hello.s
↓
汇编(as) -> hello.o
↓
链接器(ld) <- (静态链接库(libc.a))
↓
可执行文件(hello)
1. 预处理
预处理指令: gcc -E hello.c -o hello.i
预处理完成以下事情:
1. 展开所有的宏定义并删除 #define
2. 处理所有的条件编译指令, 例如 #if #else #endif #ifndef ...
3. 展开头文件, 即将#include替换为头文件实际内容, 递归进行
4. 把所有的注释 // 和 / / 替换为空格
5. 添加行号和文件名标识以供编译器使用
6. 保留所有的 #pragma 指令, 因为编译器要使用
2. 编译
编译指令: gcc -S hello.i -o hello.s
编译完成以下事情:
把预处理之后的文件进行一系列词法分析, 语法分析, 语义分析以及优化后生成相应的汇编代码文件.
3. 汇编
汇编指令: gcc -c hello.s -o hello.o
汇编完成以下事情:
就是将编译生成的汇编代码翻译为机器码, 几乎每一条汇编指令对应一句机器码.
Note: 不同操作系统之间的可执行文件的格式通常是不一样的, 所以造成编译好的 hello 可执行文件没办法直接
复制执行, 而需要在相关平台上重新编译, 当然, 不能运行的原因自然不是这一点, 不同的操作系统接口(windows API 和
Linux System Call)以及相关的类库也是原因之一.
4. 链接
链接指令: gcc hello.o -o hello (编译器默认链接C标准库)
链接完成以下事情:
将各种代码和数据部分收集起来并组合成为一个单一文件的过程, 这个文件可被加载(或被拷贝)到存储器并执行.
Note: 链接可以执行于编译时, 也就是在源代码被翻译成机器代码时; 也可以执行于加载时, 也就是在程序被加载器加载到
存储器并执行时; 甚至执行于运行时, 由应用程序来执行.
其实, 任何一个程序, 它的背后都有一套庞大的代码在支撑着它, 以使得该程序能正常运行.
这套代码至少包含入口函数, 以及其所依赖的函数构成的函数集合. 当然 , 它还包含了各种 标准库函数的实现. 这个"支撑模块"就叫做运行时库(Runtime Library). 而C语言的运行时库, 被称为C运行时库(CRT).
CRT大致包括: 启动与退出相关的代码(包括入口函数及入口函数所依赖的其它函数), 标准库函数(ANSI C标准规定的函数实现)(如: printf, scanf函数都是标准库函数), I/O相关, 堆的封装实现, 语言特殊功能的实现以及调试相关.
4.1 静态链接库
我们几乎每次写程序都难免使用库函数, 而库函数是提前编译好的, 需要的时候直接链接, 那么, 标准库是以什么形式存在? 一个目标文件? 我们知道, 链接的最小单位是一个个目标文件, 如果我们只在程序中使用标准库中的printf函数, 就需要和真个库链接的话岂不是太浪费资源, 但是, 如果把库函数分别定义在彼此独立的代码文件中, 编译出来则是一大堆目标文件, 那也太混乱了吧? 所以, 编译器系统提供了一种机制, 将所有编译出来的目标文件打包成一个单独的文件, 叫做静态库(static library).
Note: Linux/Unix系统下ANSI C的库名为libc.a, 数学函数单独在libm.a库中. 静态库采用一种称为存档(archive)的特殊文件格式保存. 其实就是一个目标文件的集合, 文件头描述了每个成员目标文件的位置和大小. 当链接器和静态库链接时, 链接器会从这个打包文件中"解压缩"出需要的部分目标文件进行链接. 这样也就解决了资源浪费的问题.
4.1.1 静态库简单实现
//在swap.c 中定义swap函数, 在add.c中定义add函数, 再添加个包含它们的calc.h文件
//swap.c
void swap(int *num1, int *num2)
{
int temp = *num1;
*num1 = *num2;
*num2 = temp;
}
//add.c
int add(int a, int b)
{
return a + b;
}
//calc.h
#ifndef CALC_H_
#define CALC_H_
#ifdef _cplusplus
extern "C" {
#endif
void swap(int *, int *);
int add(int, int);
#ifdef _cpluscplus
}
#endif
#endif
Step1. 分别编译它们得到 swap.o 和 add.o 这两个目标文件, 再使用 ar 命令将其打包称为一个静态库.
1. gcc add.c -c -o add.o
2. gcc swap.c -c -o swap.o
3. ar rcs libcalc.a swap.o add.o
Step 2. 使用静态库
//编写test.c 使用这个库中的swap函数
//test.c
#include <stdio.h>
#include <stdlib.h>
#include "calc.h"
int main(int argc, char *argv[])
{
int a = 1, b = 2;
swap(&a, &b);
printf("a= %d, b = %d \n", a, b);
return 0;
}
Step 3. 编译执行test.c
1. gcc test.c ./libcalc.a -o test
2. ./test
Note: 使用C语言标准库时, 编译器会默认进行链接, 不过因为数学函数库libm.a没有默认链接, 所以如果我们使用了数学函数, 在编译时需要在命令行指定 -lm 链接(-l 是指定链接库, m是去掉lib之后的库名), 不过目前新版本的gcc都默认链接libm.c库了.
静态库缺点:
1. 每一个使用了相同的C标准函数的程序都需要和相关目标文件进行链接, 浪费磁盘空间.
2. 当一个程序有多个副本执行时, 相同的库代码部分被载入内存, 浪费内存.
3. 当库代码更新之后, 使用这些库的函数必须全部重新编译.
4.2 动态链接库(也称共享库(shared library))
动态链接库/共享库是一个目标模块, 在运行时可以加载到任意的存储器地址, 并和一个正在运行的程序链接起来. 这个过程就是动态链接(dynamic linking), 是由一个叫动态链接器(dynamic linker)的程序完成的.
Note: Linux/Unix中共享库的后缀名通常是.so, windows的共享库后缀名是.dll.
4.2.1 动态库简单实现
step 1. 首先建立动态库, 删除上面生成的静态库和目标文件, 执行以下指令即可生成动态库, 就是这么简单.
gcc swap.c add.c -shared -o libcalc.so
step 2. 使用静态库, 使用以下命令
1. gcc test.c -o test ./libcalc.so
2. ./test
Note: 使用 ldd 命令查看test文件的依赖, 得知该文件能运行需要依赖libcalc.so这个动态库, 同时也能看到C标准库也是动态链接的(在gcc编译的命令行上加 -static可以要求静态链接)
动态库的优点:
1. 库更新之后, 只需要替换掉动态库文件即可, 无需编译所有依赖库的目标文件.
2. 程序有多个副本执行时, 内存中只需要一份库代码, 节省空间.