開發人員有必要理解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的啟動過程也非常類似。 |