PE文件各区段说明Word格式文档下载.docx
《PE文件各区段说明Word格式文档下载.docx》由会员分享,可在线阅读,更多相关《PE文件各区段说明Word格式文档下载.docx(11页珍藏版)》请在冰豆网上搜索。
你不能够
以DLL函数的真正地址初始化一个变量。
例如:
FARPROCpfnGetMessage=GetMessage;
是把GetMessage函数地址放到pfnGetMessage变量中。
在Win16这没问题,在
Win32,变量中放的其实将是稍早我说过的JMPDWORDPTR[XXXXXXXX]指令的地址。
如
果你根据这个函数指针来呼叫函数,事情会如你所预期。
但如果你要以此指针读取
GetMessage的前数个字节,幸运之神不会站在你那边。
稍后我将在「PE文件的输出
(exports)」一节中再继续讨论这个主题。
在我写完本章的第一个版本之后,VisualC++2.0推出了。
它介绍另一种新的呼叫方式。
如果你看过VisualC++2.0的系统表头文件(例如WINBASE.H),你将看到和过去不同
的东西。
在VisualC++2.0中,API函数原型都有一个__declspec(dllimport)作为原型
的一部份。
当你呼叫一个这样的函数,编译器不会在模块的另一个地方产生JMPDWORDPTR
[XXXXXXXX]指令,而是产生一个CALLDWORDPTR[XXXXXXXX]函数呼叫。
XXXXXXXX位
址位于.idata内,作用与原先在JMPDWORDPTR[XXXXXXXX]指令中的地址相同。
就我
所知,BorlandC++4.5编译器并没有这样的性质。
BorlandCODE以及.icodesections
BorlandC++4.5编译器和联结器不能够使用COFFOBJ档,它们固守IntelOMF32位
元格式。
Borland编译器当然可以吐出一个名为.text的section,但它却选择"
CODE"
这
个名称。
为了决定PE档中的一个section名称,BorlandC++联结器(TLINK32.EXE)
从OBJ档中取出section名称并把它拦断为8个字符(如果必要)。
所以,BorlandC++有
一个CODEsection而不是.textsection。
名称不同不算什么,更重要的不同存在于Borland工具联结出来的PE档中。
稍早我说
过,所有对OBJ的呼叫都经由一个JMPDWORDPTR[XXXXXXXX]指令。
在微软的系统中,
这指令来自一个importlibrary的.textsection。
也就是说联结器不需要知道如何产生这
个指令。
importlibrary可视为「需要联结到PE档中」的更多的码和资料。
Borland系统的处理方式就不一样,它比较类似16位NE文件所采行的方法。
Borland
联结器所使用的importlibrary真正只是函数名称和DLL名称的列表而已。
TLINK32有
责任决定哪一些待修正记录(fixups)是针对外部DLLs,然后为它们产生JMPDWORDPTR
[XXXXXXXX]指令。
BorlandC++4.0的TLINK32把它所产生的这个指令存放在.icode
section中,但是到了BorlandC++4.02,TLINK32又改变了,把所有这些JMP指令放
到CODEsection中。
.datasection
这是你的初始化资料的存放区。
所谓初始化数据,包括全域变量和静态变量(globaland
staticvariable),在编译器时期就给定初值。
它也包括字符串常数,像是C/C++程序中的
"
HelloWorld"
。
联结器把OBJ和LIB文件中所有的.data组合起来放到EXE文件
的.data。
区域变量(localvariable)位于线程堆栈之中,不占用.data或.bss空间。
DATASECTION
BorlandC++以DATA作为其预设的资料区域。
相当于微软编译器所制作的.data。
.bsssection
这是任何未初始化的静态变量和全域变量的存放区。
联结器把OBJ和LIB文件中所有
的.bss组合起来放到EXE文件的.bss。
在sectiontable中,.bss的RawDataOffset栏
位总是为0,表示这个section不占用文件的任何一点空间。
TLINK32并不吐出一
个.bss,它的作法是扩充DATAsection的虚拟大小,以接纳未初始化的资料。
.CRTsection
这是微软的C/C++runtimelibrary(CRT)所使用的另一个初始化的datasection。
这里所
放的资料用于「在main或WinMain之前执行的staticC++类别建构式」中。
.rsrcsection
此处内含模块资源。
早期的NT,16位RC.EXE所输出的.RES档并不被微软的联
结器所了解,那个时候的CVTRES程序就是用来把一个.RES档转换为一个COFF
OBJ,把资源放到OBJ档的一个.rsrc之中。
联结器于是就可以产生一个resourceOBJ。
也就是说,联结器不需要知道任何有关于资源的事情。
后来的微软联结器已经能够直接
处理.RES档。
我将在「PE文件的资源」一节中涵盖资源section的格式。
.idatasection
这个section内含有关于「模块从其它DLLs中输入(import)函数和资料」的相关资
讯。
它相当于NT档的modulereferencetable。
关键性的差异是,每一个输入函数都被
列在这个section之中。
如果要在NE文件中找出对等的信息,你必须深掘每一个节区的
原始内容的重定位资料。
我将在「PE文件的输入(imports)」一节中涵盖importtable的
格式。
.edatasection
这是PE档输出函数(exportfunction)的相关信息。
它的NE对等物是entrytable、resident
namestable和nonresidentnamestable的组合。
和Win16不同的是,很少有机会从一个
EXE中输出一个函数出去,所以通常你只在DLL中才会看到.edata。
BorlandC++所
产生的EXE是个例外,它总是有一个输出函数(__GetExceptDLLinfo)给runtimelibrary
的内部使用。
exporttable的格式将于本章的「PE文件的输出(exports)」一节讨论。
如果使用微软
工具,.edata的资料来自.EXP档,但是联结器没有能力产生这个文件,必须依赖函数
库管理器LIB32.EXE扫描OBJ文件然后才产生EXP档,然后才能交给联结器。
是的,
那是真的,EXP档其实就是拥有不同扩展名的OBJ档罢了。
使用PEDUMP/S观察EXP
档,你可以看到其中的输出函数(exportfunctions)。
.relocsection
这个section内含一表格的baserelocations。
所谓baserelocation是一个指令或初始化
变量的调整值。
如果加载器没有办法把EXE或DLL文件加载到预设的地址的话,就
必须做这样的调整;
否则加载器可以忽略「重定位」这件事情。
如果你希望加载器总是能够把image加载到预定的基地址,你可以使用/FIXED选
项,告诉联结器剥除本项信息。
虽然这可以节省EXE的文件空间,却可能使得EXE档
没办法在其它Win32平台上执行。
例如,你为NT开发了一个EXE,基地址为
0x10000。
如果你告诉联结器把这信息剥除,这个EXE就没有办法在Windows95上跑,
因为0x10000不适用(Windows95的最低加载地址是0x400000,也就是4MB)。
注意一点,编译器所产生的JMP和CALL指令,其所使用的offset值是与该指令成相对
地址关系,而不是真正的32位平滑节区的offset值。
如果image被加载到一个并非
联结器指定的基地址去,JMP和CALL指令不需修改,因为它们用的是相对寻址。
也就是说,其实没有如你想象中那么多的重定位动作要做。
只有使用32-bitoffset的指令
才需要重定位动作。
假设你有下面的全域变量宣告:
inti;
int*ptr=&
i;
如果联结器设定基地址是0x10000,变量i的地址是0x12004。
在被用来存放ptr的
内存中,联结器将写入0x12004,因为那是变量i的地址。
如果加载器为了某种理由
把文件加载到0x70000处,i的地址将是0x72004,然而,预先初始化过的ptr值变成
错误值,因为i现在的位置已经提升了0x60000。
这就是需要重定位信息参一脚的场合了。
.reloc用来表示「联结器所假设的加载地址」
和「真正的加载地址」之间的差异。
我将在「PE档的BaseRelocations」一节有比较详
细的讨论。
.tlssection
当你使用编译器的"
__declspec(thread)"
性质,你定义的资料并没有进入.data或.bss之
中,倒是有一份拷贝进入.tls之中。
.tls的名称是因为threadlocalstorage而来,和TlsAlloc
函数家族有密切关系。
为了简单描述所谓的threadlocalstorage,请把它想象成「让每一个线程拥有各自的全
域变量」的一种方法。
也就是说,每一个线程可以拥有它自己的一组静态资料,使用
这些资料的程序代码,不需在意现在是哪一个线程正在执行。
假设某程序有数个线程,
处理相同的工作。
也因此执行相同的码。
如果你宣告一个tls,像这样:
__declspec(thread)inti=0;
//thisisaglobalvariabledeclaration
每一个线程将因此拥有变量i的一个副本。
你可以明白地在执行时期索求并使用tls,相关函数是TlsAlloc、TlsSetValue、TlsGetValue
等(第3章对于TlsXXX函数的描述比较详细)。
通常,以__declspec(thread)在程序中
宣告你的资料,比使用TlsAlloc简单得多。
这里有一个坏消息。
在NT和Windows95中,tls机制不能够有效运作--如果运作对
象是以LoadLibrary动态加载的DLL。
至于在一个EXE或是一个隐式加载(implicitly
loaded,译注)的DLL之中,每一件事情都没问题。
如果你不能够以隐晦方式加载DLL,
但又需要让每一个线程有自己的资料,那你只好使用TlsAlloc和TlsGetValue。
注意,
每一线程真正的内存区块并不是放在.tlssection中,也就是说,当切换线程的时
候,内存管理器并不改变「实际映像至模块之.tlssection」的内存。
.tls内只不过是
一些资料,用来初始化真正的线程专属区块。
初始化动作是靠操作系统与runtimelibrary
的合作,过程之中需要另外一些储存在.rdata之中的资料:
TLSdirectory。
译注:
如果程序与DLL的importlibrary联结,我们说这是implicitlylink,并导至DLL被implicitlyloaded。
如果程序没有与DLL的importlibrary联结,而是在需要时(执行时期)呼叫LoadLibrary和GetProcAddress以取得函数地址,再呼叫之,我们称此为explicitlylinked,并导至DLL被explicitlyloaded。
.rdatasection
.rdata至少有四个用途。
第一,在被微软联结器产生的EXEs之中,.rdata内含debug
directory(OBJ档中并没有debugdirectory)。
而在TLINK32所产生的EXEs之中,debug
directory是一个名为.debug的section。
debugdirectory是一个由
IMAGE_DEBUG_DIRECTORY结构所组成的数组。
这些结构持有文件之中各种除错资
讯的型态、大小、位置。
除错信息可能有三种型态:
CodeView、COFF、FPO。
图8-5
显示PEDUMP对一典型的debugdirectory的输出结果。
Type
Size
Address
FilePtr
Charactr
TimeData
Version
COFF
000065C5
00000000
00009200
2CF83F3D
0.00
(unknown)
00000114
0000F7C8
FPO
000004B0
0000F8DC
CODEVIEW
0000B0B4
0000FD8C
图8-5一个典型的debugdirectory。
debugdirectory并不一定会在.rdata的起始处被发现。
要找到它,你必须使用datadirectory
的第7笔资料(IMAGE_DIRECTORY_ENTRY_DEBUG)。
还记得吗,datadirectory位
于PE表头的尾端。
为了确定微软联结器所做出来的debugdirectory的项目个数,请把
debugdirectory的大小(可从debugdirectory的"
size"
字段获得)除以
IMAGE_DEBUG_DIRECTORY的结构大小。
至于TLINK32则是把debugdirectories的
真正数量记录在"
字段中,而不是字节总长度。
PEDUMP可以处理这两种情况。
.rdata的第二个有用部份是descriptionstring。
如果你在程序的.DEF档中指定
DESCRIPTION,被指定的字符串就会出现在.rdata之中。
在NE档中,descriptionstring总
是nonresidentnamestable的第一个项目。
descriptionstring主要是用来设定一个有用的
字符串,用以描述这个文件。
不幸的是我还没有发现什么好方法来找到它。
我曾经看过有
些PE档的descriptionstring放在debugdirectory之前,有些却在debugdirectory之后。
.rdata的第三个用途是为了OLE程序设计所需的GUIDs。
UUID.LIB内含一系列的128
位GUIDs,当作interfaceIDs。
这些GUIDs都放在EXE或DLL的.rdata中。
.rdata的最后一个用途是用来放置TLS(ThreadLocalStorage)的directory。
TLSdirectory
是一个特殊数据结构,被编译器的runtimelibrary使用,以便能够透明化地提供TLS给
程序中宣告的变量。
TLSdirectory的格式可以在MSDN(MicrosoftDeveloperNetwork)
光盘片中找到:
PortableExecutableandCommonObjectFileFormat"
我们对TLSdirectory
的主要兴趣是指向资料(用来初始化每一个tls区块)的起头和结尾的指针,TLSdirectory
的RVA(RelativeVirtualAddress)可以在PE表头的datadirectory的
IMAGE_DIRECTORY_ENTRY_TLS项目中获得。
至于真正用来初始化TLS区块的资
料可以在.tlssection中找到。
.debug$S和.debug$Tsections
.debug$S和.debug$T只出现于COFFOBJs之中,内含CodeView的符号和型态资
看来十分奇怪的section名称系衍生自前一版微软编译器的节区名称($$SYMBOLS
和$$TYPES)。
.debug$T的唯一目的是为了放置.PDB档(内有项目中所有OBJs的
CodeView型态信息)的路径名称。
联结器利用.PDB为EXE档产生出一部份的
CodeView信息。
.drectvesection
这个section只出现在OBJ档,内含联结器命令列参数的文字表达。
例如,在微软的
VisualC++编译器,下面字符串一定会出现在.drectve中:
-defaultlib:
LIBC-defaultlib:
OLDNAMES
当你在程序代码中使用__declspec(export),编译器会制造出命令列上的对应东西,放
在.drectve之中(例如export:
MyFunction)。
含有$的sections(只针对OBJs/LIBs)
在OBJ档中,名称含有$的sections(例如.idata$2)将被联结器特别对待。
联结器把
所有拥有相同名称(直至$字符)的sections组合成为单一一个section。
例如,如果
联结器遭遇.idata$2和.idata$6,它会把它们整合为一个.idata。
被整合的sections的次序是以$之后的字符为准。
联结器以字母顺序排列之,所
以.idata$2在.idata$6之前。
.idata$A则在.idata$B之前。
那么到底带有$的section做什么用?
最普遍的用法就是importlibrary利用它们来存
放最终的.idata(importsection)的各部份资料。
这可有趣了,联结器本身并不需要从头
产生.idata,最终的.idata是由OBJ和LIB各贡献一部份而来。
杂项的sections
有时候我会从PEDUMP的输出中看到其它一些sections。
例如Windows95的GDI32.DLL
内含一个名为_GPFIX的datasection,我们推测它大概与GPfault的处理有关。
这有双重意义。
第一,不要以为你只能使用编译器或组译器提供的标准sections。
若有需
要,别犹豫不决。
在微软的C/C++编译器中,你可以使用#pragmacode_seg和#pragma
data_seg。
Borland的使用者则可以使用#pragmacodeseg和#pragmadataseg。
若是组合
语言,你只要产生一个32位节区并给予不同于「标准sections」的名称即可。
TLINK32
会把同类别的codesegments组合在一起,所以你要不就得为每一个codesegment指定
一个类别名称,要不就关闭"
codesegmentpacking"
这个性质。