跳转至

C语言编译与链接(基础)

约 1799 个字 70 行代码 7 张图片 预计阅读时间 7 分钟

在ANSIC的任何一种实现中,存在两个不同的环境

  1. 翻译环境:源代码被转换为可执行的机器指令
  2. 运行环境:实际执行代码

翻译环境

翻译环境是由编译和链接两个过程组成的,而其中的编译过程可以分解成:预处理(也称预编译)、编译、汇编三个过程

当一个C语言项目(工程)中包含多个.c为后缀的源文件时,将执行下面的步骤生成可执行程序

  1. 多个.c文件单独经过编译器编译处理生成对应的目标文件(目标文件也是二进制文件)

Note

在Windows环境下的目标文件的后缀是 .obj ,Linux环境下目标文件的后缀是 .o

  1. 多个目标文件和链接库⼀起经过链接器处理生成最终的可执行程序。

Note

链接库是指运行时库(它是支持程序运行的基本函数集合)或者第三方库

编译过程

预处理过程

在预处理阶段,源文件和头文件会被处理成为.i为后缀的文件,预处理阶段主要处理那些源文件中#开始的预编译指令

Text Only
1
2
gcc下生成.i文件的命令
gcc -E test.c -o test.i

预处理过程处理规则:

  1. 将所有的 #define 删除,并展开所有的宏定义
  2. 处理#include预编译指令,将包含的头文件的内容插入到该预编译指令的位置。这个过程是递归进行的,即被包含的头文件也可能包含其他文件
C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
源代码中
#define _CRT_SECURE_NO_WARNINGS 1

#include <stdio.h>

#define MAX 100

int main()
{
    printf("%d\n", MAX);

    return 0;
}

预处理后的代码

int main()
{

    printf("%d\n", 100);

    return 0;
}
  1. 处理所有的条件编译指令,如: #if#ifdef#elif#else#endif
  2. 删除所有的注释(本质是将注释替换为空格)
  3. 添加行号和文件名标识,方便后续编译器生成调试信息等
  4. 保留所有的#pragma的编译器指令,编译器后续会使用

经过预处理后的.i文件中不再包含宏定义,因为宏已经被展开。并且包含的头文件都被插入到.i文件中。当需要查看宏定义或者头文件是否包含正确的时候,可以查看预处理后的.i文件来确认

编译过程

编译过程就是将预处理后的⽂件进行一系列的:词法分析、语法分析、语义分析及优化,生成相应的汇编代码文件以及符号汇总

Text Only
1
2
gcc下生成.s文件的命令
gcc -S test.i -o test.s

Note

编译过程本质:将C语言代码翻译成汇编代码

词法分析

将源代码程序被输入扫描器,扫描器的任务就是简单的进行词法分析,把代码中的字符分割成一系列的记号(关键字、标识符、字面量、特殊字符等)

C
1
2
例如对于以下代码
array[index] = (index + 4) * (2 + 6);

进行词法分析后得到:

记号 类型
array 标识符
[ 左方括号
index 标识符
] 右方括号
= 赋值
( 左圆括号
index 标识符
+ 加号
4 数值
) 右圆括号
* 乘号
( 左圆括号
2 数值
+ 加号
6 数值
) 有圆括号
语法分析

在语法分析过程中,语法分析器将对扫描产生的记号进行语法分析,产生语法树。这些语法树是以表达式为节点的树

语义分析

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

生成符号汇总

对文件中的每个函数生成对应的符号

C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
add.c中
int add(int x, int y)
{
    return (x + y);
}

test.c中

extern int add(int x, int y);

int main()
{
    return 0;
}

对于上面的实例代码中,在编译过程中将生成两张符号汇总表

对于add.c的符号汇总表

函数 地址
add(函数名) 0x1000(函数有效地址)

对于test.c的符号汇总表

函数名 地址
add(由声明外部符号引出的函数名) 0x0000(无效地址)
main(函数名) 0x2000(函数有效地址)

汇编

在汇编过程中,汇编器是将汇编代码转转变成机器可执行的指令,每一个汇编语句基本上都对应一条机器指令。就是根据汇编指令和机器指令的对照表一一的进行翻译,也不做指令优化,并且此过程会将在编译过程中生成的符号汇总转化成符号表

Text Only
1
2
gcc下生成.o文件的命令
gcc -c test.s -o test.o

C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
add.c中
int add(int x, int y)
{
    return (x + y);
}

test.c中

extern int add(int x, int y);

int main()
{
    return 0;
}

对于上面的实例代码中,在汇编过程中将两张符号汇总表转化成符号表

对于add.c的符号汇总表

函数 地址
add(函数名) 0x1000(函数有效地址)

对于test.c的符号汇总表

函数 地址
add(由声明外部符号引出的函数名) 0x0000(无效地址)
main(函数名) 0x2000(函数有效地址)

链接过程

链接过程主要包括:地址和空间分配,符号决议和重定位等过程

链接解决的是⼀个项目(工程)中多文件、多模块之间互相调用的问题

C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
add.c中
int add(int x, int y)
{
    return (x + y);
}

test.c中

extern int add(int x, int y);

int main()
{
    int ret = add(3, 5);
    printf("%d\n", ret);

    return 0;
}

因为每个源文件都是单独经过编译器处理生成对应的目标文件,即test.c文件经过编译器处理生成test.oadd.c文件经过编译器处理生成add.o文件

重定位:在 test.c 文件中每一次使用 add 函数的时候必须确切的知道 add的地址,但是由于每个⽂件是单独编译的,在编译器编译 test.c 的时候并不知道 add 函数的地址,所以暂时把调用 add 的指令的目标地址搁置(即符号表中的无效地址)。等待最后链接的时候由链接器根据引用的符号 add 在其他模块中查找 add 函数的地址,然后将 test.c 中所有引用到add 的指令重新修正,让他们的目标地址为实际存在的 add 函数的地址

Note

如果在连接过程中找不到对应的函数地址,即无效地址无法由有效地址替代时,将会报出“未定义的外部符号”类似的错误

运行环境

  1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
  2. 程序的执行便开始。接着便调用main函数。
  3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack)(也称为函数栈帧),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程⼀直保留他们的值。
  4. 终止程序。正常终止main函数或者意外终止main函数