[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[Qemu-block] [PATCH] scripts/dump-qcow2.pl: Script to dump qcow2 metadat
From: |
Alberto Garcia |
Subject: |
[Qemu-block] [PATCH] scripts/dump-qcow2.pl: Script to dump qcow2 metadata |
Date: |
Wed, 28 Mar 2018 16:38:45 +0300 |
This script takes a qcow2 image and dumps its metadata: header,
snapshot table and some extensions (although not all qcow2 features
are supported yet).
It can also display a list of all host clusters and the guest -> host
address mappings, so it's useful to debug allocations.
The image is assumed not to be corrupted, and this script does not do
consistency checks (yet).
Signed-off-by: Alberto Garcia <address@hidden>
---
scripts/dump-qcow2.pl | 425 ++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 425 insertions(+)
create mode 100755 scripts/dump-qcow2.pl
diff --git a/scripts/dump-qcow2.pl b/scripts/dump-qcow2.pl
new file mode 100755
index 0000000000..7eb9a0feda
--- /dev/null
+++ b/scripts/dump-qcow2.pl
@@ -0,0 +1,425 @@
+#!/usr/bin/env perl
+# Copyright (C) 2018 Igalia, S.L.
+#
+# Authors:
+# Alberto Garcia <address@hidden>
+#
+# This work is licensed under the terms of the GNU GPL, version 2 or
+# later. See the COPYING file in the top-level directory.
+
+use strict;
+use warnings;
+use Fcntl qw(SEEK_SET SEEK_CUR);
+use POSIX qw(ceil);
+use Getopt::Long;
+
+sub showUsage {
+ print "Usage: dump-qcow2.pl [options] <qcow2-file>\n";
+ print "\nOptions:\n";
+ print "\t--host Show host cluster types\n";
+ print "\t--guest Show guest cluster mappings\n";
+ print "\t--snapshot=<id> Use this snapshot's L1 table for the mappings\n";
+ exit 0;
+}
+
+# Command-line parameters
+my $printHost;
+my $printGuest;
+my $snapshot = '';
+GetOptions('guest' => \$printGuest,
+ 'host' => \$printHost,
+ 'snapshot=s' => \$snapshot) or showUsage();
+
+# Open the qcow2 image
address@hidden == 1 or showUsage();
+my $file = $ARGV[0];
+-e $file or die "File '$file' not found\n";
+open(FILE, $file) or die "Cannot open '$file'\n";
+
+# Some global variables
+my $refcount_order = 4;
+my $file_size = -s $file;
+my $header_size = 72;
+my $incompat_bits;
+my $compat_bits;
+my $autoclear_bits;
+my @l2_tables;
+my @clusters = ( "QCOW2 Header" ); # Contents of host clusters
+my @crypt_methods = qw(no AES LUKS);
+
+# Read all common header fields
+die "Not a qcow2 file\n" if (read_uint32() != 0x514649fb);
+my $qcow2_version = read_uint32();
+my $backing_offset = read_uint64();
+my $backing_size = read_uint32();
+my $cluster_bits = read_uint32();
+my $virtual_size = read_uint64();
+my $crypt_method = read_uint32();
+my $l1_size = read_uint32();
+my $l1_offset = read_uint64();
+my $refcount_offset = read_uint64();
+my $refcount_size = read_uint32();
+my $n_snapshots = read_uint32();
+my $snapshot_offset = read_uint64();
+
+if ($qcow2_version != 2 && $qcow2_version != 3) {
+ die "Unknown QCOW2 version: $qcow2_version\n";
+}
+
+# Read v3-specific fields
+if ($qcow2_version == 3) {
+ $incompat_bits = read_uint64();
+ $compat_bits = read_uint64();
+ $autoclear_bits = read_uint64();
+ $refcount_order = read_uint32();
+ $header_size = read_uint32();
+}
+
+# Values calculated from the header fields
+my $cluster_size = 1 << $cluster_bits;
+my $refcount_bits = 1 << $refcount_order;
+my $n_clusters = ceil($file_size / $cluster_size);
+my $l1_size_clusters = ceil($l1_size * 8 / $cluster_size);
+my $l1_size_expected = ceil($virtual_size * 8 / $cluster_size / $cluster_size);
+
+foreach my $i (0..$l1_size_clusters - 1) {
+ $clusters[ncluster($l1_offset) + $i] = "L1 table $i [active]";
+}
+foreach my $i (0..$refcount_size-1) {
+ $clusters[ncluster($refcount_offset) + $i] = "Refcount table $i";
+}
+if ($snapshot_offset > 0) {
+ $clusters[ncluster($snapshot_offset)] = "Snapshot table";
+}
+
+# Print a summary of the header
+print "QCOW2 version $qcow2_version\n";
+print "Header size: $header_size bytes\n";
+print "File size: ".prettyBytes($file_size)."\n";
+print "Virtual size: ".prettyBytes($virtual_size)."\n";
+print "Cluster size: ".prettyBytes($cluster_size)." ($n_clusters clusters)\n";
+if ($backing_offset != 0) {
+ sysseek(FILE, $backing_offset, SEEK_SET);
+ my $backing_name = read_str($backing_size);
+ printf("Backing file offset: 0x%u (%u bytes): %s\n",
+ $backing_offset, $backing_size, $backing_name);
+}
+print "Number of snapshots: $n_snapshots\n";
+printf("Refcount table: %u cluster(s) (%u entries) ".
+ "at 0x%x (#%u)\n", $refcount_size,
+ $refcount_size * $cluster_size / 8,
+ $refcount_offset, ncluster($refcount_offset));
+printf("Refcount entry size: %u bits (%u entries per block)\n",
+ $refcount_bits, $cluster_size * 8 / $refcount_bits);
+printf("L1 table: %u entries at 0x%x (#%u)\n",
+ $l1_size, $l1_offset, ncluster($l1_offset));
+if ($l1_size != $l1_size_expected) {
+ printf("Expected L1 entries based on the virtual size: %u\n",
+ $l1_size_expected);
+}
+printf("Encrypted: %s\n", $crypt_methods[$crypt_method] || "unknown method");
+
+# Header extensions and additional sections
+if ($qcow2_version == 3) {
+ print "Incompatible bits:";
+ print " none" if ($incompat_bits == 0);
+ print " dirty" if ($incompat_bits & 1);
+ print " corrupted" if ($incompat_bits & 2);
+ print " unknown" if ($incompat_bits >> 2);
+ print "\n";
+
+ print "Compatible bits:";
+ print " none" if ($compat_bits == 0);
+ print " lazy_refcounts" if ($compat_bits & 1);
+ print " unknown" if ($compat_bits >> 1);
+ print "\n";
+
+ print "Autoclear bits:";
+ print " none" if ($autoclear_bits == 0);
+ print " bitmaps_extension" if ($autoclear_bits & 1);
+ print " unknown" if ($autoclear_bits >> 1);
+ print "\n";
+
+ my ($i, $hdr_type, $hdr_len) = (0);
+ sysseek(FILE, $header_size, SEEK_SET);
+ do {
+ $hdr_type = read_uint32();
+ $hdr_len = (read_uint32() + 7) & ~7; # Round to the next multiple of 8
+ if ($hdr_type == 0) {
+ # No more extensions
+ } elsif ($hdr_type == 0xE2792ACA) {
+ print "Header extension $i: backing file format name\n";
+ } elsif ($hdr_type == 0x6803f857) {
+ print "Header extension $i: feature table name\n";
+ for (my $ft_num = 0; $hdr_len >= 48; $hdr_len -= 48) {
+ my @ft_types = ("incompatible", "compatible", "autoclear");
+ my $ft_type = read_uint8();
+ my $ft_bit = read_uint8();
+ my $ft_name = read_str(46);
+ printf(" Feature %d: %s (%s, bit %d)\n",
+ $ft_num++, $ft_name, $ft_types[$ft_type], $ft_bit);
+ }
+ } elsif ($hdr_type == 0x23852875) {
+ print "Header extension $i: bitmaps extension\n";
+ } elsif ($hdr_type == 0x0537be77) {
+ print "Header extension $i: full disk encryption\n";
+ my $fde_offset = read_uint64();
+ my $fde_length = read_uint64();
+ $hdr_len -= 16;
+ my $fde_cluster = ncluster($fde_offset);
+ my $fde_nclusters = ceil($fde_length / $cluster_size);
+ foreach my $i (0..$fde_nclusters-1) {
+ $clusters[$fde_cluster + $i] = "Full disk encryption header
$i";
+ }
+ printf(" Offset 0x%x (#%u), length %u bytes (%u clusters)\n",
+ $fde_offset, $fde_cluster, $fde_length, $fde_nclusters);
+ } else {
+ printf("Header extension $i: unknown (0x%x)\n", $hdr_type);
+ }
+ sysseek(FILE, $hdr_len, SEEK_CUR);
+ $i++;
+ } while ($hdr_type != 0);
+}
+
+if ($incompat_bits >> 2) {
+ die "Incompatible bits found, aborting\n";
+}
+
+# Read the snapshot table
+my %snap_l1_offsets = read_snapshot_table();
+if ($snapshot ne '' && !defined($snap_l1_offsets{$snapshot})) {
+ die "Snapshot $snapshot not found\n";
+}
+
+read_refcount_table();
+
+# Read and parse the active L1/L2 tables
+my @l1_table = read_l1_table($l1_offset);
+read_l2_tables("active", address@hidden, $snapshot eq "");
+
+# Read all L1/L2 tables from all snapshots
+foreach my $id (keys(%snap_l1_offsets)) {
+ my $off = $snap_l1_offsets{$id};
+ @l1_table = read_l1_table($off);
+ read_l2_tables("snapshot $id", address@hidden, $snapshot eq $id);
+}
+
+close(FILE);
+
+printHostClusters() if $printHost;
+printGuestClusters() if $printGuest;
+
+exit 0;
+
+# Subroutines
+
+sub read_l1_table {
+ my $offset = shift;
+ my @table;
+ sysseek(FILE, $offset, SEEK_SET);
+ foreach my $i (0..$l1_size-1) {
+ $table[$i] = read_uint64();
+ }
+ return @table;
+}
+
+sub read_l2_tables {
+ my $name = shift;
+ my $l1_table = shift;
+ my $selected_l1_table = shift;
+ foreach my $i (0..$#{$l1_table}) {
+ my $l2_offset = ${$l1_table}[$i] & ((1<<56) - (1<<9));
+ if ($l2_offset != 0) {
+ my $l2_table =
+ read_one_l2_table($i, $l2_offset, $selected_l1_table);
+ if ($selected_l1_table) {
+ $l2_tables[$i] = $l2_table;
+ }
+ $clusters[ncluster($l2_offset)] = "L2 table $i [$name]";
+ }
+ }
+}
+
+sub read_one_l2_table {
+ my $table_num = shift;
+ my $l2_offset = shift;
+ my $selected_l1_table = shift;
+ my $num_l2_entries = $cluster_size / 8;
+ my @l2_table;
+ sysseek(FILE, $l2_offset, SEEK_SET);
+ foreach my $j (0..$num_l2_entries-1) {
+ my $entry = read_uint64();
+ my $compressed = ($entry >> 62) & 1;
+ if ($compressed) {
+ my $csize_shift = 62 - ($cluster_bits - 8);
+ my $csize_mask = (1 << ($cluster_bits - 8)) - 1;
+ my $csize = (($entry >> $csize_shift) & $csize_mask) + 1;
+ my $coffset = $entry & ((1 << $csize_shift) - 1);
+ my $cluster1 = ncluster($coffset);
+ my $cluster2 = ncluster($coffset + ($csize * 512) - 1);
+ foreach my $k ($cluster1..$cluster2) {
+ if (!$clusters[$k]) {
+ $clusters[$k] = "Compressed cluster";
+ }
+ }
+ if ($cluster1 == $cluster2) {
+ $l2_table[$j] =
+ sprintf("0x%x (#%u) (compressed, %d sectors)",
+ $coffset, $cluster1, $csize);
+ } else {
+ $l2_table[$j] =
+ sprintf("0x%x (#%u - #%u) (compressed, %d sectors)",
+ $coffset, $cluster1, $cluster2, $csize);
+ }
+ } else {
+ my $guest_off = ($table_num << ($cluster_bits * 2 - 3)) +
+ ($j << $cluster_bits);
+ my $host_off = $entry & ((1<<56) - (1<<9));
+ my $host_cluster = ncluster($host_off);
+ my $all_zeros = $entry & 1;
+ if ($all_zeros) {
+ if ($host_off != 0) {
+ $l2_table[$j] = sprintf("0x%x (#%u) (all zeros)",
+ $host_off, ncluster($host_off));
+ if ($selected_l1_table) {
+ $clusters[$host_cluster] =
+ sprintf("All zeros [guest 0x%x (#%u)]",
+ $guest_off, ncluster($guest_off));
+ } elsif (!$clusters[$host_cluster]) {
+ $clusters[$host_cluster] = "All zeros";
+ }
+ } else {
+ $l2_table[$j] = "All zeros";
+ }
+ } else {
+ if ($host_off != 0) {
+ $l2_table[$j] = sprintf("0x%x (#%u)",
+ $host_off, ncluster($host_off));
+ if ($selected_l1_table) {
+ $clusters[$host_cluster] =
+ sprintf("Data cluster [guest 0x%x (#%u)]",
+ $guest_off, ncluster($guest_off));
+ } elsif (!$clusters[$host_cluster]) {
+ $clusters[$host_cluster] = "Data cluster";
+ }
+ }
+ }
+ }
+ }
+ return address@hidden;
+}
+
+sub read_refcount_table {
+ my $n_entries = $refcount_size * $cluster_size / 8;
+ sysseek(FILE, $refcount_offset, SEEK_SET);
+ foreach my $i (0..$n_entries-1) {
+ my $entry = read_uint64() & ~511;
+ if ($entry != 0) {
+ $clusters[ncluster($entry)] = "Refcount block $i";
+ }
+ }
+}
+
+sub read_snapshot_table {
+ my %l1_offsets;
+ sysseek(FILE, $snapshot_offset, SEEK_SET);
+ for (my $i = 0; $i < $n_snapshots; $i++) {
+ my $snap_l1_offset = read_uint64();
+ my $snap_l1_size = read_uint32();
+ my $snap_l1_size_clusters = ceil($snap_l1_size * 8 / $cluster_size);
+ my $snap_id_len = read_uint16();
+ my $snap_name_len = read_uint16();
+ sysseek(FILE, 20, SEEK_CUR);
+ my $snap_extra_len = read_uint32();
+ sysseek(FILE, $snap_extra_len, SEEK_CUR);
+ my $snap_id = read_str($snap_id_len);
+ my $snap_name = read_str($snap_name_len);
+
+ my $snap_var_len = $snap_id_len + $snap_name_len + $snap_extra_len;
+ if ($snap_var_len & 7) {
+ sysseek(FILE, 8 - ($snap_var_len & 7), SEEK_CUR);
+ }
+
+ $l1_offsets{$snap_id} = $snap_l1_offset;
+
+ foreach my $i (0..$snap_l1_size_clusters - 1) {
+ $clusters[ncluster($snap_l1_offset) + $i] =
+ "L1 table $i [snapshot $snap_id]";
+ }
+ print "\nSnapshot #$snap_id ($snap_name)\n";
+ printf("L1 table: %u entries at 0x%x (#%u)\n",
+ $snap_l1_size, $snap_l1_offset, ncluster($snap_l1_offset));
+ }
+ return %l1_offsets;
+}
+
+sub read_uint8 {
+ my $data;
+ sysread(FILE, $data, 1);
+ return unpack('C', $data);
+}
+
+sub read_uint16 {
+ my $data;
+ sysread(FILE, $data, 2);
+ return unpack('S>', $data);
+}
+
+sub read_uint32 {
+ my $data;
+ sysread(FILE, $data, 4);
+ return unpack('L>', $data);
+}
+
+sub read_uint64 {
+ my $data;
+ sysread(FILE, $data, 8);
+ return unpack('Q>', $data);
+}
+
+sub read_str {
+ my $maxlen = shift;
+ my $data;
+ sysread(FILE, $data, $maxlen);
+ if (index($data,chr(0)) != -1) {
+ return substr($data, 0, index($data,chr(0))); # Strip trailing NULLs
+ } else {
+ return $data;
+ }
+}
+
+sub prettyBytes {
+ my $size = $_[0];
+ foreach ('B','KiB','MiB','GiB','TiB','PiB','EiB') {
+ return sprintf("%.2f ",$size)."$_" if $size < 1024;
+ $size /= 1024;
+ }
+}
+
+sub printHostClusters {
+ print "\nHost clusters\n";
+ foreach my $i (0..$n_clusters-1) {
+ if ($clusters[$i]) {
+ printf "0x%x (#%u): $clusters[$i]\n", $i * $cluster_size, $i;
+ }
+ }
+}
+
+sub printGuestClusters {
+ print "\nGuest address -> Host address\n";
+ for (my $i = 0; $i < $virtual_size; $i += $cluster_size) {
+ my $l1_index = $i >> ($cluster_bits * 2 - 3);
+ my $l2_index = ncluster($i) & ((1 << ($cluster_bits - 3)) - 1);
+ my $l2_table = $l2_tables[$l1_index];
+ my $mapping = ${$l2_table}[$l2_index];
+ if ($mapping) {
+ printf("0x%x (#%u): %s\n", $i, ncluster($i), $mapping);
+ }
+ }
+}
+
+# Return cluster number from its absolute offset
+sub ncluster {
+ my $offset = shift;
+ return $offset >> $cluster_bits;
+}
--
2.11.0
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [Qemu-block] [PATCH] scripts/dump-qcow2.pl: Script to dump qcow2 metadata,
Alberto Garcia <=