thinkpad-ec/scripts/fix_mbr
2019-08-06 15:15:43 +01:00

257 lines
6.2 KiB
Perl
Executable File

#!/usr/bin/env perl
use warnings;
use strict;
#
# Copyright (C) 2017-2019 Hamish Coleman
#
# The Lenovo BIOS update ISO images contain an embedded hard drive image,
# complete with a partition table.
#
# Qemu does not support disk images with more than 16 heads, but the lenovo
# disk image is created with 64 heads. Since the MBR they used does not use
# LBA to access the disk, the mismatch between the Qemu reported disk data
# and what is in the partition confuses the MBR and causes the boot to fail.
#
use Data::Dumper;
$Data::Dumper::Indent = 1;
$Data::Dumper::Sortkeys = 1;
$Data::Dumper::Quotekeys = 0;
use IO::File;
sub mbr_get {
my $imagefile = shift;
my $fh = IO::File->new($imagefile,"r");
if (!defined($fh)) {
die("Could not open $imagefile: $!");
}
my $buf;
my $count = $fh->sysread($buf,512);
die("bad read") if ($count != 512);
return $buf;
}
sub mbr_put {
my $imagefile = shift;
my $buf = shift;
die("bad buf len") if (length($buf) != 512);
my $fh = IO::File->new($imagefile,"r+");
if (!defined($fh)) {
die("Could not open $imagefile: $!");
}
my $count = $fh->syswrite($buf,512);
die("bad write") if ($count != 512);
}
sub mbr_part_unpack {
my $part = shift;
my $result = {};
$result->{_input} = unpack('H*', $part);
my @fields = qw(
flag
start_head
start_cysec
start_cyl
type
end_head
end_cysec
end_cyl
sector_offset
sector_total
);
my @values = unpack("CCCCCCCCVV",$part);
map { $result->{$fields[$_]} = $values[$_] } (0..scalar(@fields)-1);
$result->{start_cyl} |= ($result->{start_cysec} & 0xc0) <<2;
$result->{start_sec} = $result->{start_cysec} & 0x3f;
delete $result->{start_cysec};
$result->{end_cyl} |= ($result->{end_cysec} & 0xc0) <<2;
$result->{end_sec} = $result->{end_cysec} & 0x3f;
delete $result->{end_cysec};
return $result;
}
sub mbr_part_pack {
my $part = shift;
# check sanity
die("start cyl too big") if ($part->{start_cyl} > 1023);
die("start sec too big") if ($part->{start_sec} > 0x3f);
die("end cyl too big") if ($part->{end_cyl} > 1023);
die("end sec too big") if ($part->{end_sec} > 0x3f);
# TODO
# - add support for larger cylinder numbers
# This needs adding support for the bitfiddling to move the highbits to
# the other fields.
# This should only occur when the .iso image file is larger than 255Meg
if ($part->{start_cyl} > 0xff) {
printf("Unsupported start_cyl (0x%x)\n", $part->{start_cyl});
die;
}
if ($part->{end_cyl} > 0xff) {
printf("Unsupported end_cyl (0x%x)\n", $part->{end_cyl});
die;
}
$part->{start_cysec} = $part->{start_sec};
$part->{end_cysec} = $part->{end_sec};
my @fields = qw(
flag
start_head
start_cysec
start_cyl
type
end_head
end_cysec
end_cyl
sector_offset
sector_total
);
my @values;
for my $field (@fields) {
push @values, $part->{$field};
}
my $result = pack("CCCCCCCCVV",@values);
$part->{_output} = unpack('H*', $result);
return $result;
}
sub mbr_unpack {
my $mbr = shift;
my $result = {};
$result->{_input} = unpack('H*', $mbr);
my @fields = qw(
bootstrap
diskid
partitions
signature
);
my @values = unpack("a436a10a64S",$mbr);
map { $result->{$fields[$_]} = $values[$_] } (0..scalar(@fields)-1);
my @partitions;
for my $part (unpack("(a16)*",$result->{partitions})) {
push @partitions, mbr_part_unpack($part);
}
$result->{partitions} = \@partitions;
return $result;
}
sub mbr_pack {
my $mbr = shift;
$mbr->{_partitions} = $mbr->{partitions};
my @partitions;
for my $part (@{$mbr->{partitions}}) {
push @partitions, mbr_part_pack($part);
}
$mbr->{partitions} = pack("(a16)*", @partitions);
my @fields = qw(
bootstrap
diskid
partitions
signature
);
my @values;
for my $field (@fields) {
push @values, $mbr->{$field};
}
my $result = pack("a436a10a64S",@values);
$mbr->{_output} = unpack('H*', $result);
return $result;
}
# This function does the actual work of this script
#
sub fixup_part {
my $part = shift;
# dont touch it if the partition is not broken
return undef if ($part->{end_head} < 0x10);
# convert from zero-based index to the total count
$part->{end_head}++;
$part->{end_cyl}++;
my $total_sec1 = $part->{end_sec} * $part->{end_head} * $part->{end_cyl};
# Reduce the number of heads and increase the number of cylinders
while ($part->{end_head} >0x10) {
$part->{end_head} = int($part->{end_head}/2);
$part->{end_cyl} *= 2;
}
my $total_sec2 = $part->{end_sec} * $part->{end_head} * $part->{end_cyl};
# Ensure we have at least as many total sectors as before the fixup
while ($total_sec2 < $total_sec1) {
$part->{end_cyl}++;
$total_sec2 = $part->{end_sec} * $part->{end_head} * $part->{end_cyl};
}
# convert from total count back to zero-based index
$part->{end_head}--;
$part->{end_cyl}--;
return 1;
}
# Some boot records downloaded from Lenovo appear to have been corrupted
# (perhaps this is an attempt to force people to use a UEFI boot?)
#
sub fixup_boot {
my $buf = shift;
if (ord(substr($buf,0,1)) == 0) {
# No normal x86 boot instruction starts with a zero.
warn("INFO: Original Lenovo ISO contains a zero in MBR bootcode - attempting fix\n");
substr($buf,0,1) = chr(0xfa);
}
return $buf;
}
sub main() {
if (!defined($ARGV[0])) {
die("Need image filename");
}
my $imagefile = $ARGV[0];
my $buf = mbr_get($imagefile);
my $mbr = mbr_unpack($buf);
for my $part (@{$mbr->{partitions}}) {
fixup_part($part);
}
$buf = mbr_pack($mbr);
$buf = fixup_boot($buf);
if (defined($ARGV[1]) && $ARGV[1] eq 'debug') {
print(Dumper($mbr));
} else {
mbr_put($imagefile,$buf);
}
}
unless (caller) {
# only run main if we are called as a CLI tool
main();
}