內存分配粒度 分配粒度和內存頁面大小(x86處理器平臺的分配粒度是64K,內存頁是4K,所以section都是0x1000對齊,硬盤扇區大小是512字節,所以PE文件默認文件對齊是0x200)

分配粒度和內存頁面大小(x86處理器平臺的分配粒度是64K,內存頁是4K,所以section都是0x1000對齊,硬盤扇區大小是512字節,所以PE文件默認文件對齊是0x200)

 

分配粒度和內存頁面大小

x86處理器平臺的分配粒度是64K,32位CPU的內存頁面大小是4K,64位是8K,保留內存地址空間總是要和分配粒度對齊。一個分配粒度裏包含16個內存頁面。

這是個概念,具體不用自己操心,比如用VirtualAllocEx等函數,給lpAddress參數NULL系統就會自動找一個地方分配你要的內存空間。如果需要自己管理這個就累了......

一個分配粒度是64K,這就是爲什麼Null指針區域和64K進入區域都是 64K的原因,剛好就是一個分配粒度。
一個內存頁是4K,這就是爲什麼PE文件中的section都是0x1000對齊.
硬盤扇區大小是512字節,這就是爲什麼PE文件默認文件對齊是0x200.

這些數字絕對不是心血來潮設定出來的,而是綜合了硬件結構和操作系統架構設定的。

爲什麼內存空間分配總是以64K爲邊界? 

有時候你可能會有這樣的疑惑:爲什麼頁面大小是4K,而VirtualAlloc函數分配的內存是以64K(而不是4K)爲邊界呢?

事情還得從Alpha AXP處理器說起

在Alpha AXP處理器上,沒有一條指令對應”加載一個32位的證書”這樣的操作,取而代之地,實際上是加載兩個16位的整數然後將它們合併成32位的。

所以,如果內存分配粒度小於64K,則一個需要重新定位的DLL將會需要對每一次重新分配的地址做出兩次調整:一次是高16位地址,另一次是低16位地址。如果這改變了兩半地址之間的進位或借位,情況就會變得更糟。(例如,將4K的數據從0x1234F000移到0x12350000,這將迫使地址的低16位和高15位都發生改變。即使移動的地址量遠小於64K,但由於存在進位,這項操作仍然對地址的高16位部分產生影響)

除了這個,還有更糟的

Alpha AXP處理器實際上會合並兩個16位的帶符號整數到一個32位的整數。例如,爲了加載0x1234ABCD,你需要首先使用LDAH指令加載0x1234到目標寄存器的高16位,然後再使用LDA指令加上帶符號整數-0x5433(因爲,0x5433 = 0x10000 – 0xABCD),才能得到預期的結果0x1234ABCD。

因此,如果重定位導致地址在64K塊的”下半部分”和”上半部分”之間移動,則必須進行其他修正,以確保正確調整了地址上半部分的算法。由於編譯器喜歡對指令進行重新排序,因此該LDAH指令可能距離很遠,因此下半部分的重定位記錄將不得不採用某種方式來找到匹配的上半部分。

而且,編譯器很聰明,如果需要爲同一64K區域中的兩個變量計算地址,則它們之間共享LDAH指令。如果可以通過不是64K的倍數的值進行重定位,則編譯器將不再能夠執行此優化,因爲在重定位之後,這兩個變量不再屬於同一64K內存塊中。

強制以64K粒度分配內存可解決所有這些問題。

如果你仔細觀察的話,你會發現這也解釋了爲什麼2GB邊界附近有64K的”無人區”。考慮一下計算值0x7FFFABCD的方法:由於低16位在64K範圍內,該值需要通過減法而不是加法來計算。一種比較想當然的解決方案:

上面的做法行不通

Alpha AXP是64位處理器,0x8000不適合放入到16位帶符號整數中,因此必須使用-0x8000(負數)。所以,實際發生的是:

你需要添加第三條指令來清除高32位。一種巧妙的技巧是,將零加起來,然後告訴處理器將結果視爲32位整數並將其符號擴展爲64位。

如果允許訪問2GB邊界的64K範圍內的地址,則每一次內存地址計算都必須插入上面所提到的第三條ADDL指令,以防萬一該地址被重新分配到2GB邊界附近的“危險區域”。

要訪問地址空間中的最後一個64K區域,會付出一筆非常高的代價:對於所有地址計算,其性能都會受到50%的影響,以避免在實踐中永遠不會發生這種情況。因此,將這片區域設置爲永久禁用,是一種更爲明智的選擇。


windows虛擬內存管理

發佈於 2019-02-25 17:24:06
1.5K0
舉報

內存管理是操作系統非常重要的部分,處理器每一次的升級都會給內存管理方式帶來巨大的變化,向早期的8086cpu的分段式管理,到後來的80x86 系列的32位cpu推出的保護模式和段頁式管理。在應用程序中我們無時不刻不在和內存打交道,我們總在不經意間的進行堆內存和棧內存的分配釋放,所以內存是我們進行程序設計必不可少的部分。

CPU的內存管理方式

段寄存器怎麼消失了?

在學習8086彙編語言時經常與寄存器打交道,其中8086CPU採用的內存管理方式爲分段管理的方式,尋址時採用:短地址 * 16 + 偏移地址的方式,其中有幾大段寄存器比如:CS、DS、SS、ES等等,每個段的偏移地址最大爲64K,這樣總共能尋址到2M的內存。但是到32位CPU之後偏移地址變成了32位這樣每個段就可以有4GB的內存空間,這個空間已經足夠大了,這個時候在編寫相應的彙編程序時我們發現沒有段寄存器的身影了,是不是在32位中已經沒有段寄存器了呢,答案是否定了,32位CPU中不僅有段寄存器而且它們的作用比以前更大了。 在32位CPU中段寄存器不再作爲段首地址,而是作爲段選擇子,CPU爲了管理內存,將某些連續的地址內存作爲一頁,利用一個數據結構來說明這頁的屬性,比如是否可讀寫,大小,起始地址等等,這個數據結構叫做段描述符,而多個段描述符則組成了一個段描述符表,而段寄存器如今是用來找到對應的段描述符的,叫做段選擇子。段寄存器仍然是16位其中高13位表示段描述符表的索引,第二位是區分LDT(局部描述符表)和GDT(全局描述符表),全局描述符表是系統級的而LDT是每個進程所獨有的,如果第二位表示的是LDT,那麼首先要從GDT中查詢到LDT所在位置,然後才根據索引找到對應的內存地址,所以現在尋址採用的是通過段選擇子查表的方式得到一個32位的內存地址。由於這些表都是由系統維護,並且不允許用戶訪問及修改所以在普通應用程序中沒有必要也不能使用段寄存器。通過上面的說明,我們可以推導出來32位機器最多可以支持2^(13 + 1 + 32) = 64T內存。

段頁式管理

通過查表方式得到的32位內存地址是否就是真實的物理內存的地址呢,這個也是不一定的,這個還要看系統是否開啓了段頁式管理。如果沒有則這個就是真實的物理地址,如果開啓了段頁式管理那麼這個只是一個線性地址,還需要通過頁表來尋址到真實的物理內存。 32位CPU專門新贈了一個CR3寄存器用來完成分頁式管理,通過CR3寄存器可以尋址到頁目錄表,然後再將32位線性地址的高10位作爲頁目錄表的索引,通過這個索引可以找到相應的頁表,再將中間10爲作爲頁表的索引,通過這個索引可以尋址到對應物理內存的起始地址,最後通過這個其實地址和最後低12位的偏移地址找到對應真實內存。下面是這個過程的一個示例圖:

分頁式管理示意圖
分頁式管理示意圖

爲什麼要使用分頁式管理,直接讓那個32位線性地址對應真實的內存不可以嗎。當然可以,但是分頁式管理也有它自身的優點: 1. 可以實現頁面的保護:系統通過設置相關屬性信息來指定特權級別和其他狀態 2. 可以實現物理內存的共享:從上面的圖中可以看出,不同的線性地址是可以映射到相同的物理內存上的,只需要更改頁表中對應的物理地址就可以實現不同的線性地址對應相同的物理內存實現內存共享。 3. 可以方便的實現虛擬內存的支持:在系統中有一個pagefile.sys的交互頁面文件,這個是系統用來進行內存頁面與磁盤進行交互,以應對內存不夠的情況。系統爲每個內存頁維護了一個值,這個值表示該頁面多久未被訪問,當頁面被訪問這個值被清零,否則每過一段時間會累加一次。當這個值到達某個閾值時,系統將頁面中的內容放入磁盤中,將這塊內存空餘出來以便保存其他數據,同時將之前的線性地址做一個標記,表名這個線性地址沒有對應到具體的內存中,當程序需要再次訪問這個線性地址所對應的內存時系統會再次將磁盤中的數據寫入到內存中。雖說這樣做相當於擴大了物理內存,但是磁盤相對於內存來說是一個慢速設備,在內存和磁盤間進行數據交換總是會耗費大量的時間,這樣會拖慢程序運行,而採用SSD硬盤會顯著提高系統運行效率,就在於SSD提高了與內存進行數據交換的效率。如果想顯著提高效率,最好的辦法是加內存畢竟在內存和硬盤間倒換數據是要話費時間的。

保護模式

在以前的16位CPU中採用的多是實模式,程序中使用的地址都是真實的物理地址,這樣如果內存分配不合理,會造成一個程序將另外一個程序所在的內存覆蓋這樣對另外一個程序將造成嚴重影響,但是在32位保護模式下,不再會產生這種問題,保護模式將每個進程的地址空間隔離開來,還記得上面的LDT嗎,在不同的程序中即使採用的是相同的地址,也會被LDT映射到不同的線性地址上。 保護模式主要體現在這樣幾個方面: 1.同一進程中,使用4個不同訪問級別的內存段,對每個頁面的訪問屬性做了相應的規定,防止錯誤訪問的情況,同時爲提供了4中不同代碼特權,0特權的代碼可以訪問任意級別的內存,1特權能任意訪問1…3級內存,但不能訪問0級內存,依次類推。通常這些特權級別叫做ring0-ring3。 2. 對於不同的進程,將他們所用到的內存等資源隔離開來,一個進程的執行不會影響到另一個進程。

windows系統的內存管理

windows內存管理器

我們將系統中實際映射到具體的實際內存上的頁面稱爲工作集。當進程想訪問多餘實際物理內存的內存時,系統會啓用虛擬內存管理機制(工作集管理),將那些長時間未訪問的物理頁面複製到硬盤緩衝文件上,並釋放這些物理頁面,映射到虛擬空間的其它頁面上;系統的內存管理器主要由下面的幾個部分組成: 1. 工作集管理器(優先級16):這個主要負責記錄每個頁面的年齡,也就有多久未被訪問,當頁面被訪問這個年齡被清零,否則每過一段時間就進行累加1的操作。 2. 進程/棧交換器(優先級23):主要用於在進行進程或者線程切換時保存寄存器中的相關數據用以保存相關環境。 3. 已修改頁面寫出器(優先級17):當內存映射的內容發生改變時將這個改變及時的寫入到硬盤中,防止由於程序意外終止而造成數據丟失 4. 映射頁面寫出器(優先級17):當頁面的年齡達到一定的閾值時,將頁面內容寫入到硬盤中 5. 解引用段線程(優先級18):釋放以寫入到硬盤中的空閒頁面 6. 零頁面線程(優先級0):將空閒頁面清零,以便程序下次使用,這個線程保證了新提交的頁面都是乾淨的零頁面

進程虛擬地址空間的佈局

windows爲每個進程提供了平坦的4GB的線性地址空間,這個地址空間被分爲用戶分區和內核分區,他們各佔2GB大小,其中內核分區在高地址位,用戶分區在低地址位,下面是內存分佈的一個表格:

分區

地址範圍

NULL指針區

0x00000000-0x0000FFFF

用戶分區

0x00010000-0x7FFEFFFF

64K禁入區

0x7FFF0000-0x7FFFFFFF

內核分區

0x80000000-0xFFFFFFFF

從上面的圖中可以看出,系統的內核分區是2GB而用戶可用的分區並沒有2GB,在用戶分區的頭64K和尾部的64K不允許用戶使用。 另外我們可以壓縮內核分區的大小,以便使用戶分區佔更多的內存,這就是/3GB方式,下面是這種方式的具體內存分佈:

分區

地址範圍

NULL指針區

0x00000000-0x0000FFFF

用戶分區

0x00010000-0xBFFEFFFF

64K禁入區

0xBFFF0000-0xBFFFFFFF

內核分區

0xC0000000-0xFFFFFFFF

windows虛擬內存管理函數

VirtualAlloc

VirtualAlloc函數主要用於提交或者保留一段虛擬地址空間,通過該函數提交的頁面是經過0頁面線程清理的乾淨的頁面。

LPVOID VirtualAlloc(
  LPVOID lpAddress, //虛擬內存的地址
  DWORD dwSize, //虛擬內存大小
  DWORD flAllocationType,//要對這塊的虛擬內存做何種操作 
  DWORD flProtect //虛擬內存的保護屬性
); 

我們可以指定第一個參數來告知系統,我們希望操作哪塊內存,如果這個地址對應的內存已經被保留了那麼將向下偏移至64K的整數倍,如果這塊內存已經被提交,那麼地址將向下偏移至4K的整數倍,也就是說保留頁面的最小粒度是64K,而提交的最小粒度是一頁4K。 第三個參數是指定分配的類型,主要有以下幾個值

含義

MEM_COMMIT

提交,也就是說將虛擬地址映射到對應的真實物理內存中,這樣這塊內存就可以正常使用

MEM_RESERVE

保留,告知系統以這個地址開始到後面的dwSize大小的連續的虛擬內存程序要使用,進程其他分配內存的操作不得使用這段內存。

MEM_TOP_DOWN

從高端地址保留空間(默認是從低端向高端搜索)

MEM_LARGE_PAGES

開啓大頁面的支持,默認一個頁面是4K而大頁面是2M(這個視具體系統而定)

MEM_WRITE_WATCH

開啓頁面寫入監視,利用GetWriteWatch可以得到寫入頁面的統計情況,利用ResetWriteWatch可以重置起始計數

MEM_PHYSICAL

用於開啓PAE

第四個參數主要是頁面的保護屬性,參數可取值如下:

含義

PAGE_READONLY

只讀

PAGE_READWRITE

可讀寫

PAGE_EXECUTE

可執行

PAGE_EXECUTE_READ

可讀可執行

PAGE_EXECUTE_READWRITE

可讀可寫可執行

PAGE_NOACCESS

不可訪問

PAGE_GUARD

將該頁設置爲保護頁,如果試圖對該頁面進行讀寫操作,會產生一個STATUS_GUARD_PAGE 異常

下面是該函數使用的幾個例子: 1. 頁面的提交/保留與釋放

//保留並提交
    LPVOID pMem = VirtualAlloc(NULL, 4 * 4096, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
    srand((unsigned int)time(NULL));

    float* pfMem = (float*)pMem;
    for (int i = 0; i < 4 * 4096 / sizeof(float); i++)
    {
        pfMem[i] = rand();
    }

    //釋放
    VirtualFree(pMem, 4 * 4096, MEM_RELEASE);

    //先保留再提交
    LPBYTE pByte = (LPBYTE)VirtualAlloc(NULL, 1024 * 1024, MEM_RESERVE, PAGE_READWRITE);
    VirtualAlloc(pByte + 4 * 4096, 4096, MEM_COMMIT, PAGE_READWRITE);
    pfMem = (float*)(pByte + 4 * 4096);
    for (int i = 0; i < 4096/sizeof(float); i++)
    {
        pfMem[i] = rand();
    }

    //釋放
    VirtualFree(pByte + 4 * 4096, 4096, MEM_DECOMMIT);
    VirtualFree(pByte, 1024 * 1024, MEM_RELEASE);
  1. 大頁面支持
//獲得大頁面的尺寸
DWORD dwLargePageSize = GetLargePageMinimum();
LPVOID pBuffer = VirtualAlloc(NULL, 64 * dwLargePageSize, MEM_RESERVE, PAGE_READWRITE);
//提交大頁面
VirtualAlloc(pBuffer, 4 * dwLargePageSize, MEM_COMMIT | MEM_LARGE_PAGES, PAGE_READWRITE);
VirtualFree(pBuffer, 4 * dwLargePageSize, MEM_DECOMMIT);
VirtualFree(pBuffer, 64 * dwLargePageSize, MEM_RELEASE);

VirtualProtect

VirtualProtect用來設置頁面的保護屬性,函數原型如下:

BOOL VirtualProtect( 
  LPVOID lpAddress, //虛擬內存地址
  DWORD dwSize, //大小
  DWORD flNewProtect, //保護屬性
  PDWORD lpflOldProtect //返回原來的保護屬性
); 

這個保護屬性與之前介紹的VirtualAlloc中的保護屬性相同,另外需要注意的一點是一般返回原來的屬性的話,這個指針可以爲NULL,但是這個函數不同,如果第四個參數爲NULL,那麼函數調用將會失敗

LPVOID pBuffer = VirtualAlloc(NULL, 4 * 4096, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
float *pfArray = (float*)pBuffer;
for (int i = 0; i < 4 * 4096 / sizeof(float); i++)
{
    pfArray[i] = 1.0f * rand();
}

//將頁面改爲只讀屬性
DWORD dwOldProtect = 0;
VirtualProtect(pBuffer, 4 * 4096, PAGE_READONLY, &dwOldProtect);
//寫入數據將發生異常
pfArray[9] = 0.1f;
VirtualFree(pBuffer, 4 * 4096, MEM_RELEASE);

VirtualQuery

這個函數用來查詢某段虛擬內存的屬性信息,這個函數原型如下:

DWORD VirtualQuery(
  LPCVOID lpAddress,//地址 
  PMEMORY_BASIC_INFORMATION lpBuffer, //用於接收返回信息的指針
  DWORD dwLength //緩衝區大小,上述結構的大小
); 

結構MEMORY_BASIC_INFORMATION的定義如下:

typedef struct _MEMORY_BASIC_INFORMATION {
    PVOID BaseAddress; //該頁面的起始地址
    PVOID AllocationBase;//分配給該頁面的首地址
    DWORD AllocationProtect;//頁面的保護屬性
    DWORD RegionSize; //頁面大小
    DWORD State;//頁面狀態
    DWORD Protect;//頁面的保護類型
    DWORD Type;//頁面類型
} MEMORY_BASIC_INFORMATION; 
typedef MEMORY_BASIC_INFORMATION *PMEMORY_BASIC_INFORMATION; 

AllocationProtect與Protect所能取的值與之前的保護屬性的值相同。 State的取值如下: MEM_FREE:空閒 MEM_RESERVE:保留 MEM_COMMIT:已提交 Type的取值如下: MEM_IMAGE:映射類型,一般是映射到地址控件的可執行模塊如DLL,EXE等 MEM_MAPPED:文件映射類型 MEM_PRIVATE:私有類型,這個頁面的數據爲本進程私有數據,不能與其他進程共享 下面是這個的使用例子:

#include<windows.h>
#include <stdio.h>
#include <tchar.h>
#include <atlstr.h>

CString GetMemoryInfo(MEMORY_BASIC_INFORMATION *pmi);
int _tmain(int argc, TCHAR *argv[])
{
    SYSTEM_INFO sm = {0};
    GetSystemInfo(&sm);
    LPVOID dwMinAddress = sm.lpMinimumApplicationAddress;
    LPVOID dwMaxAddress = sm.lpMaximumApplicationAddress;

    MEMORY_BASIC_INFORMATION mbi = {0};
    _putts(_T("BaseAddress\tAllocationBase\tAllocationProtect\tRegionSize\tState\tProtect\tType\n"));

    for (LPVOID pAddress = dwMinAddress; pAddress <= dwMaxAddress;)
    {
        if (VirtualQuery(pAddress, &mbi, sizeof(MEMORY_BASIC_INFORMATION)) == 0)
        {
            break;
        }

        _putts(GetMemoryInfo(&mbi));
        //一般通過BaseAddress(頁面基地址) + RegionSize(頁面長度)來尋址到下一個頁面的的位置
        pAddress = (BYTE*)mbi.BaseAddress + mbi.RegionSize;
    }

}

CString GetMemoryInfo(MEMORY_BASIC_INFORMATION *pmi)
{
    CString lpMemoryInfo = _T("");

    int iBaseAddress = (int)(pmi->BaseAddress);
    int iAllocationBase = (int)(pmi->AllocationBase);

    CString szProtected = _T("\0");
    if (pmi->Protect & PAGE_READONLY)
    {
        szProtected = _T("R");
    }else if (pmi->Protect & PAGE_READWRITE)
    {
        szProtected = _T("RW");
    }else if (pmi->Protect & PAGE_WRITECOPY)
    {
        szProtected = _T("WC");
    }else if (pmi->Protect & PAGE_EXECUTE)
    {
        szProtected = _T("X");
    }else if (pmi->Protect & PAGE_EXECUTE_READ)
    {
        szProtected = _T("RX");
    }else if (pmi->Protect & PAGE_EXECUTE_READWRITE)
    {
        szProtected = _T("RWX");
    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章