C语言编译过程全面解析

今天是2025年1月26日,农历腊月二十七,一个距离新春佳节仅一步之遥的日子。城市的喧嚣中,年味已悄然弥漫——能在这个时候坚持上班的人,真可称为“牛人”了吧,哈哈。。。。

此刻,我在重新审视那些曾被遗忘的角落——C语言,这门陪伴了编程生涯初期的语言,如今再次拾起,竟有如老友重逢,倍感亲切,又回到了那个最初的起点。

C语言编译过程

四个步骤:

(1)预处理:展开头文件/宏替换/去掉注释/条件编译(test.i )。
(2)编译 :检查语法,生成汇编 ( test.s)。
(3)汇编:汇编代码转换机器码(test.o )。
(4)链接:链接到一起生成可执行程序 a.out/a.exe。

在这里插入图片描述

一、预处理

1、**展开所有的宏(macro):**预处理器会查找源代码中的宏定义(使用#define指令定义),并将所有宏调用替换为相应的宏定义。
例如,源代码#define PI 3.14,则预处理器会将所有出现的PI替换为3.14

2、**处理所有条件编译指令:**如#if#elif#else#endif等,这些指令允许程序员根据条件编译不同的代码段。

3、处理#include指令:预处理器会查找源代码中的#include指令,将被包含文件的内容插入到源文件中的指定位置。
这通常用于包含头文件,以便在多个源文件中共享定义和声明。

4、**删除所有注释:**注释是程序员为代码添加的解释性文字,对程序的运行没有实际作用,因此预处理器会将其删除。

5、 **添加行号和文件名信息:**以便在编译时编译器可以使用这些信息来显示警告或错误信息。

预处理结束后,会产生一个后缀为.i的临时文件,该文件是源代码的修改版,已经删除了注释、展开了宏、包含了头文件等。

示例:
a1.c

#include <stdio.h>
int main(void) {printf("hello world\n");system("pause");return 0;
}

执行命令:
-E 是让编译器在预处理之后就退出,不进行后续编译过程;
-o 指定输出文件名。

[admin@myhost testc]$ gcc  -E  a1.c  -o  a1.i

生成a1.i文件
在这里插入图片描述

.c 中的头文件展开、[宏展开]。生成的文件是 .i 文件,预处理之后的程序还是文本,可以用文本编辑器打开。
预处理后的文件变大

头文件

什么是头文件
头文件(Header Files)是C语言中用来声明函数、宏和数据类型的文件(只是声明,不占用内存空间),通常以**“.h”**作为后缀。使得多个源文件可以共享这些声明和定义,从而提高代码的重用性和可读性。
在这里插入图片描述
头文件的作用

声明函数和变量:头文件可以包含函数和变量的声明,使得不同的源文件可以共享这些声明。
定义宏:头文件可以定义宏,这样在多个源文件中都可以使用相同的宏。
包含其他头文件:头文件可以包含其他头文件,从而形成一个头文件的层次结构。

示例:自定义头文件

  1. 创建头文件:.h 扩展名的文件
    myheader.h的头文件。
#ifndef MYHEADER_H
#define MYHEADER_H// 函数声明
void myFunction();// 宏定义
#define MY_MACRO 100// 类型定义(可选)
typedef struct {int x;int y;
} Point;#endif // MYHEADER_H

预处理器指令#ifndef、#define和#endif来防止头文件被多次包含

  1. 创建源文件:.c文件,并实现头文件中声明的函数
    myfunctions.c的文件。
#include "myheader.h"
#include <stdio.h>void myFunction() {printf("Hello from myFunction!\n");
}
  1. 使用头文件
    main.c的文件
#include "myheader.h"int main() {myFunction();printf("MY_MACRO = %d\n", MY_MACRO);Point p;p.x = 10;p.y = 20;printf("Point p = (%d, %d)\n", p.x, p.y);return 0;
}

预处理命令

C语言的预处理命令是由预处理器在编译之前执行的指令。
这些指令以#字符开头,主要目的是在编译之前对源代码进行文本替换、条件编译、文件包含等操作。

C语言中常见的预处理命令:
在这里插入图片描述

1、宏定义 (#define)
宏可以是简单的常量、带参数的宏(类似于函数)或者更复杂的结构。
宏定义是预处理命令中最常见的一种。

(1). 定义常量宏
常量宏是最简单的宏类型,它们用于定义常量值。例如:

#define PI 3.14159
#define MAX_SIZE 100

在代码中,每当预处理器遇到PI或MAX_SIZE时,它们都会被替换为3.14159和100。

(2). 定义带参数的宏(宏函数)
宏也可以像函数一样接受参数,并在展开时替换这些参数。例如:

#define SQUARE(x) ((x) * (x))

这个宏接受一个参数x,并返回它的平方。注意,由于宏是文本替换,所以它们不执行类型检查,也不会导致函数调用的开销。

(3). 条件编译宏
宏还可以用于条件编译,根据宏的定义与否来决定是否包含某段代码。例如:

#define DEBUG#ifdef DEBUG// 这段代码在定义了DEBUG宏时会被编译printf("Debug mode is on.\n");
#else// 这段代码在没有定义DEBUG宏时会被编译// printf("Debug mode is off.\n");
#endif

如果定义了DEBUG宏,则编译器会包含printf(“Debug mode is on.\n”);这行代码;否则,它会忽略它。

2、文件包含 (#include)
用于在当前文件中包含(插入)另一个文件的内容。
例如:#include <stdio.h> 包含了标准输入输出库的头文件。
也可以包含用户自定义的头文件:#include “myheader.h”。
3、条件编译
根据宏的定义与否来决定是否编译某段代码。
#if、#ifdef(如果定义了某个宏)
#ifndef(如果没有定义某个宏)
#else、#elif(else if的缩写)
#endif指令。
例如:#ifdef DEBUG … #endif 用于在定义了DEBUG宏时编译包含的代码。
4、宏取消定义 (#undef)
用于取消之前定义的宏。
例如:#undef PI 会取消PI宏的定义。
5、行控制 (#line)
用于改变当前行号和文件名,通常用于由其他程序生成的源代码中。
例如:#line 100 “newfile.c” 会将接下来的代码行视为位于名为newfile.c的文件的第100行。
6、错误和警告 (#error 和 #warning)
用于生成编译时的错误和警告信息。
例如:#error “This is an error message” 会导致编译器显示错误消息并停止编译。
#warning “This is a warning message” 会导致编译器显示警告消息但继续编译。
7、预定义的宏
C预处理器定义了一些预定义的宏,如:
LINE(当前行号)
FILE(当前文件名)
DATE(编译日期)
TIME(编译时间)等。

二、编译

将前面预编译后的文件进行编写,命令:

gcc -S a1.i -o a1.s

编译阶段的主要任务是将预处理后的C代码转换为汇编代码。这一转换过程涉及多个步骤,包括词法分析、语法分析、语义分析和代码生成(生成汇编代码)。

1、词法分析
任务:将源代码分解成一个个基本的元素,如变量名、常量、关键字、运算符和分隔符等。
输出:这些基本元素通常被称为“词法单元”或“标记”。
2、语法分析
任务:检查源代码的结构或语法是否正确,并构建所谓的抽象语法树(AST)。
AST:是源代码逻辑结构的一个层级模型,它表示了源代码中各个元素之间的关系。
输出:如果源代码语法正确,则生成抽象语法树;如果语法错误,则编译器会报错并停止编译。
3、语义分析
任务:在语法分析的基础上,进一步检查源代码是否有语义错误,例如变量类型不匹配、使用了未声明的变量或函数等。
输出:如果源代码语义正确,则继续后续的编译过程;如果语义错误,则编译器会报错并停止编译。
4、代码生成(生成汇编代码)
任务:将经过词法分析、语法分析和语义分析后的源代码转换为汇编语言代码。
汇编代码:是一种低层次的编程语言,更接近于机器语言,但比机器语言更易于人类阅读和理解。
输出:生成的汇编代码文件通常具有.s扩展名。

举例:
example.c:

#include <stdio.h>int main() {int a = 5;int b = 10;int sum = a + b;printf("Sum: %d\n", sum);return 0;
}

预处理:
使用预处理器处理example.c,将头文件stdio.h的内容包含进来,并处理宏定义等。
输出预处理后的文件example.i
编译:
词法分析:编译器读取预处理后的代码,将其分解成词法单元,如关键字int、return,标识符a、b、sum、main,运算符+、=,以及分隔符等。
语法分析:编译器根据C语言的语法规则,检查这些词法单元是否构成了有效的语法结构,并构建抽象语法树(AST)。例如,它会识别出int a = 5;是一个变量声明和初始化的语句。
语义分析:编译器进一步检查这些语法结构是否有意义。例如,它会检查变量a、b、sum在使用前是否已被声明,以及它们的类型是否匹配。此外,它还会检查函数调用printf是否合法,即是否提供了正确类型和数量的参数。
代码生成:如果语义分析通过,编译器将抽象语法树转换为汇编代码。

.section .data
sum_fmt: .asciz "Sum: %d\n".section .text
.globl main
main:pushq   %rbpmovq    %rsp, %rbpsubq    $16, %rspmovl    $5, -4(%rbp)      ; int a = 5;movl    $10, -8(%rbp)     ; int b = 10;movl    -4(%rbp), %eax    ; eax = aaddl    -8(%rbp), %eax    ; eax = eax + bmovl    %eax, -12(%rbp)   ; int sum = eax (即 a + b 的结果)leaq    sum_fmt(%rip), %rdi ; 设置第一个参数为格式字符串movl    -12(%rbp), %eax    ; 设置第二个参数为 sum 的值movl    %eax, %esi         ; esi = eax (即 sum 的值)xorl    %eax, %eax         ; 清零 eax,作为 printf 的返回值占位符call    printf             ; 调用 printf 函数movl    $0, %eax          ; 设置返回值 0leave                     ; 清理栈帧ret                       ; 返回

三、汇编

将前面编译后的文件进行汇编,命令:

gcc -c a1.s -o a1.o

汇编阶段的主要任务是将汇编代码转换为机器代码(也称为目标代码或二进制代码)。这一转换过程是由汇编器(Assembler)完成的。

汇编指令解析
汇编器逐条读取汇编代码中的指令,并根据汇编指令和机器指令的对照表将其转换为对应的机器指令。
每条汇编指令通常都对应一条或多条机器指令。

地址和符号处理
在汇编过程中,汇编器需要处理汇编代码中的地址和符号。
例如,对于变量和函数的引用,汇编器会将其转换为相应的内存地址。
此外,汇编器还会处理标签(labels)和跳转指令(如goto、if等),确保它们能够正确地跳转到目标位置。

生成目标文件
经过汇编器处理后的代码被转换为机器代码,并存储在目标文件(通常具有.o或.obj扩展名)中。目标文件是二进制格式的,包含了机器可以直接执行的指令和数据。

在这里插入图片描述

四、链接

命令:gcc a1.o -o a1.exe

链接阶段是将多个目标文件(.o或.obj文件)和库文件合并成一个可执行文件的过程。

这个过程涉及多个步骤,包括符号解析、重定位以及处理静态库和动态库等。
1、符号解析
链接器会解析目标文件中的符号信息。
符号通常包括变量名、函数名等,它们代表了程序中的不同实体。
链接器会检查每个目标文件中的符号定义和引用,确保所有引用的符号都有相应的定义。
如果某个符号在多个目标文件中都有定义,链接器会根据链接规则(如C语言的“one definition rule”)来决定使用哪个定义。
2、重定位
在编译和汇编阶段,目标文件中的代码和数据被放置在相对地址中。
然而,在链接阶段,这些相对地址需要被转换为绝对地址,以便程序在运行时能够正确地访问内存中的代码和数据。
链接器会根据目标文件中的重定位信息,调整代码和数据的位置,确保它们能够被正确地加载和执行。

3、处理静态库和动态库
链接阶段还需要处理静态库和动态库。
静态库是一组预编译的目标文件的集合,它们在链接时被复制到最终的可执行文件中。
动态库则是在程序运行时动态加载的库文件,链接器会在可执行文件中记录动态库的依赖关系,并在程序运行时加载这些库。
使用动态库可以减小可执行文件的大小,并且当库文件更新时,无需重新编译整个程序。
静态库:
静态库是一组已经被编译和链接成二进制代码的程序模块,这些模块在编译时被合并到最终的可执行文件中。
特点:
在编译时将库的代码嵌入到可执行文件中,因此可执行文件独立于库的存在。
每次程序编译时,静态库的代码都被复制到生成的可执行文件中。
生成的可执行文件包含了库的所有必要代码,因此文件通常较大。
可执行文件不依赖于外部库文件,可以在没有库文件的机器上独立运行。
文件扩展名:通常以.a(Unix/Linux)或.lib(Windows)为文件扩展名。

使用场景:适用于对执行文件大小没有严格限制、需要在没有库文件的机器上运行或需要避免动态链接带来的依赖性的场景。

动态库:
动态库是一组已经被编译和链接成二进制代码的程序模块,但它们在运行时被加载到内存中,而不是在编译时被合并到可执行文件中。
特点:
可执行文件在运行时需要动态库的支持。
动态库可以被多个程序共享,从而减小可执行文件的大小。
需要确保目标系统上存在相应的动态库,否则程序将无法正常运行。
易于更新和维护,因为只需替换相应的动态库文件即可,无需重新编译整个程序。
文件扩展名:通常以.so(Unix/Linux)或.dll(Windows)为文件扩展名。
隐式调用:程序在编译时指定依赖的动态库,链接器会在程序运行时自动加载这些库。
显式调用:程序在运行时通过特定的API来加载和调用动态库中的函数,这种方式提高了程序的灵活性。
使用场景:适用于需要多个程序共享库代码、希望节省内存和磁盘空间或需要方便地进行库代码升级和维护的场景。

4、生成可执行文件
链接器会生成最终的可执行文件。这个文件包含了程序的所有代码和数据,并且已经被正确地组织和链接在一起,可以在操作系统上直接运行。

借用网络一张图,总结:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/8076.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【橘子Kibana】Kibana的分析能力Analytics简易分析

一、kibana是啥&#xff0c;能干嘛 我们经常会用es来实现一些关于检索&#xff0c;关于分析的业务。但是es本身并没有UI,我们只能通过调用api来完成一些能力。而kibana就是他的一个外置UI&#xff0c;你完全可以这么理解。 当我们进入kibana的主页的时候你可以看到这样的布局。…

生信软件管家——conda vs pip

pip vs conda&#xff1a; 安装过python包的人自然两种管理软件都用过&#xff0c; Pip install和Conda install在Python环境中用于安装第三方库和软件包&#xff0c;但它们在多个方面存在显著的区别 总的来说&#xff1a; pip是包管理软件&#xff0c;conda既是包管理软件&…

代码随想录——二叉树(二)

文章目录 前言二叉树最大深度二叉树的最小深度翻转二叉树对称二叉树完全二叉树的节点个数平衡二叉树二叉树的所有路径左叶子之和找左下角的值路径总和从中序与后序序列构造二叉树最大二叉树合并二叉树二叉搜索树中的搜索验证二叉搜索树二叉搜索树的最小绝对差二叉树中的众数二叉…

深入剖析 Adam 优化器:原理、优势与应用

在深度学习领域&#xff0c;优化器的选择对模型的训练效率和性能起着决定性作用。Adam优化器作为一种自适应优化算法&#xff0c;凭借其根据历史梯度信息动态调整学习率的特性&#xff0c;备受研究者和工程师的青睐。它巧妙融合了RMSProp和Momentum两种优化算法的理念&#xff…

Mybatis入门

Mybatis入门 一、mybatis的快速入门 1、创建springboot项目 直接选择必须的依赖&#xff1a;MyBatis Framework和MySQL Driver在项目下创建pojo包&#xff0c;用来存放数据库表对应的实体类 2、配置连接信息 在springboot项目的配置文件中application.properties写入一下信…

消息队列篇--通信协议篇--MQTT(通配式主题,消息服务质量Qos,EMQX的Broker,MqttClient示例,MQTT报文等)

MQTT&#xff08;Message Queuing Telemetry Transport&#xff09;是一种轻量级的消息协议。它基于发布/订阅模式&#xff0c;专为低带宽、高延迟或不可靠网络设计。它主要用于物联网&#xff08;IoT&#xff09;设备之间的通信&#xff0c;但也广泛应用于其他需要高效消息传递…

dmfldr实战

dmfldr实战 本文使用达梦的快速装载工具&#xff0c;对测试表进行数据导入导出。 新建测试表 create table “BENCHMARK”.“TEST_FLDR” ( “uid” INTEGER identity(1, 1) not null , “name” VARCHAR(24), “begin_date” TIMESTAMP(0), “amount” DECIMAL(6, 2), prim…

基于OSAL的嵌入式裸机事件驱动框架——消息队列osal_msg

参考B站up主【架构分析】嵌入式祼机事件驱动框架 感谢大佬分享 消息队列 消息分为hdr和bdy&#xff0c;把消息的头dhr和内容bdy做了一个分离的设计 dhr包括指向下一个消息的指针next&#xff0c;len在创建消息的时候使用&#xff0c;dest_id即目标任务&#xff0c;将消息和任务…

关于MySQL InnoDB存储引擎的一些认识

文章目录 一、存储引擎1.MySQL中执行一条SQL语句的过程是怎样的&#xff1f;1.1 MySQL的存储引擎有哪些&#xff1f;1.2 MyIsam和InnoDB有什么区别&#xff1f; 2.MySQL表的结构是什么&#xff1f;2.1 行结构是什么样呢&#xff1f;2.1.1 NULL列表&#xff1f;2.1.2 char和varc…

单相可控整流电路——单相桥式全控整流电路

以下是关于单相桥式整流电路的介绍&#xff1a; 电路构成&#xff08;带阻性负载的工作情况&#xff09; - 二极管&#xff1a;是电路0的核心元件&#xff0c;通常采用四个同型号或根据需求选择不同型号的二极管&#xff0c;如1N4001、1N4007等&#xff0c;如图Vt1和Vt4是一对…

Linux(Centos、Ubuntu) 系统安装jenkins服务

该文章手把手演示在Linux系统下如何安装jenkins服务、并自定义jenkins数据文件位置、以及jenkins如何设置国内镜像源加速&#xff0c;解决插件下载失败问题 安装方式&#xff1a;war包安装 阿里云提供的war下载源地址&#xff1a;https://mirrors.aliyun.com/jenkins/war/?s…

力扣算法题——11.盛最多水的容器

目录 &#x1f495;1.题目 &#x1f495;2.解析思路 本题思路总览 借助双指针探索规律 从规律到代码实现的转化 双指针的具体实现 代码整体流程 &#x1f495;3.代码实现 &#x1f495;4.完结 二十七步也能走完逆流河吗 &#x1f495;1.题目 &#x1f495;2.解析思路…

【Numpy核心编程攻略:Python数据处理、分析详解与科学计算】 1.3 广播机制:维度自动扩展的黑魔法

1.3 《广播机制&#xff1a;维度自动扩展的黑魔法》 前言 NumPy 的广播机制是 Python 科学计算中最强大的工具之一&#xff0c;它允许不同形状的数组进行运算&#xff0c;而无需显式地扩展数组的维度。这一机制在实际编程中非常有用&#xff0c;但初学者往往对其感到困惑。在…

Semantic Kernel - Kernel理解

目录 一、关于Kernel 二、案例实战 三、运行截图 一、关于Kernel 微软的 Semantic Kernel 项目中,Semantic Kernel 是一个工具框架,旨在使得开发人员能够更容易地将大语言模型(如GPT)集成到不同的应用中。它通过提供一组接口、任务模板和集成模块,使开发者能够轻松地设计…

【MySQL】--- 复合查询 内外连接

Welcome to 9ilks Code World (๑•́ ₃ •̀๑) 个人主页: 9ilk (๑•́ ₃ •̀๑) 文章专栏&#xff1a; MySQL &#x1f3e0; 基本查询回顾 假设有以下表结构&#xff1a; 查询工资高于500或岗位为MANAGER的雇员&#xff0c;同时还要满足他们的姓名首字母为…

Qt Designer and Python: Build Your GUI

1.install pyside6 2.pyside6-designer.exe 发送到桌面快捷方式 在Python安装的所在 Scripts 文件夹下找到此文件。如C:\Program Files\Python312\Scripts 3. 打开pyside6-designer 设计UI 4.保存为simple.ui 文件&#xff0c;再转成py文件 用代码执行 pyside6-uic.exe simpl…

openlayer getLayerById 根据id获取layer图层

背景&#xff1a; 在项目中使用getLayerById获取图层&#xff0c;这个getLayerById()方法不是openlayer官方文档自带的&#xff0c;而是自己封装的一个方法&#xff0c;这个封装的方法的思路是&#xff1a;遍历所有的layer&#xff0c;根据唯一标识【可能是id&#xff0c;也可能…

Qt 控件与布局管理

1. Qt 控件的父子继承关系 在 Qt 中&#xff0c;继承自 QWidget 的类&#xff0c;通常会在构造函数中接收一个 parent 参数。 这个参数用于指定当前空间的父控件&#xff0c;从而建立控件间的父子关系。 当一个控件被设置为另一控件的子控件时&#xff0c;它会自动成为该父控…

SOME/IP--协议英文原文讲解1

前言 SOME/IP协议越来越多的用于汽车电子行业中&#xff0c;关于协议详细完全的中文资料却没有&#xff0c;所以我将结合工作经验并对照英文原版协议做一系列的文章。基本分三大块&#xff1a; 1. SOME/IP协议讲解 2. SOME/IP-SD协议讲解 3. python/C举例调试讲解 一、SOM…

Ansible自动化运维实战--script、unarchive和shell模块(6/8)

文章目录 一、script模块1.1、功能1.2、常用参数1.3、举例 二、unarchive模块2.1、功能2.2、常用参数2.3、举例 三、shell模块3.1、功能3.2、常用参数3.3、举例 一、script模块 1.1、功能 Ansible 的 script 模块允许你在远程主机上运行本地的脚本文件&#xff0c;其提供了一…