#!/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 Getopt::Long; use File::Slurp; use Digest::SHA 'sha1'; 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) my $output = '-'; my $name; my $type = 'FREEFORM'; my $version; my $guid; my $depex; 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, ) 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/ 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 = ''; $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; # 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 local $/ = undef; while(<>) { $data .= section($section_type_map{$type} || 'PE32', $_); } # 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"; # 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 sub section { my $type = shift; my $data = shift; die "$type: Unknown section type\n" unless exists $section_types{$type}; my $len = length($data) + 4; 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; } # convert text GUID to hex sub guid { my $guid = shift; my ($g1,$g2,$g3,$g4,$g5) = $guid =~ / ([0-9a-fA-F]{8}) -([0-9a-fA-F]{4}) -([0-9a-fA-F]{4}) -([0-9a-fA-F]{4}) -([0-9([0-9a-fA-F]{12}) /x or die "$guid: Unable to parse guid\n"; return pack("VvvnCCCCCC", hex $g1, hex $g2, hex $g3, hex $g4, hex substr($g5, 0, 2), hex substr($g5, 2, 2), hex substr($g5, 4, 2), hex substr($g5, 6, 2), hex substr($g5, 8, 2), hex substr($g5,10, 2), ); } # 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); }