edk2下的FMMT分析与使用

导论

FD的结构

        UEFI中的FV和Section是两种不同的文件段类型,它们用于描述文件的内容和格式。FV是固件卷,它是UEFI固件的最大的组织单元,它可以包含多个FFS文件。FFS是固件文件系统,它是UEFI固件的最小的组织单元,它可以包含多个Section。Section是文件段,它是UEFI固件的最小的数据单元,它可以有不同的类型,比如PE32、RAW、GUID等。

 BIOSTree       

        FMMT是如何操作一个FD文件的,其主要就是将一个Fd文件生成为一颗如下图的BIOSTree,通过把Fd文件中的所有内容,如Fv、Ffs、Section以树的节点形式排布,而每个节点内部又存储了相应的内容,如FvNode、FfsNode等,这样就可以操作到想要操作的位置,以及内容。

 FvNode

 FfsNode

 FreeSpaceNode

SectionNode 

 BinaryNode

含义与功能

        FMMT(Firmware Module Management Tools),从名字就可以看出这个工具的作用在于管理固件模组,其位置处于edk2/BaseTools/Source/Python/FMMT。

        我们知道,FD文件是由数个FV构成的,而FV里面又包含了FFS。而FMMT的作用就是对fd文件进行ffs级别的编辑,例如:

        增:向FV文件中增加FFS文件

        删:将FV文件中某一FFS文件删除

        改:将FV文件中某一FFS文件替换为另一个FFS文件

        另外,还有解析FD、FV、FFS文件,即可以利用FMMT工具看到FD文件的内部架构

        提取:将某一FFS文件从FV文件中单独提取出来

源码流程

DeleteFfs

AddFfs

ReplaceFfs

源码解析

ViewFile

        从main函数进入,首先实例化一个FMMT类,然后调用FMMT类下的View方法。

        View方法内需要先对文件进行类型判断,以此给ROOT_TYPE赋值,并将ROOT_TYPE传给ViewFile,方便ViewFile进行对应操作。

        打开输入的文件,并实例化FMMTParser类。

        调用FMMTParser类中的方法ParserFromRoot,创建数据树。

        在ParserFromRoot实现的主要功能就是不停的调用ParserEntry类下的DataParser方法,例如先对树中的FD进行解析,之后对孩子节点FV进行解析,之后对孩子节点FFs进行解析,最后是孩子节点Section进行解析。

Factory和Product

        在ParserEntry类下,其实就是根据输入的类型不停的创建对应的工厂类与产品类,如果输入的是FV,则创建FV工厂,对应的创建FV产品,如果输入的是FFs,则创建FFs工厂,对应的创建FFs产品,执行步骤如图1、2、3.

        对应的工厂会创建对应的产品。

        从FD工厂类的产品类开始看,首先需要从FD中获取所有的FV的属性信息,此处则是调用了FdProduct类下的GetFvFromFd方法,将Fd下的所有FV信息存储到了Fd_Struct这样一个存储结构中。

        在介绍GetFvFromFd方法前,需要介绍一下FV相关的先验知识,我们知道每个固件卷都是有固件卷头的,而在头部中都会有GUID这样一个唯一标识属性,而这个GUID在规范中其实早已定义好了,总共有三类:EFI_FIRMWARE_FILE_SYSTEM2_GUID、EFI_FIRMWARE_FILE_SYSTEM3_GUID、EFI_SYSTEM_NVDATA_FV_GUID,其中第三个NVDATA其实就是存储非易失性变量的固件卷,既然所有的固件卷都是有固定GUID头的,那么从Fd中寻找所有Fv就只要寻找固定GUID就行了。

        此处以EFI_FIRMWARE_FILE_SYSTEM2_GUID为例,就是从cur_index开始遍历data_size。

        如果在whole_data中找到了EFI_FIRMWARE_FILE_SYSTEM2_GUID,那么先将target_index定位为GUID的第一个值的位置,并且根据target_index+24,定位到signature位置,校对signature是否为_FVH,两者都符合,则将类型、起始位置、大小这三个属性append到Fd_Struct中,然后将cur_index指向这个固件卷的末尾,重复遍历找寻操作。

        此处需要了解一下FD_Struct的结构,如图可以看到Fd_Struct其实是一个列表,表内有很多元素,单个元素由[类型、起始位置、大小]构成

        同样的,对EFI_FIRMWARE_FILE_SYSTEM3_GUID、EFI_SYSTEM_NVDATA_FV_GUID也进行这样的操作,最终将所有FV及其信息都添加入Fd_Struct中。

        最后,由于Fd_Struct中的信息是无序的,因此还需要按照FV的起始位置进行排序,排序后,查看列表中是否存在相同的FV,这种情况是为了防止嵌套类型的FV被放入到Fd_Struct中,毕竟,现阶段的Fd_Struct中的FV都是第一层级的FV。

        完成GetFvFromFd方法后,此时Fd_Struct中已经有了所有第一层级的所有FV的信息了,接下来需要利用这些信息来构建数据树。

        这一块应该就是为了将FV之前的二进制数据先摘出来,并将这些数据保存到一个BinaryNode中,具体步骤为实例化一个BIOSTREE,然后创建一个BinaryNode,随后填充该BinaryNode的信息,最后将该BinaryNode添加进整个树中。

        做完上一步后,接下来便可以将Fd_Struct中的FV添加到BIOSTREE中了,具体操作和上一步类似,先实例化一个BIOSTREE,然后创建一个FvNode,填充该FvNode的信息,最后将该FvNode添加进整棵树中。

        接下来就是循环操作,将Fd_Struct中的所有FV都创建好节点,并添加进树中。值得一提的是,在最后一个FV后也要进行判断,是否后面还有二进制数据,若有,也要生成一个BinaryNode,并添加进树中。到这,FD产品就算创建完成了,接下来就是创建FV产品。

        在了解了FD产品的创建后,其实FV产品的创建就如出一辙了,无非是将之前的Binary判断改成了FreeSpace判断,此处可以看到判断条件为数据大小减去偏移量小于24,则代表该段为FreeSpace,这个依据是PI规范中定义的,FFs是可以只有头部,而没有数据的,而头部刚好占24个字节,如果判断该段为FreeSpace,那么同样的实例化一个BIOSTREE,创建一个FreeSpaceNode节点,填充该节点,将该节点加入树。

        如果判断条件大于24,那么同样的,实例化BIOSTREE,创建FFs节点,但是,在填充FFs节点之前,需要进行判断,判断该段是否是一个PAD,如果是PAD,则该节点的Type设置为FFS_PAD,否则设置为FFS_TREE,最后,将FFs节点添加进树。

        接下来是创建FFs产品,同样的创建一个SectionNode,随后实例化一个BIOSTREE,接下来对SectionNode进行填充,首先要判断是否需要填充pad,接下来判断该Section的头是否0x02、0x14、0x15,这三个头需要使用ExtHeader,最后的0x19为Raw类型的Section,也要进行处理。最后同样的将处理好的SectionNode插入树中。

        最后就是Section产品了,Section有很多类型,主要如下图所示

        因此,Section产品的前半部分主要是在判断该Section的类型,并作出相应处理,其中的0x01,0x02,0x03都是压缩类型,所以如果是这三种SECTION,则需要进行解压缩,需要调用ParserSection,并且如0x02的GUID_SECTION,由于还涉及到特定的GUID,因此还需要调用GUIDTool来解析GUID,前面在Fd产品中还提到过特意将嵌套式的FV从FdStruct中移除的操作,而这就是为了在Section产品中做处理,如果判断当下的Section为Fv_Section,那么就需要创建一个FvNode,并填充它,最后将其加入树。

        Section产品中的ParserSection其实和FFs产品中的ParserData一样,其实到这就很好理解Section在fd文件中的存储方式,即:如果知识普通的、未压缩的Section,直接到FFs产品中就已经解析完了,但是如果式压缩后的Section,例如0x01、0x02、0x03类型的Section,则需要特殊处理后才可解析,而解析方式就和普通解析一样,这也就对应了Section产品中的Parser为何和FFs中的Parser一摸一样,而对于Fv_Section,则同样需要特殊处理。

Log Output和Data Encapsulation

        最后实际就是获取树的结构信息,并解析树的内容即可,并使用 GetFormatter("") 打印日志信息。如果提供了 layoutfile,则根据文件扩展名或提供的格式,生成布局文件,并将树的信息以及解析后的数据写入该布局文件。最后,如果提供了 outputfile,则进行数据封装。它会调用 FmmtParser.Encapsulation`方法来将数据封装,并将封装后的数据写入输出文件中。

实际效果

DeleteFfs

        整体DeleteFfs方法如图,前两步和ViewFile一致,读取文件,解析数据并生成数据树。从第三步开始,才是Delete正式开始,首先FindNode,根据你输入的TargetFfs的name找到其在树中的节点并加入Findlist,随后还需要检查其父Fv是否和你提供的一致,如果不一致,则从Findlist中移除。随后对Findlist中的每一个结果进行FvHandle.DeleteFfs操作。

FvHandler.DeleteFfs

  1. 开始删除处理:代码首先记录调试信息,指示删除操作开始。

  2. 获取要删除的Ffs节点(Delete_Ffs)以及其父Fv节点(Delete_Fv)。

  3. 计算要删除的Ffs节点占用的空间,包括Ffs数据的大小和填充数据的长度,并将其存储在 Add_Free_Space 中。

  4. 如果Fv的父节点(即Delete_Fv)具有剩余的空闲空间(Delete_Fv.Data.Free_Space 不为零),则根据特定规则合并新的空闲空间:

    • 如果Fv是一个Section Fv(SEC_FV_TREE),则需要重新计算新的空闲空间以保持与BlockSize的对齐。然后,将多余的空闲空间保存在 self.Remain_New_Free_Space 中,并将其放入最后一个子节点的数据中,使用0xff填充。

    • 如果Fv是第一级的Fv,新的空闲空间将与原有的空闲空间合并。

  5. 如果Ffs的父Fv没有剩余的空闲空间,将创建一个新的空闲空间节点来存储剩余的空闲空间,同时将0xff填充到新的空闲空间节点中。

  6. 更新Fv的子节点和相关属性:

    • 从父Fv的子节点列表中移除要删除的Ffs节点。

    • 更新Fv的大小(Delete_Fv.Data.Header.FvLength)和空闲空间属性。

    • 修改Fv的扩展属性。

    • 更新Fv的校验和。

  7. 重新压缩Fv节点以更新所有相关节点数据。

  8. Status 设置为 True,表示删除操作成功。

  9. 记录调试信息,指示删除操作完成。

  10. 最后,返回 Status,表示操作的成功或失败。

        首先计算出free space,如果Fv的父节点(即Delete_Fv)具有剩余的空闲空间,则根据特定规则合并新的空闲空间:如果父Fv是一个Section Fv,那么需要在确保对齐的情况下重新计算free space,而其他的存储在Remain_New_Free_Space中的空间,并将其放入最后一个子节点的数据中,使用0xff填充。如果Fv第一级的Fv,新的空闲空间将与原有的空闲空间合并。

        如果Ffs的父Fv没有剩余的空闲空间,将创建一个新的空闲空间节点来存储剩余的空闲空间,同时将0xff填充到新的空闲空间节点中,此处也需要考虑是否是Section Fv,即考虑对齐问题。

        随后更新Fv的子节点和相关属性:从父Fv的子节点列表中移除要删除的Ffs节点。更新Fv的大小(Delete_Fv.Data.Header.FvLength)和空闲空间属性。修改Fv的扩展属性。更新Fv的校验和。重新压缩Fv节点以更新所有相关节点数据,最后同样的进行封装。

AddFfs

        前面的就都不说了,直接到Create new ffs Tree,首先读取你所输入的Ffs file,后面的操作就很熟悉了,从ViewFile可以知道,这就是在创建数据树操作,此处就是创建一个Ffs树,主要的Add操作还是在FvHandler.AddFfs中。

FvHandler.AddFfs

  1. 开始添加处理:代码首先记录调试信息,指示添加操作开始。

  2. 计算新Ffs节点的填充数据(PadData),并设置为0xff的填充,以确保对齐到FFS_COMMON_ALIGNMENT。

  3. 检查要添加的Ffs节点的目标Fv节点的类型(TargetFfs.type)是否为FFS_FREE_SPACE,这里的TargetFfs其实是父Fv的最后一个Ffs节点,看是否是Free Space,如果是,那么就可以直接利用这块空间,则执行以下操作:

    • 获取目标Ffs节点的父Fv节点(TargetFv)。

    • 如果目标Fv节点的属性(TargetFv.Data.Header.Attributes)中包含 EFI_FVB2_ERASE_POLARITY 标志,则需要反转新Ffs节点的头部状态(State)。

    • 计算要添加的新Ffs节点和目标Ffs节点之间的空间差(TargetLen),如果差值小于0,表示目标Fv有足够的空闲空间,可以将部分空闲空间移动到新Ffs节点,并将空闲空间分割为新Ffs节点和新的空闲空间。

    • 如果差值等于0,表示目标Ffs与新Ffs大小刚刚好,此时只需将目标Ffs节点替换为新Ffs节点。

    • 如果差值大于0,表示目标Fv没有足够的空闲空间,需要从目标Fv的父Fv节点移动部分空闲空间到目标Fv或新Ffs节点。

    • 更新相关的Fv和Ffs节点属性,包括大小、空闲空间、扩展属性等。

    • 重新压缩目标Fv节点,以更新所有相关节点的数据。

    • Status 设置为 True,表示添加操作成功。

  4. 如果目标Ffs节点的类型不是FFS_FREE_SPACE,则说明此时没有FFS_FREE_SPACE可用了,需要向其父Fv拿一些空间。拿来的空间还需要判断是否足够多,如果多了,那就将多出的部分生成一个Free Space Node节点保存起来,同样,也需要更新相关的Fv和Ffs节点属性,包括大小、空闲空间、扩展属性等。

  5. 记录调试信息,指示添加操作完成。

  6. 最后,返回 Status,表示操作的成功或失败。

ReplaceFfs

Replace和前面的Add其实差不多,包括FindNode,Remove之类的,主要还是FvHandler.Replace才是主要实现。

FvHandler.Replace

  1. 开始替换处理:代码首先记录调试信息,指示替换操作开始。

  2. 获取目标Ffs节点的父Fv节点(TargetFv)。

  3. 如果目标Fv节点的属性(TargetFv.Data.Header.Attributes)中包含 EFI_FVB2_ERASE_POLARITY 标志,则需要反转新Ffs节点的头部状态(State)。

  4. 计算新Ffs节点的填充数据(PadData),并设置为0xff的填充,以确保对齐到FFS_COMMON_ALIGNMENT。

  5. 检查新Ffs节点的大小是否大于或等于目标Ffs节点的大小。如果是,表示新Ffs节点无法直接替换掉目标Ffs节点,因此需要执行以下操作:

    • 计算所需的空间差(Needed_Space),即新Ffs节点的大小和填充减去目标Ffs节点的大小和填充。

    • 如果目标Fv节点有足够的空闲空间(TargetFv.Data.Free_Space 大于或等于 Needed_Space),则将一部分空闲空间移动到新Ffs节点。然后,更新相关的Fv和Ffs节点属性,包括大小、空闲空间、扩展属性等。

    • 如果目标Fv没有足够的空闲空间,则需要从目标Fv的父Fv节点中移动一部分空闲空间到目标Fv或新Ffs节点中,以满足替换的需求。同样,需要更新相关的属性。

    • 最后,重新压缩目标Fv节点,以更新所有相关节点的数据,将 Status 设置为 True,表示替换操作成功。

  6. 如果新Ffs节点的大小小于目标Ffs节点的大小,表示新Ffs节点需要更少的空间,执行以下操作:

    • 计算新的空闲空间大小(New_Free_Space),即目标Ffs节点的大小减去新Ffs节点的大小。

    • 如果目标Fv节点已经具有空闲空间(TargetFv.Data.Free_Space 不为零),则将新的空闲空间合并到目标Fv节点中。如果目标Fv没有空闲空间,则需要创建一个新的空闲空间节点,并将其插入到目标Fv节点中,然后将新Ffs节点替换为目标Ffs节点。

    • 更新相关的Fv和Ffs节点属性,包括大小、空闲空间、扩展属性等。

    • 最后,重新压缩目标Fv节点,以更新所有相关节点的数据,将 Status 设置为 True,表示替换操作成功。

  7. 记录调试信息,指示替换操作完成。

  8. 最后,返回 Status,表示操作的成功或失败。

 使用

        在使用FMMT前,需要安装python工具,并且编译BaseTools

#在edk2目录下执行
make -C BaseTools

        编译成功后,还需要在edk2目录下执行edksetup脚本

#在edk2目录下执行
source edksetup.sh

         想要使用FMMT工具,需要先将edk2/BaseTools/Source/Python/下的FirmwareStorageFormat库移至FMMT目录下,这么做的原因是防止FMMT报找不到FirmwareStorageFormat库错误,该库为一个本地库。

        接下来,将需要操作的FD文件放置FMMT目录下,此处放置了OVMF.fd文件 

         接下来就可以使用FMMT工具了,切记,命令行也需要处于FMMT目录下才可执行FMMT命令,FMMT工具的使用命令如图,例如删除一个FFS文件

python3 FMMT.py -d OVMF.fd 763bed0d-de9f-48f5-81f1-3e90e1b1a015 SecMain  FD_Del.fd

        此处OVMF.fd为想要编辑的FD文件,763bed0d-de9f-48f5-81f1-3e90e1b1a015为OVMF.fd内的一个FVguid,而SecMain为该FV下的一个FFS名(也可以使用该FFS的GUID),FD_Del.fd为输出的新的FD文件名。

        命令中的FVguid与FFS名均可通过FMMT的parse命令知晓。

        最后生成的FD_Del.fd将会放在FMMT目录下。

实践

        原始FV_RECOVERY.fd文件中的drivers如图,本次操作的Ffs为EmuSimpleFileSystem与EmuBlockIo。

        删除FFS文件EmuBlockIo

        增加FFS文件EmuBlockIo

        将EmuBlockIo替换为EmuSimpleFileSystem 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值