zoukankan      html  css  js  c++  java
  • (翻译)《Metadata Tables》第一章 PE文件格式

    这本书很复杂,其精华在于探讨.NET世界中的可执行文件,而这些文件可以由任意一种编译器产品所生成。本书的核心语言是C#。因此,读者应该非常熟悉这门语言。

    在探究元数据这一重要概念之前,我们需要理解PE(Portable Executable)文件格式。元数据是PE文件中必不可少的一部分。PE文件是和.NET底层架构打交道的。

    我们不会马上讨论元数据,因为本章主要关注于PE文件格式的“内幕”。

    作为开始,让我们在驱动器中创建一个名为mdata的目录。并在其中创建一个名为b.cs的文件,它具有以下内容:

    b.cs

    public class zzz
    {
        public static void Main()
        {
            System.Console.WriteLine("hello");
        }
    }
     

    >csc b.cs

    当使用csc命令编译文件b.cs时,会生成一个名为b.exe的可执行文件。这里我们重点关注于组成这个可执行文件的字节。为了看清这个文件,还需要另一个名为a.cs的程序,它“溜进”该文件并在不同的区域生成输出。

    a.cs

    using System;
    using System.IO;
    
    public class zzz
    {
        public static void Main()
        {
            zzz a = new zzz();
            a.abc();
        }
    
        public void abc()
        {
            FileStream s = new FileStream("C:\\mdata\\b.exe", FileMode.Open);
            BinaryReader r = new BinaryReader(s);
    
            byte a, b;
            a = r.ReadByte();
            b = r.ReadByte();
    
            Console.WriteLine("{0}{1}", (char)a, (char)b);
        }
    }
     

    Output

    MZ

    我们运行由编译器生成的这个exe文件,就会显示输出:MZ。下列条款详细说明了这是如何以及为什么发生的。因此,让我们继续冒险之旅,并发掘出这个exe文件的复杂特征。

    .NET世界以类的形式提供框架代码。因此,在文件处理的一组类中,存在一个FileStream类,用于处理文件。这个类的构造函数具有两个参数:第一个是要处理的文件名,这里的名称是b.exe;第二个参数是一个枚举,用来通知构造函数将要在该文件上执行的操作。

    例如,它传输了该文件在被打开时是可读的还是可写的,或者全部,或者用于添加新内容,等等。由于我们的意图是读取这个文件,所以会使用枚举FileMode的Open成员。

    一旦文件被打开,我们将要读取这个文件。可能会有影响的是一次读1个字节还是几个字节(如4戈字节)。然而,在FileStream类中没有功能来扩展这种弹性。因此,我们使用另一个类,如BinaryReader类,它适合于每次读取一个字节,或一个短整数字节。

    在最基本的级别上,一个文件仅包括0-255范围内的数字。因此,一个字节足够用于存储一个值了。使用BinaryReader类的ReadByte方法打印这些值,变量会被转换为char,因此通常显示等价的ASCII而不是数字。

    在Microsoft世界中的任何文件的开始两个字节,都是M和Z。这两个字节的存在是有历史意义的。在我们使用Windows之前,微软的第一个操作系统命名为DOS。它实际上被称为QDOS(Quick and Dirty Operating System)。DOS下的每个文件的“头”都开始于一系列字节,描述了存储在文件剩余部分中的内容。

    使用某种形式的签名来决定文件的类型——已经广泛得到了重视。因此,设计DOS内存管理系统的人决定把他名字的首字母MZ作为文件的“魔数”(magic number)。对于Java语言,在相同的行中,每个文件的开始是“CAFÉ BABE”这样的“魔数”——用以向那些在工作到深夜的程序员提供咖啡的服务员表达致敬。设计元数据的人也采纳了这个概念——他们显然不想落在后面。

    你肯定想知道为什么当我们实际上应该工作在Window系统时,却还对DOS“情有独钟”!

    默许的事实是,Windows下的每个可执行程序最终归结为一个DOS程序。它带有和DOS程序相同的头:

    我们下载并安装UltraEdit-32,用来观察在十六进制模式下的文件内容。当打开文件b.exe时,在这个程序中,我们看到屏幕上显示十六进制数字,以及和这些数字等价的字符。在截图1.1中,我为你展示了这个文件开始的一些字节,它清楚地描述了文件中的开始字符是MZ。

    随着Windows OS的发布,微软引进了一种新的文件格式——PE(Portable Executable)文件格式,它从根本上不同于在DOS下使用的文件格式。由于微软不确定Windows作为操作系统是否能被广泛接受,所以他们保留了DOS下的文件格式而未作修改。这就导致两种文件格式的存在。那时没人能预见Windows后来会成为全世界最优秀的操作系统。

    用户在DOS环境下运行Windows PE文件——有潜在的可能性。微软认识到,在这样的环境下,显示讨厌的错误信息会使用户对在Windows操作系统上工作产生排斥。因此,这就要求每个PE文件都应该是一个有效的DOS程序。然而,当在DOS下执行这样的一个程序时,它会在退出之前向用户显示一个友好的信息,其中,用户会被通知——这个有效的程序实际上是一个Windows程序。

    随之产生的一个问题是,这种关注只是对DOS用户而言的,忽略了时至今日世界上还有少数DOS程序员的事实。

    a.cs

    using System;
    using System.IO;
    
    public class zzz
    {
        public static void Main()
        {
            zzz a = new zzz();
            a.abc();
        }
    
        public void abc()
        {
            FileStream s = new FileStream("C:\\mdata\\b.exe", FileMode.Open);
            BinaryReader r = new BinaryReader(s);
    
            Console.WriteLine(s.Position);
    
            s.Seek(60, SeekOrigin.Begin);
            Console.WriteLine(s.Position);
    
            int i = r.ReadInt32();
            Console.WriteLine(i);
            Console.WriteLine("0x" + i.ToString("X"));
            Console.WriteLine(s.Position);
        }
    } 
     

    Output

    0

    60

    128

    0x80

    64

    在文件中,Position属性显示了当前位置。新近打开的文件位置为0,这意味着它定位在文件的开始位置。为了跳到文件的其它任何位置,必须使用FileStream对象的Seek方法。

    这个方法有2个参数:

    l 第一个参数是一个数字或一个偏移量,它是由第二个参数决定的一个位置。

    l 第二个参数值必须是枚举SeekOrigin的一个成员。

    在这个枚举对象中的Begin成员,指向了从文件开始位置所移动的距离数。从而,s.Seek(60, SeekOrigin.Begin); 意味着从文件的开始位置移动60字节。

    这个枚举的其它两个值分别是:End——表示文件的结尾;Current——表示当前位置。第一个参数是由该枚举成员决定的偏移位置。如果枚举值为Current,这个值可以是正的,也可以是负的。

    这里,Seek方法定位到第60个位置上。从技术术语上讲,这个Seek方法定位了文件指针在这个文件的第60个字节上。可以由Position属性来证实。

    最后的Writeline方法还证实了这样的事实:在读取文件的字节后,文件指针相应地向前移动了。

    我们不会考虑DOS的其它类型,因为它们适用于DOS而不适用于Windows。

    a.cs

    using System;
    using System.IO;
    
    public class zzz
    {
        public static void Main()
        {
            zzz a = new zzz();
            a.abc();
        }
    
        public void abc()
        {
            FileStream s = new FileStream("C:\\mdata\\b.exe", FileMode.Open);
            BinaryReader r = new BinaryReader(s);
    
            s.Seek(60, SeekOrigin.Begin);
    
            int i = r.ReadInt32();
            s.Seek(i, SeekOrigin.Begin);
    
            byte a, b, c, d;
            a = r.ReadByte();
            b = r.ReadByte();
            c = r.ReadByte();
            d = r.ReadByte();
    
            Console.WriteLine("{0}{1} {2} {3}", (char)a, (char)b, c, d);
        }
    }
     

     

    Output

    PE 0 0 

    在跳转到这个文件的第128字节(0x80)之后,程序分别读取了接下来的4个字节并把它们存储到字节变量中。

    从这个位置提取出来的值是PE 00,这是PE文件的魔数或签名。如果魔数中的任何一个字符发生改变,都会导致操作系统生成一个错误。

    a.cs

    using System;
    using System.IO;
    
    public class zzz
    {
        public static void Main()
        {
            zzz a = new zzz();
            a.abc();
        }
    
        public void abc()
        {
            FileStream s = new FileStream("C:\\mdata\\b.exe", FileMode.Open);
            BinaryReader r = new BinaryReader(s);
    
            s.Seek(128 + 4, SeekOrigin.Begin);
    
            short machine = r.ReadInt16();
            Console.WriteLine("Machine {0}", machine.ToString("X"));
    
            short sections = r.ReadInt16();
            Console.WriteLine("Sections {0}", sections);
    
            int time = r.ReadInt32();
            Console.WriteLine("Date Time Stamp {0}", time.ToString("X"));
    
            int pointer = r.ReadInt32();
            Console.WriteLine("Pointer {0}", pointer.ToString("X"));
    
            int symbols = r.ReadInt32();
            Console.WriteLine("Symbols {0}", symbols.ToString("X"));
    
            int headersize = r.ReadInt16();
            Console.WriteLine("Size of Optional Header {0}", headersize);
    
            int characteristics = r.ReadInt16();
            Console.WriteLine("Characteristics {0}", characteristics.ToString("X"));
        }
    }
     

    Output

    Machine 14C

    Sections 3

    Date Time Stamp 3C82927f

    Pointer 0

    Symbols 0

    Size of Optional Header 224

    Characteristics 10E 

    这个程序展示了PE头,它位于第128字节上,紧随PE签名之后。微软承诺——在不同处理器芯片上运行的任意Windows操作系统下的PE文件,都是保持一致的。因此,它们在可执行文件头的每个字节都遵从于文档。这为我们确定PE头是由什么组成的提供了精确的方法。

    第一个短整数是由2个字节组成的,涉及到机器或处理器。值0x014C表示Intel家族。其它有效值是0x162(用于MIPS R3000)、0x166(用于MIPS R4000)和0x183(用于DEC Alpha AXP)。64位Intel处理器的值是0x200。

    可执行体存储了不同的实体。3种这样的实体是来自我们程序的全局数据、实际代码和资源(如菜单、图形文件等等)。PE文件分配不同区域到上面独立的区域中。分配给这些实体的各种定位,在术语上称为节(section)。机器类型紧随一个短数据类型之后,它包括了文件包含的节的数量

    紧随其后的是PE文件创建的日期和时间。这个数字存储为一个长整数,它包括了自1970年1月1日格林威治时间(GMT)起流逝的秒数。这里有丰富的函数,用以将上面的数字转换为人们看得懂的日期。

    PE文件格式还包括了OBJ,这是由C/C++编译器创建的对象文件的摘要。Obj文件通常包括函数,在技术行话上,它们被认为是符号(symbol)。然而,由于我们在这一阶段正在处理一个exe文件,所以符号信息已经被清零了(zero out)。

    在这个结构或头之后,是另一个头——被称为映像可选头。它的大小,对于32位文件是224字节,对于64位文件则是240字节。这个值存储在PE头中符号的数量之后。对于不同文档,这个值可能会改变,但是我们从未遇到过带有超过224字节大小的可选头的PE文件。

    PE头的最后一部分是被称为特征(characteristic)的字段。这个值使用ToString函数以十六进制格式来表示。我们将在短暂轻松之后阐明这些特征。

    and位运算

    a.cs

    using System;
    
    public class zzz
    {
        public static void Main()
        {
            Console.Write(7 & 0x0a);
        }
    }
     

    Output

    2

    AND操作符(&)需要两个被比较的值在逻辑上是true,从而结果可能是true。这里,代替这些值,这些被检查的实体是每个在字节上都独立的位。

    对于值7,开始的3个字节是1,而对于字母A,第2位和第4位是1,也就是说,这些位被设置为on。

    通过对7和0x0a进行AND位运算,只有第2位被转换为on。就是说,结果为2。

    下一个程序广泛地使用了AND操作,来解释包含在PE头中特征字段的值。

    a.cs

    using System;
    using System.IO;
    
    public class zzz
    {
        public static void Main()
        {
            zzz a = new zzz();
            a.abc();
        }
    
        public void abc()
        {
            FileStream s = new FileStream("C:\\mdata\\b.exe", FileMode.Open);
            BinaryReader r = new BinaryReader(s);
    
            s.Seek(128 + 4, SeekOrigin.Begin);
            s.Seek(2 + 2 + 4 + 4 + 4 + 2, SeekOrigin.Current);
    
            int characteristics = r.ReadInt16();
            Console.WriteLine("Characteristics {0}", characteristics.ToString("X"));
    
            int i = characteristics & 0x001;
            if (i == 1)
                Console.WriteLine("Relocs Stripped {0}", i);
    
            if ((characteristics & 0x002) == 2)
                Console.Write("Executable Image ");
    
            if ((characteristics & 0x004) == 0x004)
                Console.Write("Line Numbers Stripped ");
    
            if ((characteristics & 0x008) == 0x008)
                Console.Write("Local Symbols Stripped ");
    
            if ((characteristics & 0x010) == 0x010)
                Console.Write("Trim Local Set ");
    
            if ((characteristics & 0x020) == 0x020)
                Console.Write("Can Handle Address Larger than 2Gb ");
    
            if ((characteristics & 0x080) == 0x080)
                Console.Write("Bytes Reversed ");
    
            if ((characteristics & 0x0100) == 0x0100)
                Console.Write("32 Bit Machine ");
    
            if ((characteristics & 0x0200) == 0x0200)
                Console.Write("Debugging Info Stripped ");
    
            if ((characteristics & 0x0400) == 0x0400)
                Console.Write("Removable Media Swap ");
    
            if ((characteristics & 0x0800) == 0x0800)
                Console.Write("Net Swap ");
    
            if ((characteristics & 0x1000) == 0x1000)
                Console.Write("System File ");
    
            if ((characteristics & 0x2000) == 0x2000)
                Console.Write("Dll ");
    
            if ((characteristics & 0x4000) == 0x4000)
                Console.Write("Uni-Processor Only ");
    
            if ((characteristics & 0x8000) == 0x8000)
                Console.Write("High Bytes Reversed");
        }
    }
     

     

    Output

    Characteristics 10E

    Executable Image Line Numbers Stripped Local Symbols Stripped 32 Bit Machine

    在上面的例子中,我们对大部分值进行了硬编码。由于我们已经“发掘出”DOS头和PE签名的定位,我们将不会再对其进行编码。使用常量值作为代替,我们将直接调转到对我们而言极其重要的位置。

    在到达PE头之前,我们向前移动几个字节以到达特征字段上。这个字段是16字节宽并表示文件的本性。文件可以是具有.exe扩展名的可执行文件,或者具有.dll扩展名的库文件。

    为了以更迅速有效的方式访问信息,我们统一使用了位运算AND。这里,特征字段的每个位都表示一个单独的属性。通过检查哪些位是on,可以确定与文件相关的这些属性。

    在位运算中,当我们对一个位进行和1的AND运算时,我们会得到这个原始的位。然而,但我们对一个位进行和0的AND运算时,结果为0。因此,为了检查一个确定的位是否为on,通过和1个与这个位相称的数字执行位运算AND操作。所有其它位都设置为off。如果最终结果为0,那么表示原始位是off。如果结果和进行AND位运算的数字相同,就表示原始位是on。

    特征字段是通过和一个值进行位运算,来确定这个位是否为on。如果结果返回1,那么它表示这个位是on。

    因此,在if语句中,执行检查来决定变量i的值。如果它是1,那么它就假设这个特定的属性是存在的。

    在随后的例子中,我们避开使用变量i,而是直接在if语句中使用表达式。这里,括号是必须的,因为&操作符比==操作符的优先级低。

    现在让我们逐个分析这些位。

    特征字段中的第一个位与重定位有关。下一个位表示该文件是否为一个可执行文件。由于这个位是on,所以这个文件是一个可执行文件。

    下一个位检查行号是否从文件中脱离出来。如果是,这就会缩减文件的大小。这个位主要用于调试目的。由于这个值是on,所以它证实了行号已经从文件中脱离。本地符号也是脱离于文件的。

    工作集并不是主动脱离与文件的。

    这个文件是不能处理超过2G字节的地址或内存定位的内存。

    字节在存储时是可以被反转的。这就解决了Little endian和Big endian的问题,其中这就决定了低位字节存储在第一或第二个位置上.

    下一个位决定了文件是否创建在一个32位的机器上。后面的位检查调试信息的存在。这些调试信息增加文件的大小。然而,我们的文件中并没有存储任何调试信息。

    下面两个位表示exe文件是否位于可移动媒质或网络上。如果它在位络上,它就应该是从媒质上复制出来的,并在交换文件上执行。

    接下来的两个位表示这个文件是一个系统文件或一个DLL。这是exe文件和dll文件之间的唯一区别。

    紧接着是一个标志(flag),它表示文件应该运行在单处理器或多处理器的机器上。我们没有强加任何这样的约束。最后一个位是字节反转位。

    按照在Partition II 24.2.2.1中的规范,一个.NET文件要设置3个标志为on,这就暗示了行号、本地符号和调试信息都是与文件相脱离的。可以在ECMA站点上找到这些规范的有效的PDF文件。

    a.cs

    using System;
    using System.IO;
    
    public class zzz
    {
        public static void Main()
        {
            zzz a = new zzz();
            a.abc();
        }
    
        public void abc()
        {
            FileStream s = new FileStream("C:\\mdata\\b.exe", FileMode.Open);
            BinaryReader r = new BinaryReader(s);
    
            s.Seek(128 + 4 + 20, SeekOrigin.Begin);
    
            int magic = r.ReadInt16();
            Console.WriteLine("Magic Number {0}", magic.ToString("X"));
    
            int major = r.ReadByte();
            Console.WriteLine("Major Linker Version {0}", major);
    
            int minor = r.ReadByte();
            Console.WriteLine("Minor Linker Version {0}", minor);
    
            int sizeofcode = r.ReadInt32();
            Console.WriteLine("Size of code {0}", sizeofcode);
    
            int sizeofdata = r.ReadInt32();
            Console.WriteLine("Size of Data {0}", sizeofdata);
    
            int sizeofudata = r.ReadInt32();
            Console.WriteLine("Size of Data {0}", sizeofudata);
    
            int entrypoint = r.ReadInt32();
            Console.WriteLine("Memory Address {0}", entrypoint.ToString("X"));
    
            int baseofcode = r.ReadInt32();
            Console.WriteLine("Base of Code {0}", baseofcode.ToString("X"));
    
            int baseofdata = r.ReadInt32();
            Console.WriteLine("Base of Data {0}", baseofdata.ToString("X"));
    
            int ImageBase = r.ReadInt32();
            Console.WriteLine("Image base {0}", ImageBase.ToString("X"));
    
            int sectiona = r.ReadInt32();
            Console.WriteLine("Section Alignment {0}", sectiona.ToString("X"));
    
            int filea = r.ReadInt32();
            Console.WriteLine("File Alignment {0}", filea.ToString("X"));
    
            int majoros = r.ReadInt16();
            Console.WriteLine("Major Operating System Version {0}", majoros.ToString("X"));
    
            int minoros = r.ReadInt16();
            Console.WriteLine("Minor Operating System Version {0}", minoros.ToString("X"));
    
            int majorimage = r.ReadInt16();
            Console.WriteLine("Major Image Version {0}", majorimage.ToString("X"));
    
            int minorimage = r.ReadInt16();
            Console.WriteLine("Minor Image Version {0}", minorimage.ToString("X"));
    
            int majorsubsystem = r.ReadInt16();
            Console.WriteLine("Major Subsystem Version {0}", majorsubsystem.ToString("X"));
    
            int minorsubsystem = r.ReadInt16();
            Console.WriteLine("Minor Subsystem Version {0}", minorsubsystem.ToString("X"));
    
            int verison = r.ReadInt32();
            Console.WriteLine("Version {0}", verison.ToString("X"));
    
            int imagesize = r.ReadInt32();
            Console.WriteLine("Image Size {0}", imagesize);
    
            int sizeofheaders = r.ReadInt32();
            Console.WriteLine("Size of Headers {0}", sizeofheaders);
    
            int checksum = r.ReadInt32();
            Console.WriteLine("CheckSum {0}", checksum);
    
            int subsystem = r.ReadInt16();
            Console.WriteLine("Subsystem {0}", subsystem);
    
            int dllflags = r.ReadInt16();
            Console.WriteLine("Dll flags {0}", dllflags);
    
            int stackreserve = r.ReadInt32();
            Console.WriteLine("Stack Reserve {0}", stackreserve.ToString("X"));
    
            int stackcommit = r.ReadInt32();
            Console.WriteLine("Stack Commit {0}", stackcommit.ToString("X"));
    
            int heapreserve = r.ReadInt32();
            Console.WriteLine("Heap Reserve {0}", heapreserve.ToString("X"));
    
            int heapcommit = r.ReadInt32();
            Console.WriteLine("Heap Commit {0}", heapcommit.ToString("X"));
    
            int loader = r.ReadInt32();
            Console.WriteLine("Loader flags {0}", loader.ToString("X"));
    
            int datad = r.ReadInt32();
            Console.WriteLine("Number of Data Directories {0}", datad);
        }
    }
     

    Output

    Magic Number 10B

    Major Linker Verison 6

    Minor Linker Verison 0

    Size of code 1024

    Size of Data 1536

    Size of Data 0

    Memory Address 22BE

    Base of Code 2000

    Base of Data 4000

    Image base 400000

    Section Alignment 2000

    File Alignment 200

    Major Operating System Version 4

    Minor Operating System Version 0

    Major Image Version 0

    Minor Image Version 0

    Major Subsystem Version 4

    Minor Subsystem Version 0

    Version 0

    Image Size 32768

    Size of Headers 512

    CheckSum 0

    Subsystem 3

    Dll flags 0

    Stack Reserve 100000

    Stack Commit 1000

    Heap Reserve 100000

    Heap Commit 1000

    Loader flags 0

    Number of Data Directories 16 

    距离PE头20字节的尾部是映像可选头,它是224字节大小。这个头只有在obj或lib文件的情形中是可选的。

    开始的两个字节是魔数。10B表示一个32位的头,而20B表示一个364位的头,后者是今后的趋势。

    在.NET早期,编译器和链接器(linker)是独立的产品。链接器使用不同的编程语言,而编译器则是独立于语言的。然而,.NET世界合并了映像可选头的下面两个字节,用来包括版本号。更早些时候,则存储创建这个文件的产品的主版本和次版本。这个编号通常表示创建这个文件的Visual Studio版本。

    下一个整数指定了代码字段的大小。这个字段,正如名称建议的那样,指定了出现在所有节中的代码大小。

    紧接其后的是分别包括初始化和未初始化数据大小的2个字段。在大多数情形中,未初始化数据的大小为0。

    下一个整数表示在运行时包括可执行代码的第一个字节的内存定位。我们称之为进入点的地址。在dll文件的情形中,它的值通常为0。

    下一个整数指向内存的定位,其中携带代码的节将会被加载。随后的内存定位是数据节。你可能注意到代码节开始于文件被加载位置的0x2000字节处,而数据节则开始于被加载位置的0x4000字节处。

    Image Base存储了PE文件在内存中被加载的位置。所有的PE文件都符合这样的标准——它们都在0x400000内存定位上被加载。

    Section Alignment指的是在一个节中包括的字节数量。这里Section Alignment被设置为0x2000。这暗示了代码节开始于0x2000,而下一个节开始于0x4000。

    File Alignment指的是文件中每个节的开始位置。它被设置为512字节或0x200字节。从而,第一个节起始于开始位置的512字节处,第二个节起始于1024字节处,以此类推。

    当一个可执行文件被加载到内存时,文件中的起始头字节位于Image Base 0x400000位置。此后, 开始于磁盘512字节边界上的代码节被加载到与Image Base 相距0x2000的位置上。磁盘上的下一个512字节边界是数据节,被加载到与Image Base 相距0x4000的位置上。

    在Alignment之后是操作系统的主版本号和次版本号。因为Windows的版本常常发生改变,所以这两个版本号已经不再使用了。下面两个短整数是映像文件的主版本号和次版本号,它们是由链接器设置的。这些字节在.NET文件中是没什么用的。

    下面两个字(word)是运行PE文件所必需的子系统或操作系统的主版本号和次版本号。.这些值也没什么用。另一个没有用的字段是Win32的版本号,它总是0。

    接下来是映像大小的字段,它指定了操作系统在内存中存储整个映像所需要的内存数量。Size of headers表示所有头的大小,例如DOS头、可选头,这些内容我们将不会涉及到。它的值是多个File Alignment的总和,例如512字节。

    CheckSun是用来验证文件是完好的或损坏的。虽然PE文件为此预留了一个字段,但是却没有使用,因此,它的值是0。

    Subsystem字段表示Windows所需要的用户界面类型。值为3表示程序是一个Windows GUI。其它可能的值是Console或Native。由于文件不可能是一个DLL,所以DLL标记字段的值是0。

    Stack Reserve字段的大小,决定了线程可以使用的栈区(Stack Area)。通常,这个值是1MB。然而,应用程序一开始不会分配相同的区间大小。Stack Commit是栈在一开始被分配的内存数量。

    栈是在方法中所有创建的变量存储的地方,而堆是用于存储实例变量的。因此,接下来两个字段,决定了堆区(Heap Area)的数量。这个值和用于栈的值是相同的。在此之后是Loader flags字段,它也是至今没有用到的。

    最后一个字段——在96字节的文件内容之后——是文件中Data Directories的数量。这里一共有16个Data Directory。这些Data Directories的详细内容在下面的程序中高亮显示。

    a.cs

    using System;
    using System.IO;
    
    public class zzz
    {
        public static void Main()
        {
            zzz a = new zzz();
            a.abc();
        }
    
        public void abc()
        {
            FileStream s = new FileStream("C:\\mdata\\b.exe", FileMode.Open);
            BinaryReader r = new BinaryReader(s);
    
            s.Seek(128 + 4 + 20 + 96, SeekOrigin.Begin);
    
            int rva, size;
    
            rva = r.ReadInt32();
            size = r.ReadInt32();
            Console.WriteLine("Export Table RVA={0} Size={1}", rva.ToString("X"), size.ToString("X"));
    
            rva = r.ReadInt32();
            size = r.ReadInt32();
            Console.WriteLine("Import Table RVA={0} Size={1}", rva.ToString("X"), size.ToString("X"));
    
            rva = r.ReadInt32();
            size = r.ReadInt32();
            Console.WriteLine("Win32 Resource Table RVA={0} Size={1}", rva.ToString("X"), size.ToString("X"));
    
            rva = r.ReadInt32();
            size = r.ReadInt32();
            Console.WriteLine("Exception Table RVA={0} Size={1}", rva.ToString("X"), size.ToString("X"));
    
            rva = r.ReadInt32();
            size = r.ReadInt32();
            Console.WriteLine("Certificate Table RVA={0} Size={1}", rva.ToString("X"), size.ToString("X"));
    
            rva = r.ReadInt32();
            size = r.ReadInt32();
            Console.WriteLine("Base Relocation Table RVA={0} Size={1}", rva.ToString("X"), size.ToString("X"));
    
            rva = r.ReadInt32();
            size = r.ReadInt32();
            Console.WriteLine("Debug Table RVA={0} Size={1}", rva.ToString("X"), size.ToString("X"));
    
            rva = r.ReadInt32();
            size = r.ReadInt32();
            Console.WriteLine("Copyright Table RVA={0} Size={1}", rva.ToString("X"), size.ToString("X"));
    
            rva = r.ReadInt32();
            size = r.ReadInt32();
            Console.WriteLine("Mips Global Ptr RVA={0} Size={1}", rva.ToString("X"), size.ToString("X"));
    
            rva = r.ReadInt32();
            size = r.ReadInt32();
            Console.WriteLine("TLS RVA={0} Size={1}", rva.ToString("X"), size.ToString("X"));
    
            rva = r.ReadInt32();
            size = r.ReadInt32();
            Console.WriteLine("Load Config RVA={0} Size={1}", rva.ToString("X"), size.ToString("X"));
    
            rva = r.ReadInt32();
            size = r.ReadInt32();
            Console.WriteLine("Bound Import RVA={0} Size={1}", rva.ToString("X"), size.ToString("X"));
    
            rva = r.ReadInt32();
            size = r.ReadInt32();
            Console.WriteLine("IAT RVA={0} Size={1}", rva.ToString("X"), size.ToString("X"));
    
            rva = r.ReadInt32();
            size = r.ReadInt32();
            Console.WriteLine("Delay Import Descriptor RVA={0} Size={1}", rva.ToString("X"), size.ToString("X"));
    
            rva = r.ReadInt32();
            size = r.ReadInt32();
            Console.WriteLine("CLR Header RVA={0} Size={1}", rva.ToString("X"), size.ToString("X"));
    
            rva = r.ReadInt32();
            size = r.ReadInt32();
            Console.WriteLine("Reserved RVA={0} Size={1}", rva.ToString("X"), size.ToString("X"));
        }
    }
     

    Output

    Export Table RVA=0 Size=0

    Import Table RVA=226C Size=4F

    Win32 Resource Table RVA=4000 Size=318

    Exception Table RVA=0 Size=0

    Certificate Table RVA=0 Size=0

    Base Relocation Table RVA=6000 Size=C

    Debug Table RVA=0 Size=0

    Copyright Table RVA=0 Size=0

    Mips Global Ptr RVA=0 Size=0

    TLS RVA=0 Size=0

    Load Config RVA=0 Size=0

    Bound Import RVA=0 Size=0

    IAT RVA=2000 Size=8

    Delay Import Descriptor RVA=0 Size=0

    CLR Header RVA=2008 Size=48

    Reserved RVA=0 Size=0

    可选头还包括16个数据目录(data directory)结构,每一个都是8字节大小。这些结构包括PE文件的特定区域的重要信息。结构中的8位由2个整数组成,一个被称为RVA(Relative Virtual Address),另一个被称为Size。

    上面的程序展示了16个结构。其中一个结构是CLR头。CLR是公共语言运行时的简称。RVA值显示为2008,而Size给定为0x48。这就表示当b.exe执行时,CLR头将在距离Image Base 0x4000000的0x2008内存定位上;换句话说,它将被定位在0x4002008的内存定位上。

    内存中的节对齐是0x2000,而CLR头的RVA是2008,从2008中减去2000,差为8。因此,CLR头位于距离节开始处8字节的位置。

    磁盘上的文件是512字节对齐的。因此,第一个节将开始于文件起始512字节的位置。而CLR是距离这个节头8字节的,512加上8,(磁盘上的文件的节的起始),从而位于520的位置上。下面72个字节(0x48)就是从这个位置获得的,因为它们组成了CLR头,并且它们是在0x4002008的位置上被加载的。

    让我们简单说明一下上面展示的数据目录。

    PE文件允许其它PE文件调用它的函数,只要这些函数被标记为Exports。可执行体从其它DLL或EXE文件中调用代码或导入代码,也是如此(标记为Import)。

    开始的两个表列举了这些导入和导出表。数据目录中的第三个表指向驻留在PE文件中的资源。下一个表指向由异常组成的表——所有的CPU(不包括486),都合并成这样一个表。接下来是Certificate表,它不是一个RVA,而是文件的偏移量。Relocation是这样一种方法——可以将PE文件被加载到内存中的任何地方。

    接下来是Debug目录和Copyright目录。在某些情形中,这是特定于架构的数据。Global Ptr表只用于64位机器。线程使用TLS(Thread Local Storage)初始化节。Load Config只用于Windows NT、Windows 2000和Windows XP。

    Bound Import包括了PE文件绑定到的Dll文件的详细信息。

    Import Address表(IAT)指向第一个Import Address表的方向并处理Dll。Delay Loading Dll是由用于运行时库的链接器实现的,操作系统不认识它。对我们而言,CLR头或COM描述符是最重要的表,因为它指向了第一个.NET头。

    下面的程序主要关注于CLR结构。

    a.cs

    using System;
    using System.IO;
    
    public class zzz
    {
        public static void Main()
        {
            zzz a = new zzz();
            a.abc();
        }
    
        public void abc()
        {
            FileStream s = new FileStream("C:\\mdata\\b.exe", FileMode.Open);
            BinaryReader r = new BinaryReader(s);
    
            s.Seek(128 + 4 + 20 + 96 + 112, SeekOrigin.Begin);
    
            int rva, size;
            rva = r.ReadInt32();
            size = r.ReadInt32();
    
            int where = rva % 0x2000 + 512;
            Console.WriteLine(where);
    
            s.Seek(where, SeekOrigin.Begin);
            size = r.ReadInt32();
            Console.WriteLine("CLR Header size {0}", size);
    
            int majorruntimeversion;
            majorruntimeversion = r.ReadInt16();
            Console.WriteLine("Major Runtime Version {0}", majorruntimeversion);
    
            int minorruntimeversion;
            minorruntimeversion = r.ReadInt16();
            Console.WriteLine("Minor Runtime Version {0}", minorruntimeversion);
    
            rva = r.ReadInt32();
            size = r.ReadInt32();
            Console.WriteLine("MetaData RVA={0} Size={1}", rva.ToString("X"), size.ToString("X"));
    
            int flags = r.ReadInt32();
            Console.Write("Flags ");
    
            if ((flags & 0x01) == 0x01)
                Console.Write("ILONLY ");
    
            if ((flags & 0x02) == 0x02)
                Console.Write("32 Bit Required ");
    
            if ((flags & 0x08) == 0x08)
                Console.Write("Strong Name Signature ");
    
            if ((flags & 0x010000) == 0x010000)
                Console.Write("Track Debug Data ");
    
            Console.WriteLine();
    
            int entrypointtoken = r.ReadInt32();
            Console.WriteLine("Entry Point Token {0}", entrypointtoken.ToString("X"));
    
            rva = r.ReadInt32();
            size = r.ReadInt32();
            Console.WriteLine("Resources RVA={0} Size={1}", rva.ToString("X"), size.ToString("X"));
    
            rva = r.ReadInt32();
            size = r.ReadInt32();
            Console.WriteLine("Strong Name Signature RVA={0} Size={1}", rva.ToString("X"), size.ToString("X"));
    
            rva = r.ReadInt32();
            size = r.ReadInt32();
            Console.WriteLine("Code Manager Table RVA={0} Size={1}", rva.ToString("X"), size.ToString("X"));
    
            rva = r.ReadInt32();
            size = r.ReadInt32();
            Console.WriteLine("VTable Fixups RVA={0} Size={1}", rva.ToString("X"), size.ToString("X"));
    
            rva = r.ReadInt32();
            size = r.ReadInt32();
            Console.WriteLine("Export Address Table Jumps RVA={0} Size={1}", rva.ToString("X"), size.ToString("X"));
    
            rva = r.ReadInt32();
            size = r.ReadInt32();
            Console.WriteLine("Managed Native Header RVA={0} Size={1}", rva.ToString("X"), size.ToString("X"));
        }
    }
     

    Output

    520

    CLR Header size 72

    Major Runtime Version 2

    Minor Runtime Version 0

    MetaData RVA=207C Size=1F0

    Flags ILONLY

    Entry Point Token 6000001

    Resources RVA=0 Size=0

    Strong Name Signature RVA=0 Size=0

    Code Manager Table RVA=0 Size=0

    VTable Fixups RVA=0 Size=0

    Export Address Table Jumps RVA=0 Size=0

    Managed Native Header RVA=0 Size=0

    倒数第2个数据目录项是这样一个结构体,它包括了CLR头的RVA和大小。在32位机器的数据目录中,头的位置是相同的,但是在64位机器中有所改变。如果我们能碰到谁有64位的机器的话,那就是另外一回事了。

    在文件中,我们把文件指针放在第360个位置上来读取CLR头的RVA和大小。我们通过添加14个结构的208个字节到PE头的148字节上,这就到了CLR头——也就是第360个字节的前面。每个结构体都具有8字节大小。因此,字节数量可以预知为112。

    RVA分配内存位置0x2008。为了确定节中的准确定位,我们对这个值使用模(modulus)运算,模为2000。这样求得的余数为8,它决定了头代码的基位置。为了获取文件中的数据,将会在file alignment的512字节上添加8字节。开始的512字节会被跳过,因为它们包括了头的所有详细内容。因此,CLR头开始于这个文件起始位置的512字节处。

    这个头开始于Size字段。因此CLR头的大小是72(0x48)。之后是2个短整数,它们表示CLR的主版本号和次版本号。

    存储在这两个字段中的值2和0,是当可执行体运行时所希望的CLR版本号。

    接下来是元数据的RVA和Size。我们终于到了本书的精华部分。下一章将主要介绍元数据。

    按照顺序,下一个是Flag字段,和Characteristics字段一样,工作在位级(bit level)上。

    在我们的示例中,只有第1位是on,它指定了映像为一个IL映像。第2位表示代码可以在什么系统上执行,例如32位或64位。如果这个位为on,那么可执行体就能在32位机器上运行,64位上的运行时是不可以加载这个程序的。

    第4位表示一个强签名,而下一位则跟踪了调试数据标志,它总是为0。

    我们将在适当时候重新访问Entry Point Token。

    在标志之后是一系列数据目录,这里第一个数据目录指向资源而第二个数据目录指向String Name Signature。这个结构指向PE文件的哈希数据,它是由加载器用来绑定和进行版本控制的。

    根据文档,紧随其后的是CodeManagerTable,它总是为0。类中的虚函数使用了V表来执行它们的“戏法”,从而,VTable Fixups(V表修正)是必须的。最后两个数据目录,分别是Export Address Table Jumps和Managed Native Header,它们的值总是为0。

    这里,我们留有很多东西没有介绍,但是我们保证在本书结束之前将会对其全部进行说明的。

  • 相关阅读:
    winform treeview checkbox递归算法利用
    转载10种代码风格
    Canvas没有添加Background,点击就不在Canvas上?
    数据库安装文件制作(C#)
    Panel的公有属性Background
    Teechar 常用属性
    NHibernate
    解决:请购买WinRAR许可,您注册还剩下40天(WinRAR老是弹出烦人的对话框)
    在WinForm中用Visifire实现数据统计转自蓝月天南
    【转】TeeChart的用法
  • 原文地址:https://www.cnblogs.com/Jax/p/1598573.html
Copyright © 2011-2022 走看看