综述
在介绍SBL代码目录结构体的时候已经提到过SBL启动的几个阶段(Stages),这里再次说明:
阶段 | 说明 |
---|---|
Stage 1A | 内存初始化之前的阶段。 |
Stage 1B | 主要就用来进行内存初始化。 |
Stage 2 | 内存初始化之后的操作,包括:CPU初始化、IO初始化、设备初始化等等。 |
Payload | 加载、验证和启动OS,或者固件升级。 |
以上的阶段是串行执行的,大致如下图所示:
进一步说明的话如下图所示:
下面按照各个阶段结合代码分析,对应使用的单板是QEMU虚拟板,由于内容比较多,这里不会所有都讲到,只说明重点部分。
Stage1A
首先将图中Stage1A阶段的各个重点步骤列出:
- Transition to Protected Mode
- Very Early Board Init
- Call FSP-T: TempRamInit
- Jump to C Code (CAR Stack)
- Load Stage 1B
该阶段的开头是Reset Vector
,关于它在Intel的开发者手册中有明确的说明:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-giZZXl4o-1634455629087)(slimbootloader.assets/ResetVector.png)]
这里的First Instruction Executed
指的就是Reset Vector
指向的机器码,对应到代码中,它其实是一段汇编代码的开始,是Intel CPU上电之后执行指令的起点。从fdf文件(BootloaderCorePkg\BootloaderCorePkg.fdf)中可以找到如下的语句:
#------------------------------------------------------------------------------
# STAGE1A FV
#------------------------------------------------------------------------------
[FV.STAGE1A]
BlockSize = $(FLASH_BLOCK_SIZE)
# 中间部分省略,Stage1A阶段最重要的就是以下的2个模块:
INF BootloaderCorePkg/Stage1A/Stage1A.inf
INF RuleOverride = RESET_VECTOR USE = $(ARCH) BootloaderCorePkg/Stage1A/Ia32/Vtf0/Bin/ResetVector.inf
ResetVector.inf就是包含Reset Vector
汇编代码的模块,其主要内容:
[Binaries.Ia32]
RAW|ResetVector.ia32.raw|*
[Binaries.X64]
RAW|ResetVector.x64.raw|*
这里的raw文件通过工具生成,该工具是位于BootloaderCorePkg\Stage1A\Ia32\Vtf0目录下的Build.py
,它在BuildLoader.py
中被调用:
# rebuild reset vector
vtf_dir = os.path.join('BootloaderCorePkg', 'Stage1A', 'Ia32', 'Vtf0')
x = subprocess.call([sys.executable, 'Build.py', self._arch.lower()], cwd=vtf_dir)
Build.py
的具体实现这里不关注,只需要知道它使用的是nasm
编译汇编语言得到,这些汇编语言都位于BootloaderCorePkg\Stage1A\Ia32\Vtf0\目录下,关于它们的作用,可以通过查看Readme.txt文件看出来:
- Transition to IA32 flat mode
- Locate BFV (Boot Firmware Volume) by checking every 4kb boundary
- Locate SEC image
- X64 VTF0 transitions to X64 mode
- Call SEC image entry point
不过这里的说明并不是那么准确,因为它默认是给UEFI用的,所以还有SEC这样的说法,对于SBL来说,可以认为Stage1A阶段包含了SEC阶段。从上面还可以看到Stage1A阶段中的第一步Transition to Protected Mode
已经在这里完成,接着跳转到SEC入口(在SBL中,这是所有阶段的C函数入口名称),对应的模块是:
INF BootloaderCorePkg/Stage1A/Stage1A.inf
其中包含的文件:
[Sources]
Stage1A.h
Stage1A.c
[Sources.IA32]
Ia32/SecEntry.nasm
[Sources.X64]
X64/SecEntry.nasm
这里的SecEntry.nasm包含了Very Early Board Init
和Call FSP-T: TempRamInit
的操作:
global ASM_PFX(_ModuleEntryPoint)
ASM_PFX(_ModuleEntryPoint):
movd mm0, eax
;
; Read time stamp
;
rdtsc
mov esi, eax
mov edi, edx
;
; Early board hooks
;
mov esp, EarlyBoardInitRet
jmp ASM_PFX(EarlyBoardInit)
_ModuleEntryPoint
指定了Stage1A模块的入口,它包含了EarlyBoardInit
例程(注意这里还没有函数的概念,因为栈还没有),该例程位于各个平台各个单板对应的Pacakge中,以QEMU为例,代码是Platform\QemuBoardPkg\Library\Stage1ABoardInitLib\Ia32\Stage1AEarlyBoardInit.nasm,不过因为QEMU是模拟器,所以并不需要做任务事情:
SECTION .text
global ASM_PFX(EarlyBoardInit)
ASM_PFX(EarlyBoardInit):
;
; This hook is called before FSP TempRamInit API call
; ESI, EDI need to be preserved
; ESP contains return address
;
jmp esp
可以看到这里没有实际代码。从EarlyBoardInit
返回之后就进入到了FSP的调用:
EarlyBoardInitRet:
mov esp, FspTempRamInitRet
jmp ASM_PFX(FspTempRamInit)
FspTempRamInit
例程并不是真正的FSP操作,它只是一个跳转,它的实现位于FsptApiLib这个库模块中,对应的文件是FspTempRamInit.nasm,具体实现如下:
global ASM_PFX(FspTempRamInit)
ASM_PFX(FspTempRamInit):
;
; This hook is called to initialize temporay RAM
; ESI, EDI need to be preserved
; ESP contains return address
; ECX, EDX return the temprary RAM start and end
;
;
; Get FSP-T base in EAX
;
mov ebp, esp
mov eax, dword [ASM_PFX(PcdGet32(PcdFSPTBase))]
;
; Find the fsp info header
; Jump to TempRamInit API
;
add eax, dword [eax + 094h + FSP_HEADER_TEMPRAMINIT_OFFSET]
mov esp, TempRamInitStack
jmp eax
TempRamInitDone:
mov esp, ebp
jmp esp
align 16
TempRamInitStack:
DD TempRamInitDone
DD ASM_PFX(TempRamInitParams)
可以看到只是指定了位置进行跳转,而对应位置正是FSP的入口。
关于FSP,这里也不作介绍,只需要知道SBL实现了一套FSP2.0规范的调用接口,这样的接口其实也不多,后面还会介绍到具体接口,但是不会有实现说明,实际上Intel就是想要只提供二进制,而让BIOS开发者可以不用关注CPU、SoC、PCH这些CPU硬件的实现。
TempRamInitStack
是为了传递返回地址和参数给FSP API。
在SecEntry.nasm的最后,就是一个跳转:
CheckStatusDone:
; Setup HOB
push rbx ; Status
push rdi ; TimeStamp[0] [63:0]
shl rdx, 32 ; Move CarTop to high 32bit
add rdx, rcx ; Add back CarBase
push rdx
mov rcx, rsp ; Argument 1
sub rsp, 32 ; 32 bytes shadow store for x64
and esp, 0xfffffff0 ; Align stack to 16 bytes
call ASM_PFX(SecStartup) ; Jump to C code
jmp $
SecStartup()
是一个C函数了,位于BootloaderCorePkg\Stage1A\Stage1A.c,之后就是各种函数的调用,最终进入到Stage1B,这里简单说明调用关系:
SecStartup(); -->
SecStartup2(); -->
ContinueFunc(); -->
PrepareStage1B(); -->
StageEntry();
其中的SecStartup2()
包含BoardInit()
, 该函数又会hook到各个平台各个单板上的自定义初始化函数,它接受一个参数PostTempRamInit
,它是一个枚举类型,在后续的数次BoardInit()
调用中会使用到该枚举类型的不同值,它们在不同的阶段使用,对应的值在BootloaderCommonPkg\Include\Service\PlatformService.h中定义:
typedef enum {
PreTempRamInit = 0x10, // 未找到使用
PostTempRamInit = 0x20, // Stage1A
PreConfigInit = 0x30, // Stage1B
PostConfigInit = 0x40, // Stage1B
PreMemoryInit = 0x50, // Stage1B
PostMemoryInit = 0x60, // Stage1B
PreTempRamExit = 0x70, // Stage1B
PostTempRamExit = 0x80, // Stage1B
PreSiliconInit = 0x90, // Stage2
PostSiliconInit = 0xA0, // Stage2
PrePciEnumeration = 0xB0, // Stage2
PostPciEnumeration = 0xC0, // Stage2
PrePayloadLoading = 0xD0, // Stage2
PostPayloadLoading = 0xE0, // Stage2
EndOfStages = 0xF0, // Stage2
ReadyToBoot = 0xF8, // Stage2
EndOfFirmware = 0xFF // Stage2
} BOARD_INIT_PHASE;
到这里Stage1A阶段结束。
Stage1B
首先将图中Stage1B阶段的各个重点步骤列出:
- Setup Global Data Structure
- Load/Verify CFG Data
- Pre-Memory Board Init
- Call FSP-M: Memory Init
- Shadow Stage 1B
- Switch to RAM Stack
- Call FSP-M: TempRamExit
- Load/Verify Stage 2
Stage1B对应的模块可以从fdf文件中看到:
[FV.STAGE1B]
# 中间部分省略
INF BootloaderCorePkg/Stage1B/Stage1B.inf
FILE FREEFORM = 016E6CD0-4834-4C7E-BCFE-41DFB88A6A6D {
SECTION RAW = $(FV_DIR)/CfgDataInt.bin
}
主要的模块就一个Stage1B.inf,对应的代码主要是BootloaderCorePkg\Stage1B\Stage1B.c,它的入口也是SecStartup()
,事实上SBL中的几个阶段的C函数入口都是SecStartup()
,它是通过ModuleEntryLib
这个库来包装的,对应的代码也是汇编,名称也是SecEntry.nasm,正是在这里指定了C函数入口名称:
SECTION .text
extern ASM_PFX(SecStartup)
global ASM_PFX(_ModuleEntryPoint)
ASM_PFX(_ModuleEntryPoint):
jmp ASM_PFX(SecStartup) ; Jump to C code
这也是为什么在Stage1B.inf文件中要包含这个库的原因:
[LibraryClasses]
ModuleEntryLib
该阶段的C函数调用关系:
SecStartup();
SecStartup2();
ContinueFunc();
SwitchStack();
SecStartup2()
和ContinueFunc()
包含总计6个BoardInit()
,对应PreConfigInit
、PostConfigInit
、PreMemoryInit
、PostMemoryInit
、PreTempRamExit
、PostTempRamExit
这些Hook,它们在Platform\QemuBoardPkg\Library\Stage1BBoardInitLib\Stage1BBoardInitLib.c中实现,对应一个swtich
语句执行不同的代码路径。
Setup Global Data Structure
对应到的是LOADER_GLOBAL_DATA
这个结构体:
typedef struct {
UINT32 Signature;
UINT16 PlatformId;
UINT16 PlatformBomId;
UINT8 BootMode;
UINT8 LoaderStage;
UINT8 CurrentBootPartition;
UINT8 ResetReason;
UINT32 StackTop;
UINT32 MemPoolEnd;
UINT32 MemPoolStart;
UINT32 MemPoolCurrTop;
UINT32 MemPoolCurrBottom;
UINT32 MemUsableTop;
UINT32 PayloadId;
UINT32 DebugPrintErrorLevel;
UINT8 DebugPortIdx;
UINT8 Padding[3];
VOID *FspHobList;
VOID *LdrHobList;
VOID *FlashMapPtr;
VOID *VerInfoPtr;
VOID *HashStorePtr;
VOID *LibDataPtr;
VOID *ServicePtr;
VOID *PcdDataPtr;
VOID *PlatDataPtr;
VOID *CfgDataPtr;
VOID *LogBufPtr;
VOID *DeviceTable;
VOID *ContainerList;
VOID *S3DataPtr;
VOID *DebugDataPtr;
VOID *DmaBufferPtr;
UINT8 PlatformName[PLATFORM_NAME_SIZE];
UINT32 LdrFeatures;
BL_PERF_DATA PerfData;
UINT32 CarBase;
UINT32 CarSize;
} LOADER_GLOBAL_DATA;
它的位置通过GetLoaderGlobalDataPointer()
函数获取,在Stage1B的开头构建,后续的操作都会使用到这个全局的结构体。
Load/Verify CFG Data
对应到的函数是CreateConfigDatabase()
,它操作的最主要的是在fdf文件中出现的的CfgDataInt.bin,它的地址通过PCD指定,名称是PcdCfgDataIntBase
,在CreateConfigDatabase()
函数中有相关的操作:
// Add internal CFGDATA at the end
IntCfgAddPtr = (UINT8 *)(UINTN)PCD_GET32_WITH_ADJUST (PcdCfgDataIntBase);
if (IntCfgAddPtr != NULL) {
Status = AddConfigData (IntCfgAddPtr);
if (!EFI_ERROR (Status)) {
// Update the built-in CFGDATA offset
CfgBlob = (CDATA_BLOB *) IntCfgAddPtr;
CfgDataLength = CfgBlob->UsedLength - CfgBlob->HeaderLength;
CfgBlob = (CDATA_BLOB *) LdrGlobal->CfgDataPtr;
CfgBlob->ExtraInfo.InternalDataOffset = (UINT16) ((CfgBlob->UsedLength - CfgDataLength) >> 2);
} else {
DEBUG ((DEBUG_INFO, "Append Built-In CFG Data ... %r\n", Status));
}
} else {
DEBUG ((DEBUG_INFO, "No built-in CFG Data found !\n"));
}
CfgDataInt.bin也是通过BuildUtility.py
生成的。
Stage1B也有关于FSP的调用,主要是两个部分,一个是内存初始化,另一个是退出Cache。前者对应的函数是:
Status = CallFspMemoryInit (PCD_GET32_WITH_ADJUST (PcdFSPMBase), &HobList);
后者对应的函数是:
Status = CallFspTempRamExit (PCD_GET32_WITH_ADJUST (PcdFSPMBase), NULL);
内存的初始化是整个代码中最重要的一部分,它涉及到了数据段和代码段的转移,整个过程比较复杂,所以在Stage1B中可以看到很多不同于普通函数调用的操作,再加上FSP的调用,整个过程会比较复杂,不过好的一点是这些代码基本上不需要普通BIOS开发人员去修改。
Stage1B阶段的最后就是找到Stage2入口并跳转,主要的代码是:
Dst = PrepareStage2 (Stage1bParam);
if (Dst == 0) {
CpuHalt ("Failed to load Stage2!");
}
// Configure stack
StackTop = ALIGN_DOWN (LdrGlobal->StackTop - sizeof (STAGE2_PARAM), 0x10);
// Build Stage2 Param
Stage2Param = (STAGE2_PARAM *)(UINTN)StackTop;
SetMem (Stage2Param, sizeof (STAGE2_PARAM), 0);
Stage2Param->Stage2ExeBase = Dst;
Stage2Param->PayloadBase = Stage1bParam->PayloadBase;
SwitchStack ((SWITCH_STACK_ENTRY_POINT)GET_STAGE_MODULE_ENTRY (Dst), Stage2Param, NULL, (VOID *)(UINTN)StackTop);
Stage2
首先将图中Stage2阶段的各个重点步骤列出:
- Unshadow Stage 1B
- Pre-SiliconInit Board Init
- Save MRC Fast-Boot Parameters
- Call FSP-S: SiliconInit
- Post-SiliconInit Board Init
- Multiprocessor Initialization
- Pre PCI-Enumeration Board Init
- PCI Enumeration
- Setup ACPI Tables
- Post PCI-Enumeration Board Init
- Cass FSP-S: NotifyPhase (AfterPciEnumeration)
- Load/Verify Payload
- Build Payload HOBs
- Jump to Payload
Stage2对应的模块可以从fdf文件中看到:
[FV.STAGE2]
# 中间部分省略
INF BootloaderCorePkg/Stage2/Stage2.inf
!if $(HAVE_ACPI_TABLE)
INF RuleOverride = ACPITABLE $(ACPI_TABLE_INF_FILE)
!endif
!if $(HAVE_VBT_BIN)
FILE FREEFORM = E08CA6D5-8D02-43ae-ABB1-952CC787C933 {
SECTION RAW = $(FV_DIR)/Vbt.bin
}
!endif
!if $(ENABLE_SPLASH)
FILE FREEFORM = 5E2D3BE9-AD72-4D1D-AAD5-6B08AF921590 {
SECTION RAW = $(LOGO_FILE)
}
!endif
主要的模块当然是Stage2.inf,其它还包含ACPI模块、显卡配置和LOGO数据。
前面已经说过Stage2.inf的C函数入口也是SecStartup()
,这里简单说明函数调用路径及对应到的步骤:
UnmapStage (); // Unshadow Stage 1B
BoardInit (PreSiliconInit); // Pre-SiliconInit Board Init
SaveNvsData (NvsData, MrcDataLen); // Save MRC Fast-Boot Parameters
CallFspSiliconInit (); // Call FSP-S: SiliconInit
BoardInit (PostSiliconInit); // Post-SiliconInit Board Init
MpInit (EnumMpInitWakeup); // Multiprocessor Initialization
BoardInit (PrePciEnumeration); // Pre PCI-Enumeration Board Init
PciEnumeration (MemPool); // PCI Enumeration
BoardInit (PostPciEnumeration); // Post PCI-Enumeration Board Init
BoardNotifyPhase (PostPciEnumeration); // Cass FSP-S: NotifyPhase (AfterPciEnumeration)
PlatformUpdateAcpiGnvs ((VOID *)(UINTN)AcpiGnvs); // Setup ACPI Tables
AcpiInit (&AcpiBase); // Setup ACPI Tables
NormalBootPath (Stage2Param); // Load/Verify Payload, Build Payload HOBs, Jump to Payload
图中的步骤都能够在Stage2的函数中找到,最后的NormalBootPath()
实际上还有另外的一个分支是S3ResumePath()
,即睡眠唤醒。此外还有显示LOGO,SMBIOS初始化等,也可以找到对应的代码:
DisplaySplash ();
SmbiosInit ();
Stage2中其它比较重要的函数是BoardNotifyPhase()
,它会在不同的位置调用,该位置通过传入的参数决定,而这个参数跟BoardInit()
用的是同一类型的枚举。
Payload
Payload的C函数入口还是SecStartup()
,它通过PayloadEntryLib库实现。在SecStartup()
中会获取HOB、PCD等数据,然后跳转到PayloadMain()
。
PayloadMain()
可以认为是真正的Payload应用入口,它可以根据具体的要求做不同的实现,比如现在的代码中就有若干个:
PayloadPkg\FirmwareUpdate\FirmwareUpdate.inf
PayloadPkg\HelloWorld\HelloWorld.inf
PayloadPkg\OsLoader\OsLoader.inf
其中的OsLoader出现在了图中,它才是用来加载OS的Payload应用。在启动OS之前,还会执行ReadyToBoot
和EndOfFirmware
的回调:
if ((PlatformService != NULL) && (PlatformService->NotifyPhase != NULL)) {
PlatformService->NotifyPhase (ReadyToBoot);
PlatformService->NotifyPhase (EndOfFirmware);
}
不过这个过程也可以在Stage2中完成,需要根据实际情况决定,在Stage2中有说明:
PayloadId = GetPayloadId ();
if (PayloadId == 0) {
// For built-in payload including OsLoader and FirmwareUpdate, it will handle
// notification through SBL platform services, so do not call notifications
// here.
CallBoardNotify = FALSE;
} else if ((PayloadId == UEFI_PAYLOAD_ID_SIGNATURE) && (UefiSig != 0)) {
// Current open sourced UEFI payload does not call any FSP notifications,
// but some customized UEFI payload will. The 1st DWORD in UEFI payload image
// will be used to indicate if it will handle FSP notifications.
CallBoardNotify = FALSE;
} else {
CallBoardNotify = TRUE;
}
OS的加载主要就是遍历启动设备、寻找启动介质,并最终启动内核。