在做網路安全事件分析的時候,都會遇到記憶體定址的知識,例如上次跟大家分享的《 空指標漏洞防護技術》,就涉及到非法訪問記憶體地址的問題。如果這個坎兒邁不過去,你就會迷失在程式碼中,更無從分析了。今天綠盟科技的安全技術專家就講講這個記憶體定址的原理,文章分為上下兩篇《記憶體定址原理》及《記憶體定址方式》。
隨著資訊化發展和資料處理能力需求的提高,對計算機硬體產品的效能和容量也提出了新的挑戰,要求計算機處理能力也要能隨實際情況需求的變動而提升、改變。
當下,一臺普通的電腦硬碟容量也要200多G,記憶體也有4G;如此大容量的硬碟和記憶體,在處理大量資料或是大型遊戲面前還是顯得力不從心,需要通過擴容來滿足需求,比如將記憶體由4G提升到8G或是16G不等。擴容後對個人體驗確實提升不少。對於記憶體容量的提升需要有相應的硬體基礎支撐,需要有能消化掉這麼多記憶體的定址地址。比如說如果一8位微控制器如果要裝載16G的記憶體,那就是暴殄天物。
哪裡有需求哪裡就有市場 ;計算機從8位的51微控制器,20位8086定址,發展到32位 win2003,64位win10,都是由於資訊化需求的膨脹推動著計算機一代又一代的改革創新。
對於記憶體的擴容,很多人都不是很清楚應用程式如何使用的實體記憶體地址。遠了不說,單說現在常用計算機中的32位、64系統;系統是怎麼樣將虛擬地址轉化成線性地址,線性地址又是怎樣轉換成實體地址的,其中又用到了哪些暫存器或是資料結構,相信很多人對此也是一知半解;也像我一樣,想結合例項從地址轉換的本質來掌握其精髓之處。接下來就一起學習從邏輯地址到實體地址的整個轉換過程。
1.真實模式與保護模式簡介
CPU常見三種工作模式:真實模式與保護模式,虛擬8086模式。
真實模式 :CPU復位(reset)或加電(power on)的時候以真實模式啟動,處理器以真實模式工作。在真實模式下,記憶體定址方式和8086相同,由16位段暫存器的內容乘以16(10H)當做段基地址,加上16位偏移地址形成20位的實體地址,最大定址空間1MB。在真實模式下,所有的段都是可以讀、寫和可執行的。真實模式下沒有分段或是分頁機制,邏輯地址和實體地址相等。
由此得知:
- 在真實模式下最大定址空間時1M,1M以上的記憶體空間在真實模式下不會被使用。
- 在真實模式所有的記憶體資料都可以被訪問。不存在使用者態、核心態之分。
- 在BIOS載入、MBR、ntdlr啟動階段都處在真實模式下。
保護模式 :對於保護模式大家並不陌生;是目前作業系統的執行模式,利用記憶體管理機制來實現線性地址到實體地址的轉換,具有完善的任務保護機制。
保護模式常識:
- 現在應用程式執行的模式均處於保護模式。
- 橫向保護,又叫任務間保護,多工作業系統中,一個任務不能破壞另一個任務的程式碼,這是通過記憶體分頁以及不同任務的記憶體頁對映到不同實體記憶體上來實現的。
- 縱向保護,又叫任務內保護,系統程式碼與應用程式程式碼雖處於同一地址空間,但系統程式碼具有高優先順序,應用程式程式碼處於低優先順序,規定只能高優先順序程式碼訪問低優先順序程式碼,這樣杜絕使用者程式碼破壞系統程式碼。
虛擬8086 模式: 簡稱V86模式是執行在保護模式中的真實模式,為了在32位保護模式下執行純16位程式。可以把8086程式當做保護模式的一項任務來執行。虛擬8086允許在不退出保護模式的情況下執行8086程式。
虛擬8086常識:
- 定址的地址空間是1M位元組.
- 可以在虛擬8086模式下執行16位DOS程式。
- 在V86模式下,程式碼段總是可寫的;這與真實模式相同,同理,資料段也是可執行的。
- 32系統編寫V86模式的程式:
2. 保護模式定址基礎知識
接下來就以32位系統為例,介紹保護模式下,記憶體中一些地址轉換相關的寄存機和資料結構。
2.1 記憶體地址概念
邏輯地址 :在進行C語言程式設計中,能讀取變數地址值(&操作),實際上這個值就是邏輯地址,也可以是通過malloc或是new呼叫返回的地址。該地址是相對於當前程式資料段的地址,不和絕對實體地址相干。只有在Intel真實模式下,邏輯地址才和實體地址相等(因為真實模式沒有分段或分頁機制,CPU不進行自動地址轉換)。應用程式設計師僅需和邏輯地址打交道,而分段和分頁機制對一般程式設計師來說是完全透明的,僅由系統程式設計人員涉及。應用程式設計師雖然自己能直接操作記憶體,那也只能在作業系統給你分配的記憶體段操作。一個邏輯地址,是由一個段識別符號加上一個指定段內相對地址的偏移量,表示為 [段識別符號:段內偏移量]。
線性地址 :是邏輯地址到實體地址變換之間的中間層。程式程式碼會產生邏輯地址,或說是段中的偏移地址,加上相應段的基地址就生成了一個線性地址。如果啟用了分頁機制,那麼線性地址能再經變換以產生一個實體地址。若沒有啟用分頁機制,那麼線性地址直接就是實體地址。Intel 80386的線性地址空間容量為4G(2的32次方即32根地址匯流排定址)。
實體地址(Physical Address) 是指出目前CPU外部地址匯流排上的定址實體記憶體的地址訊號,是地址變換的最終結果地址。如果啟用了分頁機制,那麼線性地址會使用頁目錄和頁表中的項變換成實體地址。如果沒有啟用分頁機制,那麼線性地址就直接成為實體地址了,比如在真實模式下。
2.2 虛擬地址,線性地址,實體地址關係
對於保護模式下地址之間的轉換,對程式設計師來說是透明的。那麼實體記憶體通過記憶體管理機制是如何將虛擬地址轉換為實體地址的呢?當程式中的指令訪問某一個邏輯地址時,CPU首先會根據段暫存器的內容將虛擬地址轉化為線性地址。如果CPU發現包含該線性地址的記憶體頁不在實體記憶體中就會產生缺頁異常,該異常的處理程式通過是作業系統的記憶體管理器例程。記憶體管理器得到異常報告後會根據異常的狀態資訊。特別是CR2暫存器中包含的線性地址,將需要的記憶體頁載入到實體記憶體中。然後異常處理程式返回使處理器重新執行導致頁錯誤異常的指令,這時所需要的記憶體頁已經在實體記憶體中,所以便不會再導致頁錯誤異常。
2.3 段式機制及例項分析
前面說到線上性地址轉換為實體地址之前,要先由邏輯地址轉換為線性地址。系統採用段式管理機制來實現邏輯地址到線性地址的轉換。保護模式下,通過”段選擇符+段內偏移”定址最終的線性地址。
CPU的段機制提供一種手段可以將系統的記憶體空間劃分為一個個較小的受保護的區域,每個區域為一個段。相對32位系統,也就是把4G的邏輯地址空間換分成不同的段。每個段都有自己的起始地址(基地址),邊界和訪問許可權等屬性。實現段機制的一個重要資料結構就是段描述符。
下面是個程式例項中顯示除了各個段的值:
圖中給出了程式碼段CS,堆疊段SS,資料段DS等段暫存器的值;從得到的值可知,SS=DS=ES是相等的,至於為什麼有些段的值相等,後面會說到。 以例項中給出的地址0x83e84110 為例,哪裡是段描述符,哪裡是段內偏移, 又是如何將該邏輯地址轉換為線性地址的呢?相信很多人都迫不及待的想知道整個轉換過程,接下來就要看看邏輯地址到線性地址詳細轉換過程。
上面說到段式管理模式下有段選擇符+段內偏移定址定位線性地址,其實際轉換過程如下圖所示
從圖中可知,邏輯地址到線性地址的轉換,先是通過段選擇符從描述符表中找到段描述符,把段描述符和偏移地址相加得到線性地址。也就是說要想得到段描述符需要三個條件:
- 得到段選擇符。
- 得到段描述符表
- 從段描述符表中找到段描述符的索引定位段描述符。
前面我們提到了段描述符 + 偏移地址,並沒有提段選擇符和段描述符表。所以我們要弄清楚這幾個觀念段選擇符,段描述符表,段描述符,以及如何才能得到這幾個描述符?
2.3.1 段描述符基礎知識
從上圖可知,通過段選擇符要通過段描述符表找到段描述符,那麼段描述符表是什麼,又是怎麼得到段描述符表呢?
在保護模式下,每個記憶體段就是一個段描述符。其結構如下圖所示:
圖中看出,一個段描述符是一個8位元組長的資料結構,用來描述一個段的位置、大小、訪問控制和狀態等資訊。段描述符最基本內容是段基址和邊界。段基址以4位元組表示(圖中可看出3,4,5,8位元組)。4位元組剛好表示4G線性地址的任意地址(0x00000000-0xffffffff)。段邊界20位表示(1,2位元組及7位元組的低四位)。
2.3.2 段描述符表例項解析
在現在多工系統中,通常會同時存在多個任務,每個任務會有多個段,每個段需要一個段描述符,段描述符在上面一小節已經介紹,因此係統中會有很多段描述符。為了便於管理,需要將描述符儲存於段描述符表中,也就是上圖畫出的段描述符表。IA-32處理器有3中描述符表:GDT,LDT和IDT。
GDT是全域性描述符表。一個系統通常只有一個GDT表。GDT表也即是上圖中的段描述符表,供系統中所以程式和任務使用。至於LDT和IDT今天不是重點。
那麼如何找到GDT表的存放位置呢?系統中提供了GDTR暫存器用了表示GDT表的位置和邊界,也就是系統是通過GDTR暫存器找到GDT表的;在32位模式下,其長度是48位,高32位是基地址,低16位是邊界;在IA-32e模式下,長度是80位,高64位基地址,低16位邊界。
位於GDT表中的第一個表項(0號)的描述符保留不用,成為空描述符。如何檢視系統的GDT表位置呢?通過檢視GDTR暫存器,如下圖所示
從上圖看出GDT表位置地址是0x8095000,gdtl值看出GDT邊界1023,總長度1024位元組。前面知道每一項段描述符佔8位元組。所以總共128個表項。圖中第一表項是空描述符。
2.3.3 段選擇符結構
前面我們介紹了段描述符表和段描述符的格式結構。那麼如何通過段選擇符找到段描述符呢,段選擇符又是什麼呢?
段選擇符又叫段選擇子,是用來在段描述符表中定位需要的段描述符。段選擇子格式如下:
段選擇子佔有16位兩個位元組,其中高13位是段描述在段描述表中的索引。低3位是一些其他的屬性,這裡不多介紹。使用13位地址,意味著最多可以索引8k=8192個描述符。但是我們知道了上節GDT最多128個表項。
在保護模式下所有的段暫存器(CS,DS,ES,FS,GS)中存放的都是段選擇子。
2.3.4 邏輯地址到線性地址轉換例項解析
已經瞭解了邏輯地址到虛擬地址到線性地址的轉換流程,那就看看在前面圖中邏輯地址0x83e84110對應的線性地址是多少?
首先,地址0x83e84110對應的是程式碼段的一個邏輯地址,地址偏移已經知道,也就是段內偏移知道,通過暫存器EIP得到是0x83e34110。段選擇符是CS暫存器CS=0008,其高13位對應的GDT表的索引是1,也就是第二項段描述符(第一項是空描述符)。GDT表的第二項為標紅的8個位元組
通過段描述的3,4,5,8個位元組得到段基址。
如上圖所示第二項段描述符的3,4,5,8位元組對應的值為0x00000000。由此我們得到了段機制和段內偏移。最後的線性地址為段基址+段內偏移=0x0+0x83e34110=0x83e34110。
由此我們知道在32系統中邏輯地址就是線性地址。
其實通過觀察其他的段選擇子會發現,所有段選擇子對應的基地址都是0x0,這是因為在32系統保護模式下,使用了平坦記憶體模型,所用的基地址和邊界值都一樣。既然基地址都是0,那麼也就是線性地址就等於段內偏移=邏輯地址。
總之:
- 段描述符8位元組
- GDTR是48位
- 段選擇子2個位元組。
2.4 頁式機制及例項分析
前面介紹了由邏輯地址到線性地址的轉換過程,那麼接下來就要說說地址是如何將線性地址轉為實體地址。需要先了解一些相關的資料結構。
前面說到如果CPU發現包含該線性地址的記憶體頁不在實體記憶體中就會產生缺頁異常,該異常的處理程式通過是作業系統的記憶體管理器例程。記憶體管理器得到異常報告後會根據異常的狀態資訊。特別是CR2暫存器中包含的線性地址,將需要的記憶體頁載入到實體記憶體中。然後異常處理處理返回使處理器重新執行導致頁錯誤異常的指令,這時所需要的記憶體頁已經在實體記憶體中,所以便不會再導致也錯誤異常。
32位系統中通過頁式管理機制實現線性地址到實體地址的轉換,如下圖:
2.4.1 PDE結構及如何查詢記憶體頁目錄
從上圖中我們知道通過暫存器CR3可以找到頁目錄表。那麼CR3又是什麼呢?在32系統中CR3存放的頁目錄的起始地址。CR3暫存器又稱為頁目錄基址暫存器。32位系統中不同應用程式中4G線性地址對實體地址的對映不同,每個應用程式中CR3暫存器也不同。也就是說每個應用程式中頁目錄基址也是不同的。
從上圖知道頁目錄表用來存放頁目錄表項(PDE),頁目錄佔一個4kb記憶體頁,每個PDE長度為4位元組,所以頁目錄最多包含1KB。沒啟用PAE時,有兩種PDE,這裡我們只討論使用常見的指向4KB頁表的PDE。
頁目錄表項的高20位表示該PDE所指向的起始實體地址的高20位,該起始地址的低12位為0,也就是通過PDE高20位找到頁表。由於頁表低12位0,所以頁表一定是4KB邊界對齊。 也就是通過頁目錄表中的頁目錄表項來定位使用哪個頁表(每一個應用程式有很多頁表)。
以啟動的calc程式為例,CR3暫存器是DirBase中的值,如下圖
Calc.exe程式對應的CR3暫存器值為0x2960a000,下面是對應PDT結構
2.4.2頁表結構解析
頁表是用來存放頁表表項(PTE)。每一個頁表佔4KB的記憶體頁,每個PTE佔4個位元組。所以每個頁表最多1024個PTE。其中高20位代表要使用的最終頁面的起始實體地址的高20位。所以4KB的記憶體頁也都是4KB邊界對齊。