精华内容
下载资源
问答
  • Windows内存管理

    千次阅读 2014-04-05 23:46:50
    Windows内存管理 在驱动程序编写中,分配和管理内存不能使用熟知的Win32API函数,取而代之的是DDK提供的高效的内核函数。程序员必须小心地使用这些内存相关的内核函数。因为在内核模式下,操作系统不会检查内存使用...

    Windows内存管理

    在驱动程序编写中,分配和管理内存不能使用熟知的Win32API函数,取而代之的是DDK提供的高效的内核函数。程序员必须小心地使用这些内存相关的内核函数。因为在内核模式下,操作系统不会检查内存使用的合法性稍有不慎就可能导致操作系统的崩溃。另外,C语言和C++中大多数关于内存操作的运行时函数,大多在内核模式下是无法使用的。

    1、内存管理概念

    编写windows驱动之前,需要读者进一步理解windows操作系统是如何管理和使用内存的。

    1.1、物理内存的概念(Physical Memory Address

    PC上有三条总线,分别是数据总线,地址总线和控制总线。32位的CPU的寻址能力为4GB2^32)个字节。用户最多可以使用4GB的真实的物理内存。PC中会拥有很多设备,其中很多设备都提供了自己的设备内存。一个设备可以有好几块设备内存映射到物理内存上。

    1.2、虚拟内存地址概念(Virtual Memory Address

    虽然可以寻址4GB的内存,而在PC里往往没有如此多的真实物理内存。操作系统和硬件为使用者提供了虚拟内存的概念。Windows的所有程序包括RIng0层和Ring3层的程序可以操作的都是虚拟内存。之所以称为虚拟内存,是因为对它的所有操作,最终会变成一系列对真实物理内存的操作。

    虚拟内存转换为物理内存:在CPU中有一个重要的寄存器CR0,它是32位的寄存器,其中的一位PG位是复制告诉系统是否分页的。Windows在启动前会将它的PG位置1,即WIndows允许分页。DDK中有个宏PAGE_SIZE记录着分页大小,一般为4KB4GB的虚拟内存会被分割成1M4GB/4KB)个分页单元。

    其中,有一部分单元会和物理内存对应起来,即虚拟内存中第N个分页单元对应着物理内存第M个分页单元。这种对应不是一一对应,而是多对一的映射,多个虚拟内存页可以映射同一个物理内存页。还有一部分单元会被映射成磁盘上的文件,并标记为脏的。读取这段虚拟内存的时候,系统会发出一个异常,此时会触发异常的处理函数,异常处理函数会将这个页的磁盘文件读入内存,并标记设置为不脏。当让经常不读写的内存页,可以交换成文件,并将此页设置为脏。还有一部分单元什么也没有对应即空的。

    大部分的虚拟内存是没有被映射到物理内存上的。

     

    这样的设计基于以下两个原因:

    第一是虚拟的增加了内存的大小。不管PC是否有足够的4GB的物理内存,操作系统总会有4GB的虚拟内存。这就允许使用者申请更多的内存,当物理内存不够时,可以通过将不常用的虚拟内存页交换成文件,等需要的时候再去读取。

    第二是使不同进程的虚拟内存互不干扰,为了让系统可以同时运行不同的进程,windows操作系统让每个进程看到的虚拟内存都不同。这个方法就使得不同的进程会有不同的物理内存到虚拟内存的映射。

    1.3、用户模式地址和内核模式地址

    虚拟地址在0~0X7FFFFFFF范围内的虚拟内存,即低2GB的虚拟内存地址,被称为用户模式地址。而0X80000000~0XFFFFFFF范围内的虚拟内存,即高2GB的虚拟内存,被称为内核模式地址。WIndows规定运行在用户态Ring3层的程序只能访问用户模式地址,而运行在核心态Ring0层的程序,可以访问整个4GB的虚拟内存,即用户模式地址和内核模式地址。Windows的核心代码和Windows的驱动程序加载的位置都是在高2GB的内核地址里,所以一般的应用程序是不能访问到这新核心代码和重要数据的,这大大提高了系统的稳健性。同时,Windows操作系统在进程切换时,保存内核模式地址是完全相同的。也就是说,所有进程的内核地址映射完全一致,进程切换的时候,只改变用户模式地址的映射。

    1.4Windows驱动程序和进程的关系

    驱动程序可以看成是一个特殊的DLL文件被应用程序加载到虚拟内存中,只不过加载地址是内核模式地址,而不是用户模式地址。他能访问的只是这个进程的虚拟内存,而不能是其他进程的虚拟内存。需要指出的是,Windows驱动程序里的不同例程运行在不同的进程中。DriverEntry例程和AddDevice例程是运行在系统(System)进程中的。System进程是Windows中非常重要的进程,也是Windows第一个运行的进程。当需要加载的时候,System进程中会有一个线程将驱动程序加载到内核模式地址空间内,并调用DriverEntry例程。

    其他一些例程,例如IRP_MJ_READIRP_MJ_WRITE的派遣函数会运行与应用程序的“上下文”中。所谓运行在进程的“上下文”,指的是运行于某个进程的环境中,所能访问的虚拟地址是这个进程的虚拟地址。

    VOID DisplayItsProcessName()

    {

    //得到当前进程

    PEPROCESS pEProcess = PsGetCurrentProcess();

    //得到当前进程名称

    PTSTR ProcessName = (PTSTR)((ULONG)pEProcess+0x174);

    KdPrint(("%s\n",ProcessName));

    }

    1.5、分页与非分页内存

    前面介绍了虚拟内存页与物理内存页之间的关系,Windows规定有些虚拟内存页面是可以交换到文件中的,这类内存被称为分页内存。而有些虚拟内存页永远不会交换到文件中,这些内存被称为非分页内存。

    当程序的中断请求级在DISPATCH_LEVEL之上(包括DISPATCH_LEVEL)时,程序只能使用非分页内存,否则将导致蓝屏死机。

    如果将某个函数载入到分页内存中,我们需要在函数的实现中加入以下代码:

    #pragma PAGEDCODE

    VOID SomeFunction()

    {

    PAGED_CODE();

    //do something

    }

    PAGED_CODE()DDK提供的宏,他只在check版本中生效。它会检验这个函数是否运行低于DISOATCH_LEVEL的中断请求级,如果等于或者高于这个中断请求级,将产生一个断言。

    如果让函数加载到非分页内存中,需要在内存的实现中加入以下代码:

    #pragma LOCKEDCODE

    VOID SomeFunction()

    {

    //do something

    }

    还有一种特殊情况,就是某个例程需要在初始化的时候载入内存,然后就可以从内存中卸载掉。这种情况只出现在DriverEntry情况下。尤其是NT式的驱动,DriverEntry会很长,占据很大的空间,为了节省内存,需要及时地从内存中卸载掉。代码如下:

    #pragma INITCODE

    Extern C NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject,IN PUNICODE_STRING RegistryPath)

    {

    //do something

    }

     

    1.6、分配内核内存

    Windows驱动程序使用的内存资源非常珍贵,分配内存时要尽量节约。和应用程序一样,局部变量是存放在栈空间中的。但栈空间不会像应用程序那么大,所以驱动程序不适合递归或者局部变量是大型结构体。如果需要大型结构体,请在堆中申请。

    堆中申请内存的函数有以下几个:

    PVOID 
      ExAllocatePool(
        IN POOL_TYPE  PoolType,
        IN SIZE_T  NumberOfBytes
        );

     

    PVOID 
      ExAllocatePoolWithTag(
        IN POOL_TYPE  PoolType,
        IN SIZE_T  NumberOfBytes,
        IN ULONG  Tag
        );

     

    PVOID 
      ExAllocatePoolWithQuota(
        IN POOL_TYPE  PoolType,
        IN SIZE_T  NumberOfBytes
        );

     

    PVOID 
      ExAllocatePoolWithQuotaTag(
        IN POOL_TYPE  PoolType,
        IN SIZE_T  NumberOfBytes,
        IN ULONG  Tag
        );

     

    PoolType是个枚举变量,如果此值为NonPagedPool,则分配非分页内存,如果此值为PagedPool,则分配内存为分页内存。

    NumberOfByte是分配内存的大小,最好是4的倍数

    返回值分配的内存地址,一定是内核模式地址,如果返回0表示分配失败

    以上四个函数功能类似,函数以WithQuota结尾的代表分配的时候按配额分配。函数以WithTag结尾的函数和ExAllocatePool功能类似,唯一不同的是多了一个Tag参数,系统在要求的内存外有额外地分配了4个字节的标签,在调试的时候,可以找出是否有标有这个标签的内存没有被释放。

    将分配的内存,进行回收的函数是ExFreePoolExFreePoolWithTag

    VOID 
      ExFreePool(
        IN PVOID  P
        );

     

    NTKERNELAPI
    VOID
      ExFreePoolWithTag(
        IN PVOID  P,
        IN ULONG  Tag 
        ); 

     

    2、在驱动中使用链表

    在驱动程序开发中,经常使用链表这种数据结构,DDK为用户提供两种链表的数据结构,简化了对链表的操作。

    链表中可以记录将整型,浮点,字符型或者程序员自定义的数据结构。链表通过指针将这些数据结构组成一条“链”,链中每个袁术对应着记录的数据。对于单向链表,每个元素中有一个Next指针指向下一个元素。对于双向链表,每个元素有两个指针,指向前驱元素BLINK和后继元素FLINK。本节以双向链表为例:

    2.1、链表结构

     

    2.2、链表初始化

    初次使用时需要初始化,主要将链表头的FLinkBLink两个指针都指向自己。这意味着链表头所代表的链是空链。使用InitializeListHead宏实现初始化。

    2.3、从首部插入链表

    对链表的插入有两种方式,一种是在链表的头部插入,一种是在链表的尾部插入。在头部插入链表使用语句InsertHeadList,用法:

    InsertHeadList(&head,&mydata->ListEntry);

    HeadLIST_ENTRY结构的链表头,mydata是用户定义的数据结构,而他的子域ListEntry是包含其中的LIST_ENTRY数据结构。

    2.4、从尾部插入链表

    在尾部插入链表使用语句InsertTailList(&head,&mydata->ListEntry);

    2.5、从链表删除

    a)、从链表头删除RemoveHeadList

    b)、从链表尾删除RemoveTailList

    //链表的操作

    VOID LinkListTest()

    {

    LIST_ENTRY linkListHead;

    //初始化链表

    InitializeListHead(&linkListHead);

    PMYDATASTRUCT pData;

    ULONG i=0;

    KdPrint(("Begin insert to link list"));

    //在链表中插入10个元素

    for(i=0;i<10;i++)

    {

    //分配分页内存

    pData = (PMYDATASTRUCT)ExAllocatePool(PagedPool,sizeof(MYDADASTRUCT));

    pData->number = i;

    //从头部插入链表

    InsertHeadList(&linkListHead,&pData->ListEntry);

    }

    //从链表中取出,并显示

    KdPrint(("Begin remove from link\n"));

    while(!IsListEmpty(&linkListHead))

    {

    //从尾部删除一个元素

    PLIST_ENTRY pEntry = RemoveTailList(&linkListHead);

    pData = CONTAAINING_RECORD(pEntry,MYDATASTRUCT,ListEntry);

    KdPrint(("%d\n",pData->number));

    ExFreePool(pData);

    }

    }

     

    3、Lookaside结构

    频繁申请和回收内存,会导致在内存上产生大量的内存“空洞”,从而导致最终无法申请内存。DDK为程序员提供了Lookaside结构来解决这个问题。

    3.1、频发申请内存的弊端

    频繁地申请内存,会导致一个问题,就是在内存中产生“空洞”,即内存碎片。如果系统中存在大量的内存碎片,即使内存中有大量的可用内存,也会导致申请内存失败。在操作系统空闲时,系统会整理内存碎片,将其合并。

    3.2、使用Lookaside

    如果驱动程序需要频繁地从内存中申请、回收固定大小的内存,DDK提供了一种机制来解决,即使用Lookaside对象。

    可以将Lookaside对象想象成一个内存容器。在初始的时候,它先向Windows申请了一块比较大的内存。以后程序员每次申请内存的时候,不是直接向Windows申请 内存,而是向Lookaside申请内存。Lookaside对象会智能地避免产生内存碎片。如果有Lookaside对象内部的内存不够用时,他会向操作系统申请更多的内存。当Lookaside对象内部有大量的未使用的内存时,他会自动让Windows回收一部分内存,总之,Lookaside是一个自动的内存分配容器。通过对Lookaside对象申请内存,效率要高于直接向windows申请内存。Lookaside一般会在以下情况使用:

    a)、程序员每次申请固定大小的内存。

    b)、申请和回收的操作十分频繁。

    初始化Lookaside对象:

    VOID 
      ExInitializeNPagedLookasideList(
        IN PNPAGED_LOOKASIDE_LIST  Lookaside,
        IN PALLOCATE_FUNCTION  Allocate  OPTIONAL,
        IN PFREE_FUNCTION  Free  OPTIONAL,
        IN ULONG  Flags,
        IN SIZE_T  Size,
        IN ULONG  Tag,
        IN USHORT  Depth
        );

     

    VOID 
      ExInitializePagedLookasideList(
        IN PPAGED_LOOKASIDE_LIST  Lookaside,
        IN PALLOCATE_FUNCTION  Allocate  OPTIONAL,
        IN PFREE_FUNCTION  Free  OPTIONAL,
        IN ULONG  Flags,
        IN SIZE_T  Size,
        IN ULONG  Tag,
        IN USHORT  Depth
        );

    这两个函数分别是对非分页和分页Lookaside对象进行初始化。初始化完成后,可以进行申请内存操作:

    PVOID 
      ExAllocateFromNPagedLookasideList(
        IN PNPAGED_LOOKASIDE_LIST  Lookaside
        );

     

    PVOID 
      ExAllocateFromPagedLookasideList(
        IN PPAGED_LOOKASIDE_LIST  Lookaside
        );

    这两个函数分别是对非分页和分页内存的申请

    VOID 
      ExFreeToNPagedLookasideList(
        IN PNPAGED_LOOKASIDE_LIST  Lookaside,
        IN PVOID  Entry
        );

     

    VOID 
      ExFreeToPagedLookasideList(
        IN PPAGED_LOOKASIDE_LIST  Lookaside,
        IN PVOID  Entry
        );

    这两个函数分别是对非分页内存和分页内存的回收。

    VOID 
      ExDeleteNPagedLookasideList(
        IN PNPAGED_LOOKASIDE_LIST  Lookaside
        );

     

    VOID 
      ExDeletePagedLookasideList(
        IN PPAGED_LOOKASIDE_LIST  Lookaside
        );

    以上两个函数分别是对非分页和分页Lookaside对象的删除

     

    4、运行时函数

    一般编译器厂商,在发布其编译器的同时,会将运行时函数一起发布给用户。运行时函数是程序运行的时候必不可少的,它有编译器提供。针对不同的操作系统,运行时函数的实现方法不同,但接口基本保持一致。例如:malloc函数就是典型的运行时函数,所有编译器厂商都必须提供这个函数,它在不同操作系统上的实现方法就不尽相同。

    4.1、内存间复制(非重叠)

    在驱动程序开发中,经常用到内存的复制。例如,将需要的内容,从缓冲区复制到显卡内存中。DDK为程序提供了以下函数:

    VOID 
      RtlCopyMemory(
        IN VOID UNALIGNED  *Destination,//表示要复制内存的目的地址
        IN CONST VOID UNALIGNED  *Source,//表示要复制内存的源地址
        IN SIZE_T  Length//表示要复制内存的长度,单位为字节
        );

     

    4.2、内存间复制(可重叠)

    RtlCopyMemory可以复制内存,但其内部没有考虑内存重叠的情况。RtlCopyMemory函数的内部实现方式是依靠memcpy函数实现的。不能保证重叠部分是否被复制。

    为了保证重叠部分也被正确复制,C99规定memmove函数完成这个任务。这个函数对两个内存是否重叠进行了判断,这个判断却牺牲了速度。DDK用宏对memmove进行了封装,名称变为RtlMoveMemory

    VOID 
      RtlMoveMemory(
        IN VOID UNALIGNED  *Destination,
        IN CONST VOID UNALIGNED  *Source,
        IN SIZE_T  Length
        );

     

    4.3、填充内存

    驱动程序开发中,还经常用到对某段内存区域用固定字节填充。DDK为程序员提供了函数RtlFillMemory

    VOID 
      RtlFillMemory(
        IN VOID UNALIGNED  *Destination,
        IN SIZE_T  Length,
        IN UCHAR  Fill
        );

     

    在驱动程序开发中,还经常要对某段内存填零,DDK提供了宏RtlZeroMemoryRtlZeroByte

    VOID 
      RtlZeroMemory(
        IN VOID UNALIGNED  *Destination,
        IN SIZE_T  Length
        );

    VOID 
      RtlZeroBytes( 
        PVOID  Destination, 
        SIZE_T  Length 
        );

    4.4、内存比较

    驱动程序开发中,还会用到比较两块内存是否一致。该函数是RtlCompareMemory

    SIZE_T 
      RtlCompareMemory(
        IN CONST VOID  *Source1,
        IN CONST VOID  *Source2,
        IN SIZE_T  Length
        );

     

    4.5、关于运行时函数使用的注意事项

    DDK提供的标准的运行时函数名都是RtlXX形式。其中,大部分是以宏的形式给出。

    5、使用C++特性分配内存

    C++语言中分配内存时,可以使用new操作符,回收内存时使用delete操作符。但是在驱动程序开发中,使用newdelete操作符,将得到错误的链接指示。

    6、其他

    6.1、数据类型

    C语言的数据类型和DDK中对应的数据类型

     

    DDK中又添加了一种64位的无符号长整型整数,范围0~2^64-1,用LONGLONG类型表示,后面加上i64

    使用如下:

    LONGLONG val = 100i64;

    这种64位整数支持加减乘除等运算。

    还有一种64位的表示方法LARGE_INTEGER数据结构。

    定义如下:

    Typedef union _LARGE_INTEGER{

    Struct{

    ULONG LowPart;

    ULONG HighPart

    };

    Struct{

    ULONG LowPart;

    ULONG HighPart

    }u;

    LONGLONG QuadPart

     

    }

    LARGE_INTEGER是个联合体,这种设计非常巧妙。联合体中的三个元素可以认为是LARGE_INTEGER的三个定义。可以认为是由两个部分组分。一个是低32位的整数LowPart,一个是高32位的整数HighPart

    LARGE_INTEGER LargeValue;

    LargeValue.LowPart=100;

    LargeValue.HighPart = 0;

    6.2、返回状态值

    DDK大部分函数返回类型是NTSTATUS类型,查看DDK.h文件,可以看到

    typedef LONG NTSTATUS;

    NTSTATUS的定义和LONG等价。为了函数的形式统一,所有的函数的返回值都是NTSTATUS类型。

     

     

    6.3、检查内存可用性

    在驱动程序开发中,对内存的操作要格外小心。如果某段内存是只读的,而驱动程序试图去写操作,会导致系统的崩溃。同样,当某段内存不可读的情况下,驱动程序试图去读,同样会导致系统的崩溃。

    DDK提供了两个函数帮助程序员在不知道某段内存是否可读写的情况下试探内存可读写性。ProbeForReadProbeForWrite

    VOID 
      ProbeForRead(
        IN CONST VOID  *Address,
        IN SIZE_T  Length,
        IN ULONG  Alignment
        );

     

    VOID 
      ProbeForWrite(
        IN CONST VOID  *Address,
        IN SIZE_T  Length,
        IN ULONG  Alignment
        );

    Address:需要被检查的内存的地址

    Length:需要被检查的内存的长度,单位是字节

    Alignment:描述该段内存是多少字节对齐的

     

    6.4、结构化异常处理(try-except块)

    结构化异常处理是微软编译器提供的独特处理机制,这种处理方式能在一定程度上出现错误的情况下,免于程序崩溃。为了说明结构化异常,有两个概念需要说明一下:

    a)、异常:异常的概念类似于中断的概念,当程序中某种错误触发一个异常,操作系统会寻找处理这个异常的处理函数。如果程序提供错误处理函数,则进入错误处理函数,如果没有提供,则有操作系统的默认错误处理函数处理。在内核模式下,操作系统默认处理错误的办法往往很简单,直接让系统蓝屏,并在蓝屏上简单描述出错信息,之后系统就进入死机状态。所以一般程序员需要自己设置异常处理函数。

    b)、回卷:程序执行到某个地方出现异常错误时,系统会寻找出错点是否处于一个try{}块中,并进入try块提供才异常处理程序代码。如果当前try块没有提供异常处理,则会向更外一层的try块,寻找异常处理代码,直到最外层try{}块也没有提供异常处理程序代码,则交个操作系统处理。

    这种向更外一层寻找异常处理的机制,被成为回卷。一般处理异常,是通过try-except块来处理的。

    6.5、结构化异常处理(try-finally块)

    结构化异常处理的另外一种使用方法就是利用try-finally块,强迫函数在退出前执行一段代码。

    6.6、使用宏需要注意的地方

    宏一般由多行组成,用“\”代表换行。

    6.7、断言

    在驱动程序开发中,还有一个技巧,就是使用“断言”,在驱动程序使用“断言”,一般是通过使用ASSERT宏。

    ASSERT(表达式);

    如果表达式返回FALSE,表示断言失败,会引发一个异常。

    7、小结

    本章围绕着驱动程序中的内存操作进行了介绍,在驱动程序开发中,首先要注意分页内存和非分页内存的使用。同时,还需要区分物理内存地址和虚拟内存地址这两个概念。

    在驱动程序开发中,还会经常使用单向链表和双向链表等数据结构,本章对这些数据结构的使用进行了介绍。另外,在驱动程序开发中,内存复制,内存搬移,内存填充和应用程序有所区别,要使用DDK提供专用的内核函数,而不能使用C语言提供的运行时函数。

     

    以上内容参考自张帆 史彩成等编著的《Windows 驱动开发技术详解》第五章

    展开全文
  • Windows 内存管理

    千次阅读 2009-03-17 10:03:00
    1.Windows内存结构Windows系统中的每个进程都被赋予它自己的虚拟地址空间。对于32位进程来说,这个地址空间是4GB,因为32位指针可以拥有从0x00000000至0xFFFFFFFF之间的任何一个值。对于64位进程来说,则这个空间...

    1Windows的内存结构

    Windows系统中的每个进程都被赋予它自己的虚拟地址空间。对于32位进程来说,这个地址空间是4GB,因为32位指针可以拥有从0x000000000xFFFFFFFF之间的任何一个值。对于64位进程来说,则这个空间是16EB。由于每个进程可以接收它自己的私有的地址空间,因此当进程中的一个线程正在运行时,该线程也只能访问只属于它的进程的内存。属于所有其他进程的内存则隐藏着,并且不能被访问。

    每个进程的虚拟地址空间都要划分成各个分区,地址空间的分区时根据操作系统的基本实现来进行的,不同的windows内核,其分区也略有不同。下面以32windows 2000 (x86alpha处理器)

    分区

    地址范围

    作用

    NULL指针分配的区域

    0x00000000

    为了帮助掌握NULL指针的分配情况,任何读写都将引发访问违规

    0x0000FFFF

    用户方式分区

    0x00010000

    这是进程的私有空间,该分区是维护进程的大部分数据的地方。

    0x7FFEFFFF

    64k 禁止进入分区

    0x7FFF0000

    这个分区是禁止进入的,任何访问都将是违规。保留此分区是为了更加容易地实现操作系统。怕用户内存越界到内核区。

    0x7FFFFFFF

    内核方式

    0x80000000

    这个分区是存放操作系统代码的地方。用于线程调度、内存管理、文件系统支持等以及设备驱动程序的代码全部在这个分区加载。

    0xFFFFFFFF

     

     

     

     

    另外,如果用户想把用户方式分区扩大到3GB,则在x86windows 2000 advanced server版本和windows 2000 data center版本中可以加入/3GB开关到BOOT.INI文件中。使用/3GB开关后,将减少系统能够创建的线程、堆栈和其他资源的数量。此外,系统最多使用16GBRAM,而通常情况下可以使用64GBRAM。因为内核方式中没有足够的虚拟空间来管理更多的RAM

    2.地址空间中的区域

    当进程被创建并被赋予它的地址空间时,该可用空间的主体是空闲的,未分配的。若要使用该地址空间的各个部分,必须要调用virtualAlloc 函数来分配它里边的各个区域。对每一个地址空间的区域进行分配的操作称为保留(reserve

       当你保留地址空间的一个区域时,该区域必须是系统的页面大小的倍数,而且分配边界必须从一个分配粒度开始。例如x86的页面大小为4KB,分配粒度为64KB

     若要使用已保留的地址空间区域,则必须分配物理存储器,然后将该物理存储器映射到已保留的地址空间区域。这个过程叫提交物理存储器。也调用VirtualAlloc函数,但和前面保留的输入参数有所区别,可以自己查此函数。当然用户也可以在保留地址空间的同时提交物理存储器。

     在较老的操作系统中,物理存储器被视为计算机所有的RAM的容量。如果计算机有16MRAM,则加载和运行的应用程序最多可以使用16MRAM。今天的操作系统则使得磁盘空间看上去像内存一样。磁盘上的文件通常称为页文件,它包含了可以供所有进程使用的虚拟内存。(用户可以在“我的电脑->属性->高级”里面查看虚拟内存的页文件信息)

    这样,当一个应用程序通过调用VirtrualAlloc函数,将物理存储器提交给地址空间的一个区域时,地址空间实际上是从硬盘上的一个文件中进行分配的。

     当用户进程中的一个线程试图访问进程的地址空间中的一个数据块的时候。一般会发生两种情况:

    1. 线程试图访问的数据是在RAM中,则cpu只需要将虚拟地址映射到内存的物理地址中,然后执行需要的访问。

    2. 数据不在RAM中,而是放在页文件的某个地方。这时候,访问引起页面失效,cpu将通知操作系统,操作系统就从RAM中寻找一个空白页,如果找不到空白页,则必须释放一个页。如果该页面没有被修改过,则可以直接释放,否则必须先把此页面从RAM拷贝到页面交换文件,然后系统进入该页文件,找出需要访问的数据,并将数据加载到空闲的内存页面。然后,操作系统更新它的用于指明数据的虚拟内存地址现在已经映射到RAM中的相应的物理存储器地址中的表。

    3. Windows的内存管理方法

     windows提供了3种方法来进行内存管理:

    l         虚拟内存,最适合用来管理大型对象或者结构数组

    l         内存映射文件,最适合用来管理大型数据流(通常来自文件)以及在单个计算机上运行多个进程之间共享数据。

    l         内存堆栈,最适合用来管理大量的小对象。

    31 虚拟内存

     虚拟内存的使用主要有以下几个步骤:

    1. 在地址空间保留一个区域,调用函数VirtualAlloc

    2. 在保留区域中的提交物理存储器,当保留一个区域后,必须将物理存储器提交给该区域,然后才能访问该区域中包含的内存地址。系统从它的页文件中将已提交的物理存储器分配给一个区域。仍旧调用函数VirtualAlloc具体参数设置可以见msdn,当然,用户也可以一次性地进行操作保留区域和提交物理存储器。

    3. 回收虚拟内存和释放地址空间区域,调用VirtualFree函数,并且,如果要释放一个区域,必须释放该区域保留地所有地址空间。当然用户也可以只回收物理存储器而不释放区域,仍旧调用VirtualFree函数,但参数传入不同。

    32 内存映射文件

     与虚拟内存一样,内存映射文件可以用来保留一个地址空间的区域,并将物理存储器提交给该区域。他们之间的区别是,物理存储器来自一个已经位于磁盘上的文件,而不是系统的页文件。一旦该文件被映射,就可以访问它,就像整个文件被加载到了内存一样。

       内存映射文件一般用于3个不同的目的:

    1.       系统使用内存映射文件,以便加载和执行.exeDLL文件。这可以大大节省页文件空间和应用程序启动运行所需的时间

    2.       可以使用内存映射文件来访问磁盘上的数据文家爱女。这使你可以不比对文件执行i/o操作,并且可以不必对文件内容进行缓存

    3.       可以使用内存映射文件,使同一台计算机上运行的多个进程能够相互之间共享数据。

    若要使用内存映射文件,必须执行下列操作步骤:

    1 创建或打开一个文件内核对象,该对象用于标识磁盘上你想用作内存映射文件的文件 (CreateFile函数)

    2 创建一个文件映射内核对象,告诉系统该文件的大小和你打算如何访问该文件

    CreateFileMapping函数)

    3 让系统将文件映射对象的全部或一部分映射到你的地址空间

    MapViewOfFile函数,要求文件的位移是分配粒度的倍数)

    当完成对内存映射文件的使用时,必须执行下面的这些步骤将它清除:

    4)告诉系统从你的进程的地址空间中撤销文件映射内核对象的映象

    (UnmapViewOfFile函数)

    5)关闭文件映射内核对象

    CloseHandle函数,第2)步创建的对象)

    6)关闭文件内核对象

    (CloseHandle函数,第1)步创建的对象)

    利用内存映射文件,还可以实现进程之间的数据共享。数据共享的方法是通过让两个或多个进程映射同一个文件映射对象的视图,这也意味着他们将共享物理存储器的同一个页面。另外,用户也可以创建由系统的页文件支持的内存映射文件,而不是由专用硬盘文件支持的内存映射文件。这样,就不需要调用CreateFile函数,只需要给CreateFileMappingHfile参数传递INVALID_HANDLE_VALUE,并传递一个以0结尾的字符串作为pszName参数。别的进程就可以用CreateFileMapping或者OpenFilemapping函数。

     33 堆栈

     堆栈可以用来分配许多较小的数据块,例如对链接表和链接树进行管理等。堆栈的优点是,可以不考虑分配粒度和页面边界之类的问题。堆栈的缺点是,分配和释放内存块的速度比其他机制要慢,并且无法直接控制物理存储器的提交和回收。

       当进程初始化时,系统在进程的地址空间中创建一个堆栈。该堆栈为进程的默认堆栈。按照默认设置,该堆栈的地址空间区域的大小是1MB。系统可以扩大进程的默认堆栈。由于进程的默认堆栈可以供许多windows函数调用,因此对默认堆栈的访问是按顺序进行的。也就是,系统必须保证在规定的时间内,每次只有一个线程能分配和释放默认堆栈中的内存块 。当然,用户也可以在进程的地址空间中创建一些辅助堆栈。

     堆栈的一些操作函数如下(具体可以查msdn):

    1.创建堆栈    HeapCreate

    2. 从堆栈中分配内存块 HeapAlloc

    3. 改变内存块的大小 HeapReAlloc

    4. 释放内存块 HeapFree

    5. 撤销堆栈   HeapDestroy

    展开全文
  • 页目录,页表2.Windows内存管理3.CPU段式内存管理4.CPU页式内存管理 一、基本概念1. 两个内存概念物理内存:人尽皆知,就是插在主板上的内存条。他是固定的,内存条的容量多大,物理内存就有多大(集成显卡系统除外...

    本文主要内容:
    1.基本概念:物理内存、虚拟内存;物理地址、虚拟地址、逻辑地址;页目录,页表
    2.Windows内存管理
    3.CPU段式内存管理
    4.CPU页式内存管理
     
    一、基本概念
    1. 两个内存概念
    物理内存:人尽皆知,就是插在主板上的内存条。他是固定的,内存条的容量多大,物理内存就有多大(集成显卡系统除外)。但是如果程序运行很多或者程序本身很大的话,就会导致大量的物理内存占用,甚至导致物理内存消耗殆尽。
    虚拟内存:简明的说,虚拟内存就是在硬盘上划分一块页面文件,充当内存。当程序在运行时,有一部分资源还没有用上或者同时打开几个程序却只操作其中一个程序时,系统没必要将程序所有的资源都塞在物理内存中,于是,系统将这些暂时不用的资源放在虚拟内存上,等到需要时在调出来用。
    2.三个地址概念
    物理地址(physical address):用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。
    ——这个概念应该是这几个概念中最好理解的一个,但是值得一提的是,虽然可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到 最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样。所以,说它是“与 地址总线相对应”,是更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存一一对应,也是可以接受的。也许错误的理解更利于形而上的抽像。
    逻辑地址(logical address):是指由程序产生的与段相关的偏移地址部分。例如,你在进行C语言指针编程中,可以读取指针变量本身值(&操作),实际上这个值就是逻辑地址,它是相对于你当前进程数据段的地址,不和绝对物理地址相干。只有在Intel实模式下,逻辑地址才和物理地址相等(因为实模式没有分段或分页机制,Cpu不进行自动地址转换);逻辑也就是在Intel 保护模式下程序执行代码段限长内的偏移地址(假定代码段、数据段如果完全一样)。应用程序员仅需与逻辑地址打交道,而分段和分页机制对您来说是完全透明的,仅由系统编程人员涉及。应用程序员虽然自己可以直接操作内存,那也只能在操作系统给你分配的内存段操作。
    Intel为了兼容,将远古时代的段式内存管理方式保留了下来。逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。以上例,我们说的连接器为A分配的0x08111111这个地址就是逻辑地址。
    ——不过不好意思,这样说,好像又违背了Intel中段式管理中,对逻辑地址要求,“一个逻辑地址,是由一个段标识符加上一个指定段内相对地址的偏移量, 表示为 [段标识符:段内偏移量],也就是说,上例中那个0x08111111,应该表示为[A的代码段标识符: 0x08111111],这样,才完整一些”
    线性地址(linear address)或也叫虚拟地址(virtual address)
    跟逻辑地址类似,它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址。
    -------------------------------------------------------------
    每个进程都有4GB的虚拟地址空间
    这4GB分3部分 
    (1)一部分映射物理内存
    (2)一部分映射硬盘上的交换文件
    (3)一部分什么也不做
    程序中都是使用4GB的虚拟地址,访问物理内存需要使用物理地址,物理地址是放在寻址总线上的地址,以字节(8位)为单位。
    -------------------------------------------------------------
    CPU将一个虚拟内存空间中的地址转换为物理地址,需要进行两步:首先将给定一个逻辑地址(其实是段内偏移量,这个一定要理解!!!),CPU要利用其段式内存管理单元,先将为个逻辑地址转换成一个线程地址,再利用其页式内存管理单元,转换为最终物理地址。
    这样做两次转换,的确是非常麻烦而且没有必要的,因为直接可以把线性地址抽像给进程。之所以这样冗余,Intel完全是为了兼容而已。
    3.页表、页目录概念
    使用了分页机制之后,4G的地址空间被分成了固定大小的页,每一页或者被映射到物理内存,或者被映射到硬盘上的交换文件中,或者没有映射任何东西。对于一般程序来说,4G的地址空间,只有一小部分映射了物理内存,大片大片的部分是没有映射任何东西。物理内存也被分页,来映射地址空间。对于32bit的 Win2k,页的大小是4K字节。CPU用来把虚拟地址转换成物理地址的信息存放在叫做页目录和页表的结构里。
    物理内存分页,一个物理页的大小为4K字节,第0个物理页从物理地址 0x00000000 处开始。由于页的大小为4KB,就是0x1000字节,所以第1页从物理地址 0x00001000处开始。第2页从物理地址0x00002000处开始。可以看到由于页的大小是4KB,所以只需要32bit的地址中高20bit来寻址物理页。
    页目录:  一个页目录大小为4K字节,放在一个物理页中。由1024个4字节的页目录项组成。页目录项的大小为4 个字节(32bit),所以一个页目录中有1024个页目录项。页目录中的每一项的内容(每项4个字节)高20bit用来放一个页表(页表放在一个物理页中)的物理地址,低12bit放着一些标志。
    页表:  一个页表的大小为4K字节,放在一个物理页中。由1024个4字节的页表项组成。页表项的大小为4个字节 (32bit),所以一个页表中有1024个页表项。页表中的每一项的内容(每项4个字节,32bit)高20bit用来放一个物理页的物理地址,低 12bit放着一些标志。
    对于x86系统,页目录的物理地址放在CPU的CR3寄存器中。
    4. 虚拟地址转换成物理地址
    一个虚拟地址大小为4字节,其中包含找到物理地址的信息
    虚拟地址分3部分
    (1)31-22位(10位)是页目录中的索引
    (2)21-12位(10位)是页表中的索引
    (2)11-0位(12位)是页内偏移

    转换过程:
    首先通过CR3找到页目录所在物理页-》根据虚拟地址中的31-22找到该页目录项-》通过该页目录项找到该虚拟地址对应的页表地址-》根据虚拟地址21-12找到物理页的物理地址-》更具虚拟地址的11-0位作为偏移加上该物理页的地址就找到了 该虚拟地址对应的物理地址
    CPU把虚拟地址转换成物理地址:一个虚拟地址,大小4个字节(32bit),包含着找到物理地址的信息,分为3个部分:第22位到第31位这10位(最高10位)是页目录中的索引,第 12位到第21位这10位是页表中的索引,第0位到第11位这12位(低12位)是页内偏移。对于一个要转换成物理地址的虚拟地址,CPU首先根据CR3 中的值,找到页目录所在的物理页。然后根据虚拟地址的第22位到第31位这10位(最高的10bit)的值作为索引,找到相应的页目录项(PDE, page directory entry),页目录项中有这个虚拟地址所对应页表的物理地址。有了页表的物理地址,根据虚拟地址的第12位到第21位这10位的值作为索引,找到该页表中相应的页表项(PTE,page table entry),页表项中就有这个虚拟地址所对应物理页的物理地址。最后用虚拟地址的最低12位,也就是页内偏移,加上这个物理页的物理地址,就得到了该虚拟地址所对应的物理地址。
    -------------------------------------------------------------
    一个页目录有1024项,虚拟地址最高的10bit刚好可以索引1024项(2的10次方等于1024)。一个页表也有1024项,虚拟地址中间部分的 10bit,刚好索引1024项。虚拟地址最低的12bit(2的12次方等于4096),作为页内偏移,刚好可以索引4KB,也就是一个物理页中的每个字节。
    -------------------------------------------------------------
    一个虚拟地址转换成物理地址的计算过程就是,处理器通过CR3找到当前页目录所在物理页,取虚拟地址的高10bit,然后把这10bit右移2bit(因为每个页目录项4个字节长,右移2bit相当于乘4)得到在该页中的地址,取出该地址处PDE(4个字节),就找到了该虚拟地址对应页表所在物理页,取虚拟地址第12位到第21位这10位,然后把这10bit右移2bit(因为每个页表项4个字节长,右移2bit相当于乘4)得到在该页中的地址,取出该地址处的PTE(4个字节),就找到了该虚拟地址对应物理页的地址,最后加上12bit的页内偏移得到了物理地址。
    -------------------------------------------------------------
    32bit的一个指针,可以寻址范围0x00000000-0xFFFFFFFF,4GB大小。也就是说一个32bit的指针可以寻址整个4GB地址空间的每一个字节。一个页表项负责4K的地址空间和物理内存的映射,一个页表1024项,也就是负责1024*4k=4M的地址空间的映射。一个页目录项,对应一个页表。一个页目录有1024项,也就对应着1024个页表,每个页表负责4M地址空间的映射。1024个页表负责1024*4M=4G的地址空间映射。一个进程有一个页目录。所以以页为单位,页目录和页表可以保证4G的地址空间中的每页和物理内存的映射。
    -------------------------------------------------------------
    每个进程都有自己的4G地址空间,从0x00000000-0xFFFFFFFF。通过每个进程自己的一套页目录和页表来实现。由于每个进程有自己的页目录和页表,所以每个进程的地址空间映射的物理内存是不一样的。两个进程的同一个虚拟地址处(如果都有物理内存映射)的值一般是不同的,因为他们往往对应不同的物理页。

    4G地址空间中低2G,0x00000000-0x7FFFFFFF是用户地址空间,4G地址空间中高2G,即0x80000000-0xFFFFFFFF 是系统地址空间。访问系统地址空间需要程序有ring0的权限。
     
    二. windows内存原理
     
    主要的内容如下:
    1.概述
    2.虚拟内存
    3.物理内存
    4.映射

    1.概述:
    windows中 我们一般编程时接触的都是线性地址 也就是我们所说的虚拟地址,然而很不幸在我不断成长的过程中发现原来线性地址是操作系统自己意淫出来的,根本就不是我们数据真实存在的地方.换句话说我们在0x80000000(虚拟地址)的地方写入了"UESTC"这个字符串,但是我们这个字符串并不真实存在于物理地址的0x80000000这里.再说了真实的物理地址是利用一段N长的数组来定位的(额~看不懂这句话没关系,一会看到物理地址那你就明白了).但是为什么windows乃至linux都需要采取这种方式来寻址呢?原因很简单 为了安全.听说过保护模式吧?顾名思义就是这个模式下加入了保护系统安全的措施,同样采用线性地址也是所谓的安全措施之一.
        我们假设下如果没有使用线性地址,那么我们可以直接访问物理地址,但是这样的话当我们往内存写东西的时候操作系统无法检查这块内存是否可写,换句话说操作系统无法实现对页面访问控制.这点是很可怕的事情,就如win9x那样没事在应用态往内核地址写东西,还有没有天理了~~
        由于操作系统的安全需要,催生了虚拟地址的应用.在CPU中有个叫MMU(应该是Memory Manage Unit 内存管理单元)的东西,专门负责线性地址和物理地址之间的转化.我们每次读写内存,从CPU的结构看来不是都要经过ALU么,ALU拿到虚拟地址后扔给MMU转化成物理地址后再把数据读入寄存器中.过程就是如此.

    2.虚拟内存
        我们编程时所面对的都是虚拟地址,对于每个进程来说都拥有4G的虚拟内存(小补充: 4G虚拟内存中,高2G内存属于内核部分,是所有进程共有的,低2G内存数据是进程独有的,每个进程低2G内存都不一样),但注意的是虚拟地址是操作系统自己意淫出来的,打个比方就是说想法还未付诸实践,所以不构成任何资源损失.比如我们要在0x80000000的地方写个"UESTC"的时候,操作系统就会将这个虚拟地址映射到一块物理地址A中,你写这块虚拟地址就相当于写物理地址A.但是加入我们只申请了一段1KB的虚拟内存空间,并未读写,系统是不会分配任何物理内存的,只有当虚拟内存要使用时系统才会分配相应的物理空间.
        下面要说些细节点的了,其实说白了虚拟内存的管理是由一堆数据结构来实现的,下面给出这些数据结构:
    (懒得打那么多 就只打出重要的部分~~)
    在EPROCESS中有个数据结构如下:
    typedef struct _MADDRESS_SPACE
    {
        PMEMORY_AREA MemoryAreaRoot ; //这个指针指向一颗二叉排序树,想必学过数据结构的朋友都晓得吧~~嘿嘿~~ 主要是这个情况下采用二叉排序树能加快内存的搜索速度 
        ...
        ...
        ...
    }MADDRESS_SPACE , *PMADDRESS_SPACE ;

    然而这颗二叉排序树的节点结构结构是这样的:
    typedef struct _MEMORY_AREA
    {
        PVOID StartingAddress ; //虚拟内存段的起始地址
        PVOID EndingAddress ;   //虚拟内存段的结束地址
        struct _MEMORY_AREA *Parent ; //该节点的老爹
        struct _MEMORY_AREA *LeftChild ; //该节点的左儿子
        struct _MEMORY_AREA *RrightChild ; //该节点的左儿子
        ...
        ...
        ...

    }MEMORY_AREA , *PMEMORY_AREA ;
        这个节点内主要记录了已分配的虚拟内存空间,如果要申请虚拟内存空间就跑来这里创建个节点就好了,如果要删除空间同样把对应节点删除就好了.不过说来简单,其实还会涉及到很多操作,比如要平衡这棵树什么的.
        那么我们在分配虚拟内存空间时系统会跑去找到这颗树,然后通过一定算法遍历这颗树,找到符合条件的内存空隙(未分配的内存空间),创建个节点挂到这颗树上,返回起始地址就完成了.


    3.物理内存
        接下来就到物理内存的东东了,其实呢 物理内存的管理是基于一个数组来管理的,听说过分页机制吧,下面说下分页.windows下分页是4kb一页 那么假设我们物理内存有4GB 那么windows会将这4GB空间分页,分成4GB/4KB = 1M页    那么每一页(就是4KB)的物理空间都由一个叫PHYSICAL_PAGE的数据结构管理,这个数据结构就不写啦....一来我写的手酸 二来看的人也累~~说说思路就好了.
        接着以上假设 4GB的内存 操作系统就会相应产生一个PHYSICAL_PAGE数组,这个数组有多少个元素呢?有1M个 正好覆盖了4GB的物理地址.这就是所谓的分页机制的原型.说白了就是把内存按4k划分来管理.那么物理地址就好定位了,所谓的物理内存地址就可以直接以数组下标来表示,其实这里的物理内存地址指的是物理内存地址的页面号... 具体地址还是要根据虚拟内存地址的低12位和物理内存的页面号一起确定的 
         再说下物理内存的管理吧,在内核下有3个队列 这些队列内的元素就是上边所说的PHYSICAL_PAGE结构 
    它们分别是:
    A.已分配的内存队列 :存放正被使用的内存
    B.待清理的内存队列 :存放已被释放的内存,但是这些内存未被清理(清零)
    C.空闲队列 :存放可使用的空闲内存

    系统管理流程如下:
    1).每隔一段时间,系统会自动从B队列中提取队列元素进行清理,然后放入空闲队列中.
    2).每当释放物理内存时,系统会将要释放的内存从A队列提取出来,放入B队列中.
    3).申请内存时,系统将要分配的内存从C队列中提取出来,放入A队列中

    4.映射 
        说到映射得先从虚拟内存的32位地址说起.在CPU中存在个CR3寄存器,里面存放着每个进程的页目录地址

    我们可以把转换过程分成几步看
    1.根据CR3(注:CR3中的值是物理地址)的值我们可以定位到当前进程的页目录基址,然后通过虚拟地址的高10位做偏移量来获得指定的PDE(Page Directory Entry),PDE内容有4字节,高20位部分用来做页表基址,剩下的比特位用来实现权限控制之类的东西了.系统只要检测相应的比特位就可以实现内存的权限控制了.
    2.通过PDE提供的基址加上虚拟内存的中10位(21-12)做偏移量就找到了页表PTE(Page Table Entry)地址,然后PTE的高20位就是物理内存的基址了(其实就是那个PHYSICAL_PAGE数组的下标号....),剩下的比特位同样用于访问控制之类的.
    3.通过虚拟内存的低12位加上PTE中高20位做基址就可以确定唯一的物理内存了.

       
    三. CPU段式内存管理,逻辑地址如何转换为线性地址
    一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,如图:

    Windows内存管理 - 啸百川 - 啸百川的博客

    最后两位涉及权限检查,本贴中不包含。

    索引号,或者直接理解成数组下标——那它总要对应一个数组吧,它又是什么东东的索引呢?这个东东就是“段描述符(segment descriptor)”,呵呵,段描述符具体地址描述了一个段(对于“段”这个字眼的理解,我是把它想像成,拿了一把刀,把虚拟内存,砍成若干的截—— 段)。这样,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描 述符就描述了一个段,我刚才对段的抽像不太准确,因为看看描述符里面究竟有什么东东——也就是它究竟是如何描述的,就理解段究竟有什么东东了,每一个段描 述符由8个字节组成,如下图:

    Windows内存管理 - 啸百川 - 啸百川的博客

    这些东东很复杂,虽然可以利用一个数据结构来定义它,不过,我这里只关心一样,就是Base字段,它描述了一个段的开始位置的线性地址。

    Intel设计的本意是,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表 (LDT)”中。那究竟什么时候该用GDT,什么时候该用LDT呢?这是由段选择符中的T1字段表示的,=0,表示用GDT,=1表示用LDT。

    GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。

    好多概念,像绕口令一样。这张图看起来要直观些:

    Windows内存管理 - 啸百川 - 啸百川的博客

    首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],
    1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
    2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。
    3、把Base + offset,就是要转换的线性地址了。

    还是挺简单的,对于软件来讲,原则上就需要把硬件转换所需的信息准备好,就可以让硬件来完成这个转换了。OK,来看看Linux怎么做的。

    Linux的段式管理
    Intel要求两次转换,这样虽说是兼容了,但是却是很冗余,呵呵,没办法,硬件要求这样做了,软件就只能照办,怎么着也得形式主义一样。
    另一方面,其它某些硬件平台,没有二次转换的概念,Linux也需要提供一个高层抽像,来提供一个统一的界面。所以,Linux的段式管理,事实上只是“哄骗”了一下硬件而已。

    按照Intel的本意,全局的用GDT,每个进程自己的用LDT——不过Linux则对所有的进程都使用了相同的段来对指令和数据寻址。即用户数据段,用 户代码段,对应的,内核中的是内核数据段和内核代码段。这样做没有什么奇怪的,本来就是走形式嘛,像我们写年终总结一样。
    [Copy to clipboard] [ - ]
    CODE:
    #define GDT_ENTRY_DEFAULT_USER_CS 14
    #define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS * 8 + 3)

    #define GDT_ENTRY_DEFAULT_USER_DS 15
    #define __USER_DS (GDT_ENTRY_DEFAULT_USER_DS * 8 + 3)

    #define GDT_ENTRY_KERNEL_BASE 12

    #define GDT_ENTRY_KERNEL_CS (GDT_ENTRY_KERNEL_BASE + 0)
    #define __KERNEL_CS (GDT_ENTRY_KERNEL_CS * 8)

    #define GDT_ENTRY_KERNEL_DS (GDT_ENTRY_KERNEL_BASE + 1)
    #define __KERNEL_DS (GDT_ENTRY_KERNEL_DS * 8)
    把其中的宏替换成数值,则为:
    [Copy to clipboard] [ - ]
    CODE:
    #define __USER_CS 115        [00000000 1110   0   11]
    #define __USER_DS 123        [00000000 1111   0   11]
    #define __KERNEL_CS 96    [00000000 1100   0   00]
    #define __KERNEL_DS 104 [00000000 1101   0   00]
    方括号后是这四个段选择符的16位二制表示,它们的索引号和T1字段值也可以算出来了
    [Copy to clipboard] [ - ]
    CODE:
    __USER_CS              index= 14 T1=0
    __USER_DS             index= 15 T1=0
    __KERNEL_CS           index=   12   T1=0
    __KERNEL_DS           index= 13 T1=0
    T1均为0,则表示都使用了GDT,再来看初始化GDT的内容中相应的12-15项(arch/i386/head.S):
    [Copy to clipboard] [ - ]
    CODE:
    .quad 0x00cf9a000000ffff /* 0x60 kernel 4GB code at 0x00000000 */
    .quad 0x00cf92000000ffff /* 0x68 kernel 4GB data at 0x00000000 */
    .quad 0x00cffa000000ffff /* 0x73 user 4GB code at 0x00000000 */
    .quad 0x00cff2000000ffff /* 0x7b user 4GB data at 0x00000000 */

    按照前面段描述符表中的描述,可以把它们展开,发现其16-31位全为0,即四个段的基地址全为0。
    这样,给定一个段内偏移地址,按照前面转换公式,0 + 段内偏移,转换为线性地址,可以得出重要的结论,“在Linux下,逻辑地址与线性地址总是一致(是一致,不是有些人说的相同)的,即逻辑地址的偏移量字段的值与线性地址的值总是相同的。!!!”
    忽略了太多的细节,例如段的权限检查。呵呵。
    Linux中,绝大部份进程并不例用LDT,除非使用Wine ,仿真Windows程序的时候。

    四.CPU页式内存管理

    CPU的页式内存管理单元,负责把一个线性地址,最终翻译为一个物理地址。从管理和效率的角度出发,线性地址被分为以固定长度为单位的组,称为页 (page),例如一个32位的机器,线性地址最大可为4G,可以用4KB为一个页来划分,这页,整个线性地址就被划分为一个tatol_page [2^20]的大数组,共有2的20个次方个页。这个大数组我们称之为页目录。目录中的每一个目录项,就是一个地址——对应的页的地址。
    另一类“页”,我们称之为物理页,或者是页框、页桢的。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。
    这里注意到,这个total_page数组有2^20个成员,每个成员是一个地址(32位机,一个地址也就是4字节),那么要单单要表示这么一个数组,就要占去4MB的内存空间。为了节省空间,引入了一个二级管理模式的机器来组织分页单元。文字描述太累,看图直观一些:

    Windows内存管理 - 啸百川 - 啸百川的博客

    如上图,
    1、分页单元中,页目录是唯一的,它的地址放在CPU的cr3寄存器中,是进行地址转换的开始点。万里长征就从此长始了。
    2、每一个活动的进程,因为都有其独立的对应的虚似内存(页目录也是唯一的),那么它也对应了一个独立的页目录地址。——运行一个进程,需要将它的页目录地址放到cr3寄存器中,将别个的保存下来。
    3、每一个32位的线性地址被划分为三部份,面目录索引(10位):页表索引(10位):偏移(12位)
    依据以下步骤进行转换:
    1、从cr3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器);
    2、根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。
    3、根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址;
    4、将页的起始地址与线性地址中最后12位相加,得到最终我们想要的葫芦;

    这个转换过程,应该说还是非常简单地。全部由硬件完成,虽然多了一道手续,但是节约了大量的内存,还是值得的。那么再简单地验证一下:
    1、这样的二级模式是否仍能够表示4G的地址;
    页目录共有:2^10项,也就是说有这么多个页表
    每个目表对应了:2^10页;
    每个页中可寻址:2^12个字节。
    还是2^32 = 4GB

    2、这样的二级模式是否真的节约了空间;
    也就是算一下页目录项和页表项共占空间 (2^10 * 4 + 2 ^10 *4) = 8KB。哎,……怎么说呢!!!(真的减少了吗?因该是增加了吧,(4 + 2^10 * 4 + 2 ^10 * 2 ^10 *4) = 4100KB+4Byte)

    值得一提的是,虽然页目录和页表中的项,都是4个字节,32位,但是它们都只用高20位,低12位屏蔽为0——把页表的低12屏蔽为0,是很好理解的,因 为这样,它刚好和一个页面大小对应起来,大家都成整数增加。计算起来就方便多了。但是,为什么同时也要把页目录低12位屏蔽掉呢?因为按同样的道理,只要 屏蔽其低10位就可以了,不过我想,因为12>10,这样,可以让页目录和页表使用相同的数据结构,方便。

    本贴只介绍一般性转换的原理,扩展分页、页的保护机制、PAE模式的分页这些麻烦点的东东就不啰嗦了……可以参考其它专业书籍。
     
     
     
     Win32通过一个两层的表结构来实现地址映射,因为每个进程都拥有私有的4G的虚拟内存空间,相应的,每个进程都有自己的层次表结构来实现其地址映射。
          第一层称为页目录,实际就是一个内存页,Win32的内存页有4KB大小,这个内存页以4个字节分为1024项,每一项称为“页目录项”(PDE);
          第二层称为页表,这一层共有1024个页表,页表结构与页目录相似,每个页表也都是一个内存页,这个内存页以4KB的大小被分为1024项,页表的每一项被称为页表项(PTE),易知共有1024×1024个页表项。每一个页表项对应一个物理内存中的某一个“内存页”,即共有1024×1024个物理内存页,每个物理内存页为4KB,这样就可以索引到4G大小的虚拟物理内存。
    如下图所示(注下图中的页目录项和页表项的大小应该是4个字节,而不是4kB):

          Win32提供了4GB大小的虚拟地址空间。因此每个虚拟地址都是一个32位的整数值,也就是我们平时所说的指针,即指针的大小为4B。它由三部分组成,如下图:

          这三个部分的第一部分,即前10位为页目录下标,用来寻址页目录项,页目录项刚好1024个。找到页目录项后,找对页目录项对应的的页表。第二部分则是用来在页表内寻址,用来找到页表项,共有1024个页表项,通过页表项找到物理内存页。第三部分用来在物理内存页中找到对应的字节,一个页的大小是4KB,12位刚好可以满足寻址要求。
    具体的例子:
    假设一个线程正在访问一个指针(Win32的指针指的就是虚拟地址)指向的数据,此指针指为0x2A8E317F,下图表示了这一个过程:

    0x2A8E317F的二进制写法为0010101010_0011100011_000101111111,为了方便我们把它分为三个部分。
    首先按照0010101010寻址,找到页目录项。因为一个页目录项为4KB,那么先将0010101010左移两位,001010101000(0x2A8),用此下标找到页目录项,然后根据此页目录项定位到下一层的某个页表。
    然后按照0011100011寻址,在上一步找到页表中寻找页表项。寻址方法与上述方法类似。找到页表项后,就可以找到对应的物理内存页。
    最后按照000101111111寻址,寻找页内偏移。
          上面的假设的是此数据已在物理内存中,其实判断访问的数据是否在内存中也是在地址映射过程中完成的。Win32系统总是假设数据已在物理内存中,并进行地址映射。页表项中有一位标志位,用来标识包含此数据的页是否在物理内存中,如果在的话,就直接做地址映射,否则,抛出缺页中断,此时页表项也可标识包含此数据的页是否在调页文件中(外存),如果不在则访问违例,程序将会退出,如果在,页表项会查出此数据页在哪个调页文件中,然后将此数据页调入物理内存,再继续进行地址映射。为了实现每个进程拥有私有4G的虚拟地址空间,也就是说每个进程都拥有自己的页目录和页表结构,对不同进程而言,即使是相同的指针(虚拟地址)被不同的进程映射到的物理地址也是不同的,这也意味着在进程之间传递指针是没有意义的。


    Linux的页式内存管理
    原理上来讲,Linux只需要为每个进程分配好所需数据结构,放到内存中,然后在调度进程的时候,切换寄存器cr3,剩下的就交给硬件来完成了(呵呵,事实上要复杂得多,不过偶只分析最基本的流程)。

    前面说了i386的二级页管理架构,不过有些CPU,还有三级,甚至四级架构,Linux为了在更高层次提供抽像,为每个CPU提供统一的界面。提供了一个四层页管理架构,来兼容这些二级、三级、四级管理架构的CPU。这四级分别为:

    页全局目录PGD(对应刚才的页目录)
    页上级目录PUD(新引进的)
    页中间目录PMD(也就新引进的)
    页表PT(对应刚才的页表)。

    整个转换依据硬件转换原理,只是多了二次数组的索引罢了,如下图:

    Windows内存管理 - 啸百川 - 啸百川的博客

    那么,对于使用二级管理架构32位的硬件,现在又是四级转换了,它们怎么能够协调地工作起来呢?嗯,来看这种情况下,怎么来划分线性地址吧!
    从硬件的角度,32位地址被分成了三部份——也就是说,不管理软件怎么做,最终落实到硬件,也只认识这三位老大。
    从软件的角度,由于多引入了两部份,,也就是说,共有五部份。——要让二层架构的硬件认识五部份也很容易,在地址划分的时候,将页上级目录和页中间目录的长度设置为0就可以了。
    这样,操作系统见到的是五部份,硬件还是按它死板的三部份划分,也不会出错,也就是说大家共建了和谐计算机系统。

    这样,虽说是多此一举,但是考虑到64位地址,使用四层转换架构的CPU,我们就不再把中间两个设为0了,这样,软件与硬件再次和谐——抽像就是强大呀!!!

    例如,一个逻辑地址已经被转换成了线性地址,0x08147258,换成二制进,也就是:
    0000100000 0101000111 001001011000
    内核对这个地址进行划分
    PGD = 0000100000
    PUD = 0
    PMD = 0
    PT = 0101000111
    offset = 001001011000

    现在来理解Linux针对硬件的花招,因为硬件根本看不到所谓PUD,PMD,所以,本质上要求PGD索引,直接就对应了PT的地址。而不是再到PUD和 PMD中去查数组(虽然它们两个在线性地址中,长度为0,2^0 =1,也就是说,它们都是有一个数组元素的数组),那么,内核如何合理安排地址呢?
    从软件的角度上来讲,因为它的项只有一个,32位,刚好可以存放与PGD中长度一样的地址指针。那么所谓先到PUD,到到PMD中做映射转换,就变成了保 持原值不变,一一转手就可以了。这样,就实现了“逻辑上指向一个PUD,再指向一个PDM,但在物理上是直接指向相应的PT的这个抽像,因为硬件根本不知 道有PUD、PMD这个东西”。

    然后交给硬件,硬件对这个地址进行划分,看到的是:
    页目录 = 0000100000
    PT = 0101000111
    offset = 001001011000
    嗯,先根据0000100000(32),在页目录数组中索引,找到其元素中的地址,取其高20位,找到页表的地址,页表的地址是由内核动态分配的,接着,再加一个offset,就是最终的物理地址了。
     
     
    五. 存储方式
    保护模式现代操作系统的基础,理解他是我们要翻越的第一座山。保护模式是相对实模式而言的,他们是处理器的两种工作方式。很久以前大家使用的dos就是运行在实模式下,而现在的windows操作系统则是运行在保护模式下。两种运行模式有着较大的不同,
    实模式由于是由8086/8088发展而来因此他更像是一个运行单片机的简单模式,计算机启动后首先进入的就是实模式,通过8086/8088只有20根地址线所以它的寻址范围只有2的20次幂,即1M。内存的访问方式就是我们熟悉的seg:offset逻辑地址方式,例如我们给出地址逻辑地址它将在cpu内转换为20的物理地址,即将seg左移4位再加上offset值。例如地址1000h:5678h,则物理地址为10000h+5678h=15678h。实模式在后续的cpu中被保留了下来,但实模式的局限性是很明显的,由于使用seg:offset逻辑地址只能访问1M多一点的内存空间,在拥有32根地址线的cpu中访问1M以上的空间则变得很困难。而且随着计算机的不断发展实模式的工作方式越来越不能满足计算机对资源(存储资源和cpu资源等等)的管理,由此产生了新的管理方式——保护模式。
    80386及以上的处理器功能要大大超过其先前的处理器,但只有在保护模式下,处理器才能发挥作用。在保护模式下,全部32根地址线有效,可寻址4G的物理地址空间;扩充的存储分段机制和可选的存储器分页机制,不仅为存储器共享和保护提供了硬件支持,而且为实现虚拟存储器提供了硬件支持;支持多任务;4个特权级和完善的特权级检查机制,实现了数据的安全和保密。计算机启动后首先进入的就是实模式,通过设置相应的寄存器才能进入保护模式(以后介绍)。保护模式是一个整体的工作方式,但分步讨论由浅入深更利于学习。

    存储方式主要体现在内存访问方式上,由于兼容和IA32框架的限制,保护模式在内存访问上延用了实模式下的seg:offset的形式(即:逻辑地址),其实seg:offset的形式在保护模式下只是一个躯壳,内部的存储方式与实模式截然不同。在保护模式下逻辑地址并不是直接转换为物理地址,而是将逻辑地址首先转换为线性地址,再将线性地址转换为物理地址。

    线性地址是个新概念,但大家不要把它想的过于复杂,简单的说他就是0000000h~ffffffffh(即0~4G)的线性结构,是32个bite位能表示的一段连续的地址,但他是一个概念上的地址,是个抽象的地址,并不存在在现实之中。线性地址地址主要是为分页机制而产生的。处理器在得到逻辑地址后首先通过分段机制转换为线性地址,线性地址再通过分页机制转换为物理地址最后读取数据。
     
    分段机制是必须的,分页机制是可选的,当不使用分页的时候线性地址将直接映射为物理地址,设立分页机制的目的主要是为了实现虚拟存储(分页机制在后面介绍)。先来介绍一下分段机制,以下文字是介绍如何由逻辑地址转换为线性地址。
    分段机制在保护模式中是不能被绕过得,回到我们的seg:offset地址结构,在保护模式中seg有个新名字叫做“段选择子”(seg..selector)。段选择子、GDT、LDT构成了保护模式的存储结构,GDT、LDT分别叫做全局描述符表和局部描述符表,描述符表是一个线性表(数组),表中存放的是描述符。
    “描述符”是保护模式中的一个新概念,它是一个8字节的数据结构,它的作用主要是描述一个段(还有其他作用以后再说),用描述表中记录的段基址加上逻辑地址(sel:offset)的offset转换成线性地址。描述符主要包括三部分:段基址(Base)、段限制(Limit)、段属性(Attr)。一个任务会涉及多个段,每个段需要一个描述符来描述,为了便于组织管理,80386及以后处理器把描述符组织成表,即描述符表。在保护模式中存在三种描述符表 “全局描述符表”(GDT)、“局部描述符表”(LDT)和中断描述符表(IDT)(IDT在以后讨论)。
    (1)全局描述符表GDT(Global Descriptor Table)在整个系统中,全局描述符表GDT只有一张,GDT可以被放在内存的任何位置,但CPU必须知道GDT的入口,也就是基地址放在哪里,Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此积存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。GDTR中存放的是GDT在内存中的基地址和其表长界限。

    (2)段选择子(Selector)由GDTR访问全局描述符表是通过“段选择子”(实模式下的段寄存器)来完成的,如图三①步。段选择子是一个16位的寄存器(同实模式下的段寄存器相同)

    段选择子包括三部分:描述符索引(index)、TI、请求特权级(RPL)。他的index(描述符索引)部分表示所需要的段的描述符在描述符表的位置,由这个位置再根据在GDTR中存储的描述符表基址就可以找到相应的描述符(如图三①步)。然后用描述符表中的段基址加上逻辑地址(SEL:OFFSET)的OFFSET就可以转换成线性地址(如图三②步),段选择子中的TI值只有一位0或1,0代表选择子是在GDT选择,1代表选择子是在LDT选择。请求特权级(RPL)则代表选择子的特权级,共有4个特权级(0级、1级、2级、3级)。例如给出逻辑地址:21h:12345678h转换为线性地址
    a. 选择子SEL=21h=0000000000100 0 01b 他代表的意思是:选择子的index=4即100b选择GDT中的第4个描述符;TI=0代表选择子是在GDT选择;左后的01b代表特权级RPL=1
    b. OFFSET=12345678h若此时GDT第四个描述符中描述的段基址(Base)为11111111h,则线性地址=11111111h+12345678h=23456789h
    (3)局部描述符表LDT(Local Descriptor Table)局部描述符表可以有若干张,每个任务可以有一张。我们可以这样理解GDT和LDT:GDT为一级描述符表,LDT为二级描述符表。如图五

    LDT和GDT从本质上说是相同的,只是LDT嵌套在GDT之中。LDTR记录局部描述符表的起始位置,与GDTR不同LDTR的内容是一个段选择子。由于LDT本身同样是一段内存,也是一个段,所以它也有个描述符描述它,这个描述符就存储在GDT中,对应这个表述符也会有一个选择子,LDTR装载的就是这样一个选择子。LDTR可以在程序中随时改变,通过使用lldt指令。如图五,如果装载的是Selector 2则LDTR指向的是表LDT2。举个例子:如果我们想在表LDT2中选择第三个描述符所描述的段的地址12345678h。
    1. 首先需要装载LDTR使它指向LDT2 使用指令lldt将Select2装载到LDTR
    2. 通过逻辑地址(SEL:OFFSET)访问时SEL的index=3代表选择第三个描述符;TI=1代表选择子是在LDT选择,此时LDTR指向的是LDT2,所以是在LDT2中选择,此时的SEL值为1Ch(二进制为11 1 00b)。OFFSET=12345678h。逻辑地址为1C:12345678h
    3. 由SEL选择出描述符,由描述符中的基址(Base)加上OFFSET可得到线性地址,例如基址是11111111h,则线性地址=11111111h+12345678h=23456789h
    4. 此时若再想访问LDT1中的第三个描述符,只要使用lldt指令将选择子Selector 1装入再执行2、3两步就可以了(因为此时LDTR又指向了LDT1)
    由于每个进程都有自己的一套程序段、数据段、堆栈段,有了局部描述符表则可以将每个进程的程序段、数据段、堆栈段封装在一起,只要改变LDTR就可以实现对不同进程的段进行访问。
    存储方式是保护模式的基础,学习他主要注意与实模式下的存储模式的对比,总的思想就是首先通过段选择子在描述符表中找到相应段的描述符,根据描述符中的段基址首先确定段的位置,再通过OFFSET加上段基址计算出线性地址。

    展开全文
  • Windows驱动开发(2) - Windows内存管理

    千次阅读 2016-04-10 21:30:23
    Windows驱动开发(2) - Windows内存管理1、内存管理概念1.1 物理内存32位的CPU的寻址能力为4GB(2^32)个字节。用户最多可以使用4GB的真实物理内存。PC中的很多设备都提供了自己的设备内存,这部分的内存会映射到PC的...

    Windows驱动开发(2) - Windows内存管理

    1、内存管理概念

    1.1 物理内存

    32位的CPU的寻址能力为4GB(2^32)个字节。用户最多可以使用4GB的真实物理内存。PC中的很多设备都提供了自己的设备内存,这部分的内存会映射到PC的物理内存上。

    1.2 虚拟内存

    Windows的所有程序(ring0,ring3),可以操作的都是虚拟内存。CPU中寄存器CR0一个位PG位来告诉系统是否分页的。1为允许分页。DDK中宏PAGE_SIZE记录着分页大小,一般为4K,4GB的虚拟内存会被分割成1M个单元。
    Memory map
    图 物理内存的映射

    1.3 用户模式地址和内核模式地址

    4G虚拟地址中,低2G(0~0x7FFFFFFFF)为用户模式,高2G(0x80000000~0xFFFFFFFF)为内核模式。Windows规定用户态程序只能访问用户模式地址,而内核态程序可以访问整个4G虚拟地址。进程切换时,所有进程的内核地址映射完全一致,进程切换时改变,只是改变用户模式地址的映射。
    User mode and kernel mode
    图 用户模式和内核模式

    1.4 驱动与进程的关系

    Windwos驱动程序里的不同例程运行在不同的进程中。
    打印当前进程的进程名:

    void DisplayItsProcessName()
    {
        //得到当前进程
        PEPROCESS pEProcess = PsGetCurrentProcess();
        //得到当前进程名称
        PTSTR = ProcessName = (PTSTR)((ULONG)pEProcess + 0x174);
        KdPrint(("%s\n", ProcessName));
    }

    1.5 分页内存与非分页内存

    可以交换到文件中的虚拟内存页面称为分页内存,否则称为非分页内存。当程序的中断请求级大于等于DISPATCH_LEVEL时,程序只能使用非分页内存。
    指定某个例程和某个全局变量是载入分页内存还是非分页内存,需要做如下定义

    #define PAGEDCODE code_seg("PAGE")
    #define LOCKEDCODE code_seg()
    #define INITCODE code_seg("INIT")
    #define PAGEDDATA data_seg("PAGE")
    #define LOCKEDDATA data_seg()
    #define INITDATA data_seg("INIT")

    将PAGEDCODE 等放在函数前来表现是可否可以分页等情况。

    #pragma PAGEDCODE
    VOID SomeFunction()
    {
        PAGED_CODE();
        //其他代码
    }

    PAGED_CODE()是DDK提供的宏,它只在check版本中生效,来检查运行是否低于DISPATCH_LEVEL的中断请求级,如果不低于则产生断言。

    1.6 分配内核内存

    Windows驱动程序使用的内存资源非常珍贵,栈空间也不像应用程序那么大,所以在定义大型结构体是应在堆中申请。
    堆中申请内存的函数有一下几个:

    PVOID ExAllocatePool(
      _In_ POOL_TYPE PoolType,
      _In_ SIZE_T    NumberOfBytes
    );
    PVOID ExAllocatePoolWithTag(
      _In_ POOL_TYPE PoolType,
      _In_ SIZE_T    NumberOfBytes,
      _In_ ULONG     Tag
    );
    PVOID ExAllocatePoolWithQuota(
      _In_  POOL_TYPE PoolType,
      _In_  SIZE_T NumberOfBytes
    );
    PVOID ExAllocatePoolWithQuotaTag(
      _In_ POOL_TYPE PoolType,
      _In_ SIZE_T    NumberOfBytes,
      _In_ ULONG     Tag
    );
    • PoolType:PoolType是个枚举变量,如果此值为NonPagedPool,则分配非分页内存。如果此值为PagedPool,则分配内存为分页内存。
      • NonPagedPool:指定要求分配非分页内存。
      • PagedPool:指定要求分配分页内存。
      • NonPagedPoolMustSucceed:指定要求分配非分页内存,必须成功。
      • DontUseThisType:未指定。
      • NonPagedPoolCacheAligned:指定要求分配非分页内存,而且必须内存对齐。
      • PagedPoolCacheAligned:指定要求分配分页内存,而且必须内存对齐。
      • NonPagedPoolCacheAlignedMustS:指定要求分配非分页内存,而且必须内存对齐,且必须成功。
    • NumberOfBytes:分配内存的大小,最好是4的倍数。
    • Tag:系统额外分配4个字节的标签。
    • 返回值:返回分配的内存地址,一定是内核模式地址,如果返回0,则代表分配失败。
    VOID ExFreePool(
      _In_ PVOID P
    );
    VOID ExFreePoolWithTag(
      _In_ PVOID P,
      _In_ ULONG Tag
    );

    将分配的内存回收。
    - p:要释放的地址。
    - Tag:标签。

    2、在驱动中使用链表

    2.1 链表结构

    双向链表有两个指针,BLINK指向前一个元素,FLINK指向下一个元素。
    List Struct

    typedef struct _LIST_ENTRY {
        struct _LIST_ENTRY *Flink;
        struct _LIST_ENTRY *Blink;
    } LIST_ENTRY, *PLIST_ENTRY;

    2.2 链表初始化

    初始化链表头用InitializeListHead宏实现。
    检查链表是否为空使用IsListEmpty(&head);
    自定义链表

    typedef struct _MYDATASTRUCT{
        LIST_ENTRY ListEntry;
        //自定义的数据
        //......
    }

    2.3 插入链表

    (1)首部插入链表

    InsertHeadList(&head, &mydata->ListEntry);

    (2)尾部插入链表

    InsertTailList(&head, &mydata->ListEntry);

    2.4 从链接删除

    (1)首部删除链表

    PLIST_ENTRY pEntry = RemoveHeadList(&head);

    (2)尾部删除链表

    PLIST_ENTRY pEntry = RemoveTailList(&head);

    其中head是链表头,pEntry是从链表删除下的元素中的ListEntry。
    当LIST_ENTRY是自定义数据结构的第一个字段时,pEntry可以当做自定义数据的地址。

    3、Lookaside结构

    如果驱动程序频繁地从内存中申请回收固定大小的内存,可以使用Lookaside对象。可以将Lookaside对象想象成一个自动的内存分配容器。避免产生内存空洞。

    3.1 初始化:

    VOID ExInitializeNPagedLookasideList(
      _Out_    PNPAGED_LOOKASIDE_LIST Lookaside,
      _In_opt_ PALLOCATE_FUNCTION     Allocate,
      _In_opt_ PFREE_FUNCTION         Free,
      _In_     ULONG                  Flags,
      _In_     SIZE_T                 Size,
      _In_     ULONG                  Tag,
      _In_     USHORT                 Depth
    );
    VOID ExInitializePagedLookasideList(
      _Out_    PPAGED_LOOKASIDE_LIST Lookaside,
      _In_opt_ PALLOCATE_FUNCTION    Allocate,
      _In_opt_ PFREE_FUNCTION        Free,
      _In_     ULONG                 Flags,
      _In_     SIZE_T                Size,
      _In_     ULONG                 Tag,
      _In_     USHORT                Depth
    );

    3.2 初始完内存后,可以申请内存了:

    PVOID ExAllocateFromNPagedLookasideList(
      _Inout_ PNPAGED_LOOKASIDE_LIST Lookaside
    );
    PVOID ExAllocateFromPagedLookasideList(
      _Inout_ PPAGED_LOOKASIDE_LIST Lookaside
    );

    3.3 回收内存

    VOID ExFreeToNPagedLookasideList(
      _Inout_ PNPAGED_LOOKASIDE_LIST Lookaside,
      _In_    PVOID                  Entry
    );
    VOID ExFreeToPagedLookasideList(
      _Inout_  PPAGED_LOOKASIDE_LIST Lookaside,
      _In_     PVOID Entry
    );

    3.4 删除Lookaside对象

    VOID ExDeleteNPagedLookasideList(
      _Inout_ PNPAGED_LOOKASIDE_LIST Lookaside
    );
    VOID ExDeletePagedLookasideList(
      _Inout_  PPAGED_LOOKASIDE_LIST Lookaside
    );

    4、运行时函数

    由编译器提供,不同操作系统实现不同,但是接口一样,如malloc。

    4.1 内存间复制(非重叠)

    VOID RtlCopyMemory(
      _Out_       VOID UNALIGNED *Destination,
      _In_  const VOID UNALIGNED *Source,
      _In_        SIZE_T         Length
    );
    • Destination:要复制内存的目的地址。
    • Source:要复制内存的源地址。
    • Length:要复制内存的长度,单位是字节。

    4.2 可重叠复制

    VOID RtlMoveMemory(
      _Out_       VOID UNALIGNED *Destination,
      _In_  const VOID UNALIGNED *Source,
      _In_        SIZE_T         Length
    );
    • Destination:要复制内存的目的地址。
    • Source:要复制内存的源地址。
    • Length:要复制内存的长度,单位是字节。

    4.3 填充内存

    VOID RtlFillMemory(
      _Out_ VOID UNALIGNED *Destination,
      _In_  SIZE_T         Length,
      _In_  UCHAR          Fill
    );
    • Destination:目的地址。
    • Length:长度。
    • Fill:需要填充的字节。
    VOID RtlZeroMemory(
      _Out_ VOID UNALIGNED *Destination,
      _In_  SIZE_T         Length
    );
    • Destination:目的地址。
    • Length:长度。

    4.4 内存比较

    SIZE_T RtlCompareMemory(
      _In_ const VOID   *Source1,
      _In_ const VOID   *Source2,
      _In_       SIZE_T Length
    );
    • Source1:比较的第一个内存地址。
    • Source2:比较的第二个内存地址。
    • Length:比较的长度,单位为字节。
    • 返回值:相等的字节数。

    DDK提供的运行时函数都是RtlXX形式。

    5、使用C++特性分配内存

    驱动开发不能直接使用new和delete。因为MS编译器没有提供内核模式下的new操作符,我们可以对其进行重载来使用。
    重载有两种方法,一种是类中重载,一种全局重载。

    //全局new操作符
    void * __cdecl operator new(size_t size,POOL_TYPE PoolType=PagedPool)
    {
        KdPrint(("global operator new\n"));
        KdPrint(("Allocate size :%d\n",size));
        return ExAllocatePool(PagedPool,size);
    }
    //全局delete操作符
    void __cdecl operator delete(void* pointer)
    {
        KdPrint(("Global delete operator\n"));
        ExFreePool(pointer);
    }
    
    class TestClass
    {
    public:
        //构造函数
        TestClass()
        {
            KdPrint(("TestClass::TestClass()\n"));
        }
    
        //析构函数
        ~TestClass()
        {
            KdPrint(("TestClass::~TestClass()\n"));
        }
    
        //类中的new操作符
        void* operator new(size_t size,POOL_TYPE PoolType=PagedPool)
        {
            KdPrint(("TestClass::new\n"));
            KdPrint(("Allocate size :%d\n",size));
            return ExAllocatePool(PoolType,size);
        }
    
        //类中的delete操作符
        void operator delete(void* pointer)
        {
            KdPrint(("TestClass::delete\n"));
            ExFreePool(pointer);
        }
    private:
        char buffer[1024];
    };
    
    void TestNewOperator()
    {
        TestClass* pTestClass = new TestClass;
        delete pTestClass;
    
        pTestClass = new(NonPagedPool) TestClass;
        delete pTestClass;
    
        char *pBuffer = new(PagedPool) char[100];
        delete []pBuffer;
    
        pBuffer = new(NonPagedPool) char[100];
        delete []pBuffer;
    }

    6、其他

    6.1 NTSTATUS含义

    NTSTATUS含义
    图 NTSTATUS含义
    常用NTSTATUS状态返回值
    - STATUS_SUCCESS:函数执行成功
    - STATUS_UNSUCCESSFUL:函数执行不成功
    - STATUS_NOT_IMPLEMENTED:函数未被实现
    - STATUS_INVALID_INFO_CLASS:输入参数是无效的类别
    - STATUS_INFO_LENGTH_MISMATCH:输入参数长度不匹配
    - STATUS_ACCESS_VIOLATION:不允许访问
    - STATUS_IN_PAGE_ERROR:发生页故障
    - STATUS_INVALID_HANDLE:输入是无效的句柄
    - STATUS_INVALID_PARAMETER:输入是无效的参数
    - STATUS_NO_SUCH_DEVICE:指定设备不存在
    - STATUS_NO_SUCH_FILE:指定文件比存在
    - STATUS_INVALID_DEVICE_REQUEST:无效的设备请求
    - STATUS_END_OF_FILE:文件已到结尾
    - STATUS_INVALID_SYSTEM_SERVICE:无效的系统调用
    - STATUS_ACCESS_DENIED:访问被拒绝
    - STATUS_BUFFER_TOO_SMALL:是蠕动缓冲区过小
    - STATUS_OBJECT_TYPE_MISMATCH:是蠕动对象类型不匹配
    - STATUS_OBJECT_NAME_INVALID:输入的对象名无效
    - STATUS_OBJECT_NAME_NOT_FOUND:输入的对象没用找到
    - STATUS_PORT_DISCONNECTED:需要的端口没用被连接
    - STATUS_OBJECT_PATH_INVALID:输入的对象路径无效

    6.2 检查内存可用性

    VOID ProbeForRead(
      _In_ PVOID  Address,
      _In_ SIZE_T Length,
      _In_ ULONG  Alignment
    );
    • Address:需要被检查的内存地址
    • Length:需要被检查的内存长度,单位是字节
    • Alignment:描述该段内存是以多少字节对齐的
    VOID ProbeForWrite(
      _Inout_ PVOID  Address,
      _In_    SIZE_T Length,
      _In_    ULONG  Alignment
    );
    • Address:需要被检查的内存地址
    • Length:需要被检查的内存长度,单位是字节
    • Alignment:描述该段内存是以多少字节对齐的

    6.3 结构化异常处理

    异常概念类似于中断

    A)try-except块
    try-except-statement :

    __try
    {
        compound-statement
    }
    __except ( expression )
    {
        compound-statement
    }

    expression有三种情况:
    - EXCEPTION_CONTINUE_EXECUTION:数值为1,进入到__except进行错误处理,处理完后不再回到__try{}块中,转而继续执行。
    - EXCEPTION_CONTINUE_SEARCH:数值为0,不适用__except块中的异常处理,转而向上一层回卷。
    - EXCEPTION_EXECUTE_HANDLER:数值为-1,重复先前错误指令,很少用到。

    例子如下:

    #pragma INITCODE
    VOID ProbeTest()
    {
        PVOID badPointer = NULL;
        KdPrint(("Enter ProbeTest\n"));
        __try
        {
            KdPrint(("Enter __try block\n"));
            //判断空指针是否可读,显然会导致异常
            ProbeForWrite(badPointer,100,4);
            //由于在上面引发异常,所以以后语句不会被执行!
            KdPrint(("Leave __try block\n"));
        }   
        __except(EXCEPTION_EXECUTE_HANDLER)
        {
            KdPrint(("Catch the exception\n"));
            KdPrint(("The program will keep going\n"));
        }
        //该语句会被执行
        KdPrint(("Leave ProbeTest\n"));
    }

    其它引发异常函数:
    - ExRaiseAccessViolation:触发STATUS_ACCESS_VIOLATION异常
    - ExRaiseDatatypeMisalignment:触发STATUS_DATATYPE_MISALIGNMENT异常
    - ExRaiseStatus:用指定状态代码触发异常

    B)try-finally 块
    try-finally-statement :

    __try compound-statement
    __finally compound-statement

    强迫函数有退出前执行一段代码。常用来一些资源的回收工作。

    #pragma INITCODE
    NTSTATUS TryFinallyTest()
    {
        NTSTATUS status = STATUS_SUCCESS;
        __try
        {
            //做一些事情
            return STATUS_SUCCESS;
        }
        __finally
        {
            KdPrint(("Enter finallly block\n"));
            return status;
        }
    }

    6.4 防止“侧效”错误**

    也就是多行宏在if等语句中表达的意思出现了不同,在if,while,for等语句中,无论是否只有一句话,都不能省略{}。

    6.5 ASSERT断言

    NTSTATUS Foo(PCHAR* str)
    {
        ASSERT(srt!=NULL);
    }
    展开全文
  • windows内存管理和API函数

    千次阅读 2013-04-08 23:33:40
    windows内存管理知识: 1.分段或分页内存管理 2.物理地址和虚拟地址,虚拟地址空间. 3.虚拟内存布局,内存分工,堆,栈. 4.内存存取权限. 5.标准C内存管理函数与windows内存管理API的关系. 内存保护属性和存取权限 ...
  • 浅议windows内存管理

    千次阅读 2009-05-22 11:21:00
    windows内存管理的内部机制,将在以后加以介绍。首先,用户用到的内存都是虚拟内存,windows内存管理器负责将虚拟地址转译成物理内存。对于32位机器,虚拟地址空间就是4G大小,用4个byte就可以覆盖,因此,32位机...
  • Windows内存管理的几种方式和优缺点

    千次阅读 2016-07-27 11:34:03
    Windows内存管理方式主要分为:页式管理、段式管理和段页式管理。 页式管理的基本原理是将各进程的虚拟空间划分为若干个长度相等的页。把内存空间按页的大小划分为片或者页面,然后把页式虚拟地址与内存地址建立...
  • Linux和Windows内存管理的区别

    千次阅读 2019-06-06 14:37:06
    【1】Linux 系统: Linux 优先使用物理内存,当物理内存还有空闲时,linux是不会释放内存的, 即时占用内存的程序已经被关闭了(这部分内存就用来做缓存了)。...windows则总是给内存留下一定的空闲空...
  • WINDOWS核心编程——Windows内存管理

    千次阅读 2017-07-18 20:53:07
    想要了解Windows内存体系结构首先要对系统的内存的分段分页和进程隔离机制要有所了解。系统为了对进程进行隔离,使得每个进程只能访问自己申请的内存而不能访问其他进程的内存资源,对每个进程的内存使用线性地址...
  • Windows内存管理 - 内存映射文件

    千次阅读 2012-03-17 12:57:38
     Windows提供了3种进行内存管理的方法: • 虚拟内存,最适合用来管理大型对象或结构数组。 • 内存映射文件,最适合用来管理大型数据流(通常来自文件)以及在单个计算机上运行的多个进程之间共享数据。 • ...
  • 本文背景:在编程中,很多Windows或...本文目的:对Windows内存管理机制了解清楚,有效的利用C++内存函数管理和使用内存。本文内容:本文一共有六节,由于篇幅较多,故按节发表。其他章节请看本人博客的Windows内存管
  • 本文背景:在编程中,很多Windows...本文目的:对Windows内存管理机制了解清楚,有效的利用C++内存函数管理和使用内存。本文内容:本文一共有六节,由于篇幅较多,故按节发表。其他章节请看本人博客的Windows内存管理
  • 本文背景:在编程中,很多Windows或...本文目的:对Windows内存管理机制了解清楚,有效的利用C++内存函数管理和使用内存。本文内容:本文一共有六节,由于篇幅较多,故按节发表。其他章节请看本人博客的Windows内存管
  • 本文背景:在编程中,很多Windows...本文目的:对Windows内存管理机制了解清楚,有效的利用C++内存函数管理和使用内存。本文内容:本文一共有六节,由于篇幅较多,故按节发表。其他章节请看本人博客的Windows内存管理
  • 本文背景:在编程中,很多Windows或C++的内存...本文目的:对Windows内存管理机制了解清楚,有效的利用C++内存函数管理和使用内存。本文内容:本文一共有六节,由于篇幅较多,故按节发表。其他章节请看本人博客的Window
  • Windows内存管理的方法

    千次阅读 2012-09-22 23:53:54
    一、先说说内存的概念 1.物理内存:即插在主板上的内存条。他是固定的,内存条的容量多大,物理内存就有多大(集成显卡系统除外)。但是如果程序运行很多或者程序本身很大的话,就会导致大量的物理内存占用,甚至...
  • Windows内存管理的API函数

    千次阅读 2011-09-24 00:13:23
    数据所在的内存地址,内存最小存储单元是字节,在32位系统上使用32位数来表示内存地址. 一共可以表示2^32次 地址空间: 32位可以使用4GB内存,那么地址空间就是0x00000000~0xFFFFFFFF 物理内存: 硬件系统中真实存在...
  • 本文基本上是windows via c/c++上的内容,笔记做得不错。。 本文背景: ...根本的原因是,没有清楚的理解操作系统的内存管理机制,本文企图通过简单的总结描述,结合实例来阐明这个机制。 本文目
  • 描述Windows内存管理的方法

    千次阅读 2009-07-05 17:03:00
    (1)有三种方法:虚拟内存内存映射文件,内存堆栈。 虚拟内存是将页文件加载到内存,适用于比较大... 这些在windows核心编程中都描述得很详细的。 (2) 当进程要读自己的虚拟地址空间中的数据时 if(数据在物理内

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 389,281
精华内容 155,712
关键字:

windows内存管理