xref: /openbmc/qemu/scripts/dump-guest-memory.py (revision b2c623a3)
1# This python script adds a new gdb command, "dump-guest-memory". It
2# should be loaded with "source dump-guest-memory.py" at the (gdb)
3# prompt.
4#
5# Copyright (C) 2013, Red Hat, Inc.
6#
7# Authors:
8#   Laszlo Ersek <lersek@redhat.com>
9#
10# This work is licensed under the terms of the GNU GPL, version 2 or later. See
11# the COPYING file in the top-level directory.
12#
13# The leading docstring doesn't have idiomatic Python formatting. It is
14# printed by gdb's "help" command (the first line is printed in the
15# "help data" summary), and it should match how other help texts look in
16# gdb.
17
18import struct
19
20class DumpGuestMemory(gdb.Command):
21    """Extract guest vmcore from qemu process coredump.
22
23The sole argument is FILE, identifying the target file to write the
24guest vmcore to.
25
26This GDB command reimplements the dump-guest-memory QMP command in
27python, using the representation of guest memory as captured in the qemu
28coredump. The qemu process that has been dumped must have had the
29command line option "-machine dump-guest-core=on".
30
31For simplicity, the "paging", "begin" and "end" parameters of the QMP
32command are not supported -- no attempt is made to get the guest's
33internal paging structures (ie. paging=false is hard-wired), and guest
34memory is always fully dumped.
35
36Only x86_64 guests are supported.
37
38The CORE/NT_PRSTATUS and QEMU notes (that is, the VCPUs' statuses) are
39not written to the vmcore. Preparing these would require context that is
40only present in the KVM host kernel module when the guest is alive. A
41fake ELF note is written instead, only to keep the ELF parser of "crash"
42happy.
43
44Dependent on how busted the qemu process was at the time of the
45coredump, this command might produce unpredictable results. If qemu
46deliberately called abort(), or it was dumped in response to a signal at
47a halfway fortunate point, then its coredump should be in reasonable
48shape and this command should mostly work."""
49
50    TARGET_PAGE_SIZE = 0x1000
51    TARGET_PAGE_MASK = 0xFFFFFFFFFFFFF000
52
53    # Various ELF constants
54    EM_X86_64   = 62        # AMD x86-64 target machine
55    ELFDATA2LSB = 1         # little endian
56    ELFCLASS64  = 2
57    ELFMAG      = "\x7FELF"
58    EV_CURRENT  = 1
59    ET_CORE     = 4
60    PT_LOAD     = 1
61    PT_NOTE     = 4
62
63    # Special value for e_phnum. This indicates that the real number of
64    # program headers is too large to fit into e_phnum. Instead the real
65    # value is in the field sh_info of section 0.
66    PN_XNUM = 0xFFFF
67
68    # Format strings for packing and header size calculation.
69    ELF64_EHDR = ("4s" # e_ident/magic
70                  "B"  # e_ident/class
71                  "B"  # e_ident/data
72                  "B"  # e_ident/version
73                  "B"  # e_ident/osabi
74                  "8s" # e_ident/pad
75                  "H"  # e_type
76                  "H"  # e_machine
77                  "I"  # e_version
78                  "Q"  # e_entry
79                  "Q"  # e_phoff
80                  "Q"  # e_shoff
81                  "I"  # e_flags
82                  "H"  # e_ehsize
83                  "H"  # e_phentsize
84                  "H"  # e_phnum
85                  "H"  # e_shentsize
86                  "H"  # e_shnum
87                  "H"  # e_shstrndx
88                 )
89    ELF64_PHDR = ("I"  # p_type
90                  "I"  # p_flags
91                  "Q"  # p_offset
92                  "Q"  # p_vaddr
93                  "Q"  # p_paddr
94                  "Q"  # p_filesz
95                  "Q"  # p_memsz
96                  "Q"  # p_align
97                 )
98
99    def __init__(self):
100        super(DumpGuestMemory, self).__init__("dump-guest-memory",
101                                              gdb.COMMAND_DATA,
102                                              gdb.COMPLETE_FILENAME)
103        self.uintptr_t     = gdb.lookup_type("uintptr_t")
104        self.elf64_ehdr_le = struct.Struct("<%s" % self.ELF64_EHDR)
105        self.elf64_phdr_le = struct.Struct("<%s" % self.ELF64_PHDR)
106
107    def int128_get64(self, val):
108        assert (val["hi"] == 0)
109        return val["lo"]
110
111    def qtailq_foreach(self, head, field_str):
112        var_p = head["tqh_first"]
113        while (var_p != 0):
114            var = var_p.dereference()
115            yield var
116            var_p = var[field_str]["tqe_next"]
117
118    def qemu_get_ram_block(self, ram_addr):
119        ram_blocks = gdb.parse_and_eval("ram_list.blocks")
120        for block in self.qtailq_foreach(ram_blocks, "next"):
121            if (ram_addr - block["offset"] < block["length"]):
122                return block
123        raise gdb.GdbError("Bad ram offset %x" % ram_addr)
124
125    def qemu_get_ram_ptr(self, ram_addr):
126        block = self.qemu_get_ram_block(ram_addr)
127        return block["host"] + (ram_addr - block["offset"])
128
129    def memory_region_get_ram_ptr(self, mr):
130        if (mr["alias"] != 0):
131            return (self.memory_region_get_ram_ptr(mr["alias"].dereference()) +
132                    mr["alias_offset"])
133        return self.qemu_get_ram_ptr(mr["ram_addr"] & self.TARGET_PAGE_MASK)
134
135    def guest_phys_blocks_init(self):
136        self.guest_phys_blocks = []
137
138    def guest_phys_blocks_append(self):
139        print "guest RAM blocks:"
140        print ("target_start     target_end       host_addr        message "
141               "count")
142        print ("---------------- ---------------- ---------------- ------- "
143               "-----")
144
145        current_map_p = gdb.parse_and_eval("address_space_memory.current_map")
146        current_map = current_map_p.dereference()
147        for cur in range(current_map["nr"]):
148            flat_range   = (current_map["ranges"] + cur).dereference()
149            mr           = flat_range["mr"].dereference()
150
151            # we only care about RAM
152            if (not mr["ram"]):
153                continue
154
155            section_size = self.int128_get64(flat_range["addr"]["size"])
156            target_start = self.int128_get64(flat_range["addr"]["start"])
157            target_end   = target_start + section_size
158            host_addr    = (self.memory_region_get_ram_ptr(mr) +
159                            flat_range["offset_in_region"])
160            predecessor = None
161
162            # find continuity in guest physical address space
163            if (len(self.guest_phys_blocks) > 0):
164                predecessor = self.guest_phys_blocks[-1]
165                predecessor_size = (predecessor["target_end"] -
166                                    predecessor["target_start"])
167
168                # the memory API guarantees monotonically increasing
169                # traversal
170                assert (predecessor["target_end"] <= target_start)
171
172                # we want continuity in both guest-physical and
173                # host-virtual memory
174                if (predecessor["target_end"] < target_start or
175                    predecessor["host_addr"] + predecessor_size != host_addr):
176                    predecessor = None
177
178            if (predecessor is None):
179                # isolated mapping, add it to the list
180                self.guest_phys_blocks.append({"target_start": target_start,
181                                               "target_end"  : target_end,
182                                               "host_addr"   : host_addr})
183                message = "added"
184            else:
185                # expand predecessor until @target_end; predecessor's
186                # start doesn't change
187                predecessor["target_end"] = target_end
188                message = "joined"
189
190            print ("%016x %016x %016x %-7s %5u" %
191                   (target_start, target_end, host_addr.cast(self.uintptr_t),
192                    message, len(self.guest_phys_blocks)))
193
194    def cpu_get_dump_info(self):
195        # We can't synchronize the registers with KVM post-mortem, and
196        # the bits in (first_x86_cpu->env.hflags) seem to be stale; they
197        # may not reflect long mode for example. Hence just assume the
198        # most common values. This also means that instruction pointer
199        # etc. will be bogus in the dump, but at least the RAM contents
200        # should be valid.
201        self.dump_info = {"d_machine": self.EM_X86_64,
202                          "d_endian" : self.ELFDATA2LSB,
203                          "d_class"  : self.ELFCLASS64}
204
205    def encode_elf64_ehdr_le(self):
206        return self.elf64_ehdr_le.pack(
207                                 self.ELFMAG,                 # e_ident/magic
208                                 self.dump_info["d_class"],   # e_ident/class
209                                 self.dump_info["d_endian"],  # e_ident/data
210                                 self.EV_CURRENT,             # e_ident/version
211                                 0,                           # e_ident/osabi
212                                 "",                          # e_ident/pad
213                                 self.ET_CORE,                # e_type
214                                 self.dump_info["d_machine"], # e_machine
215                                 self.EV_CURRENT,             # e_version
216                                 0,                           # e_entry
217                                 self.elf64_ehdr_le.size,     # e_phoff
218                                 0,                           # e_shoff
219                                 0,                           # e_flags
220                                 self.elf64_ehdr_le.size,     # e_ehsize
221                                 self.elf64_phdr_le.size,     # e_phentsize
222                                 self.phdr_num,               # e_phnum
223                                 0,                           # e_shentsize
224                                 0,                           # e_shnum
225                                 0                            # e_shstrndx
226                                )
227
228    def encode_elf64_note_le(self):
229        return self.elf64_phdr_le.pack(self.PT_NOTE,         # p_type
230                                       0,                    # p_flags
231                                       (self.memory_offset -
232                                        len(self.note)),     # p_offset
233                                       0,                    # p_vaddr
234                                       0,                    # p_paddr
235                                       len(self.note),       # p_filesz
236                                       len(self.note),       # p_memsz
237                                       0                     # p_align
238                                      )
239
240    def encode_elf64_load_le(self, offset, start_hwaddr, range_size):
241        return self.elf64_phdr_le.pack(self.PT_LOAD, # p_type
242                                       0,            # p_flags
243                                       offset,       # p_offset
244                                       0,            # p_vaddr
245                                       start_hwaddr, # p_paddr
246                                       range_size,   # p_filesz
247                                       range_size,   # p_memsz
248                                       0             # p_align
249                                      )
250
251    def note_init(self, name, desc, type):
252        # name must include a trailing NUL
253        namesz = (len(name) + 1 + 3) / 4 * 4
254        descsz = (len(desc)     + 3) / 4 * 4
255        fmt = ("<"   # little endian
256               "I"   # n_namesz
257               "I"   # n_descsz
258               "I"   # n_type
259               "%us" # name
260               "%us" # desc
261               % (namesz, descsz))
262        self.note = struct.pack(fmt,
263                                len(name) + 1, len(desc), type, name, desc)
264
265    def dump_init(self):
266        self.guest_phys_blocks_init()
267        self.guest_phys_blocks_append()
268        self.cpu_get_dump_info()
269        # we have no way to retrieve the VCPU status from KVM
270        # post-mortem
271        self.note_init("NONE", "EMPTY", 0)
272
273        # Account for PT_NOTE.
274        self.phdr_num = 1
275
276        # We should never reach PN_XNUM for paging=false dumps: there's
277        # just a handful of discontiguous ranges after merging.
278        self.phdr_num += len(self.guest_phys_blocks)
279        assert (self.phdr_num < self.PN_XNUM)
280
281        # Calculate the ELF file offset where the memory dump commences:
282        #
283        #   ELF header
284        #   PT_NOTE
285        #   PT_LOAD: 1
286        #   PT_LOAD: 2
287        #   ...
288        #   PT_LOAD: len(self.guest_phys_blocks)
289        #   ELF note
290        #   memory dump
291        self.memory_offset = (self.elf64_ehdr_le.size +
292                              self.elf64_phdr_le.size * self.phdr_num +
293                              len(self.note))
294
295    def dump_begin(self, vmcore):
296        vmcore.write(self.encode_elf64_ehdr_le())
297        vmcore.write(self.encode_elf64_note_le())
298        running = self.memory_offset
299        for block in self.guest_phys_blocks:
300            range_size = block["target_end"] - block["target_start"]
301            vmcore.write(self.encode_elf64_load_le(running,
302                                                   block["target_start"],
303                                                   range_size))
304            running += range_size
305        vmcore.write(self.note)
306
307    def dump_iterate(self, vmcore):
308        qemu_core = gdb.inferiors()[0]
309        for block in self.guest_phys_blocks:
310            cur  = block["host_addr"]
311            left = block["target_end"] - block["target_start"]
312            print ("dumping range at %016x for length %016x" %
313                   (cur.cast(self.uintptr_t), left))
314            while (left > 0):
315                chunk_size = min(self.TARGET_PAGE_SIZE, left)
316                chunk = qemu_core.read_memory(cur, chunk_size)
317                vmcore.write(chunk)
318                cur  += chunk_size
319                left -= chunk_size
320
321    def create_vmcore(self, filename):
322        vmcore = open(filename, "wb")
323        self.dump_begin(vmcore)
324        self.dump_iterate(vmcore)
325        vmcore.close()
326
327    def invoke(self, args, from_tty):
328        # Unwittingly pressing the Enter key after the command should
329        # not dump the same multi-gig coredump to the same file.
330        self.dont_repeat()
331
332        argv = gdb.string_to_argv(args)
333        if (len(argv) != 1):
334            raise gdb.GdbError("usage: dump-guest-memory FILE")
335
336        self.dump_init()
337        self.create_vmcore(argv[0])
338
339DumpGuestMemory()
340