程序员自我修养之长篇连载

本文是本人读《程序员的自我修养》一书,夹杂本人理解与感受写成的长篇连载,主要以本书节奏为主线,不保证一定与该书章节前后顺序一致。不定期更新...

  • 开篇

开篇打算从一个C 语言版本的hello world程序入手。

#include 
    
    
     
     

int main()
{
    printf("hello world\r\n");
    return 0;
}

    
    

我假设你写这个程序花掉了两分半钟,你十分清楚每一行代码的行为。当你在VC里面按下F7后,它就被编译了一个a.exe的文件,你再按F5,就像发生了神奇的魔法,在一个弹出来的黑框框里面出现了hello world,就像有一股你看不见的力量,在你不知道的地方向你说话,“hello world”,多么亲切!但你还是挺费解的,按下F7发生了什么,按下F5又发生了什么?里里外外包含了众多的细节,你当然可以将这些细节托管给你的编程环境和运行环境,但若你想了解隐藏在背后的东西,一些实质的东西,这篇文章或者能提供一些帮助。这些细节包含:C语言,编译链接,执行环境。

这篇文章不打算花大量的篇幅讲解C语言,有太多优秀的书本,blog有详细的分析,此文只打算花下面一段话简要说说,就当是正式内容开始前的一个热身。说到C语言,相信大家都不会反驳,它是进入大学后第一个被指定学习的编程语言,由Dennis Ritchie发明,他和Ken ThompsonKen Thompson一起发明了UNIX。C语言是系统级语言,中低级语言。C语言发展到现在,最新的C标准是C99,即最新的ANSI C,是2000年采纳的,其规范早已确定并极少有完善。Antoine de Saint Expuery说过:“无一分可增不叫完美,无一分可减才完美”,它只定义了24个头文件和为数不多的类型,操作符,设计思路产生了深远的影响,说起KISS原则,想必UNIX世界和linux世界的程序员无不知晓!Keep it Simple, stupid.学好C语言好处众多,最好举例的就是C系语言太通俗了,C++,java,php都在各自领域发挥重大作用,C语言还是深入理解UNIX/LINUX的必经之路。

好啦,如果你不精通C语言,也不要紧,本文不会牵扯到C的过多细节,如果存在,我也会想办法解释清楚的。

根据紧凑原则,你会希望从下面的章节看到。。。呃,编译链接和执行环境。嗯,是的,让我们从编译和链接开始!

  • 编译和链接

编译和链接的概念你可能清楚,但我还是想花一段通俗的话来解释一下,加深一下你的印象。
编译过程就是把源码文件转化为汇编代码文件。
汇编过程就是把汇编代码转化为机器指令文件,也就是目标文件。
链接过程就是把多个目标文件综合起来,生成一个可执行文件。
但这些只是概念,并没有说清楚到底做了哪些工作,也就是软件工程里面的需求的概念。我发现有些项目经理或者老板经常把这两个概念搞混,这里插入一则笑话:
经理:“我们要建立一套完整的课程内容管理系统,包括视频,音频,图片,文字,并且能够随时随地地进行浏览。另外,我们还会将这些内容打包出售给某些机构,从机构直接收取费用。将来,我们还会考虑手机等载体上能够播放这些课程。”
需求分析员:“我已经明白了这个项目的大体结构框架,但在开始实施之前,我们必须收集一些需求。了解清楚后,我才可以发现哪些是能够实现的,哪些是需要开发的,这样可以节省很多时间。”
经理感到纳闷:“我刚才不是已经给你描述需求了吗?直接把你们的系统给我介绍一下吧?”
需求分析员:“实际上,您只说明了整个项目的概念和目标,这些业务需求还不足以为开发提供支撑。我需要与实际业务人员进行讨论,然后才能真正明白达到业务目标所需要功能和用户要求。”
经理:“业务人员都很忙,没有时间与你们详细讨论各种细节。让我来告诉你我们的需求,不就是一个内容管理系统吗?实际上我告诉你,我也很忙。马上开发吧,如果有问题,我们再慢慢调整。”
没有黑的意思,这则笑话只是说,不同层次的人看到的东西是不一样的,领导就是要抽象,是么?
不过,这里我恐怕有岐义,我着重说明一下,领导要抽象,是说,领导要具有抽象的能力,要看到更抽象的东西!所以, 要具有抽象的能力!没有具体的实力,抽象的能力从何而来?天下大事,必作于细!可见,具体的实力是很重要的!
把焦点拉回来,我们要搞清楚一些细节,那么,编译到链接这些过程具体要怎么做呢?

1.编译

编译是一个翻译的过程。把编译程序看成一个翻译官,它把你用编程语言说的话,转化成机器能听懂的话。这个过程中,翻译官做的事,大致就等于编译程序做的事。他先要听清楚你说的每一句话,对你说的话进行整理,去掉噪声,开场白等等。他还要判断出一句话里面的所有词语,知道它们表达的意思。最后一步,他在懂了你说的话的意思后,他还要复述给机器听,他可能会把你要表达的意思经过精简,优化后复述出来。
这一系列的处理其实正是编译原理里面的:预处理,词法分析,语法分析,语义分析,优化。
你可能会说,人为什么不直接说机器能听懂的话,这样不是更简单吗?是的,处理的流程变短了,但人的工作量太大了。早期计算机编程就是采用这种方式进行的,使用机器编码,直接运行,执行效率高,且冗余的代码几乎没有,节省内存。但使用机器编码是一件很辛苦的事。随着计算机内存,主存越来越大,CPU主频越来越高,且它们的价格越来越低,又出现了多核架构,使高性能的计算机得到了很好的流行,人们意识到了,在软件过程中,最宝贵的是人的时间,牺牲人的时间去提升编译时间,执行时间越来越不值得。于是编程语言就越来越高级化,抽象化。就出现了类自然语言的编程语言。我们都知道,“任何问题都可以通过增加一个中间件得到解决”,这个中间件在此处就是编译器了,它负责翻译更贴近人的自然语言,转化为机器语言。
词法分析是将源代码进行扫描,运用有限状态机算法将它分割成一系列的记号。如lex。
语法分析以词法分析的输出为输入,将它们进行上下文无关语法分析,得到一棵语法树。语法树是以表达式为节点的树,非节点处就是运行符或操作符。如 yacc。
语义分析是对语法树进行静态语义分析,即,一个表达式是否是符合某个标准。这些标准即是编程语言规定的语法规则。与静态语义对应的是动态语义,也即运行期语义,一个表达式运行期的语义即它说明的真实意图。我认为,一个程序要表达的意义就是其动态语义的集合。
代码优化是一个策略问题,如变量个数优化(用不到的变量不定义),常量替换(相同的表达式只计算一次,用常量替换所有表达式),等等。
编译完成后输出的就是目标文件集合。目标文件是怎样的呢?
在windows下目标文件是.obj文件,在linux下目标文件是.o文件,它和可执行文件的文件格式很相似。windows可执行文件格式为PE-COFF文件格式,linux下为 ELF文件,UNIX下早期的a.out格式,MS-DOS下为.com格式。使用这些格式的不仅仅有可执行文件和目标文件,还有动态链接库文件(windows的.dll和linux的.so),静态链接库文件(windows的.lib和linux的.a),核心转储文件等。
目标文件格式的发展历史几乎就是操作系统的发展历史,COFF由Unix System V引入的,PE和ELF都从它发展而来,所以这两者很相似。
目标文件主要含有:数据,指令,符号表,调试信息,字符串等资源,以Section为单位存储。如.data是数据段,.text是代码段,.bss是一个数据段用来存放未初始化全局变量和局部静态变量。之前的一篇文章const static extern 存储与进程空间布局已经详细分析过了。
本质上,目标文件包含的内容与编译器有关,它可以在里面放任何的东西,因此研究到底有哪些东西并试图记住它们是没有意义的,只有我们遇到了某个问题与它有关时,我们再去研究,这里才显得有意义。所以此处不详细分析目标文件中存放的东西。我们只需知道,目标文件里面存放的东西会用作下一步的链接即可。

2.静态链接

有下面代码:
/* a.c */
extern int shared;
int main()
{
    int a = 100;
    swap(&a,&shared);
}

/* b.c */
int shared = 1;
void swap(int *a,int *b)
{
    *a ^= *b ^= *a ^= *b;
}
分别编译但不链接这两个C文件,得到a.o,b.o这两个目标文件。从源码上看,a.c有这样一个表达式 &shared,表示对shared 取地址,但shared这个变量都不是在a.c中定义的,extern int shared说明这个变量在别的文件中定义,它就是在b.c文件中定义的,所以如果要得到正确的执行指令,a.o必须要用到b.o中的shared信息。这就是链接,更进一步的,是静态链接。等到讲动态链接时,我们再回头对比。

假设你有一定的基础,听说过,链接其实就是以目标文件集合为输入,将它们加工,合并生成一个输出文件,即可执行文件。
那么,静态链接到底是怎么完成的呢?
有一种最简单的实现方法,将这些目标文件按序叠加。但这样做会存在问题:输出文件中会存在很多很多段(4个目标文件叠加就存在4个.bss段.data段...),由于段的内存对齐的缘故,会浪费较大空间。
既然这样,可以选择这样的策略,将相同的段合并为一个段。采用如下的两步链接法:
1.输出文件空间与地址分配。扫描所有输入的目标文件,得到各段长度,属性与位置,合并为一个合适大小的输出文件,各段位置映射。
2.符号解析与重定位,读取输入目标文件,查找符号表,调整需要地址重定位的点(全局变量,函数)。
符号解析与重定位是链接的核心,下面重点介绍。

第一步完成后,各段在输出文件中的各段虚拟内存地址就可以确定。这是因为,所有输入目标文件中的各段大小都是确定的,那么将它合并后,大小必然也是确定的,且各段相对于合并生成的文件头的地址是确定的,更进一步的,考虑虚拟内存地址。虚拟内存地址是指,输出文件(也即最终程序文件)在执行时,操作系统为会它生成一个进程,并把它载入到进程中去执行,那么,输出文件必然要指定怎么执行,去哪个地址取数据,取指令,这时候的地址指的是进程中的虚拟内存地址。也就是说,输出文件中指示的符号的地址实际上就是它将来执行时用到的虚拟内存地址。那么,怎么为这些符号指定地址呢。这有一套规则,首个被指定的虚拟内存地址是确定的,在linux下,ELF可执行文件默认从0x08048000开始分配地址。那么,在输出文件里面,就会将ELF各段依次分配从0x08048000起始处往后分配内存空间。一个可能的情况是,上面程序将a.o,b.o链接后,输出文件中的.text段分配的虚拟内存地址是从0x08048094开始的0x72个字节,.data段从0x8049108处开始的4字节。各个段里面的符号地址也确定下来了,因为它们相对于段的地址是明确的。总之,初步链接后,一些定义明确的符号能够得到正确的虚拟内存地址。如,main这个符号在输出文件中分配的虚拟内存是 0x08048000 + 0,0说明main符号相对于.text的偏移为0。
a.c中的shared和swap两个符号尽管没有明确定义,但它们在其它模块中,地址可以确定下来。按理说,将各个目标文件按段合并时,只需要将a.o中的shared和swap两个符号映射到的最终文件中的位置处标志为需要重定位,然后以各自的虚拟内存地址替换不就行了吗?是的,对于变量可以直接替换虚拟地址,但对于函数,还有一些额外的事情要做。我们看一下,调用一个函数使用call 指令。那call 指令是一条近址相对位移调用指令,call后面的地址是被调用指令相对于call后面一条指令的位置的偏移量(设 call 后面一条指令的地址是 x,偏移量为 n,则 call 后面的那个值为 x + n)。如:
80480ba: e8 0x09000000
80480bf: 83 c424
第一句中的 e8 表示call,整句表示:调用的指令相对于"83 c424"指令的偏移量为9(0x09000000是小端数字,真实数据为9).即,call 80480c8。
那么,假设a.o文件中的机器码是这样的:
26: e8fc ff ff ff
2b: 83c4 24
编译器会将输出文件中fc ff ff ff 这四个字节标注为需要修改的地方(在目标文件的符号表中标志此处为末决议),它就是指定的swap函数的地址相对于83 c4 24 的偏移量,由于swap函数的地址在a.o中并不正确,因此合并后仍然不正确。链接过程后期,编译器解析全局符号表后,得知swap这个符号的确切虚拟内存地址,那么,编译器就会利用已知的swap地址,去更改fc ff ff ff这个不正确的数字。怎样修改呢?很简单,假设地址 e8 fc ff ff ff 处后面的一条指令的地址为x,而swap的虚拟内存地址是 0x08048010(也即 call 真实调用的指令的地址) ,则这里的fc ff ff ff应该改为0x08048010 - x;
实际上,可以抽取出一个模型,得到计算公式:
A=保存在被修正位置的值(不靠谱的值)
P=被修正的位置
S=符号的实际虚拟内存地址
R=修改后的值
则:R = S+A-P。

因为 A 是一个错误的值,所以这里填什么都不要紧,编译器为了纠正的方便,直接将 A 在32 位机器上被填充为 -4,在 64 位机器上被填充为 -8.这样,就刚好符合上面那个公式了。

所以,R + P - A = S;这个正好是“近址相对位移调用指令”的定义。


对于变量还存在一个COMMON块的问题,为了把问题说明清楚,写出以下代码。
/* x.c */
int global = 0x00000111;//256 + 16 + 1


/* y.c */
#include 
     
     
      
      
char global;
int main()
{
    printf("value = %d,size = %d \r\n",(int)global,sizeof(global));
}

     
     
在VC++2012下面输出结果是 value = 17, size = 1。
在y.c中global并没有明确定义,它是一个弱符号。在x.c中global是定义并给了初始值,是明确定义,是一个强符号。
在链接时,若一个全局符号在某些文件中被明确定义而在另一些文件中被不明确定义,则会触发一个COMMON块机制:当不同文件对某个符号需要的空间不一致时,以最大的空间为主。


以上讲了一些链接的原理,说了这么多,到底什么是静态链接呢?实际上,静态链接中的“静态”相对于动态而言,指的是在运行之前全部链接完毕。假设a.o用到了b.o里面的某一个符号,或者某一个函数,根据上面讲的两步链接法,静态链接器就需要将a.o与b.o的所有段合并,以此将a.o里面末明确的符号确定下来。可知,静态链接是以目标文件为单位的。

linux下的lib.a静态库文件是由ar压缩程序将成百上千个目标文件压缩组成的,这些目标文件包括:fread.o,fwrite.o,date.o,time.o,printf.o,scanf.o等等,之所以一个目标文件只有一个函数,是因为静态链接以目标文件为单位进行链接,若将多个函数同时组织在一个目标文件中,则在静态链接时,除了会将有用的函数链接进来也会将没有用的函数链接进来,这样会增大最终输出文件的尺寸。

由于,我们就可以总结静态链接的优点与缺点了。优点是,输出文件包含所有的输入文件的指令,数据,它可以独立于被链接的文件执行。缺点是,输出文件一般会比较大,因为它可以被链接了无用的指令,数据。另外,一旦输出文件依赖的目标文件发生变化,还得重新进行链接,对于程序的发布,流传造成很大的不便。另一个缺点是,想想看,假设有一个lib.o是一个系统级的标准的静态链接库,在操作系统上运行N个程序都需要链接到它,则每个程序都会包含一个lib.o,那么,在内存中,lib.o就会存在N份,这对于内存是极大的浪费。

既然静态链接有这么多的缺点,那有没有好的解决方案呢?有,那就是动态链接。



3.动态链接

动态链接是相对于静态链接而言的:静态链接在运行前进行链接,动态链接是在运行时进行链接。

运行时链接意味着有更好的程序可扩展性和兼容性,因为某个动态链接库发生变化,不会导致重新链接,只需要将其替换即可。另一方面我们还希望动态链接库在节省内存上面有一番作为。windows下的动态链接库是dll(dynamic link library),linux下是so(share object),从so的取名来看,它带有共享的意思。如果我们能让动态链接库在多个程序中共享,那也就节省了内存。

那么动态链接如何实现呢?

首先,运行时链接,生成可执行文件时不链接。

再次,可执行文件运行时动态链接库在内存中只会存在一份,多个程序之间共享。

声明一下,在静态链接中的重定位叫链接时重定位;下文中可能也会出现多次“链接重定位”,是指在动态链接中对于末决议符号进行虚拟内存地址绑定,希望大家不要误解。

动态链接库和静态链接库的文件结构并没有什么不同,它们的不同只是在于链接的时期不同。链接的方式都是由链接器发现依赖关系,将所需要的目标文件依次载入内存,进行符号虚拟地址重定位。动态链接由动态链接器完成,程序每次运行都需要重新进行链接,性能上有一点损失,由于动态链接过程可以进行优化,如延迟绑定的技术,可以使损失尽可能小,和静态链接相比,性能损失在5%以下,这点性能损失用来换取程序在空间上的节省和程序可扩展与兼容性来说是微不足道的。

再者,动态链接库中有一个符号表,它表示了符号信息,数据结构可以考察ELF文件结构定义中的符号表数据结构。所以,进行动态链接时,链接器查看到程序中存在末决议的符号,就会试图去动态链接库中链接。

考虑一下动态链接过程中进程的内存分布。首先,程序本身是首先要被映射到进程空间中的,它所依赖的共享库也要被映射进来,最后 ,动态链接器也要被链接进来,动态链接器实际上也是一个动态链接库,在linux下为ld-2.6.so。那么,这些动态链接库被载入进程的内存空间的地址如何决议?显然,由于一个程序可能需要载入多个共享库,所以载入的地址不能是一个固定地址,它是系统根据进程有效地址依次载入的。下面谈谈动态链接过程中如何解决重定位问题。

最容易想到的一个方法是:各个库文件被载入到进程空间后,地址都是确定的,而又由于各个库文件中明确定义的符号的地址相对于库文件首地址来说都是确定的,即,一旦库文件在内存中的地址确定了,那么明确定义的符号的地址也就确定了,那么只需要遍历全局符号表,将所有符号地址重定位到某个确定的值即可。此法为“装载时重定位”。

你看出这有一个很大的问题了吗?首先,重定位需要更改共享库的指令。再者,前面谈到了,库文件载入进程中的内存地址是不确定的,则各进程会对共享库的内存空间进行“只对自己适用的更改”,那么一旦如此更改,其它的进程就使用不了共享库了!所以此方法是行不通的!除非你要求共享库对于每个进程都有一个副本,或者共享库中不共享的数据部分(每个进程都会复制一份来自共享库数据的副本)也可能采用这种重定位方法!

我们再考虑另一种方法,地址无关代码(PIC, Position-independent Code),它可以解决动态链接过程重定位问题。地址无关代码的中心思想是将要更改的指令放入数据段里面。因为共享的数据段对于进程都有一份,数据不共享,指令共享。那么,各个进程对自己的数据段进行修改,将代码更改为适应自己的版本,这也叫做地址无关代码。字面上的意思是:“对于多个进程都会用到的共享库,不同的进程将共享库载入到不同的地址,其指令对于每个进程都是有效的,与共享库在进程的地址是无关的”。那我们再思考一下此方案能不能实现。先考虑下列四种需求:1.模块内部数据访问;2.模块内部函数调用;3.模块之间数据访问;4.模块之间函数调用;对应地代码:

static int a;
extern int a;
extern void ext();

void bar()
{
    a = 1;  //模块内部数据访问
    b = 2;  //模块之间数据访问
}

void foo()
{
    bar();  //模块内部函数调用
    ext();  //模块之间函数调用
}

对于1.由于同一个模块内明确定义的符号,它们的相对地址都是确定的,进一步的,在执行期,以页的形式将指令和数据载入内存,页之间的相对地址确定,那么,当前执行的指令与任何一个符号的相对地址都是确定的,只需要用当前指令的地址加上固定的偏移量就可以了。当前指令的地址很容易就可以获得,如利用call指令的方法。因为执行call指令后,下一条指令的地址会被压到栈顶,而esp寄存器始终指向栈顶,只需要在调用call后立即再执行 mov (%esp),%ecx。等call返回时,ecx寄存器中就是当前指令的地址。

对于2.道理同上,同模块内的函数调用毫无压力。

对于3.模块之间数据访问。这也是比较容易做到的,因为共享库的数据区在进程中有一个副本,其中的符号总可以得到一个正确的地址,且各个共享库互不干扰的享有这些符号,虽然某个变量被跨模块访问,但这些模块总还在一个进程里面,它们的地址相互来说是有效的,可以毫无压力的访问。具体可以这样做,对每个模块,如果有末决议的数据符号,则生成一个全局偏移表GOT,这是一个数组,存放指向这些末决议符号的偏移。详细来说的话,若 a.so里面用到定义在其它模块里的变量符号:extern int global;它在b.so中被明确定义。那么,a.so文件中的.data段就会存在一个GOT,设第一项就是global.那么,在动态链接时,链接器发现global定义在b.so中并通过将b.so被载入的内存地址加上它在b.so中的偏移量得到真实的虚拟内存地址x,并再使用1中的方法,通过当前指令的地址找到GOT的地址,将它的第一项的值填为x,它即是真实的global地址,到此,就解决了模块之间的数据访问。

实现模块间数据访问看起来有这么多句话,其实思想极其简单,因为变量没定义在当前模块中,则在当前模块中肯定需要对此变量重定位!而由于共享库的共享作用,不可直接在指令中更改变量的地址,因为这样的话会导致其它进程访问不到正确的数据!那么,只能在模块中不被共享的区域存放变量的地址,并在执行时按这样的通用规则获得变量地址:让所有进程获得(它自己的)GOT,再获得变量地址,这样所有的进程都可以正确寻址,也就实现了模块间数据访问。这其实应证了一句名言:“一切的问题都可以通过增加一个中间件解决”。

对于4.模块之间函数调用。毫无压力,只需要将3中的数据地址换成函数地址即可。

还有一个微妙的问题需要考虑:共享模块的全局变量问题。在主模块(编译生成可执行文件的模块)里面有如下代码:

extern int global;

int foo()

{

global = 1;

}

编译器就搞不清楚了,这个global是定义在主模块的其它文件里面,还是定义在其它模块里面呢?如果定义在主模块里面,直接就能重定位其地址。但如果在其它模块里面,则必须等到链接时才能重定位地址。但是,主模块不是共享库,不会存在GOT这个“中间件”,那么,该如何解决重定位问题呢?此时,就有另一个替代的中间件了,bss 段,熟悉吧,它本来是指末初始化的全局变量,静态变量存放的段。为了让主模块编译(静态)链接通过,解决方法是在bss段中定义一个global变量,让主模块链接到此global变量。接下来,在运行期,动态链接时,还要解决共享库中的global与bss段中的global重定义问题,只需要让共享库中的global指向bss中的global同时把共享库中的初值搬运过来。如果主模块中没有引用global,只在共享库中引用,那么就退化为模块之间的数据访问,直接使用地址无关代码解决。

既然说到了动态链接,就不得不说一下延迟绑定。延迟绑定的出现是为了解决动态链接相比静态链接效率较低的问题,因为一个动态链接库被程序载入时,需要确定末决议的符号,每次启动那个程序时都需要重复那些动作,程序启动速度比较慢,效率相对静态链接来说比较低。延迟绑定就是为了解决这个问题,其核心思想是:只要必要的时候进行链接必要的符号,一开始并不链接所有符号。必要指的是“运行时期用到的符号才有必要链接”。

“延迟绑定”由用户决定什么时候绑定某符号(一般是函数符号)的,也即显示绑定。一般会通过调用一个系统库函数完成这个操作,如win32 API里面就是 LoadLibrary,原型为:HMODULE WINAPI LoadLibrary( _In_ LPCTSTR l pFileName),它接受一个路径名以表示动态链接库,返回一个动态链接库句柄。此函数将动态链接库映射到当前的进程空间中。如果需要决议某一个函数地址,则需要使用win32 API的GetProcAddress,原型为:FARPROC GetProcAddress( HMODULE hModule,LPCSTR lpProcName),接受一个动态链接库句柄和一个字符串以表示函数名,返回的是函数指针。

那么显示链接是如何实现的呢?我们来回忆一下,如果是单纯的动态链接,则是通过各共享库中的GOT实现的。那么,显示链接则是通过在GOT与动态链接器之间加上一个中间层PLT,它实现地址跳转。每个外部函数都在PLT中有一个相应的项,到真正显示加载某个函数时,才去决议该函数符号的地址,并将其填充到PLT相应的表项中,以后使用该函数时直接从PLT中取出。

设在模块A中使用模块B中的bar函数,使用显示链接,则编译器对此实现的代码是:

jmp *(X)

push n

push moduleID

jump _dl_runtime_resolve

末执行显示链接前,X为push n这条指令的地址,相当于跳过该行代码向后面执行,push n,push moduleID,n是重定位表中的索引,以指示某需要重定位的符号,moduleID是共享库惟一标志,如句柄,地址等待,这两句将这两个值压入栈当成resolve的参数,进行函数地址决议,决议的过程大概是:1.依动态链接步骤,将此符号载入到进程空间(如果还没有载入到进程空间的话)2.将上一步得到的函数地址填充到当前模块的GOT表中,如果没有GOT表,则说明是主模块,则在BBS段中生成一个全局符号将其赋值为该地址并让所有相同的符号指向它。完成这些后,X会被填充为bar@GOT,它表示bar在GOT中的表项地址,有了GOT表项中的地址,则可以正确决议出函数的地址了。可以看出,首次显示链接后,之后再链接该函数就可以正确跳转到函数地址了。

运行

1.装载


2.动态链接


3.再谈库


4.内存管理


5.系统调用


6.如何实现一个库




...不定期更新


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值