8.25.2008

Windows Embedded CE 6.0 是如何啟動的?


As writer said, the most interesting thing in Embedded system is how the Kernel start. This article gives us insights into kernel and start process.
Hopefully, I can understand this artical more by translating into Chinese.



Windows Embedded CE 6.0 是如何啟動的?

posted by Kurt Kennett, Senior Development Lead, Windows CE OS Core


作業系統程式,如同我的其中一個同事最近才了解的,就只是一個"程式"而已。它沒有存在任何魔法或高深的知識。事實上,一個作業系統程式跟其它程式比起來也只是有較好的結構及設計而已。當你再懷疑時,它就已經是了。愈來愈多的人需要了解及維護一套能支援除錯的作業系統程式的核心。這些人藉由不斷的進步及換工作去尋找新的挑戰來持續學習這樣的核心程式。但這些人當中,如果只有一個人了解作業系統的運作方式,那將是多麼大的風險。

在工作上,我追過作業系統的流程,其中最有趣的部份是它如何啟動。雖然初始化在設計裡是最後一步(最不重要?),但同時卻也揭開,使用原理的最根本的基礎。試想一個平台要啟動,從一開始什麼都沒有,就只有一棵能執行指令(有時甚至連記憶體也沒能使用)的CPU,到能夠將一個平台從原點帶到完全功能的系統,一個不只是只會利用硬體的系統,而還要能夠讓硬體之間有個共通協定的系統。

在這篇文章我要仔細的討論Winodows CE 6.0 kernel是如何啟動的,以及Microsoft 的 kernel 與 OEM 的程式之間的關係。透過這樣的了解,希望有更多的人有個較好的 IDEA,Microsoft "如何"及"為什麼"這樣設計。

一開始,讓我們快速的複習一下 operation system software 是如何被建立的。Microsoft tool chain 發行了.EXE 以及/或者 .DLL 程式檔。這兩個檔案格式都是"Portable Executable"或叫做"PE"格式。他們在許多方面幾乎是一樣的:
  • 他們都是Common Object File Format(COFF) 格式
  • 他們都有輸入表格及輸出表格(EXE的輸出表格通常是空白的)
  • 他們都有個entry pointer(就是程式從哪執行),它是被定義在他們的headers裡

至於opperation system kernel program,也沒有什麼特別的,就是被標準的 compler 加一些很少的 definitions 所 compile 出來的。它的.EXE(叫做NK.EXE)也沒有連接任何 external library 或 DLL -實際上它也不能。當它開始執行時沒有任何東西在系統裡,就此而言甚至連所謂的系統也沒有。因為 EXE 是已知的格式(PE COFF), 所以你可以從它檔案的開頭裡找到它的entry point。意思就是說我們知道CPU's instruction pointer 會設在哪裡,程式會從哪裡開始執行。

PE 檔還有一個額外的特性就是它可以被安置以至於它能"execute in place"。意思就是說如果 file data 被放在特定的 virtual address,那麼檔案裡的 program code 的部份不需做任何改變就能知道其它code 和data 的正確位址。舉例來說,假設 Microsoft linker program 將 Kernel program 放在 virtual address 0x80000000,然後將它與 code (function entry point) 的關聯也放在 EXE file ,那麼其它的code 就可以透過address(定址?)跳到這一個code。如果 function foo()是在位址0x80001000 然後裡面的一條 code call function bar() 在位址 0x80005000 ,那麼就會有個從 foo() call 到 0x80005000 的h指令,直接存在程式碼裡。虛線只是代表 function code 的開始和結束。


















如果Kernel 的EXE 程式不能放在0x80000000 而且必須要被移,那 bar() function 就要跟著移,而且在 foo() 的呼叫指令也必需被改變去呼叫到正確的新位址。否則它會呼叫到錯的地方:

你可見上面的例子,如果 kernel EXE 檔被設計放在位址 0x80000000 但卻被載入在位址 0x8050000,那麼程式裡的指令就會不正確。

當一個 EXE 或 DLL 程式檔在被載入到對應的實際載入位址時需要做一些改變,這樣的改變的程序就叫做 "fixed up" 。(改變的)記錄會被放在一個標準的、允許程式檔被 fixed up 的 EXE 檔裡。然而,直到 fixup 程序完成,EXE 檔的 function 位址會一直是錯的。為了避免這樣的情況,Windows CE kernel EXE 檔會事先 fixed up 好,然後才載入到特定的位址。有個程式叫做 ROMIMAGE ,當它再 build operation system image file (NK.BIN)時,實際上就對 kernel EXE 及一些被使用的 DLLs 做事前的處理並將它們 fix up。


概要重述一下,我們有個已經 fixed-up 的 EXE 檔叫到 NK.EXE,它包含了一部份的 operating system kernel。這個EXE有個 entry point 定義在裡面,就像每一個其它 COFF EXE DLL 一樣。在執行的一開始,系統的 bootloader 會image file 放到正確的位址,並找到這個 EXE entry point,然後跳到那裡執行。bootloader 是另一個部份,我們將不在這討論,況且每個 bootloader 在不同的平台上也不盡相同。在這篇文章裡,我們簡單的假設 bootloader OS image 檔放在記憶體裡的特定的位址。從下面我們可以看到,bootloader image 裡可以找到 NK.EXE 檔並找到它的 entry point




NK.EXE 只是Windows Embedded CE 6.0 kernel的一部份- 它包含了OEM Adaptation Layer (OAL) 和一些永不變的程式來啟動系統。 Operating system kernel (kernel.dll) 的主要的部份做了所有的process, threadmemory的功能。這個 DLL 是已經被 ROMIMAGE “fixed up” 過的,並放在記憶體裡特定的 virtual memory。所以意思是說,至少有兩個可執行的檔,我們需要知道它們的位址及 entry point這個 Entry point 是被存在 EXE/DLL 檔裡,所以我們可以找的到,但我們怎麼知道 EXE DLL 檔在 image 裡的位址呢?

Windows CE images 有一個很重要的結構,叫做 “Table Of Contents”, 或 TOC,它是由 ROMIMAGE 建立並放在 image 檔裡。TOC 包含了operating system image 檔的 pointers 及 metadata。然後在靠近 image 檔一開始的地方,放個 marker - "CECE" (0x44424442)。在這個 maker 之後就是TOC的位址。如此 bootloader 或其它程式可以找到關於這個 image 的資訊。除了透過 marker 可以知道 TOC 的位址外,OAL 還必需定義一個公用的 symbol 叫做 'pTOC' (依照 'C' 命名規則),當 ROMIMAGE 再準備 image 檔時,它會找 pTOC 並填入 TOC 的 virtual address。在編譯時,NK.EXE 的 pTOC 變數必須是 0xFFFFFFFF。當 ROMIMAGE 再準備 NK.BIN OS system imgae 時,ROMIMAGE 會做下列事 (除了其它工作以外):

  1. 載入 NK.EXE 並 fix it up
  2. 建立 TOC 並在 image 檔裡找個位址放(當 os image 被載入時,是在 virtual memory裡)
  3. 在 NK.EXE 裡找 'pTOC' 變數並確定值為 0xFFFFFFFF。
  4. 將TOC的位址值(2)設給pTOC


這個方式,當 NK.EXE 開始時它會參考這個變數來知道 TOC 在哪裡。透過使用TOC,程式可以找到 operating system image 的其它部份。



ROMIMAGE 使用配置檔 .BIB檔來知道image和RAM 應該放哪裡。在 CONFIG.BIB 裡有兩個重要的部份- RAMIMAGE 及 RAM。這裡有個範例來自 DEvice Emulator's CONFIG.BIB:

NK 0x80070000 0x02000000 RAMIMAGE

RAM 0x82070000 0x01E7F000 RAM

這些 entryies 告訴 ROMIMAGE 要做什麼。它會知道將 OS image 檔放在 0x80070000,以及可以從0x82070000 開始 讀/寫 memory。依照這個資訊,它可以將一些 modules 像是 NK.EXE 和 KERNEL.DLL 放進virtual memory, 然後產生一個TOC並且將TOC 放入image裡。 為了幫助kernel 啟動,TOC 也包含 RAM 位址的資訊。更詳細的資訊有關 image 檔如何放在memory裡,顯示如下:



為了實際讓 operating system 運作起來,bootloader 需要:

1.將image檔放到記憶體裡正確的位址

2.找 "CECE" marker

3.使用 TOC pointer,就是在找到TOC之後

4.在TOC裡找"NK.EXE" 檔的起始點

5. 掃描這個EXE檔的entry point(它是標準的PE格式檔)

6.跳到與這個entry point 相符合的位址


自NK.EXE 啟動之後,有一些有趣的事發生。一般來說,NK.EXE 有一些自有的工作需要做:
  1. 設定 virtual memory並且啟動它
  2. 收集一些資訊,這些資訊是 KERNEL.DLL 會用來跑系統的資訊
  3. 使用 pTOC 來找 TOC 以知道 KERNEL.DLL 在 operatinhg system image 的哪裡
  4. 找 KERNEL.DLL 的 entry point
  5. 在呼叫他自己的entry 時把在搜集到的重要資訊(2)給 KNERNEL.DLL


我們會仔細的走過一遍這些動作來更了解他們。有一些啟動程序在不同的CPU有不同的方式。舉例來說,ARM CPU 和 X86 CPU就有不同的vitual memory management 硬體和映設結講。然而,為了唯持一致性,一般的程序還是會被maintained。 無論何時,可能的話我會試著針對架構說明任何的運作。



當NK.EXE啟動時,有一些系統的準備事項:
  1. 所有的 caches disabled
  2. RAMIMAGE 和 RAM 的區域的 entry ,就是在 CONFIG.BIB 檔明確說到的 ,是可定址和讀取的在實際位址
  3. Virtual memory是在預先定義的狀態(CPU 基本上是執行在 physical address 模式)
另一個預先處理的事情在NK.EXE啟動前會被滿足的,或是在NK.EXE一開始執行就被完成的,就是
4.RAM 應該可寫,不用任何設定(例如,memory controller 的設定)


這些假設允許 NK.EXE 的啟動程式碼去帶起任何必要的特定系統,而不用擔心什麼已做什麼還沒做。上面的第三點也許反直覺,但由於Kernel 必需要很自給自足的執行,要依靠bootloader來設定virtual memory再它啟動之前是無義意的。



Kernel 在physical address mode下開始執行指令時,NK.EXE的第一個動作就是去計算OEMAddressTable symbol 的 physical address。它是一個表格由 kernel 所建立的,kernel 定義靜態(不會改變)的virtual memory的預設區域。 NK.EXE 知道:
  1. 在virtual memory裡,它有自己的位置(它將會執行指令的地方)
  2. 在physical memory裡,它有自己的位置(它正在執行指令的地方)
  3. OEMAddressTable 的 virtual address (當 ROMIMAGE 建立 NK.EXE 並隨後 fixed up 所決定的位置)
透過下列的資訊,一個簡單的計算可告訴 Kernel OEMAddressTable 的 physical addreaa
NK::PhysicalBase + (NK::Virtual OEMAddressTable – NK::Virtual Base) è NK Physical OEMAddressTable

The OEMAddressTable has triads of DWORDS making up a line in a table, with the following format:

<region virtual start> <region physical start> <region size in MB>

<region virtual start> <region physical start> <region size in MB>

...

從這個表格找到的資訊,可以讓 NK.EXE 為 Memory Management Unit (MMU) 設定 virtual memory mapping tables,好讓 MMU 來執行它自己的功能。virtual memory mapping tables 存放著 MMU-formated mapping tables,這些 tables 在不同平台上都不太相同 - OEMAddressTable 是一個很簡單的格式以至於適用於各種架構。使用OEMAddressTAble 裡的資料,Virtual memory 可以被設定及啟動,然後 NK.EXE 轉換到它可以執行程式的 virtual address上。
有一件事要注意的就是,在這個時間點任何在 RAM 的東西都還不能用(這個 RAM 是需要事先被初始化的(被填為0或其它已知的值))。這個RAM在一開始仍然是乾靜的,可以放入任何東西。初始值(就是在image檔裡(NK.EXE的 .data 部份及其它模組))被使用前,必須要從 image 裡 copy 到實體 RAM 的位址. 不過 NK.exe 怎麼知道什麼東西要 copy 或要放到 virtual RAM 的哪裡? 答案就是TOC。
TOC 不只是列出在 image 裡所有模組的起始位址, 它也描述了 RAM 及每一個模組可讀寫的位址,以至於 Kernel 可以依照這些資訊來工作。OS imaeg 的一部份叫做"copy entries",需要被 copy 到 RAM 去。在NK.EXE 可以存取(讀/寫)它自己的變數之前,它需要 copy "copy entries" 到 RAM 裡。這裡就有個問題, pTOC 也是個變數,不是嘛?那 NK.EXE 如何知道 pTOC 在哪裡如果 NK.EXE 還沒設定好?答案就是,pTOC 是一個唯讀的變數,只有 ROMIMAGE 寫它當 image 被建立時。 pTOC 儲存的地方不是在 RAM 且在它的值被使用之前也不需要被 copy 。在 NK.EXE 裡,透過 pTOC ,負責 copy 所有的 "copy entries"到RAM 的 function 叫做 "KernelReloacate()"。它是一個簡單的程序,就是參照一個結構的表格然後將一塊 virtual memory copy 到另一個地方。一但這一些都完成了, NK.EXE 的變數就可以被讀寫,就像其它程式一樣。

在這個時候,我們有個就像其它程式一樣的運作中的程式。它能執行指令,可以呼叫 functions,也可以讀寫記憶體位址。沒有 threads, processes, 也沒有 operating system constructs,但所有的東西被放在已知的地方且也可以被存取來讓我們去做較高階系統的啟動程式。Virtual memory 允許巨大的彈性(什麼彈性?為什麼要有physical adddress 和 virtual address 之分?)。Windos CE 保留一些 virtual memory 的區域,來作為它在 OS kernel 裡私自使用。在 virtual memory 高位址區有保留有幾塊 4k 的pages,大概從 0xFFFE0000 往上開始。Kernel 映射一些 physcial memory 在這個區域來儲存它的 'global' 動態資料。有些則給特定結構的 MMU 的 memory mapping table 使用。另一些則保留給 Kernel-mode 和中斷堆疊。最重要的是,至少有一個 4k pages 是特別保留作為 'Kernel Data Page'。這個 page 包含大量的資料欄位,這些欄位是 kernel 的一個版本特有的。NK.EXE 設定位址並直接地初始這個 page 的內容。
NK.EXE 儲存了三個重要的值在這個結構裡:
  1. 備份 pTOC
  2. OEMAddressTable 位址。
  3. OEMInitGlobals() function 的位址
前兩點放在 Kernel Data Page 好讓任何知道這個 page 位址的程式都能知道有什麼東西在 OS image 裡並知道 virtual memory 的基本 layout。最後一點的資訊會被特別用到,當控制權轉到 KERNEL.DLL 時, NK.EXE 的內容還可以被使用。一般來說,這塊 virtual memory 保留區看起來像這樣:

此時 kernel data page 已經被初始化並且 virutal memory 也在動作中,接著我們就可以跳到 Microsoft KERNEL.DLL 的可執行的 entry point 裡(entry point 指的是程式碼的第一行?) 別忘了,我可以在 image 裡找到 KERNEL.DLL 透過使用 TOC,然後我們也可以掃出 module 的 entry point 。 雖然 NK.EXE 之前就知道它將會將 kernel data page 放到 virtual memory 的哪裡,但 KERNEL.DLL 卻不能自己認為 kernel data page 的位址在哪就在哪。因此,我們就需要把 kernel data page 的 virtual addres 傳給 KERNEL.DLL 的 entry point(怎麼傳?)。雖然 Microsoft 程式可以 callback NK.EXE function 的位址,但 NK.EXE 程式從不會有完整的控制權。
在 jump 之後,我們 現在就開始執行 Microsoft kernel code。 Microsoft kernel code 的 entry point 會被給予 Kernel Data Page 的位址,然後透過 TOC 的欄位,就可以知道任何有關 OS image 的資訊。Kernel 做了一些他自己的的設定,也設定一些它自己要用的重要資料欄位在 Kernel Data Page裡。

KERNEL.DLL 有個靜態的 functions 和 data 的 table,叫做 "NKGlobals" (簡單地被建立在 DLL裡,如同一個靜態資料結構一樣)。因為 ROMIMAGE 已將 KERNEL.DLL fixed up 使它執行在特定的 virtual address,所以當 KERNEL.DLL 程式開跑時 NKGlobals 的 function pointers 會是正確的。有一些在這個 structure 裡的 functions 像是 SetLastError() 和 NKwvsprintfW(),這些 rounine function 是充許NK.EXE 呼叫它們的。然而,有一件重要的事需要知道的就是,在這個時間點, NK.EXE 不知道這些 functions 在 KERNEL.DLL 的哪裡 - NK.EXE 仍然需要被告知這些 functions 和 data 是放在 KERNEL.DLL 裡面的哪裡。
KERNEL.DLL 將 "NKGlobals" 的位址傳回給 NK.EXE 透過一個 function call 到 OEMInitGlobals()裡,而 OEMInitGlobals() 的位址是留在 Kernel Data Page裡的。所以在基本上,function call 的圖看起來像這樣:

就上面顯示的, OEMInitGlobals() function 儲存了 NKGlobals 結構的指標 ,這個結構是屬於 KERNEL.DLL 的結構。在它儲存了這個指標之後, NK.EXE 可以用它來找到 KERNEL.DLL 裡充許被呼叫的 function 的位址。OEMInitGlobals (通過 function 傳回值) 也傳回一個指標到它自己的結構,叫做 "OEMGlobals"。這個結構對 kernel 來說是很重要的,尤其是當 kernel 要能夠存取整個 NK.EXE 裡平台特有的功能時。
KERNEL.DLL 被建構成可以跑在任何特定結構(X86, ARM, etc)的處理器。而NK.EXE 是特定系列(像 SXCALE 或 OMAP 處理器)的結構及支援這個結構的平台的抽象化 (abstraction)。OEMGlobals 結構包含了一些 function poniters 及 data 就像 NKGlobals。它的一些成員包含下列:
  • PFN_InitDebugSerial(), PFN_WriteDebugByte(), PFN_ReadDebugByte()
  • PFN_SetRealTime(), PFN_GetRealTime(), PFN_SetAlarmTime()
  • PFN_Ioctl()

這些 function 的指標都指到 OEM 關連的 functions 像是 OEMInitDebugSerial 及 OEMIoctl,這些 function 都存在 NK.EXE 裡。許多其它 functions 也會被列出來,好讓 KERNEL.DLL 可以做一些必需要做的事情,針對某種特定的平台。這些 functions 也會用很明顯的名字寫在 MSDN 裡。

一旦 OEMInitGlobals() 跑完,KERNEL.DLL 就會有所有它需要的東西來執行 architecture-generic 和 platform-specific 程序。它知道記憶體在哪以及知道記憶體如何虛擬地排列,還有每一個模組在記憶體的位址。NK.EXE 也有它可呼叫的 function 指標。基本上,這兩個程式碼模組已經執行了一個人工的'交握'了(透過執行很簡單的人工動態連結的方式達到)。
到目前為止,NK.EXE 和 KERNEL.DLL 執行及被執行,都沒有透過任何 processes 或 theads, 也沒有透過任何 kernel services running。為了要帶起系統其它的部份,KERNEL.DLL 還必需做下列三件事:
  1. Architecture-specific 設定
  2. Architecture-neutral 設定
  3. Platform-specific 設定 (特定 CPU 和 BSP 的初始化)
Architecture-specific 設定會先被完成,透過呼叫一個 KERNEL.DLL 的 function,叫做 <architecture>Setup。在 ARM 平台,它會叫做 ARMSetup()。在 X86 平台,它會叫做 X86Setup()。Architecture-specific 程式碼做了很多事,但它們都執行在單線程內容裡且沒有任何 processes 在跑。下列這些動作會被執行,但不只侷限這些:
  • 設定 hard required page table 及保留 VM 給 kernel page tables
  • 在 Page Tables 裡更新 cache 的資訊
  • 清除 Transition Lookaside Buffer (TLB)
  • 設定 architecture-specific 匯流排及元件 (相關的 chips,處理器等)
另一件事,architecture-specific 程式碼會做的就是設定 Interlocked API code,好讓 NK.EXE 知道它在哪,然後能夠呼叫它。這其實只是小小的一部份,但我會仔細的解譯它是因為它是 OS 很重要的一部份。
甚至在最基本的階段,Windows CE 也需要在不同的 threads of execution 中做協調 - 甚至一些跑在 Kernel 裡的,以及跑在 specific procees 領域外的。 有個機制很有效率的做這些事,就是 Interlocked API。這個 API 由一些有用的 functions 所組成的,其中最重要的一個 function 是 InterlockedCompareExchage()。這個 function 的目的是:
  1. 將 memory (M) 的值,寫到暫存器(R)裡
  2. 比較 (R) 和另一個有同樣值的暫存器 (R2)
  3. 如果 (R) 和 (R2) 不相等,則離開
  4. 將另一個暫存器 (R3)值,寫到 memory (M) 的位址
這四個步驟意謂著是要原子地執行的(什麼是原子執行?不能被中斷?),因為它們是從兩個不同 threads 間的協調準則而來的。那表示,(1) 到 (4) 指令間是不能被中斷的。唯一的方法保證不被中斷,就是確保 interrupt 是 disabled 的,特別是一些在現今處理器上,硬體直接就是無支援這麼做的地方,更要這麼做。在此拋出一個問題,由於 user-mode processes 沒有足夠的權利去 disable interrupts,並且這麼做也是很沒效率(每次兩個 treads 要互相協調動作時就要做一個 system call 去 disable interrupts)。
所以為了更有效率,在整個系統裡,有個獨一的地方可以執行 InterlockedCompareExchange() ,就是將上面四個程式碼放到 Kernel Data Page 的一個眾所皆知的地方。然後 NK.EXE 和 KERNEL.Dll (及任何有Kernel Data Page maped 的 process) 都可以呼叫這個程式碼,且指令都存在同一個地方。如此這個 API 就是個可重新開始跑的 API。這是什麼意思? 為什麼我們這麼做?
在作業系統裡,Thread switch會發生有下列三個理由:
  • 執行的 threads 有個特別地要求
  • thread 的 time-slice 到期 (由 timer interrupt event 來通知) 然後換另一個 thread 開始跑
  • 另一種中斷發生,這個中斷需要執行高優先權的 thread

第二部份的兩個 cases 幾乎是一樣的- 當 interrupt 發生時,最終就會造成 thread switch。一旦 interrupt 發生在(1) 到 (4) 之間並有可能把 thread 給 switch out 掉,那這四個步驟(我們需要將它原子化的步驟(又來了什麼是原子化))就不能讓一些 thread run 在 (2) 到 (3) 之間,舉例來說。為了要確保指令 (1) 到 (4) 能原子執行(atomic again!!)每一次都有個中斷 來簡單的檢查邊界,來看看是否 CPU 正在執行 (1) 到 (4) 之間(這是什麼樣的中斷?每一次是指每執行一個指令?)。如果中斷發生在當 CPU 執行在(1)之前(4)之後時,就將目前 thread 的指令指標 reset 到指令 (1),如此這個運作也就糾正回來了。而為了讓 interrupt 的程式碼能夠檢查是否 CPU 是在執行指令(1)到(4)之間,程式碼一定要放在唯一大家都知道的地方。這個地方就是在 Kernel Data Page 裡。
一旦 Interlocked API 程式碼 copy 到 Kernel Data Page,NK.EXE 就知道它在哪以及當 multiple threads 發生時 NK.EXE 也可以跟 KERNEL.DLL 互相協調透過 Interlocked API。
回到我們的主題,KERNEL.DLL 啟動的下一步就是 architectue-neutral 設定。Architectural-neutral 的其中一件事就是去看,是否 OS image (包含 KITL.DLL的 image) 允許跟 OS kernel 的debugging 溝通。KITL 代表的就是 "Kernel Independent Transport Layer"。這是一個基本的機制,透個這個機制將資料封包,特別對Windows CE 系統的封包,在 device of kernel 和桌上電腦的Platform Builder 之間傳遞。通常,一部份的 KITL 會被 implement 在 NK.EXE,這些 KITL code 就是單純地對傳輸及資料封包的傳輸的編碼而己。Board Support Package (BSP) 不需要知道任何有關 device 和 電腦之間傳遞的資料- 只要幫忙傳送和接受能夠正確就好了。KITL 封包的傳輸機制包含(但不只限制這些) RS2323 Serial,Ethernet,和 USB。KITL 完整的描述超過這篇 BLOG 文章的範圍。其它在 architecture-neutral 設定的動作包含:
  1. 初始化 Kernel Debug Output (透過呼叫 OEMInitDEbugSerial(),這個 function point 是在 OEMGlobals structer)
  2. 丟個message("Windows CE Kernel Version xxxx") 到 debug output
  3. 從變數選項選擇 kernel processor type

當 architecture-neutral 部份已經完成時,我們可以開始做 platform-specific 設定。這個程式碼是在 NK.EXE 因為它是 OEM 和 board 特有的。透過呼叫 OEMInit() 來初始化這個部份,它的 function pointer 是在 OEMGlobals 的結構裡。 OEMInit 做了 board-specific 初始化,並且也做了另一個重要的事 - 啟動 KITL。
如果 KITL 是被建立在 NK.EXE,那麼它就能從 NK.EXE 直接被存取。如果 KITL 是在 DLL 裡,那麼這個 DLL 在 architecture-neutral 設定的一開始就會被載入到 kernel。無論哪一個,OEMInit() function 都會呼叫 Kernel IO control 來宣告 KITL 應該要被啟動。基本上,無論 KITL.DLL 是否被找到與否,kernel 都知道要做什麼。
回到 OEMInit(),kernel 已經準備好開始讓 processes 和 thread 跑起來。它也同步它的cashe,然後進入 processor architecture's service mode (如果它還沒跑進去的話)。然後做所有的一次性初始化,在不用到現行的thread。這些動作包含:
  1. 列舉可用的記憶體(選擇性的呼叫 OEMEnumExtensionDRAM())
  2. 初始化 kernel 裡不可缺的部份(這個部份有使用 Interlocked API, 它的設定在上面有討論過)
  3. 初始化 heap 結構
  4. 初始化 process 和 thread tracking 結構
  5. 完成任何動作在 multi-threading 開啟之前

在所有的 single-threaded 初始化都完成之後,kernel 就準備好安排第一個 thread 。這個第一個 thread 叫做 "SystemStartupFunc()",它是在 KERNEL.DLL 裡。為了開始 thread,kernel 會表明沒有任何現行的 thread 會被交換,設定第一個 thread 如同唯一 可跑的 thread,然後呼叫進 thread scheduler 的程式碼。這個 scheduler 程式碼管所有可用的 threads 並選擇下一個 thread 執行。在 starup 的這個時間點,我們只有一個由手動設定的 thread 在跑,就是那個被 switch 的thread。
經由沖刷 system cache , SystemStartupFunc() 開始執行,然後做一些事情(使 "current" thread 可執行的事情)。這些動作包含:
  1. 初始化系統 loader
  2. 初始化 paging pool
  3. 初始化系統 logging
  4. 初始化系統 debugger

SystemStartupFunc() 會再呼叫一個 OEM function 在它完成初始化之前 - 透過在 OEMGlobals 的 function pointer 及參數 "OEM_HAL_POSTINIT",它會呼叫 OEMIoctl() 。這告訴 NK.EXE,所有的 system startup 已完成,並且我們準備 schedule threads 和 processes。
在呼叫OEMIoctl()之後,SystemStartupFunc() 初始化了 system message queue, 以及所有的 watchdogs,然後建立並開始 power manager、file system 的 threads。因此,作業系統剩餘的高階部份開始在這執行。SystemStartupFunc() 的最後一個動作是建立另一個 thread ,叫"RunAppsAtStartup()"。這個 function 會建立第一個 user processes。
此時,kernel、power manager、以及 file system 都執行起來了,然後那些在 system registry 有描述的 applications 也可以開始執行起來了。

這篇說明了 windows embedded CE 6.0 如何啟動的文章就到這結束。Windows CE 的內部很有趣的,同是它也是有很好的結構在裡面,藉由上面描述的 startup process 能夠讓你洞悉最重要的 system components。在未來我希望能發表其它文章有關 system registry, file system, device and power managers 的內部構造。



No comments: