轉:CE6內核啓動過程-新角度

CE6內核啓動過程-新角度

開發人員有必要理解CE系統啓動過程。首先回顧一下系統怎樣建立起來的。微軟工具鏈生成.exe和.dll文件。這些文件都包含了Portable Executable格式,簡稱PE格式。它們的結構都是一樣的:
1、  是一種common object文件格式的擴展
2、  有導入、導出表
3、  頭部有入口點,是開始執行的地方。

操作系統都是由編譯器生成的,一個exe(nk.exe)不會連接到任何外部的庫或者DLL。當這個文件執行時候,系統中還沒有任何東西。Exe需要具有一個已知的頭部(PE),來決定程序入口點。因而CPU能知道從那裏開始執行。

另外,PE文件可以按序排列,所以可以XIP(execute in place)。這意味着,加入文件的數據放在某一個虛擬地址,不需要改變情況下,程序代碼可以訪問和使用這個地址中的數據。例如,使用微軟的鏈接器,把內核代碼文件放到虛擬地址0x80000000。那麼程序入口地址會放到exe的文件中,執行時候就能依靠地址,跳到真正的代碼段執行。如果函數foo是放在0x80001000的,foo中又調用了函數bar,bar地址在0x80005000。那麼會有一段結構直接保存在代碼中,去調用地址0x80005000。如下,虛線是函數代碼的分割線。


假如內核的exe文件改變的地址,bar函數也會跟着移動。Foo函數的調用地址,現在指向不對了,需要指向新的地址。


上圖是內核exe文件從0x80000000移到0x80050000,foo函數內的調用地址就不對了。

進程當加載到真正的地址空間後,修改exe和dll文件的動作,稱爲——修正。普通的exe文件允許程序修正地址的記錄,不修正前地址都是錯誤的。所以CE內核exe在加載到特定地址前,會做地址修正。ROMIMAGE程序在生成系統鏡像文件前(nk.bin),會修正內核的exe和某些dll的地址。

最後,我們得到一個修正後的exe——nk.exe,系統內核的一部分。這個exe和其他exe、dll一樣,有程序入口點。執行前,系統的bootloader會把鏡像文件放到正確的地址中。下面我們來看看bootloader如何在鏡像中,找到nk.exe和它的入口點。


Nk.exe是CE6內核的唯一部分,包含了OAL和系統啓動的模板流程。這個流程主要的部分,操作系統內核的所有進程、線程和內存管理放到kernel.dll中。這個dll也是經過ROMIMAGE修正過運行的虛擬地址了。這就是說至少有2個可執行模塊,我們需要找到存放的地址和入口點。入口點的地址在exe和dll中,但在鏡像中怎樣找到exe和dll呢。

CE鏡像有一個重要的結構體,通過ROMIMAGE生成的,叫Table Of Contents,簡稱TOC。TOC保存了系統的指針和數據。在鏡像文件開頭附近,有一個標誌,內容是CECE(0x44424442)。這個標誌後面就存放着TOC的偏移值,那麼bootloader和其他程序可以通過TOC找到鏡像相關的信息。這個偏移值在OAL中定義了一個全局指針pTOC來保存,ROMIMAGE可以使用這個指針來找到和填充TOC的內容。編譯時候,nk.exe的pTOC變量是0xFFFFFFFF,當生產nk.bin時候,ROMIMAGE會做以下處理:
1、  加載nk.exe,然後修正
2、  生產TOC內容,找到鏡像文件存放TOC的地方
3、  找到pTOC指針,確認是指向0xFFFFFFFF
4、  把pTOC的指針,執行真正TOC所在位置

那麼當nk.exe開始運行時候,就知道在那裏能找到TOC。再根據TOC的內容,找到鏡像其他部分。

ROMIMAGE通過bib文件,獲取系統鏡像的地址分佈。Config.bib有2個重要部分,RAMIMAGE和RAM。下面是例子:
NK      0x80070000   0x02000000    RAMIMAGE
RAM     0x82070000   0x01E7F000    RAM

這是告訴ROMIMAGE該怎樣做,系統鏡像在地址0x80070000,可讀寫的內存地址在0x82070000。根據這些信息,就能知道那裏可以加載模塊運行,然後建立TOC內容。爲了讓內核運行起來,TOC也會存放這RAM的信息。下圖是內存中內核放置的示意圖:


操作系統要運行,還需要bootloader做以下工作:
1、  把鏡像放到內存的正確地方
2、  找到CECE標記
3、  使用TOC指針,找到TOC
4、  在TOC中,找到nk.exe的地址
5、  掃描exe文件,找到入口點(通過PE)
6、  跳到入口點地址,開始執行

Nk.exe運行時:
1、  建立和打開虛擬內存映射
2、  收集kernel.dll運行需要的信息
3、  使用pTOC找到kernel.dll
4、  找到kernel.dll入口點
5、  把收集到的信息,傳入kernel.dll的入口點

不同的處理器在啓動過程不太相同,ARM和X86的CPU有不同的虛擬內存管理器(MMU)。但是大體的流程是相同的。
當nk.exe運行前,系統有些條件是一致的:
1、  所有的cache是關閉的
2、  在config.bib配置的RAMIMAGE和RAM段,物理上可訪問的,可讀的。
3、  虛擬地址是預先確定好的
4、RAM無需額外操作,就可以寫入。

以上是任何系統啓動前的先決條件。內核運行是獨立的,不會依賴運行前的bootloader配置的虛擬內存。當內核運行時,nk.exe首先是計算OEMAddressTable中的物理地址。OEMAddressTable是靜態定義了虛擬地址和物理地址的映射。Nk.exe知道:
1、  所屬的虛擬內存
2、  所屬的物理內存
3、  OEMAddressTable的虛擬地址空間

一個簡單公式,計算內核OEMAddressTable的物理地址:
NK:hysicalBase + (NK::Virtual OEMAddressTable – NK::Virtual Base) è NK Physical OEMAddressTable

OEMAddressTable的格式:
<region virtual start>  <region physical start> <region size in MB>
<region virtual start>  <region physical start> <region size in MB>
...
根據以上表格的信息,nk.exe可以通過MMU設置虛擬內存的映射關係。虛擬內存使用OEMAddressTable中的數據,並且使其生效,然後Nk.exe轉換爲可執行的虛擬地址。

注意的是,所有在RAM中的模塊都還沒初始化。不管RAM初始化後的數據是多少,初始化數據都還保存在鏡像文件中(data段的數據)。對數據的讀寫,必須要把鏡像的真實數據內容,複製到RAM中,才允許使用。那麼nk.exe如何知道數據段在鏡像那個位置呢,通過TOC。

TOC不但列出了鏡像中,各個模塊的開始地址,還描述了各個模塊的讀寫指針。從系統鏡像複製到RAM的動作稱爲——copy entries。Nk.exe在訪問讀寫變量之前,需要copy entries到RAM中。指針pTOC就必須是有效的,如何保證pTOC是有效呢。pTOC是隻讀變量,在鏡像文件創建時,ROMIMAGE就會把pTOC寫入。保存pTOC的介質不是RAM,在使用pTOC前,不需要複製到RAM中。Nk.exe有函數把所有的相關信息複製到RAM,稱爲KernelRelocate。這是一個簡單的過程,只是遍歷一個表格內的結構體,然後把虛擬內存內容複製出來。當這個動作結束後,nk.exe的變量才能像其他程序一樣,可以被正常的訪問。


這時,我們纔有真正可以工作的程序,像之前提到那樣可以執行、調用函數、讀寫內存。這還不是線程、進程或任何系統的對象,但是所有東西都放到已知的地方,在系統高端地址開始執行時候,可以使用到。

虛擬內存有很大的彈性,CE保留了一些虛擬地址段,只給系統內核使用。大小爲4K頁面的虛擬內存,在0xFFFE0000以上的高端地址空間中,保留起來。內核映射了一些物理地址到這些頁面,用來保存全局動態的數據。它們一部分用來MMU的內存映射,一部分保留用來做內核態和中斷的堆棧,最重要是一部分保留作爲Kernel Data Page。根據內核版本,保留不同的頁面大小。Nk.exe直接可以訪問和初始化這些頁面。

Nk.exe的3個重要數據
1、   pTOC的備份
2、  OEMAddressTable的地址
3、OEMInitGolbals函數的地址

前2項內容保存在Kernel Data Page中,任何代碼知道這個頁的地址,就可以找到系統鏡像的內容和基本的虛擬映射關係。最後一項信息比較特殊,nk.exe使用一次後就傳遞給kernel.dll了。放置方式如下:


現在Kernel Data Page被初始化了,虛擬內存也激活了,可以跳入到微軟的kernel.dll中入口了。記住,我們通過TOC找到鏡像的kernel.dll,同時也可以找到其他模塊的入口。即使Nk.exe知道如何把Kernel Data Page放到虛擬內存中,但kernel.dll不知道確認它自己運行位置。因此,我們需要把Kernel Data Page的虛擬地址傳遞給kernel.dll的入口。

跳轉完成後,開始執行內核代碼。入口點獲取了Kernel Data Page的地址,因此通過TOC可以獲取任何系統鏡像的信息。內核開始做一些準備工作和臨界區,確保它是Kernel Data Page當前唯一使用者。

Kernel.dll有一個靜態函數和數據表,編譯時候作爲dll的一個靜態數據結構體,稱爲NKGlobals。由於kernel.dll被ROMIMAGE修正過,運行在特定的地址中,所以運行時NKGlobals的指針也會被修改成正確的地址。這些函數指針中,如SetLastError()和NKwvsprintfW(),內核允許它們直接調用。但內核並不清楚這些函數其實在kernel.dll中,接着內核會被告知這部分的函數和數據,其實是在kernel.dll中。

Kernel.dll通過OEMInitGlobals,把NKGlobals的地址傳回nk.exe。流程如下:


如上,OEMInitGlobals函數保存了一個指向OMEGlobals結構體的指針。這個結構體是內核能夠其他功能函數的關鍵。Kernel.dll模塊確立後,可以被任何一種結構的處理器運行(如x86、ARM等)。Nk.exe提取了這類處理器的特有部分,提供給平臺,來確保系統的運行(xcale或OAMP,它們與ARM有些微差別)。OMEGlobals的組成與NKGlobals類似,有以下成員:
  • PFN_InitDebugSerial(), PFN_WriteDebugByte(), PFN_ReadDebugByte()
  • PFN_SetRealTime(), PFN_GetRealTime(), PFN_SetAlarmTime()
  • PFN_Ioctl()
這些函數指針指向OEM提供的函數,如nk.exe中的OEMInitDebugSerial 和OEMIoctl。這裏會列出許多函數,因此kernel.dll能知道特定處理器環境下的功能函數。

OEMInitGlobals完成後, kernel.dll對特定環境下的工作環境就能確定下來。它能知道那裏有內存,內存怎樣映射,鏡像每個模塊的地址等。Nk.exe也有個指針能獲取這些信息,因此2個模塊通過握手方式,在動態連接環境下進行簡單的數據交互。

Nk.exe和kernel.dll在沒有進程、線程和內核服務的情況下,完成了所有該做的事情。爲讓系統繼續運行下去,kernel.dll還需要做3件事情:
1、  處理器特定的設置
2、  處理器本地的設置
3、  平臺特殊的設置

處理器特定的設置,是由kernel.dll調用特定的處理設置函數,如ARM芯片的是ARMSetup函數,X86是X86Setup函數。雖然處理器特定設置的代碼較多,但是在一個線程中執行的,沒有進程存在。因此這個操作有些限制:
1、  設置很難申請頁表和保留的虛擬內存給內核頁表
2、  在頁表中根系cache信息
3、  刷新TLB
4、  配置處理器的總線和協處理器
處理器特定設置代碼中,還要設置Interlocked函數,以便nk.exe可以調用它。即使是運行在比較早的階段,CE需要在多個線程之間做同步工作。其中使用頻率最高的就是Interlocked函數,它有多個功能函數組成,包括InterlockedCompareExchange。InterlockedCompareExchange函數流程是:
1、  讀取本地內存,設置到寄存器中(R1)
2、  與其他寄存器(R2)讀取值,做比較
3、  如果2個寄存器(R1和R2)的值不相等,則退出
4、  將另外一個寄存器值(R3)寫回到本來內存中

這4步維繫着線程之間的同步。但這4步之間可能會被中斷,要保證處理器執行函數時候的正確,那麼就要確保能之間操作到硬件,硬件的中斷必須關閉。可這又引出一個問題,由於用戶態進程沒有權限關閉中斷,每次在線程之間同步,就要通過內核去關閉中斷,是比較低效的。
爲了提升效率,整個系統只能有一個地方讓InterlockedCompareExchange運行。4個步驟的代碼都放到Kernel Data Page的一個特定位置中,nk.exe和kernel.dll(其他能訪問到Kernel Data Pag的進程)就能調用到函數,那麼所有的操作都在同一個位置上執行。這樣的設置後,函數需要是可從頭執行的(意味着即使線程切換後,函數不是由現場恢復,而是從頭再開始運行),爲什麼要這樣呢?

首先我們來看看操作系統中,線程切換的情況:
1、  正在運行的線程有特殊的操作(sleep、wait等)
2、  線程的時間片輪用完(timer中斷裏面做判斷),其他線程開始運行
3、  中斷產生了,一個高優先級的線程要開始運行

後2種情況是一樣的,中斷產生導致線程的切換。由於同步函數1-4步之間都有可能被中斷打斷,產生線程切換。這就需要我們執行函數時候,保持原子性操作。
爲了讓1-4步的操作是原子性,每次有中斷產生時候,一個邊界檢查會判斷CPU是否在執行1-4步的操作代碼中。如果判斷到,CPU是在1-4步執行過程中,產生的中斷。那麼一個運行指針會被重置到函數步驟1的位置,那麼這個操作可以從頭再來一次。爲讓中斷代碼可以檢查到CPU是否正運行在1-4步中,那這段代碼必須放到Kernel Data Page中。當Interlocked函數放到Kernel Data Page後,nk.exe和kernel.dll都能使用它,做多線程的同步工作了。

回到正題,kernel.dll執行的下一步,即是處理器本地的設置。這裏第一步就是設置KITL.dll,用來調試系統內核的工具。
KITL(Kernel Independent Transport Layer),是設備內核和桌面PB之間進行數據通訊的方式。通常KITL由內核提供,做數據編碼和傳輸的工作。BSP(Board Support Package)不需要關心設備與桌面PC之間的數據內容,只需用完成數據的正確通訊就可。使用KITL的通訊載體,可以是RS232串口、USB、網卡等串行設備。

處理器本地設置的另外一些操作,包括
1、  初始化系統內核的調試輸出(OEMGlobals 結構內的OEMInitDebugSerial函數)
2、  輸出調試字符串(Windows CE Kernel Version xxxx)
3、  爲處理器選擇可用的配置
當處理器本地設置完成後,就是平臺的特殊設置步驟了。由於是OEM和板子相關代碼,因此存放在nk.exe中。初始化時候,內核通過OEMGlobals的OEMInit函數進行。OEMInit是初始化板子相關的設置,另外還會啓動KITL。

如果KITL是Nk.exe包含的,nk.exe就能之間訪問。如果KITL是dll的形式,那麼內核調試時候,在處理器本地設置階段,就要加載這個dll。無論那種形式,內核都會使用OEMInit來啓動KITL。

OEMInit完畢後,內核開始允許進程、線程運行了。接着同步cache,如果還沒準備好運行,就進入處理器服務模式。這裏會做一些操作,包括:
1、  枚舉有效的內存(OEMEnumExtensionDRAM)
2、  爲內核初始化臨界區
3、  初始化堆
4、  初始化進程和線程的結構體
5、  多線程模式啓動前的其他操作

當所有線程的初始化完畢後,內核準備調度第一個線程。這個線程在kernel.dll中,叫SystemStartupFunc。爲了讓線程運行起來,內核設置成沒有其他線程切換,第一個線程是有效線程,然後才調用線程調度代碼。線程調度先查看有效的線程,選擇下一個運行的線程。這時,系統只有一個線程手動的配置運行起來,然後再一個個的切換其他線程。

SystemStartupFunc在cache刷新後,就可以被執行了。爲了順利的運行線程,還有以下工作:
1、  初始化系統加載器
2、  初始化頁池
3、  初始化系統logging
4、  初始化系統debugger

SystemStartupFunc通過OEM函數來完成初始化動作,這個函數在OEMGlobals中,通過OEMIoctl傳入OEM_HAL_POSTINIT。這會告訴nk.exe,開始的準備工作都完成了,可以進行進程、線程的調度了。

從OEMIoctl退出後,SystemStartupFunc繼續會初始化系統的消息隊列、watchdogs,然後創建電源管理和文件系統的線程。因此,操作系統其他高端內容開始被執行。SystemStartupFunc最後一步會創建其他線程,來執行RunAppsAtStartup。這個函數會創建第一個用戶態進程。

至此,內核、電源管理、文件系統等都被創建和執行了,應用程序也開始就緒運行,系統註冊表也可以使用了,系統內核啓動完畢。

以上是CE6的內核啓動過程,CE5的啓動過程也非常類似。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章