標籤:

UEFI到操作系統的虛擬地址轉換

大家一定對計算機系統的地址空間概念有所了解。那麼,UEFI的地址空間是什麼樣的呢?UEFI載入並把控制權交給操作系統時,地址空間又發生了什麼變化呢?在這篇文章中,我們來一起探討一下。

物理地址與虛擬地址

首先,我們來複習一下物理地址和虛擬地址的概念。

所謂物理地址,就是從CPU 發出的讀寫請求所引用的地址。請注意,物理地址並不一定指向內存,它有可能指向一個晶元組的功能塊,也可能指向外設,甚至什麼也沒有指到。一個讀寫請求,從CPU出發,經由晶元組,橋接設備,及各種地址解碼硬體配合工作,被最終「引導」到目標設備,從而完成一次讀寫操作。

所謂虛擬地址,則是程序的指令所使用的地址 ( 包括指令本身所在的地址,已及指令所讀寫的目標的地址 ) 。每當CPU遇到一個虛擬地址(可能是CPU從內存讀入指令時,也可能是CPU按指令要求讀寫操作數時),CPU都會進行從虛擬地址到物理地址的轉換 ( 簡單起見這裡只畫出來頁表 ) ,然後使用最終得到的物理地址發出讀寫請求。

為什麼計算機設計者要引入兩種不同的地址概念呢?是為了顯示自己很聰明,故意把事情搞複雜嗎?當然不是(我倒希望是的,這樣我們就可以理直氣壯地少費些腦細胞了)。事實上,對虛擬地址和物理地址加以區分,在操作系統中十分有用。因為這樣可以使每段程序都有自己「固定」的虛擬地址空間,不隨內存分配的調整(比如把程序在內存中移動或剔除)而變動,這個特性在一段程序被多段程序調用時顯得格外重要。此外頁表所提供的保護機制,可以使操作系統更安全地管理整個內存,也為在內存緊張時將部分內存換出到磁碟提供了可能。

X86中的地址轉換

X86體系結構從誕生到大放光彩,其卓越的向後兼容性起了很大作用。成也蕭何,敗也蕭何。保證了用戶一致性體驗的同時,也背上了沉重的歷史包袱。

X86體系架構對虛擬地址的支持有一個發展過程,最早在8086中並沒有虛擬地址的概念,只有物理地址。為了在16位ALU和16位代碼和寄存器上定址20位地址匯流排(2^20=1MB),Intel想到了一個折中的辦法:把內存分段,並設計了4個段寄存器,CS,DS,ES和SS,分別用於指令、數據、其它和堆棧。這樣,一個完整的物理內存地址就由兩部分組成,高16位的段基址和低16位的段內偏移量,當然它們有12位是重疊的,它們兩部分相加在一起,才構成完整的物理地址。

計劃趕不上變化,1982年80286誕生了,24位的地址線催生了保護模式的產生,線性地址概念和段描述符等等被啟用。線性地址通過MMU轉化成物理地址,即:

線性地址=>物理地址

同時實模式可以完全兼容8086的代碼,保護了現有的投資。

1985年收到現代計算機體系結構的影響,80386又引入了頁機制,從而引入了虛擬地址,從而地址轉換又添加了一層

邏輯地址(虛擬地址)=>線性地址=>物理地址

如圖

而地址開啟頁模式也要經歷開啟段模式的過程,過程十分繁複。幸運的是,從此以後,X86在沒有加入更多的層次,只是在現有的基礎上小修小補,譬如頁表變大些(4k->2M->1G),加入更多保護,頁表分層更多等等,這裡不再細表,感興趣的同學可以參看IA32硬體參考手冊。

UEFI中的地址轉換

好了,有了對虛擬地址和物理地址的了解,我們來看看UEFI的地址空間吧。UEFI並不是一個操作系統,不需要先進的進程管理,不會把已裝載入內存的程序進一步移動,所以UEFI採取了最簡單的地址映射機制:虛擬地址==物理地址

註:UEFI固件在resetvector後是實模式,在SEC開始就進入保護模式並打開了段,進入Flat mode。後期如果是64位的UEFI固件的話,會在PEI末期Dxeipl打開頁模式。但虛擬地址==物理地址總是有效的。

這樣一來,各個UEFI模塊(驅動程序或者應用程序)被逐一裝入內存的不同地址,共存於內存中。在每個模塊裝載過程中,根據裝載的首地址,裝載程序把這個模塊內部各處對函數和數據的訪問所使用的絕對地址重新改寫一遍,這個過程叫做重定位(relocation)。這樣,模塊在自己的裝載地址上就能夠正確運行了。請注意,裝載程序對模塊中絕對地址的改寫是有依據的,它的依據是模塊中的一張重定位表(relocation table),這張表是模塊生成時連接器自動生成的,這張表指出了這個模塊中哪些地方編譯器生成了對絕對地址的引用。

這個系統運行得很好,直到它裝載了操作系統,這時操作系統就會接管機器,建立自己一整套全新的虛擬地址系統 ( 這時虛擬地址在絕大部分情況下不再等於物理地址 ),即OS會換上一套自己的頁表 。這看上沒有瑕疵。但是我們要考慮到,有一部分UEFI代碼在操作系統運行時仍然發揮作用, 這就是UEFI Runtime Services所用到的代碼(及數據)。前面已經說過,這些代碼已經在UEFI環境下被重定位過,那麼這些代碼在操作系統環境下能否被直接調用呢?答案是否定的,因為UEFI環境下的虛擬地址很可能和操作系統環境下的虛擬地址衝突,比如,RuntimeServiceA()的UEFI虛擬地址可能是0xCDEF0123,而操作系統很可能已經把0xCDEF0123映射在了另一端代碼或數據上了。

如何解決這個問題呢?聰明的你一定想到了,那就是讓操作系統的引導程序為UEFI Runtime Services專門指定一段虛擬地址空間,然後請UEFI把UEFI Runtime Services所用到的代碼根據新的地址空間再次重定位到指定的虛擬地址上。於是,這些再次重定位後的Runtime Services代碼,就可以在操作系統的虛擬地址環境中繼續發光發熱了!請注意重定位後的代碼在物理內存中的位置是不需要變的,只是虛擬地址變了。( 詳情請參閱UEFI規範中SetVirtualAddressMap() )

我們的問題好像終於解決了:) 只是我們如果不夠仔細的話,我們很可能會忽視一個細節,那就是,一個Runtime模塊為操作系統所做的重定位,是否也是如它初始為UEFI所做的重定位一樣,完全依賴於連接器生成的重定位表呢 ( Relocation Table )?在繼續往下看之前,請先思考下 ……

好了,答案出來了,是否定的。為操作系統做重定位時,這個模塊已經運行了一段時間,它的狀態已經發生了一定變化,具體來說,就是各個全局變數(包括局部靜態變數)的內容可能已經包含了一些新的絕對地址,比如指向了UEFI其他數據結或或新分配的內存。這些變化的內容,都是編譯器和鏈接器無法預測的,當然也就不可能反映在重定位表中。同時,有些絕對地址在UEFI中是一個常數(如Memory Mapped IO地址,也就是設備的物理地址),對這個常數地址的訪問也不會出現在重定位表中,而在操作系統環境下這個地址仍然要被訪問。如果存在這些情況,為了讓這些絕對地址在新的虛擬地中空間中也能指向正確的目標,UEFI Runtime Services驅動程序需要額外寫一些代碼,把這些指針根據操作系統的要求加以修正。(當然,基於重定位表的重定位仍然是需要的,這項工作UEFI Core會替我們做,我們的額外代碼做的是重定位表做不了的事。)請參考UEFI規範和開源UEFI代碼中關於VIRTUAL_ADDRESS_CHANGE event和ConvertPointer()的解釋和使用。

這裡尤其要注意的是:

1)如果一個新地址指向了結構體,而結構體內還有指針成員又指向了一個新地址,那指向結構體的指針和成員指針都要修正;

2)不要重複修正同一個指針。比如,一個動態分配的結構體里包含了指針,而這個結構體被多個UEFI Runtime驅動共享,那麼每個驅動需要修正它指向該結構體的指針,但必須且只能有一個驅動修正該結構體內部的指針。為了防止出錯,建議盡量避免這樣的UEFI Runtime數據共享方式。

3)每段被修正的地址都必須具備Runtime屬性。這對於使用EfiRuntimeServicesCode, EfiRuntimeServicesData類型分配獲取的內存是自然成立的。但對於常數地址,比如Memory Mapped IO 地址,需要採取特殊方法。這可以先用 gDS->GetMemorySpaceDescriptor (BaseAddress, &MemorySpaceDescriptor); 取得這段地址的屬性(Attributes),然後 Attributes = MemorySpaceDescriptor.Attributes | EFI_MEMORY_RUNTIME; 最後用 gDS->SetMemorySpaceAttributes (..); 把Runtime屬性設置回去。請注意考察MemorySpaceDescriptor的BaseAddress和Length是否覆蓋了所需要的地址範圍,如果不是,需要對多段地址進行Runtime屬性設置,直到所需地址範圍被覆蓋。相關函數的詳細說明,請參閱UEFI Platform Initialization Specification。

地址轉換的時間點如下圖:

好了,關於從UEFI到操作系統的虛擬地址轉換就介紹到這裡。更多有趣的UEFI設計和代碼等待您的探究和建議。

歡迎大家關注本專欄和用微信掃描下方二維碼加入微信公眾號"UEFIBlog",在那裡有最新的文章。同時歡迎大家給本專欄和公眾號投稿!

用微信掃描二維碼加入UEFIBlog公眾號


推薦閱讀:

TAG:UEFI | BIOS | 固件 |