托管PE文件

文/玄魂

 

 中间语言

在.NET框架中,公共语言基础结构使用CLS来绑定不同的语言。通过要求不同的语言至少要实现CTS包含在CLS中的部分,公共语言基础结构允许不同的语言使用.NET框架。因此,在.NET框架中,所有的语言(C#、VB.NET、Effil.NET等)最后都被转换为了一种通用语言:微软中间语言(Microsoft Intermediate Language,MSIL,以下简称IL)。

IL是一种介于高级语言和基于Intel的汇编语言的中间语言,是.NET平台的汇编语言。当用户编译一个.NET程序时,编译器将源代码翻译成一组可以有效地转换为本机代码且独立于CPU的指令。当执行这些指令时,实时编译器将它们转化为CPU特定的代码。由于CLR支持多种实时编译器,因此同一段IL代码可以被不同的编译器实时编译并运行在不同的结构上。

IL包括用于加载、存储和初始化对象以及对对象调用方法的指令,还包括用于算术和逻辑运算、控制流、直接内存访问、异常处理和其他操作的指令。要使代码可运行,必须先将 IL 转换为特定于 CPU 的代码,这通常是通过实时(JIT) 编译器来完成的。由于CLR为它支持的每种计算机结构都提供了一种或多种JIT编译器,因此同一组IL可以在所支持的任何结构上JIT编译和运行。

当编译器产生IL时,它也产生元数据。元数据描述代码中的类型,包括每种类型的定义、每种类型的成员的签名、代码引用的成员和运行库在执行时使用的其他数据。IL和元数据包含在一个可移植可执行(PE)文件中,下面重点介绍托管PE文件,以及元数据的相关知识。

1.3.1       托管PE文件

PE(Portable Execute,可移植执行体)是微软Windows操作系统上的程序文件,常见的如EXE、DLL、OCX、SYS、COM。图1-3展示了标准的PE/COFF文件头部格式。

 

托管PE文件

图1-3 标准的PE/COFF文件头部格式

MS DOS头是DOS系统的遗传内容,表示一个应用程序可以在DOS环境下运行。MS DOS根(stub)是一段代码,如果Windows程序在DOS环境下运行,会给出“该程序不能在DOS环境下运行”(This program cannot be run in DOS mode)的提示。在偏移量0x3c处,MS DOS头指向了PE标识(PE Signature)的地址。

PE标识表示该文件是一个PE文件。其值始终为00004550h,45h代表字符E,50h代表字符P。

COFF头(COFF Header)提供了COFF或者可执行文件的最一般的信息。

PE头(PE Header)提供了操作系统加载文件所需的信息。这对于PE文件是最重要的地方,其间包含了数据索引表和节。

关于标准PE文件的详细内容请读者阅读相关资料,本节内容只关注托管PE文件的特殊信息。CLR对传统的PE文件进行了扩展,如图1-4所示是托管PE文件的格式。

 

托管PE文件

图1-4 托管PE文件的格式

标准的Windows PE文件头和COFF(通用对象文件格式)头类似,分为PE32和PE32+两种。如果文件头采用PE32格式,则该文件可运行在32位或64位操作系统上。如果文件头采用PE32+格式,则该文件只能在64位的操作系统上运行。PE32 或者 PE32+ 头也包含文件类型信息:GUI、CUI或者DLL。如果包含本地CPU代码的模块,则PE32或者PE32+ 头将包含有关本地CPU代码的相关信息。

CLR头包含使这个模块被托管的相关信息。这些信息包括CLR需要的版本信息、一些标识、入口方法的元数据信息、模块的元数据位置和大小信息、资源信息、强名称和一些其他信息。

每一个托管模块都包含元数据表。元数据表有两种,一种是描述源代码中的类型描述和成员描述的元数据表,另一种是包含源代码引用的类型描述和成员描述的元数据表。

IL代码是编译器编译产生的中间代码,程序运行时CLR负责将中间代码编译成本地代码执行。

CLR头定义在.NET Framework的CorHdr.h中,代码如代码清单1-4所示。

代码清单1-4 CLR 头定义

 1 typedef struct IMAGE_COR20_HEADER

 2 

 3 {

 4 

 5 ULONG cb;

 6 

 7 USHORT MajorRuntimeVersion;

 8 

 9 USHORT MinorRuntimeVersion;

10 

11 // Symbol table and startup information

12 

13 IMAGE_DATA_DIRECTORY MetaData;

14 

15 ULONG Flags;

16 

17 union {

18 

19 DWORD EntryPointToken;

20 

21 DWORD EntryPointRVA;

22 

23 };

24 

25 // Binding information

26 

27 IMAGE_DATA_DIRECTORY Resources;

28 

29 IMAGE_DATA_DIRECTORY StrongNameSignature;

30 

31 // Regular fixup and binding information

32 

33 IMAGE_DATA_DIRECTORY CodeManagerTable;

34 

35 IMAGE_DATA_DIRECTORY VTableFixups;

36 

37 IMAGE_DATA_DIRECTORY ExportAddressTableJumps;

38 

39 IMAGE_DATA_DIRECTORY ManagedNativeHeader;

40 

41 } IMAGE_COR20_HEADER;

 

关于CLR头中的各个字段的解释见表1-1,后文会对PE文件中的节信息做简要介绍,关于PE文件的详细信息参看书后附录中的参考书籍。

表1-1  CLR头字段说明

偏移(offset

大小(size

字段(field

描述(description

0

4

Cb

头的长度(bytes)

4

2

MajorRuntimeVersion

CLR运行程序所必须的最小版本(Minimum Version)信息的主码(Major Number)

6

2

MinorRuntimeVersion

CLR运行程序所需要的版本信息中的次要编码(Minor Number)

8

8

MetaData

相对虚拟地址(RAV)和元数据的大小

16

4

Flags

二进制标志位组合,包含系统相关,程序调用等相关信息

20

4

EntryPointToken/EntryPointRVA

文件的入口点元数据标识符,对于DLL文件可以被设置为0

24

8

Resources

托管资源的大小和相对虚拟地址

32

8

StrongNameSignature

当前PE问件的hash数据的大小和相对偏移地址,被加载器用来绑定和版本验证

40

8

CodeManagerTable

Code Manager table的大小和相对偏移地址。目前为保留字段被设置为0

48

8

VTableFixups

一组V-Table的大小和相对虚拟地址信息

56

8

ExportAddressTableJumps

用于C++的输出跳转地址表的RVA和size,大多数情况为0

64

8

ManagedNativeHeader

为本地映像的保留字段,设置为0

       

下面通过ILDasm查看HelloWorld.exe的文件头信息。单击菜单“view-headers”,如图1-5所示。

 

托管PE文件

图1-5 查看文件头信息

头信息的主要代码如代码清单1-5所示。

代码清单1-5 HelloWorld.exe 头信息

----- DOS Header:



Magic:                      0x5a4d



Bytes on last page:         0x0090



......(省略)



File addr. of COFF header:  0x0080



----- COFF/PE Headers:



Signature:                  0x00004550



----- COFF Header:



Machine:                    0x014c



Number of sections:         0x0003



Time-date stamp:            0x4b1b1d3a



Ptr to symbol table:        0x00000000



Number of symbols:          0x00000000



Size of optional header:    0x00e0



Characteristics:            0x0102



----- PE Optional Header (32 bit):



Magic:                          0x010b



......(省略)



Directory:      



......(省略)



Table:     



0x00000000 [0x00000000] address [size] of Delay Load IAT:           



0x00002008 [0x00000048] address [size] of CLR Header:               



......(节信息,略)



 



Base Relocation Table



              0x00002000 Page RVA



              0x0000000c Block Size



              0x00000002 Number of Entries



              Entry 1: Type 0x3 Offset 0x000007a0



              Entry 2: Type 0x0 Offset 0x00000000



 



Import Address Table



     DLL : mscoree.dll



          ......(省略)



 



Delay Load Import Address Table



// No data.



 



Entry point code:



FF 25 00 20 40 00



 



 



----- CLR Header:



Header size:                        0x00000048



Major runtime version:              0x0002



Minor runtime version:              0x0005



......(省略)        



 



Metadata Header



    Storage Signature:



             ......(省略)



    Storage Header:



                    0x00 Flags



                  0x0005 Number of Streams



    Stream 1:



              0x0000006c Offset



              0x000001e8 Size



              '#~' Name



   ......(省略)



    Stream 5:



              0x00000510 Offset



              0x00000130 Size



              '#Blob' Name



 



    Metadata Stream Header:



              0x00000000 Reserved



                    0x02 Major



                    0x00 Minor



                    0x00 Heaps



                    0x01 Rid



      0x0000000900001547 MaskValid



      0x000016003325fa00 Sorted



Code Manager Table:



  default



Export Address Table Jumps:



// No data.

 

上面代码中涉及很多节信息,下面做简要论述。

1.       Relocation(重定位)

映像文件的.reloc节包括了Fixup表,它包含了映像文件中的所有定位项。.reloc节的RVA和大小都由PE头的基地址重定位(Base Relocation)表目录定义。Fixup表由定位块组成,每个块都包括了一个4KB页的定位。这些块都是4字节对齐的。

每个定位都描述了映像文件中特定地址的位置,以及操作系统加载程序在将映像文件载入内存的时候,应该如何修改这个位置上的地址。

每个定位块都开始于两个4字节无符号整数:页面的RVA,这个页面包含了需要定位的地址、块的大小。紧随其后的是页面的定位项每个项都是16位宽的,其中的4个最高权重位包括了所需要的重定位类型,剩下的12位包括了页面中重定位地址的偏移量。

为了对地址进行重定位,操作系统加载程序会计算出首选的基地址(PE头的ImageBase字段)和实际加载映像文件的基地址之间的差异(delta)。接着根据重定位的类型,将这个delta应用到地址上。如果在首选位置加载映像文件,就不需要进行定位。

说明 Windows XP或者更新的版本都是支持CLR的操作系统,既不需要CLR的启动Stub,也不需要IAT来调用CLR。因此,如果CLR头标志指出映像文件是纯IL(COMIMAGE_FLAGS_ ILONLY),那么操作系统就会完全地忽略.reloc节。

2.       Text(文本)

PE文件的.text节是只读节。在托管PE文件中,它包括了元数据表、IL代码、导入表、CLR头、CLR非托管启动Stub。在由IL汇编器生成的映像文件中,这个节还包括了托管资源、强签名的散列值、调试数据以及非托管导出Stub。所以.text节是托管PE文件对传统PE文件改变最多的地方。

图1-6总结了由IL汇编器生成的映像文件的.text节的通用结构。

托管PE文件

 

图1-6 .text节的通用结构

3.       Data(数据)

由IL汇编器生成的映像文件的数据节(.sdata)是可读写的节,它包括了数据常量、V表、非托管导出表以及TLS的目录结构。声明为特定于线程的数据位于一个不同的节,也就是.tls节。

4.       Data Constants(数据常量)

数据常量代表了静态字段的映射,通常包括映射字段的初始化数据。

字段映射是一种使用ANSI字符串、Blob或结构来初始化静态字段的方法。另一种初始化静态字段的方法(对于CLR来说更正式的方法)是通过在类的构造函数中显式地进行初始化。

一方面,映射到数据节的字段就像类型控制和垃圾收集那样,是CLR控制机制所触及不到的;另一方面,它是完全开放的,可以不受限制地访问和修改。这将导致加载程序阻止特定的字段类型被映射。映射字段的类型不能包括对象引用、向量、数组或任何非公共的子结构。如果为静态字段初始化使用类的构造函数,就不会出现这样的问题。

5.       V-Table(V表)

在纯粹的托管代码模块中,V表用于将托管方法公开给非托管代码来调用。V表由一些项组成,每个项又由一个或多个槽组成。V表的这些项和槽都定义在V表定位中。每个定位指定了每个项中槽的数量和宽度(4字节或8字节)。V表的每个槽都包含各个方法的元数据标记,这些元数据标记在运行期间将会替换成方法本身的地址或者封送thunk,用于提供方法的非托管入口。因为这些定位是在运行期间执行的,所以托管PE文件的V表必须驻留于可读写的节中。IL汇编器将这个V表放在了.sdata节中,不像VTFixup表是驻留于.text节中的。

非托管映像文件的V表完全在链接期间定义,并只需操作系统加载程序执行的基地址重定位。因为在执行期间不需要改变V表(例如把方法标记替换成托管映像中的地址),所以非托管映像文件可以把它们的V表放在只读节中。

6.       Unmanaged Export Table(非托管导出表)

在非托管映像文件中的非托管导出表占据一个单独的节——.edata。在IL汇编器生成的映像文件中,非托管导出表和它引用的V表都驻留于.sdata节中。

7.       Thread Local Storage(线程局部存储)

ILAsm和VC++允许用户定义属于TLS的数据常量,并将静态字段映射到这些数据常量上。TLS是一种特殊的存储类,类中的数据对象不是栈变量而是各个独立线程的局部变量。因此,每个线程都可以为这样的变量维护不同的值。

TLS数据在TLS目录中描述,IL汇编器将其放置于.sdata节中。32位映像文件的TLS目录结构定义在Winnt.h中,如代码清单1-6所示。

代码清单1-6 32位映像文件的TLS目录结构

typedef struct _IMAGE_TLS_DIRECTORY32 {



ULONG StartAddressOfRawData;



ULONG EndAddressOfRawData;



ULONG AddressOfIndex;



ULONG AddressOfCallBacks;



ULONG SizeOfZeroFill;



ULONG Characteristics;



} IMAGE_TLS_DIRECTORY32;

 

64位映像(IMAGE_TLS_DIRECTORY64)的TLS目录结构是类似的,除了开头的4个字段是8字节无符号整数(ULONGLONG),而不是4字节无符号整数(ULONG)。

TLS目录结构的RVA和大小存储在PE头的第10个数据目录(TLS)中。构成了TLS模板的TLS数据常量,驻留于映像文件的.tls节中。

8.       Resources(资源)

在托管PE文件中可以嵌入两种不同的资源:特定于平台的非托管资源和特定于CLR的托管资源。它们驻留于托管映像文件的不同节,并通过不同的API进行访问。

(1)       Unmanaged Resources(非托管资源)

非托管资源在PE文件的.rsrc节中。嵌入的非托管资源的起始RVA和大小都在PE头的资源数据目录中表示。

非托管资源由类型、名称和语言进行索引,并根据这三个特征的顺序进行二进制排序。

IL汇编器创建.rsrc节,并且会嵌入命令行选项指定的.res文件中的非托管资源。编译器只能为每个模块嵌入一个非托管资源文件。

当IL反汇编器分析托管PE文件并找到.rsrc节的时候,它会从这个节中读取数据和结构,并流并释放出包括在PE文件中所有非托管资源的.res文件。

(2)       Managed Resources(托管资源)

CLR头的Resource字段包括了内嵌在PE文件中的托管资源的RVA和大小。它与PE头的Resource目录无关,后者指定了特定于平台的非托管资源的RVA和大小。

在IL汇编器生成的PE文件中,非托管资源驻留于映像文件的.rsrc节中,而托管资源和元数据、IL代码等都位于.text节中。托管资源在.text节中连续地存放。元数据携带着ManifestResource记录,每一笔记录对应着一个托管资源,包括了托管资源的名称以及从CLR头的Resources字段中指定的起始RVA算起的资源开始处的偏移量。在这个偏移位置上,会使用4字节无符号整数指出资源的字节长度。紧跟其后的是资源本身。

当IL反汇编器处理托管映像文件并找到嵌入的托管资源时,它会将每个资源各自写到根据资源名称命名的单独文件中。

当IL汇编器创建PE文件时,它会根据资源名称读取在源代码中定义为嵌入资源的所有托管资源,将它们写到.text节中,并在每个资源的前面放置该资源的指定长度。

-----------------------注:本文改编自《.net 安全揭秘》1.3节

 

 

你可能感兴趣的:(文件)