本文的实现是在MIPS平台龙芯架构实现的,在其他平台上实现方式可能不一样,但是大体上都是相同的。
首先Sec阶段是UEFI最早的一个阶段,CPU上电后从固定地址取指开始执行。每种架构的CPU上电后第一条指令的地址是不同的,龙芯的CPU上电执行的第一条指令的32位地址是0x1fc00000,用cache的64位地址来访问就是0xffffffff9fc00000的地址。这里为什么上电后就能够使用cache来访问呢?是因为Cache是硬件初始化的,上电后cache就可以使用。这个地址我们在UEFI的fdf文件中写死了上电需要执行的第一条指令。
(1)上电执行的第一条指令
其实现如下:
DEFINE VARIABLE_OFFSET = 0x00000000
DEFINE VARIABLE_SIZE = 0x6000
$(VARIABLE_OFFSET)|$(VARIABLE_SIZE)
DATA = {
#jmp to 0xfffffff9fc10000
0xc1, 0x9f, 0x1f, 0x3c, 0x08, 0x00, 0xe0, 0x03,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
}
上面的DATA数据经过CPU翻译之后的指令就是“lui ra 9fc1” "jr ra",lui指令就是将数据加载到32位地址的高16位,这条指令执行完之后ra的值就是0x9fc10000,然后下面的jr ra就是跳转到ra 寄存器中的地址处开始执行代码
(2)flash布局
这段数据被编译后放到了flash中的开头,也就是flash的头两天指令,flash的起始地址也就是上电的32位地址0x9fc00000,我们使用的是2M大小的flash。其他部分的代码依次从这个地址之后开始存放。这里我们的布局如下:
0x9fc00000-----0x9fc10000 (64k)存放的UEFI中的variable 和 event log和Fwt等信息
0x9fc10000-----0x9fc20000(64k)存放的是sec阶段的代码,这里面有可能是.bin也有可能是sec.fv,后面后介绍两种方式的区别。
0x9fc20000-----0x9fca0000(512k)存放的是pei阶段的代码,这里就是pei.fv
0x9fca0000-----0x9fce00000(2M-512k-128k)存放的是Dxe阶段的代码,也就是Dxe.fv
(3)sec阶段的流程
上电后跳转到0x9fc10000的位置刚好是SecMain.bin或者SecMain.fv的位置。刚好跳过了flash中前面预留的64k大小的空间。到这个地址后执行的代码是由前期的汇编代码编译后的指令,汇编代码主要是对CPU的初始化。如果这个位置是存放的是SecMain.bin,那么这里就直接就是代码段,就是汇编代码的起始代码,CPU可以直接拿来执行,如果是SecMain.fv那么这个地方存放的就是fv的头,代码段在其后偏移0x1094个字节开始的地址才是真正的代码段。
到这里我们首先说明下SecMain.bin和SecMain.fv的区别:
SecMain.bin:
首先sec.bin是bin文件,bin文件就是存放的代码段,就是CPU可以执行的代码,其没有任何的头,(可以使用hexdump sec.bin > sec.log查看二进制)。那么如何让sec阶段的代码编译成.bin文件呢?这里需要修改uefi的编译规则文件build_rule.template这个文件,在里面定义sec阶段的编译规则:
[Assembly-Code-File.SEC.MIPS64EL]
<InputFile.GCC, InputFile.GCCLD>
?.S, ?.s
<ExtraDependency>
$(MAKE_FILE)
<OutputFile>
$(OUTPUT_DIR)(+)${s_dir}(+)${s_base}.obj
<Command.GCC, Command.GCCLD, Command.RVCT>
"$(ASM)" $(ASM_FLAGS) -o ${dst} $(INC) ${src};
[Binary.SEC.MIPS64EL]
<InputFile>
*.dll
<OutputFile>
$(DEBUG_DIR)(+)$(MODULE_NAME).bin
<Command>
"$(OBJCOPY)" -O binary $(DEBUG_DIR)(+)$(MODULE_NAME).dll $(DEBUG_DIR)(+)$(MODULE_NAME).bin
按照上面的编译规则修改后,就可以在编译后的目录中生成SecMain.bin文件,将这个文件放到flash的0x9fc10000的地址就可以了,跳转过来后直接开始运行这里的代码。到这里是不是很想知道SecMain.bin是如何放到flash中的0x9fc10000的地址呢?其实这都是uefi的fdf文件实现的,其实现如下:
[FD.LS3A30007A]
BaseAddress = $(FD_BASE_ADDRESS)
Size = $(FD_SIZE)
ErasePolarity = 1
BlockSize = $(BLOCK_SIZE)
NumBlocks = $(FD_BLOCKS)
!include Loongson.fdf.inc
$(SECFV_OFFSET)|$(SECFV_SIZE)
FILE=$(OUTPUT_DIRECTORY)/$(TARGET)_$(TOOL_CHAIN_TAG)/$(ARCH)/$(PLATFORM_DIRECTORY)Sec/SecMain/DEBUG/SecMain.bin
这里面SECFV_OFFSET=0x10000,SECFV_SIZE=0x10000,这个SECFV_OFFSET就是基于地址0x9fc00000这个地址开始偏移的。这样就把secMain.bin放到了0x9fc10000这个地址。
这里需要注意在sec阶段的前期是汇编代码实现的,在汇编代码中是不涉及到flash中的物理地址的,每条指令的地址都是与位置无关的。然后到后期cache锁定之后,使用cache as ram使用,这时开始建立C环境,然后跳转到C代码中去执行。这里面实现跳转的代码和对应的C代码如下:
汇编代码:
PRINTSTR("Copy Sec&PEI code to cache.\r\n")
__dli a0, SEC_FLASH_CODE_START
__dli a1, SEC_CACHE_CODE_START
__dli a2, SEC_AND_PEI_CODE_SIZE
1:
__ld a3, 0(a0)
__sd a3, 0(a1)
__ld a3, 8(a0)
__sd a3, 8(a1)
__ld a3, 16(a0)
__sd a3, 16(a1)
__ld a3, 24(a0)
__sd a3, 24(a1)
__ld a3, 32(a0)
__sd a3, 32(a1)
__ld a3, 40(a0)
__sd a3, 40(a1)
__ld a3, 48(a0)
__sd a3, 48(a1)
__ld a3, 56(a0)
__sd a3, 56(a1)
__dsubu a2, a2, 64
__daddu a0, 64
__daddu a1, 64
__bnez a2, 1b
__nop
__sync
call_centry:
__dli___a0, 0x9800000110000000 # rambase
__lui___a1, 0x1 # size
__daddu_sp, a0, a1 # stackbase
__move__a1, sp # stackbase
__dsubu_sp, sp, 8
__dli___a0, 0x9800000110110000 # PeiFvBase
__dla___ra, SecCoreStartupWithStack
__jr____ra
__nop
C代码:
VOID
EFIAPI
SecCoreStartupWithStack(
IN EFI_FIRMWARE_VOLUME_HEADER *BootFv,
IN VOID *TopOfCurrentStack
)
{
EFI_SEC_PEI_HAND_OFF SecCoreData;
EFI_FIRMWARE_VOLUME_HEADER *BootPeiFv = ((EFI_FIRMWARE_VOLUME_HEADER *)BootFv);
DbgPrint(EFI_D_INFO, "Entering C environment\n");
ProcessLibraryConstructorList(NULL, NULL);
上面的代码就是从flash中将SecMain.bin和PeiFv拷贝到cache中,然后跳转到cache中去运行。那么这里面有一点需要注意,cache当做ram来使用了那么锁定的2M的cache的地址是什么呢?我们的代码拷贝到cache中了,那么在cache里面执行的指令的地址就变成了cache的地址,每条指令编译的地址和拷贝后存放的地址是怎么对应的呢?
首先我们锁定的cache的地址是从 0x9800000110000000 - 0x9800000110200000这2M的地址作为ram来使用,后面内存初始化好之后解锁cache之后,这段地址的数据就会刷新到对应的内存(高端内存)上。建立堆栈的位置是0x9800000110000000至
0x9800000110010000的64k的大小。然而从flash中拷贝过来的代码是放在了0x9800000110100000即2M中的后1M开始存放,sec代码占0x10000(64K)pei占(0x80000)512K的大小,所以从0x9800000110100000到0x9800000110190000的位置都是存放的代码。现在假设函数SecCoreStartupWithStack是距离Sec阶段第一条汇编指令0x100的位置才是这个函数的实际地址,那么现在在cache中这个函数的虚拟地址就是0x9800000110100100,那么上面汇编代码中
dla___ra, SecCoreStartupWithStack
jr ra
为什么就能直接跳转到0x9800000110100100这个地址呢?那是因为我们在SecMain.inf中定义好了代码编译的起始地址,这个起始就是就是代码拷贝过来的起始地址0x9800000110100000。其实现如下:
[Defines]
INF_VERSION = 0x00010005
BASE_NAME = SecMain
FILE_GUID = 1f488fc5-adba-4e8d-8916-dddc2772b410
MODULE_TYPE = SEC
VERSION_STRING = 1.0
SECMAIN_CODE_BASE = 0x9800000110100000
[BuildOptions.MIPS64EL]
*_GCC44_MIPS64EL_DLINK_FLAGS == --gc-sections -u $(IMAGE_ENTRY_POINT) -e $(IMAGE_ENTRY_POINT) -Map $(DEST_DIR_DEBUG)/$(BASE_NAME).map -melf64ltsmip --defsym=PECOFF_HEADER_SIZE=$(SECMAIN_CODE_BASE)
因此这里就完全吻合了。这样跳转过去后每一条指令的地址和编译链接的地址都是一样的。这样才能够稳定的运行。
SecMain.fv:
Fv就是firmvarm volume(简称固件卷)在编译生成的fd中,包含有PeiFv/DxeFv每个Fv中含有自己的Ffs,每个Ffs就是将每一个.efi封装一下,封装成ffs,他们有自己的格式。这里不做详细介绍。具体的可以看uefi build部分的spec里面有详细的介绍。
现在将Sec阶段编译成Fv的方式放到了flash中,那么就涉及到的问题就不只是代码的问题。在其他模块load一个.efi的时候,都是要先找到Fv然后去找Ffs然后去load每一个.efi,在load过程中还要做基于load基地址的relocation。但是在Sec阶段是没有这样的代码的,是找不大Fv地址,也找不到其中的SecMain.efi,还不能做relocation。因此要使用Fv就要解决这些问题。
下面看看如何编译出SecMain.fv,还是修改编译规则的文件:
[Assembly-Code-File.SEC.MIPS64EL]
<InputFile.GCC, InputFile.GCCLD>
?.S, ?.s
<ExtraDependency>
$(MAKE_FILE)
<OutputFile>
$(OUTPUT_DIR)(+)${s_dir}(+)${s_base}.obj
<Command.GCC, Command.GCCLD, Command.RVCT>
"$(ASM)" $(ASM_FLAGS) -o ${dst} $(INC) ${src};
然后修改fdf文件,如下所示:
[FD.LS3A30007A]
BaseAddress = $(FD_BASE_ADDRESS)
Size = $(FD_SIZE)
ErasePolarity = 1
BlockSize = $(BLOCK_SIZE)
NumBlocks = $(FD_BLOCKS)
!include Loongson.fdf.inc
$(SECFV_OFFSET)|$(SECFV_SIZE)
FV = SECFV
[FV.SECFV]
FvNameGuid = 763BED0D-DE9F-48F5-81F1-3E90E1B1A015
FvBaseAddress = $(FLASH_CODE_SECFV_BASE_ADDRESS)
BlockSize = 0x1000
FvAlignment = 16
ERASE_POLARITY = 1
MEMORY_MAPPED = TRUE
STICKY_WRITE = TRUE
LOCK_CAP = TRUE
LOCK_STATUS = TRUE
WRITE_DISABLED_CAP = TRUE
WRITE_ENABLED_CAP = TRUE
WRITE_STATUS = TRUE
WRITE_LOCK_CAP = TRUE
WRITE_LOCK_STATUS = TRUE
READ_DISABLED_CAP = TRUE
READ_ENABLED_CAP = TRUE
READ_STATUS = TRUE
READ_LOCK_CAP = TRUE
READ_LOCK_STATUS = TRUE
INF LoongsonPlatFormPkg/Sec/SecMain.inf
这样flash中0x9fc10000这个位置就放的是SecFv,SecMain.bin就不需要了。
第一个问题:如何找到SecFv的entry point的,是如何跳转到代码段执行的?
这样我们上电后跳转到0x9fc10000这个位置后,就找到了Fv的头,我们在编译的时候,将头这部分信息做了处理,找到了Fv的entrypoint的之后,将相对这个地址的偏移找到到,然后将Fv头的前16个字节封装了一个跳转指令跳转到entrypoint地点去执行。这些工作都是在BaseTools下面的GenFv下面生成Fv的编译工具的源代码里面做的,也就是在编译的时候封装Fv的时候就做好了。这样才能找到SecMain.efi的entrypoint 去执行代码。
第二个问题:如何解决不需要reloaction就能运行的问题?
这个地方和上面的额SecMain.bin的实现是一样的,就是在编译的时候就已经指定好了链接的起始地址,也就是代码段的第一条指令的地址,下面的代码就是编译的实现:
[Defines]
INF_VERSION = 0x00010005
BASE_NAME = SecMain
FILE_GUID = 1f488fc5-adba-4e8d-8916-dddc2772b410
MODULE_TYPE = SEC
VERSION_STRING = 1.0
SECMAIN_CODE_BASE = 0x9800000110100000
[BuildOptions.MIPS]
*_GCC44_MIPS_DLINK_FLAGS == --gc-sections -u $(IMAGE_ENTRY_POINT) -e $(IMAGE_ENTRY_POINT) -Map $(DEST_DIR_DEBUG)/$(BASE_NAME).map -melf64ltsmip --defsym=PECOFF_HEADER_SIZE=$(SECMAIN_CODE_BASE)
这样编译后的第一条指令的地址就是0x9800000110100000,所以我们使用Fv的时候,锁定cache之后,会将Sec阶段的代码段和PEI整个的Fv都拷贝到cache中,然后跳转到cache中去执行。这里面需要注意,拷贝Sec阶段的时候,不能将整个Sec的Fv都拷贝出去,因为我们放的位置是从cache地址0x9800000110100000这个地址开始放代码,为什么这个位置一定要放代码段而不能放Fv的头呢,因为我们在Sec阶段的编译过程中指定了代码编译的其实地点就是这个地址。如果这里面放了Fv的头,那么实际的代码地址就比编译的代码的地址向上偏移了Fv头的大小的地址,当程序跳转到绝对的虚拟地址的手就会出现问题。
这段代码是写在SecMain.inf文件中的。这样编译出来的SecMain.dll文件反汇编后第一条指令的地址就是0x9800000110100000。
注意我们使用Fv后,拷贝Sec阶段的代码和是从偏移过Fv的头的位置开始放的,这个地址可能不是一个8字节对齐的地址。就不能使用64位操作的指令,所以这里拷贝的代码和上面使用SecMain.bin的时候有一些不一样。下面是代码:
#define SEC_FLASH_FV_BASE 0xffffffff9fc10000
#define PEI_FLASH_FV_BASE 0xffffffff9fc20000
#define SEC_CACHE_CODE_START 0x9800000110100000
#define PEI_CACHE_FV_BASE 0x9800000110110000
#define SEC_CODE_SIZE 0x10000
#define PEI_FV_SIZE 0x80000
PRINTSTR("Copy Sec code to SCache-As-Ram.\r\n")
__dli a0, SEC_FLASH_CODE_START
__dli a1, SEC_CACHE_CODE_START
__dli a2, SEC_CODE_SIZE
1:
__lw a3, 0(a0)
__sw a3, 0(a1)
__lw a3, 0x4(a0)
__sw a3, 0x4(a1)
__lw a3, 0x8(a0)
__sw a3, 0x8(a1)
__lw a3, 0xc(a0)
__sw a3, 0xc(a1)
__lw a3, 0x10(a0)
__sw a3, 0x10(a1)
__lw a3, 0x14(a0)
__sw a3, 0x14(a1)
__lw a3, 0x18(a0)
__sw a3, 0x18(a1)
__lw a3, 0x1c(a0)
__sw a3, 0x1c(a1)
__lw a3, 0x20(a0)
__sw a3, 0x20(a1)
__lw a3, 0x24(a0)
__sw a3, 0x24(a1)
__lw a3, 0x28(a0)
__sw a3, 0x28(a1)
__lw a3, 0x2c(a0)
__sw a3, 0x2c(a1)
__lw a3, 0x30(a0)
__sw a3, 0x30(a1)
__lw a3, 0x34(a0)
__sw a3, 0x34(a1)
__lw a3, 0x38(a0)
__sw a3, 0x38(a1)
__lw a3, 0x3c(a0)
__sw a3, 0x3c(a1)
__dsubu a2, a2, 0x40
__daddu a0, 0x40
__daddu a1, 0x40
__bnez a2, 1b
__nop
__sync
PRINTSTR("Copy PEI code to SCache-As-Ram.\r\n")
__dli a0, PEI_FLASH_FV_BASE
__dli a1, PEI_CACHE_FV_BASE
__dli a2, PEI_FV_SIZE
1:
__ld a3, 0(a0)
__sd a3, 0(a1)
__ld a3, 8(a0)
__sd a3, 8(a1)
__ld a3, 16(a0)
__sd a3, 16(a1)
__ld a3, 24(a0)
__sd a3, 24(a1)
__ld a3, 32(a0)
__sd a3, 32(a1)
__ld a3, 40(a0)
__sd a3, 40(a1)
__ld a3, 48(a0)
__sd a3, 48(a1)
__ld a3, 56(a0)
__sd a3, 56(a1)
__dsubu a2, a2, 0x40
__daddu a0, 0x40
__daddu a1, 0x40
__bnez a2, 1b
__nop
__sync
这里注意有一个坑,SEC_FLASH_FV_BASE 0xffffffff9fc10000这个地址要使用高32位为ffffffff的地址,使用90或者98开始的地址会出问题,因为在这个时候地址窗口还没有配置,使用64地址窗口来访问flash会死机。这里面拷贝Sec阶段的代码使用的是lw sw指令,这个指令操作的都是32bit的地址。4字节拷贝的,然而拷贝pei的代码,采用的是ld sd操作的都是64bit的地址。这里为什么Sec要使用32位的呢?那就是Sec的代码段起始地址是基于Fv头的偏移0x1094个字节的地址,这样地址并不是8字节对齐,因此只能用32bit的地址来访问。
按照上面的配置之后,整个Sec阶段就都可以正常的运行了。