From nobody Mon Apr 29 15:10:46 2024 Delivered-To: importer@patchew.org Received-SPF: pass (zoho.com: domain of gnu.org designates 208.118.235.17 as permitted sender) client-ip=208.118.235.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists.gnu.org; Authentication-Results: mx.zohomail.com; dkim=fail; spf=pass (zoho.com: domain of gnu.org designates 208.118.235.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org Return-Path: Received: from lists.gnu.org (lists.gnu.org [208.118.235.17]) by mx.zohomail.com with SMTPS id 1522244447402713.976038650374; Wed, 28 Mar 2018 06:40:47 -0700 (PDT) Received: from localhost ([::1]:39362 helo=lists.gnu.org) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1f1BJm-0006GU-IS for importer@patchew.org; Wed, 28 Mar 2018 09:40:46 -0400 Received: from eggs.gnu.org ([2001:4830:134:3::10]:55524) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1f1BIr-0005ic-GI for qemu-devel@nongnu.org; Wed, 28 Mar 2018 09:39:51 -0400 Received: from Debian-exim by eggs.gnu.org with spam-scanned (Exim 4.71) (envelope-from ) id 1f1BIo-0003TJ-9d for qemu-devel@nongnu.org; Wed, 28 Mar 2018 09:39:49 -0400 Received: from fanzine.igalia.com ([91.117.99.155]:51914) by eggs.gnu.org with esmtps (TLS1.0:RSA_AES_128_CBC_SHA1:16) (Exim 4.71) (envelope-from ) id 1f1BIn-0003Dr-PY; Wed, 28 Mar 2018 09:39:46 -0400 Received: from [194.100.51.2] (helo=perseus.local) by fanzine.igalia.com with esmtpsa (Cipher TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim) id 1f1BIB-00036T-Vq; Wed, 28 Mar 2018 15:39:08 +0200 Received: from berto by perseus.local with local (Exim 4.89) (envelope-from ) id 1f1BHv-0005NV-9E; Wed, 28 Mar 2018 16:38:51 +0300 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=igalia.com; s=20170329; h=Message-Id:Date:Subject:Cc:To:From; bh=TA2XbKM+E4yCK0K3YbPkw9Bl6KKlYxe/WKpOSSivSpo=; b=Pdz1IZd2oMeaE0+1Rp3JC3EpUgqUip3V1nrKT1Xdmu88BuG59Z9BrO0c2s4tZ/0V+PbNHGCTXvNeHdlJuIQ1ZnvHDKYcLjrNjJtq4LSTgZZ78gXqRxfrm3jSnAwuCmWIE2CR0KJJEcu5QME2+kgYqbF+yHczKc9slD0gj7xEssuQj3BjxE7IoXqwxed+xP028JC2Z/ZbKj3PVHeuHin/TQpNfJTSvpbLdxSBW1xTKFdip0erJy0PZXmDtQ4iqm513RgeGOC3Zv4maQhXXl7Yk+UqoBpLLAGWuI7M0HS+AyPuVSqtu+e0VeZneqtvLcQyO8NVzpPa5lhoisU+Zq/AaQ==; From: Alberto Garcia To: qemu-devel@nongnu.org Date: Wed, 28 Mar 2018 16:38:45 +0300 Message-Id: <20180328133845.20632-1-berto@igalia.com> X-Mailer: git-send-email 2.11.0 X-detected-operating-system: by eggs.gnu.org: GNU/Linux 2.2.x-3.x (no timestamps) [generic] [fuzzy] X-Received-From: 91.117.99.155 Subject: [Qemu-devel] [PATCH] scripts/dump-qcow2.pl: Script to dump qcow2 metadata X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.21 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Cc: Kevin Wolf , Alberto Garcia , qemu-block@nongnu.org, Max Reitz Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: "Qemu-devel" X-ZohoMail-DKIM: fail (Header signature does not verify) X-ZohoMail: RDKM_2 RSF_0 Z_629925259 SPT_0 Content-Transfer-Encoding: quoted-printable MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" 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 --- 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 +# +# 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] \n"; + print "\nOptions:\n"; + print "\t--host Show host cluster types\n"; + print "\t--guest Show guest cluster mappings\n"; + print "\t--snapshot=3D Use this snapshot's L1 table for the mappi= ngs\n"; + exit 0; +} + +# Command-line parameters +my $printHost; +my $printGuest; +my $snapshot =3D ''; +GetOptions('guest' =3D> \$printGuest, + 'host' =3D> \$printHost, + 'snapshot=3Ds' =3D> \$snapshot) or showUsage(); + +# Open the qcow2 image +@ARGV =3D=3D 1 or showUsage(); +my $file =3D $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 =3D 4; +my $file_size =3D -s $file; +my $header_size =3D 72; +my $incompat_bits; +my $compat_bits; +my $autoclear_bits; +my @l2_tables; +my @clusters =3D ( "QCOW2 Header" ); # Contents of host clusters +my @crypt_methods =3D qw(no AES LUKS); + +# Read all common header fields +die "Not a qcow2 file\n" if (read_uint32() !=3D 0x514649fb); +my $qcow2_version =3D read_uint32(); +my $backing_offset =3D read_uint64(); +my $backing_size =3D read_uint32(); +my $cluster_bits =3D read_uint32(); +my $virtual_size =3D read_uint64(); +my $crypt_method =3D read_uint32(); +my $l1_size =3D read_uint32(); +my $l1_offset =3D read_uint64(); +my $refcount_offset =3D read_uint64(); +my $refcount_size =3D read_uint32(); +my $n_snapshots =3D read_uint32(); +my $snapshot_offset =3D read_uint64(); + +if ($qcow2_version !=3D 2 && $qcow2_version !=3D 3) { + die "Unknown QCOW2 version: $qcow2_version\n"; +} + +# Read v3-specific fields +if ($qcow2_version =3D=3D 3) { + $incompat_bits =3D read_uint64(); + $compat_bits =3D read_uint64(); + $autoclear_bits =3D read_uint64(); + $refcount_order =3D read_uint32(); + $header_size =3D read_uint32(); +} + +# Values calculated from the header fields +my $cluster_size =3D 1 << $cluster_bits; +my $refcount_bits =3D 1 << $refcount_order; +my $n_clusters =3D ceil($file_size / $cluster_size); +my $l1_size_clusters =3D ceil($l1_size * 8 / $cluster_size); +my $l1_size_expected =3D ceil($virtual_size * 8 / $cluster_size / $cluster= _size); + +foreach my $i (0..$l1_size_clusters - 1) { + $clusters[ncluster($l1_offset) + $i] =3D "L1 table $i [active]"; +} +foreach my $i (0..$refcount_size-1) { + $clusters[ncluster($refcount_offset) + $i] =3D "Refcount table $i"; +} +if ($snapshot_offset > 0) { + $clusters[ncluster($snapshot_offset)] =3D "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 !=3D 0) { + sysseek(FILE, $backing_offset, SEEK_SET); + my $backing_name =3D 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 !=3D $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 =3D=3D 3) { + print "Incompatible bits:"; + print " none" if ($incompat_bits =3D=3D 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 =3D=3D 0); + print " lazy_refcounts" if ($compat_bits & 1); + print " unknown" if ($compat_bits >> 1); + print "\n"; + + print "Autoclear bits:"; + print " none" if ($autoclear_bits =3D=3D 0); + print " bitmaps_extension" if ($autoclear_bits & 1); + print " unknown" if ($autoclear_bits >> 1); + print "\n"; + + my ($i, $hdr_type, $hdr_len) =3D (0); + sysseek(FILE, $header_size, SEEK_SET); + do { + $hdr_type =3D read_uint32(); + $hdr_len =3D (read_uint32() + 7) & ~7; # Round to the next multipl= e of 8 + if ($hdr_type =3D=3D 0) { + # No more extensions + } elsif ($hdr_type =3D=3D 0xE2792ACA) { + print "Header extension $i: backing file format name\n"; + } elsif ($hdr_type =3D=3D 0x6803f857) { + print "Header extension $i: feature table name\n"; + for (my $ft_num =3D 0; $hdr_len >=3D 48; $hdr_len -=3D 48) { + my @ft_types =3D ("incompatible", "compatible", "autoclear= "); + my $ft_type =3D read_uint8(); + my $ft_bit =3D read_uint8(); + my $ft_name =3D read_str(46); + printf(" Feature %d: %s (%s, bit %d)\n", + $ft_num++, $ft_name, $ft_types[$ft_type], $ft_bit); + } + } elsif ($hdr_type =3D=3D 0x23852875) { + print "Header extension $i: bitmaps extension\n"; + } elsif ($hdr_type =3D=3D 0x0537be77) { + print "Header extension $i: full disk encryption\n"; + my $fde_offset =3D read_uint64(); + my $fde_length =3D read_uint64(); + $hdr_len -=3D 16; + my $fde_cluster =3D ncluster($fde_offset); + my $fde_nclusters =3D ceil($fde_length / $cluster_size); + foreach my $i (0..$fde_nclusters-1) { + $clusters[$fde_cluster + $i] =3D "Full disk encryption hea= der $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 !=3D 0); +} + +if ($incompat_bits >> 2) { + die "Incompatible bits found, aborting\n"; +} + +# Read the snapshot table +my %snap_l1_offsets =3D 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 =3D read_l1_table($l1_offset); +read_l2_tables("active", \@l1_table, $snapshot eq ""); + +# Read all L1/L2 tables from all snapshots +foreach my $id (keys(%snap_l1_offsets)) { + my $off =3D $snap_l1_offsets{$id}; + @l1_table =3D read_l1_table($off); + read_l2_tables("snapshot $id", \@l1_table, $snapshot eq $id); +} + +close(FILE); + +printHostClusters() if $printHost; +printGuestClusters() if $printGuest; + +exit 0; + +# Subroutines + +sub read_l1_table { + my $offset =3D shift; + my @table; + sysseek(FILE, $offset, SEEK_SET); + foreach my $i (0..$l1_size-1) { + $table[$i] =3D read_uint64(); + } + return @table; +} + +sub read_l2_tables { + my $name =3D shift; + my $l1_table =3D shift; + my $selected_l1_table =3D shift; + foreach my $i (0..$#{$l1_table}) { + my $l2_offset =3D ${$l1_table}[$i] & ((1<<56) - (1<<9)); + if ($l2_offset !=3D 0) { + my $l2_table =3D + read_one_l2_table($i, $l2_offset, $selected_l1_table); + if ($selected_l1_table) { + $l2_tables[$i] =3D $l2_table; + } + $clusters[ncluster($l2_offset)] =3D "L2 table $i [$name]"; + } + } +} + +sub read_one_l2_table { + my $table_num =3D shift; + my $l2_offset =3D shift; + my $selected_l1_table =3D shift; + my $num_l2_entries =3D $cluster_size / 8; + my @l2_table; + sysseek(FILE, $l2_offset, SEEK_SET); + foreach my $j (0..$num_l2_entries-1) { + my $entry =3D read_uint64(); + my $compressed =3D ($entry >> 62) & 1; + if ($compressed) { + my $csize_shift =3D 62 - ($cluster_bits - 8); + my $csize_mask =3D (1 << ($cluster_bits - 8)) - 1; + my $csize =3D (($entry >> $csize_shift) & $csize_mask) + 1; + my $coffset =3D $entry & ((1 << $csize_shift) - 1); + my $cluster1 =3D ncluster($coffset); + my $cluster2 =3D ncluster($coffset + ($csize * 512) - 1); + foreach my $k ($cluster1..$cluster2) { + if (!$clusters[$k]) { + $clusters[$k] =3D "Compressed cluster"; + } + } + if ($cluster1 =3D=3D $cluster2) { + $l2_table[$j] =3D + sprintf("0x%x (#%u) (compressed, %d sectors)", + $coffset, $cluster1, $csize); + } else { + $l2_table[$j] =3D + sprintf("0x%x (#%u - #%u) (compressed, %d sectors)", + $coffset, $cluster1, $cluster2, $csize); + } + } else { + my $guest_off =3D ($table_num << ($cluster_bits * 2 - 3)) + + ($j << $cluster_bits); + my $host_off =3D $entry & ((1<<56) - (1<<9)); + my $host_cluster =3D ncluster($host_off); + my $all_zeros =3D $entry & 1; + if ($all_zeros) { + if ($host_off !=3D 0) { + $l2_table[$j] =3D sprintf("0x%x (#%u) (all zeros)", + $host_off, ncluster($host_off)= ); + if ($selected_l1_table) { + $clusters[$host_cluster] =3D + sprintf("All zeros [guest 0x%x (#%u)]", + $guest_off, ncluster($guest_off)); + } elsif (!$clusters[$host_cluster]) { + $clusters[$host_cluster] =3D "All zeros"; + } + } else { + $l2_table[$j] =3D "All zeros"; + } + } else { + if ($host_off !=3D 0) { + $l2_table[$j] =3D sprintf("0x%x (#%u)", + $host_off, ncluster($host_off)= ); + if ($selected_l1_table) { + $clusters[$host_cluster] =3D + sprintf("Data cluster [guest 0x%x (#%u)]", + $guest_off, ncluster($guest_off)); + } elsif (!$clusters[$host_cluster]) { + $clusters[$host_cluster] =3D "Data cluster"; + } + } + } + } + } + return \@l2_table; +} + +sub read_refcount_table { + my $n_entries =3D $refcount_size * $cluster_size / 8; + sysseek(FILE, $refcount_offset, SEEK_SET); + foreach my $i (0..$n_entries-1) { + my $entry =3D read_uint64() & ~511; + if ($entry !=3D 0) { + $clusters[ncluster($entry)] =3D "Refcount block $i"; + } + } +} + +sub read_snapshot_table { + my %l1_offsets; + sysseek(FILE, $snapshot_offset, SEEK_SET); + for (my $i =3D 0; $i < $n_snapshots; $i++) { + my $snap_l1_offset =3D read_uint64(); + my $snap_l1_size =3D read_uint32(); + my $snap_l1_size_clusters =3D ceil($snap_l1_size * 8 / $cluster_si= ze); + my $snap_id_len =3D read_uint16(); + my $snap_name_len =3D read_uint16(); + sysseek(FILE, 20, SEEK_CUR); + my $snap_extra_len =3D read_uint32(); + sysseek(FILE, $snap_extra_len, SEEK_CUR); + my $snap_id =3D read_str($snap_id_len); + my $snap_name =3D read_str($snap_name_len); + + my $snap_var_len =3D $snap_id_len + $snap_name_len + $snap_extra_l= en; + if ($snap_var_len & 7) { + sysseek(FILE, 8 - ($snap_var_len & 7), SEEK_CUR); + } + + $l1_offsets{$snap_id} =3D $snap_l1_offset; + + foreach my $i (0..$snap_l1_size_clusters - 1) { + $clusters[ncluster($snap_l1_offset) + $i] =3D + "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 =3D shift; + my $data; + sysread(FILE, $data, $maxlen); + if (index($data,chr(0)) !=3D -1) { + return substr($data, 0, index($data,chr(0))); # Strip trailing NUL= Ls + } else { + return $data; + } +} + +sub prettyBytes { + my $size =3D $_[0]; + foreach ('B','KiB','MiB','GiB','TiB','PiB','EiB') { + return sprintf("%.2f ",$size)."$_" if $size < 1024; + $size /=3D 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 =3D 0; $i < $virtual_size; $i +=3D $cluster_size) { + my $l1_index =3D $i >> ($cluster_bits * 2 - 3); + my $l2_index =3D ncluster($i) & ((1 << ($cluster_bits - 3)) - 1); + my $l2_table =3D $l2_tables[$l1_index]; + my $mapping =3D ${$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 =3D shift; + return $offset >> $cluster_bits; +} --=20 2.11.0