本文的启动分析是基于龙心3A2000+780E桥片的处理器的。
首先:
SEC(安全性)阶段:
系统开始执行第一条指令,这时的Memory没有被初始化。主要工作是建立临时的Memory,它可以是处理器的Cache,或是system Static RAM(SRAM)。另外,SEC Phase需要知道一些早期的内存被映射到的位置以及BFV(Boot Firmware Volume)的位置。
PEI(EFI前初始化)阶段:(详细的Pei阶段请看:http://www.doc88.com/p-587672390607.html)
主要工作是Memory初始化以及一些必要的CPU、Chipset等等的初始化,为加载DXE做准备,准备HOB数据。
Sec:
开机系统第一条指令由硬件规定死,从地址0x1fc00000地址出开始执行,由于这个地址是物理地址,cpu要从这个物理地址取指执行,就需要能访问这个地址。由于在mips体系中,32位虚拟地址空间中,kseg0(0x80000000-0x9fffffff)是cache模式,kseg1(0xa0000000-0xbfffffff)是uncache模式。这个时候cache还没有初始化,因此需要使用uncache模式访问这个地址。因此实际访问的虚拟地址就是0xbfc00000,而64位虚拟地址的时候,0x9800000000000000开始的地址是cache模式的,0x9000000000000000开始的地址是uncache模式的,因此为了在64位模式下能访问1fc00000这个地址,就需要使用0x900000001fc00000这个虚拟地址。CPU上电执行的第一个虚拟地址就是这个地址。下图是flash中存储fd文件的位置。
由于在这个地址上存储的第一条指令就是跳转指令,跳转到Sec代码的起始处开始执行。即跳转到0x980000001fc10000这个地址。这是sec阶段代码的起始地址。位于start.s中的汇编代码,在这里主要做了对cpu的初始化和初始化cpu的串口。然后开始锁cache,这是为了早点进入C环境,将Cache作为临时的内存使用,这是uefi的一个特点,当然这里也可以不锁,那就等内存初始化好之后,再跳到内存中执行。这里锁cache的好处在于,锁定cache之后,就可以建立C环境,这样内存初始化的代码就可以用C来实现。否则只能用汇编代码。我们这里采用了锁cache的方式。锁cache的代码如下,锁定了2M的空间:
PRINTSTR("\r\nLock 2MB scache 0x9800000410000000 - 0x9800000410200000\r\n")
dli t0, 0x900000003ff00000
dli t1, 0xffffffffffe00000
sd t1, 0x248(t0)
dli t1, 0x8000000410000000
sd t1, 0x208(t0)
ld t1, 0x208(t0) //make sure config is ok sync
PRINTSTR("Lock Scache Done.\r\n")
锁定cache之后,就准备跳转到C环境中去执行。下面的代码就是跳转的过程,SecCoreStartupWithStack就是C环境的入口函数。位于SecMain.c中
call_centry:
dli __a0, 0x9800000410000000 # rambase
lui a1, 0x1 # size
daddu __sp, a0, a1 # stackbase
dsubu __sp, sp, 8
dla __v0, SecCoreStartupWithStack
jalr _v0
nop
跳转到VOID
EFIAPI
SecCoreStartupWithStack (UINT64 mbase, UINT64 msize)这个函数的入参mbase也就是上面的0x9800000410000000,为什么是这个地址还不确定。msize就是
lui a1, 0x1 # size
daddu __sp, a0, a1 # stackbase
dsubu __sp, sp, 8
这三条汇编执行设置的堆栈的大小0x10000(64k)的大小。这里2Mcache是如何使用的请看下面的图片
这个函数中主要初始化了sec阶段要使用的数据结构,和设置上图所设置的堆栈大小和地址的分配。接下来进入到函数
SecStartupPhase2 (&SecCoreData),这里SecCoreData的数据类型就是下面的结构
typedef struct _EFI_SEC_PEI_HAND_OFF {
UINT16 DataSize;
VOID *BootFirmwareVolumeBase;
UINTN BootFirmwareVolumeSize;
VOID *TemporaryRamBase;
UINTN TemporaryRamSize;
VOID *PeiTemporaryRamBase;
UINTN PeiTemporaryRamSize;
VOID *StackBase;
UINTN StackSize;
} EFI_SEC_PEI_HAND_OFF;
在上面的函数中已经初始化。在这个函数中主要主要的工作就是FindAndReportEntryPoint (&BootFv, &PeiCoreEntryPoint);这个函数来查找Pei阶段的入口点。PeiCoreEntryPoin这个值就是一个64位的虚拟地址,这个地址就是Pei阶段的Entry point函数的入口地址,然后通过(*PeiCoreEntryPoint) (SecCoreData, (EFI_PEI_PPI_DESCRIPTOR *)&mPrivateDispatchTable); 这段代码跳转到Pei阶段。跳转的函数位于MdePkg/Library/PeiCoreEntryPoint/PeiCoreEntryPoint.c中的VOID
EFIAPI
_ModuleEntryPoint(
IN CONST EFI_SEC_PEI_HAND_OFF *SecCoreData,
IN CONST EFI_PEI_PPI_DESCRIPTOR *PpiList
)函数,这个函数又调用ProcessModuleEntryPointList (SecCoreData, PpiList, NULL);函数,这个函数位于AutoGen.c中,这个文件是脚本生成的,在这个函数中调用了PeiCore入口函数
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阶段。
Pei:
从Sec阶段中直接查找到PeiCore的位置,并跳转到入口函数开始执行,Pei阶段的入口地址函数位于MdeModulePkg/Core/Pei/PeiMain/PeiMain.c中的PeiCore(这部分代码位于PeiCore.efi中,现在mem没有初始化,还在cache中执行,后续内存初始化之后,还会再执行一遍。)
VOID
EFIAPI
PeiCore (
IN CONST EFI_SEC_PEI_HAND_OFF *SecCoreDataPtr,
IN CONST EFI_PEI_PPI_DESCRIPTOR *PpiList,
IN VOID *Data
)
这个函数有三个入参,Sec阶段执行跳转的函数只传了两个,第三个参数为NULL。
Pei阶段使用的系统一些服务的函数都位于下面这个表中。
EFI_PEI_SERVICES gPs = {
{
PEI_SERVICES_SIGNATURE,
PEI_SERVICES_REVISION,
sizeof (EFI_PEI_SERVICES),
0,
0
},
PeiInstallPpi,
PeiReInstallPpi,
PeiLocatePpi,
PeiNotifyPpi,
PeiGetBootMode,
PeiSetBootMode,
PeiGetHobList,
PeiCreateHob,
PeiFfsFindNextVolume,
PeiFfsFindNextFile,
PeiFfsFindSectionData,
PeiInstallPeiMemory,
PeiAllocatePages,
PeiAllocatePool,
(EFI_PEI_COPY_MEM)CopyMem,
(EFI_PEI_SET_MEM)SetMem,
PeiReportStatusCode,
PeiResetSystem,
&gPeiDefaultCpuIoPpi,
&gPeiDefaultPciCfg2Ppi,
PeiFfsFindFileByName,
PeiFfsGetFileInfo,
PeiFfsGetVolumeInfo,
PeiRegisterForShadow,
PeiFfsFindSectionData3,
PeiFfsGetFileInfo2,
PeiResetSystem2
};
Pei阶段几个专业的名词的作用:
PEI Core–T在Pei阶段负责Dispatch PeiM和提供系统服务的函数
•PEIM–Pei阶段的各个模块(这里包含四个.efi文件,这个文件是由下面的四个inf文件生成的
INF MdeModulePkg/Universal/PCD/Pei/Pcd.inf
INF IntelFrameworkModulePkg/Universal/StatusCode/Pei/StatusCodePei.inf
INF MdeModulePkg/Core/DxeIplPeim/DxeIpl.inf
INF $(PLATFORM_DIRECTORY)/PlatformPei/PlatformPei.inf)
•PPI–(Peim到Peim之间相互调用的接口函数)
•PEI Dispatcher–(寻找个Dispatch Peim的核心函数)
•PEI Services–(Pei阶段为Peim提供服务的函数)
在函数PeiCore中,前期主要做了一些初始化的操作,具体的操作如下:
//初始化系统服务表
CopyMem (&PrivateData.ServiceTableShadow, &gPs, sizeof (gPs));
//设置全局变量系统服务表的指针指向上面的表
SetPeiServicesTablePointer ((CONST EFI_PEI_SERVICES **)&PrivateData.Ps);
// Initialize PEI Core Services
InitializeMemoryServices (&PrivateData, SecCoreData, OldCoreData);
// init Ppi Service
InitializePpiServices (&PrivateData, OldCoreData);
//init SecurityService
InitializeSecurityServices (&PrivateData.Ps, OldCoreData);
InitializeDispatcherData (&PrivateData, OldCoreData, SecCoreData);
InitializeImageServices (&PrivateData, OldCoreData);
//Dispatch Peim
PeiDispatcher (SecCoreData, &PrivateData);在这个函数中开始查找PeiM并执行,我们这里一共有四个,执行的顺序是PcdPeim.efi、StatusCodePei.efi、PlatformPei.efi(初始化内存,在这里执行,内存初始化之后还要再执行一次PeiCore)、PeiCore.efi(从内存中执行)、DxeIpl.efi(在这个模块里将Pei阶段向Dxe阶段跳转的函数注册进去)
下面介绍一下Pei阶段的四个驱动的主要作用:
PcdPeim.efi主要用来注册pcd(platform configuration data)处理函数,通过其getpcd函数可以获取在编译阶段定义的全局配置值(比如file volume 要加载的位置/大小等信息);具体的实现请看PcdPeimInit这个函数。
StatusCodePei.efi函数主要完用于打印报告uefi运行状态,函数实现请看PeiStatusCodeDriverEntry函数
PlatformPei.efi是我们的mips平台初始化(窗口配置,ht配置,ddr配置以及将DXE从flash拷贝到ddr中);
DxeIpl.efi主要是初始化Dxe阶段需要的数据结构和注册跳转到Dxe阶段的函数DxeLoadCore。
在DxeLoadCore函数中,首先进行启动模式的判断,UEFI中是支持S3启动的。然后开始组建传给Dxe阶段需要的HOB表,然后开始查找DxeCore阶段的入口函数,这个函数是通过DxeIplFindDxeCore ()实现的,找到之后将Dxe阶段的代码load到内存的一个位置。然后通过函数HandOffToDxeCore (DxeCoreEntryPoint, HobList);将控制权交给Dxe阶段。在这个函数里面重新分配了Dxe阶段需要运行的堆栈的大小和位置。然后通过函数SwitchStack实现的指令的跳转。(这个函数在具体的平台实现是不同的)