C语言的编译和链接:从源代码到可执行文件

📝前言:
这篇文章主要讲解一下C语言的编译和链接,帮我们更好的理解程序的执行过程,更好的理解计算机系统。

🎬个人简介:努力学习ing
📋个人专栏:C语言入门基础
🎀优快云主页 愚润求学
🌄每日鸡汤:山高路远,唯有逢山开路,遇水架桥


一,编译和链接

1. 什么是编译和链接?

在编写C语言程序时,我们通常会写一个或多个.c文件(源代码文件)。计算机并不能直接理解这些文本文件,需要将它们转换为机器可以执行的二进制文件。这个过程分为两个主要步骤:

  • 编译:将源代码(.c文件)转换为目标文件(.o.obj文件)。
  • 链接:将多个目标文件和库文件合并,生成最终的可执行文件(如.exe.out文件)。

下面我们详细讲解这两个过程。

2. 编译过程

编译是将C语言源代码转换为机器代码的过程。它分为以下几个步骤:

2.1 预处理(Preprocessing)

预处理阶段,源文件和头文件会被处理成以.i为后缀的文件。预处理主要处理源文件中以#开头的预编译指令,比如:

  1. 宏定义处理:删除所有#define,并展开所有宏定义。假设代码中有#define PI 3.14159,预处理后代码中所有的PI都会被替换为3.14159
  2. 条件编译指令处理:处理#if#ifdef#elif#else#endif等条件编译指令,根据条件决定代码的取舍。
  3. 头文件包含处理:处理#include预编译指令,将包含的头文件内容插入到该指令的位置,这个过程是递归进行的。如果test.c#include "stdio.h",预处理时stdio.h的内容会被插入到#include所在的位置。
  4. 保留#pragma指令:保留#pragma编译器指令,供后续编译器使用。
  5. 注释删除:删除所有注释,让代码更加简洁,便于后续处理。

在gcc环境下,可以使用gcc -E test.c -o test.i命令查看预处理后的结果。

示例:

#include <stdio.h>
#define PI 3.14159

int main() {
    printf("PI = %f\n", PI);
    return 0;
}

预处理后,#include <stdio.h>会被替换为stdio.h文件的内容,PI会被替换为3.14159


2.2 编译(Compilation)

编译过程是将预处理后的文件进行词法分析、语法分析、语义分析及优化,生成相应的汇编代码文件,使用gcc -S test.i -o test.s命令。
汇编代码一般具备的信息:

  • 指令信息:这些指令对应着计算机处理器的基本操作,如数据传送(MOV)、算术运算(ADD、SUB)、逻辑运算(AND、OR)、跳转(JMP、JE)等
  • 数据信息:定义了程序中使用的各种数据,包括变量、常量等。
  • 控制信息:包含了程序的控制结构信息,如循环、条件判断等。这些控制结构通过跳转指令(如 JMP、JE、JNE 等)来实现。
  • 内存和寄存器信息明确了程序中使用的内存地址和寄存器。寄存器是处理器内部的高速存储单元,汇编代码会频繁地使用寄存器来进行数据处理和传递。

下面通过array[index] = (index + 4) * (2 + 6);这段代码来看看编译过程:

  1. 词法分析:将源代码输入扫描器,扫描器把代码中的字符分割成一系列记号,如关键字、标识符、字面量、特殊字符等。上述代码经过词法分析后会得到array[index6)等16个记号。

  2. 语法分析:语法分析器对扫描产生的记号进行语法分析,生成以表达式为节点的语法树。在这个过程中,编译器会检查代码的语法结构是否正确,比如括号是否匹配、语句是否完整等。
    在这里插入图片描述

  3. 语义分析:语义分析器完成语义分析,主要包括声明和类型的匹配、类型的转换等,同时报告错误的语法信息。比如,如果代码中把两个不同类型的变量进行不兼容的运算,语义分析阶段就会报错。
    在这里插入图片描述

总结:语法错误(拼写,括号匹配),类型不匹配,使用未声明标识符,重复定义,作用域问题等容易引发编译成错误。


2.3 汇编(Assembly)

汇编器将汇编代码转变成机器可执行的指令,每一个汇编语句几乎都对应一条机器指令。使用gcc -c test.s -o test.o命令完成汇编过程。汇编器会根据汇编指令和机器指令的对照表进行翻译,这个过程不做指令优化。


3. 链接过程

链接是将多个目标文件和库文件合并,生成最终可执行文件的过程。链接器的主要任务包括:

3.1 符号解析(Symbol Resolution)

在编译过程中,每个源文件会生成一个目标文件。如果多个文件之间有函数调用或变量引用,链接器需要解析这些符号(函数名、变量名等),确保它们能够正确关联。

示例:

  • main.c中调用了math.c中的add函数。
  • 链接器会找到add函数的定义,并将其与main.c中的调用关联起来。

3.2 重定位(Relocation)

目标文件中的地址通常是相对地址。链接器会将这些相对地址转换为最终可执行文件中的绝对地址。

假设有test.cadd.c两个文件,test.c中使用了add.c中的Add函数和g_val变量。每个源文件单独编译生成对应的目标文件,test.c生成test.oadd.c生成add.o。在编译test.c时,并不知道Add函数和g_val变量的地址,所以暂时搁置调用Add指令的目标地址和g_val的地址。链接器会根据引用的符号Add在其他模块中查找Add函数的地址,然后将test.c中所有引用到Add的指令重新修正,让它们的目标地址为真正的Add函数的地址,对于全局变量g_val也采用类似方法修正地址,这个过程就是重定位。

3.3 生成可执行文件

链接器将所有目标文件和库文件合并,生成一个可执行文件(如a.outprogram.exe)。这个文件可以直接在操作系统中运行。


4. 编译和链接的示意图

以下是一个简单的示意图,展示了从源代码到可执行文件的过程:

在这里插入图片描述

5. 实际使用中的编译和链接

在实际开发中,我们通常使用编译器(如gcc)来自动完成编译和链接的过程。例如:

gcc main.c math.c -o program

这条命令会:

  1. 编译main.cmath.c,生成目标文件。
  2. 链接目标文件,生成可执行文件program

二, 翻译环境和运行环境

在这里插入图片描述

1. 翻译环境

翻译环境就是由上面提到的两个过程:编译和链接组成。而编译又可以进一步细分为预处理、编译、汇编三个子过程。
总结一下:

  • 预处理:主要处理源文件中以#开头的预编译指令
  • 编译:将预处理后的文件进行词法分析、语法分析、语义分析及优化,生成相应的汇编代码文件
  • 汇编:汇编代码转变成机器可执行的指令,每一个汇编语句几乎都对应一条机器指令
  • 链接:将多个目标文件和库文件合并,生成最终可执行文件的过程。

2. 运行环境

程序在翻译环境生成可执行文件后,就进入运行环境:

  1. 程序载入内存:在有操作系统的环境中,一般由操作系统完成程序的载入;在独立环境中,程序的载入可能需要手工安排,或者通过可执行代码置入只读内存来完成。
  2. 程序执行开始:程序载入内存后,开始执行,首先调用main函数。
  3. 执行程序代码:程序使用运行时堆栈存储函数的局部变量和返回地址,同时也可以使用静态内存,静态内存中的变量在程序整个执行过程中一直保留其值。
  4. 终止程序:程序执行结束,正常情况下是main函数执行完毕返回,也有可能因为意外情况终止。

🌈我的分享也就到此结束啦🌈
要是我的分享也能对你的学习起到帮助,那简直是太酷啦!
若有不足,还请大家多多指正,我们一起学习交流!
📢公主,王子:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

愚润泽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值