目录
IAT(Import Address Table)、INT(Import Name Table)、IDT(Import Directory Table)
对一个动态链接库里导出的函数的调用,既可以通过函数名称来进行,也可以通过函数在导出表的索引来进行。
前言:
可以从这里入门https://www.cnblogs.com/milantgh/p/3953713.html
配合这个博客查找上面不懂的https://www.cnblogs.com/bokernb/articles/6116512.html
开始需要大概了解一些名词:
关于地址和一些名词的理解,这个博客非常细致:https://blog.csdn.net/hguisu/article/details/5713099
需要知道的一些地址(后面会再来分析):
- VA:virtrual address 虚拟地址,也就是内存中的地址:
- RVA:relative virtrual address 相对虚拟地址,等于VA-ImageBase
- ImageBase:基地址
虚拟内存不考虑物理内存的大小和信息存放的实际位置,只规定进程中相互关联信息的相对位置。每个进程都拥有自己的虚拟内存,且虚拟内存的大小由处理机的地址结构和寻址方式决定。
关于程序如何运行
- 首先是要编译,由编译程序(Compiler)将用户源代码编译成cpu可执行的目标代码,产生了若干个目标模块(Object Module)(即若干程序段),
- 其次是链接,由链接程序(Linker)将编译后形成的一组目标模块(程序段),以及它们所需要的库函数链接在一起,形成一个完整的装入模块(Load Module);
- 最后是装入,由装入程序(Loader)将装入模块装入内存
程序的装入(地址的变换)
绝对装入方式:程序中的逻辑地址与实际内存地址完全相同
静态地址重定位(可重定位装入方式) :多道程序环境下,编译程序不可能预知所编译的目标模块应放在内存的何处,所得到的目标模块的起始地址通常是从 0 开始的,程序中的其它地址也都是相对于起始地址计算的,根据内存的当前情况,将装入模块装入到内存的适当位置。 静态地址重定位即在程序开始运行前,程序中指令和数据的各个地址均已完成重定位,即完成虚拟地址到内存地址映射。地址变换通常是在装入时一次完成的,以后不再改变。 采用可重定位装入程序将装入模块装入内存后, 会使装入模块中的所有逻辑地址与实际装入内存的物理地址不同
可以看个例题:
动态地址重地位(动态运行时装入方式):可重定位装入方式可将装入模块装入到内存中任何允许的位置,故可用于多道程序环境;但这种方式并不允许程序运行时在内存中移动位置。因为,程序在内存中的移动,意味着它的物理位置发生了变化, 这时必须对程序和数据的地址(是绝对地址)进行修改后方能运行。然而,实际情况是,在运行过程中它在内存中的位置可能经常要改变,此时就应采用动态运行时装入的方式。
在每次访问内存单元前才将要访问的程序或数据地址变换成内存地址
动态重定位可使装配模块不加任何修改而装入内存,装入之后再搬迁也不会影响其正确执行。为使地址转换不影响指令的执行速度,这种方式需要一个重定位寄存器的支持。一个程序由若干个相对独立的目标模块组成时,每个目标模块各装入一个存储区域,这些存储区域可以不是顺序相邻的,只要各个模块有自己对应的定位寄存器就行。
分页机制:
- 当使用分页时,处理器会把线性地址空间划分成固定大小的页面(4KB),这些页面可以映射到物理内存中或磁盘存储空间中,
- 当一个程序引用内存中的逻辑地址时,处理器会把该逻辑地址转换成一个线性地址,然后使用分页机制把该线性地址转换成对应的物理地址。
即(分页机制:https://blog.csdn.net/programmingring/article/details/21258529)
背景:
因为段的长度不定, 在分配内存时:可能会发生内存中的空闲区域小于要加载的段, 或者空闲区域远远大于要加载的段.
在前一种情况下, 需要另外寻找合适的空闲区域;
在后一种情况下, 分配会成功, 但太过于浪费.
为了解决这个问题, 从80386处理器开始, 引入了分页机制.
分页功能从总体上来说, 是用长度固定的页来代替长度不一定的段,
藉此解决因段长度不同而带来的内存空间管理问题. 尽管操作系统也可以用软件来实施固定长度的内存分配, 但太过于复杂, 由处理器固件来做这件事, 可以使速度和效率最大化.
分页机制概述:
处理器中有负责分段管理的段部件, 每个程序或任务都有自己的段, 这些段都用段描述符定义. 随着程序的执行, 当要访问内存, 就用段地址加上偏移量, 段部件就会输出一个线性地址. (在单纯的分段模式(例如单任务DOS系统)下, 线性地址就是物理地址.)
一旦决定采用页式内存管理, 就应当把4GB内存分成大小相同的页.
页的最小单位是4KB, 也就是4096字节, 用十六进制表示就是0x1000.
因此, 第一个页的物理地址就是0x00000000, 第2个页的物理地址是0x00001000, 第3个页的物理地址是0x00002000....最后一个页的物理地址是0xfffff000.
这样, 4GB内存划分为1048576(0x100000)个页. 很显然, 页的物理地址, 其低12位始终为0.(最后一个页的物理地址0xfffff000,32位地址,000表示最后12位(3*4)。)
段管理机制对于Intel处理器来说是最基本的, 任何时候都无法关闭. 也就是说, 即使启用页管理功能, 分段机制依然是起作用的, 段部件依然工作.
分页是分的进程,而内存是分块的,页表要解决的就是进程的分页的号码与内存分块号码的对应。 一定要学会用集合的概念去理解东西。即:
分页是对程序逻辑地址进行划分,
分块是对内存物理地址进行划分,
页表记录了页号和块号的对应关系,是页号和块号对应关系的集合
注:分页机制与分段机制的不同 :
分页与分段的最大的不同之处在于分页使用了固定长度的页面。
段的长度通常与存放在其中的代码或数据结构有相同的长度。
与段不同,页面有固定的长度。
- 如果仅使用分段地址转换,那么存储在物理内存中的一个数据结构将包含其所有的部分。
- 如果使用了分页,那么一个数据结构就可以一部分存储于物理内存中,而另一部分保存在磁盘中。
为了减少地址转换所要求的总线周期数量,最近访问的页目录和页表会被存放在处理器的一个叫做转换查找缓冲区(TLB)的缓冲器件中。
TLB 可以满足大多数读页目录和页表的请求而无需使用总线周期。
- 程序载入内存的时候只是一种内存映射
- 执行程序的时候系统会通过内存中的页表去查找数据和指令的真实地址
大概带着这些知识后,再来看PE的结构:
PE结构的一切元素,只有一个目的就是为了让程序载入内存。
这是一个根本解决也是伴随为什么产生PE这种结构的原因。
它要解决的问题就是一个地址转换的问题:怎么将磁盘上的地址转换为内存中的地址,并利于程序的执行。
一些结构的定义:
可以看这篇博文比较详细的记载了导入表相关的结构:http://blog.sina.com.cn/s/blog_56ea069101000bcg.html
关于这一块,我参照上面的博文,写了一个Xmind思维导图,预览图大概如下:
PS:做的导图放在我的CSDN资源上了,不嫌弃的话可以自取。(#^.^#)
一些结构的定义如下:(方便以后查阅)
_IMAGE_IMPORT_DESCRIPTOR结构
typedef struct _IMAGE_IMPORT_DESCRIPTOR
{
union
{
DWORD Characteristics;
DWORD OriginalFirstThunk; (PIMAGE_THUNK_DATA)
};
DWORD TimeDateStamp;
(new BIND)
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
IMAGE_THUNK_DATA结构
typedef struct _IMAGE_THUNK_DATA32
{
union {
PBYTE ForwarderString;
PDWORD Function;
DWORD Ordinal; //**比较常用的成员**//
PIMAGE_IMPORT_BY_NAME AddressOfData; //**比较常用的成员**//
} u1;
} IMAGE_THUNK_DATA32; //******注意此结构有32位和64位之分*******//
IMAGE_SECTION_HEADER的结构:
typedef struct _IMAGE_SECTION_HEADER
{
+0h BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 节表名称,如“.text”
//IMAGE_SIZEOF_SHORT_NAME=8
union
+8h {
DWORD PhysicalAddress; // 物理地址
DWORD VirtualSize; // 真实长度,这两个值是一个联合结构,可以使用其中的任何一个,一
// 般是取后一个
} Misc;
+