Linux系统下的编译、连接与运行

众所周知,我们程序员所写的代码都是英文字母与数字的集合,我们人能看懂,但是电脑只能识别高低电压,也就是所说的01代码,它是如何识别我们程序员所写的代码呢,比如c语言、c++。

我们所写的代码又是经过了哪些过程之后,计算机就能识别了呢?也就是本文要讲的我们所写的代码是如何变成可执行的二进制文件的。

这节讲的是Linux系统下,我们所写代码文件(.c/.cpp)文件是如何变成可执行的二进制文件的。

一个cpp文件要变成可执行文件要经过预编译、编译、汇编、连接等步骤:

1、预编译(将 .cpp /c文件生成 .i 文件)

在预编译阶段编译器会来生成 .i 文件,只要处理规则如下:

(1)#define   ,宏替换,也就是将所有用宏表示的东西都用它原来的属性替换掉。

(2)#include ,拷贝头文件  ,将.cpp文件中添加的所有头文件拷贝进cpp/c文件中。注意这个过程是递归进行的,也就是说被包含的文件也可能包含其他文件。

(3)#if  、#else 、#endif  、  #elif  、#ifdef,处理掉所有的条件预编译指令。

(4)删除注释  ,将所有的注释删除 “//”  和 “/**/”

(5)添加行号和文件标识,比如#2 “hello.c” 2 ,以便于编译时编译器产生调试调用的行号信息及用于编译时产生的编译错误或警告时能够显示行号。

(6)保留所有的 #pragam 编译器指令,因为编译器需要使用它们。

2、编译(将 .i 文件生成 .s 文件)

在编译阶段也好经过几个步骤:

(1)词法分析。

arr[i] = (i + 4) * 2;

当源代码程序被输入到扫描器(Scanner),扫描器的任务就是运用一种类似于 有限状态机(Finite State Machine)的算法将源代码字符序列分割成一系类的记号(Token)。比如上面那行代码,总共包含了15个非空字符,经过扫面以后,产生了13个记号,入下表所示。

词法分析产生的记号一般可以分为以下几类:关键字、标识符、字面量(包含数字、字符串等)和特殊符号(如加号、等号)。在识别记号的同时,扫面器也完成了其他工作。比如将标识符放到符号表,将数字、字符串常量存放到文字表等,以备后面的步骤使用。

词法分析一般分析词法错误,比如关键字的拼写。

(2)、语法分析

语法分析就是由语法分析器(Grammar Parser)将对由扫描器产生的记号进行语法分析,从而产生语法树(Syntax Tree)。整个分析过程采用了上下文无关语法(Context-free Grammar)的分析手段。由语法分析器生成的语法树就是表达式(Expression)为结点的树。

语法分析一般分析语法错误,比如下面这种语法错误就是在语法分析阶段通过语法分析器分析出来的。

int fun(int a = 10,int b);

(3)、语义分析

语义分析,由语义分析器(Semantic Analyzer)来完成。编译器所能分析的语义是静态语义(Static Semantic),所谓的静态语义是指在编译期间可以确定的语义,与之对应的动态语义(Dynamic Semantic)就是只有在运行期间才能确定的语义。

静态语义通常包括声明和类型的匹配,类型的转换。比如将一个浮点型赋值给一个指针的时候,语义分析程序就会发现这个类型不匹配,编译器就会报错。动态语义一般指在运行期间出现的语义相关问题,比如将0作为除数就是一个运行期间的语义错误。

经过语义分析阶段以后,整个语法树的表达式都被标识了类型,如果有些类型需要做隐式转换,语义分析程序就会在语法树中插入相应的转换结点。语义分析器还对符号表里的符号类型也做了更新。

语义分析一般是在语法分析之后结合上下文进行分析,如下代码为例

int fun(int a,int b,int c = 10);
int fun(int a,int b = 10;int c);
int fun(int a = 10,int b,int c);

在上面的代码中如果单单来看第二行或者第三行代码,就会发现,在语法分析阶段,语法分析器就会检测出默认值必须从右向左赋值的错误,但是在结合上下文之后就会发现,从上往下看默认值是从右向左依次赋值的。

(4)、代码优化

代码优化就是指将代码中可以优化的部分进行优化,比如在预编译阶段进行了宏替换后,下面的代码变化

#define MAX 10

int fun()
{
    int a = MAX + 10;
}

这一段代码将变为

int fun()
{
    int a = 20;
}

3、汇编 (将 .s 文件生成 .o/.obj 文件)

在汇编阶段要做的事情就是将代码指令翻译成可重入二进制文件。汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。

4、链接(将 .o 文件链接成 .exe 文件 )

链接的主要内容就是把各个模块之间相互引用的部分都处理好,是的各个模块之间能够正确地衔接。链接阶段是最后的阶段,它也有四个步骤:

(1)合并段和符号表。链接的.o文件可以是一个也可以是多个,每一个.o文件都有自己的段和符号表,链接阶段最开始要做的事情就是将所有相同的段和符号表合并。

(2)符号解释,也叫符号决议、符号绑定、名称绑定、名称决议,甚至还有叫做地址绑定、指令绑定的,但是大体上他们的意思都一样。

(3)分配地址和空间

(4)符号的重定位

在连接的过程中,对其他定义在目标文件中的函数调用的指令需要重新被调整,对使用其他定义在其他目标文件的变量来说,也存在同样的的问题。让我们结合具体的CPU指令来了解这个过程。假设我们有个全局变量叫var ,它在目标文件A里。我们在目标文件B里面要访问这个全局变量,比如我们在目标文件B里有这么一条指令:

mov    $0x2a, var

这条指令就是给var这个变量赋值0x2a,相当于C语言里面的语句var = 42;。然后我们编译目标文件B,得到这条指令机器码。

由于在编译目标文件B的时候,编译器并不知道变量var的目标地址,所以编译器在没法确定地址的情况下,将这条mov指令的目标地址设置为0,等待连接器将目标文件A和B连接起来的时候在将其修正。我们假设A和B链接后,变量var的地址确定下来为0x1000,那么链接器将会把这个指令的目标地址修改成0x10000。这个修改地址的过程也叫做重定位(Relocation),每个要被修正的地方叫一个重定位入口(Relocation Entry)。重定位所做的事情就是给程序中每个这样的绝对地址引用的位置“打补丁”,使他们指向正确的地址。

5.运行

一端代码,在经过上面的步骤之后,就成了一个程序,但也仅仅只是程序,它想要成为一个进程,还需要将其加载到计算机内存中运行起来。

一个程序想要运行需要经过以下步骤:

1)创建虚拟地址空间和物理内存的映射(PCB映射的结构体)。

2)加载指令和数据。将程序所需要的指令和数据加载到内存中。

3)将程序的第一条指令写入PC寄存器中。PC寄存器是下一条指令寄存器,CPU会从PC寄存器中提取指令来执行下一步操作。

 

 

 

 

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值