C语言看完我这篇编译与链接就够啦!!!
1. 前言
Hello!大家好我是小陈,今天来给大家介绍最详细的C语言编译与链接。
2. 编译和链接
我们通常用的编译器,比如Visual Sudio,这样的IDE(集成开发环境)一般将编译和链接的过程一步完成,通常将这这种编译和链接合并到一起的过程称为构建bulid。
 我们经常使用这些编译器后,就不会用一些指令再去编译和链接,因为强大的集成开发环境已经满足了这些编译和链接,但是在代码出错的时候,有时我们会无从下手,所以我觉得我们还是需要深入了解编译和链接的具体步骤。
2.1 被隐藏了的过程
只要你稍微学习一点编程,那么下面的语句你并不陌生,因为这是每个程序员刚开始学的第一个语句,我们在vs编译器操作如下。
#include <stdio.h>
int main()
{char arr[] = { "hello world!!!" };printf("%s\n",arr);//你好世界return 0;
}

 在 Linux 下,当我们使用 GCC 来编译 Hello World 程序时,只须使用最简单的命令(假设源代码文件名 hello.c):
$ gcc hello.c
$ ./a.out
hello world !!!
事实上,上面可以分解为4个步骤,分别是预处理(Prepressing)编译(Compilation)汇编(Assembly)和链接(Linking)。
 
2.1.1 预编译
首先是源代码文件 hello.c 和相关的头文件,如 stdio.h 等被 预编译器 cpp 预编译成一个 .i 文件。对于 C++ 程序来说,它的源代码文件的扩展名可能是 .cpp 或 .cxx,头文件的扩展名可能是 .hpp,而预编译后的文件扩展名是 .ii。第一步预编译的过程相当于如下命令(-E 表示只进行预编译):
$ gcc -E hello.c -o hello.i
or
$ cpp hello.c > hello.i
预编译过程主要处理那些源代的文件中的以 # 开始的预编译指令。比如 #include、#define 等,主要处理规则如下:
- 将所有的 #define 删除,并且展开所有的宏定义。
- 处理所有条件编译指令,比如:#if、#ifdef、#elif、#else、#endif。
- 处理 #include 预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。
- 删除所有的注释 // 和 /* */。
- 添加行号和文件名标识,比如 #2 “hello.c” 2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。
- 保留所有的 #pragma 编译器指令,因为编译器须要使用它们。
当然我们要注意,经过预编译后的.i文件不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到.i文件中。所以当我们无法判断宏定义是否正确,或者头文件包含是否正确时,可以查看预编译后的文件来确定问题。
2.1.2 编译
编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件。
$ gcc -S hello.i -o hello.s
如何进行编译呢?
array[index] = (index+4)*(2+6);
2.1.3 汇编
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。
$ as hello.s -o hello.o
or
$ gcc -c hello.s -o hello.o
使用 gcc 命令从 C 源代码文件开始,经过预编译、编译和汇编直接输出 目标文件
$ gcc -c hello.c -o hello.o
2.1.4 链接
链接说白了就是把许多文件链接在一起,但是这些文件格式好多不相同,有着一步下一步的关系
 链接的过程主要包括 :地址和空间分配,符号决议和重定位等这些步骤。
 链接解决的是一个项目中的文件,多模块之间互相调用的问题。
ld -static crt1.o crti.o crtbeginT.o hello.o -start-group -lgcc -lgcc_eh -lc
-end-group crtend.o crtn.o
//test.c
#include <stdio.h>
//test.c
//声明外部函数
extern int Add(int x, int y);
//声明外部的全局变量
extern int g_val;
int main()
{int a = 10;int b = 20;int sum = Add(a, b);printf("%d\n", sum);return 0;
}
//add.c
int g_val = 2022;
int Add(int x, int y)
{return x+y;
}
我们已经知道,每个源⽂件都是单独经过编译器处理⽣成对应的⽬标⽂件。
 test.c 经过编译器处理⽣成 test.o
 add.c 经过编译器处理⽣成 add.o
 我们在 test.c 的⽂件中使⽤了 add.c ⽂件中的 Add 函数和 g_val 变量。
 我们在 test.c ⽂件中每⼀次使⽤ Add 函数和 g_val 的时候必须确切的知道 Add 和 g_val 的地 址,但是由于每个⽂件是单独编译的,在编译器编译 test.c 的时候并不知道 Add 函数和 g_val 变量的地址,所以暂时把调⽤ Add 的指令的⽬标地址和 g_val 的地址搁置。等待最后链接的时候由 链接器根据引⽤的符号 Add 在其他模块中查找 Add 函数的地址,然后将 test.c 中所有引⽤到 Add 的指令重新修正,让他们的⽬标地址为真正的 Add 函数的地址,对于全局变量 g_val 也是类 似的⽅法来修正地址。这个地址修正的过程也被叫做:重定位。
2.2 编译器干了什么活
从最直观的角度来讲,编译器就是将高级语言翻译成机器语言的一个工具。比如我们用 C/C++ 语言写的一个程序可以使用编译器将其翻译成机器可以执行的指令及数据。我们前面也提到了,使用机器指令或汇编语言编写程序是十分令费事及乏昧的事情,它们使得程序开发的效率十分低下。并且使用机器语言或汇编语言编写的程序依赖于特定的机器,一个为某种 CPU 编写的程序在另外一种 CPU 下完全无法运行,需要重新编写,这几乎是令人无法接受的。
 
 用一行C语言代码的例子来讲述
array[index] = (index + 4) * (2 + 6)
CompilerExpression.c
2.2.1 词法分析
首先源代码程序被输入到 扫描器(Scanner),扫描器的任务很简单,它只是简单地进行词法分析,运用一种类似于 有限状态机(Finite State Machine) 的算法可以很轻松地将源代码的字符序列分割成一系列的 记号(Token)。
 
 词法分析产生的记号一般可以分为如下几类:关键字、标识符、字面量(包含数字、字符串等) 和 特殊符号(如加号、等号)。在识别记号的同时,扫描器也完成了其他工作。比如将标识符存放到 符号表,将数字、字符串常量存放到 文字表 等,以备后面的步骤使用。
 有一个叫做 lex 的程序可以实现词法扫描,它会按照用户之前描述好的 词法规则 将输入的字符串分割成一个个记号。因为这样一个程序的存在,编译器的开发者就无须为每个编译器开发一个独立的词法扫器,而是根据需要改变词法规则就可以了。
 另外对于一些有预处理的语言,比如 C 语言,它的宏替换和文件包含等工作一般不归入编译器的范围而交给一个独立的预处理器。
2.2.2 语法分析
语法分析器(Grammar Parser) 将对由扫描器产生的记号进行语法分析,从而产生 语法树(Syntax Tree)。
 上下文无关语法(Context-free Grammar)在计算机科学中,若一个 形式文法 G = (N, Σ, P, S) 的产生式规则都取如下的形式:V->w,则谓之。 其中 V∈N ,w∈ (N∪Σ)*
 C语言上下文无关法的定义
 语法树
 
2.2.3 语义分析
编译器所能分析的语义是 静态语义(Static Semantic),所谓静态语义是指在编译期可以确定的语义,与之对应的 动态语义(Dynamic Semantic) 就是只有在运行期才能确定的语义。
 静态语义通常包括声明和类型的匹配,类型的转换。比如当一个浮点型的表达式赋值给一个整型的表达式时,其中隐含了一个浮点型到整型转换的过程,语义分析过程中需要完成这个步骤。
2.2.4 中间语言生成
三地址码(Three-address Code) 和 P-代码(P-Code)。我们就拿最常见的三地址码来作为例子
x = y op z
这个三地址码表示将变量 y 和 z 进行 op 操作以后,赋值给 x。这里 op 操作可以是算数运算,比如加减乘除等,也可以是其他任何可以应用到 y 和 z 的操作。三地址码也得名于此,因为一个三地址码语句里而有三个变量地址。我们上面的例子中的语法树可以被翻译成三地址码后是这样的:
t1 = 2 + 6
t2 = index + 4
t3 = t2 * t1
array[index] := t3
我们可以看到,为了使所有的操作都符合三地址码形式,这里利用了几个临时变量: t1、t2 和 t3。在三地址码的基础上进行优化时,优化程序会将 2+6 的结果计算出来,得到 t1 = 6。然后将后面代码中的 t1 替换成数字 6。还可以省去一个临时变量 t3,因为 t2 可以重复利用。经过优化以后的代码如下:
t2 = index + 4
t2 = t2 * 8
array[index] := t2
中间代码使得编译器可以被分为前端和后端。编译器前端负责产生机器无关的中间代码,编译器后端将中间代码转换成目标机器代码。这样对于一些可以跨平台的编译器而言,它们可以针对不同的平台使用同一个前端和针对不同机器平台的数个后端。
2.2.5 目标代码生成与优化
源代码级优化器产生中间代码标志着下面的过程都属于编译器后端。编译器后端主要包括 代码生成器(Code Generator) 和 目标代码优化器(Target Code Optimizer)。
movl index, %ecx            ; value of index to ecx
addl $4, %ecx               ; ecx = ecx + 4
mull $8, %ecx               ; ecx = ecx * 8
movl index, %eax            ; value of index to eax
movl %ecx, array(,eax,4)    ; array[index] = ecx
//上面都是一个代码反汇编后的指令
面的例子中,乘法由一条相对复杂的 基址比例变址寻址(Base Index Scale Addressing) 的 lea 指令完成,随后由一条 mov 指令完成最后的赋值操作,这条 mov 指令的寻址方式与 lea 是一样的:
movl   index, %edx
leal   32(,%edx,8), %eax
movl   %eax, array(,%edx,4)
2.3 链接器年龄比编译器长
参考书籍:《C语言程序员的自我修养》
2.4 模块拼装-静态链接
程序设计的模块化是人们一直在追求的目标,因为当一个系统十分复杂的时候,我们不得不将一个复杂的系统逐步分割成小的系统以达到各个突破的目的。一个复杂的软件也如此,人们把每个源代码模块独立地编译,然后按照须要将它们“组装”起来,这个组装模块的过程就是 链接(Linking)。链接的主要内容就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确地衔接。链接器所要做的工作其实跟前面所描述的“程序员人工调整地址”本质上没什么两样,只不过现代的高级语言的诸多特性和功能,使得编译器、链接器更为复杂,功能更为强大,但从原理上来讲,它的工作无非就是把一些指令对其他符号地址的引用加以修正。链接过程主要包括了 地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution) 和 重定位(Relocation) 等这些步骤。
符号决议有时候称为 符号绑定(Symbol Binding)、名称绑定(Name Binding)、名称决议(Name Resolution),甚至还有叫做 地址绑定(Address Binding)、指令绑定(Instruction Binding) 的,大体上它们的意思都一样,但从细节角度来区分,它们之间还是存在一定区别的,比如“决议”更倾向于静态链接,而“绑定”更倾向于动态链接,即它们所使用的范围不一样。在静态链接,我们将统一称为 符号决议。
2.5 运行环境
- 程序必须载⼊内存中。在有操作系统的环境中:⼀般这个由操作系统完成。在独⽴的环境中,程序 的载⼊必须由⼿⼯安排,也可能是通过可执⾏代码置⼊只读内存来完成。
- 程序的执⾏便开始。接着便调⽤main函数。
- 开始执⾏程序代码。这个时候程序将使⽤⼀个运⾏时堆栈(stack),存储函数的局部变量和返回 地址。程序同时也可以使⽤静态(static)内存,存储于静态内存中的变量在程序的整个执⾏过程 ⼀直保留他们的值。
- 终⽌程序。正常终⽌main函数;也有可能是意外终⽌。
相关文章:
 
C语言看完我这篇编译与链接就够啦!!!
1. 前言 Hello!大家好我是小陈,今天来给大家介绍最详细的C语言编译与链接。 2. 编译和链接 我们通常用的编译器,比如Visual Sudio,这样的IDE(集成开发环境)一般将编译和链接的过程一步完成,通常将这这种编译和链接合…...
 
【React】react 使用 lazy 懒加载模式的组件写法,外面需要套一层 Loading 的提示加载组件
react 组件按需加载问题解决 1 错误信息2 解决方案 1 错误信息 react 项目在创建 router 路由时,使用 lazy 懒加载时,导致以下报错: The above error occurred in the <Route.Provider> component:Uncaught Error: A component suspe…...
 
IDEA的Scala环境搭建
目录 前言 Scala的概述 Scala环境的搭建 一、配置Windows的JAVA环境 二、配置Windows的Scala环境 编写一个Scala程序 前言 学习Scala最好先掌握Java基础及高级部分知识,文章正文中会提到Scala与Java的联系,简单来讲Scala好比是Java的加强版&#x…...
LeetCode第四天(448. 找到所有数组中消失的数字)
给你一个含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果。 官解: 方法一:原地修改 思路及解法 我们可以用一个哈希表记录数组 nums 中…...
【vivado】在原有工程上新建工程
一、前言 在工作中,我们经常需要接触到别人的工程,并在别人的工程上新加设计功能,此时我们需要以别人工程为基础新建工程。 二、在已有工程上新建工程的方法 2.1 vivado 页面file-project-save as... 该方法的优点为:可以直接…...
 
(原型与原型链)前端八股文修炼Day5
一 原型链的理解 原型链定义: 原型链是 JavaScript 中实现对象继承的关键机制之一,它是一种对象之间的关系,通过这种关系,一个对象可以继承另一个对象的属性和方法。 原型链的组成: 每个对象都有一个指向另一个对象的…...
 
逐步学习Go-并发通道chan(channel)
概述 Go的Routines并发模型是基于CSP,如果你看过七周七并发,那么你应该了解。 什么是CSP? "Communicating Sequential Processes"(CSP)这个词组的含义来自其英文直译以及在计算机科学中的使用环境。 CSP…...
 
鸿蒙HarmonyOS应用开发之Node-API开发规范
当传入napi_get_cb_info的argv不为nullptr时,argv的长度必须大于等于传入argc声明的大小。 当argv不为nullptr时,napi_get_cb_info会根据argc声明的数量将JS实际传入的参数写入argv。如果argc小于等于实际JS传入参数的数量,该接口仅会将声明…...
单例模式
文章目录 单例模式特殊类的设计单例模式饿汉模式懒汉模式懒汉VS饿汉懒汉的线程安全单例对象的释放 单例模式 认识单例模式之前先认识一下几种常见的特殊类的设计。 特殊类的设计 设计一个类 只能再堆上创建对象 只能再堆上创建,则通过new来创建对象。 将类的构造函…...
Android OpenMAX - 开篇
Android Media是一块非常庞大的内容,上到APP的书写,中到播放器的实现、封装格式的了解,下到OMX IL层的实现、Decoder的封装,每一块都需要我们下很大的功夫学习。除此之外,我们还要对一些相关的模块进行了解,…...
ubuntu开启ssh服务
1.安装openssh-server sudo apt-get install openssh-server 2.开启服务 sudo servive ssh start 3.设置开机自启动 sudo systemctl enable ssh 4.检查是否开启成功 ps -ef | grep ssh 如使用xshell等连接时失败,可以尝试关闭防火墙: sudo sys…...
测试缺陷定位的基本方法
前后端bug特征 后端: 业务逻辑问题:如任务状态未扭转成功,创建任务失败等数据类问题:如新增的任务在页面没有展示出来等性能类问题:提交任务一直显示创建中、批量操作等待耗时长超时等 前端: 页面显示类…...
 
【数字图像处理matlab系列】数组索引
【数字图像处理matlab系列】数组索引 【先赞后看养成习惯】【求点赞+关注+收藏】 MATLAB 支持大量功能强大的索引方案,这些索引方案不仅简化了数组操作,而且提高了程序的运行效率。 1. 向量索引 维数为1xN的数组称为行向量。行向量中元素的存取是使用一维索引进行的。因此…...
 
【2024系统架构设计】案例分析- 3 数据库
目录 一 基础知识 二 真题 一 基础知识 1 ORM ORM(Object—Relationl Mapping...
 
vue基础——java程序员版(总集)
前言:  这是一个java程序员的vue学习记录。  vue是前端的主流框架,按照如今的就业形式作为后端开发的java程序员也是要有所了解的,下面是本人的vue学习记录,包括vue2的基本使用以及引入element-ui,使用的开发工具…...
Rancher(v2.6.3)——Rancher配置Harbor镜像仓库
Rancher配置Harbor镜像仓库详细说明文档:https://gitee.com/WilliamWangmy/snail-knowledge/blob/master/Rancher/Rancher%E4%BD%BF%E7%94%A8%E6%96%87%E6%A1%A3.md#8rancher%E9%85%8D%E7%BD%AEharbor ps:如果觉得作者写的还行,能够满足您的需…...
 
C++类和对象、面向对象编程 (OOP)
文章目录 一、封装1.抽象、封装2.类和对象(0)学习视频(1)类的构成(2)三种访问权限(3)struct和class的区别(4)私有的成员变量、共有的成员函数(5)类内可以直接访问私有成员,不需要经过对象 二、继承三、多态1.概念2.多态的满足条件3.多态的使用条件4.多态原理剖析5.纯…...
 
Introduction to Data Mining 数据挖掘
Why Data Mining? • The Explosive Growth of Data: from terabytes to petabytes — Data collection and data availability ◦ Automated data collection tools, database systems, Web, computerized society — Major sources of abundant data ◦ Business: Web, e-co…...
常用的 Git 命令
初始化一个新的仓库: git init 克隆一个仓库: git clone <仓库地址> 查看文件状态: git status 添加文件到暂存区: git add <文件名> 提交文件到仓库: git commit -m "提交说明" 查看提交历…...
jackson:JSON字符串(String)类型的成员序列化和反序列化
对于如下类型定义TestTaskInfo,props字段为JSON字符串(这在数据库经常用到),可以自由保存各种类型的数据 Data public class TestTaskInfo {private String id;private String props;public TestTaskInfo() {}public TestTaskInfo(String id, String props) {super…...
 
使用VSCode开发Django指南
使用VSCode开发Django指南 一、概述 Django 是一个高级 Python 框架,专为快速、安全和可扩展的 Web 开发而设计。Django 包含对 URL 路由、页面模板和数据处理的丰富支持。 本文将创建一个简单的 Django 应用,其中包含三个使用通用基本模板的页面。在此…...
k8s从入门到放弃之Ingress七层负载
k8s从入门到放弃之Ingress七层负载 在Kubernetes(简称K8s)中,Ingress是一个API对象,它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress,你可…...
 
【入坑系列】TiDB 强制索引在不同库下不生效问题
文章目录 背景SQL 优化情况线上SQL运行情况分析怀疑1:执行计划绑定问题?尝试:SHOW WARNINGS 查看警告探索 TiDB 的 USE_INDEX 写法Hint 不生效问题排查解决参考背景 项目中使用 TiDB 数据库,并对 SQL 进行优化了,添加了强制索引。 UAT 环境已经生效,但 PROD 环境强制索…...
在rocky linux 9.5上在线安装 docker
前面是指南,后面是日志 sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo sudo dnf install docker-ce docker-ce-cli containerd.io -y docker version sudo systemctl start docker sudo systemctl status docker …...
C++ 基础特性深度解析
目录 引言 一、命名空间(namespace) C 中的命名空间 与 C 语言的对比 二、缺省参数 C 中的缺省参数 与 C 语言的对比 三、引用(reference) C 中的引用 与 C 语言的对比 四、inline(内联函数…...
Spring AI 入门:Java 开发者的生成式 AI 实践之路
一、Spring AI 简介 在人工智能技术快速迭代的今天,Spring AI 作为 Spring 生态系统的新生力量,正在成为 Java 开发者拥抱生成式 AI 的最佳选择。该框架通过模块化设计实现了与主流 AI 服务(如 OpenAI、Anthropic)的无缝对接&…...
 
全志A40i android7.1 调试信息打印串口由uart0改为uart3
一,概述 1. 目的 将调试信息打印串口由uart0改为uart3。 2. 版本信息 Uboot版本:2014.07; Kernel版本:Linux-3.10; 二,Uboot 1. sys_config.fex改动 使能uart3(TX:PH00 RX:PH01),并让boo…...
 
深度学习习题2
1.如果增加神经网络的宽度,精确度会增加到一个特定阈值后,便开始降低。造成这一现象的可能原因是什么? A、即使增加卷积核的数量,只有少部分的核会被用作预测 B、当卷积核数量增加时,神经网络的预测能力会降低 C、当卷…...
作为测试我们应该关注redis哪些方面
1、功能测试 数据结构操作:验证字符串、列表、哈希、集合和有序的基本操作是否正确 持久化:测试aof和aof持久化机制,确保数据在开启后正确恢复。 事务:检查事务的原子性和回滚机制。 发布订阅:确保消息正确传递。 2、性…...
 
springboot 日志类切面,接口成功记录日志,失败不记录
springboot 日志类切面,接口成功记录日志,失败不记录 自定义一个注解方法 import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;/***…...
