首页 > C > Hello world与编程基础 阅读:57,774

程序的编译与链接,PE格式

如图2-2所示,一个完整的PEPortable Executable)文件由DOS头,PE文件头,块表,块和调试信息(调试版本有效)组成。

 

 

2-2 PE文件格式

DOS头部,以e_magic开头,它的值是固定的”0x5a4d”,即(MZ)开头。最开头的是部分是DOS部首,DOS部首由两部分组成:DOSMZ文件标志和DOS stub(DOS存根程序)。之所以设置DOS部首是微软为了兼容原有的DOS系统下的程序而设立的。

DOS Stub即为一句话“This program Cannot be run in DOS mode”。

 

typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header

     WORD   e_magic;                     // Magic number

     WORD   e_cblp;                      // Bytes on last page of file

     WORD   e_cp;                        // Pages in file

     WORD   e_crlc;                      // Relocations

     WORD   e_cparhdr;                   // Size of header in paragraphs

     WORD   e_minalloc;                  // Minimum extra paragraphs needed

     WORD   e_maxalloc;                  // Maximum extra paragraphs needed

     WORD   e_ss;                        // Initial (relative) SS value

     WORD   e_sp;                        // Initial SP value

     WORD   e_csum;                      // Checksum

     WORD   e_ip;                        // Initial IP value

     WORD   e_cs;                        // Initial (relative) CS value

     WORD   e_lfarlc;                    // File address of relocation table

     WORD   e_ovno;                      // Overlay number

     WORD   e_res[4];                    // Reserved words

     WORD   e_oemid;                     // OEM identifier (for e_oeminfo)

     WORD   e_oeminfo;                   // OEM information; e_oemid specific

     WORD   e_res2[10];                  // Reserved words

     LONG   e_lfanew;                    // File address of new exe header

   } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

DOS头部,有一个结构成员e_lfanew指向了真正的PE头。值得注意的是PE文件头中的IMAGE_OPTIONAL_HEADER32是一个非常重要的结构,PE文件中的导入表、导出表、资源、重定位表等数据的位置和长度都保存在这个结构里。

typedef struct _IMAGE_NT_HEADERS {

    DWORD Signature; //PE头签名PE\0\0

    IMAGE_FILE_HEADER FileHeader;//PE文件头

    IMAGE_OPTIONAL_HEADER32 OptionalHeader;//PE扩展头

} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

typedef struct _IMAGE_FILE_HEADER {

    WORD    Machine;  //014C-IMAGE_FILE_MACHINE_I386

    WORD    NumberOfSections; //PE节数量-0007个节

    DWORD   TimeDateStamp;   //时间戳E72B4FA9

    DWORD   PointerToSymbolTable;  //指向符号表0000

    DWORD   NumberOfSymbols;  //符号表数量0000

    WORD    SizeOfOptionalHeader; //扩展PE头大小00E0

    WORD Characteristics; //文件属性

} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

 

typedef struct _IMAGE_OPTIONAL_HEADER {

    //

    // Standard fields.

    //

 

    WORD    Magic;   //010B-IMAGE_NT_OPTIONAL_HDR32_MAGIC

    BYTE    MajorLinkerVersion;   //0A-连接器主版本号

    BYTE    MinorLinkerVersion;  //00-连接器小版本号

    DWORD   SizeOfCode; //0000008A(138)-代码节大小

    DWORD   SizeOfInitializedData;   //0000004C(76)-已初始化数据大小

    DWORD   SizeOfUninitializedData;  //00000000(0)-为初始化数据大小

    DWORD   AddressOfEntryPoint;  //000110AA程序入口地址

    DWORD   BaseOfCode;  //00001000程序段基地址

    DWORD   BaseOfData;  //00001000数据段基地址 

 

    //

    // NT additional fields.

    //

 

    DWORD   ImageBase;  //镜像加载基地址00400000

    DWORD   SectionAlignment; //节对其0001000(4096)

    DWORD   FileAlignment;   //文件对齐0000200(512)

    WORD    MajorOperatingSystemVersion;  //操作系统主版本号0005

    WORD    MinorOperatingSystemVersion;  //操作系统小版本号0001

    WORD    MajorImageVersion;  //镜像主版本号0000

    WORD    MinorImageVersion;  //镜像小版本号0000

    WORD    MajorSubsystemVersion;   //子系统主版本号0005

    WORD    MinorSubsystemVersion;   //子系统小版本号0001

    DWORD   Win32VersionValue;   //0

    DWORD   SizeOfImage;  //镜像大小00022000

    DWORD   SizeOfHeaders;    //头大小0400

    DWORD   CheckSum;  //0

    WORD    Subsystem;  //03-IMAGE_SUBSYSTEM_WINDOWS_CUI

    WORD    DllCharacteristics;  

    DWORD   SizeOfStackReserve; //栈初始化大小010000

    DWORD   SizeOfStackCommit; //栈提交大小01000

    DWORD   SizeOfHeapReserve;   //堆初始化大小010000

    DWORD   SizeOfHeapCommit;  //堆提交大小01000

    DWORD   LoaderFlags;   //0

    DWORD   NumberOfRvaAndSizes;   //10(16)

    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//数据目录表

} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

PE头,最开始的值是一个PE文件特有的签名,即“PE\0\0”,一旦操作系统在执行PE的时候,发现这个位置如果不是这个值,就会报错。然后,PE头分为2个组成部分:File HeaderOptional Header。其中在File Header里,包含有如下信息:

l         WORD Machine;//该文件运行所需要的CPU,对于Intel平台是14Ch

l         WORD NumberOfSections;//文件的节数目

l         DWORD TimeDateStamp;//文件创建日期和时间

l         DWORD PointerToSymbolTable;//用于调试

l         DWORD NumberOfSymbols;//符号表中符号个数

l         WORD SizeOfOptionalHeader;//OptionalHeader 结构大小

l         WORD Characteristics;//文件信息标记,区分文件是exe还是dll

    Optional Header里,包含了如下信息:

 

WORD Magic;//标志字(总是010bh)

BYTE MajorLinkerVersion;//连接器高版本号

BYTE MinorLinkerVersion;//连接器低版本号

DWORD SizeOfCode;//代码段大小
DWORD SizeOfInitializedData;//
已初始化数据块大小
DWORD SizeOfUninitializedData;//
未初始化数据块大小
DWORD AddressOfEntryPoint;//PE
装载器准备运行的PE文件的第一个指令的RVA
//
若要改变整个执行的流程,可以将该值指定到新的RVA,这样新RVA处的指令首先

//被执行。
DWORD BaseOfCode;//
代码段起始RVA
DWORD BaseOfData;//
数据段起始RVA
DWORD ImageBase;//PE
文件的装载地址
DWORD SectionAlignment;//
块对齐因子
DWORD FileAlignment;//
文件块对齐因子
WORD MajorOperatingSystemVersion;//
所需操作系统高位版本号
WORD MinorOperatingSystemVersion;// 
所需操作系统低位版本号
WORD MajorImageVersion;//
用户自定义高位版本号
WORD MinorImageVersion;//
用户自定义低位版本号
WORD MajorSubsystemVersion;//win32
子系统版本。若PE文件是专门为Win32设计的
WORD MinorSubsystemVersion;//
该子系统版本必定是4.0否则对话框不会有3维立体感
DWORD Win32VersionValue;//
保留值,系统没用到的,一般被作为是否感染的标志
DWORD SizeOfImage;//
内存中整个PE映像体的尺寸
DWORD SizeOfHeaders;//
所有头+节表的大小
DWORD CheckSum;//
校验和
WORD Subsystem;//NT
用来识别PE文件属于哪个子系统
WORD DllCharacteristics;// 
用来表示一个DLL映像是否为进程和线程的初始化及终止包含入口点的标记
DWORD SizeOfStackReserve;//
DWORD SizeOfStackCommit;//
DWORD SizeOfHeapReserve;//
DWORD SizeOfHeapCommit;//
//
堆栈大小 这些域控制要保留的地址空间数量,并且负责栈和默认堆的申请。在默认//情况下,栈和堆都拥有1个页面的申请值以及16个页面的保留值
DWORD LoaderFlags;// 
告知装载器是否在装载时中止和调试,或者默认地正常运行
DWORD NumberOfRvaAndSizes;// 
该字段标识了接下来的DataDirectory数组个数。
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
//IMAGE_DATA_DIRECTORY 
结构数组。每个结构给出一个重要数据结构的RVA,比如//引入地址表等

 

 

Optional Header里,有一个重要的数组即DataDirectory数组。在这个数组里面,保存着很多重要的表,比如导入表(IMP),导出表(EMP),IAT表等。

导入表记录了PE使用了多少库函数。导入表的结构如下:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;// 
指向一个 IMAGE_THUNK_DATA 结构数组的RVA
}
DWORD TimeDateStamp;// 
文件生成的时间
DWORD ForwarderChain;// 
这个数据一般为0,可以不关心
DWORD Name1;  // RVA
,指向DLL名字的指针,ASCII字符串
DWORD FirstThunk; //
指向一个 IMAGE_THUNK_DATA 结构数组的RVA,这个数据与IAT所指向的地址一致

}IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR

    IMAGE_THUNK_DATA 这是一个DWORD类型的集合。通常我们将其解释为指向一个 IMAGE_IMPORT_BY_NAME 结构的指针,其定义如下:

IMAGE_THUNK_DATA{
    union {
        PBYTE ForwarderString;
        PDWORD Function;
        DWORD Ordinal;//
判定当前结构数据是不是以序号为输出的,如果是的话该值为

        //0x800000000,此时PIMAGE_IMPORT_BY_NAME不可做为名称使用
        PIMAGE_IMPORT_BY_NAME AddressOfData;
    }u1;
} IMAGE_THUNK_DATA,*PIMAGE_THUNK_DATA;

typedef struct _IMAGE_IMPORT_BY_NAME{
    WORD Hint;// 
函数输出序号
    BYTE Name1[1];//
输出函数名称

} IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME


    节表头结构:

typedef struct _IMAGE_SECTION_HEADER {

BYTE Name[IMAGE_SIZEOF_SHORT_NAME];//节表名称,如“.text

//IMAGE_SIZEOF_SHORT_NAME=8

union {

DWORD PhysicalAddress;//物理地址

DWORD VirtualSize;//真实长度,这两个值是一个联合结构,可以使用其中的任何一个,

//一般是节的数据大小

} Misc;

DWORD VirtualAddress;//RVA

DWORD SizeOfRawData;//物理长度

DWORD PointerToRawData;//节基于文件的偏移量

DWORD PointerToRelocations;//重定位的偏移

DWORD PointerToLinenumbers;//行号表的偏移

WORD NumberOfRelocations;//重定位项数目

WORD NumberOfLinenumbers;//行号表的数目

DWORD Characteristics;//节属性 如可读,可写,可执行等

} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

1.概述(general

-------

所有的节在载入内存后都按“SectionAlignment”(节对齐)对齐,在文件中则以“FileAlignment”(文件对齐)对齐。节由节头中的相关项来描述:在文件中你可通过“PointerToRawData”(原始数据指针)来找到,在内存中你可通过“VirtualAddress”(虚拟地址)来找到;长度由“SizeOfRawData”(原始数据长度)决定。

 

根据节中包含的内容,可分为好几种节。大多数(并非所有)情况下,节中至少由一个数据目录,并在可选头的数据目录数组中有一个指针指向它。

 

 

2.代码节(code section

------------------------

首先,我将提到代码节。此节,至少,要将“IMAGE_SCN_CNT_CODE”(含有代码节)、“IMAGE_SCN_MEM_EXECUTE”(内存可执行节)和“IMAGE_SCN_MEM_READ”(内存可读节)等标志位设为1,并且“AddressOfEntryPoint”(入口点地址)将指向节中的某个地方,指向开发者希望首先执行的那个函数的开始处。

BaseOfCode”(代码基址)通常指向这一节的开始处,但是,如果一些非代码字节被放在代码之前的话,它也可能指向节中靠后的某个地方。

通常,除了可执行代码外,本节没有别的东东,并且通常只有一个代码节,但是不要太迷信这一点。

典型的节名有“.text”、“.code”、“AUTO”之类。

 

 

3.数据节(data section

------------------------

我们要讨论的下一件事情就是已初始化变量;本节包含的是已初始化的静态变量(象“static int i = 5;”)。它将,至少,使“IMAGE_SCN_CNT_INITIALIZED_DATA”(含有已初始化数据节)、“IMAGE_SCN_MEM_READ”(内存可读节)和“IMAGE_SCN_MEM_WRITE”(内存可写节)等标志位被置为1

一些链接器可能会将常量放在没有可写标志位的它们自己的节中。如果有一部分数据可共享,或者有其它的特定情况,那么可能会有更多的节,且它们的合适的标志位会被设置。

不管是一节,还是多节,它们都将处于从“BaseOfData”(数据基址)到“BaseOfData+SizeOfInitializedData”(数据基址+已初始化数据的大小)的范围之内。

典型的名称有“.data”、“.idata”、“DATA”、等等。

 

 

4.BSS节(bss section

----------------------

其后就是未初始化的数据(一些象“static int k;”之类的静态变量);本节十分象已初始化的数据,但它的“PointerToRawData”(文件偏移量)却为0,表明它的内容不存储在文件中;并且“IMAGE_SCN_CNT_UNINITIALIZED_DATA”(含有未初始化数据节)而不是“IMAGE_SCN_CNT_INITIALIZED_DATA”(含有已初始化数据节)标志位被置为1,表明在载入时它的内容应该被置为0。这就意味着,在文件中只有节头,没有节身;节身将由加载器创建,并全部为0字节。

它的长度由“SizeOfUninitializedData”(未初始化数据大小)确定。

典型的名称有“.bss”、“BSS”之类。

 

有些节数据“没有”被数据目录指向。它们的内容和结构是由编译器而不是链接器提供。

(栈段和堆段不是二进制文件中的节,它们是由加载器根据可选头中的栈大小和堆大小项来创建的。)

ELF(Executable and Linking Format)是一种对象文件的格式,用于定义不同类型的对象文件(Object files)中都放了什么东西、以及都以什么样的格式去放这些东西。

ELF文件格式提供了两种视图:连接视图和运行视图。在两种视图中,ELF头部(ELF Header)都位于文件的开始部分,位置固定,保存了路线图(road map),描述了该文件的组织情况。

 

2-3 ELF文件格式

   

    在链接视图中,程序头部表(program header table)为可选。从程序的执行来看文件格式,程序头部表告诉系统如何来创建一个进程的内存映象。被用来建立进程映象(执行一个程序)的文件必须要有一个程序头部表,可重定位文件不需要这个头部表。

    节区(section)保存着目标文件的信息,从链接视图看,包括指令,数据,符号表和重定位信息等等。从执行视图看,一个段通常包含几个节区,同样保存着指令,数据,符号表和重定位等信息。

    节区头部表(section header table)包含了描述节区的信息。每个节区在这个表中有一个入口,该入口给出了节区的名字,大小等等信息。链接过程中的文件必须有一个节区头部表,而在执行视图中这个节区头部表为可选。

目标文件格式支持8位字节/32位体系结构。不过这种格式是可以扩展的,因此,目标文件以某些机器独立的格式来表达某些控制数据,使得能够以一种的公共的方式来识别和解释其内容。目标文件中的其它数据使用目标处理器的编码结构,而不管文件在何种机器上创建。

 

内存的寻址模式讨论

 

逻辑地址,线性地址,物理地址

 

1.逻辑地址是编译器生成的,我们使用在linux环境下,使用C语言指针时,指针的值就是逻辑地址。对于每个进程而言,他们都有一样的进程地址空间,类似的逻辑地址,甚至很可能相同。逻辑地址由段地址+段内偏移组成

2.线性地址是由分段机制将逻辑地址转化而来的,如果没有分段机制作用,那么程序的逻辑地址就是线性地址了。

3.物理地址是CPU在地址总线上发出的电平信号,要得到物理地址,必须要将逻辑地址经过分段,分页等机制转化而来。

 

x86体系结构下,使用的较多的内存寻址模型主要有三种:

1. 实模式扁平模型 real mode flat model

2. 实模式分段模型 real mode segment model

3. 保护模式扁平模型 protected mode flat model

 

实模式和保护模式相对,实模式运行于20位地址总线,保护模式则启用了32位地址总线,地址使用的是虚拟地址,引入了描述符表;虽然二者都引入了段这样一个概念,但是实模式的段是64KB固定大小,只有16个不同的段,CS,DS等存储的是段的序号。保护模式则引入了GDTLDT段描述符表的数据结构来定义每个段。

 

扁平模型和分段模型相对,区别在于程序的线性地址是共享一个地址空间还是需要分成多个段,即为多个程序是同时运行在同一个CSDS的范围内还是每个程序都拥有自己的CSDS:也就是说前者(flat)指令的逻辑地址要形成线性地址,不需要切换CSDS;后者的逻辑地址,必须要经过段选择子去查找段描述符,切换CSDS,才能形成线性地址。

 

实模式分段模型 real mode segment model

 

在实模式里,20位地址总线,16位的寄存器无法表示,一个基址寄存器+一个段寄存器联合起来则可以表示更大的一个地址空间。于是发明了这种段寄存器左移4+基址寄存器用以间接寻址。 20根地址线,表示 0x00000 - 0xfffff这个范围的地址(即1M 而寄存器16位,还有4位怎么办?于是8086CPU1MB的存储器空间分成许多逻辑段,每个段最大限制为64KB(为了能让16位寄存器寻址,2^20=2^10*2^10=2^10*2^6*2^4==16*64K), 段地址就是逻辑段在主存中的起始位置。为了能用16位寄存器表示段地址,8086规定段地址必须是模16地址,即为xxxx0H形式,省略低40,段地址就可以用16位数据表示,它通常被保存在16位的段寄存器中。存单元距离段起始位置的偏移量简称偏移地址,由于限定每段不超过64KB,所以偏移地址也可以用16位数据表示。物理地址:在1M字节的存储器里,每一个存储单元都有一个唯一的20位地址,称为该存储单元的物理地址,把段地址左移4(因为段地址低4位都是零)再加上偏移地址就形成物理地址。Seg<<4+Offset 对于 8086/8088 运行在实模式的程序,其实就是运行在实模式分段模型中。对于不同的程序,有不同的CSDS值,每个程序的段起始地址都不同。对于这样的程序而言,偏移地址16位的特性决定了每个段只有64KB大小。

 

实模式扁平模型 real mode flat model

 

该模式只有在386及更高的处理器中才能出现。80386的实模式,就是指CPU可用的地址线只有20位,能寻址0~1MB的地址空间。注意:80386的实模式并不等同于8086/8088的实模式,后者的实模式其实就是实模式分段模型。扁平模型,意味着我们这里不使用任何的分段寄存器。(尽管也使用了CS,DS,只是不用程序员去显示地为该寄存器赋值,jmp指令时就已经将CS, DS设置好了)

 

保护模式扁平模型 protected mode flat model

 

Linux Window XP/7采用的内存寻址模型,Linux中,段主要分为4种,即为内核代码段,内核数据段,用户代码段,用户数据段。 对于内核代码段和数据段而言,CS,DS的值是0xC00000000,而用户代码和数据段的CS,DS的值是0x00000000 CPU运行于32位模式时,不管怎样,寄存器和指令都可以寻址整个线性地址空间,所以根本就不需要再去使用基地址。基址可以设为一个统一的值。

周哥教IT,分享编程知识,提高编程技能,程序员的充电站。跟着周哥一起学习,每天都有进步。

通俗易懂,深入浅出,一篇文章只讲一个知识点。

文章不深奥,不需要钻研,在公交、在地铁、在厕所都可以阅读,随时随地涨姿势。

当你决定关注「周哥教IT」,你已然超越了90%的程序员!

IT黄埔-周哥教IT技术交流QQ群:213774841,期待您的加入!

二维码
微信扫描二维码关注