网上看到的一篇文章,写的还不错,对于入门来说 ^^自己又添了点东西
所有文件都是一些连续(当然实际存储在磁盘上的时候不一定是连续的)的数据组织起来的,不同类型的文件其组织形式也各不相同,PE文件格式便是一种文件组织形式,它是32位Window系统中的可执行文件EXE以及动态连接库文件DLL的组织形式。
加上几句:操作系统识别可执行文件的方法是按照文件格式而不是按照扩展名。扩展名只起到了一部分作用。例如将exe改为pif或者scr,双击后照样能运行起来。
为 什么我们双击一个EXE文件之后它就会被Window运行,而我们双击一个DOC文件就会被Word打开并显示其中的内容?这说明文件中肯定除了存在那些 文件的主体内容(比如EXE文件中的代码、数据等,DOC文件中的文件内容等)之外还存在其他一些重要的信息。这些信息是给文件的使用者看的,比如说 EXE文件的使用者是Windows,而DOC文件的使用者是Word。Windows可以根据这些信息知道把文件加载到地址空间的哪个位置,知道从哪个 地址开始执行,加载到内存后如何修正一些指令中的地址等等。那么PE文件中的这些重要信息都是由谁加入的?是由编译器和连接器完成的。针对不同的编译器和 连接器通常会提供不同的选项让我们在编译和联结生成PE文件时对其中的那些Windows需要的信息进行设定,当然也可以按照默认的方式编译连接生成 Windows中默认的信息。例如:WindowNT默认的程序加载基址是0x40000,你可以在用VC连接生成EXE文件的时候使用选项更改这个地址 值。在不同的操作系统中可执行文件的格式是不同的,比如在Linux上就有一种流行的ELF格式;当然它是由在Linux上的编译器和连接器生成的,所以 编译器、连接器是针对不同的CPU架构和不同的操作系统而设计出来的。在嵌入式领域中我们经常提到交叉编译器一词,它的作用就是在一个平台下编译出能在另 一个平台下运行的程序,例如,我们可以使用交叉编译器在跑Linux的X86机器上编译出能在Arm上运行的程序。
程序运行的过程
一 个程序从编写出来到运行一共需要那些工具,它们都对程序做了些什么,里面都涉及哪些知识需要学习呢?先说工具:编辑器、编译器、连接器、加载器。首先我们 使用编辑器编辑源文件;然后使用编译器编译程序目标文件OBJ,这里涉及到编译原理的知识;连接器把OBJ文件和其他一些库文件和资源文件连接起来生成 EXE文件,这里面涉及到不同的连接器的知识,连接器根据OS的需要生成EXE文件保存着磁盘上;当我们运行EXE文件时,Windows的加载器负责把 EXE文件加载到线性地址空间,加载的时候便是根据上一段中说到的PE文件格式中的那些重要信息;最后生成一个进程,如果进程中涉及到多个线程还要生成一 个主线程,此后进程便开始运行。这里面涉及的东西很多,包括:PE文件格式的内容、内存管理(CPU内存管理的硬件环境以及在此基础上的OS内存管理方 式)、模块、进程、线程的知识,只有把这些都弄清楚之后才能比较清楚的了解这整个过程。下面就让我们先来学习PE文件格式吧。
PE文件包括以下几个部分:(这里跟我在其他书上看的不太一样,不过浅谈嘛,估计是把主要的挑上来了)
DOS MZ header
DOS stub
PE header
Section table
Section 1
Section 2
Section ...
Section n
我们的EXE文件在磁盘上就是按照上面的格式顺序存储的,当运行的时候它就很容易被加载器加载到线性地址空间,但是在线性空间中和在磁盘上不同,在线性空间中各个部分不一定是占据连续的线性地址空间。下面对PE文件格式的每个部分进行介绍。
几个重要的基本概念
节。 PE文件的真正内容划分成块,称之为sections(节)。每节是一块拥有共同属性的数据,比如代码/数据、读/写等。我们可以把PE文件想象成一逻辑 磁盘,PE header是磁盘的boot扇区,而sections就是各种文件,每种文件自然就有不同属性如只读、系统、隐藏、文档等等。值得我们注意 的是,节的划分是基于各组数据的共同属性,而不是逻辑概念。重要的不是数据/代码是如何使用的,也不必关心节中类似于“data”、“code”或其他的 逻辑概念,如果PE文件中的数据/代码拥有相同属性,它们就能被归入同一节中。(节名称仅仅是个区别不同节的符号而已,类似“data”、“code”的 命名只为了便于识别,唯有节的属性设置决定了节的特性和功能)。如果某块数据想赋为只读属性,可以将该块数据放入置为只读的节中,当PE装载器映射节内容 时,它会检查相关节属性并置对应内存块为指定属性。
虚拟地址。虚拟地址即程序中使用的地址,也就是从程序员的角度看到的地址, 有时也叫逻辑地址,通常使用段地址、偏移量的形式表示。不过在32位系统中使用的是平坦(Flat)内存模式,所以我们可以不用管段地址,只考虑32位的 偏移量即可,认为32位的偏移量就是虚拟地址,这样一来程序员就可以认为他是在一个段中写程序,这个段的大小是2^32=4G的容量,当然这部分地址空间是 程序和OS共享的,程序员可以利用的大约有2G(具体可以参考Win98和WinNT的内存布局),所以我们平时在写程序申请内存的时候实际上申请的就是 这2G的线性地基空间。由于所有的4G线性地址空间都被OS作为资源来管理(这4G的线性地址空间是通过页表表现出来的,OS分配线性地址空间给进程也就 是分配相应的页表给进程),所以我们无论用什么方式使用内存最终都是转换为OS为我们分配线性地址空间,至于分配的线性地址空间又如何被映射为真正的物理 内存完全是有OS负责的(更详细资料参见“Windows内存管理”),程序员不必操心。相对虚拟地址。相对虚拟地址 (Relative VirtualAddress,RVA)即相对于上面的基地址的偏移量。PE文件中的许多字段内容都是以RVA表示,一个RVA是某 一资料项的offset(偏移)值——从文件被映像进来的起点(即基地址)算起。举个例子,我们说Windows加载器把一个PE文件映像到虚拟地址空间 的0x400000处,如果此p_w_picpath有一个表格开始于0x401464,那么这个表格的RVA就是0x1464:虚拟地址。
PE文件各个部分的作用
(一)DOS MZ header 和 DOS Stub
如 果在DOS下执行PE格式文件就会执行后面的DOS Stub,显示字符串 “This program cannot run in DOS mode”;如果在Windows下执行PE格式文件,PE加载器就会根据 DOS MZ header中的最后一个域e_lfnew跳过DOS Stub直接转到PE Header。DOS MZ header和 DOS Stub的贡献仅此而已。
(二)PE Header
当加载器跳到PE Header后,根 据里面的各个域首先检查这是不是有效的PE文件格式,能否在当前的CPU架构下运行,优先加载基址是多少,一共有几个节(section),这是一个 EXE文件还是DLL文件等总体信息,有了这些总体信息之后加载器就会跳到下面的Section table。
(三)Section table
有 了上面从PE Header获得的总体信息后,加载器并不能准确地加载文件,因为要准确的加载文件,加载器还需要一些关于每一节的更具体的信息,比如:每 一节在磁盘文件上的起始位置、大小,应该被加载的线性地址空间的哪一部分,这一节是代码还是数据,读写属性如何等等。所有这些信息都保存在 Section table里面,Section table是一个结构数组,数组里面的每一个结构对应PE文件中的一个节。PE加载器就会遍历这个结构 数组把PE文件的每一节准确的加载到线性地址空间。这里还要注意两点:一是PE加载器把PE文件的每一节加载到线性地址空间并不是说把磁盘上的文件调入物理内存,而只是为它分配线性地址空间,分配线性地址空间意味着申请本进程需要的页表,并把相应的信息添入页表中。线性地址空间也可以看作是一种资源,它是 通过页表来体现的,当一个页表被添入相应的信息被占用之后那么这个页表对应的那块线性地址空间也就被分配出去了。需要注意的另一点是PE加载器对每一节采 用文件映射的方式把相应的磁盘文件映射到内存,而不是把整个PE文件采用文件映射的方法把磁盘文件映射到内存。
(四)Sections
PE文件最后的部分就是各个节了,比如.text,.data,.idata等等。明白了PE文件的结构以及对应结构的作用,对于一些非法PE文件或者由于部分结构缺失造成的程序错误就能够有更加深刻的体会。
所有文件都是一些连续(当然实际存储在磁盘上的时候不一定是连续的)的数据组织起来的,不同类型的文件其组织形式也各不相同,PE文件格式便是一种文件组织形式,它是32位Window系统中的可执行文件EXE以及动态连接库文件DLL的组织形式。
加上几句:操作系统识别可执行文件的方法是按照文件格式而不是按照扩展名。扩展名只起到了一部分作用。例如将exe改为pif或者scr,双击后照样能运行起来。
为 什么我们双击一个EXE文件之后它就会被Window运行,而我们双击一个DOC文件就会被Word打开并显示其中的内容?这说明文件中肯定除了存在那些 文件的主体内容(比如EXE文件中的代码、数据等,DOC文件中的文件内容等)之外还存在其他一些重要的信息。这些信息是给文件的使用者看的,比如说 EXE文件的使用者是Windows,而DOC文件的使用者是Word。Windows可以根据这些信息知道把文件加载到地址空间的哪个位置,知道从哪个 地址开始执行,加载到内存后如何修正一些指令中的地址等等。那么PE文件中的这些重要信息都是由谁加入的?是由编译器和连接器完成的。针对不同的编译器和 连接器通常会提供不同的选项让我们在编译和联结生成PE文件时对其中的那些Windows需要的信息进行设定,当然也可以按照默认的方式编译连接生成 Windows中默认的信息。例如:WindowNT默认的程序加载基址是0x40000,你可以在用VC连接生成EXE文件的时候使用选项更改这个地址 值。在不同的操作系统中可执行文件的格式是不同的,比如在Linux上就有一种流行的ELF格式;当然它是由在Linux上的编译器和连接器生成的,所以 编译器、连接器是针对不同的CPU架构和不同的操作系统而设计出来的。在嵌入式领域中我们经常提到交叉编译器一词,它的作用就是在一个平台下编译出能在另 一个平台下运行的程序,例如,我们可以使用交叉编译器在跑Linux的X86机器上编译出能在Arm上运行的程序。
程序运行的过程
一 个程序从编写出来到运行一共需要那些工具,它们都对程序做了些什么,里面都涉及哪些知识需要学习呢?先说工具:编辑器、编译器、连接器、加载器。首先我们 使用编辑器编辑源文件;然后使用编译器编译程序目标文件OBJ,这里涉及到编译原理的知识;连接器把OBJ文件和其他一些库文件和资源文件连接起来生成 EXE文件,这里面涉及到不同的连接器的知识,连接器根据OS的需要生成EXE文件保存着磁盘上;当我们运行EXE文件时,Windows的加载器负责把 EXE文件加载到线性地址空间,加载的时候便是根据上一段中说到的PE文件格式中的那些重要信息;最后生成一个进程,如果进程中涉及到多个线程还要生成一 个主线程,此后进程便开始运行。这里面涉及的东西很多,包括:PE文件格式的内容、内存管理(CPU内存管理的硬件环境以及在此基础上的OS内存管理方 式)、模块、进程、线程的知识,只有把这些都弄清楚之后才能比较清楚的了解这整个过程。下面就让我们先来学习PE文件格式吧。
PE文件包括以下几个部分:(这里跟我在其他书上看的不太一样,不过浅谈嘛,估计是把主要的挑上来了)
DOS MZ header
DOS stub
PE header
Section table
Section 1
Section 2
Section ...
Section n
我们的EXE文件在磁盘上就是按照上面的格式顺序存储的,当运行的时候它就很容易被加载器加载到线性地址空间,但是在线性空间中和在磁盘上不同,在线性空间中各个部分不一定是占据连续的线性地址空间。下面对PE文件格式的每个部分进行介绍。
几个重要的基本概念
节。 PE文件的真正内容划分成块,称之为sections(节)。每节是一块拥有共同属性的数据,比如代码/数据、读/写等。我们可以把PE文件想象成一逻辑 磁盘,PE header是磁盘的boot扇区,而sections就是各种文件,每种文件自然就有不同属性如只读、系统、隐藏、文档等等。值得我们注意 的是,节的划分是基于各组数据的共同属性,而不是逻辑概念。重要的不是数据/代码是如何使用的,也不必关心节中类似于“data”、“code”或其他的 逻辑概念,如果PE文件中的数据/代码拥有相同属性,它们就能被归入同一节中。(节名称仅仅是个区别不同节的符号而已,类似“data”、“code”的 命名只为了便于识别,唯有节的属性设置决定了节的特性和功能)。如果某块数据想赋为只读属性,可以将该块数据放入置为只读的节中,当PE装载器映射节内容 时,它会检查相关节属性并置对应内存块为指定属性。
虚拟地址。虚拟地址即程序中使用的地址,也就是从程序员的角度看到的地址, 有时也叫逻辑地址,通常使用段地址、偏移量的形式表示。不过在32位系统中使用的是平坦(Flat)内存模式,所以我们可以不用管段地址,只考虑32位的 偏移量即可,认为32位的偏移量就是虚拟地址,这样一来程序员就可以认为他是在一个段中写程序,这个段的大小是2^32=4G的容量,当然这部分地址空间是 程序和OS共享的,程序员可以利用的大约有2G(具体可以参考Win98和WinNT的内存布局),所以我们平时在写程序申请内存的时候实际上申请的就是 这2G的线性地基空间。由于所有的4G线性地址空间都被OS作为资源来管理(这4G的线性地址空间是通过页表表现出来的,OS分配线性地址空间给进程也就 是分配相应的页表给进程),所以我们无论用什么方式使用内存最终都是转换为OS为我们分配线性地址空间,至于分配的线性地址空间又如何被映射为真正的物理 内存完全是有OS负责的(更详细资料参见“Windows内存管理”),程序员不必操心。相对虚拟地址。相对虚拟地址 (Relative VirtualAddress,RVA)即相对于上面的基地址的偏移量。PE文件中的许多字段内容都是以RVA表示,一个RVA是某 一资料项的offset(偏移)值——从文件被映像进来的起点(即基地址)算起。举个例子,我们说Windows加载器把一个PE文件映像到虚拟地址空间 的0x400000处,如果此p_w_picpath有一个表格开始于0x401464,那么这个表格的RVA就是0x1464:虚拟地址。
PE文件各个部分的作用
(一)DOS MZ header 和 DOS Stub
如 果在DOS下执行PE格式文件就会执行后面的DOS Stub,显示字符串 “This program cannot run in DOS mode”;如果在Windows下执行PE格式文件,PE加载器就会根据 DOS MZ header中的最后一个域e_lfnew跳过DOS Stub直接转到PE Header。DOS MZ header和 DOS Stub的贡献仅此而已。
(二)PE Header
当加载器跳到PE Header后,根 据里面的各个域首先检查这是不是有效的PE文件格式,能否在当前的CPU架构下运行,优先加载基址是多少,一共有几个节(section),这是一个 EXE文件还是DLL文件等总体信息,有了这些总体信息之后加载器就会跳到下面的Section table。
(三)Section table
有 了上面从PE Header获得的总体信息后,加载器并不能准确地加载文件,因为要准确的加载文件,加载器还需要一些关于每一节的更具体的信息,比如:每 一节在磁盘文件上的起始位置、大小,应该被加载的线性地址空间的哪一部分,这一节是代码还是数据,读写属性如何等等。所有这些信息都保存在 Section table里面,Section table是一个结构数组,数组里面的每一个结构对应PE文件中的一个节。PE加载器就会遍历这个结构 数组把PE文件的每一节准确的加载到线性地址空间。这里还要注意两点:一是PE加载器把PE文件的每一节加载到线性地址空间并不是说把磁盘上的文件调入物理内存,而只是为它分配线性地址空间,分配线性地址空间意味着申请本进程需要的页表,并把相应的信息添入页表中。线性地址空间也可以看作是一种资源,它是 通过页表来体现的,当一个页表被添入相应的信息被占用之后那么这个页表对应的那块线性地址空间也就被分配出去了。需要注意的另一点是PE加载器对每一节采 用文件映射的方式把相应的磁盘文件映射到内存,而不是把整个PE文件采用文件映射的方法把磁盘文件映射到内存。
(四)Sections
PE文件最后的部分就是各个节了,比如.text,.data,.idata等等。明白了PE文件的结构以及对应结构的作用,对于一些非法PE文件或者由于部分结构缺失造成的程序错误就能够有更加深刻的体会。