[util] Improve processing of ROM images in Option::ROM
[ipxe.git] / src / util / Option / ROM.pm
1 package Option::ROM;
2
3 # Copyright (C) 2008 Michael Brown <mbrown@fensystems.co.uk>.
4 #
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License as
7 # published by the Free Software Foundation; either version 2 of the
8 # License, or any later version.
9 #
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 # General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 # 02110-1301, USA.
19
20 =head1 NAME
21
22 Option::ROM - Option ROM manipulation
23
24 =head1 SYNOPSIS
25
26 use Option::ROM;
27
28 # Load a ROM image
29 my $rom = new Option::ROM;
30 $rom->load ( "rtl8139.rom" );
31
32 # Modify the PCI device ID
33 $rom->pci_header->{device_id} = 0x1234;
34 $rom->fix_checksum();
35
36 # Write ROM image out to a new file
37 $rom->save ( "rtl8139-modified.rom" );
38
39 =head1 DESCRIPTION
40
41 C<Option::ROM> provides a mechanism for manipulating Option ROM
42 images.
43
44 =head1 METHODS
45
46 =cut
47
48 ##############################################################################
49 #
50 # Option::ROM::Fields
51 #
52 ##############################################################################
53
54 package Option::ROM::Fields;
55
56 use strict;
57 use warnings;
58 use Carp;
59 use bytes;
60
61 sub TIEHASH {
62 my $class = shift;
63 my $self = shift;
64
65 bless $self, $class;
66 return $self;
67 }
68
69 sub FETCH {
70 my $self = shift;
71 my $key = shift;
72
73 return undef unless $self->EXISTS ( $key );
74 my $raw = substr ( ${$self->{data}},
75 ( $self->{offset} + $self->{fields}->{$key}->{offset} ),
76 $self->{fields}->{$key}->{length} );
77 my $unpack = ( ref $self->{fields}->{$key}->{unpack} ?
78 $self->{fields}->{$key}->{unpack} :
79 sub { unpack ( $self->{fields}->{$key}->{pack}, shift ); } );
80 return &$unpack ( $raw );
81 }
82
83 sub STORE {
84 my $self = shift;
85 my $key = shift;
86 my $value = shift;
87
88 croak "Nonexistent field \"$key\"" unless $self->EXISTS ( $key );
89 my $pack = ( ref $self->{fields}->{$key}->{pack} ?
90 $self->{fields}->{$key}->{pack} :
91 sub { pack ( $self->{fields}->{$key}->{pack}, shift ); } );
92 my $raw = &$pack ( $value );
93 substr ( ${$self->{data}},
94 ( $self->{offset} + $self->{fields}->{$key}->{offset} ),
95 $self->{fields}->{$key}->{length} ) = $raw;
96 }
97
98 sub DELETE {
99 my $self = shift;
100 my $key = shift;
101
102 $self->STORE ( $key, 0 );
103 }
104
105 sub CLEAR {
106 my $self = shift;
107
108 foreach my $key ( keys %{$self->{fields}} ) {
109 $self->DELETE ( $key );
110 }
111 }
112
113 sub EXISTS {
114 my $self = shift;
115 my $key = shift;
116
117 return ( exists $self->{fields}->{$key} &&
118 ( ( $self->{fields}->{$key}->{offset} +
119 $self->{fields}->{$key}->{length} ) <= $self->{length} ) &&
120 ( ! defined $self->{fields}->{$key}->{check} ||
121 &{$self->{fields}->{$key}->{check}} ( $self, $key ) ) );
122 }
123
124 sub FIRSTKEY {
125 my $self = shift;
126
127 keys %{$self->{fields}};
128 return each %{$self->{fields}};
129 }
130
131 sub NEXTKEY {
132 my $self = shift;
133 my $lastkey = shift;
134
135 return each %{$self->{fields}};
136 }
137
138 sub SCALAR {
139 my $self = shift;
140
141 return 1;
142 }
143
144 sub UNTIE {
145 my $self = shift;
146 }
147
148 sub DESTROY {
149 my $self = shift;
150 }
151
152 sub checksum {
153 my $self = shift;
154
155 my $raw = substr ( ${$self->{data}}, $self->{offset}, $self->{length} );
156 return unpack ( "%8C*", $raw );
157 }
158
159 ##############################################################################
160 #
161 # Option::ROM
162 #
163 ##############################################################################
164
165 package Option::ROM;
166
167 use strict;
168 use warnings;
169 use Carp;
170 use bytes;
171 use Exporter 'import';
172
173 use constant ROM_SIGNATURE => 0xaa55;
174 use constant PCI_SIGNATURE => 'PCIR';
175 use constant PCI_LAST_IMAGE => 0x80;
176 use constant PNP_SIGNATURE => '$PnP';
177 use constant UNDI_SIGNATURE => 'UNDI';
178 use constant IPXE_SIGNATURE => 'iPXE';
179
180 our @EXPORT_OK = qw ( ROM_SIGNATURE PCI_SIGNATURE PCI_LAST_IMAGE
181 PNP_SIGNATURE UNDI_SIGNATURE IPXE_SIGNATURE );
182 our %EXPORT_TAGS = ( all => [ @EXPORT_OK ] );
183
184 use constant JMP_SHORT => 0xeb;
185 use constant JMP_NEAR => 0xe9;
186 use constant CALL_NEAR => 0xe8;
187
188 sub pack_init {
189 my $dest = shift;
190
191 # Always create a near jump; it's simpler
192 if ( $dest ) {
193 return pack ( "CS", JMP_NEAR, ( $dest - 6 ) );
194 } else {
195 return pack ( "CS", 0, 0 );
196 }
197 }
198
199 sub unpack_init {
200 my $instr = shift;
201
202 # Accept both short and near jumps
203 my $jump = unpack ( "C", $instr );
204 if ( $jump == JMP_SHORT ) {
205 my $offset = unpack ( "xC", $instr );
206 return ( $offset + 5 );
207 } elsif ( $jump == JMP_NEAR ) {
208 my $offset = unpack ( "xS", $instr );
209 return ( $offset + 6 );
210 } elsif ( $jump == CALL_NEAR ) {
211 my $offset = unpack ( "xS", $instr );
212 return ( $offset + 6 );
213 } elsif ( $jump == 0 ) {
214 return 0;
215 } else {
216 carp "Unrecognised jump instruction in init vector\n";
217 return 0;
218 }
219 }
220
221 sub check_pcat_rom {
222 my $self = shift;
223 my $key = shift;
224
225 my $pci = $self->{rom}->pci_header ();
226
227 return ! defined $pci || $pci->{code_type} == 0x00;
228 }
229
230 =pod
231
232 =item C<< new () >>
233
234 Construct a new C<Option::ROM> object.
235
236 =cut
237
238 sub new {
239 my $class = shift;
240
241 my $hash = {};
242 tie %$hash, "Option::ROM::Fields", {
243 rom => $hash, # ROM object itself
244 data => undef,
245 offset => 0x00,
246 length => 0x20,
247 file_offset => 0x0,
248 fields => {
249 signature => { offset => 0x00, length => 0x02, pack => "S" },
250 length => { offset => 0x02, length => 0x01, pack => "C" },
251 # "init" is part of a jump instruction
252 init => { offset => 0x03, length => 0x03,
253 pack => \&pack_init, unpack => \&unpack_init,
254 check => \&check_pcat_rom },
255 checksum => { offset => 0x06, length => 0x01, pack => "C",
256 check => \&check_pcat_rom },
257 ipxe_header => { offset => 0x10, length => 0x02, pack => "S",
258 check => \&check_pcat_rom },
259 bofm_header => { offset => 0x14, length => 0x02, pack => "S",
260 check => \&check_pcat_rom },
261 undi_header => { offset => 0x16, length => 0x02, pack => "S",
262 check => \&check_pcat_rom },
263 pci_header => { offset => 0x18, length => 0x02, pack => "S" },
264 pnp_header => { offset => 0x1a, length => 0x02, pack => "S",
265 check => \&check_pcat_rom },
266 },
267 };
268 bless $hash, $class;
269 return $hash;
270 }
271
272 =pod
273
274 =item C<< set ( $data [, $file_offset ] ) >>
275
276 Set option ROM contents, optionally sets original file offset.
277
278 =cut
279
280 sub set {
281 my $hash = shift;
282 my $self = tied(%$hash);
283 my $data = shift;
284 my $file_offset = shift // 0x0;
285
286 # Store data
287 $self->{data} = \$data;
288 $self->{file_offset} = $file_offset;
289
290 # Split out any data belonging to the next image
291 delete $self->{next_image};
292 my $pci_header = $hash->pci_header();
293 if ( ( defined $pci_header ) &&
294 ( ! ( $pci_header->{last_image} & PCI_LAST_IMAGE ) ) ) {
295 my $length = ( $pci_header->{image_length} * 512 );
296 my $remainder = substr ( $data, $length );
297 $data = substr ( $data, 0, $length );
298 $self->{next_image} = new Option::ROM;
299 $self->{next_image}->set ( $remainder, $self->{file_offset} + $length );
300 }
301 }
302
303 =pod
304
305 =item C<< get () >>
306
307 Get option ROM contents.
308
309 =cut
310
311 sub get {
312 my $hash = shift;
313 my $self = tied(%$hash);
314
315 my $data = ${$self->{data}};
316 $data .= $self->{next_image}->get() if $self->{next_image};
317 return $data;
318 }
319
320 =pod
321
322 =item C<< load ( $filename ) >>
323
324 Load option ROM contents from the file C<$filename>.
325
326 =cut
327
328 sub load {
329 my $hash = shift;
330 my $self = tied(%$hash);
331 my $filename = shift;
332
333 $self->{filename} = $filename;
334
335 open my $fh, "<$filename"
336 or croak "Cannot open $filename for reading: $!";
337 binmode $fh;
338 read $fh, my $data, -s $fh;
339 $hash->set ( $data );
340 close $fh;
341 }
342
343 =pod
344
345 =item C<< save ( [ $filename ] ) >>
346
347 Write the ROM data back out to the file C<$filename>. If C<$filename>
348 is omitted, the file used in the call to C<load()> will be used.
349
350 =cut
351
352 sub save {
353 my $hash = shift;
354 my $self = tied(%$hash);
355 my $filename = shift;
356
357 $filename ||= $self->{filename};
358
359 open my $fh, ">$filename"
360 or croak "Cannot open $filename for writing: $!";
361 my $data = $hash->get();
362 binmode $fh;
363 print $fh $data;
364 close $fh;
365 }
366
367 =pod
368
369 =item C<< length () >>
370
371 Length of option ROM data. This is the length of the file, not the
372 length from the ROM header length field.
373
374 =cut
375
376 sub length {
377 my $hash = shift;
378 my $self = tied(%$hash);
379
380 return length ${$self->{data}};
381 }
382
383 =pod
384
385 =item C<< pci_header () >>
386
387 Return a C<Option::ROM::PCI> object representing the ROM's PCI header,
388 if present.
389
390 =cut
391
392 sub pci_header {
393 my $hash = shift;
394 my $self = tied(%$hash);
395
396 my $offset = $hash->{pci_header};
397 return undef unless $offset;
398
399 return Option::ROM::PCI->new ( $self, $offset );
400 }
401
402 =pod
403
404 =item C<< pnp_header () >>
405
406 Return a C<Option::ROM::PnP> object representing the ROM's PnP header,
407 if present.
408
409 =cut
410
411 sub pnp_header {
412 my $hash = shift;
413 my $self = tied(%$hash);
414
415 my $offset = $hash->{pnp_header};
416 return undef unless $offset;
417
418 return Option::ROM::PnP->new ( $self, $offset );
419 }
420
421 =pod
422
423 =item C<< undi_header () >>
424
425 Return a C<Option::ROM::UNDI> object representing the ROM's UNDI header,
426 if present.
427
428 =cut
429
430 sub undi_header {
431 my $hash = shift;
432 my $self = tied(%$hash);
433
434 my $offset = $hash->{undi_header};
435 return undef unless $offset;
436
437 return Option::ROM::UNDI->new ( $self, $offset );
438 }
439
440 =pod
441
442 =item C<< ipxe_header () >>
443
444 Return a C<Option::ROM::iPXE> object representing the ROM's iPXE
445 header, if present.
446
447 =cut
448
449 sub ipxe_header {
450 my $hash = shift;
451 my $self = tied(%$hash);
452
453 my $offset = $hash->{ipxe_header};
454 return undef unless $offset;
455
456 return Option::ROM::iPXE->new ( $self, $offset );
457 }
458
459 =pod
460
461 =item C<< next_image () >>
462
463 Return a C<Option::ROM> object representing the next image within the
464 ROM, if present.
465
466 =cut
467
468 sub next_image {
469 my $hash = shift;
470 my $self = tied(%$hash);
471
472 return $self->{next_image};
473 }
474
475 =pod
476
477 =item C<< checksum () >>
478
479 Calculate the byte checksum of the ROM.
480
481 =cut
482
483 sub checksum {
484 my $hash = shift;
485 my $self = tied(%$hash);
486
487 my $raw = substr ( ${$self->{data}}, 0, ( $hash->{length} * 512 ) );
488 return unpack ( "%8C*", $raw );
489 }
490
491 =pod
492
493 =item C<< fix_checksum () >>
494
495 Fix the byte checksum of the ROM.
496
497 =cut
498
499 sub fix_checksum {
500 my $hash = shift;
501 my $self = tied(%$hash);
502
503 return unless ( exists $hash->{checksum} );
504 $hash->{checksum} = ( ( $hash->{checksum} - $hash->checksum() ) & 0xff );
505 }
506
507 =pod
508
509 =item C<< file_offset () >>
510
511 Get file offset of image.
512
513 =cut
514
515 sub file_offset {
516 my $hash = shift;
517 my $self = tied(%$hash);
518
519 return $self->{file_offset};
520 }
521
522 ##############################################################################
523 #
524 # Option::ROM::PCI
525 #
526 ##############################################################################
527
528 package Option::ROM::PCI;
529
530 use strict;
531 use warnings;
532 use Carp;
533 use bytes;
534
535 sub new {
536 my $class = shift;
537 my $rom = shift;
538 my $offset = shift;
539
540 my $hash = {};
541 tie %$hash, "Option::ROM::Fields", {
542 rom => $rom,
543 data => $rom->{data},
544 offset => $offset,
545 length => 0x0c,
546 fields => {
547 signature => { offset => 0x00, length => 0x04, pack => "a4" },
548 vendor_id => { offset => 0x04, length => 0x02, pack => "S" },
549 device_id => { offset => 0x06, length => 0x02, pack => "S" },
550 device_list => { offset => 0x08, length => 0x02, pack => "S" },
551 struct_length => { offset => 0x0a, length => 0x02, pack => "S" },
552 struct_revision =>{ offset => 0x0c, length => 0x01, pack => "C" },
553 prog_intf => { offset => 0x0d, length => 0x01, pack => "C" },
554 sub_class => { offset => 0x0e, length => 0x01, pack => "C" },
555 base_class => { offset => 0x0f, length => 0x01, pack => "C" },
556 image_length => { offset => 0x10, length => 0x02, pack => "S" },
557 revision => { offset => 0x12, length => 0x02, pack => "S" },
558 code_type => { offset => 0x14, length => 0x01, pack => "C" },
559 last_image => { offset => 0x15, length => 0x01, pack => "C" },
560 runtime_length => { offset => 0x16, length => 0x02, pack => "S" },
561 conf_header => { offset => 0x18, length => 0x02, pack => "S" },
562 clp_entry => { offset => 0x1a, length => 0x02, pack => "S" },
563 },
564 };
565 bless $hash, $class;
566
567 my $self = tied ( %$hash );
568 my $length = $rom->{rom}->length ();
569
570 return undef unless ( $offset + $self->{length} <= $length &&
571 $hash->{signature} eq Option::ROM::PCI_SIGNATURE &&
572 $offset + $hash->{struct_length} <= $length );
573
574 # Retrieve true length of structure
575 $self->{length} = $hash->{struct_length};
576
577 return $hash;
578 }
579
580 sub device_list {
581 my $hash = shift;
582 my $self = tied(%$hash);
583
584 my $device_list = $hash->{device_list};
585 return undef unless $device_list;
586
587 my @ids;
588 my $offset = ( $self->{offset} + $device_list );
589 while ( 1 ) {
590 my $raw = substr ( ${$self->{data}}, $offset, 2 );
591 my $id = unpack ( "S", $raw );
592 last unless $id;
593 push @ids, $id;
594 $offset += 2;
595 }
596
597 return @ids;
598 }
599
600 ##############################################################################
601 #
602 # Option::ROM::PnP
603 #
604 ##############################################################################
605
606 package Option::ROM::PnP;
607
608 use strict;
609 use warnings;
610 use Carp;
611 use bytes;
612
613 sub new {
614 my $class = shift;
615 my $rom = shift;
616 my $offset = shift;
617
618 my $hash = {};
619 tie %$hash, "Option::ROM::Fields", {
620 rom => $rom,
621 data => $rom->{data},
622 offset => $offset,
623 length => 0x06,
624 fields => {
625 signature => { offset => 0x00, length => 0x04, pack => "a4" },
626 struct_revision =>{ offset => 0x04, length => 0x01, pack => "C" },
627 struct_length => { offset => 0x05, length => 0x01, pack => "C" },
628 checksum => { offset => 0x09, length => 0x01, pack => "C" },
629 manufacturer => { offset => 0x0e, length => 0x02, pack => "S" },
630 product => { offset => 0x10, length => 0x02, pack => "S" },
631 bcv => { offset => 0x16, length => 0x02, pack => "S" },
632 bdv => { offset => 0x18, length => 0x02, pack => "S" },
633 bev => { offset => 0x1a, length => 0x02, pack => "S" },
634 },
635 };
636 bless $hash, $class;
637
638 my $self = tied ( %$hash );
639 my $length = $rom->{rom}->length ();
640
641 return undef unless ( $offset + $self->{length} <= $length &&
642 $hash->{signature} eq Option::ROM::PNP_SIGNATURE &&
643 $offset + $hash->{struct_length} * 16 <= $length );
644
645 # Retrieve true length of structure
646 $self->{length} = ( $hash->{struct_length} * 16 );
647
648 return $hash;
649 }
650
651 sub checksum {
652 my $hash = shift;
653 my $self = tied(%$hash);
654
655 return $self->checksum();
656 }
657
658 sub fix_checksum {
659 my $hash = shift;
660 my $self = tied(%$hash);
661
662 $hash->{checksum} = ( ( $hash->{checksum} - $hash->checksum() ) & 0xff );
663 }
664
665 sub manufacturer {
666 my $hash = shift;
667 my $self = tied(%$hash);
668
669 my $manufacturer = $hash->{manufacturer};
670 return undef unless $manufacturer;
671
672 my $raw = substr ( ${$self->{data}}, $manufacturer );
673 return unpack ( "Z*", $raw );
674 }
675
676 sub product {
677 my $hash = shift;
678 my $self = tied(%$hash);
679
680 my $product = $hash->{product};
681 return undef unless $product;
682
683 my $raw = substr ( ${$self->{data}}, $product );
684 return unpack ( "Z*", $raw );
685 }
686
687 ##############################################################################
688 #
689 # Option::ROM::UNDI
690 #
691 ##############################################################################
692
693 package Option::ROM::UNDI;
694
695 use strict;
696 use warnings;
697 use Carp;
698 use bytes;
699
700 sub new {
701 my $class = shift;
702 my $rom = shift;
703 my $offset = shift;
704
705 my $hash = {};
706 tie %$hash, "Option::ROM::Fields", {
707 rom => $rom,
708 data => $rom->{data},
709 offset => $offset,
710 length => 0x16,
711 fields => {
712 signature => { offset => 0x00, length => 0x04, pack => "a4" },
713 struct_length => { offset => 0x04, length => 0x01, pack => "C" },
714 checksum => { offset => 0x05, length => 0x01, pack => "C" },
715 struct_revision =>{ offset => 0x06, length => 0x01, pack => "C" },
716 version_revision =>{ offset => 0x07, length => 0x01, pack => "C" },
717 version_minor => { offset => 0x08, length => 0x01, pack => "C" },
718 version_major => { offset => 0x09, length => 0x01, pack => "C" },
719 loader_entry => { offset => 0x0a, length => 0x02, pack => "S" },
720 stack_size => { offset => 0x0c, length => 0x02, pack => "S" },
721 data_size => { offset => 0x0e, length => 0x02, pack => "S" },
722 code_size => { offset => 0x10, length => 0x02, pack => "S" },
723 bus_type => { offset => 0x12, length => 0x04, pack => "a4" },
724 },
725 };
726 bless $hash, $class;
727
728 my $self = tied ( %$hash );
729 my $length = $rom->{rom}->length ();
730
731 return undef unless ( $offset + $self->{length} <= $length &&
732 $hash->{signature} eq Option::ROM::UNDI_SIGNATURE &&
733 $offset + $hash->{struct_length} <= $length );
734
735 # Retrieve true length of structure
736 $self->{length} = $hash->{struct_length};
737
738 return $hash;
739 }
740
741 sub checksum {
742 my $hash = shift;
743 my $self = tied(%$hash);
744
745 return $self->checksum();
746 }
747
748 sub fix_checksum {
749 my $hash = shift;
750 my $self = tied(%$hash);
751
752 $hash->{checksum} = ( ( $hash->{checksum} - $hash->checksum() ) & 0xff );
753 }
754
755 ##############################################################################
756 #
757 # Option::ROM::iPXE
758 #
759 ##############################################################################
760
761 package Option::ROM::iPXE;
762
763 use strict;
764 use warnings;
765 use Carp;
766 use bytes;
767
768 sub new {
769 my $class = shift;
770 my $rom = shift;
771 my $offset = shift;
772
773 my $hash = {};
774 tie %$hash, "Option::ROM::Fields", {
775 rom => $rom,
776 data => $rom->{data},
777 offset => $offset,
778 length => 0x06,
779 fields => {
780 signature => { offset => 0x00, length => 0x04, pack => "a4" },
781 struct_length => { offset => 0x04, length => 0x01, pack => "C" },
782 checksum => { offset => 0x05, length => 0x01, pack => "C" },
783 shrunk_length => { offset => 0x06, length => 0x01, pack => "C" },
784 build_id => { offset => 0x08, length => 0x04, pack => "L" },
785 },
786 };
787 bless $hash, $class;
788
789 my $self = tied ( %$hash );
790 my $length = $rom->{rom}->length ();
791
792 return undef unless ( $offset + $self->{length} <= $length &&
793 $hash->{signature} eq Option::ROM::IPXE_SIGNATURE &&
794 $offset + $hash->{struct_length} <= $length );
795
796 # Retrieve true length of structure
797 $self->{length} = $hash->{struct_length};
798
799 return $hash;
800 }
801
802 sub checksum {
803 my $hash = shift;
804 my $self = tied(%$hash);
805
806 return $self->checksum();
807 }
808
809 sub fix_checksum {
810 my $hash = shift;
811 my $self = tied(%$hash);
812
813 $hash->{checksum} = ( ( $hash->{checksum} - $hash->checksum() ) & 0xff );
814 }
815
816 1;