原創翻譯,轉載請注明出處。
arm64的異常模型由一組異常級別(EL0-EL3)組成。EL0,EL1有安全模式和非安全模式的區別。EL2是虛擬機管理級別並且只有非安全模式。EL3是最高優先級並且只存在安全模式中。
為了描述方便,下面將使用術語“boot loader”來簡化所有執行在cpu將控制權轉交給內核之前的軟件的稱呼。這里包含了安全監視器(secure monitor)和虛擬機管理器(hypervisor)的代碼,或者可能是少量用來准備一個最小的啟動環境的指令。
基本上,boot loader至少提供以下幾個功能:
- 安裝與初始化物理內存
- 安裝設備樹
- 解壓內核鏡像
- 啟動內核鏡像
1、安裝與初始化物理內存
boot loader需要初始化物理內存,內核將使用這些內存來存儲volatile類型的數據。這個是與機器有關的,可能使用了內部算法自動的定位並取得物理內存的大小,
或者可能是機器有關內存方面的特性,也可能是boot loader設計者知道的獲取內存某種方法。(囧)
2、安裝設備樹
dtb(device tree blob)必須位於8-BYTE對齊的位置並且不能超過2MB的大小。因為dtb會被映射到最大2MB的緩存塊上,它不能放在任何映射了特定屬性的2M區域內。
注意,在內核4.2以前,要求將DTB放在內核鏡像里以text_offset為起始位置的512M區域內。
3、解壓內核鏡像(這個是可選的)
arm64(aarch64)的內核當前並不提供自解壓功能,因此需要解壓在boot loader里完成(比如gzip格式)。如果boot loader不支持解壓,可以使用不壓縮的鏡像來啟動。
4、啟動內核鏡像
解壓后的內核鏡像包含64byte的頭,頭結構定義如下:
u32 code0; /* Executable code */ u32 code1; /* Executable code */ u64 text_offset; /* Image load offset, little endian */ u64 image_size; /* Effective Image size, little endian */ u64 flags; /* kernel flags, little endian */ u64 res2 = 0; /* reserved */ u64 res3 = 0; /* reserved */ u64 res4 = 0; /* reserved */ u32 magic = 0x644d5241; /* Magic number, little endian, "ARM\x64" */ u32 res5; /* reserved (used for PE COFF offset) */
頭結構說明:
(1)、在內核3.17版本以前,所有字段都是小端字節序,除非有特別說明。
(2)、code0/code1 是為了響應 stext 分支。
(3)、如果以EFI(可擴展固件接口 Extensible Firmware Interface)啟動,code0/code1一開始就會被跳過。res5是指PE頭和有EFI入口點(efi_stub_entry)的PE頭的偏移。當efi完成了它的工作,就會跳轉到 code0 的位置繼續正常的啟動流程。
(4)、在內核3.17版本以前,text_offset字段的字節序是不確定的。舉個栗子,在內核字節序里,image_size 為0,text_offset為 0x80000。如果 image_size 是一個非0值,必須注意了,這時image_size 是小端字節序。如果 image_size 為0,那么text_offset可以認為是 0x80000。
(5)、flags 字段(內核3.17版本引入的)是一個小端字節序的64bit字段,它的組成如下:
Bit 0: 內核字節序標識, 1是大端, 0是小端; Bit 1-2:內核頁的大小, 0 - 表示未說明,1 - 表示4K大小,2 - 16K,3 - 64K; Bit 3:內核物理布局, 0 - 基地址2MB對齊並且基地址應該離DRAM的基地址越可能的近,因為是線性映射,所以內存地址低於它的不能訪問。 1 - 基地址2MB對齊,可以位於物理內存的任何地方。 Bit 4-63:保留字段。
(6)如果 image_size 為0,在內核鏡像啟動結束之后,bootloader應提供盡量多的空閑內存給內核使用。這個空間的數量會隨着不同的特性變化,實際上是沒有明確的限制的。
內核鏡像位於任何可用的 text_offset 大小的字節數的內存基地址上,這個地址必須是2MB對齊的。2MB對齊的基地址與內核鏡像起始位置這之間的區域對內核是沒有特別的意義的,可以用作它用。
在內核鏡像起始的位置起,至少 image_size 大小的字節數必須是空閑的,以供內核使用。
注意:在內核4.6版本之前不能使用低於鏡像大小的物理偏移的內存,所以推薦鏡像放在離物理內存地址起始位置盡可能近的地方。
如果 initd/initramfs 在啟動的時候傳遞給了內核,它必須整個屬於1GB對齊的物理內存窗口到32GB大小之間,以全部覆蓋內核鏡像為好。
任何描述給內核的內存(包括低於鏡像起始地址的),如果沒有標記為保留的(dtb里的 /memreserve指定)將被內核認為是可以使用的。
在跳轉到內核之前,下面的條件必須滿足:
(1)禁用所有的具有DMA能力的設備,這樣內存就不會被偽造的網絡數據包或者硬盤數據污染。這將節省你大量的調試時間。
(2)主CPU的通用寄存器設置:
x0 = dtb在系統內存的物理地址 x1 = 0 (保留給以后使用) x2 = 0 (保留給以后使用) x3 = 0 (保留給以后使用)
(3)CPU模式
所有的中斷都必須在 PSTATE.DAIF (Debug,SError,IRQ,FIQ) 中設置掩碼位。CPU必須處於EL2(推薦模式,方便虛擬化擴展訪問)或者非安全模式的EL1模式中。
(4)Caches,MMUs
MMU必須關閉。
指令緩存可以開啟或關閉。
對應於內核鏡像的地址范圍應該清理成PoC(PoC不知道是啥)。要使能系統緩存或者其他一致性主緩存,要求緩存維護通過VA,而不是 set/way 操作。
系統緩存 (依賴體系結構的緩存維護通過VA操作的)必須被配置並且使能,而不依賴體系結構通過VA維護的系統緩存必須禁止。
(5)定時器
CNTFRQ 必須對定時器頻率是可編程的,並且CNTVOFF必須對在所有CPU上具有一致性的值是可編程的。如果進入內核時是在EL1模式,CNTHCTL_EL2 必須有EL1PCTEN (bit 0)設置可用。
(6)一致性
所有CPU通過內核啟動必須是相同一致的內核入口的一部分。這將要求“IMPLEMENTATION DEFINED”的初始化來使能每個CPU來接收維護操作。
(7)系統寄存器
所有可寫的系統寄存器在這內核鏡像將要進入的異常級別(EL)必須在一個更高的異常級別(EL)通過軟件初始化,來防止在一個未知的狀態執行。
在一個有GICv3的中斷控制器的系統可以使用v3模式:
1、如果是 EL3 : ICC_SRE_EL3.Enable (bit 3) 必須初始化為 0b1. ICC_SRE_EL3.SRE (bit 0) 必須初始化為 0b1. 2、內核是在 EL1 : ICC.SRE_EL2.Enable (bit 3) 必須初始化為 0b1 ICC_SRE_EL2.SRE (bit 0) 必須初始化為 0b1. 3、DT或者ACPI表必須在GICv3中斷控制器中。
上述的CPU模式,緩存,MMU,定時器,一致性,系統寄存器對應所有的CPU,所有CPU必須在相同異常級別進入內核。
bootloader在進入內核(每個cpu)都有如下規則:
(1)主CPU直接跳轉到內核鏡像的第一條指令。dtb傳給每個CPU必須包含“enable-method”屬性,這個屬性在下面會描述。
bootloader會生成這些設備樹的屬性並在內核入口之前插入到二進制執行文件中。
(2)帶有“spin-table”使能方法的CPU必須有一個“cpu-release-addr”的屬性節點。這個屬性標識符以64bit自然對齊並在內存中初始化為0。
這些CPU在內核之外的保留的內存區域(dtb里的 /memreserve/ 的指定區域)空轉,並輪詢“cpu-release-addr”地址,該地址也在保留區域內。
“wfe”指令可以用來插入減少這種busy-loop的開銷,並且主CPU會發出“sev”(嘛東西。)。 當讀取“cpu-release-addr”返回一個非0值,這個CPU必須跳轉到這個值的地址。
這個值就是一個簡單的64bit的小端的數值,所有這些cpu必須轉換成它自己的原生字節序之后才能跳轉過去。
(3)具有“psci”的使能方法的CPU應該停留在內核之外的保留內存區域。內核會發出“CPU_ON”的調用來將CPU帶入內核。
設備樹應該包含一個“psci”節點。可以參考Documentation/devicetree/bindings/arm/psci.txt。
(4)從CPU上的通用寄存器設置:
x0 = 0 (reserved for future use) x1 = 0 (reserved for future use) x2 = 0 (reserved for future use) x3 = 0 (reserved for future use)