UEFI sec阶段的实现

本文的实现是在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阶段就都可以正常的运行了。

 

 

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值