网站手机端跳转页面模板,如何建立p2p网站,广告seo是什么意思,软件开发流程图片1. 前言
Hello#xff01;大家好我是小陈#xff0c;今天来给大家介绍最详细的C语言编译与链接。
2. 编译和链接
我们通常用的编译器#xff0c;比如Visual Sudio,这样的IDE(集成开发环境#xff09;一般将编译和链接的过程一步完成#xff0c;通常将这这种编译和链接合…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.ior
$ 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] (index4)*(26);2.1.3 汇编
汇编器是将汇编代码转变成机器可以执行的指令每一个汇编语句几乎都对应一条机器指令。
$ as hello.s -o hello.oor
$ gcc -c hello.s -o hello.o使用 gcc 命令从 C 源代码文件开始经过预编译、编译和汇编直接输出 目标文件
$ gcc -c hello.c -o hello.o2.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 xy;
}我们已经知道每个源⽂件都是单独经过编译器处理⽣成对应的⽬标⽂件。 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.c2.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。在三地址码的基础上进行优化时优化程序会将 26 的结果计算出来得到 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函数也有可能是意外终⽌。