Skip to content

Commit 7844673

Browse files
committed
feat: Enable gdb debugging on x86
Enabling GDB support for debugging the guest kernel. This allows us to connect a gdb server to firecracker and debug the guest. Signed-off-by: Jack Thomson <[email protected]>
1 parent 0661dd7 commit 7844673

File tree

13 files changed

+1456
-10
lines changed

13 files changed

+1456
-10
lines changed

Cargo.lock

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

resources/seccomp/x86_64-unknown-linux-musl.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,6 +1238,30 @@
12381238
}
12391239
]
12401240
},
1241+
{
1242+
"syscall": "ioctl",
1243+
"args": [
1244+
{
1245+
"index": 1,
1246+
"type": "dword",
1247+
"op": "eq",
1248+
"val": 1078505115,
1249+
"comment": "KVM_SET_GUEST_DEBUG"
1250+
}
1251+
]
1252+
},
1253+
{
1254+
"syscall": "ioctl",
1255+
"args": [
1256+
{
1257+
"index": 1,
1258+
"type": "dword",
1259+
"op": "eq",
1260+
"val": 3222843013,
1261+
"comment": "KVM_TRANSLATE"
1262+
}
1263+
]
1264+
},
12411265
{
12421266
"syscall": "ioctl",
12431267
"args": [

src/firecracker/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ serde_json = "1.0.128"
4949

5050
[features]
5151
tracing = ["log-instrument", "seccompiler/tracing", "utils/tracing", "vmm/tracing"]
52+
debug = ["vmm/debug"]
5253

5354
[lints]
5455
workspace = true

src/vmm/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ crc64 = "2.0.0"
1919
derive_more = { version = "1.0.0", default-features = false, features = ["from", "display"] }
2020
displaydoc = "0.2.5"
2121
event-manager = "0.4.0"
22+
gdbstub = { version = "0.7.2", optional = true }
23+
gdbstub_arch = { version = "0.3.0", optional = true }
2224
kvm-bindings = { version = "0.9.1", features = ["fam-wrappers", "serde"] }
2325
kvm-ioctls = "0.18.0"
2426
lazy_static = "1.5.0"
@@ -56,7 +58,9 @@ itertools = "0.13.0"
5658
proptest = { version = "1.5.0", default-features = false, features = ["std"] }
5759

5860
[features]
61+
default = []
5962
tracing = ["log-instrument"]
63+
debug = ["gdbstub", "gdbstub_arch"]
6064

6165
[[bench]]
6266
name = "cpu_templates"

src/vmm/src/builder.rs

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use std::convert::TryFrom;
88
use std::fmt::Debug;
99
use std::io::{self, Seek, SeekFrom};
10+
#[cfg(feature = "debug")]
11+
use std::sync::mpsc;
1012
use std::sync::{Arc, Mutex};
1113

1214
use event_manager::{MutEventSubscriber, SubscriberOps};
@@ -56,6 +58,8 @@ use crate::devices::virtio::net::Net;
5658
use crate::devices::virtio::rng::Entropy;
5759
use crate::devices::virtio::vsock::{Vsock, VsockUnixBackend};
5860
use crate::devices::BusDevice;
61+
#[cfg(feature = "debug")]
62+
use crate::gdb;
5963
use crate::logger::{debug, error};
6064
use crate::persist::{MicrovmState, MicrovmStateError};
6165
use crate::resources::VmResources;
@@ -128,6 +132,9 @@ pub enum StartMicrovmError {
128132
/// Error configuring ACPI: {0}
129133
#[cfg(target_arch = "x86_64")]
130134
Acpi(#[from] crate::acpi::AcpiError),
135+
/// Error starting GDB debug session
136+
#[cfg(feature = "debug")]
137+
GdbServer(gdb::target::Error),
131138
}
132139

133140
/// It's convenient to automatically convert `linux_loader::cmdline::Error`s
@@ -304,6 +311,13 @@ pub fn build_microvm_for_boot(
304311
cpu_template.kvm_capabilities.clone(),
305312
)?;
306313

314+
#[cfg(feature = "debug")]
315+
let (gdb_tx, gdb_rx) = mpsc::channel();
316+
#[cfg(feature = "debug")]
317+
vcpus
318+
.iter_mut()
319+
.for_each(|vcpu| vcpu.attach_debug_info(gdb_tx.clone()));
320+
307321
// The boot timer device needs to be the first device attached in order
308322
// to maintain the same MMIO address referenced in the documentation
309323
// and tests.
@@ -351,16 +365,28 @@ pub fn build_microvm_for_boot(
351365
boot_cmdline,
352366
)?;
353367

368+
let vmm = Arc::new(Mutex::new(vmm));
369+
370+
#[cfg(feature = "debug")]
371+
if let Some(gdb_socket_addr) = &vm_resources.gdb_socket_addr {
372+
gdb::server::gdb_thread(vmm.clone(), &vcpus, gdb_rx, entry_addr, gdb_socket_addr)
373+
.map_err(GdbServer)?;
374+
} else {
375+
debug!("No GDB socket provided not starting gdb server.");
376+
}
377+
354378
// Move vcpus to their own threads and start their state machine in the 'Paused' state.
355-
vmm.start_vcpus(
356-
vcpus,
357-
seccomp_filters
358-
.get("vcpu")
359-
.ok_or_else(|| MissingSeccompFilters("vcpu".to_string()))?
360-
.clone(),
361-
)
362-
.map_err(VmmError::VcpuStart)
363-
.map_err(Internal)?;
379+
vmm.lock()
380+
.unwrap()
381+
.start_vcpus(
382+
vcpus,
383+
seccomp_filters
384+
.get("vcpu")
385+
.ok_or_else(|| MissingSeccompFilters("vcpu".to_string()))?
386+
.clone(),
387+
)
388+
.map_err(VmmError::VcpuStart)
389+
.map_err(Internal)?;
364390

365391
// Load seccomp filters for the VMM thread.
366392
// Execution panics if filters cannot be loaded, use --no-seccomp if skipping filters
@@ -374,7 +400,6 @@ pub fn build_microvm_for_boot(
374400
.map_err(VmmError::SeccompFilters)
375401
.map_err(Internal)?;
376402

377-
let vmm = Arc::new(Mutex::new(vmm));
378403
event_manager.add_subscriber(vmm.clone());
379404

380405
Ok(vmm)

src/vmm/src/gdb/event_loop.rs

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use std::os::unix::net::UnixStream;
5+
use std::sync::mpsc::Receiver;
6+
use std::sync::mpsc::TryRecvError::Empty;
7+
use std::sync::{Arc, Mutex};
8+
9+
use gdbstub::common::{Signal, Tid};
10+
use gdbstub::conn::{Connection, ConnectionExt};
11+
use gdbstub::stub::run_blocking::{self, WaitForStopReasonError};
12+
use gdbstub::stub::{DisconnectReason, GdbStub, MultiThreadStopReason};
13+
use gdbstub::target::Target;
14+
use vm_memory::GuestAddress;
15+
16+
use super::target::{vcpuid_to_tid, Error, FirecrackerTarget};
17+
use crate::logger::trace;
18+
use crate::Vmm;
19+
20+
/// Starts the GDB event loop which acts as a proxy between the Vcpus and GDB
21+
pub fn event_loop(
22+
connection: UnixStream,
23+
vmm: Arc<Mutex<Vmm>>,
24+
gdb_event_receiver: Receiver<usize>,
25+
entry_addr: GuestAddress,
26+
) {
27+
let target = FirecrackerTarget::new(vmm, gdb_event_receiver, entry_addr);
28+
let connection: Box<dyn ConnectionExt<Error = std::io::Error>> = { Box::new(connection) };
29+
let debugger = GdbStub::new(connection);
30+
31+
gdb_event_loop_thread(debugger, target);
32+
}
33+
34+
struct GdbBlockingEventLoop {}
35+
36+
impl run_blocking::BlockingEventLoop for GdbBlockingEventLoop {
37+
type Target = FirecrackerTarget;
38+
type Connection = Box<dyn ConnectionExt<Error = std::io::Error>>;
39+
40+
type StopReason = MultiThreadStopReason<u64>;
41+
42+
/// Poll for events from either Vcpu's or packets from the GDB connection
43+
fn wait_for_stop_reason(
44+
target: &mut FirecrackerTarget,
45+
conn: &mut Self::Connection,
46+
) -> Result<
47+
run_blocking::Event<MultiThreadStopReason<u64>>,
48+
run_blocking::WaitForStopReasonError<
49+
<Self::Target as Target>::Error,
50+
<Self::Connection as Connection>::Error,
51+
>,
52+
> {
53+
loop {
54+
match target.gdb_event.try_recv() {
55+
Ok(cpu_id) => {
56+
// The Vcpu reports it's id from raw_id so we straight convert here
57+
let tid = Tid::new(cpu_id).expect("Error converting cpu id to Tid");
58+
// If notify paused returns false this means we were already debugging a single
59+
// core, the target will track this for us to pick up later
60+
target.update_paused_vcpu(tid);
61+
trace!("Vcpu: {tid:?} paused from debug exit");
62+
63+
let stop_reason = target
64+
.get_stop_reason(tid)
65+
.map_err(WaitForStopReasonError::Target)?;
66+
67+
let Some(stop_response) = stop_reason else {
68+
// If we returned None this is a break which should be handled by
69+
// the guest kernel (e.g. kernel int3 self testing) so we won't notify
70+
// GDB and instead inject this back into the guest
71+
target
72+
.inject_bp_to_guest(tid)
73+
.map_err(WaitForStopReasonError::Target)?;
74+
target
75+
.request_resume(tid)
76+
.map_err(WaitForStopReasonError::Target)?;
77+
78+
trace!("Injected BP into guest early exit");
79+
continue;
80+
};
81+
82+
trace!("Returned stop reason to gdb: {stop_response:?}");
83+
return Ok(run_blocking::Event::TargetStopped(stop_response));
84+
}
85+
Err(Empty) => (),
86+
Err(_) => {
87+
return Err(WaitForStopReasonError::Target(Error::GdbQueueError));
88+
}
89+
}
90+
91+
if conn.peek().map(|b| b.is_some()).unwrap_or(false) {
92+
let byte = conn
93+
.read()
94+
.map_err(run_blocking::WaitForStopReasonError::Connection)?;
95+
return Ok(run_blocking::Event::IncomingData(byte));
96+
}
97+
}
98+
}
99+
100+
/// Invoked when the GDB client sends a Ctrl-C interrupt.
101+
fn on_interrupt(
102+
target: &mut FirecrackerTarget,
103+
) -> Result<Option<MultiThreadStopReason<u64>>, <FirecrackerTarget as Target>::Error> {
104+
// notify the target that a ctrl-c interrupt has occurred.
105+
let main_core = vcpuid_to_tid(0)?;
106+
107+
target.request_pause(main_core)?;
108+
target.update_paused_vcpu(main_core);
109+
110+
let exit_reason = MultiThreadStopReason::SignalWithThread {
111+
tid: main_core,
112+
signal: Signal::SIGINT,
113+
};
114+
Ok(Some(exit_reason))
115+
}
116+
}
117+
/// This loop will run while communication with GDB is in progress, after GDB disconnects we
118+
/// shutdown firecracker and the VM
119+
fn gdb_event_loop_thread(
120+
debugger: GdbStub<FirecrackerTarget, Box<dyn ConnectionExt<Error = std::io::Error>>>,
121+
mut target: FirecrackerTarget,
122+
) {
123+
match debugger.run_blocking::<GdbBlockingEventLoop>(&mut target) {
124+
Ok(disconnect_reason) => match disconnect_reason {
125+
DisconnectReason::Disconnect => {
126+
trace!("Client disconnected")
127+
}
128+
DisconnectReason::TargetExited(code) => {
129+
trace!("Target exited with code {}", code)
130+
}
131+
DisconnectReason::TargetTerminated(sig) => {
132+
trace!("Target terminated with signal {}", sig)
133+
}
134+
DisconnectReason::Kill => trace!("GDB sent a kill command"),
135+
},
136+
Err(e) => {
137+
if e.is_target_error() {
138+
trace!("target encountered a fatal error: {e:?}")
139+
} else if e.is_connection_error() {
140+
trace!("connection error: {e:?}")
141+
} else {
142+
trace!("gdbstub encountered a fatal error {e:?}")
143+
}
144+
}
145+
}
146+
147+
target.shutdown();
148+
}

0 commit comments

Comments
 (0)