PE学习

来源:互联网 发布:java split正则表达式 编辑:程序博客网 时间:2024/05/17 09:07
常会遇到的Sections
 
关于 sections 的意义以及它如何定位,相信你已有个概念。现在我们要看看在 EXE 和
OBJ 档中的一些常见的 sections 。虽然我所列的并不是全部,但已经涵盖了你每天会接
触到(但也许你自己并不知道)的 sections 。排列次序是根据其重要性以及遭遇它们的频
繁度。
 
.text section
 
.text 内含所有一般性的程序代码。由于 PE 文件在 32 位模式下跑,并且不受约束于 16 位
元节区,所以没有理由把程序代码分开放到不同的 sections 中。联结器把所有来自 .OBJ
的 .text 集合到一个大的 .text 中。如果你使用Borland C++,其编译器制作出来的 code
section 名为 CODE 而不是 .text 。请看稍后的「Borland CODE 以及 .icode sections 」一
节。
 
我很惊讶地发现,在 .text 中除了编译器制作出来的码,以及runtime library 的码之外,
还有一些其它东西。在 PE 文件中,当你呼叫另一模块中的函数(例如 USER32.DLL 中
的 GetMessage ),编译器制造出来的 CALL 指令并不会把控制权直接传给 DLL 中的
函数,而是传给一个JMP DWORD PTR [XXXXXXXX]指令,后者也位于 .text 中。JMP 指
令跳到一个地址去,此地址储存在 .idata 的一个 DWORD 之中。这个DWORD 内含该
函数的真正地址(函数进入点),如图8-4 所示。
 
                             
 
图8-4 一个 PE 档呼叫 imported function 。
 
沉思良久,我终于了解为什么 DLL 的呼叫需要以这种方式实现。把对同一个DLL 函数
的所有呼叫都集中到一处,加载器就不再需要修补每一个呼叫 DLL 的指令。PE 加载器
需要做的,就只是把 DLL 函数的真实地址放到 .idata 的那个 DWORD 之中,根本就
没有程序代码需要修补。这和 NE 档有极明显的差异。NE 档的每一个节区内含一串待修
正记录(fixup records ),如果某一节区呼叫同一个 DLL 函数 20 次,加载器就必须忙
碌 20 次,将函数地址拷贝到待修正记录之中。PE 档这种处理方式也有缺点:你不能够
以 DLL 函数的真正地址初始化一个变量。例如:
 
FARPROC pfnGetMessage = GetMessage;
 
是把 GetMessage 函数地址放到 pfnGetMessage 变量中。在 Win16 这没问题,在
Win32 ,变量中放的其实将是稍早我说过的JMP DWORD PTR [XXXXXXXX]指令的地址。如
果你根据这个函数指针来呼叫函数,事情会如你所预期。但如果你要以此指针读取
GetMessage 的前数个字节,幸运之神不会站在你那边。稍后我将在「PE 文件的输出
(exports )」一节中再继续讨论这个主题。
 
在我写完本章的第一个版本之后,Visual C++ 2.0 推出了。它介绍另一种新的呼叫方式。
如果你看过 Visual C++ 2.0 的系统表头文件(例如 WINBASE.H ),你将看到和过去不同
的东西。在 Visual C++ 2.0 中,API 函数原型都有一个 __declspec(dllimport )作为原型
的一部份。当你呼叫一个这样的函数,编译器不会在模块的另一个地方产生JMP DWORD PTR
[XXXXXXXX]指令,而是产生一个CALL DWORD PTR [XXXXXXXX]函数呼叫。XXXXXXXX
址位于 .idata 内,作用与原先在JMP DWORD PTR [XXXXXXXX]指令中的地址相同。就我
所知,Borland C++ 4.5 编译器并没有这样的性质。
 
Borland CODE 以及 .icode sections
 
Borland C++ 4.5 编译器和联结器不能够使用 COFF OBJ 档,它们固守 Intel OMF 32 位
元格式。Borland 编译器当然可以吐出一个名为 .text 的section ,但它却选择 "CODE" 这
个名称。为了决定 PE 档中的一个 section 名称,Borland C++ 联结器(TLINK32.EXE )
从OBJ 档中取出 section 名称并把它拦断为8 个字符(如果必要)。所以,Borland C++ 有
一个 CODE section 而不是 .text section 。
 
名称不同不算什么,更重要的不同存在于 Borland 工具联结出来的 PE 档中。稍早我说
过,所有对 OBJ 的呼叫都经由一个JMP DWORD PTR[XXXXXXXX] 指令。在微软的系统中,
这指令来自一个 import library 的 .text section 。也就是说联结器不需要知道如何产生这
个指令。import library 可视为「需要联结到 PE 档中」的更多的码和资料。
 
Borland 系统的处理方式就不一样,它比较类似 16 位 NE 文件所采行的方法。Borland
联结器所使用的 import library 真正只是函数名称和 DLL 名称的列表而已。TLINK32 有
责任决定哪一些待修正记录(fixups )是针对外部 DLLs ,然后为它们产生JMP DWORD PTR
[XXXXXXXX] 指令。Borland C++ 4.0 的TLINK32 把它所产生的这个指令存放在 .icode
section 中,但是到了Borland C++ 4.02 ,TLINK32 又改变了,把所有这些 JMP 指令放
到 CODE section 中。
 
.data section
 
这是你的初始化资料的存放区。所谓初始化数据,包括全域变量和静态变量(global and
static variable ),在编译器时期就给定初值。它也包括字符串常数,像是 C/C++ 程序中的
"Hello World"。联结器把 OBJ 和 LIB 文件中所有的 .data 组合起来放到 EXE 文件
的 .data 。区域变量(local variable )位于线程堆栈之中,不占用 .data 或 .bss 空间。
 
DATA SECTION
 
Borland C++ 以 DATA 作为其预设的资料区域。相当于微软编译器所制作的 .data 。
 
.bss section
 
这是任何未初始化的静态变量和全域变量的存放区。联结器把 OBJ 和 LIB 文件中所有
的 .bss 组合起来放到 EXE 文件的 .bss 。在 section table 中,.bss 的 RawDataOffset 栏
位总是为 0 ,表示这个 section 不占用文件的任何一点空间。TLINK32 并不吐出一
个 .bss ,它的作法是扩充 DATA section 的虚拟大小,以接纳未初始化的资料。
 
.CRT section
 
这是微软的 C/C++ runtime library (CRT )所使用的另一个初始化的 data section 。这里所
放的资料用于「在 main 或 WinMain 之前执行的 static C++ 类别建构式」中。
 
.rsrc section
 
此处内含模块资源。早期的 NT ,16 位 RC.EXE 所输出的 .RES 档并不被微软的联
结器所了解,那个时候的 CVTRES 程序就是用来把一个 .RES 档转换为一个 COFF
OBJ ,把资源放到 OBJ 档的一个 .rsrc 之中。联结器于是就可以产生一个 resource OBJ 。
也就是说,联结器不需要知道任何有关于资源的事情。后来的微软联结器已经能够直接
处理 .RES 档。我将在「PE 文件的资源」一节中涵盖资源 section 的格式。
 
.idata section
 
这个 section 内含有关于「模块从其它 DLLs 中输入(import )函数和资料」的相关资
讯。它相当于 NT 档的 module reference table 。关键性的差异是,每一个输入函数都被
列在这个 section 之中。如果要在 NE 文件中找出对等的信息,你必须深掘每一个节区的
原始内容的重定位资料。我将在「PE 文件的输入(imports )」一节中涵盖 import table 的
格式。
 
.edata section
 
这是 PE 档输出函数(export function )的相关信息。它的 NE 对等物是entry table 、resident
names table 和 nonresident names table 的组合。和 Win16 不同的是,很少有机会从一个
EXE 中输出一个函数出去,所以通常你只在 DLL 中才会看到 .edata 。Borland C++ 所
产生的EXE 是个例外,它总是有一个输出函数(__GetExceptDLLinfo )给runtime library
的内部使用。
 
export table 的格式将于本章的「PE 文件的输出(exports )」一节讨论。如果使用微软
工具,.edata 的资料来自 .EXP 档,但是联结器没有能力产生这个文件,必须依赖函数
库管理器 LIB32.EXE 扫描 OBJ 文件然后才产生 EXP 档,然后才能交给联结器。是的,
那是真的,EXP 档其实就是拥有不同扩展名的 OBJ 档罢了。使用 PEDUMP /S 观察 EXP
档,你可以看到其中的输出函数(export functions )。
 
.reloc section
 
这个 section 内含一表格的 base relocations 。所谓 base relocation 是一个指令或初始化
变量的调整值。如果加载器没有办法把 EXE 或 DLL 文件加载到预设的地址的话,就
必须做这样的调整;否则加载器可以忽略「重定位」这件事情。
 
如果你希望加载器总是能够把 image 加载到预定的基地址,你可以使用 /FIXED 选
项,告诉联结器剥除本项信息。虽然这可以节省 EXE 的文件空间,却可能使得EXE 档
 
没办法在其它 Win32 平台上执行。例如,你为 NT 开发了一个 EXE ,基地址为
0x10000 。如果你告诉联结器把这信息剥除,这个 EXE 就没有办法在 Windows 95 上跑,
因为 0x10000 不适用(Windows 95 的最低加载地址是0x400000 ,也就是 4MB )。
 
注意一点,编译器所产生的JMP 和CALL 指令,其所使用的 offset 值是与该指令成相对
地址关系,而不是真正的 32 位平滑节区的 offset 值。如果 image 被加载到一个并非
联结器指定的基地址去,JMP 和 CALL 指令不需修改,因为它们用的是相对寻址。
也就是说,其实没有如你想象中那么多的重定位动作要做。只有使用32-bit offset 的指令
才需要重定位动作。假设你有下面的全域变量宣告:
 
int i;
int *ptr = &i;
 
如果联结器设定基地址是 0x10000 ,变量 i 的地址是 0x12004 。在被用来存放 ptr 的
内存中,联结器将写入 0x12004 ,因为那是变量 i 的地址。如果加载器为了某种理由
把文件加载到 0x70000 处,i 的地址将是 0x72004 ,然而,预先初始化过的 ptr 值变成
错误值,因为 i 现在的位置已经提升了 0x60000 。
 
这就是需要重定位信息参一脚的场合了。.reloc 用来表示「联结器所假设的加载地址」
和「真正的加载地址」之间的差异。我将在「PE 档的 Base Relocations 」一节有比较详
细的讨论。
 
.tls section
 
当你使用编译器的 "__declspec(thread)" 性质,你定义的资料并没有进入 .data 或 .bss 之
中,倒是有一份拷贝进入 .tls 之中。.tls 的名称是因为thread local storage 而来,和 TlsAlloc
函数家族有密切关系。
 
为了简单描述所谓的 thread local storage ,请把它想象成「让每一个线程拥有各自的全
域变量」的一种方法。也就是说,每一个线程可以拥有它自己的一组静态资料,使用
这些资料的程序代码,不需在意现在是哪一个线程正在执行。假设某程序有数个线程,
处理相同的工作。也因此执行相同的码。如果你宣告一个 tls ,像这样:
 
__declspec(thread) int i = 0; // this is a global variable declaration
 
每一个线程将因此拥有变量 i 的一个副本。
 
你可以明白地在执行时期索求并使用 tls ,相关函数是 TlsAlloc 、TlsSetValue 、TlsGetValue
等(第3 章对于 TlsXXX 函数的描述比较详细)。通常,以 __declspec(thread) 在程序中
宣告你的资料,比使用 TlsAlloc 简单得多。
 
这里有一个坏消息。在 NT 和 Windows 95 中,tls 机制不能够有效运作 -- 如果运作对
象是以 LoadLibrary 动态加载的 DLL 。至于在一个 EXE 或是一个隐式加载(implicitly
loaded ,译注)的DLL 之中,每一件事情都没问题。如果你不能够以隐晦方式加载 DLL ,
但又需要让每一个线程有自己的资料,那你只好使用 TlsAlloc 和 TlsGetValue 。注意,
每一线程真正的内存区块并不是放在 .tls section 中,也就是说,当切换线程的时
候,内存管理器并不改变「实际映像至模块之 .tls section 」的内存。.tls 内只不过是
一些资料,用来初始化真正的线程专属区块。初始化动作是靠操作系统与runtime library
的合作,过程之中需要另外一些储存在 .rdata 之中的资料:TLS directory 。
译注:如果程序与 DLL 的 import library 联结,我们说这是 implicitly link ,并导至 DLL 被 implicitly loaded 。如果程序没有与 DLL 的 import library 联结,而是在需要时(执 行时期)呼叫 LoadLibrary 和 GetProcAddress 以取得函数地址,再呼叫之,我们称此为 explicitly linked ,并导至 DLL 被 explicitly loaded 。
.rdata section
 
.rdata 至少有四个用途。第一,在被微软联结器产生的 EXEs 之中,.rdata 内含debug
directory (OBJ 档中并没有 debug directory )。而在 TLINK32 所产生的EXEs 之中,debug
directory 是一个名为 .debug 的 section 。debug directory 是一个由
IMAGE_DEBUG_DIRECTORY 结构所组成的数组。这些结构持有文件之中各种除错资
讯的型态、大小、位置。除错信息可能有三种型态:CodeView 、COFF 、FPO 。图8-5
显示 PEDUMP 对一典型的 debug directory 的输出结果。
 
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 一个典型的 debug directory 。
 
debug directory 并不一定会在 .rdata 的起始处被发现。要找到它,你必须使用data directory
的第 7 笔资料(IMAGE_DIRECTORY_ENTRY_DEBUG )。还记得吗,data directory 位
于 PE 表头的尾端。为了确定微软联结器所做出来的debug directory 的项目个数,请把
debug directory 的大小(可从debug directory 的 "size" 字段获得)除以
IMAGE_DEBUG_DIRECTORY 的结构大小。至于 TLINK32 则是把 debug directories 的
真正数量记录在 "size" 字段中,而不是字节总长度。PEDUMP 可以处理这两种情况。
 
.rdata 的第二个有用部份是 description string 。如果你在程序的 .DEF 档中指定
DESCRIPTION ,被指定的字符串就会出现在 .rdata 之中。在 NE 档中,description string 总
是 nonresident names table 的第一个项目。description string 主要是用来设定一个有用的
字符串,用以描述这个文件。不幸的是我还没有发现什么好方法来找到它。我曾经看过有
些PE 档的description string 放在debug directory 之前,有些却在debug directory 之后。
 
.rdata 的第三个用途是为了 OLE 程序设计所需的 GUIDs 。UUID.LIB 内含一系列的 128
位 GUIDs ,当作 interface IDs 。这些 GUIDs 都放在EXE 或 DLL 的 .rdata 中。
 
.rdata 的最后一个用途是用来放置 TLS (Thread Local Storage )的 directory 。TLS directory
是一个特殊数据结构,被编译器的runtime library 使用,以便能够透明化地提供 TLS 给
程序中宣告的变量。TLS directory 的格式可以在 MSDN (Microsoft Developer Network )
光盘片中找到:"Portable Executable and Common Object File Format"。我们对 TLS directory
的主要兴趣是指向资料(用来初始化每一个 tls 区块)的起头和结尾的指针,TLS directory
的 RVA (Relative Virtual Address )可以在 PE 表头的 data directory 的
IMAGE_DIRECTORY_ENTRY_TLS 项目中获得。至于真正用来初始化 TLS 区块的资
料可以在 .tls section 中找到。
 
.debug$S 和 .debug$T sections
 
.debug$S 和 .debug$T 只出现于 COFF OBJs 之中,内含 CodeView 的符号和型态资
讯。看来十分奇怪的 section 名称系衍生自前一版微软编译器的节区名称($$SYMBOLS
和 $$TYPES )。.debug$T 的唯一目的是为了放置 .PDB 档(内有项目中所有 OBJs 的
CodeView 型态信息)的路径名称。联结器利用 .PDB 为 EXE 档产生出一部份的
CodeView 信息。
 
.drectve section
 
这个 section 只出现在 OBJ 档,内含联结器命令列参数的文字表达。例如,在微软的
Visual C++ 编译器,下面字符串一定会出现在 .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 做什么用?最普遍的用法就是 import library 利用它们来存
放最终的 .idata (import section )的各部份资料。这可有趣了,联结器本身并不需要从头
产生 .idata ,最终的 .idata 是由 OBJ 和 LIB 各贡献一部份而来。
 
杂项的sections
 
有时候我会从PEDUMP 的输出中看到其它一些sections 。例如Windows 95 的GDI32.DLL
内含一个名为 _GPFIX 的 data section ,我们推测它大概与GP fault 的处理有关。
 
这有双重意义。第一,不要以为你只能使用编译器或组译器提供的标准 sections 。若有需
要,别犹豫不决。在微软的 C/C++ 编译器中,你可以使用 #pragma code_seg 和 #pragma
data_seg 。Borland 的使用者则可以使用 #pragma codeseg 和 #pragma dataseg 。若是组合
语言,你只要产生一个 32 位节区并给予不同于「标准 sections 」的名称即可。TLINK32
会把同类别的 code segments 组合在一起,所以你要不就得为每一个 code segment 指定
一个类别名称,要不就关闭 "code segment packing" 这个性质
 
原创粉丝点击