Skip to content

Instantly share code, notes, and snippets.

Created Jun 22, 2020
What would you like to do?
Perl script to extract partitions from OpenPOWER PNOR firmware images.
#!/usr/bin/perl -s
# v1.0 by Cameron Kaiser,
# 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.
# Say -prefix=prefix to use a different filename prefix.
$prefix ||= "pnor.";
# Say -verbose or -v for more stuff.
$verbose ||= $v;
(@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);
$x, # reserved?
$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.
$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);
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);
# 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";
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