静态链接之编译和链接

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

一、编译的过程

首先还是经典的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 4
extern 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" 2
int 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.o
gcc命令从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 + 6
t2 = index + 4
t3 = t2 * t1
array[index]=t3

上述代码经过优化后:


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

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

5.目标代码生成与优化

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

movl index,%ecx ; value of index to ecx
addl $4,%ecx
mull $8,%ecx
movl index, %eax
movl %ecx,array(,eax,4)

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

movl index,%ecx ; value of index to ecx
leal 32(,%edx,8),%eax
movl %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
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值