PE文件各区段说明.docx
- 文档编号:6507960
- 上传时间:2023-01-07
- 格式:DOCX
- 页数:11
- 大小:24.59KB
PE文件各区段说明.docx
《PE文件各区段说明.docx》由会员分享,可在线阅读,更多相关《PE文件各区段说明.docx(11页珍藏版)》请在冰豆网上搜索。
PE文件各区段说明
PE文件各节所包含的内容
2008-11-1313:
34
关于sections的意义以及它如何定位,相信你已有个概念。
现在我们要看看在EXE和OBJ档中的一些常见的sections。
虽然我所列的并不是全部,但已经涵盖了你每天会接触到(但也许你自己并不知道)的sections。
排列次序是根据其重要性以及遭遇它们的频繁度。
.textsection
.text内含所有一般性的程序代码。
由于PE文件在32位模式下跑,并且不受约束于16位元节区,所以没有理由把程序代码分开放到不同的sections中。
联结器把所有来自.OBJ的.text集合到一个大的.text中。
如果你使用BorlandC++,其编译器制作出来的codesection名为CODE而不是.text。
请看稍后「BorlandCODE以及.icodesections」一节。
我很惊讶地发现,在.text中除了编译器制作出来的码,以及runtimelibrary的码之外,
还有一些其它东西。
在PE文件中,当你呼叫另一模块中的函数(例如USER32.DLL中
的GetMessage),编译器制造出来的CALL指令并不会把控制权直接传给DLL中的
函数,而是传给一个JMPDWORDPTR[XXXXXXXX]指令,后者也位于.text中。
JMP指
令跳到一个地址去,此地址储存在.idata的一个DWORD之中。
这个DWORD内含该
函数的真正地址(函数进入点),如图8-4所示。
图8-4一个PE档呼叫importedfunction。
沉思良久,我终于了解为什么DLL的呼叫需要以这种方式实现。
把对同一个DLL函数
的所有呼叫都集中到一处,加载器就不再需要修补每一个呼叫DLL的指令。
PE加载器
需要做的,就只是把DLL函数的真实地址放到.idata的那个DWORD之中,根本就
没有程序代码需要修补。
这和NE档有极明显的差异。
NE档的每一个节区内含一串待修
正记录(fixuprecords),如果某一节区呼叫同一个DLL函数20次,加载器就必须忙
碌20次,将函数地址拷贝到待修正记录之中。
PE档这种处理方式也有缺点:
你不能够
以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
00000000
2CF83F3D
0.00
(unknown)
00000114
00000000
0000F7C8
00000000
2CF83F3D
0.00
FPO
000004B0
00000000
0000F8DC
00000000
2CF83F3D
0.00
CODEVIEW
0000B0B4
00000000
0000FD8C
00000000
2CF83F3D
0.00
图8-5一个典型的debugdirectory。
debugdirectory并不一定会在.rdata的起始处被发现。
要找到它,你必须使用datadirectory
的第7笔资料(IMAGE_DIRECTORY_ENTRY_DEBUG)。
还记得吗,datadirectory位
于PE表头的尾端。
为了确定微软联结器所做出来的debugdirectory的项目个数,请把
debugdirectory的大小(可从debugdirectory的"size"字段获得)除以
IMAGE_DEBUG_DIRECTORY的结构大小。
至于TLINK32则是把debugdirectories的
真正数量记录在"size"字段中,而不是字节总长度。
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"这个性质。
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- PE 文件 各区 说明
![提示](https://static.bdocx.com/images/bang_tan.gif)