再谈计算机内存访问.docx
- 文档编号:5993607
- 上传时间:2023-01-02
- 格式:DOCX
- 页数:12
- 大小:86.55KB
再谈计算机内存访问.docx
《再谈计算机内存访问.docx》由会员分享,可在线阅读,更多相关《再谈计算机内存访问.docx(12页珍藏版)》请在冰豆网上搜索。
再谈计算机内存访问
第1章再谈计算机内存访问
本章提要
Ø内存管理概述
Ø虚拟内存访问的操作函数
Ø文件的内存映射
Ø深入认识C语言指针的真正含义
1.1引言
要成为一个程序编写高手,不仅需要熟悉各种计算机语言,而且还需要懂得计算机是如何工作的。
虽然不必对计算机的各个部件了解得十分清楚,但至少需要懂得计算机操作系统对内存是如何管理的。
只有这样,才能编写出计算机内核级的程序来。
本书的以后章节会涉及到内存管理的一些操作函数,因此,有必要先介绍这方面的内容。
其实,有关计算机内存管理的书籍已经很多了,为了加深读者对这些内存管理函数使用方法的进一步认识,这里重复谈论此话题。
计算机是由各种电子器件组成的,其核心部分是中央处理器,它的英文名字叫CPU,也常被称为微处理器。
微处理器有各种各样的型号,如80386、80486、80586(Pentium,这个词是“第五代”的意思)和80686(Pentium2)等。
通常可以通过向CPU送出指令对计算机内存进行访问。
在计算机硬件发展的同时,PC计算机的操作系统也在不断更新。
20世纪90年代以前,个人电脑的操作系统是DOS,此后Windows操作系统逐步成为主导。
DOS和Windows操作系统对计算机内存管理的方式不同,前者主要采用实模式管理,而后者主要采用保护模式管理。
本章首先介绍保护模式下的分页机制,然后介绍如何进行内存访问的操作。
1.2内存管理概述
内存管理是操作系统最重要的一部分,它决定了操作系统的性能。
为了说明如何进行内存访问的操作,有必要先介绍有关内存管理的一些术语及背景。
1.2.1虚拟内存
所谓虚拟内存就是用硬盘空间来弥补计算机物理内存不足的技术。
Windows操作系统用虚拟内存来动态管理运行时的交换文件。
为了提供比实际物理内存还多的内存容量,Windows操作系统占用了硬盘上的一部分空间作为虚拟内存。
当CPU有要求时,首先会读取内存中的资料。
当内存容量不够用时,Windows就会将需要暂时存储的数据写入硬盘。
所以,计算机的内存大小等于实际物理内存容量加上“分页文件”(就是交换文件)的大小。
Windows98中分页文件名采用Win386.swp形式,而Windows2K/XP/2003中采用pagefile.sys,默认位于系统分区的根目录下,具有隐藏属性。
如果需要的话,“分页文件”会动用硬盘上所有可以使用的空间。
安装好Windows以后,系统采用默认的设置自动处理虚拟内存,为了优化系统的工作性能,根据Windows操作系统中虚拟内存的设置方法,可以自己动手设置内存管理参数。
1.2.2CPU工作模式
计算机系统有不同的工作模式,在不同的模式下,CPU的寻址方式是不一样的,通常见到的CPU工作模式如下所述。
1.实模式
实模式是为了Pentium处理器与8086/8088兼容而设置的。
8086和8088只能工作于实模式,而80286及以上的处理器可工作于实模式或者保护模式下。
实模式操作方式只允许微处理器寻址第一个1MB的存储空间,从0x00000~0xFFFFF。
在实模式下的存储器寻址是段地址+偏移地址。
例如段寄存器的内容是0x1000,则它寻址开始于0x10000的段,偏移量大小从0x0000~0xFFFF,即偏移量的空间大小是216=64KB。
2.保护地址模式
保护地址模式又称为虚拟地址存储管理方式。
保护模式下主要有两种特征。
(1)内存分段管理
在保护模式下,各个16位的段寄存器里面放置的是选择符。
各项任务共享的内存空间由全局选择符来索引;而某个任务独立使用的内存空间由局部选择符来索引。
由选择符的高13位作为偏移量,再以CPU内部事先初始化好的GDTR(全局描述符表寄存器)中的32位基地址为基,可以获得相应的描述符。
由描述符中的线性地址决定段的基地址。
再利用指令(或其他方式)给出的偏移量,便可以得到线性地址,即
线性地址=段线性基地址+偏移量
保护模式采用上面介绍的分段管理,可以实现的存储器寻址范围为4GB,通常把通过段变换获得的地址称为线性地址。
这种线性地址是同32位物理地址对应的,为了获得更大的寻址范围,还可以对线性地址实行分页管理。
在保护模式下,处理器通过CRO控制寄存器的PG(page)位进行管理,当PG=0时,由段变换获得的线性地址可直接作为物理地址使用;若PG=1,则进一步进行页变换。
(2)内存分页管理
分页管理的基本思想是将内存分为大小固定为4KB或者1MB的若干页,通过一定机制对内存进行管理。
与前面的分段管理类似,程序或数据将根据其长度分配若干页。
为了进行页面管理,在分页管理机制中采用了页表、页目录对线性地址作页变换。
1.2.3逻辑、线性和物理地址
在保护地址模式下,经常遇到三种地址:
逻辑地址(LogicalAddress)、线性地址(LinearAddress)和物理地址(PhysicalAddress)。
CPU通过分段机制将逻辑地址转换为线性地址,再通过分页机制将线性地址转换为物理地址。
(1)逻辑地址
这是内存地址的精确描述,通常表示为十六进制:
xxxx:
YYYYYYYY,这里xxxx为selector(选择器),而YYYYYYYY是针对selector所选择的段地址的线性偏移量。
除了指定xxxx的具体数值外,还可使用具体的段寄存器的名字来替代,如CS(代码段),DS(数据段),ES(扩展段),FS(附加数据段#1),GS(附加数据段#2)和SS(堆栈段)。
这些符号都来自旧的“段:
偏移量”风格,在8086实模式下使用此种方式来指定“farpointers”(远指针)。
(2)线性地址
线性地址是逻辑地址到物理地址变换之间的中间层,是处理器可寻址的内存空间(称为线性地址空间)中的地址。
程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。
如果启用了分页机制,那么线性地址可以再经变换以产生一个物理地址。
若没有启用分页机制,那么线性地址直接就是物理地址。
不过,在开启分页功能之后,一个线性地址可能没有相对映的物理地址,因为它所对应的内存可能被交换到硬盘中。
32位线性地址可用于定位4GB存储单元。
(3)物理地址
所谓物理地址,就是指系统内存的真正地址。
对于32位的操作系统,它的范围为0x00000000~0xFFFFFFFF,共有4GB。
只有当CPU工作于分页模式时,此种类型的地址才会变得非常“有趣”。
本质上,一个物理地址是CPU插脚上可测量的电压。
操作系统通过设立页表将线性地址映射为物理地址。
Windows2K/XP所用页表布局的某些属性对于调试软件开发人员非常有用。
1.2.4存储器分页管理机制
程序代码和数据必须驻留在内存中才能得以运行,然而系统内存量很有限,往往不能容纳一个完整程序的所有代码和数据,特别是在多任务系统中,如Windows,可能需要同时打开多个执行程序,如画图程序,浏览器等,想让内存驻留所有这些程序显然不大可能,因此首先能想到的就是将程序分割成小部分,只让当前系统运行它所有需要的那部分留在内存,其他部分都留在硬盘(虚拟内存)。
当系统处理完当前任务片段后,再从外存中调入下一个待运行的任务片段。
于是,存储器分页管理机制随之而被发明。
如前所述,在保护模式下,控制寄存器CR0中的最高位PG位控制分页管理机制是否生效。
如果PG=1,分页机制生效,把线性地址转换为物理地址。
如果PG=0,分页机制无效,线性地址就直接作为物理地址。
必须注意,只有在保护方式下分页机制才可能生效。
只有在保证使PE位为1的前提下,才能够使PG位为1,否则将引起通用保护故障。
分页机制把线性地址空间和物理地址空间分别划分为大小相同的块。
这样的块称为页。
通过在线性地址空间的页与物理地址空间的页之间建立映射,分页机制可以实现线性地址到物理地址的转换。
线性地址空间的页与物理地址空间的页之间的映射可根据需要来确定。
线性地址空间的任何一页,可以映射为物理地址空间中的任何一页。
1.2.5线性地址到物理地址的转换
线性地址空间的页到物理地址空间的页之间的映射用表来描述。
目前所见到的有4KB和1MB大小的物理分页,对于4KB页面的分页,线性地址到物理地址的转换过程如图1.1所示。
对于1MB页面分页,线性地址到物理地址的转换与4KB的基本相似,不同的是线性地址的低22位对应一个物理页面。
对于4KB页面的线性地址到物理地址的转换示意图
对于4KB页面分页,页映射表的第一级称为页目录表,存储在一个物理页中。
页目录表共有1024个页目录项(PDE,pagedirectoryentry),其中,每个PDE为4字节长,包含对应第二级表所在物理地址空间页的页码。
页映射表的第二级称为页表,每张页表也被存储在一个物理页中。
每张页表有1024个页表项(PTE,pagetableentry),每个PTE为4字节长,其中PTE的低12位用来存放诸如“页是否存在于内存”或“页的权限”等信息。
一个线性地址大小为4个字节(32bit),包含着找到物理地址的信息,分为3个部分:
第22位到第31位这10位(最高10位)是页目录中的索引,第12位到第21位这10位是页表中的索引,第0位到第11位这12位(低12位)是页内偏移。
在把一个线性地址转换成物理地址时,CPU首先根据CR3中的值,找到页目录所在的物理页。
然后根据线性地址的第22位到第31位这10位(最高的10bit)的值作为索引,找到相应的PDE,其中含有这个虚拟地址所对应页表的物理地址。
有了页表的物理地址,再把虚拟地址的第12位到第21位这10位的值作为索引,找到该页表中相应的PTE,其中就有这个虚拟地址所对应物理页的物理地址。
最后用线性地址的最低12位,也就是页内偏移,加上这个物理页的物理地址,就得到了该线性地址所对应的物理地址。
1.3虚拟内存访问
每个进程都拥有自己的虚拟地址空间,那么怎样才能访问这个空间呢?
这就需要用到WindowsAPI函数。
这些函数直接与编写程序相关,因而更受软件工程师的关注。
有关这方面的函数较多,这里介绍几个重要的函数。
1.3.1获取系统信息
在一个程序中不能直接应用某个系统的设备参数,否则将不利于程序的移植。
因此,如果确实需要用到这样的设备参数,则需要一个系统信息函数来获得。
VC++编译器所提供这样的函数为GetSystemInfo()。
该函数需要一个指向SYSTEM_INFO结构的指针作为参数。
其原型表示为:
voidGetSystemInfo(LPSYSTEM_INFOlpSystemInfo);
其中lpSystemInfo返回LPSYSTEM_INFO结构的地址,用于装载适当的系统信息,这个结构体定义为:
typedefstruct_SYSTEM_INFO{
union{
DWORDdwOemId;
struct{
WORDwProcessorArchitecture;
WORDwReserved;
};
};
DWORDdwPageSize;
LPVOIDlpMinimumApplicationAddress;
LPVOIDlpMaximumApplicationAddress;
DWORD_PTRdwActiveProcessorMask;
DWORDdwNumberOfProcessors;
DWORDdwProcessorType;
DWORDdwAllocationGranularity;
WORDwProcessorLevel;
WORDwProcessorRevision;
}SYSTEM_INFO;
其中参数含义如下所述。
dwOemId:
是一个过时选项,用于与WindowsNT3.5以及以前的版本兼容。
wProcessorArchitecture:
指明处理的结构,如Intel、Alpha、Intel64位或Alpha64位。
dwPageSize:
用于显示CPU的页面大小。
在x86CPU上,这个值是4096字节。
在AlphaCPU上,这个值是8192字节。
在IA-64上,这个值是8192字节。
lpMinimumApplicationAddress:
用于给出每个进程可用地址空间的最小内存地址。
在Windows98上,这个值是0x400000,因为每个进程的地址空间中下面的4MB是不能使用的。
在Windows2K/XP上,这个值是0x10000,因为每个进程的地址空间中开头的64KB总是空闲的。
lpMaximumApplicationAddress:
用于给出每个进程可用地址空间的最大内存地址。
在Windows98上,这个地址是0x7FFFFFFF,因为共享内存映射文件区域和共享操作系统代码包含在上面的2GB分区中。
在WindowsXP上,这个地址是0x7FFEFFFF。
dwActiveProcessorMask:
位屏蔽,指明哪个CPU是活动的。
dwNumberOfProcessors:
计算机中CPU的数目。
dwProcessorType:
处理器类型。
dwAllocationGranularity:
保留的地址空间区域的分配粒度。
wProcessorLevel:
进一步细分处理器的结构。
wProcessorRevision:
用于进一步细分处理器的级别。
wReserved:
保留供将来使用。
在以上参数中只有lpMinimumApplicationAddress、lpMaximumApplicationAddress、dwPageSize和dwAllocationGranularity与内存有关。
1.3.2在应用程序中使用虚拟内存
对内存分配可以采用不同的方法,常用的方法有:
用C/C++语言的内存分配函数,例如,用malloc()和free()、new和delete函数分配和释放堆内存;用Windows传统的全局或者局部内存分配函数,如GlobalAlloc()和GlobalFree();用Win32的堆分配函数,如HeapAlloc()和HeapFree();用Win32的虚拟内存分配函数,如VirtualAlloc()和VirtualFree()。
注意,用不同的方法分配内存后,要用相对应的函数来释放所占用的内存。
这里只介绍Win32的虚拟内存分配函数。
在进程创建之初并被赋予地址空间时,其虚拟地址空间尚未分配,处于空闲状态。
这时地址空间内的内存是不能使用的,必须通过VirtualAlloc()函数来分配其中的各个区域,对其进行保留。
VirtualAlloc()函数原型为:
LPVOIDVirtualAlloc(
LPVOIDlpAddress,
DWORDdwSize,
DWORDflAllocationType,
DWORDflProtect
);
该函数用来分配一定范围的虚拟页。
参数1指定起始地址;参数2指定分配内存的长度;参数3指定分配方式,取值MEM_COMMINT或者MEM_RESERVE;参数4指定控制访问本次分配的内存的标识,取值为PAGE_READONLY、PAGE_READWRITE或者PAGE_NOACCESS。
分配完成后,即在进程的虚拟地址空间中保留了一个区域,可以对此区域中的内存进行保护权限许可范围内的访问。
当不再需要访问此地址空间区域时,应释放此区域,由VirtualFree()负责完成。
其函数原型为:
BOOLVirtualFree(
LPVOIDlpAddress,
DWORDdwSize,
DWORDdwFreeType
);
其中参数含义如下所述。
lpAddress:
指向待释放页面区域的指针。
如果参数dwFreeType指定了MEM_RELEASE,则lpAddress必须为页面区域保留由VirtualAlloc()所返回的基地址。
dwSize:
指定了要释放的地址空间区域的大小,如果参数dwFreeType指定了MEM_RELEASE标志,则将dwSize设置为0,由系统计算在特定内存地址上的待释放区域的大小。
dwFreeType:
为所执行的释放操作的类型,其可能的取值为MEM_RELEASE和MEM_DECOMMIT,其中MEM_RELEASE标志指明要释放指定的保留页面区域,MEM_DECOMMIT标志则对指定的占用页面区域进行占用的解除。
如果VirtualFree()执行完成,将回收全部范围的已分配页面,此后如再对这些已释放页面区域内存进行访问将引发内存访问异常。
释放后的页面区域可供系统继续分配使用。
1.3.3获取虚存状态
WindowsAPI函数GlobalMemoryStatus()可用于检索关于当前内存状态的动态信息。
在软件的About对话框中,通常用这个函数来获取系统内存的使用情况。
其函数原型为:
voidGlobalMemoryStatus(LPMEMORYSTATUSlpmstMemStat);
其中lpmstMemStat返回MEMORYSTATUS结构的地址,这个结构体的定义为:
typedefstructMEMORYSTATUS{
DWORDdwLength;
DWORDdwMemoryLoad;
DWORDdwTotalPhys;
DWORDdwAvailPhys;
DWORDdwTotalPageFile;
DWORDdwAvailPageFile;
DWORDdwTotalVirtual;
DWORDdwAvailVirtual;
}MEMORYSTATUS,*LPMEMORYSTATUS;
其中参数含义如下所述。
dwLength:
MEMORYSTATUS结构大小。
dwMemoryLoad:
已使用内存所占的百分比。
dwTotalPhys:
物理存储器的总字节数。
dwAvailPhys:
空闲物理存储器的字节数。
dwTotalPageFile:
页文件包含的最大字节数。
dwAvailPageFile:
用户模式分区中空闲内存大小。
dwTotalVirtual:
用户模式分区大小。
dwAvailVirtual:
表示当前进程中还剩下的自由区域的总和。
在调用GlobalMemoryStatus()之前,必须将dwLength成员初始化为用字节表示的结构的大小,即一个MEMORYSTATUS结构的大小。
这个初始化操作使得Microsoft能够在新版本Windows系统中将新成员添加到这个结构中,而不会破坏现有的应用程序。
当调用GlobalMemoryStatus()时,它将对该结构的其余成员进行初始化并返回。
如果某个应用程序在内存大于4GB的计算机上运行,或者合计交换文件的大小大于4GB,那么可以使用新的GlobalMemoryStatusEx()函数。
其函数的原型为:
BOOLGlobalMemoryStatusEx(MEMORYSTATUSEX&mst);
其中mst返回MEMORYSTATUSEX结构的填充信息,该结构体与原先的MEMORYSTATUS结构基本相同,差别在于新结构的所有成员的大小都是64位宽,因此它的值可以大于4GB。
1.3.4确定虚拟地址空间的状态
对内存的管理除了对当前内存的使用状态信息进行获取外,还经常需要获取有关进程的虚拟地址空间的状态信息。
例如,如何得到一个进程已提交的页面范围?
这就要用到两个API函数VirtualQuery()或VirtualQueryEx()来进行查询。
这两个函数的功能相似,不同就是VirtualQuery()只是查询本进程内存空间信息,而VirtualQueryEx()可以查询指定进程的内存空间信息。
VirtualQuery()函数原型如下:
DWORDVirtualQuery(
LPVOIDlpAddress,
PMEMORY_BASIC_INFORMATIONlpBuffer,
DWORDdwLength
);
VirtualQueryEx()函数原型如下:
DWORDVirtualQueryEx(
HANDLEhProcess,
LPCVOIDlpAddress,
PMEMORY_BASIC_INFORMATIONlpBuffer,
DWORDdwLength
);
其中参数含义如下所述。
hProcess:
进程的句柄。
lpAddress:
想要了解其信息的虚存地址。
lpBuffer:
返回MEMORY_BASIC_INFORMATION结构的地址。
dwLength:
返回的字节数。
PWEMORY_BASIC_INFORMATION的定义如下:
typedefstruct_MEMORY_BASIC_INFORMATION{
PVOIDBaseAddress;
PVOIDAllocationBase;
DWORDAllocationProtect;
DWORDRegionSize;
DWORDState;
DWORDProtect;
DWORDType;
}MEMORY_BASIC_INFORMATION,*PMEMORY_BASIC_INFORMATION;
其中参数含义如下所述。
BaseAddress:
被查询内存块的基地址。
AllocationBase:
用VirtualAlloc()分配该内存时实际分配的基地址。
AllocationProtect:
分配该页面时,页面的一些属性,如PAGE_READWRITE、PAGE_EXECUTE等(其他属性可参考PlatformSDK)。
RegionSize:
从BaseAddress开始,具有相同属性的页面的大小。
State:
页面的状态,有3种可能值:
MEM_COMMIT、MEM_FREE和MEM_RESERVE,这个参数是最重要的,从中可知指定内存页面的状态。
Protect:
页面的属性,它可能的取值与AllocationProtect相同。
Type:
指明了该内存块的类型,有3种可能值:
MEM_IMAGE、MEM_MAPPED和MEM_PRIVATE。
1.3.5改变内存页面保护属性
在进行进程挂钩时,经常要向内存页中写入部分代码,这就需要改变内存页的保护属性。
有幸的是Win32提供了两个API函数VirtualProtect()和VirtualProtectEx(),它们可以对改变内存页保护。
例如,在使用这两个函数时,可以先按PAGE_READWRITE属性来提交一个页的地址,并且立即将数据填写到该页中,然后再把该页的属性改变为PAGE_READONLY,这样可以有效地保护数据不被该进程中的任何其他线程重写。
在调用这两个函数之前最好先了解有关页面的信息,可以通过VirtualQuery()来实现。
VirtualProtect()与VirtualProtectEx()函数的区别在于VirtualProtect()只适用于本进程,而VirtualProtectEx()可以适用于其他进程。
VirtualProtect()函数原型如下:
BOOLVirtualProtect(
PVOIDpvAddress,
DWORDdwSize,
DWORDflNewProtect,
PDWORDpflOldProtect
);
VirtualProtectEx()函数原型如下:
BOOLVirtu
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 计算机 内存 访问