#!/usr/bin/perl # Create a UEFI "Firmware File" (FFS) with optional features. # Address Size Designation # ------- ---- ----------- # # EFI_FFS_FILE_HEADER: # 0x0000 16 Name (EFI_GUID) # 0x0010 1 IntegrityCheck.Header (Header Checksum) # 0x0011 1 IntegrityCheck.File -> set to 0xAA (FFS_FIXED_CHECKSUM) and clear bit 0x40 of Attributes # 0x0012 1 FileType -> 0x07 = EFI_FV_FILETYPE_DRIVER # 0x0013 1 Attributes -> 0x00 # 0x0014 3 Size, including header and all other sections # 0x0017 1 State (unused) -> 0X00 # # EFI_COMMON_SECTION_HEADER: # 0x0000 3 Size, including this header # 0x0003 1 Type -> 0x10 (EFI_SECTION_PE32) # 0x0004 #### # # EFI_COMMON_SECTION_HEADER: # 0x0000 3 Size, including this header # 0x0003 1 Type -> 0x15 (EFI_SECTION_USER_INTERFACE) # 0x0004 #### NUL terminated UTF-16 string (eg "FAT\0") # # EFI_COMMON_SECTION_HEADER: # 0x0000 3 Size, including this header # 0x0003 1 Type -> 0x14 (EFI_SECTION_VERSION) # 0x0004 #### NUL terminated UTF-16 string (eg "1.0\0") use warnings; use strict; use FindBin; use lib "$FindBin::Bin/../lib"; use Getopt::Long; use File::Temp 'tempfile'; use Digest::SHA 'sha1'; use GUID; my $usage = <<""; Usage: $0 -o output.ffs [options] file.efi [...] Options: -o | --output output.ffs Output file (default is stdout) -n | --name FileName Name to include in UI Section -t | --type Type FREEFORM|DRIVER|SMM|DXE_CORE|SMM_CORE|PEIM -v | --version 1.0 Version section -g | --guid GUID This file GUID (default is hash of Name) -d | --depex 'guid guid..' Optional dependencies (all ANDed, or TRUE) -z | --compress Enable LZMA compression my $output = '-'; my $name; my $type = 'FREEFORM'; my $version; my $guid; my $depex; my $compress; GetOptions( "o|output=s" => \$output, "n|name=s" => \$name, "t|type=s" => \$type, "v|version=s" => \$version, "g|guid=s" => \$guid, "d|depex=s" => \$depex, "z|compress+" => \$compress, ) or die $usage; my %file_types = qw/ RAW 0x01 FREEFORM 0x02 SECURITY_CORE 0x03 PEI_CORE 0x04 DXE_CORE 0x05 PEIM 0x06 DRIVER 0x07 COMBINED_PEIM_DRIVER 0x08 APPLICATION 0x09 SMM 0x0A FIRMWARE_VOLUME_IMAGE 0x0B COMBINED_SMM_DXE 0x0C SMM_CORE 0x0D DEBUG_MIN 0xe0 DEBUG_MAX 0xef FFS_PAD 0xf0 /; my %section_types = qw/ GUID_DEFINED 0x02 PE32 0x10 PIC 0x11 TE 0x12 DXE_DEPEX 0x13 VERSION 0x14 USER_INTERFACE 0x15 COMPATIBILITY16 0x16 FIRMWARE_VOLUME_IMAGE 0x17 FREEFORM_SUBTYPE_GUID 0x18 RAW 0x19 PEI_DEPEX 0x1B SMM_DEPEX 0x1C /; # Some special cases for non-PE32 sections my %section_type_map = qw/ FREEFORM RAW FIRMWARE_VOLUME_IMAGE FIRMWARE_VOLUME_IMAGE /; # Special cases for DEPEX sections my %depex_type_map = qw/ PEIM PEI_DEPX DRIVER DXE_DEPEX SMM SMM_DEPEX /; my $data = ''; # Read entire files at a time and append a new section # for each file read. Some special types have their own # section type; otherwise we're adding a PE32 # The file goes first so that it will be aligned correctly # on the 4 KB page inside the FV. local $/ = undef; while(<>) { $data .= section($section_type_map{$type} || 'PE32', $_); } # Put the optional parts after the input data $data .= section(USER_INTERFACE => ucs16($name)) if $name; $data .= section(VERSION => ucs16(chr(0x00) . $version)) if $version; $data .= depex($type, split /\s+/, $depex) if $depex; # If no GUID was provided, make one from the name # if there is no name from the data if ($guid) { $guid = guid($guid); } else { # Generate a deterministic GUID based on either # the UI name or the hash of the input data $guid = substr(sha1($name || $data), 0, 16); } my $file_type = $file_types{$type} or die "$type: unknown file type\n"; # If we're compressing, compress the data and wrap it with a GUIDed header if ($compress) { my ($fh,$filename) = tempfile(); print $fh $data; close $fh; # -7 produces the same bit-stream as the UEFI tools my $lz_data = `lzma --compress --stdout -7 $filename`; printf STDERR "%d compressed to %d\n", length($data), length($lz_data); # fixup the size field in the lzma compressed data substr($lz_data, 5, 8) = pack("VV", length($data), 0); # wrap the lzdata in a GUIDed section my $lz_header = '' . guid('EE4E5898-3914-4259-9D6E-DC7BD79403CF') . chr(0x18) # data offset . chr(0x00) . chr(0x01) # Processing required . chr(0x00) ; # and replace our data with the GUID defined LZ compressed data $data = section(GUID_DEFINED => $lz_header . $lz_data); } # Generate the FFS header around the sections my $len = length($data) + 0x18; my $ffs = '' . $guid # 0x00 . chr(0x00) # 0x10 header checksum . chr(0x00) # 0x11 FFS_FIXED_CHECKSUM . chr(hex $file_type) # 0x12 . chr(0x28) # 0x13 attributes . chr(($len >> 0) & 0xFF) # 0x14 length . chr(($len >> 8) & 0xFF) . chr(($len >> 16) & 0xFF) . chr(0x07) # 0x17 state (done?) ; # fixup the header checksum my $sum = 0; for my $i (0..length($ffs)-2) { $sum -= ord(substr($ffs, $i, 1)); } substr($ffs, 0x10, 2) = chr($sum & 0xFF) . chr(0xAA); # Add the rest of the data $ffs .= $data; # should we pad to align the FFS length? #my $unaligned = length($ffs) % 8; #$ffs .= chr(0x00) x (8 - $unaligned) # if $unaligned != 0; if ($output eq '-') { print $ffs; } else { open OUTPUT, ">", $output or die "$output: Unable to open: $!\n"; print OUTPUT $ffs; close OUTPUT; } # Convert a string to UCS-16 and add a nul terminator sub ucs16 { my $val = shift; my $rc = ''; for(my $i = 0 ; $i < length $val ; $i++) { $rc .= substr($val, $i, 1) . chr(0x0); } # nul terminate the string $rc .= chr(0x0) . chr(0x0); return $rc; } # output an EFI Common Section Header # Since we might be dealing with ones larger than 16 MB, we should use extended # section type that gives us a 4-byte length. sub section { my $type = shift; my $data = shift; die "$type: Unknown section type\n" unless exists $section_types{$type}; my $len = length($data) + 4; die "Section length $len > 16 MB, can't include it in a section!\n" if $len >= 0x1000000; my $sec = '' . chr(($len >> 0) & 0xFF) . chr(($len >> 8) & 0xFF) . chr(($len >> 16) & 0xFF) . chr(hex $section_types{$type}) . $data; my $unaligned = length($sec) % 4; $sec .= chr(0x00) x (4 - $unaligned) if $unaligned != 0; return $sec; } # Generate a DEPEX sub depex { my $type = shift; my $section_type = $depex_type_map{$type} or die "$type: DEPEX is not supported\n"; if ($depex eq 'TRUE') { # Special case for short-circuit return section($section_type, chr(0x06) . chr(0x08)); } my $data = ''; my $count = 0; for my $guid (@_) { # push the guid $data .= chr(0x02) . guid($guid); $count++; } # AND them all together (1 minus the number of GUIDs) $data .= chr(0x03) for 1..$count-1; $data .= chr(0x08); return section($section_type, $data); }