Last time, we looked briefly at the standard firmware file system which is described in the UEFI Platform Initialization specification. This file system is optimized for use in firmware and provides fault-tolerance for writes. It also provides a standard format for delivery of PEI and DXE drivers.
This time we'll look a little closer at the standard firmware volume format. Every standard UEFI PI firmware volume begins with a header.
Why Have A Firmware Volume Header?
One of the first things you notice about the PI firmware volume formats is that there is a firmware volume header at all. When we talk about fault-tolerance in a flash file system there is always the possibility that one of the flash sectors gets wiped out. So, with this file format, if the first flash sector (containing the firmware volume header) is erased, there is no way to recover.
Why is this important? Well, if you remember our previous discussion, the UEFI PI firmware file system is designed to be fault-tolerant. When creating a file or updating an existing file, even should the power fail at any point, the file system is still stable. Now this works up until the firmware volume is full--that is, there is not enough space left for a new file. But there may still be free space left...
What? Free space? But you just said that the firmware volume is full! That's true. But remember how files are deleted in the PI firmware file system: we just set a bit in the file header. So even though the file is deleted, the space is not freed up in the volume. That may seem brain dead, but it is really a requirement which stems from the nature of flash technology. With flash devices, it is easy to change a bit from off to on but it is hard to change a bit from on to off. Changing the bits back to off requires an erase cycle. Erase cycles change a big chunk called a 'sector' or 'block' back to their default values. These blocks range in size from 512 bytes up to 64KB or more, depending on the device. For flash devices based on NOR technology, this default value is '1' while for flash devices based on NAND technology, this default value is '0'. Erases are time consuming. Also, because the blocks are large and ROM space is precious, there may be bits of other files in the same flash block. So erasing one file might require temporarily erasing part of another file, meaning we have to copy those other pieces somewhere else temporarily. Rather than copy the contents of a block to a spare block every time it needs to delete a file, the firmware file system deletes files by just setting a bit in the file header.
When the UEFI file system really, really, really needs the free space, it copies the contents of a single flash block over to a spare block, notes somewhere which block has copied, performs the erase and updates the block. If the power fails during the update, the file system can correct by copying over from the spare block when mounting the firmware volume.
UEFI PI Firmware Volume Header Explained
Here's the prototype for a standard UEFI PI firmware volume:
typedef struct {
UINT8 ZeroVector[16];
EFI_GUID FileSystemGuid;
UINT64 FvLength;
UINT32 Signature;
EFI_FV_ATTRIBUTES Attributes;
UINT16 HeaderLength;
UINT16 Checksum;
UINT16 ExtHeaderOffset;
UINT8 Reserved[1];
UINT8 Revision;
EFI_FV_BLOCK_MAP BlockMap[];
} EFI_FIRMWARE_VOLUME_HEADER;
The firmware volume header consists of two parts: the fixed header and the extended header. Let's work our way through the fields one by one.
ZeroVector - This is just an empty 16-bytes. Why waste 16-bytes? Ok, back to CPU Architecture 101. On X86 CPUs, the reset vector is 16 bytes below 4GB. The reset vector is the address of the first instruction that the CPU will execute after reset. It hasn't always been near 4GB. The 286 CPU placed the reset vector just below 16MB and the 8086 CPU placed the reset vector below 1MB. But I digress. Normally we put the BIOS near the reset vector since we want the CPU to have something to run after reset. Otherwise it sails the sea of infinite zeroes.... Slap to self! Back to topic. Some CPU architectures (most notably ARM) don't place the reset vector at the top of the CPU address space. Instead they put the reset vector at address 0. So that's where the firmware goes. And the firmware is located in a firmware volume. So on these systems, the reset vector actually sits in the firmware volume header. Thus 16 bytes were set aside so that they could be patched with reset vector code (usually a JMP) for these CPUs.
FileSystemGuid - This uniquely identifies the file system which is used within the firmware volume. While UEFI PI has one "official" firmware file system, it also sufficiently abstracts its behavior to allow other formats to be supported. For example, Microsoft's TFAT or Phoenix's own firmware file system (PFFS) can both be supported by assigning them a new GUID and providing the appropriate PPIs (EFI_PEI_FIRMWARE_VOLUME_PPI) and protocols (EFI_FIRMWARE_VOLUME2_PROTOCOL).
FvLength - This is the length of the firmware volume, in bytes. Interestingly, this means that you don't need to know the entire size of the firmware volume before beginning to process the volume. You just need the header. Some other formats require that you know the media size (TFAT) or volume size (PFFS) in order to process them. TFAT needs the knowledge up front because it creates backup copies of the File Allocation Tables (FAT) at the end of the media. PFFS needs the knowledge up front because it allows the volume header to "float" and needs to know the maximum extent it should search for the volume header. Otherwise a malformed volume could cause PFFS to crash.
Signature - This is always hard-coded to '_FVH'. Placing the signature in the header makes it easy to identify when looking at a firmware volume in memory.
Attributes - These attributes describe some of the properties of the firmware volume. For example, this field describes whether the firmware volume should be write-protected or read-protected and whether this should be changeable. The file system itself doesn't guarantee these attributes, but assumes that these are honored by the firmware file system code which manipulates the firmware volume. In some case, the firmware file system code may enforce these attributes using hardware resources, such as block-locking in the flash device or the chipset.
This field also describes some attributes of the flash device itself which the file system code needs to know. First, it describes whether writes to the device are "sticky". This is related to the discussion above, where we talked about how it is easy to write a bit from off to on but hard to write it from on to off. If a flash device is not classified is "sticky" it means that writes to 1 and writes to 0 can be performed without an erase cycle. Second, it describes whether an erase cycle will change all bits in a flash block to 0 or 1. The file system uses this information to invert the polarity of the status byte in the file header. For example, on a NOR part, the normal file status is 0xF8 while on a NAND part, the normal status is 0x07. Notice that these are inverted forms of each other, because on a NOR part it is easy to change a bit from 1 to 0 while on NAND parts it is easy to change a bit from 0 to 1. Third, the volume attributes describe whether the volume is memory mapped.
HeaderLength - This describes the size of the entire header (fixed and extended), in bytes. The first byte past the header is the start of the file system within the firmware volume.
Checksum - This is a 16-bit checksum of the firmware volume header such that, when summing all bytes of the firmware volume header (including this field and ignoring overflow), the result should be 0.
ExtHeaderOffset - This is the unsigned offset, in bytes and relative to the first byte of the firmware volume header, of the extended header. If the extended header is not present, then this field should be 0.
FvBlockMap - This array describes the logical arrangement of blocks within the firmware volume, including the header. Each entry in the array describes a run of blocks which have the same size. There is no requirement that all blocks in a firmware volume be the same size, although that makes it easier to use when using other file systems. The array is terminated with a block run with a size of zero.
The extended file header is optional and consists of a fixed portion and zero or more extensions. The fixed portion looks like this:
typedef struct
{
EFI_GUID FvName;
UINT32 ExtHeaderSize;
};
FvName - This is the "name" of the firmware volume.
ExtHeaderSize - This is the size of the extended header. More on this later when we talk about "file types".
Pheww...more than you ever wanted to know. We'll talk about files next.
Tim