22# pylint: disable=invalid-name
33
44from argparse import ArgumentParser
5+ import contextlib
56import os
67from pathlib import Path
78import platform
@@ -24,6 +25,8 @@ def __init__(self):
2425 # Properties that can be adjusted by the user or class
2526 self .cmdline = []
2627 self .efi = False
28+ self .gdb = False
29+ self .gdb_bin = ''
2730 self .interactive = False
2831 self .kernel = None
2932 self .kernel_dir = None
@@ -45,7 +48,6 @@ def __init__(self):
4548 '-display' , 'none' ,
4649 '-nodefaults' ,
4750 '-no-reboot' ,
48- '-serial' , 'mon:stdio' ,
4951 ] # yapf: disable
5052 self ._qemu_path = None
5153 self ._ram = '512m'
@@ -107,6 +109,64 @@ def _prepare_initrd(self):
107109
108110 return dst
109111
112+ def _run_fg (self ):
113+ # Pretty print and run QEMU command
114+ qemu_cmd = []
115+
116+ if not self .interactive :
117+ utils .check_cmd ('timeout' )
118+ qemu_cmd += ['timeout' , '--foreground' , self .timeout ]
119+
120+ utils .check_cmd ('stdbuf' )
121+ qemu_cmd += ['stdbuf' , '-eL' , '-oL' ]
122+
123+ qemu_cmd += [self ._qemu_path , * self ._qemu_args ]
124+
125+ print (f"$ { ' ' .join (shlex .quote (str (elem )) for elem in qemu_cmd )} " )
126+ try :
127+ subprocess .run (qemu_cmd , check = True )
128+ except subprocess .CalledProcessError as err :
129+ if err .returncode == 124 :
130+ utils .red ("ERROR: QEMU timed out!" )
131+ else :
132+ utils .red ("ERROR: QEMU did not exit cleanly!" )
133+ sys .exit (err .returncode )
134+
135+ def _run_gdb (self ):
136+ qemu_cmd = [self ._qemu_path , * self ._qemu_args ]
137+
138+ utils .check_cmd (self .gdb_bin )
139+ utils .check_cmd ('lsof' )
140+
141+ gdb_cmd = [
142+ self .gdb_bin ,
143+ Path (self .kernel_dir , 'vmlinux' ),
144+ '-ex' ,
145+ 'target remote :1234' ,
146+ ]
147+
148+ while True :
149+ lsof = subprocess .run (['lsof' , '-i:1234' ],
150+ capture_output = True ,
151+ check = False )
152+ if lsof .returncode == 0 :
153+ utils .die ('Port 1234 is already in use, is QEMU running?' )
154+
155+ utils .green ('Starting QEMU with gdb connection on port 1234...' )
156+ with subprocess .Popen (qemu_cmd ,
157+ preexec_fn = os .setpgrp ) as qemu_proc :
158+ utils .green (f"Starting { self .gdb_bin } ..." )
159+ with subprocess .Popen (gdb_cmd ) as gdb_proc , \
160+ contextlib .suppress (KeyboardInterrupt ):
161+ gdb_proc .wait ()
162+
163+ utils .red ('Killing QEMU...' )
164+ qemu_proc .kill ()
165+
166+ answer = input ('Re-run QEMU + gdb [y/n] ' )
167+ if answer .lower () == 'n' :
168+ break
169+
110170 def run (self ):
111171 # Make sure QEMU binary is configured and available
112172 if not self ._qemu_arch :
@@ -157,6 +217,8 @@ def run(self):
157217 # Kernel options
158218 if self .interactive :
159219 self .cmdline .append ('rdinit=/bin/sh' )
220+ if self .gdb :
221+ self .cmdline .append ('nokaslr' )
160222 if self .cmdline :
161223 self ._qemu_args += ['-append' , ' ' .join (self .cmdline )]
162224 self ._qemu_args += ['-kernel' , self .kernel ]
@@ -177,27 +239,14 @@ def run(self):
177239 utils .green (f"QEMU location: \033 [0m{ self ._qemu_path .parent } " )
178240 utils .green (f"QEMU version: \033 [0m{ self ._get_qemu_ver_string ()} " )
179241
180- # Pretty print and run QEMU command
181- qemu_cmd = []
182-
183- if not self .interactive :
184- utils .check_cmd ('timeout' )
185- qemu_cmd += ['timeout' , '--foreground' , self .timeout ]
242+ if self .gdb :
243+ self ._qemu_args += ['-s' , '-S' ]
186244
187- utils .check_cmd ('stdbuf' )
188- qemu_cmd += ['stdbuf' , '-eL' , '-oL' ]
245+ self ._run_gdb ()
246+ else :
247+ self ._qemu_args += ['-serial' , 'mon:stdio' ]
189248
190- qemu_cmd += [qemu_path , * self ._qemu_args ]
191-
192- print (f"$ { ' ' .join (shlex .quote (str (elem )) for elem in qemu_cmd )} " )
193- try :
194- subprocess .run (qemu_cmd , check = True )
195- except subprocess .CalledProcessError as err :
196- if err .returncode == 124 :
197- utils .red ("ERROR: QEMU timed out!" )
198- else :
199- utils .red ("ERROR: QEMU did not exit cleanly!" )
200- sys .exit (err .returncode )
249+ self ._run_fg ()
201250
202251 def supports_efi (self ):
203252 return False
@@ -275,6 +324,15 @@ def parse_arguments():
275324 parser .add_argument ('--efi' ,
276325 action = 'store_true' ,
277326 help = 'Boot kernel via UEFI (x86_64 only)' )
327+ parser .add_argument (
328+ '-g' ,
329+ '--gdb' ,
330+ action = 'store_true' ,
331+ help = "Start QEMU with '-s -S' then launch gdb on 'vmlinux'" )
332+ parser .add_argument (
333+ '--gdb-bin' ,
334+ default = 'gdb-multiarch' ,
335+ help = 'gdb binary to use for debugging (default: gdb-multiarch)' )
278336 parser .add_argument (
279337 '-k' ,
280338 '--kernel-location' ,
@@ -321,6 +379,10 @@ def parse_arguments():
321379 raise FileNotFoundError (
322380 f"Supplied kernel location ('{ kernel_location } ') does not exist!" )
323381 if kernel_location .is_file ():
382+ if args .gdb and kernel_location .name != 'vmlinux' :
383+ raise RuntimeError (
384+ 'Debugging with gdb requires a kernel build folder to locate vmlinux' ,
385+ )
324386 runner .kernel = kernel_location
325387 else :
326388 runner .kernel_dir = kernel_location
@@ -335,13 +397,17 @@ def parse_arguments():
335397 f"EFI boot requested on unsupported architecture ('{ args .architecture } '), ignoring..." ,
336398 )
337399
400+ if args .gdb :
401+ runner .gdb = True
402+ runner .gdb_bin = args .gdb_bin
403+
338404 if args .no_kvm :
339405 runner .use_kvm = False
340406
341407 if args .smp :
342408 runner .smp = args .smp
343409
344- runner .interactive = args .interactive
410+ runner .interactive = args .interactive or args . gdb
345411 runner .timeout = args .timeout
346412
347413 runner .run ()
0 commit comments