Skip to content

Commit 42f3c14

Browse files
adrobvzstefanhaRH
authored andcommitted
scripts/qemugdb: coroutine: Add option for obtaining detailed trace in coredump
Commit 772f868 ("scripts/qemu-gdb: Support coroutine dumps in coredumps") introduced coroutine traces in coredumps using raw stack unwinding. While this works, this approach does not allow to view the function arguments in the corresponding stack frames. As an alternative, we can obtain saved registers from the coroutine's jmpbuf, patch them into the coredump's struct elf_prstatus in place, and execute another gdb subprocess to get backtrace from the patched temporary coredump. While providing more detailed info, this alternative approach, however, is more invasive as it might potentially corrupt the coredump file. We do take precautions by saving the original registers values into a separate binary blob /path/to/coredump.ptregs, so that it can be restores in the next GDB session. Still, instead of making it a new deault, let's keep raw unwind the default behaviour, but add the '--detailed' option for 'qemu bt' and 'qemu coroutine' command which would enforce the new behaviour. That's how this looks: (gdb) qemu coroutine 0x7fda9335a508 #0 0x5602bdb41c26 in qemu_coroutine_switch<+214> () at ../util/coroutine-ucontext.c:321 #1 0x5602bdb3e8fe in qemu_aio_coroutine_enter<+493> () at ../util/qemu-coroutine.c:293 #2 0x5602bdb3c4eb in co_schedule_bh_cb<+538> () at ../util/async.c:547 #3 0x5602bdb3b518 in aio_bh_call<+119> () at ../util/async.c:172 #4 0x5602bdb3b79a in aio_bh_poll<+457> () at ../util/async.c:219 #5 0x5602bdb10f22 in aio_poll<+1201> () at ../util/aio-posix.c:719 #6 0x5602bd8fb1ac in iothread_run<+123> () at ../iothread.c:63 #7 0x5602bdb18a24 in qemu_thread_start<+355> () at ../util/qemu-thread-posix.c:393 (gdb) qemu coroutine 0x7fda9335a508 --detailed patching core file /tmp/tmpq4hmk2qc found "CORE" at 0x10c48 assume pt_regs at 0x10cbc write r15 at 0x10cbc write r14 at 0x10cc4 write r13 at 0x10ccc write r12 at 0x10cd4 write rbp at 0x10cdc write rbx at 0x10ce4 write rip at 0x10d3c write rsp at 0x10d54 #0 0x00005602bdb41c26 in qemu_coroutine_switch (from_=0x7fda9335a508, to_=0x7fda8400c280, action=COROUTINE_ENTER) at ../util/coroutine-ucontext.c:321 #1 0x00005602bdb3e8fe in qemu_aio_coroutine_enter (ctx=0x5602bf7147c0, co=0x7fda8400c280) at ../util/qemu-coroutine.c:293 #2 0x00005602bdb3c4eb in co_schedule_bh_cb (opaque=0x5602bf7147c0) at ../util/async.c:547 #3 0x00005602bdb3b518 in aio_bh_call (bh=0x5602bf714a40) at ../util/async.c:172 #4 0x00005602bdb3b79a in aio_bh_poll (ctx=0x5602bf7147c0) at ../util/async.c:219 #5 0x00005602bdb10f22 in aio_poll (ctx=0x5602bf7147c0, blocking=true) at ../util/aio-posix.c:719 #6 0x00005602bd8fb1ac in iothread_run (opaque=0x5602bf42b100) at ../iothread.c:63 #7 0x00005602bdb18a24 in qemu_thread_start (args=0x5602bf7164a0) at ../util/qemu-thread-posix.c:393 #8 0x00007fda9e89f7f2 in start_thread (arg=<optimized out>) at pthread_create.c:443 #9 0x00007fda9e83f450 in clone3 () at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:81 CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru> CC: Peter Xu <peterx@redhat.com> Originally-by: Vladimir Sementsov-Ogievskiy <vsementsov@virtuozzo.com> Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com> Reviewed-by: Stefan Hajnoczi <stefanha@redhat.com> Message-id: 20251204105019.455060-5-andrey.drobyshev@virtuozzo.com Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>
1 parent 03a37ce commit 42f3c14

1 file changed

Lines changed: 242 additions & 15 deletions

File tree

scripts/qemugdb/coroutine.py

Lines changed: 242 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,119 @@
99
# This work is licensed under the terms of the GNU GPL, version 2
1010
# or later. See the COPYING file in the top-level directory.
1111

12+
import atexit
1213
import gdb
14+
import os
15+
import pty
16+
import re
17+
import struct
18+
import textwrap
19+
20+
from collections import OrderedDict
21+
from copy import deepcopy
1322

1423
VOID_PTR = gdb.lookup_type('void').pointer()
1524

25+
# Registers in the same order they're present in ELF coredump file.
26+
# See asm/ptrace.h
27+
PT_REGS = ['r15', 'r14', 'r13', 'r12', 'rbp', 'rbx', 'r11', 'r10', 'r9',
28+
'r8', 'rax', 'rcx', 'rdx', 'rsi', 'rdi', 'orig_rax', 'rip', 'cs',
29+
'eflags', 'rsp', 'ss']
30+
31+
coredump = None
32+
33+
34+
class Coredump:
35+
_ptregs_suff = '.ptregs'
36+
37+
def __init__(self, coredump, executable):
38+
atexit.register(self._cleanup)
39+
40+
self.coredump = coredump
41+
self.executable = executable
42+
self._ptregs_blob = coredump + self._ptregs_suff
43+
self._dirty = False
44+
45+
with open(coredump, 'rb') as f:
46+
while f.read(4) != b'CORE':
47+
pass
48+
gdb.write(f'core file {coredump}: found "CORE" at 0x{f.tell():x}\n')
49+
50+
# Looking for struct elf_prstatus and pr_reg field in it (an array
51+
# of general purpose registers). See sys/procfs.h.
52+
53+
# lseek(f.fileno(), 4, SEEK_CUR): go to elf_prstatus
54+
f.seek(4, 1)
55+
56+
# lseek(f.fileno(), 112, SEEK_CUR):
57+
# offsetof(struct elf_prstatus, pr_reg)
58+
f.seek(112, 1)
59+
60+
self._ptregs_offset = f.tell()
61+
62+
# If binary blob with the name /path/to/coredump + '.ptregs'
63+
# exists, that means proper cleanup didn't happen during previous
64+
# GDB session with the same coredump, and registers in the dump
65+
# itself might've remained patched. Thus we restore original
66+
# registers values from this blob
67+
if os.path.exists(self._ptregs_blob):
68+
with open(self._ptregs_blob, 'rb') as b:
69+
orig_ptregs_bytes = b.read()
70+
self._dirty = True
71+
else:
72+
orig_ptregs_bytes = f.read(len(PT_REGS) * 8)
73+
74+
values = struct.unpack(f"={len(PT_REGS)}q", orig_ptregs_bytes)
75+
self._orig_ptregs = OrderedDict(zip(PT_REGS, values))
76+
77+
if not os.path.exists(self._ptregs_blob):
78+
gdb.write(f'saving original pt_regs in {self._ptregs_blob}\n')
79+
with open(self._ptregs_blob, 'wb') as b:
80+
b.write(orig_ptregs_bytes)
81+
82+
gdb.write('\n')
83+
84+
def patch_regs(self, regs):
85+
# Set dirty flag early on to make sure regs are restored upon cleanup
86+
self._dirty = True
87+
88+
gdb.write(f'patching core file {self.coredump}\n')
89+
patched_ptregs = deepcopy(self._orig_ptregs)
90+
int_regs = {k: int(v) for k, v in regs.items()}
91+
patched_ptregs.update(int_regs)
92+
93+
with open(self.coredump, 'ab') as f:
94+
gdb.write(f'assume pt_regs at 0x{self._ptregs_offset:x}\n')
95+
f.seek(self._ptregs_offset, 0)
96+
gdb.write('writing regs:\n')
97+
for reg in self._orig_ptregs.keys():
98+
if reg in int_regs:
99+
gdb.write(f" {reg}: {int_regs[reg]:#16x}\n")
100+
f.write(struct.pack(f"={len(PT_REGS)}q", *patched_ptregs.values()))
101+
102+
gdb.write('\n')
103+
104+
def restore_regs(self):
105+
if not self._dirty:
106+
return
107+
108+
gdb.write(f'\nrestoring original regs in core file {self.coredump}\n')
109+
with open(self.coredump, 'ab') as f:
110+
gdb.write(f'assume pt_regs at 0x{self._ptregs_offset:x}\n')
111+
f.seek(self._ptregs_offset, 0)
112+
f.write(struct.pack(f"={len(PT_REGS)}q",
113+
*self._orig_ptregs.values()))
114+
115+
self._dirty = False
116+
gdb.write('\n')
117+
118+
def _cleanup(self):
119+
if os.path.exists(self._ptregs_blob):
120+
self.restore_regs()
121+
gdb.write(f'\nremoving saved pt_regs file {self._ptregs_blob}\n')
122+
os.unlink(self._ptregs_blob)
123+
124+
16125
def pthread_self():
17126
'''Fetch the base address of TLS.'''
18127
return gdb.parse_and_eval("$fs_base")
@@ -77,6 +186,55 @@ def symbol_lookup(addr):
77186

78187
return f"{func_str} at {path}:{line}"
79188

189+
def run_with_pty(cmd):
190+
# Create a PTY pair
191+
master_fd, slave_fd = pty.openpty()
192+
193+
pid = os.fork()
194+
if pid == 0: # Child
195+
os.close(master_fd)
196+
# Attach stdin/stdout/stderr to the PTY slave side
197+
os.dup2(slave_fd, 0)
198+
os.dup2(slave_fd, 1)
199+
os.dup2(slave_fd, 2)
200+
os.close(slave_fd)
201+
os.execvp("gdb", cmd) # Runs gdb and doesn't return
202+
203+
# Parent
204+
os.close(slave_fd)
205+
206+
output = bytearray()
207+
try:
208+
while True:
209+
data = os.read(master_fd, 65536)
210+
if not data:
211+
break
212+
output.extend(data)
213+
except OSError: # in case subprocess exits and we get EBADF on read()
214+
pass
215+
finally:
216+
try:
217+
os.close(master_fd)
218+
except OSError: # in case we get EBADF on close()
219+
pass
220+
221+
# Wait for child to finish (reap zombie)
222+
os.waitpid(pid, 0)
223+
224+
return output.decode('utf-8')
225+
226+
def dump_backtrace_patched(regs):
227+
cmd = ['gdb', '-batch',
228+
'-ex', 'set debuginfod enabled off',
229+
'-ex', 'set complaints 0',
230+
'-ex', 'set style enabled on',
231+
'-ex', 'python print("----split----")',
232+
'-ex', 'bt', coredump.executable, coredump.coredump]
233+
234+
coredump.patch_regs(regs)
235+
out = run_with_pty(cmd).split('----split----')[1]
236+
gdb.write(out)
237+
80238
def dump_backtrace(regs):
81239
'''
82240
Backtrace dump with raw registers, mimic GDB command 'bt'.
@@ -120,16 +278,20 @@ def dump_backtrace_live(regs):
120278

121279
selected_frame.select()
122280

123-
def bt_jmpbuf(jmpbuf):
281+
def bt_jmpbuf(jmpbuf, detailed=False):
124282
'''Backtrace a jmpbuf'''
125283
regs = get_jmpbuf_regs(jmpbuf)
126284
try:
127285
# This reuses gdb's "bt" command, which can be slightly prettier
128286
# but only works with live sessions.
129287
dump_backtrace_live(regs)
130288
except:
131-
# If above doesn't work, fallback to poor man's unwind
132-
dump_backtrace(regs)
289+
if detailed:
290+
# Obtain detailed trace by patching regs in copied coredump
291+
dump_backtrace_patched(regs)
292+
else:
293+
# If above doesn't work, fallback to poor man's unwind
294+
dump_backtrace(regs)
133295

134296
def co_cast(co):
135297
return co.cast(gdb.lookup_type('CoroutineUContext').pointer())
@@ -138,28 +300,90 @@ def coroutine_to_jmpbuf(co):
138300
coroutine_pointer = co_cast(co)
139301
return coroutine_pointer['env']['__jmpbuf']
140302

303+
def init_coredump():
304+
global coredump
305+
306+
files = gdb.execute('info files', False, True).split('\n')
307+
308+
if not 'core dump' in files[1]:
309+
return False
310+
311+
core_path = re.search("`(.*)'", files[2]).group(1)
312+
exec_path = re.match('^Symbols from "(.*)".$', files[0]).group(1)
313+
314+
if coredump is None:
315+
coredump = Coredump(core_path, exec_path)
316+
317+
return True
141318

142319
class CoroutineCommand(gdb.Command):
143-
'''Display coroutine backtrace'''
320+
__doc__ = textwrap.dedent("""\
321+
Display coroutine backtrace
322+
323+
Usage: qemu coroutine COROPTR [--detailed]
324+
Show backtrace for a coroutine specified by COROPTR
325+
326+
--detailed obtain detailed trace by copying coredump, patching
327+
regs in it, and runing gdb subprocess to get
328+
backtrace from the patched coredump
329+
""")
330+
144331
def __init__(self):
145332
gdb.Command.__init__(self, 'qemu coroutine', gdb.COMMAND_DATA,
146333
gdb.COMPLETE_NONE)
147334

335+
def _usage(self):
336+
gdb.write('usage: qemu coroutine <coroutine-pointer> [--detailed]\n')
337+
return
338+
148339
def invoke(self, arg, from_tty):
149340
argv = gdb.string_to_argv(arg)
150-
if len(argv) != 1:
151-
gdb.write('usage: qemu coroutine <coroutine-pointer>\n')
341+
argc = len(argv)
342+
if argc == 0 or argc > 2 or (argc == 2 and argv[1] != '--detailed'):
343+
return self._usage()
344+
detailed = True if argc == 2 else False
345+
346+
is_coredump = init_coredump()
347+
if detailed and not is_coredump:
348+
gdb.write('--detailed is only valid when debugging core dumps\n')
152349
return
153350

154-
bt_jmpbuf(coroutine_to_jmpbuf(gdb.parse_and_eval(argv[0])))
351+
try:
352+
bt_jmpbuf(coroutine_to_jmpbuf(gdb.parse_and_eval(argv[0])),
353+
detailed=detailed)
354+
finally:
355+
coredump.restore_regs()
155356

156357
class CoroutineBt(gdb.Command):
157-
'''Display backtrace including coroutine switches'''
358+
__doc__ = textwrap.dedent("""\
359+
Display backtrace including coroutine switches
360+
361+
Usage: qemu bt [--detailed]
362+
363+
--detailed obtain detailed trace by copying coredump, patching
364+
regs in it, and runing gdb subprocess to get
365+
backtrace from the patched coredump
366+
""")
367+
158368
def __init__(self):
159369
gdb.Command.__init__(self, 'qemu bt', gdb.COMMAND_STACK,
160370
gdb.COMPLETE_NONE)
161371

372+
def _usage(self):
373+
gdb.write('usage: qemu bt [--detailed]\n')
374+
return
375+
162376
def invoke(self, arg, from_tty):
377+
argv = gdb.string_to_argv(arg)
378+
argc = len(argv)
379+
if argc > 1 or (argc == 1 and argv[0] != '--detailed'):
380+
return self._usage()
381+
detailed = True if argc == 1 else False
382+
383+
is_coredump = init_coredump()
384+
if detailed and not is_coredump:
385+
gdb.write('--detailed is only valid when debugging core dumps\n')
386+
return
163387

164388
gdb.execute("bt")
165389

@@ -173,13 +397,16 @@ def invoke(self, arg, from_tty):
173397
if co_ptr == False:
174398
return
175399

176-
while True:
177-
co = co_cast(co_ptr)
178-
co_ptr = co["base"]["caller"]
179-
if co_ptr == 0:
180-
break
181-
gdb.write("Coroutine at " + str(co_ptr) + ":\n")
182-
bt_jmpbuf(coroutine_to_jmpbuf(co_ptr))
400+
try:
401+
while True:
402+
co = co_cast(co_ptr)
403+
co_ptr = co["base"]["caller"]
404+
if co_ptr == 0:
405+
break
406+
gdb.write("\nCoroutine at " + str(co_ptr) + ":\n")
407+
bt_jmpbuf(coroutine_to_jmpbuf(co_ptr), detailed=detailed)
408+
finally:
409+
coredump.restore_regs()
183410

184411
class CoroutineSPFunction(gdb.Function):
185412
def __init__(self):

0 commit comments

Comments
 (0)