UEFI BIOS —— SEC阶段分析

UEFI BIOS —— SEC阶段分析_第1张图片

SEC(Security Phase)- 安全阶段

一、SEC阶段主要功能

SEC阶段是平台初始话的第一个阶段,计算机系统加电后首先进入这个阶段。

SEC阶段的功能:UEFI系统开机或重启后首先进入SEC阶段,SEC阶段系统执行以下四种任务:

  1. 接收并处理系统启动和重启信号,系统加电信号、系统重启信号、系统运行过程中的异常信号。
  2. 初始化临时存储区域:系统运行在SEC阶段时,仅CPU和CPU内部资源被初始化,而各种外部设备和内存都没有被初始化。因此系统需要一部分临时内存用于代码和数据的存储,一般称为临时RAM,临时RAM只能位于CPU内部(CPU和CPU内部的资源最先被初始化)。最常用的临时RAM是Cache,通过将Cache设置为no-eviction模式(noeviction:不删除策略,不淘汰,如果内存已满,添加数据是报错的),来把其当成内存使用(此时读取命中则返回Cache中的数据,读取缺失并不会向主存发出缺失事件;写命中时写入Cache,写缺失时也不会向主存发出缺失事件),这种技术称为CAR(Cache As RAM)。
  3. SEC阶段是可信系统的根:作为系统启动的第一部分,只有SEC能被系统信任,以后的各个阶段才有被信任的基础。因此,大部分情况下SEC再转交控制权给PEI前可以验证PEI是否可信。
  4. 传递系统参数给下一阶段:SEC阶段的一切工作都是为PEI阶段做准备的,最重要把系统的控制权转交给PEI,并将SEC阶段的运行信息汇报给PEI。SEC通过将以下信息作为参数传递给PEI的入口程序来向PEI汇报信息:
    • 系统当前状态,PEI根据状态值判断系统当前的健康情况。
    • 可启动固件(Boot Firmware Volume)的地址和大小,PEI据此判断可用硬件。
    • 临时RAM区域的地址和大小。
    • 栈的地址和大小。

二、SEC阶段执行流程

根据临时RAM是否初始化为界限,SEC阶段分为两部分:临时RAM初始化前称为Reset Vector阶段;临时RAM初始化后调用SEC入口函数从而进入SEC功能区。我们简单理下从上电到SEC阶段的大致流程:
在这里插入图片描述
第一条指令所在位置被称为 Reset Vector,之后进入Reset Vector阶段。

SEC代码流程图如下
UEFI BIOS —— SEC阶段分析_第2张图片
其中BP启动AP参考

2.1 Reset Vector

Reset Vector的执行流程如下:

  1. 进入固件入口。
  2. 从实模式转换到32位平坦模式(包含模式)。
  3. 定位固件中的BFV(Boot Firmware Volume)。
  4. 定位BFV中的SEC映像。
  5. 如果是64位系统,则从32位模式转换至64位模式。
  6. 调用SEC入口函数。
resetVector

追踪下代码执行流程,复位之后第一条指令位置对应:
UefiCpuPkg\ResetVector\Vtf0\Ia16\ResetVectorVtf0.asmresetVector位置

BITS    16 ;该伪指令说明程序是以16位的方式运行

ALIGN   16 ;按照 16 个字节的倍数对齐下一个符号,空隙默认用0 来填充

resetVector:
;
; Reset Vector
;
; This is where the processor will begin execution
;
    nop
    nop
    jmp     EarlyBspInitReal16

通过jmp跳转到 UefiCpuPkg\ResetVector\Vtf0\Ia16\Init16.asmEarlyBspInitReal16位置

EarlyBspInitReal16
; DI(destination index)是目的变址寄存器,用做隐含的目的串地址,默认在ES中;ES叫做额外的段寄存器. 它通常跟DI一起用来做指针使用. 
; BP是基指针, 通常BP用来保存使用局部变量的地址.
EarlyBspInitReal16:
    mov     di, 'BP'
    jmp     short Main16

可以看出,最开始指令是两个啥也不干的NOP和一个短跳转,那么:
1. 为什么开局是CPU不做操作(有时候作为占位符和微调timing用)的NOP,而不是直接有用的JMP开局?

关于为什么是NOP在IA32手册有说明
UEFI BIOS —— SEC阶段分析_第3张图片
简单来说就是内核可以顺利执行NOP的变成BSP;而其他的变成AP,被放在wait-for-SIPI的状态,等待BSP调度。

2. 为啥一个NOP不够,而要两个?

第二个NOP是占位符,为了让随后的JMP可以16位地址对齐,这样性能高一些。

3. JMP后面的占位符是谁Fixed UP的?

这里实际是要跳转到UEFI的SEC Core中去,但在写代码时候是不知道SEC Core在Flash哪里的,所以这里仅仅占位两个字节。其后,UEFI的BaseTools在产生FV的时候会找到SEC Core的入口,然后填写一个相对地址在这里。具体在Edk2\BaseTools\Source\C\GenFv\GenFvInternalLib.c

//
// Write SecCore Entry point relative address into the jmp instruction in reset vector.
//
  Ia32ResetAddressPtr  = (UINT32 *) ((UINTN) FvImage->Eof - IA32_SEC_CORE_ENTRY_OFFSET);

  Ia32SecEntryOffset   = (INT32) (SecCorePhysicalAddress - (FV_IMAGES_TOP_ADDRESS - IA32_SEC_CORE_ENTRY_OFFSET + 2));
  if (Ia32SecEntryOffset <= -65536) {
    Error (NULL, 0, 3000, "Invalid", "The SEC EXE file size is too large, it must be less than 64K.");
    return STATUS_ERROR;
  }
Main16

通过jmp跳转到UefiCpuPkg\ResetVector\Vtf0\Main.asm下的Main16位置

BITS    16

; Modified:  EBX, ECX, EDX, EBP
;
; @param[in,out]  RAX/EAX  Initial value of the EAX register
;                          (BIST: Built-in Self Test)
; @param[in,out]  DI       'BP': boot-strap processor, or
;                          'AP': application processor
; @param[out]     RBP/EBP  Address of Boot Firmware Volume (BFV)
; @param[out]     DS       Selector allowing flat access to all addresses
; @param[out]     ES       Selector allowing flat access to all addresses
; @param[out]     FS       Selector allowing flat access to all addresses
; @param[out]     GS       Selector allowing flat access to all addresses
; @param[out]     SS       Selector allowing flat access to all addresses
;
; @return         None  This routine jumps to SEC and does not return

Main16:
    ; ESP - EAX 寄存器的初始值(BIST:内置自检)
    ; OneTimeCall是宏
    OneTimeCall EarlyInit16
    
    ; 从实模式转换到32位平坦模式
    ; 平坦模式:直接用一个地址寄存器来线性访问4G的内存.32位的CPU最多可以寻址4GB的内存空间,如果物理内存大于4GB,超出的部分CPU是无法寻址到的。
    ; 保护模式: 在这种状态下, 一切程序都可以用线性地址(不分段)访问自己所拥有的4G的内存空间, 但是不能访问其他程序的空间. 
    ; 实模式:寻址采用和8086相同的16位段和偏移量,最大寻址空间1MB,最大分段64KB。可以使用32位指令。32位的x86 CPU用做高速的8086。
   
    OneTimeCall TransitionFromReal16To32BitFlat 
    
BITS    32

    ; Search for the Boot Firmware Volume (BFV)
    ;定位固件中的 BFV
    ; FV:固件卷,指在FD上一个连续的部分,我们可以把它看成一个逻辑设备,因为我们代码真正操作的是FV,而非FD。
    ; FFS的概念也是以FV的形式存在,它描述了FV中的文件组织方式。FV之于FD,类似于thread之于package。
    ; 
    OneTimeCall Flat32SearchForBfvBase  

    ; 搜索SEC入口点
    ; 定位BFV中SEC映像
    OneTimeCall Flat32SearchForSecEntryPoint

    ; ESI - SEC Core entry point 
    ; ESI称为源变址寄存器,通常存放 要处理的数据的内存地址。
    ; EBP - Start of BFV
    ; esi存放了SEC的入口地址,EBP存放了BFV起始地址

%ifdef ARCH_IA32

    ; 将初始 EAX 值恢复到 EAX 寄存器
    ; ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
    mov     eax, esp

    ; Jump to the 32-bit SEC entry point
    ; 跳转到 32 位 SEC 入口点
    jmp     esi

%else

    ; Transition the processor from 32-bit flat mode to 64-bit flat mode
    ;32位模式转换为64位模式
    OneTimeCall Transition32FlatTo64Flat

BITS    64

    ; Some values were calculated in 32-bit mode.  Make sure the upper
    ; 32-bits of 64-bit registers are zero for these values.
    ; 一些值是在 32 位模式下计算的。 对于这些值,3264 位寄存器的上高32位为零。
    mov     rax, 0x00000000ffffffff
    and     rsi, rax
    and     rbp, rax
    and     rsp, rax

    ; RSI - SEC Core entry point
    ; RBP - Start of BFV
    ; RSI - SEC 核心入口点,RBP - BFV 的开始

    ; Restore initial EAX value into the RAX register
    ; 将初始 EAX 值恢复到 RAX 寄存器中
    mov     rax, rsp

    ; Jump to the 64-bit SEC entry point
    ; 跳转到 64 位 SEC 入口点
    jmp     rsi

%endif

在Reset Vector部分,因为系统还没有RAM,因而不能使用基于栈的程序设计,所有函数调用都使用 jmp 指令模拟。OneTimeCall是宏,用于模拟call指令。例:
UEFI BIOS —— SEC阶段分析_第4张图片
OneTimeCall Flat32SearchForBfvBase 怎么定位 BFV ?

BFV其实就是uefi image的起点,这里就是BFV,结构可以参考uefi手册3.2.1章节
edk2\BaseTools\Source\C\Include\Common\PiFirmwareVolume.h

typedef struct {
  UINT32 NumBlocks;
  UINT32 Length;
} EFI_FV_BLOCK_MAP_ENTRY;

//
// Describes the features and layout of the firmware volume.
//
typedef struct {
  UINT8                     ZeroVector[16];
  EFI_GUID                  FileSystemGuid;
  UINT64                    FvLength;
  UINT32                    Signature;
  EFI_FVB_ATTRIBUTES_2      Attributes;
  UINT16                    HeaderLength;
  UINT16                    Checksum;
  UINT16                    ExtHeaderOffset;
  UINT8                     Reserved[1];
  UINT8                     Revision;
  EFI_FV_BLOCK_MAP_ENTRY    BlockMap[1];
} EFI_FIRMWARE_VOLUME_HEADER;

#define EFI_FVH_SIGNATURE SIGNATURE_32 ('_', 'F', 'V', 'H')

UEFI BIOS —— SEC阶段分析_第5张图片
最后ebp寄存器存放了BFV的起始地址

OneTimeCall Flat32SearchForSecEntryPoint 怎么定位 BFV 的 SEC
UEFI BIOS —— SEC阶段分析_第6张图片

最后esi 寄存器存放了SEC 的入口地址

2.2 SEC Core

2.2.1 SEC Core功能分析

进入SEC功能区后,首先利用 CAR 技术初始化栈,初始化 IDT,初始化EFI_SEC_PEI_HAND_OFF,将控制权转交给PEI,并将 EFI_SEC_PEI_HAND_OFF 传递给PEI。

  1. CAR (Cache ASRAM),在Cashe上开辟一段空间作为内存使用(此时内存尚未初始化,相关C语言运行需要内存和栈的空间) ;
  2. IDT ( Interrupt Descriptor Table ) 中断描述表,,记录了0~255的中断号和调用函数之间的关系。结构体如下所述:
_SEC_IDT_TABLE

edk2\UefiCpuPkg\SecCore\SecMain.h

#define SEC_IDT_ENTRY_COUNT  34

typedef struct _SEC_IDT_TABLE {
  // 在 IDT 之前保留 8 个字节来存储 EFI_PEI_SERVICES**,因为 IDT 基础地址应该是 8 字节对齐。
  // 注意:对于IA32,只有IDT前面的4个字节用于存储EFI_PEI_SERVICES**
  UINT64            PeiService;
  UINT64            IdtTable[SEC_IDT_ENTRY_COUNT];
} SEC_IDT_TABLE;
EFI_SEC_PEI_HAND_OFF

EFI_SEC_PEI_HAND_OFF实际上就是一个结构体,是UEFI当中在SEC阶段最重要的一个数据结构,将环境从以汇编语言执行转向C语言执行。
edk2\MdePkg\Include\Pi\PiPeiCis.h

/// EFI_SEC_PEI_HAND_OFF 结构包含有关PEI核心的运行环境,如位置大小、临时 RAM、堆栈位置和 BFV 位置。
typedef struct _EFI_SEC_PEI_HAND_OFF {
  UINT16  DataSize;  //数据结构的大小。
  VOID    *BootFirmwareVolumeBase; //指向BFV的第一个字节,PEI Dispatcher应该搜索PEI模块
  UINTN   BootFirmwareVolumeSize;  //BFV的大小,以字节为单位。
  VOID    *TemporaryRamBase;  //指向临时RAM的第一个字节。
  UINTN   TemporaryRamSize;  //临时RAM的大小,以字节为单位

  /// 指向PEI可以使用的临时RAM的第一个字节。
  /// PeiTemporaryRamBase 和 PeiTemporaryRamSize 描述的区域不得超出 TemporaryRamBase & TemporaryRamSize 描述的区域之外。
  /// 这个区域不应该与StackBase和StackSize返回的区域重叠。
  VOID    *PeiTemporaryRamBase;
  
  UINTN   PeiTemporaryRamSize;  //PEI使用的可用临时RAM的大小,以字节为单位。
  VOID    *StackBase;  //指向堆栈的第一个字节。这可能是由TemporaryRamBase和TemporaryRamSize描述的内存的一部分,也可能是一个完全独立的区域。
  UINTN   StackSize;  //堆栈的大小,以字节为单位。
} EFI_SEC_PEI_HAND_OFF;
2.2.1 SEC Core 执行实例
OVMF实例

不同硬件平台,SEC代码会有不同实现方式,但大致过程相似。下面以OVMF为例,介绍SEC功能区执行过程。

edk2\OvmfPkg\Sec\X64\SecEntry.nasm

extern ASM_PFX(SecCoreStartupWithStack)

; SecCore 入口点

global ASM_PFX(_ModuleEntryPoint)
ASM_PFX(_ModuleEntryPoint):

    ; 临时RAM已经初始化,设置栈地址,PcdOvmfSecPeiTempRamBase和PcdOvmfSecPeiTempRamSize在OvmfPkgIa32X64.fdf
    ; 用初始堆栈值填充临时 RAM。
    mov     rax, (FixedPcdGet32 (PcdInitValueInTempStack) << 32) | FixedPcdGet32 (PcdInitValueInTempStack)

    mov     rdi, FixedPcdGet32 (PcdOvmfSecPeiTempRamBase)     ; 相对于ES, qword来存储基址
    
    mov     rcx, FixedPcdGet32 (PcdOvmfSecPeiTempRamSize) / 8 ; qword从基地计数存储
    cld                                                                                                       
    rep stosq

    ; 基于 PCD 加载临时 RAM 堆栈
    %define SEC_TOP_OF_STACK (FixedPcdGet32 (PcdOvmfSecPeiTempRamBase) + \
                          FixedPcdGet32 (PcdOvmfSecPeiTempRamSize))
    mov     rsp, SEC_TOP_OF_STACK
    nop

    ;   rcx: BootFirmwareVolumePtr  BFV首地址
    ;   rdx: TopOfCurrentStack  栈起始地址
    ;  设置参数并调用 SecCoreStartupWithStack,
    mov     rcx, rbp    ; BFV 首地址,rbp 为传参数
    mov     rdx, rsp    ; 栈起始地址
    sub     rsp, 0x20
    call    ASM_PFX(SecCoreStartupWithStack)  ;此时栈已可用,故可使用 call 指令
Sec入口函数

根据最后的call指令找到跳转到SecCoreStartupWithStack函数,在edk2\OvmfPkg\Sec\SecMain.c中的SecCoreStartupWithStack函数

edk2\MdePkg\Include\Library\BaseLib.h

#pragma pack (1)
typedef struct {
  UINT16  Limit;
  UINTN   Base;
} IA32_DESCRIPTOR;
#pragma pack ()

edk2\OvmfPkg\Sec\SecMain.c

  1. 初始化浮点寄存器
  2. 初始化 IDT
  3. 初始化 SecCoreData, 将临时 RAM 地址、栈地址、BFV地址赋值给 SecCoreData
VOID
EFIAPI
SecCoreStartupWithStack (
  IN EFI_FIRMWARE_VOLUME_HEADER       *BootFv,
  IN VOID                             *TopOfCurrentStack
  )
{
  EFI_SEC_PEI_HAND_OFF        SecCoreData;
  SEC_IDT_TABLE               IdtTableInStack;
  IA32_DESCRIPTOR             IdtDescriptor;
  UINT32                      Index;
  volatile UINT8              *Table;

  // 为确保 SMM 在 S3 恢复时不会受到损害,我们必须强制重新初始化 BaseExtractGuidedSectionLib。 
  // 由于这是在调用库构造函数之前,我们必须使用循环而不是 SetMem。
  Table = (UINT8*)(UINTN)FixedPcdGet64 (PcdGuidedExtractHandlerTableAddress);
  for (Index = 0;
       Index < FixedPcdGet32 (PcdGuidedExtractHandlerTableSize);
       ++Index) {
    Table[Index] = 0;
  }

  // 初始化 IDT - 由于这是在调用库构造函数之前,我们使用循环而不是 CopyMem。
  IdtTableInStack.PeiService = NULL;
  for (Index = 0; Index < SEC_IDT_ENTRY_COUNT; Index ++) {
    UINT8  *Src;
    UINT8  *Dst;
    UINTN  Byte;

    Src = (UINT8 *) &mIdtEntryTemplate;
    Dst = (UINT8 *) &IdtTableInStack.IdtTable[Index];
    for (Byte = 0; Byte < sizeof (mIdtEntryTemplate); Byte++) {
      Dst[Byte] = Src[Byte];
    }
  }

  IdtDescriptor.Base  = (UINTN)&IdtTableInStack.IdtTable;
  IdtDescriptor.Limit = (UINT16)(sizeof (IdtTableInStack.IdtTable) - 1);

  if (SevEsIsEnabled ()) {
    SevEsProtocolCheck ();
    // 目前是运行在flash芯片里的,内存此时还用不了。
    // InitializeCpuExceptioInHandlers就是把idt entry里的所有handler赋值为CommonInterruptEntry
    //定义在UefiCpuPkg\Library\CpuExceptionHandlerLib\Ia32\ExceptionHandlerAsm.nasm。    
    AsmWriteIdtr (&IdtDescriptor);
    InitializeCpuExceptionHandlers (NULL);
  }

  ProcessLibraryConstructorList (NULL, NULL);

  if (!SevEsIsEnabled ()) {
    // 对于非 SEV-ES guests,只需加载 IDTR。
    AsmWriteIdtr (&IdtDescriptor);
  } else {
    // 在 SEV-ES 下,管理程序无法修改 CR0,因此无法启用缓存以加快启动速度。 
    // 尽早为 SEV-ES guest启用缓存。
    AsmEnableCache ();
  }

  DEBUG ((DEBUG_INFO,
    "SecCoreStartupWithStack(0x%x, 0x%x)\n",
    (UINT32)(UINTN)BootFv,
    (UINT32)(UINTN)TopOfCurrentStack
    ));

  // 初始化浮点操作环境以符合 UEFI 规范。
  InitializeFloatingPointUnits ();

#if defined (MDE_CPU_X64)
  // 断言页表由复位向量代码设置为我们期望的地址。
  ASSERT (AsmReadCr3 () == (UINTN) PcdGet32 (PcdOvmfSecPageTablesBase));
#endif

  //
  // |-------------|       <-- TopOfCurrentStack
  // |   Stack     | 32k
  // |-------------|
  // |    Heap     | 32k
  // |-------------|       <-- SecCoreData.TemporaryRamBase
  //

  ASSERT ((UINTN) (PcdGet32 (PcdOvmfSecPeiTempRamBase) +
                   PcdGet32 (PcdOvmfSecPeiTempRamSize)) ==
          (UINTN) TopOfCurrentStack);

  // 初始化 SECOND 切换状态
  SecCoreData.DataSize = sizeof(EFI_SEC_PEI_HAND_OFF);

  SecCoreData.TemporaryRamSize       = (UINTN) PcdGet32 (PcdOvmfSecPeiTempRamSize);
  SecCoreData.TemporaryRamBase       = (VOID*)((UINT8 *)TopOfCurrentStack - SecCoreData.TemporaryRamSize);

  SecCoreData.PeiTemporaryRamBase    = SecCoreData.TemporaryRamBase;
  SecCoreData.PeiTemporaryRamSize    = SecCoreData.TemporaryRamSize >> 1;

  SecCoreData.StackBase              = (UINT8 *)SecCoreData.TemporaryRamBase + SecCoreData.PeiTemporaryRamSize;
  SecCoreData.StackSize              = SecCoreData.TemporaryRamSize >> 1;

  SecCoreData.BootFirmwareVolumeBase = BootFv;
  SecCoreData.BootFirmwareVolumeSize = (UINTN) BootFv->FvLength;

  // 确保在初始化调试代理之前屏蔽 8259 并启用调试计时器
  IoWrite8 (0x21, 0xff);
  IoWrite8 (0xA1, 0xff);

  // 在初始化调试代理和启用调试定时器之前,初始化本地 APIC 定时器硬件并禁用本地 APIC 定时器中断。
  InitializeApicTimer (0, MAX_UINT32, TRUE, 5);
  DisableApicTimerInterrupt ();

  // 在内存准备好之前,初始化调试代理以支持 SEC/PEI 阶段中的源代码级调试。
  InitializeDebugAgent (DEBUG_AGENT_INIT_PREMEM_SEC, &SecCoreData, SecStartupPhase2);
}
IA32实例

上述是 OVMF 平台的入口函数相关内容,我们再看下 IA32 平台的入口函数相关内容,在 IA32 平台入口函数是 SecStartup

  ;
  ; Pass Control into the PEI Core
  ;
  call ASM_PFX(SecStartup)
/**
SEC的C语言阶段的入口点。SEC汇编代码初始化一些临时内存并建立堆栈后,控制被转移到这个函数。

  @param SizeOfRam           可用的临时内存的大小。
  @param TempRamBase         临时内存的基址
  @param BootFirmwareVolume  BFV的基本地址。
**/
VOID
NORETURN
EFIAPI
SecStartup (
  IN UINT32                   SizeOfRam,
  IN UINT32                   TempRamBase,
  IN VOID                     *BootFirmwareVolume
  )
{
  EFI_SEC_PEI_HAND_OFF        SecCoreData;
  IA32_DESCRIPTOR             IdtDescriptor;
  SEC_IDT_TABLE               IdtTableInStack;
  UINT32                      Index;
  UINT32                      PeiStackSize;
  EFI_STATUS                  Status;

  //
  // Report Status Code to indicate entering SEC core
  // 报告状态码指示进入SEC核心
  // 如果状态代码类型已启用,则报告带有最小参数的状态代码。
  // 如果由type指定的状态码类型在PcdReportStatusCodeProperyMask中启用,就调用ReportStatusCode()传入类型和值。
  //
  REPORT_STATUS_CODE (
    EFI_PROGRESS_CODE,
    EFI_SOFTWARE_SEC | EFI_SW_SEC_PC_ENTRY_POINT
    );
  
  //PcdPeiTemporaryRamStackSize的值是指定临时RAM中的堆栈大小。0表示临时ramsize的一半。
  PeiStackSize = PcdGet32 (PcdPeiTemporaryRamStackSize);
  if (PeiStackSize == 0) {
    PeiStackSize = (SizeOfRam >> 1);
  }

  ASSERT (PeiStackSize < SizeOfRam);

  //
  // Process all libraries constructor function linked to SecCore.
  // 处理所有链接到SecCore的库构造函数。
  // 为模块的所有依赖库调用库构造函数的自动生成函数。一旦建立了堆栈,SEC核心必须调用这个函数。
  //
  ProcessLibraryConstructorList ();

  //
  // Initialize floating point operating environment to be compliant with UEFI spec.
  // 初始化浮点操作环境以符合UEFI规范。初始化浮点寄存器
  // 这个函数将浮点控制字初始化为0x027F(所有异常都被屏蔽,双精度,四舍五入到最接近),
  // 多媒体扩展控制字(如果支持)初始化为0x1F80(所有异常都被屏蔽,四舍五入到最接近,对于被屏蔽的下流,flush为零)。
  //
  InitializeFloatingPointUnits ();

  // |-------------------|---->
  // |IDT Table          |
  // |-------------------|
  // |PeiService Pointer |    PeiStackSize
  // |-------------------|
  // |                   |
  // |      Stack        |
  // |-------------------|---->
  // |                   |
  // |                   |
  // |      Heap         |    PeiTemporayRamSize
  // |                   |
  // |                   |
  // |-------------------|---->  TempRamBase
  
  //  初始化IDT
  //  在IDT之前保留8个字节来存储EFI_PEI_SERVICES**,因为IDT基址应该是8字节对齐。
  //  注意:对于IA32,只有IDT前面的4个字节用于存储EFI_PEI_SERVICES**
  //
  IdtTableInStack.PeiService = 0;
  for (Index = 0; Index < SEC_IDT_ENTRY_COUNT; Index ++) {
    // mIdtEntryTemplate	IA32_IDT_GATE_DESCRIPTOR	IDT 中断门描述符
    // IDT里的描述符就是描述中断处理程序的数据结构
    CopyMem ((VOID*)&IdtTableInStack.IdtTable[Index], (VOID*)&mIdtEntryTemplate, sizeof (UINT64));
  }
  //IdtDescriptor  IA32_DESCRIPTOR  IDTR, GDTR, LDTR描述符的字节打包结构
  //描述符是存储描述信息的数据结构。GDT/LDT里描述符就是描述段地址和门的描述符。
  IdtDescriptor.Base  = (UINTN) &IdtTableInStack.IdtTable;
  IdtDescriptor.Limit = (UINT16)(sizeof (IdtTableInStack.IdtTable) - 1);
  
  // 写入当前中断描述符表寄存器(GDTR)描述符。
  // 写入当前IDTR描述符并在IDTR中返回它。此功能仅适用于IA-32和X64。
  // 如果Idtr为NULL,则ASSERT()。
  AsmWriteIdtr (&IdtDescriptor);

  // Setup the default exception handlers
  // 初始化调试代理。
  // 该功能用于为SMM代码的源代码调试设置调试环境。
  // 如果InitFlag为DEBUG_AGENT_INIT_SMM,则会覆盖IDT表项,初始化调试端口。它将从GUIDed HOB获得调试代理邮箱,
  // 如果它存在,调试代理将把它复制到SMM空间的本地邮箱中。它将覆盖IDT表项并初始化调试端口。Context将为空。
  // 如果“InitFlag”为“DEBUG_AGENT_INIT_ENTER_SMI”,则调试代理将保存调试寄存器并在SMM空间中获取本地邮箱。Context将为空。
  // 当“InitFlag”为“DEBUG_AGENT_INIT_EXIT_SMI”时,调试代理将恢复调试寄存器。Context将为空。
  Status = InitializeCpuExceptionHandlers (NULL);
  ASSERT_EFI_ERROR (Status);

  // Update the base address and length of Pei temporary memory
  // 初始化SecCoreData,将临时的RAM地址,栈地址、BFV地址赋值给SecCoreData
  SecCoreData.DataSize               = (UINT16) sizeof (EFI_SEC_PEI_HAND_OFF);
  SecCoreData.BootFirmwareVolumeBase = BootFirmwareVolume;
  SecCoreData.BootFirmwareVolumeSize = (UINTN)((EFI_FIRMWARE_VOLUME_HEADER *) BootFirmwareVolume)->FvLength;
  SecCoreData.TemporaryRamBase       = (VOID*)(UINTN) TempRamBase;
  SecCoreData.TemporaryRamSize       = SizeOfRam;
  SecCoreData.PeiTemporaryRamBase    = SecCoreData.TemporaryRamBase;
  SecCoreData.PeiTemporaryRamSize    = SizeOfRam - PeiStackSize;
  SecCoreData.StackBase              = (VOID*)(UINTN)(TempRamBase + SecCoreData.PeiTemporaryRamSize);
  SecCoreData.StackSize              = PeiStackSize;

  // Initialize Debug Agent to support source level debug in SEC/PEI phases before memory ready.
  // 在内存准备好之前,初始化调试代理以支持SEC/PEI阶段的源级调试。
  InitializeDebugAgent (DEBUG_AGENT_INIT_PREMEM_SEC, &SecCoreData, SecStartupPhase2);

  // Should not come here.
  UNREACHABLE ();
}
参与子函数 InitializeDebugAgent
/**
  初始化调试代理。

  此函数用于设置调试环境,以支持源代码级的调试。
  如果某些调试代理库实例有一些私人数据保存在堆栈中,这个函数必须在此模式工作不返回给调用者,
  然后调用者需要结束后所有其他逻辑InitializeDebugAgent()成一个函数并将其传递到InitializeDebugAgent () .
  InitializeDebugAgent()负责调用传入函数结束时InitializeDebugAgent()。

  如果参数函数不为空,调试代理库实例将通过在上下文中传递参数来调用它。

  如果Function()为空,调试代理库实例将在安装调试环境后返回。

  @param[in] InitFlag     Init flag is used to decide the initialize process.
  @param[in] Context      Context needed according to InitFlag; it was optional.
  @param[in] Function     Continue function called by debug agent library; it was
                          optional.
**/
VOID
EFIAPI
InitializeDebugAgent (
  IN UINT32                InitFlag,
  IN VOID                  *Context, OPTIONAL
  IN DEBUG_AGENT_CONTINUE  Function  OPTIONAL
  );

2.3 跳转到PEI 入口地址

SecStartupPhase2()-> PeiCore ()

SecStartup 或者 SecStartupPhase2 最后会调用:
InitializeDebugAgent (DEBUG_AGENT_INIT_PREMEM_SEC, &SecCoreData, SecStartupPhase2),
SecStartupPhase2 最后会调用 PEI 入口函数 :
(*PeiCoreEntryPoint) (SecCoreData, (EFI_PEI_PPI_DESCRIPTOR *)&mPrivateDispatchTable);
以 OVMF为例,PATH:edk2\OvmfPkg\Sec\SecMain.c

EFI_PEI_PPI_DESCRIPTOR mPrivateDispatchTable[] = {
  {
    (EFI_PEI_PPI_DESCRIPTOR_PPI | EFI_PEI_PPI_DESCRIPTOR_TERMINATE_LIST),
    &gEfiTemporaryRamSupportPpiGuid,
    &mTemporaryRamSupportPpi
  },
};

/**
 调用方提供了在 InitializeDebugAgent() 结束时调用的函数。

   SEC 的 C 语言阶段的入口点。 SEC 大会后
   代码已经初始化了一些临时内存并设置了堆栈,
   控制权转移到此功能。

   @param[in] Context InitializeDebugAgent() 的第一个输入参数。

**/
VOID
EFIAPI
SecStartupPhase2(
  IN VOID                     *Context
  )
{
  EFI_SEC_PEI_HAND_OFF        *SecCoreData;
  EFI_FIRMWARE_VOLUME_HEADER  *BootFv;
  EFI_PEI_CORE_ENTRY_POINT    PeiCoreEntryPoint;

  SecCoreData = (EFI_SEC_PEI_HAND_OFF *) Context;

  // 找到 PEI Core 入口点。 如果启用远程调试,它将报告 SEC 和 Pei Core 调试信息。
  BootFv = (EFI_FIRMWARE_VOLUME_HEADER *)SecCoreData->BootFirmwareVolumeBase;
  FindAndReportEntryPoints (&BootFv, &PeiCoreEntryPoint);
  SecCoreData->BootFirmwareVolumeBase = BootFv;
  SecCoreData->BootFirmwareVolumeSize = (UINTN) BootFv->FvLength;

  // 将控制权转移到 PEI 核心
  (*PeiCoreEntryPoint) (SecCoreData, (EFI_PEI_PPI_DESCRIPTOR *)&mPrivateDispatchTable);

  // 如果我们到达这里,则 PEI 核心返回,这是不可恢复的。
  ASSERT (FALSE);
  CpuDeadLoop ();
}

最后传入EFI_SEC_PEI_HAND_OFF(前面已写) 和 EFI_PEI_PPI_DESCRIPTOR这个类型指针到pei阶段

// PEI Ppi 服务列表描述符
#define EFI_PEI_PPI_DESCRIPTOR_PIC              0x00000001
#define EFI_PEI_PPI_DESCRIPTOR_PPI              0x00000010
#define EFI_PEI_PPI_DESCRIPTOR_NOTIFY_CALLBACK  0x00000020
#define EFI_PEI_PPI_DESCRIPTOR_NOTIFY_DISPATCH  0x00000040
#define EFI_PEI_PPI_DESCRIPTOR_NOTIFY_TYPES     0x00000060
#define EFI_PEI_PPI_DESCRIPTOR_TERMINATE_LIST   0x80000000

//PEIM用来描述PEI Foundation可用服务的数据结构
typedef struct {
  /// 这个字段是一组标记,描述这个导入的表条目的特征。
  /// 所有标记都定义为EFI_PEI_PPI_DESCRIPTOR_***,也可以组合为一个标记。
  /// Flags描述了PPI的特征
  UINTN     Flags;
  /// 命名接口的EFI_GUID的地址。
  /// Guid是PPI的名字。
  EFI_GUID  *Guid;
  /// 指向PPI的指针。它包含安装服务所需的信息。
  /// Ppi是service的实体,这个是PPI的真正意义。
  VOID      *Ppi;
} EFI_PEI_PPI_DESCRIPTOR;
FindAndReportEntryPoints()
/*
  找到并返回PEI核心入口点。
  它还可以查找SEC和PEI核心文件的调试信息。如果启用了远程调试,它将报告它们。
  
  @param   BootFirmwareVolumePtr    指向BFV。
  @param   PeiCoreEntryPoint        PEI核心的入口.
**/
VOID
FindAndReportEntryPoints (
  IN  EFI_FIRMWARE_VOLUME_HEADER       **BootFirmwareVolumePtr,
  OUT EFI_PEI_CORE_ENTRY_POINT         *PeiCoreEntryPoint
  )
{
  EFI_STATUS                       Status;
  EFI_PHYSICAL_ADDRESS             SecCoreImageBase;
  EFI_PHYSICAL_ADDRESS             PeiCoreImageBase;
  PE_COFF_LOADER_IMAGE_CONTEXT     ImageContext;

  // 查找SEC核心和PEI核心Image base
  Status = FindImageBase (*BootFirmwareVolumePtr, &SecCoreImageBase);
  ASSERT_EFI_ERROR (Status);

  FindPeiCoreImageBase (BootFirmwareVolumePtr, &PeiCoreImageBase);

  ZeroMem ((VOID *) &ImageContext, sizeof (PE_COFF_LOADER_IMAGE_CONTEXT));

  // 当启用远程调试时,报告SEC核心调试信息
  ImageContext.ImageAddress = SecCoreImageBase;
  ImageContext.PdbPointer = PeCoffLoaderGetPdbPointer ((VOID*) (UINTN) ImageContext.ImageAddress);
  PeCoffLoaderRelocateImageExtraAction (&ImageContext);

  // 当开启远程调试时,上报PEI核心调试信息
  ImageContext.ImageAddress = (EFI_PHYSICAL_ADDRESS)(UINTN)PeiCoreImageBase;
  ImageContext.PdbPointer = PeCoffLoaderGetPdbPointer ((VOID*) (UINTN) ImageContext.ImageAddress);
  PeCoffLoaderRelocateImageExtraAction (&ImageContext);

  // 找到PEI核心入口点
  Status = PeCoffLoaderGetEntryPoint ((VOID *) (UINTN) PeiCoreImageBase, (VOID**) PeiCoreEntryPoint);
  if (EFI_ERROR (Status)) {
    *PeiCoreEntryPoint = 0;
  }

  return;
}

PE部分参考:PE详解

PeiCoreEntryPoin这个值就是一个64位的虚拟地址,这个地址是Pei阶段的Entry point函数的入口地址,然后通过(*PeiCoreEntryPoint) (SecCoreData, (EFI_PEI_PPI_DESCRIPTOR *)&mPrivateDispatchTable); 跳转到Pei阶段。

2.4 PEI 入口函数

UEFI最重要的特点就是模块化设计,模块载入内存生成Image。Image的入口函数是_ModuleEntryPoint。而PEI也是一个模块,PEI入口函数在:
edk2\MdePkg\Library\PeiCoreEntryPoint\PeiCoreEntryPoint.c
_ModuleEntryPoint

/**
  PE/COFF图像的入口点为PEI核心。
  
  这个函数是PEI Foundation的入口点,它允许SEC阶段传递关于堆栈、临时RAM和引导固件卷的信息。
  此外,它还允许SEC阶段以一个或多个ppi的形式传递服务和数据,以供PEI阶段使用。
  从SEC传递到PEI Foundation的额外PPIs数量没有限制。作为初始化阶段的一部分,
  PEI Foundation将把这些sec托管的PPIs添加到其PPI数据库中,
  这样PEI Foundation和任何模块都可以利用这些早期PPIs中的相关服务调用and/or代码。
  这个函数需要调用ProcessModuleEntryPointList(),并将上下文参数设置为NULL。ProcessModuleEntryPoint()永远不会返回。
  PEI核心负责在PEI服务表和PEI核心本身的文件句柄建立之后调用ProcessLibraryConstructorList()。
  如果ProcessModuleEntryPointList()返回,则ASSERT()并停止系统。

  @param SecCoreData 指向一个包含PEI核心操作环境信息的数据结构,
                     例如临时RAM的大小和位置,堆栈位置和BFV位置

  @param PpiList    指向PEI核心最初要安装的一个或多个PPI描述符的列表。
                    空的PPI列表由单个描述符组成,其结束标记为EFI_PEI_PPI_DESCRIPTOR_TERMINATE_LIST。
                    作为初始化阶段的一部分,PEI Foundation将把这些sec托管的PPIs添加到其PPI数据库中,
                    这样PEI Foundation和任何模块都可以利用这些早期PPIs中的相关服务调用和/或代码。
**/
VOID
EFIAPI
_ModuleEntryPoint(
  IN CONST  EFI_SEC_PEI_HAND_OFF    *SecCoreData,
  IN CONST  EFI_PEI_PPI_DESCRIPTOR  *PpiList
)
{
  ProcessModuleEntryPointList (SecCoreData, PpiList, NULL);

  //
  // Should never return
  //
  ASSERT(FALSE);
  CpuDeadLoop ();
}

build ovmf (参考UEFI开发环境的 使用qemu虚拟机调试OvmfPkg部分) 找到其 ProcessModuleEntryPointList , 位于:edk2\Build\OvmfX64\RELEASE_VS2019\X64\MdeModulePkg\Core\Pei\PeiMain\DEBUG\AutoGen.c

VOID
EFIAPI
ProcessModuleEntryPointList (
  IN CONST  EFI_SEC_PEI_HAND_OFF    *SecCoreData,
  IN CONST  EFI_PEI_PPI_DESCRIPTOR  *PpiList,
  IN VOID                           *Context
  )

{
  PeiCore (SecCoreData, PpiList, Context);
}

最终调用PEI入口函数 PeiCore ,其位于
edk2\MdeModulePkg\Core\Pei\PeiMain\PeiMain.c

/**
  该例程在转换过程中由 PeiMain 模块的主入口调用从 SEC 到 PEI。
  在PEI核心切换堆栈后,会重启与旧的核心数据。

  @param SecCoreDataPtr  指向包含有关 PEI 核心的操作信息的数据结构
                         环境,例如临时 RAM 的大小和位置、堆栈位置和
                         BFV 位置。
                        
  @param PpiList         指向由 PEI 核心最初安装的一个或多个 PPI 描述符的列表。
                         一个空的 PPI 列表由一个带有结束标签的描述符组成
                         EFI_PEI_PPI_DESCRIPTOR_TERMINATE_LIST。 作为其初始化的一部分
                         阶段,PEI 基金会会将这些 SEC 托管的 PPI 添加到其 PPI 数据库中,例如
                         PEI 基金会和任何模块都可以利用相关服务
                         这些早期 PPI 中的调用和/或代码
  @param Data            指向旧核心数据的指针,用于初始化
                         核心的数据区域。
                         如果为NULL,则首先进入PeiCore。

**/
VOID
EFIAPI
PeiCore (
  IN CONST EFI_SEC_PEI_HAND_OFF        *SecCoreDataPtr,
  IN CONST EFI_PEI_PPI_DESCRIPTOR      *PpiList,
  IN VOID                              *Data
  );

本文主要来源:

  1. EDKII
  2. 《UEFI原理与编程》
  3. UEFI社区
  4. 《Intel® 64 and IA-32 Architectures Software Developer’s Manual》

你可能感兴趣的:(UEFI,c,uefi)