《Windows核心编程 - 内存管理》
第13章 Windows内存体系结构
1.32位系统有4G的虚拟地址空间,我们需要把物理存储器映射到相应的地址空间,才能访问。
2.进程的地址空间:
a.空指针赋值分区,只是为了捕获对空指针的赋值访问(x86 32位 0x00000000 - 0x0000FFFF 其实空指针返回这个区域的任何一个值都可以而不仅仅是NULL(0));
b.用户模式分区(3GB和2GB两种模式);exe/dll/映射文件
64位操作系统中默认会在低位的2GB空间运行,设置/LARGEADDRESSAWARE连接器开关后可以为64位应用程序打开大地址开关,但对DLL系统会忽略此标志,所有的DLL必须正确编写以便能在4TB用户模式分区的情况下正确运行。
c.64KB禁入区域(用来隔离用户模式和内核模式的一个边界区)
d.内核模式分区
与线程调度、内存管理、文件系统支持、网络支持以及设备驱动程序相关的代码。驻留在这一分区的任何东西为所有进程共有。
3.通过调用VirtualAlloc和VirtualFree来预定和释放地址空间。应用程序预定的起始地址必然是分配粒度(目前所有CPU平台均为64KB)的整数倍,但操作系统以应用程序预定的地址空间区域却不一定,例如PEB/TEB。
4.通过VirtualAlloc和VirtualFree来对物理存储器进行调拨(committing)和撤销调拨(decommitting)。调拨时总是以页面为单位进行调拨(x86/x64上为4KB)。
5.物理存储器:
可以理解为内存和磁盘上的页交换文件(其中包含虚拟内存)及文件影响(file image,exe/dll/数据文件)的总和。线程访问的数据在内存中时系统把数据的虚拟内存地址映射到内存的物理地址,不在时找到一块空内存页,把页交换文件中的数据调拨到地址空间中。
6.页面保护属性:
PAGE_NOACCESS
PAGE_READONLY
PAGE_READWRITE
PAGE_EXECUTE
PAGE_EXECUTE_READ
PAGE_EXECUTE_READWRITE
PAGE_WRITECOPY
PAGE_EXECUTE_WRITECOPY
启动数据执行保护DEP后,操作系统只对那些真正需要执行代码的内存区域使用PAGE_EXECUTE_*属性,其他保护属性(最常用的是PAGE_READWRITE)对那些只应存放数据的内存区域(线程栈或应用程序的堆)使用。
若指定了写时复制属性,当应用程序把exe或dll映射到地址空间时,会从页交换文件中分配存储空间预备给其中的可写区域,但只有当其中的数据改变时,程序才使用这些区域复制改变的内存页。
PAGE_NOCACHE:为了让需要操控内存缓冲区的驱动城区开发人员使用
PAGE_WRITECOMBINE:允许对单个设备的多次写操作合在一起,以提高性能。
PAGE_GUARD:使应用程序能够在页面总的任何一个字节被写入时得到通知。
7.内存区域的类型
a.闲置free:尚未预定
b.私有private:虚拟地址以系统的页交换文件为后辈存储器
c.映像image:一开始以映像文件(exe、dll)为后备存储器,但以后可能由于写时复制等原因改用页交换文件作为后备存储器。
d.已映射mapped:一开始以内存映射文件(memorymapped file)为后备存储器,但此后可能由于写时复制机制等改用页交换文件作为后备存储器。
e.保留Reserve:没有任何后备物理存储器。
当windows将PE文件映射到进程的虚拟地址空间时,每一段(section)必须另起一页,而且其实地址必须是系统页面大小的整数倍,则PE文件所需的虚拟地址空间大小一般来说要大于文件本身大小
给区域制定保护属性完全是为了效率,如果同时为区域和物理存储器制定保护属性,那么以后者为准。
“区域”是一个完整的连续的内存地址空间,是在预定的时候确定的,操作系统会保存区域的大小。一般用来描述一个文件映射,exe/dll映射,或以页交换文件为物理存储区域的一块调拨的内存地址空间。“block”是区域中具备相同保护属性的连续页面,并且以相同类型的物理存储器作为后备存储器。
8.数据对齐
第14章 探索虚拟内存
1.调用GetSyatemInfo函数取得与主机相关的值;调用GetLogicalProcessorInformation得到与处理器有关的信息。调用IsWow64Process判断是否是32位程序在64位系统上运行。
2.调用GlobalMemoryStatus函数得到当前虚拟内存状态,若预计内存大于4GB或页交换文件可能大于4GB,则应调用GlobalMemoryStatusEx
(PS.在结构中添加结构长度的成员,可以在以后给结构添加更多的成员,而不必担心会破坏已有的应用程序。)用户无法得到当前进程所拥有的物理内存的数量信息。
3.NUMA机器中的内存管理
4.调用VIrtualQuery或VirtualQueryEx得到内存中某地址处的信息,例如是否给某个地址调拨物理存储器或者是否能读取或访问某个内存地址。若需要知道某个已预定区域的大小或某个区域中块的数量或某个区域中是否包含有线程栈,则需要多次调用,综合分析。
5. windows vista 提供一个名为地址空间布局随机化(Address SpaceLayoutRandomization)的新特性,使得windows在第一次载入一个DLL时,为它选择一个新的基地址,使得黑客更难以发现常用系统DLL地址。(2005sp1开始,开发人员只要在构建exe或dll时使用/dynamicbase开关,就能让他们也参与ASLR,能让经过ASLR基地址重定位的页面为所有进程共享,从而提高内存使用率)。
第15章 在应用程序中使用虚拟内存
0. MicrosoftWindows提供了以下三种机制来对内存进行控制
a.虚拟内存:最适合用来管理大型对象数据或大型结构数组
b.内存映射文件:最适合用来管理大型数据流(通常是文件),以及在同一机器上运行的多个进程之间共享数据
c.堆:最适合用来管理大量的小型对象
1. 预定地址空间区域
使用VirtualAlloc函数预定,其参数如下:
PVOIDpvAddress:告诉系统需要在哪预定此地址,系统会向下取整到64k的整数倍 ; SIZE_T dwSize:告诉系统我们需要预定多大,会向上取整到页面的整数倍;
DWORDfdwAllocationType:
MEM_TOP_DOWN:尽可能从高地址预定,防止在中部地址预定产生的内存碎片
MEM_RESERVE:预定
MEM_COMMIT:调拨物理存储器
DWORDfdwProtect:区域的保护属性,最好和将要调拨的物理存储器属性一致
2. 调拨物理存储器
使用MEMCOMMIT调用VirtualAlloc;
系统是基于整个页面来制定保护属性的,因此不可能楚翔同一物理存储页面有不同保护属性的情况,但是同一区域总的两个页面可能有不同的保护属性,因调拨物理存储器的时候可以以页面为单位进行调拨。
3. 同时预定和调拨物理存储器
同时用MEM_RESERVE | MEM_COMMIT来调用VitualAlloc即可。
使用大页面:
a.要分配的内存块大小必须是GetLargePageMinimum返回值的整数倍
b.必须同时预定和调拨物理存储器
c.必须传PAGE_READWRITE属性给fdwProtect属性
4. 对一个结构数组 CELLDATA CellData[200][256],使用虚拟内存的步骤如下:
a.预定一块足够大的区域来容纳CELLDATA结构的整个数组,此时不消耗物理存储器;
b.当用户在单元格中输入数据时,首先确定CELLDATA结构在地址中的位置,此时还未调拨物理存储器,访问会引发违规访问;
c.给第二步的内存地址调拨足够的物理存储器,我们可以告诉系统只给某个区域调拨;
d.设置CELLDATRA结构的成员。
确定是否需要给区域中的某一部分调拨物理存储器的方法:
a.总是尝试调拨,靠VirtualAlloc函数每次访问时调拨的成功或失败判断;
b.使用VirtualQuery每次访问时确定是否已调拨;
c.记录哪些页面已调拨哪些未调拨;
d.使用结构化一场处理,当程序试图访问尚未调拨的物理存储器的内存地址时,让系统通知应用程序,应用程序就可以调拨物理存储器,并告诉系统再重新执行那条引发一场的指令。(BEST AND ONLY)
5. 撤销调拨物理存储器及释放区域
a. 使用VirtualFree函数:
LPVOIDpvAddress:欲释放的地址,若为基地址,则释放整个页面;若不是基地址则释放地址所在页面;
SIZE_TdwSize:释放区域的尺寸,若pvAddress为区域基地址,此参数必为0;若不是基地址,此值所涉及的页面均会被释放;
DWORDfdwFreeType:
MEM_RELEASE:撤销整个页面;
MEMDECOMMIT:撤销一部分页面;
b. 适时撤销物理存储器的方法
1)将结构设计的正好等于页面大小,不用时即撤销;
2)记录哪些表格正在使用,一般可以使用一个位图完成;
3)实现一个垃圾回收函数,检查结构中的标志位,并作为一个低优先级运行。
6. 改变保护属性
通过VirtualProtect函数改变一个内存页面的保护属性,当若干连续物理存储页跨越了不同的区域时,必须调用此函数多次。
7. 重置物理存储器的内容
调用VirtualAlloc并使用MEM_RESET标识进行物理存储器的重置,相当于应用程序通知系统,所占用的物理存储页不再需要,可被随时收回,且即使物理存储器被改动了也不要写入页交换文件。
注意重置时,地址是向上取整到页大小整数倍的,大小是向下取整到页面的整数倍的。
8. 地址窗口扩展
第16章 线程栈
0. 基本知识
a.每个线程有自己的栈
b.可以通过使用链接选项、CreateThread或_beginthreadex更改默认栈大小;
c.栈的基地址为栈顶,栈向下生长,开始时系统给最顶部的两个栈调拨物理存储器。
d.当线程需要新的页面时,操作系统调拨新的物理存储器,并把下一个页面设置为GUARD,但若下一个页面是倒数第二个页面,则不会设置GUARD属性,而是会抛出EXCEPTION_STACK_OVERFLOW异常。最后一个页面是永远不会调拨物理存储器的,这样做是为了捕获线程对站外区域的访问。
e.所谓statck underflow就是访问了超出栈的高地址区域(记住栈是向下生长的)。
1.C/C++运行库的栈检查函数
第17章 内存映射文件
0. 内存映射文件的应用场合
a.载入exe和dll文件
b.使用内存映射文件来访问磁盘上的数据文件
c.在同一台机器的不同进程之间共享数据
1. 映射到内存的可执行文件和DLL
1.0 当一个线程调用CreateProcess时,首先找到对应的exe,然后创建地址空间,并把exe文件映射其中,并把所有需要的dll载入并映射其中。
1.1 操作系统会为打开的多个可执行文件使用写时复制机制,当其中的数据被修改或代码被调试器修改时,便分配新的虚拟内存页,并更新相应实例的映射地址空间。(默认情况多个实例不共享全局或静态数据)
1.2 在exe间共享静态或全局数据
a.exe或dll文件映像由许多段组成,段的大小为页面大小的整数倍,其属性有:
READ:读
WRITE:写
EXECUTE:可执行该段内容
SHARED:该段内容被多个实例共享,实际上是关闭了写时复制。
b.创建共享段
#pragmadata_Seg(“Shared”)
LONGg_lInstanceCount=0;//此种把变量放入段的方式,变量必须初始化
#pragmadata_seg()
__declspec(allocate(“Shared”)) int d;//此种方式变量不必初始化,但段名称必须已申明。
然后须打开链接选项:
/SECTION:Shared,RWS
或在程序中使用如下形式:
#pragmacomment(linker,”/SECTION:Shared,RWS”)
这行代码告诉编译器把其中的字符串潜入到所生成的.obj文件中的一个特殊的段中,这个段名叫“.drectve”。当连接器把所有的.obj模块合并到一起的时候,连接器会检查每个.obj模块的 “.drectve”段,并将所有的字符串当做是传给连接器的命令行参数。
c.可以使用共享变量的方法实现登陆一个程序后,再打开另一个即自动登陆。但使用这种方法的一个问题是,黑客只要写一个小程序来载入我们的DLL并监视共享内存块就可以了。只要用户输入密码,黑客的程序就可以得到密码。另外也可以不断地猜测密码并写入共享内存。因为目前任何程序都可以载入任何一个DLL
2. 映射到内存的数据文件
把一个大型数据文件按字节颠倒存放:
a.一块内存,一个文件:读入内存,颠倒完了后再写入文件。若大文件则行不通。
b.两个文件,一块内存:读入一小段字节到内存,颠倒后放入新文件。
c.一个文件,两块缓存:把首尾的一小段数据都读入内存,颠倒后再写入首尾位置,这个过程一直持续,直到文件的中间。
d.一个文件,零个缓存:把文件映射进内存地址空间,让系统为我们处理所有与文件缓存有关的操作,我们直接调用_tcsrev即可。
3. 使用内存映射文件
使用内存映射文件,需要执行下面三个步骤:
a.创建或打开一个文件内核对象
b.创建一个文件映射内核对象(file-mapping kernalobject)来告诉系统文件的大小以及我们打算如何访问文件。
c.告诉系统把文件映射对象的部分或全部映射到进程的地址空间中。
d.告诉系统从进程地址空间中取消对文件映射内核对象的映射
e.关闭文件映射内核对象;
f.关闭文件内核对象
3.1创建或打开文件内核对象
调用此函数是为了告诉操作系统文件映射的物理存储器所在的位置。传入的路径是文件在磁盘上所在的位置。
HANDLECreateFile(
PCSTRpszFIleName:想要创建或打开的文件名称
DWORDdwDesiredAccess : GENERIC_READ
GENERIC_WRITE
GENERIC_READ|GENERIC_WRITE
DWORDdwShareMode: FILE_SHARE_READ
FILE_SHARE_WRITE
FILE_SHARE_READ|FILE_SHARE_WRITE
3.2创建文件映射内核对象
现在我们必须告诉系统文件映射对象需要多大的物理存储器:
HANDLECreateFileMapping(
HANDLE hFile :CreateFIle传回的句柄
PSECURITY_ATTRIBUTES psa,
DWORD fdwProtect :给进程地址空间设置的属性:
PAGE_READONLY:下面两个参数指定的大小必须不大于文件大小
PAGE_READWRITE:下面两个参数指定的大小必须不小于文件大小
PAGE_WRITECOPY:下面两个参数指定的大小必须不大于文件大小
PAGE_EXECUTE_READ
PAGE_EXECUTE_READWRITE:指定的大小必须不小于文件大小
还可以与5种段属性参数按位或起来。
dwMaximumSizeHigh
dwMaximumSizeLow 告诉系统内存映射文件的最大小,若不想改变文件大小则传0;若要追加则要增加相应大小;若文件本身为0则不能设为0.
PCTSTR pszName:用于在不同进程中共享内存映射文件。
)
创建一个内存映射文件,相当于先预定一块i地址空间区域,然后给区域调拨物理存储器。唯一不同之处在于内存映射文件的物理存储器来自于磁盘上的文件,而不是从系统的也交换文件中分配的。
3.3 将文件的数据映射到进程的地址空间
创建文件映射对象后,还需要为文件的数据预定一块地址空间区域并将文件的数据作为物理存储器调拨给区域:
PVOIDMapViewofFile(
HANDLEhFIleMappingObject:文件映射对象句柄
DWOEDdwDesiredAccess,
FILE_MAP_WRITE:可以读取和写入文件,PAGE_READWRITE
FILE_MAP_READ:可以读取文件,PAGE_READONLY,PAGE_READWRITRE
FILE_MAP_ALL_ACCESS:等同于FILE_MAP_WRITE|READ|COPY
FILE_MAP_COPY;可读取写入,写时复制,PAGE_WRITECOPY. 系统对原始数据进行复制时,页交换文件的属性会是PAGE_READWRITE
FILE_MAP_EXECUTE:作为代码执行,PAGE_EXECUTE_READWEITE
PAGE_EXECUTE_READ
DWORDdwFileOffsetHigh
DWORDdwFileOffsetLow 文件数据开始映射的偏移量,大小必须是系统分配粒度的整数倍(64KB)
SIZE_TdwFileOfBytesToMap 文件数据映射的大小
)
3.4 从进程的地址空间撤销对文件数据的映射
BOOLUnmapViewOfFile(
PVOID pvBaseAddress: 区域的基地址,必须和MapViewOfFile相同
)
强制吧把缓存写入磁盘:(若无任何缓存则返回0)
BOOLFlushViewOfFile(
PVOIDpvAddress:内存映射文件的试图中第一个字节的地址,向下取整到页面大小的整数倍
dwNumberOfBytesToFlush:想要刷新的字节数,向上取整到页面整数倍。
)
若开始时用FILE_MAP_COPY标识映射的,那么对文件数据的任何修改实际上是对保存在页交换文件中的文件数据副本的修改。若此时调用UnmapViewOfFile则函数不需要对磁盘文件进行任何更新,但它会释放页交换文件中的页面,从而导致数据丢失。需通过另外的途径保存。
3.5 关闭文件映射对象和文件对象
可以:
hFile=CreateFile
hFileMapping=CreateIfleMapping
PVOIDpvFile=MapViewOfFile
UnMapViewOfFile
CloseHandle(hFileMapping)
CloseHandle(hFile)
也可以:
hFile=CreateFile
hFileMapping=CreateIfleMapping
CloseHandle(hFile)
PVOIDpvFile=MapViewOfFile
CloseHandle(hFileMapping)
UnMapViewOfFile
因为系统会增加引用计数
4. 用内存映射文件处理大文件
方法是一次只映射一部分文件到视图中,但这每一部分必须是分配粒度的整数倍,文件的偏移量也必须是分配粒度的整数倍。
5. 内存映射文件和一致性
同一个数据文件能够映射到多个视图,系统保证多个视图之间的内容一致(因为他们本来就是同一个东西)。
只读文件比较适合用于内存映射文件。决不应该用内存映射文件来跨网络共享可写文件,因为系统无法保证数据视图的一致性。
6. 给内存映射文件指定基地址
可通过VirtualAlloc和MapViewOfFileEx函数给内存映射文件指定基地址,用来在内存映射文件跨进程共享数据的时候,若两个进程都把数据映射到同一个地址,则共享数据中的指针就可以直接使用。
7. 两次调用MapViewOfFile得到的指针没有必然的位置关系。
8. 用内存映射文件在进程间共享数据
Windows的进程间通讯机制:RPC\COM\OLE\DDE\Windows消息(特别是WM_COPYDATA)\剪贴板\邮件槽(mailslot)\管道(pipe)\套接字(socket)。但在同一台机器上共享数据的最底层机制就是内存映射文件。
这种机制通过让两个或多个进程映射同一个文件映射对象的视图来实现的,而进程使用的文件映射对象的名称必须完全相同。
同所有的内核对象一样,我们可以通过三种技术来跨进程共享对象:句柄继承、命名和句柄复制。
9. 以页交换文件为后备存储器的内存映射文件
直接调用CreateFileMapping创建文件映射对象,并传INVALID_HANDLE_VALUE作为hFile的实参传入,就把一个视图映射到了进程的地址空间中,再用字符串作为pszName的实参传入,这样其他想要访问共享数据的进程就能够以同一个名称为参数来调用CreateFileMapping或OpenFileMapping
10. 稀疏调拨的内存映射文件
在创建CreateFileMapping的时候传入INVALID_HANDLE_VALUE,系统会创建以页交换文件为后备存储器的文件映射对象。若设置SEC_RESERVE标识,系统就不会实际调拨物理存储器,它只返回文件映射对象的句柄。此时可以调用MapViewOfFile给这个内存映射文件创建一个视图,函数会预定一块地址空间区域,但由于没有调拨物理存储器,视图访问会违规。再通过VirutalAlloc函数调拨物理存储器(无论是通过VirtualAlloc和MEME_RESERVE标识得到的,还是对内存映射文件进行映射得到的,给他们调拨物理存储器的方法都是一样的)。
第18章 堆
0. 堆适合分配大量的小型数据,优点是不必理会分配粒度和页面边界这类事情,缺点是分配和释放内存块的速度比其他方式慢,而且也无法再对物理存储器的调拨和撤销调拨进行直接控制。
在系统内部,堆就是一块预定的地址空间区域。开始时区域内的大部分页面都没有调拨物理存储器,随着我们不断的从堆中分配和释放内存,堆管理器会给对调拨越来越多的从页交换文件中分配的物理存储器。
1. 进程的默认堆
默认堆在进程开始运行之前由系统自动创建,在进程终止后自动销毁。
可以调用GetProcessHeap来得到进程的默认堆的句柄。
许多Windows函数需要用到进程的默认堆,例如我们调用windows函数的ANSI版本时。
系统保证不管在什么时候,一次只让一个线程从默认堆中分配或释放内存块。
如果程序只有一个线程,而我们又希望以最快的速度访问堆,那么我们应该创建自己的堆而不是要使用进程的默认堆。
2. 为什么药创建额外的堆
a.不同类型的对象混杂在同一个堆中,对缺陷进行跟踪和定位会非常困难。且不同对象中可能会互相影响。
b.若在堆中分配不同大小的对象,则释放时会产生内存碎片。
c.内存访问局部化:连续存储同样的对象,可使得访问时页交换文件和内存之间的交换减少。
d.避免线程同步的开销:避免系统对进程顺序访问进行协调时产生的开销。
e.快速释放:把数据存入专属堆后,只需要一次性释放整个堆就可以。
3. 如何创建额外的堆
3.0.
HANDLEHeapCreate(
DWORDfdwOptions:
HEAP_NO_SERIALIZE:打开后关闭线程必须顺序访问的机制
只有确认同一时间只有一个线程访问时才可以开启
HEAP_GENERATE_EXCEPTION:产生错误时抛出异常,开启方式:调用函数
HeapSetInfomation(NULL,HeapEnableTerminationOnCorruption,NULL,0);
HEAP_CREATE_ENABLE_EXECUTE:执行在堆中的代码
SIZE_TdwInitialSize:初始大小;
SIZE_TdwMaximumSize:设为0时可增长
)
3.1.从堆中分配内存块
PVOIDHeapAlloc(
HANDLEhHeap,
DWORDfdwFlags:
HEAP_ZERO_MEMORY
HEAP_GENERATE_EXECPTIONS
HEAP_NO_SERIALIZE
SIZE_TdwBytes
);
失败时抛出异常:
STATUS_NO_MEMORY
STATUS_ACCESS_VIOLATION
转换为低碎片堆:
HeapSetInfomation
3.2.调整内存块的大小
PVOIDHeapReAlloc(
HANDLEhHeap
DWORDfdwFlags
HEAP_GENERATE_EXCEPTIONS
HEAP_NO_SERIALIZE
HEAP_ZERO_MEMORY:增大内存时有用
HEAP_REALLOC_IN_PLACE_ONLY扩大时尽量不移动原数据
PVOIDpvMem,
SIZE_TdwBytes
)
3.3.获得内存块的大小
SIZE_T HEAPSize(
HANDLEhHeap,
DWORDfdwFlags
LPCVOIDpvMem
)
3.4.释放内存块
BOOLHeapFree(
HANDLEhHeap,
DWORDfdwFlags
PVOIDpvMem
)
3.5.销毁堆
BOOLHeapDestroy(HANDLE hHeap);不能销毁默认堆
3.6.在C++中使用堆
重载new delete 操作符
classCSomeClass{
private:
staticHANDLE s_hHeap;
staticUINT s_uNumAllocsInHeap;
public:
void* operator new(size_t size);
voidoperator delete(void * p);
}
定义了重载操作符后,下面语句中的new操作符就会调用这个重载的版本
CSomeClass* pSomeClass = new CSomeClass;
若有子类继承本类,则也会继承这个堆。
3.7.其他堆函数
GetProcessHeaps;
HeapValidate;
HeapCompact;
HeapLock;HeapUnlock;
HeapWalk;