UEFI实战SlimBootloader代码流程分析

Posted jiangwei0512

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UEFI实战SlimBootloader代码流程分析相关的知识,希望对你有一定的参考价值。

综述

在介绍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文件看出来:

  1. Transition to IA32 flat mode
  2. Locate BFV (Boot Firmware Volume) by checking every 4kb boundary
  3. Locate SEC image
  4. X64 VTF0 transitions to X64 mode
  5. 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 InitCall 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是为了将CAR(Cache As RAM)作为栈,这样就可以使用C函数了,这才有了之后的Jump to C Code (CAR Stack)。在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(),对应PreConfigInitPostConfigInitPreMemoryInitPostMemoryInitPreTempRamExitPostTempRamExit这些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之前,还会执行ReadyToBootEndOfFirmware的回调:

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的加载主要就是遍历启动设备、寻找启动介质,并最终启动内核。

以上是关于UEFI实战SlimBootloader代码流程分析的主要内容,如果未能解决你的问题,请参考以下文章

UEFI实战SlimBootloader集成UEFI Payload

UEFI实战SlimBootloader使用

UEFI实战SlimBootloader中的构建脚本BuildLoader.py

UEFI实战SlimBootloader定制化

UEFI实战SlimBootloader定制化

UEFI实战SlimBootloader中调用FSP