SEC阶段是平台初始话的第一个阶段,计算机系统加电后首先进入这个阶段。
SEC阶段的功能:UEFI系统开机或重启后首先进入SEC阶段,SEC阶段系统执行以下四种任务:
根据临时RAM是否初始化为界限,SEC阶段分为两部分:临时RAM初始化前称为Reset Vector阶段;临时RAM初始化后调用SEC入口函数从而进入SEC功能区。我们简单理下从上电到SEC阶段的大致流程:
第一条指令所在位置被称为 Reset Vector,之后进入Reset Vector阶段。
Reset Vector的执行流程如下:
追踪下代码执行流程,复位之后第一条指令位置对应:
UefiCpuPkg\ResetVector\Vtf0\Ia16\ResetVectorVtf0.asm的resetVector位置
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.asm的EarlyBspInitReal16位置
; DI(destination index)是目的变址寄存器,用做隐含的目的串地址,默认在ES中;ES叫做额外的段寄存器. 它通常跟DI一起用来做指针使用.
; BP是基指针, 通常BP用来保存使用局部变量的地址.
EarlyBspInitReal16:
mov di, 'BP'
jmp short Main16
可以看出,最开始指令是两个啥也不干的NOP和一个短跳转,那么:
1. 为什么开局是CPU不做操作(有时候作为占位符和微调timing用)的NOP,而不是直接有用的JMP开局?
关于为什么是NOP在IA32手册有说明
简单来说就是内核可以顺利执行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;
}
通过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 位模式下计算的。 对于这些值,32 位 64 位寄存器的上高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指令。例:
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')
OneTimeCall Flat32SearchForSecEntryPoint 怎么定位 BFV 的 SEC
最后esi 寄存器存放了SEC 的入口地址
进入SEC功能区后,首先利用 CAR 技术初始化栈,初始化 IDT,初始化EFI_SEC_PEI_HAND_OFF,将控制权转交给PEI,并将 EFI_SEC_PEI_HAND_OFF 传递给PEI。
- CAR (Cache ASRAM),在Cashe上开辟一段空间作为内存使用(此时内存尚未初始化,相关C语言运行需要内存和栈的空间) ;
- IDT ( Interrupt Descriptor Table ) 中断描述表,,记录了0~255的中断号和调用函数之间的关系。结构体如下所述:
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实际上就是一个结构体,是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;
不同硬件平台,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 指令
根据最后的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
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);
}
上述是 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()。
如果参数函数不为空,调试代理库实例将通过在上下文中传递参数来调用它。
如果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
);
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;
/*
找到并返回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阶段。
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
);
本文主要来源: