《程序员的自我修养——链接、装载与库》——链接

导读

  对于平常的应用程序开发,我们很少需要关注编译和链接过程,因为通常的开发环境都是流行的集成开发环境(IDE),比如Visual Studio、Myeclipse等。这样的IDE一般都将编译和链接的过程一步完成,通常将这种编译和链接合并在一起的过程称为构建,即使使用命令行来编译一个源代码文件,简单的一句”gcc hello.c”命令就包含了非常复杂的过程。然而,正是因为集成开发环境的强大,很多系统软件的运行机制与机理被掩盖,其程序的很多莫名其妙的错误让我们无所适从,面对程序运行时种种性能瓶颈我们束手无策。我们看到的是这些问题的现象,但是却很难看清本质,所有这些问题的本质就是软件运行背后的机理及支撑软件运行的各种平台和工具,如果能深入了解这些机制,那么解决这些问题就能够游刃有余,收放自如了。

  现在我们通过一个C语言的经典例子,来具体了解一下这些机制:

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

在linux下只需要一个简单的命令(假设源代码文件名为hello.c):

$ gcc hello.c
$ ./a.out
Hello World

  事实上,上述gcc过程可分解为4个步骤:预处理、编译、汇编、链接,如图所示:

目录

  • 预处理(Prepressing)
  • 编译(Compliation)
  • 汇编(Assembly)
  • 链接(Linking)

正文

1、  预编译(prepressing)

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

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

或者,

$ cpp hello.c > hello.i

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

  • 将所有的”#define”删除,并展开所有的宏定义
  • 处理所有条件预编译指令,比如”#if”,”#ifdef”,”#elif” ”,#else”,”#endif”
  • 处理”#include”预编译指令,将被包含的文件插入到该预编译指令的位置。
  • 删除所有的注释“//”和“/**/”
  • 添加行号和文件名标识,比如#2 “hello.c” 2。
  • 保留所有的#pragma编译器指令

2、  编译(compliation)

  编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件,此过程是整个程序构建的核心部分,也是最复杂的部分之一。其编译过程相当于如下命令:

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

Gcc是好多后台程序的包装,它会根据不同的参数要求去调用预编译程序cc1、汇编器as、链接器ld。

3、  汇编(assembly)

  汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎对应一条机器令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了。其汇编过程相当于如下命令:

as hello.s –o hello.o

或者,

gcc –c hello.s –o hello.o

或者使用gcc命令从C源代码文件开始,经过预编译、编译和汇编直接输出目标文件:

gcc –c hello.c –o hello.o

4、  链接(linking)

  链接通常是一个让人比较费解的过程,为什么汇编器不直接输出可执行文件而是输出一个目标文件呢?为什么要链接?下面让我们来看看怎么样调用ld才可以产生一个能够正常运行的Hello World程序:

$ ld –static /usr/lib/crt1.o /usr/lib/crti.o

/usr/lib/gcc/i486-linux-gnu/4.3.1/crtbeginT.o

-L/usr/lib/gcc/i486-linux-gnu/4.3.1 –L/usr/lib/ -L/lib hello.o –start-group

-lgcc –lgcc_eh –lc –end-group /usr/lib/gcc/i486-linux-gnu/4.3.1/crtend.o

/usr/lib/ctrn.o

如果把所有的路径都省略掉,那么上面的命令:

ld –static crti.o crtbeginT.o hello.o –start-group -lgcc –lgcc_eh –lc –end-group crtend.o ctrn.o

可以看到,我们需要将一大堆文件链接起来才可以得到“a.out”,即最终的可执行文件。看到这么复杂的命令,你可能会问,这些.o文件是什么?它们做什么用的?–lgcc_eh –lc –end-group这些又是些什么参数?为什么要使用它们?为什么要将它们和hello.o链接起来才可能得到可执行文件?等等。

  现在我们一起在探索他们的条案吧。

  在现代软件开发过程中,软件的规模往往都很大,动辄数百万行代码,如果都放在一个模块肯定是无法想象的。所以现代的大型软件往往拥有成千上万个模块,这些模块之间相互依赖又相对独立。这种层次化及模块化存储和组织源代码有很多好处,比如代码更容易阅读、理解、重用,每个模块可以单独开发、编译、测试,改变部分代码不需要编译整个程序等。

  在一个程序被分割成多个模块以后,这些模块之间最后如何组合形成一个单一的程序是须解决的问题。模块之间如何组合的问题可以归结为模块之间如何通信的问题,最常见的属于静态语言的C/C++模拟之间通信有两种方式:

  • 模块间的函数调用
  • 模块间的变量访问

  函数访问须知道目标函数的地址,变量访问也须知道目标变量的地址,所以,这两种方式都可以归结为一种方式,那就是模块间符号的引用。模块间依靠符号来通信类似于拼图版,定义符号的模块多出一块区域,引用该符号的模块刚好少了那一块区域,两者一拼接刚好完美组合。这个模块的拼接过程就是链接。链接的主要内容就是把各个模块之间相互引用的部分都处理好,使得各个模块之间都能够正确的衔接,也就是把一些指令对其他符号地址的引用加以修正。链接过程主要包括了地址和空间分配(Address and storage allocation)、符号决议(symbol resolution)和重定位(relocation)等这些步骤。

  现在我们举例解释一下编译和链接的概念。比如我们在程序模块main.c中使用另外一个模块func.c中的函数foo()。我们在main.c模块中每一处调用foo()的时候都必须确切知道foo()这个函数的地址,但是由于每个模块都是单独编译的,在编译器编译main.c的时候,它并不知道foo()函数的地址,所以它暂时把这些调用foo()的指令的目标地址搁置,等待最后链接的时候由链接器去将这些指令的目标地址修正。如果没有链接器,须要我们手工把每个调用foo()的指令进行修正,则填入正确的foo()函数地址。当func.c模块被重新编译,foo()函数的地址有可能改变时,那么我们在main.c中所有使用到foo()的地址的指令将要全部重新调整,这些繁琐的工作将成为程序员的噩梦。使用链接器,我们可以直接引用其他模块的函数和全局变量而无须知道它们的地址,因为链接器在链接的时候,会根据你所引用的符号foo(),自动去相应的func.c模块查找foo()地址,然后将main.c模块中所有引用到foo()的指令重新修正,让它们的目标地址为真正的foo()的指令重新修正,让它们的目标地址为真正的foo()函数地址。这也就是静态链接的最基本的过程和作用。

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

mov1 $0x2a, var

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

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

时间: 2024-10-15 07:05:20

《程序员的自我修养——链接、装载与库》——链接的相关文章

读《程序员的自我修养 —— 装载与动态链接》乱摘

2016.05.14 – <程序员的自我修养 -- 链接.装载与库>的装载与动态链接部分. - 余甲子 石凡 潘爱民编 个人选读笔记 - 学点表皮. 05.14 PART II 装载与动态链接 1 可执行文件的装载与进程 1.1 进程虚拟地址空间的大小 每个进程拥有自己独立的虚拟地址空间,该虚拟地址空间的大小由计算机的硬件平台决定,具体地说是由CPU的位数决定的(地址线 -- C语言中的指针所占空间).硬件决定了地址空间的最大理论上限,即硬件的寻址空间大小,如32位的硬件平台决定了虚拟地址空间

程序员的自我修养—链接、装载与库pdf

下载地址:网盘下载 内容简介 编辑 <程序员的自我修养:链接.装载与库>对装载.链接和库进行了深入浅出的剖析,并且辅以大量的例子和图表,可以作为计算机软件专业和其他相关专业大学本科高年级学生深入学习系统软件的参考书.同时,还可作为各行业从事软件开发的工程师.研究人员以及其他对系统软件实现机制和技术感兴趣者的自学教材. 媒体评论 编辑 这是一本深人阐述链接.装载和库等问题的优秀图书,读来让人愉悦,你从巾可以清晰地了解程序的前世今生,彻底理解敲人的代码如何变成程序任系统中运行.通读本书不管对于开发

《程序员的自我修养》 第二章——编译和链接

摘自http://blog.chinaunix.net/uid-26548237-id-3839979.html <程序员的自我修养>第二章——编译和链接 2.1 被隐藏了的过程    C语句的经典,“Hello World”程序几乎是每个程序员闭着眼睛都能写出的,编译运行一气呵成,基本成了程序入门和开发环境测试的默认标准. #include <stdio.h> int main() { printf("Hello World\n"); return 0; 在L

程序员的自我修养:(1)目标文件

程序员的自我修养:(1)目标文件 1.目标文件 1.1 编译与链接 在使用像Visual Studio或Qt Creator等IDE时,通常有一个叫做"构建"的按钮.当编辑完成要运行和测试时点一下它,程序就能跑起来了,所以我们很少关心编译和链接.其实,编译和链接合并在一起就称为 构建(Build).简单的一次按键,实际背后却是异常复杂的过程: 预编译(Preprocessing) 编译(Compilation) 扫描:算法类似有限状态机(FSM),将字符转换成Token. 语法分析:分

读《程序员的自我修养》感受

这书不错,链接-装载-库 我觉得是很底层的东西.比如很多人闭着眼睛都能写出来的hello world(当然不包括brianfuck,如果你会,你真的闹残了吗= =), 其实链接编译器做了很多,不然就哪来的printf,这IO初始化也是CRT(c runtime)库完成的.堆栈的初始化,还有系统装载让程序运行等等.涉及很多. 书里后面就讲了一个CRT库,自己写一个,感觉不错,学了很多.比如malloc,free的实现,话说还是跨平台的.当然库很小,功能不多,不过写这个也可以学学算法.内存的分配,这

《程序员的自我修养》第三章学习笔记

1,  编译器编译源代码生成的文件叫做目标文件. 从结构上说,是编译后的可执行文件,只不过还没有经过链接 3.1 目标文件的格式 1,可执行文件的格式: Windows下的PE  和   Linux下的ELF 2,从广义上说,目标文件与可执行文件的格式几乎是一样的,所以广义上可以将目标文件与可执行文件看成是一种类型的文件. 3,可执行文件,动态链接库,静态链接库都按照可执行文件格式存储(Windows下是 PE-COFF格式,Linux下是ELF格式). 4,Linux下命令: $: file 

一、《程序员的自我修养》笔记-前言

引子:在linux上写了三年多的c了,平时遇到一些编译和链接的问题仍然很是头痛,感觉很无力,好基友推荐<程序员的自我修养>,趁着周末,速速围观. 先记录下作者在书中抛出来的问题 1.为啥程序是从main函数开始执行? 2.PE/ELF文件存的是啥? 3.如何写一个直接跑在未安装os裸机上的程序? 4.目标文件是啥?链接是啥? 5.链接为啥报错? 6.句柄到底是啥? 7.普通c/c++代码如何被编译成牧宝文件及程序在目标文件中如何存储? 8.目标文件如何被链接器链接到一起,并形成可执行文件? 9

程序员的自我修养

本书主要介绍系统软件的运行机制和原理,涉及在Windows和Linux两个系统平台上,一个应用程序在编译.链接和运行时刻所发生的各种事项,包括:代码指令是如何保存的,库文件如何与应用程序代码静态链接,应用程序如何被装载到内存中并开始运行,动态链接如何实现,C/C++运行库的工作原理,以及操作系统提供的系统服务是如何被调用的.每个技术专题都配备了大量图.表和代码实例,力求将复杂的机制以简洁的形式表达出来.本书最后还提供了一个小巧且跨平台的C/C++运行库MiniCRT,综合展示了与运行库相关的各种

读书笔记:程序员的自我修养-----第一章(综述)

题前:30--45天读完,一周至少3篇读书笔记.不能坚持,不再联系,不再找你. 一. hello world 程序引出的问题,看40天后,再回来看看自己的答案,提升多少. Q1:程序为什么要被编译器编译之后才可以运行?   A1 : 系统执行的机器语言,即二进制文件,程序是文本文件需要编译之后,由链接器链接需要的基本库生成二进制文件. Q2: 编译器在把C语言程序转换成可以执行的机器码的过程中作了什么,怎么做的?   A2: 预处理,汇编器生成汇编文件,编译器生成目标文件,链接器链接生成可执行文

程序员的自我修养笔记

1,为什么内存需要分段和分页机制? 早起的计算机中,程序都是直接运行在物理内存上的.这样做有几个问题: 1)地址空间不隔离,计算机的安全性和稳定性没有办法保证,由于所有的程序都可以访问物理内存,恶意的程序可以很容易修改其他程序的内容,达到破坏的目的. 2)内存使用效率低,当前执行的程序(列入进程A)必须被整个装载到内存中执行,如果需要执行另一个程序时,发现内存空间不足,则需要将进程A的数据整体换出到磁盘. 3)程序运行的地址不确定 为了解决上述的三个问题,引入了虚拟地址.分段和分页的概念. 有了