package Option::ROM; # Copyright (C) 2008 Michael Brown . # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation; either version 2 of the # License, or any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. =head1 NAME Option::ROM - Option ROM manipulation =head1 SYNOPSIS use Option::ROM; # Load a ROM image my $rom = new Option::ROM; $rom->load ( "rtl8139.rom" ); # Modify the PCI device ID $rom->pci_header->{device_id} = 0x1234; $rom->fix_checksum(); # Write ROM image out to a new file $rom->save ( "rtl8139-modified.rom" ); =head1 DESCRIPTION C provides a mechanism for manipulating Option ROM images. =head1 METHODS =cut ############################################################################## # # Option::ROM::Fields # ############################################################################## package Option::ROM::Fields; use strict; use warnings; use Carp; use bytes; sub TIEHASH { my $class = shift; my $self = shift; bless $self, $class; return $self; } sub FETCH { my $self = shift; my $key = shift; return undef unless $self->EXISTS ( $key ); my $raw = substr ( ${$self->{data}}, ( $self->{offset} + $self->{fields}->{$key}->{offset} ), $self->{fields}->{$key}->{length} ); my $unpack = ( ref $self->{fields}->{$key}->{unpack} ? $self->{fields}->{$key}->{unpack} : sub { unpack ( $self->{fields}->{$key}->{pack}, shift ); } ); return &$unpack ( $raw ); } sub STORE { my $self = shift; my $key = shift; my $value = shift; croak "Nonexistent field \"$key\"" unless $self->EXISTS ( $key ); my $pack = ( ref $self->{fields}->{$key}->{pack} ? $self->{fields}->{$key}->{pack} : sub { pack ( $self->{fields}->{$key}->{pack}, shift ); } ); my $raw = &$pack ( $value ); substr ( ${$self->{data}}, ( $self->{offset} + $self->{fields}->{$key}->{offset} ), $self->{fields}->{$key}->{length} ) = $raw; } sub DELETE { my $self = shift; my $key = shift; $self->STORE ( $key, 0 ); } sub CLEAR { my $self = shift; foreach my $key ( keys %{$self->{fields}} ) { $self->DELETE ( $key ); } } sub EXISTS { my $self = shift; my $key = shift; return ( exists $self->{fields}->{$key} && ( ( $self->{fields}->{$key}->{offset} + $self->{fields}->{$key}->{length} ) <= $self->{length} ) && ( ! defined $self->{fields}->{$key}->{check} || &{$self->{fields}->{$key}->{check}} ( $self, $key ) ) ); } sub FIRSTKEY { my $self = shift; keys %{$self->{fields}}; return each %{$self->{fields}}; } sub NEXTKEY { my $self = shift; my $lastkey = shift; return each %{$self->{fields}}; } sub SCALAR { my $self = shift; return 1; } sub UNTIE { my $self = shift; } sub DESTROY { my $self = shift; } sub checksum { my $self = shift; my $raw = substr ( ${$self->{data}}, $self->{offset}, $self->{length} ); return unpack ( "%8C*", $raw ); } ############################################################################## # # Option::ROM # ############################################################################## package Option::ROM; use strict; use warnings; use Carp; use bytes; use Exporter 'import'; use constant ROM_SIGNATURE => 0xaa55; use constant PCI_SIGNATURE => 'PCIR'; use constant PCI_LAST_IMAGE => 0x80; use constant PNP_SIGNATURE => '$PnP'; use constant UNDI_SIGNATURE => 'UNDI'; use constant IPXE_SIGNATURE => 'iPXE'; our @EXPORT_OK = qw ( ROM_SIGNATURE PCI_SIGNATURE PCI_LAST_IMAGE PNP_SIGNATURE UNDI_SIGNATURE IPXE_SIGNATURE ); our %EXPORT_TAGS = ( all => [ @EXPORT_OK ] ); use constant JMP_SHORT => 0xeb; use constant JMP_NEAR => 0xe9; use constant CALL_NEAR => 0xe8; sub pack_init { my $dest = shift; # Always create a near jump; it's simpler if ( $dest ) { return pack ( "CS", JMP_NEAR, ( $dest - 6 ) ); } else { return pack ( "CS", 0, 0 ); } } sub unpack_init { my $instr = shift; # Accept both short and near jumps my $jump = unpack ( "C", $instr ); if ( $jump == JMP_SHORT ) { my $offset = unpack ( "xC", $instr ); return ( $offset + 5 ); } elsif ( $jump == JMP_NEAR ) { my $offset = unpack ( "xS", $instr ); return ( $offset + 6 ); } elsif ( $jump == CALL_NEAR ) { my $offset = unpack ( "xS", $instr ); return ( $offset + 6 ); } elsif ( $jump == 0 ) { return 0; } else { carp "Unrecognised jump instruction in init vector\n"; return 0; } } sub check_pcat_rom { my $self = shift; my $key = shift; my $pci = $self->{rom}->pci_header (); return ! defined $pci || $pci->{code_type} == 0x00; } =pod =item C<< new () >> Construct a new C object. =cut sub new { my $class = shift; my $hash = {}; tie %$hash, "Option::ROM::Fields", { rom => $hash, # ROM object itself data => undef, offset => 0x00, length => 0x20, file_offset => 0x0, fields => { signature => { offset => 0x00, length => 0x02, pack => "S" }, length => { offset => 0x02, length => 0x01, pack => "C" }, # "init" is part of a jump instruction init => { offset => 0x03, length => 0x03, pack => \&pack_init, unpack => \&unpack_init, check => \&check_pcat_rom }, checksum => { offset => 0x06, length => 0x01, pack => "C", check => \&check_pcat_rom }, ipxe_header => { offset => 0x10, length => 0x02, pack => "S", check => \&check_pcat_rom }, bofm_header => { offset => 0x14, length => 0x02, pack => "S", check => \&check_pcat_rom }, undi_header => { offset => 0x16, length => 0x02, pack => "S", check => \&check_pcat_rom }, pci_header => { offset => 0x18, length => 0x02, pack => "S" }, pnp_header => { offset => 0x1a, length => 0x02, pack => "S", check => \&check_pcat_rom }, }, }; bless $hash, $class; return $hash; } =pod =item C<< set ( $data [, $file_offset ] ) >> Set option ROM contents, optionally sets original file offset. =cut sub set { my $hash = shift; my $self = tied(%$hash); my $data = shift; my $file_offset = shift // 0x0; # Store data $self->{data} = \$data; $self->{file_offset} = $file_offset; # Split out any data belonging to the next image delete $self->{next_image}; my $pci_header = $hash->pci_header(); if ( ( defined $pci_header ) && ( ! ( $pci_header->{last_image} & PCI_LAST_IMAGE ) ) ) { my $length = ( $pci_header->{image_length} * 512 ); my $remainder = substr ( $data, $length ); $data = substr ( $data, 0, $length ); $self->{next_image} = new Option::ROM; $self->{next_image}->set ( $remainder, $self->{file_offset} + $length ); } } =pod =item C<< get () >> Get option ROM contents. =cut sub get { my $hash = shift; my $self = tied(%$hash); my $data = ${$self->{data}}; $data .= $self->{next_image}->get() if $self->{next_image}; return $data; } =pod =item C<< load ( $filename ) >> Load option ROM contents from the file C<$filename>. =cut sub load { my $hash = shift; my $self = tied(%$hash); my $filename = shift; $self->{filename} = $filename; open my $fh, "<$filename" or croak "Cannot open $filename for reading: $!"; binmode $fh; read $fh, my $data, -s $fh; $hash->set ( $data ); close $fh; } =pod =item C<< save ( [ $filename ] ) >> Write the ROM data back out to the file C<$filename>. If C<$filename> is omitted, the file used in the call to C will be used. =cut sub save { my $hash = shift; my $self = tied(%$hash); my $filename = shift; $filename ||= $self->{filename}; open my $fh, ">$filename" or croak "Cannot open $filename for writing: $!"; my $data = $hash->get(); binmode $fh; print $fh $data; close $fh; } =pod =item C<< length () >> Length of option ROM data. This is the length of the file, not the length from the ROM header length field. =cut sub length { my $hash = shift; my $self = tied(%$hash); return length ${$self->{data}}; } =pod =item C<< pci_header () >> Return a C object representing the ROM's PCI header, if present. =cut sub pci_header { my $hash = shift; my $self = tied(%$hash); my $offset = $hash->{pci_header}; return undef unless $offset; return Option::ROM::PCI->new ( $self, $offset ); } =pod =item C<< pnp_header () >> Return a C object representing the ROM's PnP header, if present. =cut sub pnp_header { my $hash = shift; my $self = tied(%$hash); my $offset = $hash->{pnp_header}; return undef unless $offset; return Option::ROM::PnP->new ( $self, $offset ); } =pod =item C<< undi_header () >> Return a C object representing the ROM's UNDI header, if present. =cut sub undi_header { my $hash = shift; my $self = tied(%$hash); my $offset = $hash->{undi_header}; return undef unless $offset; return Option::ROM::UNDI->new ( $self, $offset ); } =pod =item C<< ipxe_header () >> Return a C object representing the ROM's iPXE header, if present. =cut sub ipxe_header { my $hash = shift; my $self = tied(%$hash); my $offset = $hash->{ipxe_header}; return undef unless $offset; return Option::ROM::iPXE->new ( $self, $offset ); } =pod =item C<< next_image () >> Return a C object representing the next image within the ROM, if present. =cut sub next_image { my $hash = shift; my $self = tied(%$hash); return $self->{next_image}; } =pod =item C<< checksum () >> Calculate the byte checksum of the ROM. =cut sub checksum { my $hash = shift; my $self = tied(%$hash); my $raw = substr ( ${$self->{data}}, 0, ( $hash->{length} * 512 ) ); return unpack ( "%8C*", $raw ); } =pod =item C<< fix_checksum () >> Fix the byte checksum of the ROM. =cut sub fix_checksum { my $hash = shift; my $self = tied(%$hash); return unless ( exists $hash->{checksum} ); $hash->{checksum} = ( ( $hash->{checksum} - $hash->checksum() ) & 0xff ); } =pod =item C<< file_offset () >> Get file offset of image. =cut sub file_offset { my $hash = shift; my $self = tied(%$hash); return $self->{file_offset}; } ############################################################################## # # Option::ROM::PCI # ############################################################################## package Option::ROM::PCI; use strict; use warnings; use Carp; use bytes; sub new { my $class = shift; my $rom = shift; my $offset = shift; my $hash = {}; tie %$hash, "Option::ROM::Fields", { rom => $rom, data => $rom->{data}, offset => $offset, length => 0x0c, fields => { signature => { offset => 0x00, length => 0x04, pack => "a4" }, vendor_id => { offset => 0x04, length => 0x02, pack => "S" }, device_id => { offset => 0x06, length => 0x02, pack => "S" }, device_list => { offset => 0x08, length => 0x02, pack => "S" }, struct_length => { offset => 0x0a, length => 0x02, pack => "S" }, struct_revision =>{ offset => 0x0c, length => 0x01, pack => "C" }, prog_intf => { offset => 0x0d, length => 0x01, pack => "C" }, sub_class => { offset => 0x0e, length => 0x01, pack => "C" }, base_class => { offset => 0x0f, length => 0x01, pack => "C" }, image_length => { offset => 0x10, length => 0x02, pack => "S" }, revision => { offset => 0x12, length => 0x02, pack => "S" }, code_type => { offset => 0x14, length => 0x01, pack => "C" }, last_image => { offset => 0x15, length => 0x01, pack => "C" }, runtime_length => { offset => 0x16, length => 0x02, pack => "S" }, conf_header => { offset => 0x18, length => 0x02, pack => "S" }, clp_entry => { offset => 0x1a, length => 0x02, pack => "S" }, }, }; bless $hash, $class; my $self = tied ( %$hash ); my $length = $rom->{rom}->length (); return undef unless ( $offset + $self->{length} <= $length && $hash->{signature} eq Option::ROM::PCI_SIGNATURE && $offset + $hash->{struct_length} <= $length ); # Retrieve true length of structure $self->{length} = $hash->{struct_length}; return $hash; } sub device_list { my $hash = shift; my $self = tied(%$hash); my $device_list = $hash->{device_list}; return undef unless $device_list; my @ids; my $offset = ( $self->{offset} + $device_list ); while ( 1 ) { my $raw = substr ( ${$self->{data}}, $offset, 2 ); my $id = unpack ( "S", $raw ); last unless $id; push @ids, $id; $offset += 2; } return @ids; } ############################################################################## # # Option::ROM::PnP # ############################################################################## package Option::ROM::PnP; use strict; use warnings; use Carp; use bytes; sub new { my $class = shift; my $rom = shift; my $offset = shift; my $hash = {}; tie %$hash, "Option::ROM::Fields", { rom => $rom, data => $rom->{data}, offset => $offset, length => 0x06, fields => { signature => { offset => 0x00, length => 0x04, pack => "a4" }, struct_revision =>{ offset => 0x04, length => 0x01, pack => "C" }, struct_length => { offset => 0x05, length => 0x01, pack => "C" }, checksum => { offset => 0x09, length => 0x01, pack => "C" }, manufacturer => { offset => 0x0e, length => 0x02, pack => "S" }, product => { offset => 0x10, length => 0x02, pack => "S" }, bcv => { offset => 0x16, length => 0x02, pack => "S" }, bdv => { offset => 0x18, length => 0x02, pack => "S" }, bev => { offset => 0x1a, length => 0x02, pack => "S" }, }, }; bless $hash, $class; my $self = tied ( %$hash ); my $length = $rom->{rom}->length (); return undef unless ( $offset + $self->{length} <= $length && $hash->{signature} eq Option::ROM::PNP_SIGNATURE && $offset + $hash->{struct_length} * 16 <= $length ); # Retrieve true length of structure $self->{length} = ( $hash->{struct_length} * 16 ); return $hash; } sub checksum { my $hash = shift; my $self = tied(%$hash); return $self->checksum(); } sub fix_checksum { my $hash = shift; my $self = tied(%$hash); $hash->{checksum} = ( ( $hash->{checksum} - $hash->checksum() ) & 0xff ); } sub manufacturer { my $hash = shift; my $self = tied(%$hash); my $manufacturer = $hash->{manufacturer}; return undef unless $manufacturer; my $raw = substr ( ${$self->{data}}, $manufacturer ); return unpack ( "Z*", $raw ); } sub product { my $hash = shift; my $self = tied(%$hash); my $product = $hash->{product}; return undef unless $product; my $raw = substr ( ${$self->{data}}, $product ); return unpack ( "Z*", $raw ); } ############################################################################## # # Option::ROM::UNDI # ############################################################################## package Option::ROM::UNDI; use strict; use warnings; use Carp; use bytes; sub new { my $class = shift; my $rom = shift; my $offset = shift; my $hash = {}; tie %$hash, "Option::ROM::Fields", { rom => $rom, data => $rom->{data}, offset => $offset, length => 0x16, fields => { signature => { offset => 0x00, length => 0x04, pack => "a4" }, struct_length => { offset => 0x04, length => 0x01, pack => "C" }, checksum => { offset => 0x05, length => 0x01, pack => "C" }, struct_revision =>{ offset => 0x06, length => 0x01, pack => "C" }, version_revision =>{ offset => 0x07, length => 0x01, pack => "C" }, version_minor => { offset => 0x08, length => 0x01, pack => "C" }, version_major => { offset => 0x09, length => 0x01, pack => "C" }, loader_entry => { offset => 0x0a, length => 0x02, pack => "S" }, stack_size => { offset => 0x0c, length => 0x02, pack => "S" }, data_size => { offset => 0x0e, length => 0x02, pack => "S" }, code_size => { offset => 0x10, length => 0x02, pack => "S" }, bus_type => { offset => 0x12, length => 0x04, pack => "a4" }, }, }; bless $hash, $class; my $self = tied ( %$hash ); my $length = $rom->{rom}->length (); return undef unless ( $offset + $self->{length} <= $length && $hash->{signature} eq Option::ROM::UNDI_SIGNATURE && $offset + $hash->{struct_length} <= $length ); # Retrieve true length of structure $self->{length} = $hash->{struct_length}; return $hash; } sub checksum { my $hash = shift; my $self = tied(%$hash); return $self->checksum(); } sub fix_checksum { my $hash = shift; my $self = tied(%$hash); $hash->{checksum} = ( ( $hash->{checksum} - $hash->checksum() ) & 0xff ); } ############################################################################## # # Option::ROM::iPXE # ############################################################################## package Option::ROM::iPXE; use strict; use warnings; use Carp; use bytes; sub new { my $class = shift; my $rom = shift; my $offset = shift; my $hash = {}; tie %$hash, "Option::ROM::Fields", { rom => $rom, data => $rom->{data}, offset => $offset, length => 0x06, fields => { signature => { offset => 0x00, length => 0x04, pack => "a4" }, struct_length => { offset => 0x04, length => 0x01, pack => "C" }, checksum => { offset => 0x05, length => 0x01, pack => "C" }, shrunk_length => { offset => 0x06, length => 0x01, pack => "C" }, build_id => { offset => 0x08, length => 0x04, pack => "L" }, }, }; bless $hash, $class; my $self = tied ( %$hash ); my $length = $rom->{rom}->length (); return undef unless ( $offset + $self->{length} <= $length && $hash->{signature} eq Option::ROM::IPXE_SIGNATURE && $offset + $hash->{struct_length} <= $length ); # Retrieve true length of structure $self->{length} = $hash->{struct_length}; return $hash; } sub checksum { my $hash = shift; my $self = tied(%$hash); return $self->checksum(); } sub fix_checksum { my $hash = shift; my $self = tied(%$hash); $hash->{checksum} = ( ( $hash->{checksum} - $hash->checksum() ) & 0xff ); } 1;