Perl script to extract partitions from OpenPOWER PNOR firmware images.
#!/usr/bin/perl -s | |
# v1.0 by Cameron Kaiser, talospace@floodgap.com | |
# Public domain | |
# By default extract Skiboot and Petitboot. | |
# Say -parts=DIFFERENT,PARTS to extract, you know, different parts. | |
# We do not support obtaining the 'part' partition yet (you can just do | |
# that by truncating the rest of the file anyway). | |
# Specifying a part multiple times is considered undefined behaviour. | |
$parts ||= "PAYLOAD,BOOTKERNEL"; | |
# Say -prefix=prefix to use a different filename prefix. | |
$prefix ||= "pnor."; | |
# Say -verbose or -v for more stuff. | |
$verbose ||= $v; | |
### NO USER SERVICEABLE PARTS BELOW THIS POINT ### | |
(@parts) = split(/,/, $parts); | |
die("Partition list doesn't make sense: $parts\n") if (!scalar(@parts)); | |
foreach(@parts) { | |
die("Obtaining 'part' is not supported yet\n") if ($_ eq 'part'); | |
$f = $prefix . $_; | |
die("$f already exists, not overwriting!\n") if (-e $f); | |
} | |
# Abuse <> to read either from stdin or a file argument. | |
# Slurp the magic number and set the filehandle all in one go. | |
$/ = \4; $magic = <>; | |
die("This isn't a PNOR archive: @{[ unpack('H*', $magic) ]}\n") | |
if ($magic ne 'PART'); | |
# Read remainder of PNOR header. | |
# These are big-endian 32-bit integers. | |
read(ARGV, $buf, 44); | |
die("PNOR header is incomplete!\n") if (length($buf) != 44); | |
($version, | |
$size, | |
$entrysize, | |
$entrycount, | |
$blocksize, | |
$blockcount, | |
$x, # reserved? | |
$x, | |
$x, | |
$x, | |
$chksum) = unpack("N11", $buf); | |
print "Version $version PNOR archive with $entrycount entries.\n"; | |
print "Warning: unexpected PNOR version!\n" if ($version != 1); | |
die "At least two entries must be in PNOR, aborting!\n" if ($entrycount < 2); | |
undef @finder; | |
undef %did_find; | |
$bytes_read = 48; | |
$first_non_part = 0; | |
# Read all the entries and figure out which ones we want. | |
for($i=0; $i<$entrycount; $i++) { | |
read(ARGV, $buf, $entrysize); | |
die("Truncated directory entry found, aborting!\n") | |
if (length($buf) != $entrysize); | |
# Extract partition name, base offset and length (in 4K pages). | |
# We don't care about the rest here. | |
# Partition name is the first 16 bytes, null padded. | |
($pname, | |
$base, | |
$length, | |
$x) = unpack("H32NNH*", $buf); | |
# Length must be at least 2 pages. | |
die("Illegal length $length, aborting\n") if ($length < 2); | |
1 while ($pname =~ s/00$//); | |
die("Null partition name in directory entry, aborting\n") | |
if (!length($pname)); | |
$pname = pack("H*", $pname); | |
# We assume that the partition table is in the lead position. | |
die("Partition table was not in position zero!\n") | |
if ($i == 0 && $pname != "part"); | |
print "$pname (offset $base, length $length)\n" if ($verbose); | |
print "Warning: duplicate partition $pname, last one will be used\n" | |
if ($did_find{$pname}++); | |
foreach $w (@parts) { | |
if ($w eq $pname) { | |
# Found it. Extract base offset and length (in | |
# 4K pages). | |
push(@finder, [$w, $base, $length]); | |
} | |
} | |
# Remember the partition *after* "part", because our data starts there. | |
$first_non_part = $base if ($i == 1); | |
$bytes_read += $entrysize; | |
} | |
foreach (@parts) { | |
print "Warning: partition $_ was not found, skipping\n" | |
if (!$did_find{$_}); | |
} | |
if (!scalar(@finder)) { | |
print "No partitions extracted.\n"; | |
print "Use -verbose to see more info.\n" unless ($verbose); | |
exit 1; | |
} | |
# Skip over the rest of "part" since we know it must be first. | |
$skip = ($first_non_part * 4096) - $bytes_read; | |
print "Skipping partition table ($skip bytes to offset $first_non_part)\n" | |
if ($verbose); | |
read(ARGV, $buf, $skip); | |
die("Truncated file, aborting!\n") if (length($buf) != $skip); | |
# We now have a sorted "edit decision" list. Extract in file order. | |
# Read in 4K pages until we get the right spot since we can't use seek() | |
# in case this is coming in over stdin. | |
$fpos = 0; | |
foreach $entry (@finder) { | |
$w = $entry->[0]; | |
$base = ($entry->[1] - $first_non_part); | |
$length = $entry->[2]; | |
while ($fpos < $base) { | |
read(ARGV, $buf, 4096); | |
die("Truncated partition, aborting!\n") | |
if (length($buf) != 4096); | |
$fpos++; | |
} | |
print "Extracting $w at offset $base.\n"; | |
open(K, ">${prefix}${w}") || die("Unable to write: $!\n"); | |
for($i=0; $i<$length; $i++) { | |
read(ARGV, $buf, 4096); | |
die("Truncated partition, aborting!\n") | |
if (length($buf) != 4096); | |
$fpos++; | |
# The first page contains metadata and is skipped. | |
next unless ($i); | |
print K $buf; | |
# For the second page only, analyse the magic number. | |
next unless ($i == 1); | |
$magic = unpack("H8", $buf); | |
if ($magic eq 'fd377a58') { | |
print "This is a xz format image.\n"; | |
} elsif ($magic eq '7f454c46') { | |
print "This is an ELF executable image.\n"; | |
} | |
} | |
close(K); | |
$length--; | |
print "Wrote @{[ $length * 4 ]}K successfully.\n"; | |
} | |
print "Extracted @{[ scalar(@finder) ]} partitions successfully.\n"; | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment