ARM64的啓動過程之(五):UEFI

原文地址: http://www.wowotech.net/linux_kenrel/UEFI.html

 

一、前言

在準備大刀闊斧進入start_kernel之際,我又重新review了一下head.S文件,看看是否有一些遺漏的知識點,很不幸,看到了CONFIG_EFI這個配置項。當然,在一年前閱讀kernel代碼的時候就瞭解過相關的內容,但是,做爲一個嵌入式工程師總是或多或少對其有些排斥,因此習慣性的忽略掉CONFIG_EFI相關的代碼,逃避總不是辦法,在本文中,我們一起來探討ARM64平臺上UEFI相關的內容。

二、背景介紹

1、UEFI是什麼鬼?

在個人電腦剛興趣的時代,能夠進入BIOS(Basic Input/Output System)解決一些計算機的問題絕對是高手中的高手(當年我就是這麼騙到老婆的)。所謂BIOS實際上就是IBM PC兼容機(多麼古老的一個詞彙啊)主板上的固件(firmware),這些固件可以在系統啓動過程中初始化硬件,self test,加載bootloader或者OS kernel,並且能爲OS提供一些基礎的服務。由於各種存在的問題,後來,Intel提出來EFI(Extensible Firmware Interface)來取代BIOS interface。2005年,Intel終止了EFI規範的開發,替代它的是Unified EFI Forum負責的UEFI(Unified Extensible Firmware Interface)specification。UEFI在系統中的位置如下(圖片來自wiki):

 

隨着PC和服務器的飛速發展,軟件和硬件廠商都不斷的研發各種新的產品來應對客戶的需求,在整合成系統的時候,有大量的協調的工作需要做,並且是越來越複雜。爲了加快整合,降低設計複雜度,需要一個統一的接口標準,也就是傳說中的UEFI了。有了UEFI,OS(軟件廠商陣營)和固件(硬件廠商陣營)就有了接口規格,這樣,大家可以各自進行開發,只要符合UEFI規格就OK了。如果硬件廠商有了創新性的硬件特性,如果不需要修改UEFI接口,那麼系統還是可以無縫的銜接,如果需要修改接口,那麼提前修改接口規格,讓參與整個系統構建的廠商可以同步前進。同樣的,從軟件角度看,如果創新性的軟件算法需要HW的支持,那麼可以通過UEFI這樣的接口和硬件廠商陣營進行交互,大大加快了將整個系統交付給客戶的時間。

2、UEFI關ARM什麼事?

如果ARM僅僅是將目光放在移動(嵌入式)市場,那麼UEFI當然不關ARM什麼事情。在嵌入式ARM平臺上,ROM code + bootloader(例如Uboot)+ linux kernel這樣的組合可以很好的工作。但是,在推出ARMv8以及64 bit架構的的處理器之後,ARM的野心已經不滿足在移動市場上稱王了。不過嵌入式平臺和server或者PC類的平臺是有區別的:嵌入式平臺往往是高度定製化的平臺,各個硬件模塊都是不可分割的。如果你購買了一個手機,如果你覺得LCD不滿意,是不可能單獨去市場購買一個LCD屏更換的。而server(PC)類產品則不然,各個模塊是可以更換的。例如:可以自由的去購買一個硬盤或者顯卡進行更換。

在移動平臺上,firmware(ROM code)怎麼做是自己的事情,只要在應用層面提供一致性的接口就OK了,反正硬件以及OS不會更換。來到服務器平臺,ARM必須和她的合作伙伴(SOC,外圍硬件,OS廠商等等)一起面對這樣的問題:

(1)硬件平臺(firmware)和OS之間的接口如何定義?

(2)如何向OS傳遞硬件信息?

爲了讓各個廠商能夠協同工作,儘快將ARM服務器推向市場,選擇一個標準讓大家follow是一個不錯的主意。我們以OS提供商爲例描述選擇標準的好處。如果定義了硬件平臺和OS之間的標準,OS提供商可以爲ARMv8 server發佈一個image而不會因爲任何一點硬件平臺的修改就得發佈一個新的OS。因此,ARMv8 server選擇UEFI是很自然的事情了。

3、UEFI如何定義系統的啓動過程?

相信大家對傳統的嵌入式ARM平臺的啓動過程都是有所瞭解的,系統reset後,各個ARM SOC的從ROM代碼開始執行(一般ARM reset之後,PC=0,而ROM缺省地址就是0)。根據SOC廠商約定的規則,ROM code會從外部設備(串口、網絡、NAND flash、USB磁盤設備或者其他磁盤設備)加載linux bootloader,bootloader會收集硬件信息,之後加載linux kernel。在UEFI規範中定義了BOOT manager,它會根據保存在NVRAM參數來決定如何load EFI Application(可能是bootloader或者其他的image file)。EFI Application的格式必須符合PE(Portable Executable )格式。PE是一種二進制可執行文件的格式(在linux世界中,我們多半熟悉的是ELF格式),由微軟開發,廣泛應用在Windows平臺上。

在ARMv8平臺上,firmware中的boot manager可以加載支持UEFI的傳統的bootloader(例如uboot),然後由uboot加載kernel,這樣,kernel其實不必關心什麼UEFI。當然這樣有些不直觀,本來OS kernel關心的那些firmeare提供的各種信息都是由bootloader進行轉接,嚴重影響了系統整合的效率(bootloader和kernel是由不同的團隊開發),因此,linux kernel image自身也可以包裝成一個EFI image,由boot manager直接加載,完成啓動過程。

4、PE格式介紹

下面的圖片是一個PE文件格式的示意圖:

pe-file1

PE文件主要由兩部分組成,一部分是爲了兼容MS-DOS操作系統而包裝的外殼(灰色block),主要由64B的MZ header和MS-DOS stub代碼區組成。在遙遠的MSDOS時代,其可執行文件就需要這樣的一個header,MSDOS的program loader就會根據這個header加載程序運行。在Windows時代,微軟提出了PE這種格式文件,它主要是運行在windows系列的操作系統中,但是,還需要考慮MSDSO的兼容性(也就是說當MSDOS執行PE格式的文件也能夠提供足夠的信息讓用戶知道如何處理)。MS-DOS stub block是一段stub code,這段區域的主要作用是:當PE格式的image在MS-DOS下加載運行的時候,程序會執行這個區域的代碼(PE的代碼都是for windows的,不可能在DOS下實際執行,因此,只能執行這些stub程序),當然運行的結果僅僅是打印“This program cannot be run in DOS mode”。

另外一個區域就是實際的PE格式的文件了。主要包括PE header(綠色block)、各種Section header(藍色block,用於描述各個section)和各個section的實際的Data。各個域的具體含義我們會結合具體的代碼在下一章描述。

三、代碼分析:

1、MZ header。相關代碼如下所示:

#ifdef CONFIG_EFI 
efi_head: 
    add    x13, x18, #0x16 --------------------------(2) 
    b    stext 
#else 
    b    stext--------------------------------(1) 
    .long    0  
#endif 
    .quad    _kernel_offset_le-------------------------(3) 
    .quad    _kernel_size_le  
    .quad    _kernel_flags_le 
    .quad    0                // reserved 
    .quad    0                // reserved 
    .quad    0                // reserved 
    .byte    0x41------------Magic number, "ARM\x64" 
    .byte    0x52 
    .byte    0x4d 
    .byte    0x64

#ifdef CONFIG_EFI 
    .long    pe_header - efi_head-----------------------(4) 
#else 
    .word    0                // reserved 
#endif

這裏定義了64字節的kernel image header,應對兩種場景:一種是從普通的linux bootloader加載內核,另外一種是從UEFI firmware直接加載kernel(定義了CONFIG_EFI ),在這種場景下,這64B的內容被解釋爲MZ header。

(1)大部分的kernel image header都是相同的,除了第一個8-Byte和最後的4-Byte。沒有定義CONFIG_EFI 是大家都比較熟悉的場景,當bootloader完成kernel image的從外設到RAM的搬移之後會執行kernel image的第一條指令。因此,這裏是一條跳轉到stext的指令。

(2)如果想把自己僞裝成一個UEFI image,kernel需要符合PE格式,下面是一個簡化版本的PE格式的示意圖(僅僅包括部分格式,主要用來說明兼容MS-DOS 相關部分的內容):

 

上圖中的灰色區域就是64-Byte的MZ header(對應kernel image header的內容),當然,對於linux kernel而言,它只是僞裝成PE格式而已,只要能夠提供足夠的信息給UEFI firmware的boot manager就OK了。PE格式的文件除了包括一個MZ header,還包括一段MS-DOS stub(上圖中的黃色區域),當然,對於linux kernel image,我們沒有提供這部分的內容。這裏“add    x13, x18, #0x16”這條指令沒有任何實際的意義,這條指令的opcode實際上就是MZ signature,用來標識這是一個DOS MZ executable的image。

(3)對於UEFI firmware而言,MS-DOS header大部分的區域都是沒有什麼用處的,因此正好可以用來提供信息,以便讓linux的bootloader可以知道如何加載kernel(非UEFI加載的情況)。_kernel_offset_le標識加載kernel的位置,如果等於0,表示加載到RAM的0地址的位置上。_kernel_size_le表示需要加載的kernel image的長度,_kernel_flags_le是表示kernel的一些屬性,目前僅僅使用了bit 0,表示kernel的endianess。

(4)在UEFI firmware加載kernel的情況下,需要找到PE header以及各個section的定義了,以便boot manager完成加載kernel image的任務。在MS-DOS header中(offset是0x3c)有四個字節指向了PE header,通過它可以找到如何加載內核的各種信息。這個過程是這樣的:UEFI firmware的boot manager如果發現了MZ header,那麼就認爲這是一個符合標準的EFI image,並在0x3c處獲取PE header的位置,並繼續解析其內容以便加載kernel image。

2、PE header相關代碼

PE header包括三部分的內容:PE signature、COFF(Common Object File Format)file header和optional header。PE signature和COFF file header的代碼如下:

pe_header: 
    .ascii    "PE" ----------------PE header signanature 
    .short     0 
coff_header: 
    .short    0xaa64----------表示machine type是AArch64 
    .short    2------------該PE文件有多少個section 
    .long    0------------該文件的創建時間 
    .long    0------------符號表信息 
    .long    1------------符號表中的符號的數目 
    .short    section_table - optional_header --------optional header的長度 
    .short    0x206---------------Characteristics,具體的含義請查看PE規格書

上節我們說過,通過MZ header可以找到PE header,所謂PE header的開始位置實際上就是一個“PE\0\0”的signature,隨後緊接着就是COFF file header,COFF file header具體的定義如下(該表格來自PE specification):

Offset

Size

Field

Description

0

2

Machine

The number that identifies the type of target machine

2

2

NumberOfSections

The number of sections. This indicates the size of the section table, which immediately follows the headers.

4

4

TimeDateStamp

The low 32 bits of the number of seconds since 00:00 January 1, 1970 (a C run-time time_t value), that indicates when the file was created.

8

4

PointerToSymbolTable

The file offset of the COFF symbol table, or zero if no COFF symbol table is present. This value should be zero for an image because COFF debugging information is deprecated.

12

4

NumberOfSymbols

The number of entries in the symbol table. This data can be used to locate the string table, which immediately follows the symbol table. This value should be zero for an image because COFF debugging information is deprecated.

16

2

SizeOfOptionalHeader

The size of the optional header, which is required for executable files but not for object files. This value should be zero for an object file. For a description of the header format, see section 3.4, “Optional Header (Image Only).”

18

2

Characteristics

The flags that indicate the attributes of the file

NumberOfSections定義了PE文件中的section的數目,對於linux kernel image的PE文件,包括了兩個section,一個是.reloc section(這是EFI application loader需要的,我們這裏只是提供了一個dummy版本的.reloc section),另外一個是.text section(整個kernel image)。

通過COFF file header中的SizeOfOptionalHeader域,UEFI firmware可以知道optional header的size。之所以是“optional”主要是因爲這些header內容不一定會存在。例如:對於object文件,這些header不存在。當然,我們是UEFI image file(可執行文件),因此這些optional header是必須提供的。optional_header的最開始的域是optional header magic number,用來確定該PE文件是PE32還是PE32+格式的。根據UEFI規範,UEFI application file應該是PE32+格式的。PE32+格式的optional header格式如下:

Offset

Size

Header part

Description

0

28/24

Standard fields

Fields that are defined for all implementations of COFF, including UNIX.

28/24

68/88

Windows-specific fields

Additional fields to support specific features of Windows (for example, subsystems).

96/112

Variable

Data directories

Address/size pairs for special tables that are found in the image file and are used by the operating system (for example, the import table and the export table).

Standard fields包括瞭如何加載以及如何運行的信息。相關的代碼如下:

optional_header: 
    .short    0x20b                // PE32+ format 
    .byte    0x02                // MajorLinkerVersion 
    .byte    0x14                // MinorLinkerVersion 
    .long    _end - stext            // SizeOfCode 
    .long    0                // SizeOfInitializedData 
    .long    0                // SizeOfUninitializedData 
    .long    efi_stub_entry - efi_head    // AddressOfEntryPoint 
    .long    stext_offset            // BaseOfCode

比較重要的信息包括:代碼段在image file中的偏移(BaseOfCode),正文段的大小(SizeOfCode),data段的大小(SizeOfInitializedData),bss段的大小(SizeOfUninitializedData),加載到memory後入口函數(AddressOfEntryPoint,對於linux kernel而言,入口函數是efi_stub_entry)。

Windows-specific fields和Data directories主要被Windows操作系統的linker和loader使用的,這裏就不詳述了。

3、Section table和section Data

大家有興趣可以自己查閱PE規格,我這裏就偷懶啦,^_^。

 

四、參考文獻:

1、https://lwn.net/Articles/584123/

2、http://www.linaro.org/blog/when-will-uefi-and-acpi-be-ready-on-arm/

3、https://lwn.net/Articles/574439/

4、PE規格書

5、UEFI規格書

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章