精华内容
下载资源
问答
  • 页目录,页表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内存注入

    千次阅读 2016-10-31 09:54:34
    针对Windows内存注入的方式大概也就那么几个,分别为使用函数QueueUserAPC DLL注入,调用SetWindowsHookEx注入,code cave注入和前不久的PowerLoaderEx内存注入。 利用AtomBombing技术后,它可以和其它的合法进程...

    针对Windows内存注入的方式大概也就那么几个,分别为使用函数QueueUserAPC DLL注入,调用SetWindowsHookEx注入,code cave注入和前不久的PowerLoaderEx内存注入。 利用AtomBombing技术后,它可以和其它的合法进程建立一个不会被查杀的通讯。比如我们可以让Chrome.exe和shellcode.exe进行通信,进程中只有chrome.exe,杀软就不会杀掉有证书的进程。

    展开全文
  • 另外通过前面的《Windows内存体系(2) – 页交换文件》文章,我们可以知道,这些API分配(调拨)的内存区域最初都是位于“页交换文件”上面,当程序对该区域的某些“页面”(对虚拟内存的管理以页面为单位进行的)...

    虚拟内存方面的API属于页面粒度API,通过这些API分配的内存的最小粒度是64KB。另外通过前面的《Windows内存体系(2) – 页交换文件》文章,我们可以知道,这些API分配(调拨)的内存区域最初都是位于“页交换文件”上面,当程序对该区域的某些“页面”(对虚拟内存的管理以页面为单位进行的)进行读写时,才会将这些页面交换到物理内存上面。

    《Windows内存体系(1) – 虚拟地址空间》中我们知道虚拟地址空间要经过预定调拨2个步骤之后才能使用,这2个步骤都可以通过VirtualAlloc函数实现:

    LPVOID VirtualAlloc(
      LPVOID lpAddress, 
      DWORD dwSize, 
      DWORD flAllocationType, 
      DWORD flProtect 
    ); 

    当预定或者调拨的空间我们不在需要时,我们需要调用VirtualFree来释放该地址空间:

    BOOL VirtualFree(
      LPVOID lpAddress, 
      DWORD dwSize, 
      DWORD dwFreeType 
    ); 

    下面是的示例演示了在预定、调拨、使用等操作前后,进程的各项内存的占用情况:

    #include <windows.h>
    
    int main()
    {
        SIZE_T size = 1 << 30; // 1GB
    
        // 预定1GB的空间
        char *pVirtualAddress = (char *)VirtualAlloc(NULL, size, MEM_RESERVE, PAGE_READWRITE);
        if (pVirtualAddress == NULL) {
            printf("Reserve 1GB failed.\n");
            return 1;
        }
    
        // 验证分配粒度是不是64KB
        int n = (long)pVirtualAddress % (64*1024);
        if (n == 0) {
            printf("分配粒度为64K\n");
        }
    
        printf("已经预定1GB\n");
        getchar(); // 暂停
    
        if (VirtualAlloc(pVirtualAddress, size, MEM_COMMIT, PAGE_READWRITE) == NULL) {
            printf("Commit 1GB failed.\n");
            return 1;
        }
    
        printf("已经调拨1GB\n");
        getchar(); // 暂停
    
        // 页面大小为4K,访问2560个页面,即2560*4K = 10MB
        // 
        for (int i = 0; i < 2560; i++) {
            char * p = pVirtualAddress + i * (4 * 1024);
            *p = 'A'; // 只访问每个页面的第一个字节
        }
    
        printf("已经使用前10MB\n");
        getchar(); // 暂停
    
        return 0;
    }

    在程序运行各个阶段进程的内存情况如下图:(“内存专用工作集”表示占用的物理内存的大小,“提交大小”表示调拨的页交换文件的大小)

    这里写图片描述

    展开全文
  • WINDOWS核心编程——Windows内存管理

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

    想要了解Windows内存体系结构首先要对系统的内存的分段分页和进程隔离机制要有所了解。系统为了对进程进行隔离,使得每个进程只能访问自己申请的内存而不能访问其他进程的内存资源,对每个进程的内存使用线性地址编制,在通过内存的分页机制在进程需要访问物理内存时通过进程的页表找到世界的物理内存的地址通过系统读写内存中的数据。在早期总线(20位寻址1M)大于寄存器(16位寻址64k)的情况下为了表示更多的物理内存地址采用了分段技术,现在已经不需要分段技术了(32位的内表示4GB,64位内表示16EB)采用平坦模型。

    32位的系统支持4GB的内存,线性地址的各个区间有不同的作用:


    1.空指针赋值分区:用来给空指针赋值的,这个分区不可操作,操作就报错。

    2.用户模式分区:用户代码在这里跑,堆栈都在这里,用户可以随便用,一般出错都在这里。

    3.64kb禁入分区:不知道干什么用的,估计就是为了区隔内核模式跟用户模式的。

    4.内核模式分区:系统运行的空间,所有进程共用的,用户模式的的代码不能访问这部分代码,若要访问续的通过系统提供的API进入到内核态。

    windows的内存体系结构基于虚拟的线性的地址和分页机制。对于线性地址的分配也是以页为单位进行的,物理地址的管理更是以页为单位。我们可以调用函数从地址空间中预定一块内存,在实际使用的时候再从物理内存中调拨,相当于C语言中的声明与定义,当不再需要内存的时候可以还给系统,先将一块内存标记为可用的(标记线性空间中的地址空闲可用),当积攒够了一定的空闲内存是在取消提交(把物理内存归还给操作系统)。对于物理内存而言,在暂时不用或者内存紧张的情况下可以被交换到磁盘上的页交换文件中,在需要的时候(CPU缺页中断)再从也交换文件中载入到内存中,这样就提高了内存的使用效率。页交换文件的使用当然需要一定的代价,频繁的在磁盘与内存将交换页会导致系统性能下降(硬盘颠簸),一般而言采用增加内存的办法比提升CPU对系统的性能改善更大。对于程序的数据可以采用交换页的技术来扩展内存以提高物理内存的使用效率,对于一些相对于数据的内容多变而且大小不可预计的内存使用方式而言交换页确实能提高效率,但是对于可以预知整块内存大小且需要连续的空间而言如文件镜像,固定大小的数据文件等使用内存映射文件是效率更高的方式。分页内存机制调配内存的过程可以粗略的描述如下:


    系统在对内存访问的安全性方面做的不只是按区段来控制内存的访问,也可以对每一个内存页指定保护属性:


    我们将整个4GB的线性地址空间称为虚拟内存(地址称为逻辑地址),我们所有的内存操作只在逻辑地址上完成,系统会帮我们处理物理地址映射,缺页等所有的情况。系统的内存的状态也主要是通过虚拟内存的状态来表现的,主要通过如下接口获得内存的状态:

    //获取系统信息 64位系统要通过GetNativeSystemInfo
    void WINAPI GetSystemInfo(
        LPSYSTEM_INFO lpSystemInfo  
    );
    typedef struct _SYSTEM_INFO {  
      union {  
        DWORD  dwOemId;  
        struct {  
          WORD wProcessorArchitecture;  //处理器体系结构  
          WORD wReserved;  //保留
        } ;  
      } ;  
      DWORD     dwPageSize;   //分页大小
      LPVOID    lpMinimumApplicationAddress;  //进程最小寻址空间
      LPVOID    lpMaximumApplicationAddress; //进程最大寻址空间  
      DWORD_PTR dwActiveProcessorMask;  //处理器掩码; 0..31 表示不同的处理器
      DWORD     dwNumberOfProcessors;  //CPU数量  
      DWORD     dwProcessorType;  //处理器类型
      DWORD     dwAllocationGranularity;  //虚拟内存空间的粒度
      WORD      wProcessorLevel;  //处理器等级
      WORD      wProcessorRevision;  //处理器版本
    } SYSTEM_INFO;  
    
    //获取当前系统中关系内存使用情况
    BOOL WINAPI GlobalMemoryStatusEx(
        LPMEMORYSTATUSEX lpBuffer  
    );
    typedef struct _MEMORYSTATUSEX {  
      DWORD     dwLength;  // sizeof (MEMORYSTATUSEX)
      DWORD     dwMemoryLoad; //已使用内存数量  
      DWORDLONG ullTotalPhys;  //系统物理内存总量  
      DWORDLONG ullAvailPhys;  //空闲的物理内存  
      DWORDLONG ullTotalPageFile;//页交换文件大小  
      DWORDLONG ullAvailPageFile;//空闲的页交换空间  
      DWORDLONG ullTotalVirtual;  //进程可使用虚拟机地址空间大小  
      DWORDLONG ullAvailVirtual;  //空闲的虚拟地址空间大小  
      DWORDLONG ullAvailExtendedVirtual;  //ullAvailExtendedVirtual保留字段
    } MEMORYSTATUSEX, *LPMEMORYSTATUSEX  
    
    //获取当前进程的内存使用情况
    BOOL WINAPI GetProcessMemoryInfo(
        HANDLE Process, //进程句柄
        PPROCESS_MEMORY_COUNTERS ppsmemCounters, //返回内存使用情况的结构
        DWORD cb  //结构的大小
    ); 
    typedef struct _PROCESS_MEMORY_COUNTERS_EX {  
      DWORD  cb;  //结构的大小
      DWORD  PageFaultCount; //发生的页面错误  
      SIZE_T PeakWorkingSetSize;  //使用过的最大工作集  
      SIZE_T WorkingSetSize;      //目前的工作集  
      SIZE_T QuotaPeakPagedPoolUsage;//使用过的最大分页池大小  
      SIZE_T QuotaPagedPoolUsage;  //分页池大小  
      SIZE_T QuotaPeakNonPagedPoolUsage;//非分页池使用过的  
      SIZE_T QuotaNonPagedPoolUsage;  //非分页池大小  
      SIZE_T PagefileUsage; //页交换文件使用大小  
      SIZE_T PeakPagefileUsage; //历史页交换文件使用  
      SIZE_T PrivateUsage;  //进程运行过程中申请的内存大小  
    } PROCESS_MEMORY_COUNTERS_EX, *PPROCESS_MEMORY_COUNTERS_EX  
    
    //查询当前进程虚拟地址空间的某个地址所属的块信息
    SIZE_T WINAPI VirtualQuery(
        LPCVOID                   lpAddress, //查询内存的地址
        PMEMORY_BASIC_INFORMATION lpBuffer, //接收内存信息
        SIZE_T                    dwLength //结构的大小
    );
    //查询进程虚拟地址空间的某个地址所属的块信息
    DWORD VirtualQueryEx(
        HANDLE hProcess, //进程句柄
        LPCVOID lpAddress, //查询内存的地址
        PMEMORY_BASIC_INFORMATION lpBuffer, //接收内存信息
        DWORD dwLength //结构的大小
    );
    typedef struct _MEMORY_BASIC_INFORMATION {  
      PVOID  BaseAddress;  //区域基地址  
      PVOID  AllocationBase;//使用VirtualAlloc分配的基地址  
      DWORD  AllocationProtect; //保护属性  
      SIZE_T RegionSize;    //区域大小  
      DWORD  State;     //页属性  
      DWORD  Protect;  //区域属性  
      DWORD  Type;  //区域类型  
    } MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;  
    程序不能直接操作物理内存的,所有的数据都需要保存在线性的虚拟内存(逻辑地址)中。使用虚拟内存主要使用函数VirtualAlloc来预定和提交内存,使用VirtualFree来归还或取消提交内存。

    虚拟内存的操作以页为粒度,适合用来管理大型对象数组或大型结构数组。对于存在页交换文件的内存页若我们能确定整页的内存数据不会改变,或者放弃在内存中的改变,下回直接从页交换文件中重新载入,则称该内存页为可重设的,不需要被交换到页文件中,直接覆盖其中的内容,在需要的时候重新从也文件中载入。预定提交重设用的同一个函数说明如下:

    //预定虚拟内存和调拨物理内存,失败返回NULL,成功返回lpAddress的取整的值
    LPVOID VirtualAlloc{
         LPVOID lpAddress, // 要分配的内存区域的地址,按分配粒度向上取整,为NULL则由系统决定
         DWORD dwSize, // 分配的大小,分配粒度的整数倍
         DWORD flAllocationType, // 分配的类型
         DWORD flProtect // 该内存的初始保护属性
    };
    对函数VirtualAlloc中的类型和保护属性说明如下:


    VirtualAlloc的逆向操作为VirtualFree用于释放和清理虚拟内存:

    BOOL WINAPI VirtualFree(
        LPVOID lpAddress, //释放(取消预定或提交)的页的首地址
        SIZE_T dwSize,  //大小
        DWORD dwFreeType  //MEM_DECOMMIT 取消VirtualAlloc提交的页, MEM_RELEASE 释放指定页
        //当释放整个区域时 dwFreeType 设置为MEM_RELEASE,lpAddress设置为区域的起始地址,dwSize设置为0,
    );
    对于VirtualAlloc时指定的保护方式可以通过函数VirtualProtect来更改:

    BOOL VirtualProtect(
        LPVOID lpAddress, // 目标地址起始位置
        DWORD dwSize, // 大小
        DWORD flNewProtect, // 请求的保护方式
        PDWORD lpflOldProtect // 保存老的保护方式
    );
    为了允许一个32位进程分配和访问更多的物理内存,突破这一受限地址空间所能表达的内存范围,Windows提供了一组函数,称为地址窗口扩展(AWE , Address  Windowing  Extensions)。用到的不多可以稍微了解下。

    而更常见的在有限的地址空间中处理大数据量(大到4GB的地址空间无法容纳所有数据)是,我们通常采用内存映射文件的办法一段段的处理数据。所谓映射就是把一段逻辑地址与文件的一段内容一一对应起来(同一段地址可以多次对应不同的文件内容)。映射原理如下(图片摘自网络如有版权问题请联系删除):

    正是由于内存映射文件的这几个特性所以特别合适用来处理下列事情:

    1:系统使用内存映射文件来将exe或是dll文件本身作为后备存储器,而非系统页交换文件,这大大节省了系统页交换空间,由于不需要将exe或是dll文件加载到页系统交换文件,也提高了启动速度。由于是映射到各自的逻辑地址的所以每个进程保存自己的副本,所有的变量之间也互不共享,但是可以通过DLL的数据段在使用同一DLL的不同进程间共享变量。
    2:使用内存映射文件来将磁盘上的文件映射到进程的空间区域,使得开发人员操作文件就像操作内存数据一样,将对文件的操作交由操作系统来管理,简化了开发人员的工作。这是最常用的方式,使用方式如下:

    1.创建或打开一个文件内核对象
    HANDLE WINAPI CreateFile(
        LPCTSTR lpFileName,
        DWORD dwDesiredAccess,
        DWORD dwShareMode,
        LPSECURITY_ATTRIBUTES lpSecurityAttributes,
        DWORD dwCreationDisposition,
        DWORD dwFlagsAndAttributes,
        HANDLE hTemplateFile
    );
    2.创建一个文件映射内核对象
    HANDLE WINAPI CreateFileMapping(
        HANDLE hFile,  //文件句柄
        LPSECURITY_ATTRIBUTES lpAttributes, //安全属性
        DWORD flProtect, //保护属性
        DWORD dwMaximumSizeHigh, //文件映射的最大长度的高32位
        DWORD dwMaximumSizeLow, //文件映射的最大长度的低32位
        LPCTSTR lpName //内核文件命名
    );
    5.关闭文件对象
    CloseHandle(hFile);
    
    3.将文件映射对象映射到进程地址空间
    LPVOID WINAPI MapViewOfFile(
        HANDLE hFileMappingObject, //文件句柄
        DWORD dwDesiredAccess, //文件数据的访问方式要与CreateFileMapping()的保护属性相匹配
        DWORD dwFileOffsetHigh, //表示文件映射起始偏移的高32位
        DWORD dwFileOffsetLow, //表示文件映射起始偏移的低32位
        SIZE_T dwNumberOfBytesToMap //指定映射文件的字节数
    );
    
    6.关闭文件映射对象
    CloseHandle(hFileMapping);
    
    4.从进程的地址空间中撤消文件数据的映像
    BOOL UnmapViewOfFile(
        PVOID pvBaseAddress //pvBaseAddress由MapViewOfFile函数返回
    );
    
    //可以按以上顺序执行或者看情况执行4,5,6
    //对于修改过的数据的一部分或全部强制重新写入磁盘映像中
    BOOL FlushViewOfFile(
       PVOID pvAddress, //内存映射文件中的视图的一个字节的地址
       SIZE_T dwNumberOfBytesToFlush //想要刷新的字节数
    );
    对于一些参数的说明如下:

    使用fdwProtect 参数设定的部分保护属性

    dwDesiredAccess用于标识如何访问该数据



    3:windows提供了多种进程间通信的方法,但他们都是基于内存映射文件来实现的。

    对于进程间通信只要在不同进程中映射了同一个文件内容,当其中一个映射被改变时(就算还没有保存到磁盘上)其他进程自动会获取到改变。

    windows的进程除了直接向系统申请内存之外还可以使用运行时库提供的内存堆和栈,简单的有如下说明:

    http://blog.csdn.net/pokeyode/article/details/53303029

    http://blog.csdn.net/pokeyode/article/details/53336826
    虽然运行时库提供的堆足以满足我们的需要,但我们还是会基于一下原因来创建自己的堆(引用自):

    一:对数据保护。创建两个或多个独立的堆,每个堆保存不同的结构,对两个堆分别操作,可以使问题局部化。
    二:更有效的内存管理。创建额外的堆,管理同样大小的对象。这样在释放一个空间后可以刚好容纳另一个对象。
    三:内存访问局部化。将需要同时访问的数据放在相邻的区域,可以减少缺页中断的次数。
    四:避免线程同步开销。默认堆的访问是依次进行的。堆函数必须执行额外的代码来保证线程安全性。通过创建额外的堆可以避免同步开销。
    五:快速释放。我们可以直接释放整个堆而不需要手动的释放每个内存块。这不但极其方便,而且还可以更快的运行。
    要创建并管理自己的堆,需要使用以下接口,首先要创建堆:

    HANDLE HeapCreate(
        DWORD fdwOptions, //如何操作堆
        SIZE_T dwInitilialize, //一开始要调拨给堆的字节数向上取整到CPU页面大小的整数倍
        SIZE_T dwMaximumSize //堆所能增长到的最大大小,即预定的地址空间的最大大小。若为0,那么堆可增长到用尽所有的物理存储器为止。
    ); 

    fdwOptions表示对堆的操作该如何进行
    HEAP_NO_SERIALIZE标志使得多个线程可以同时访问一个堆,这使得堆中的数据可能会遭到破坏,因此应该避免使用。
    HEAP_GENERATE_EXCEPTIONS标志告诉系统,每当在堆中分配或者重新分配内存块失败的时候,抛出一个异常。
    HEAP_CREATE_ENABLE_EXECUTE标志告诉系统,我们想在堆中存放可执行代码。如果不设置这个标志,那么当我们试图在来自堆的内存块中执行代码时,系统会抛出EXCEPTION_ACCESS_VIOLATION异常。

    有了堆之后从堆中分配内存时要:

    1.遍历已分配的内存的链表和闲置内存的链表。
    2.找到一块足够大的闲置内存块。
    3.分配一块新的内存,将2找到的内存块标记为已分配。
    4.将新分配的内存块添加到已分配的链表中。

    调用函数来从堆中分配并在需要时调整内存大小:

    PVOID HeapAlloc(
        HANDLE hHeap, //堆句柄,表示要从哪个堆分配内存
        DWORD fdwFlags, //堆分配时的可选参数
        SIZE_T dwBytes //要分配堆的字节数
    );  
    PVOID HeapReAlloc(
        HANDLE hHeap, //堆句柄
        DWORD fdwFlags, //HeapAlloc的fdwFlags一样
        PVOID pvMem, //指定要调整大小的内存块
        SIZE_T dwBytes //指定内存块的新大小
    );  
    fdwFlags说明如下:

    HeapReAlloc的fdwFlags特别的有HEAP_REALLOC_IN_PLACE_ONLY 如果HeapReAlloc函数能在不移动内存块的前提下就能让它增大,那么函数会返回原来的内存块地址。另一方面,如果HeapReAlloc必须移动内存块的地址,那么函数将返回一个指向一块更大内存块的新地址。如果一个内存块是链表或者树的一部分,那么需要指定这个标志。因为链表或者树的其他节点可能包含指向当前节点的指针,把节点移动到堆中其他的地方会破坏链表或树的完整性。

    若成功则返回内存地址若失败则返回NULL,若指定了HEAP_GENERATE_EXCEPTIONS报异常:


    在不需要内存是把内存归还给堆:

    BOOL HeapFree( 
        HANDLE hHeap, //堆句柄
        DWORD fdwFlags,
        PVOID pvMem //指定要调整大小的内存块
    ); 
    
    在不需要堆时销毁堆

    BOOL HeapDestroy(HANDLE hHeap);  

    展开全文
  • Windows内存管理

    千次阅读 2014-04-05 23:46:50
    Windows内存管理 在驱动程序编写中,分配和管理内存不能使用熟知的Win32API函数,取而代之的是DDK提供的高效的内核函数。程序员必须小心地使用这些内存相关的内核函数。因为在内核模式下,操作系统不会检查内存使用...
  • 前面的《Windows内存体系(3) – 内存映射文件》文章,对内存映射文件技术的原理进行了介绍,本篇文章着重介绍该技术的使用场景。 一、内存映射文件技术介绍 常用的有Win32 API的CreateFile()、WriteFile()、...
  • 从上图可以看到,虚拟内存机制(Virtual Memory)是windows内存体系的基础,无论你是使用堆,还是使用内存映射文件,它们的底层都是基于虚拟内存来实现的。 从上往下,每一层的API在内部会依次调用下一层的API。下...
  • Windows内存泄漏检测工具总结

    千次阅读 2018-03-26 00:02:24
    Windows内存泄漏检测工具总结 前言 Win32内存管理结构 Windows内存泄漏检测工具简单原理 Windows内存泄漏检测工具总结 前言 做C\C++开发的时候,经常会有程序内存(资源)泄漏的Bug。对于这类Bug,除了提高...
  • 另外建议先阅读《Windows内存体系》系列文章的前几篇文章,对“虚拟内存”,“堆”,“内存API体系结构”有所了解。 《Windows核心编程 第五版》第19章 DLL基础(511页)中给出了一个建议:“当一个MT版本的模块...
  • Qt 获取Windows内存信息

    千次阅读 2016-06-02 11:04:46
    通过调用Windows API来获取Windows 内存信息,并显示在界面上,通过与Windows 内存管理器对比可以看到结果是正确的。 实现的小工具的界面效果与内存管理器对比如下: 实现的完整代码: #pragma execution_...
  • windows 内存原理与管理

    千次阅读 2017-02-14 17:01:50
    本文基本上是windows via c/c++上的内容,笔记做得不错。。 本文背景: 在编程中,很多Windows或C++的内存函数不知道有什么区别,更别...对Windows内存管理机制了解清楚,有效的利用C++内存函数管理和使用内存。
  • 一、内存为什么要对齐 虽然所有的变量都是保存在特定地址的内存中,但最好还是按照内存对齐的要求来存储。这主要出于两个方面的原因考虑: 平台原因: 不是所有的硬件平台(特别是嵌入式系统中使用的低端处理器)...
  • Windows内存计数器

    千次阅读 2012-01-12 23:17:57
    Windows内存中有三个数据很重要,Pages/sec, Available Bytes和Committed Bytes.  在测试中内存的缺少会造成频繁的页错误,导致系统不停在磁盘和内存中交换数据,有可能表现出IO瓶颈的现象,实则内存不足。在页...
  • Windows驱动开发(2) - Windows内存管理

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

    千次阅读 2019-01-20 17:08:42
    监控Java应用程序Windows内存使用情况
  • Windows 内存的分配

    千次阅读 2010-12-13 10:54:00
     Windows 内存的分配 收藏 Windows 使用一种 分页请求虚拟内存系统,现在我们就来分析一下这种系统。 虚拟地址空间 虚拟内存的概念在上个世纪五十年代就提出了,当时是作为解决不能一次装入实际内存...
  • 详解Windows内存分页机制

    千次阅读 2015-03-14 13:53:07
    标 题: 【原创】详解Windows内存分页机制 作 者: 莫灰灰 时 间: 2011-06-11,19:18:49 链 接: http://bbs.pediy.com/showthread.php?t=135274 昨天新买了两本书, 看到了内存分页部分, 特此记录下,
  • Windows内存体系结构      理解Windows内存体系结构是每一个励志成为优秀的Windows程序员所必须的。   进程虚拟地址空间    每个进程都有自己的虚拟地址空间。对于32位操作系统来说,它的地址空间是4GB...
  • 数字取证技术 :Windows内存信息提取

    千次阅读 2017-04-05 17:44:19
    数字取证技术 :Windows内存信息提取。后面会花一部分时间,写一些数字取证相关的文章。攻击技术贴多如牛毛,眼下不管是网安,还是安全厂商, 欠缺的是对取证技术的研究。 大致想了一下,主要会从以下几个方面逐一...
  • windows内存管理和API函数

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

    千次阅读 2016-09-06 10:43:54
    在进行Windows的学习过程中,经常看到不同的内存分配方式,例如VirtualAlloc, HeapAlloc, malloc和new。它们之间存在一些差异。   (1) VirtualAlloc PVOID VirtualAlloc(PVOID pvAddress, SIZE_T dwSize,...
  • Windows内存清理----其实是没必要的

    万次阅读 2018-09-26 22:54:30
    Windows内存清理----其实是没必要的 目前,有很多清理内存的工具,如Wise Memory Optimizer、 MemoryZipperPlus、SweepRAM等,360安全卫士、腾讯电脑管家、鲁大师等等系统工具也带有清理内存的功能。这些工具主要...
  • Windows内存清理工具实现——从现象到本质

    万次阅读 多人点赞 2016-10-16 15:58:30
    Windows内存清理工具实现——从现象到本质
  • 浅议windows内存管理

    千次阅读 2009-05-22 11:21:00
    这里仅是对windows内存的简单介绍,适合编写windows应用程序的人阅读,主要参考《windows核心编程》及《深入解析windows操作系统》第四版。对windows内存管理的内部机制,将在以后加以介绍。首先,用户用到的内存都...
  • Windows内存分配方式 Win32的堆分配函数 每一个进程都可以使用堆分配函数创建一个私有的堆──调用进程地址空间的一个或者多个页面。DLL创建的私有堆必定在调用DLL的进程的地址空间内,只能被调用进程访问。 ...
  • 前面的《Windows内存体系(9) – 跨MT模块的内存相互释放的问题》文章解释了跨MT模块分配的内存相互释放为什么会崩溃的问题。 一、问题描述 这篇文章主要介绍我们在实际开发中经常遇到的一个问题:针对MT模块...
  • windows 内存管理方式及其优缺点

    千次阅读 2016-03-17 19:37:57
    windows 内存管理方式主要分为:页式管理,段式管理,段页式管理。页式管理基本原理是将各进程的虚拟空间划分为若干个长度相等的页;页式管理把内存空间按照页的大小划分成片或者页面,然后把页式虚拟地址与内存地址...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 46,103
精华内容 18,441
关键字:

windows内存