静态链接之编译和链接

来源:互联网 发布:亚投行招聘 知乎 编辑:程序博客网 时间:2024/05/18 22:54

本文是《程序员的自我修养》写的记录笔记,来加深对程序程序生成的理解。

一、编译的过程

首先还是经典的helloworld c语言程序作为例子

#include<stdio.h>int main(){    printf("Hello World\n");    return 0;}

在linux下,我们用GCC编译该程序并运行:
这里写图片描述

事实上,上述过程可以分解成4个步骤,分别是预处理(Prepressing)编译(Compilation)汇编(Assembly)链接(Linking) 如图:

1.预编译

源代码文件hello.c和相关的头文件被预编译器cpp预编译成一个 .i 文件。 第一步预编译的过程相当于如下命令(-E 表示只进行预编译)

$ gcc -E hello.c -o hello.i

或者

$cpp hello.c > hello.i

编译生成才hello.i 的文件内容

 ....extern int pclose (FILE *__stream);extern char *ctermid (char *__s) __attribute__ ((__nothrow__ , __leaf__));# 913 "/usr/include/stdio.h" 3 4extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));# 943 "/usr/include/stdio.h" 3 4# 2 "hello.c" 2int main(){    printf("Hello World\n");    return 0;}

预编译过程主要处理那些源代码文件中的以“#”开始的预编译指令。比如“#include”、“#define”等,主要处理规则如下:

  • 将所有的“define”删除,并且展开所有的宏定义
  • 处理所有条件预编译指令,比如“#if”、”#ifdef”等。
  • 处理”#include“预编译指令,将被包含的文件插入到该预编译指令的位置。注意这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。
  • 添加行号和文件名标识,比如# 2 “helo.c”,义便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。
  • 删除所有的注释”//”和”/**/”。
  • 保留所有的#pragma编译器指令,因为编译器需要使用它们。

    -经过预编译后的 .i 文件不包含任何宏定义,因为所有的宏已经被展开,并且包含才文件也被插入到.i 文件中。所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。

2.编译

编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生产相应才汇编代码文件。编译过程相当于如下命令:

$gcc -S hello.i -o hello.s

由.c文件直接生成相应才.s文件,可以使用下面的命令:

$gcc -S hello.c  -o hello.s

输出的内容:

对于C语言的代码来说,这个预编译和编译的程序是ccl,对于C++来说,有对应才程序叫做cclplus,Object-C是cclobj,fortran是f771,Java是jcl。
所以实际上gcc这个命令只是这些后台程序的包装,它会根据不同的参数要求去调用预编译程序cc1、汇编器as、链接器ld。

3.汇编

汇编是将汇编代码变成机器可以执行的指令,每一条汇编语句几乎对应一条机器指令。汇编器只需要根据汇编指令和机器指令的对照表一一翻译就可以。

$as hello.s -o hello.o

或者

$gcc -c hello.s  -o hello.ogcc命令从C源代码文件开始,经过预编译、编译和汇编直接输出目标文件(Object File)
$gcc -c hello.c -o hello.o

链接

如果把所有的路径都省略,那么命令就是:

ld -static crt1.o crti.o crtbeginT.o  hello.o -start -group -lgcc -lgcc_eh -lc -end-group crtend.o crtn.o

二、编译器工作

编译过程一般可以分为6步:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化。整个过程如下:

我们将结合上图来简单描述从源代码(Source Code)最终目标代码(Final Target)的过程。我们将以一段很简单的C语言的代码为例子来讲述这个过程。

array[index] = (index + 4) * (2 + 6)

1.词法分析

首先源代码程序被输入到扫描器(Scanner),扫描器的任务很简单,它只是简单地进行词法分析,运用一种类似于有限状态机(Finite State Machine)的算法可以很轻松地将源代码的字符序列分割成一系列的记号(Token)。比如上面的那行程序,总共包含了28个非空字符,经过扫描以后,产生了16个记号。

记号 类型 array 标识符 [ 左方括号 index 标识符 ] 右方括号 = 赋值 ( index 标识符 + 4 ) * ( 2 + 6 数字 )

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

2.语法分析

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

符号和数字是最小的表达式,它们不是由其他的表达式来组成的,所以它们通常作为整个语法树的叶节点。在进行语法分析的同时,很多运算符号的优先级和含义也被确定下来。

3.语义分析

语义分析由语义分析器(Semantic Analyzer)来完成。语法分析仅仅是完成了对表达式的语法层面的分析,但是它并不了解这个语句是否真正有意义。编译器所能分析的语义是静态语义(Static Semantic),所谓静态语义是指在编译期可以确定的语义,与之对应的动态语义(Dynamic Semantic)就是只有在运行期才能确定的语义。

静态语义通常包括声明和类型的匹配,类型的转换。动态语义一般指在运行期出现的语义相关的问题,比如将0作为除数是一个运行期语义错误。
经过语义分析阶段以后,整个语法表达式都被标识了类型,如果有些类型需要做隐式转换,语义分析程序会在语法树中插入相应的转换节点。

4. 中间语言的生成

编译器有着很多层次的优化,往往在源代码级别会有一个优化过程。我们这里所描述的源码级优化器(Source Code Optimizer)在不同编译器中可能会有不同的定义和有一些其他的差异。源代码级优化器会在原代码级别进行优化。例如(2+6)的值在编译期就可以被确定,因此可以被优化掉。直接在语法树上作优化比较困难,所以源代码优化器往往将整个语法树转换成中间代码(Intermedaite Code),它是语法树的顺序表示,非常接近目标代码,但与目标机器和运行时环境是无关的,比如它不包含数据才尺寸、变量地址和寄存器的名称等。中间代码有很多类型,不同的编译器中有着不同的形式,比较常见的有:三地址码(Three-address Code)P-代码(P-Code)。 最基本的三地址码是这样:

x = y op z

上述的语法树经过优化以后的代码如下:

t1 = 2 + 6t2 = index + 4t3 = t2 * t1array[index]=t3

上述代码经过优化后:

t2 = index + 4t2= t2 * 8array[index]=t2

中间代码使得编译器可以分为前端和后端。编译器前端负责产生机器无关的中间代码,编译器后端将中间代码转换成目标机器代码。

5.目标代码生成与优化

源代码级优化器产生中间代码标志着下面的过程都都属于编译器后端。编译器后端主要包括代码生成器(Code Generator)目标代码优化器(Target Code Optimizer)。代码生成器将中间代码转换成目标机器代码,这个过程十分依赖于目标机器,因为不同机器有着不同的字长、寄存器、整数数据类型和浮点数数据类型等。(我们用x86的汇编语言来表示,并且假设index的类型为int型,array的类型为int型数组):

movl index,%ecx ; value of index to ecxaddl $4,%ecxmull $8,%ecxmovl index, %eaxmovl %ecx,array(,eax,4)

上面的例子中,乘法由一条相对复杂的基址比例变址寻址(Base Index Scale Addressing)的lea指令完成,随后由一条mov指令完成最后的赋值操作,这条mov指令的寻址指令与lea是一样的。

movl index,%ecx ; value of index to ecxleal 32(,%edx,8),%eaxmovl %eax,array(,%edx,4)

经过这些扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化,编译器忙活了这么多个步骤以后,源代码终于被编译成了目标代码。但是这个目标代码中有一个问题是:index和array的地址还没有确定。如果index和array定义在跟上面的源代码同一个编译单元里面,那么编译器可以为index和array分配空间,确定它们的地址。事实上,目标代码中有变量定义在其他模块,定义其他模块的全局变量和函数在最终运行时的绝对地址都要在最终链接时候才能确定。所以现代的编译器可以将一个源代码文件编译成一个未链接的目标文件,然后由链接器最终将这些目标文件链接起来形成可执行文件。

三、链接器的年龄比编译器的年龄长

重新计算各个目标的地址过程被叫做重定位(Relocation).
符号(Symbol)这个概念随着汇编语言的普及迅速被使用,它用来表示一个地址,这个地址可能是一段子层序(后来发展成函数)的起始地址。

一个程序被分割成多个模块,静态语言的C/C++模块之间通信有两种方式,一种是模块间的函数调用,另一种是模块间的变量访问。两者都需要知道访问对象的地址,因此可以归结为一种方式,那就是模块间符号的引用。这个拼接过程就是本书的一个主题:链接(Linking)

四、 模块拼接–静态链接

组装模块的过程就是链接(Linking)。 链接过程主要包括了地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution) 和 重定位(Relocation) 等这些步骤。每个模块的源代码文件(如.c)文件经过编译器编译成目标文件(ObjectFile,一般扩展名为 .o或 .obj),目标文件和库(Library)一起链接最终可执行文件。而最常见的库就是运行时库(Runtime Library)

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 应用安装在sd卡打不开怎么办 安装ps打不开安装包怎么办 安装好的软件打不开怎么办? win10系统语言修改不了怎么办 一个月婴儿吵夜怎么办 玩游戏一直闪退怎么办 钱站一直闪退怎么办 win7重装连不上网怎么办 笔记本屏幕横过来了怎么办 3D贴图丢了怎么办 百度文库安装后手机打不开怎么办 win7系统不带usb驱动怎么办 手机网页上的pdf打不开怎么办 网页下载pdf后缀是.do怎么办 ps界面太小怎么办win10 ps软件打不开程序错误怎么办 ps打开后 未响应怎么办 ps图层无法解锁怎么办 ie8浏览器电脑不能用怎么办 系统要ie6.0才能打开怎么办 2g手机内存不够怎么办 2g运行内存不够怎么办 手机运行内存2g不够怎么办 手机无法加载程序秒退怎么办 电脑账户密码忘记了怎么办 玩绝地求生卡顿怎么办 地下城总运行时间错误怎么办 逆战更新太慢怎么办 win7我的电脑没了怎么办 剑灵启动游戏慢怎么办 网页页面结束进程也关不掉怎么办 开机就启动微信怎么办 微信突然无法启动怎么办 微信发送太频繁怎么办 微信在电脑上打不开文件怎么办 微信照片电脑上打不开怎么办 换一部手机微信怎么办 微信支付宝停止运行怎么办 剑三重制版卡顿怎么办 剑三客户端更新不动了怎么办 安装包安装失败怎么办有内存