首页 > 代码库 > 程序地址重定位和模块绑定

程序地址重定位和模块绑定

1.程序的构建

在构建程序的时候,链接器都会给程序设置一个默认的加载地址,即首选基地址,它表示该模块被映射到进程地址空间时最佳的内存地址。默认情况下,对于EXE程序而言,windows链接器会将它的首选基地址设置为0X400000(四十万),而DLL程序的首选基地址则被设置为0X10000000(1千万),然后链接器将该地址以及一些相关数据和代码的地址写入到PE文件中。首选基地址的是为了系统程序加载器设计的,作用是告诉加载器把程序优选加载到该首选基地址,然后就可以直接将其他的数据和代码加载到内存中。

由于程序中会有代码和数据,而这些代码所引用的数据地址在程序构建完成后就已经确定了,但是这些数据地址是相对地址,是相对于设置好的首选基地址。对于某个EXE文件,假如代码中有一条指令:

                   MOV    EAX, [0X52050]

假如该EXE被加载到首选基地址(0X400000,一般而言,EXE都会加载的首选基地址的,因为EXE都有自己的独立地址空间),那么该条指定实际上是把该进程空间内的地址为0X52050+0X400000=0X452050处的内容移到寄存器EAX中,但是如果该EXE不是加载到首选基地址,那么该条指令的数据地址就不再是0X452050了,而是EXE实际加载的基地址+0X52050,这个就涉及到程序的加载和地址重定位的问题了。


2.程序的加载

系统程序加载程序的时候是根据程序的PE文件格式加载的,一般而言,由于EXE文件都是有自己独立的地址空间的,所以EXE一般都可以加载到EXE的首选基地址空间上。而对于DLL程序而言,由于DLL一般是加载到其他的进程的地址空间内,而对应的进程往往需要加载多个DLL模块,所以大多时候的DLL都不会加载到首选基地址上,这个时候程序模块的重定位段的作用就凸现出来了。
所谓重定位就是当某模块未被载入到首选基地址时,加载器会计算模块实际载入的地所以DLL一定要有重定位段,除非在链接时使用/FIXED 开关。此时链接器会去掉重定位段。如果此后模块未被载入首选基地址,由于无法重定位,模块不会被载入,导致程序无法运行。址跟首选基地址的差值,将这个差值加到机器指令所引用的原来的地址,得到的就是模块中各指令所引用的数据在本进程地址空间的正确地址,随后加载器会修正模块中对每个内存地址的引用。为了便于系统有能力对各数据的地址进行修正,windows提供了重定位段,它包含很多基址重定位信息,这个段是有很多项组成,每一项表示一个要重定位的地址,它包含一个字节偏移量列。该偏移量表示一条机器指令所使用的内存地址,这便于系统在确认该模块需要重定位时对需要重定位的数据进行定位。
由于程序有时候并不能加载到首选基地址,所以需要在加载的时候做重定位,根据程序实际加载的基地址和程序的重定位段对重定位段中的每一项做重定位。由于DLL的特性导致DLL一定要有重定位段,除非在链接时使用/FIXED 开关,此时链接器会去掉重定位段。如果此后DLL模块未被载入首选基地址,由于无法重定位,模块不会被载入,导致程序无法运行。如果加载程序将模块加载到它的首选基地址,那么系统就不会访问模块的重定位段。否则系统会打开模块的重定位段并遍历其中的条目,对每个条目,加载程序先找到包含机器指令的那个存储页面,算出差值,将差值加到机器指令当前正在使用的内存地址上。
对于重定位而言,其对系统的开销还是蛮大的:
a) 由于每次重定位需要对重定位段中的每一项做重定位操作,需要修改对应的代码,影响程序的初始化效率;
b) 当加载程序写入模块的代码页面时,由于它们具有写时复制属性,写时复制机制会导致系统从页交换文件中分配空间来容纳这些修改后的页面。

对于EXE文件而言,系统首先给该EXE创建进程空间,然后将该EXE映射到内存中,接着检查该EXE的导入段,把需要加载的DLL加载过来,在加载DLL的时候检查该DLL的导入段,将DLL对应的附加DLL也加载过来,当所有的DLL加载完毕后需要修复EXE的导入函数表。此时,检查对应DLL的实际记载的基地址和对于函数的相对偏移地址(RVA),计算出函数在进程内的虚拟地址(VA)=基地址+RVA,然后用该地址修复对应的导入符号的引用即可。

所谓的模块绑定就是说在运行之前,所有导入符号在进程地址空间的地址已经获得,不需要加载时在计算出来这节省初始化时间,另外将导入符号的虚拟地址写入exe模块的导入段,也会由于写时复制机制将要修改的页面以系统页交换文件为后备存储器。这会遇到与基址重定位相似的问题。所以模块绑定对提高系统性能的提高是显著的。

对于DLL而言,因为其一般是加载到其他进程空间的,如果加载到目标进程空间内不需要重定位,那么其加载过程和EXE的加载类似,如果需要重定位,则首选需要对模块做重定位,然后再映射到目标进程内,其后续的过程也和EXE的加载类似。


3.VS工具

根据地址重定位的原理和修复导入表的过程可知,如果希望程序能够快速加载,那么就希望程序加载的时候不需要做地址重定位,也不需要修复导入符号引用地址,这样加载程序就会很顺畅,提升系统性能。对于解决上述的两个问题,微软给我们提供了两个工具:ReBase和Bind。顾名思义,ReBase是用来解决地址重定位的问题,而Bind则用来解决模块绑定的问题。ReBase的工作原理是针对某个应用程序所使用的DLL的首选基地址做优化,使得优化后加载DLL时不再需要地址重定位,而Bind则模拟程序加载,获取导入符号的地址,并修复程序导入符号表,同时将修复后的地址写入磁盘文件中。
Rebase.exe的工具,如果在执行它时为它提供一组映像文件名,它会执行以下操作:
a) 它会模拟创建一个地址空间。
b) 它会打开这一组映像文件,并得到它们的大小和首选基地址。
c) 它会在模拟的地址空间对模块重定位的过程进行模拟,以便各模块没有交叉。
d) 对于每个需要重定位的模块,它会解析模块的重定位段,并修改模块在磁盘文件中的代码。
e) 将每个模块新的首选基地址写入各个模块磁盘文件中。
所以通过这种方法后可解除一个程序的地址重定位问题,但是需要注意的是,这种优化只针对某个特定程序有效,当这几个DLL同时加载到其他程序里面的时候就未必不会发生地址重定位的问题了。
Bind.exe工具,如果在执行它的时候传给它一个映像文件名,它会对其执行模块绑定操作。具体过程为:
a) 它会打开模块的导入段。
b) 对导入段列出的每个DLL,它会检查该DLL文件的文件头,来确定该DLL的首选基地址。
c) 它会在DLL的导出段查看每个符号。
d) 取得符号的RAV,并将其与模块的首选基地址相加。得到导入符号的虚拟地址(VA)。
e) 在映像文件的导入段中添加额外信息。这些信息包括映像文件被绑定的各DLL的名称,以及各模块的时间戳。
因为该工具假设DLL会加载到首选基地址,所以如果程序在加载时出现了地址重定位问题,那么模块绑定是无效的,该工具有效的时候需要满足的条件是:
a) 进程初始化时所需的DLL都被映射到了它们的首选基地址。
b) 绑定完成之后,DLL导出段所列出的符号的位置没有发生改变。这可以通过检查每个DLL的时间戳来保证。
如果上述假设有一个不成立。加载程序必须像绑定之前一样,手动修正可执行文件导入段。如果都成立加载程序就可以不用做这些工作了。


参考

1 《Windows核心编程系列》谈谈基址重定位和模块绑定 ithzhang