Skip to content

Commit 601cc82

Browse files
committed
feat: Support GDB debugging on ARM
Add support for debugging aarch64 with gdb Signed-off-by: Jack Thomson <[email protected]>
1 parent 1881187 commit 601cc82

File tree

5 files changed

+316
-50
lines changed

5 files changed

+316
-50
lines changed

src/vmm/src/arch/aarch64/regs.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,16 @@ arm64_sys_reg!(SYS_CNTV_CVAL_EL0, 3, 3, 14, 3, 2);
9999
// https://elixir.bootlin.com/linux/v6.8/source/arch/arm64/include/asm/sysreg.h#L459
100100
arm64_sys_reg!(SYS_CNTPCT_EL0, 3, 3, 14, 0, 1);
101101

102+
// Translation Table Base Register
103+
// https://developer.arm.com/documentation/ddi0595/2021-03/AArch64-Registers/TTBR1-EL1--Translation-Table-Base-Register-1--EL1-
104+
arm64_sys_reg!(TTBR1_EL1, 3, 0, 2, 0, 1);
105+
// Translation Control Register
106+
// https://developer.arm.com/documentation/ddi0601/2024-09/AArch64-Registers/TCR-EL1--Translation-Control-Register--EL1-
107+
arm64_sys_reg!(TCR_EL1, 3, 0, 2, 0, 2);
108+
// AArch64 Memory Model Feature Register
109+
// https://developer.arm.com/documentation/100798/0400/register-descriptions/aarch64-system-registers/id-aa64mmfr0-el1--aarch64-memory-model-feature-register-0--el1
110+
arm64_sys_reg!(ID_AA64MMFR0_EL1, 3, 0, 0, 7, 0);
111+
102112
/// Vector lengths pseudo-register
103113
/// TODO: this can be removed after https://github.com/rust-vmm/kvm-bindings/pull/89
104114
/// is merged and new version is used in Firecracker.

src/vmm/src/builder.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@ use vm_superio::Rtc;
2828
use vm_superio::Serial;
2929
use vmm_sys_util::eventfd::EventFd;
3030

31-
#[cfg(all(feature = "gdb", target_arch = "aarch64"))]
32-
compile_error!("GDB feature not supported on ARM");
33-
3431
#[cfg(target_arch = "x86_64")]
3532
use crate::acpi;
3633
use crate::arch::InitrdConfig;

src/vmm/src/gdb/arch/aarch64.rs

Lines changed: 272 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,308 @@
11
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
use std::mem::offset_of;
5+
46
use gdbstub_arch::aarch64::reg::AArch64CoreRegs as CoreRegs;
7+
use kvm_bindings::{
8+
kvm_guest_debug, kvm_regs, user_pt_regs, KVM_GUESTDBG_ENABLE, KVM_GUESTDBG_SINGLESTEP,
9+
KVM_GUESTDBG_USE_HW, KVM_GUESTDBG_USE_SW_BP, KVM_REG_ARM64, KVM_REG_ARM_CORE, KVM_REG_SIZE_U64,
10+
};
511
use kvm_ioctls::VcpuFd;
6-
use vm_memory::GuestAddress;
12+
use vm_memory::{Bytes, GuestAddress};
713

14+
use crate::arch::aarch64::regs::{
15+
arm64_core_reg_id, Aarch64RegisterVec, ID_AA64MMFR0_EL1, TCR_EL1, TTBR1_EL1,
16+
};
17+
use crate::arch::aarch64::vcpu::get_registers;
818
use crate::gdb::target::GdbTargetError;
19+
use crate::Vmm;
20+
21+
/// Configures the number of bytes required for a software breakpoint.
22+
///
23+
/// The breakpoint instruction operation also includes the immediate argument which we 0 hence the size.
24+
pub const SW_BP_SIZE: usize = 4;
25+
26+
/// The bytes stored for a software breakpoint.
27+
///
28+
/// This is the BRK instruction with a 0 immediate argument.
29+
/// https://developer.arm.com/documentation/ddi0602/2024-09/Base-Instructions/BRK--Breakpoint-instruction-
30+
pub const SW_BP: [u8; SW_BP_SIZE] = [0, 0, 32, 212];
31+
32+
/// Register id for the program counter
33+
const PC_REG_ID: u64 = arm64_core_reg_id!(KVM_REG_SIZE_U64, offset_of!(user_pt_regs, pc));
34+
35+
/// Retrieve a single register from a Vcpu
36+
fn get_sys_reg(reg: u64, vcpu_fd: &VcpuFd) -> Result<u64, GdbTargetError> {
37+
let mut register_vec = Aarch64RegisterVec::default();
38+
get_registers(vcpu_fd, &[reg], &mut register_vec)?;
39+
let register = register_vec
40+
.iter()
41+
.next()
42+
.ok_or(GdbTargetError::ReadRegisterVecError)?;
43+
44+
Ok(register.value())
45+
}
46+
47+
/// Gets the PC value for a Vcpu
48+
pub fn get_instruction_pointer(vcpu_fd: &VcpuFd) -> Result<u64, GdbTargetError> {
49+
get_sys_reg(PC_REG_ID, vcpu_fd)
50+
}
951

10-
/// Configures the number of bytes required for a software breakpoint
11-
pub const SW_BP_SIZE: usize = 1;
52+
/// Helper to extract a specific number of bits at an offset from a u64
53+
macro_rules! extract_bits_64 {
54+
($value: tt, $offset: tt, $length: tt) => {
55+
($value >> $offset) & (!0u64 >> (64 - $length))
56+
};
57+
}
58+
59+
/// Mask to clear the last 3 bits from the page table entry
60+
const PTE_ADDRESS_MASK: u64 = !0b111u64;
1261

13-
/// The bytes stored for a software breakpoint
14-
pub const SW_BP: [u8; SW_BP_SIZE] = [0];
62+
/// Read a u64 value from a guest memory address
63+
fn read_address(vmm: &Vmm, address: u64) -> Result<u64, GdbTargetError> {
64+
let mut buf = [0; 8];
65+
vmm.guest_memory().read(&mut buf, GuestAddress(address))?;
1566

16-
/// Gets the RIP value for a Vcpu
17-
pub fn get_instruction_pointer(_vcpu_fd: &VcpuFd) -> Result<u64, GdbTargetError> {
18-
unimplemented!()
67+
Ok(u64::from_le_bytes(buf))
1968
}
2069

21-
/// Translates a virtual address according to the vCPU's current address translation mode.
22-
pub fn translate_gva(_vcpu_fd: &VcpuFd, _gva: u64) -> Result<u64, GdbTargetError> {
23-
unimplemented!()
70+
/// The grainsize used with 4KB paging
71+
const GRAIN_SIZE: usize = 9;
72+
73+
/// Translates a virtual address according to the Vcpu's current address translation mode.
74+
/// Returns the GPA (guest physical address)
75+
///
76+
/// To simplify the implementation we've made some assumptions about the paging setup.
77+
/// Here we just assert firstly paging is setup and these assumptions are correct.
78+
pub fn translate_gva(vcpu_fd: &VcpuFd, gva: u64, vmm: &Vmm) -> Result<u64, GdbTargetError> {
79+
// Check this virtual address is in kernel space
80+
if extract_bits_64!(gva, 55, 1) == 0 {
81+
return Err(GdbTargetError::GvaTranslateError);
82+
}
83+
84+
// Translation control register
85+
let tcr_el1: u64 = get_sys_reg(TCR_EL1, vcpu_fd)?;
86+
87+
// If this is 0 then translation is not yet ready
88+
if extract_bits_64!(tcr_el1, 16, 6) == 0 {
89+
return Ok(gva);
90+
}
91+
92+
// Check 4KB pages are being used
93+
if extract_bits_64!(tcr_el1, 30, 2) != 2 {
94+
return Err(GdbTargetError::GvaTranslateError);
95+
}
96+
97+
// ID_AA64MMFR0_EL1 provides information about the implemented memory model and memory
98+
// management. Check this is a physical address size we support
99+
let pa_size = match get_sys_reg(ID_AA64MMFR0_EL1, vcpu_fd)? & 0b1111 {
100+
0 => 32,
101+
1 => 36,
102+
2 => 40,
103+
3 => 42,
104+
4 => 44,
105+
5 => 48,
106+
_ => return Err(GdbTargetError::GvaTranslateError),
107+
};
108+
109+
// A mask of the physical address size for a virtual address
110+
let pa_address_mask: u64 = !0u64 >> (64 - pa_size);
111+
// A mask used to take the bottom 12 bits of a value this is as we have a grainsize of 9
112+
// asserted with our 4kb page, plus the offset of 3
113+
let lower_mask: u64 = 0xFFF;
114+
// A mask for a physical address mask with the lower 12 bits cleared
115+
let desc_mask: u64 = pa_address_mask & !lower_mask;
116+
117+
let page_indices = [
118+
(gva >> (GRAIN_SIZE * 4)) & lower_mask,
119+
(gva >> (GRAIN_SIZE * 3)) & lower_mask,
120+
(gva >> (GRAIN_SIZE * 2)) & lower_mask,
121+
(gva >> GRAIN_SIZE) & lower_mask,
122+
];
123+
124+
// Transition table base register used for initial table lookup.
125+
// Take the bottom 48 bits from the register value.
126+
let mut address: u64 = get_sys_reg(TTBR1_EL1, vcpu_fd)? & pa_address_mask;
127+
let mut level = 0;
128+
129+
while level < 4 {
130+
// Clear the bottom 3 bits from this address
131+
let pte = read_address(vmm, (address + page_indices[level]) & PTE_ADDRESS_MASK)?;
132+
address = pte & desc_mask;
133+
134+
// If this is a valid table entry and we aren't at the end of the page tables
135+
// then loop again and check next level
136+
if (pte & 2 != 0) && (level < 3) {
137+
level += 1;
138+
continue;
139+
}
140+
break;
141+
}
142+
143+
// Generate a mask to split between the page table entry and the GVA. The split point is
144+
// dependent on which level we terminate at. This is calculated by taking the level we
145+
// hit multiplied by the grainsize then adding the 3 offset
146+
let page_size = 1u64 << ((GRAIN_SIZE * (4 - level)) + 3);
147+
// Clear bottom bits of page size
148+
address &= !(page_size - 1);
149+
address |= gva & (page_size - 1);
150+
Ok(address)
24151
}
25152

26153
/// Configures the kvm guest debug regs to register the hardware breakpoints
27154
fn set_kvm_debug(
28-
_control: u32,
29-
_vcpu_fd: &VcpuFd,
30-
_addrs: &[GuestAddress],
155+
control: u32,
156+
vcpu_fd: &VcpuFd,
157+
addrs: &[GuestAddress],
31158
) -> Result<(), GdbTargetError> {
32-
unimplemented!()
159+
let mut dbg = kvm_guest_debug {
160+
control,
161+
..Default::default()
162+
};
163+
164+
for (i, addr) in addrs.iter().enumerate() {
165+
// DBGBCR_EL1 (Debug Breakpoint Control Registers, D13.3.2):
166+
// bit 0: 1 (Enabled)
167+
// bit 1~2: 0b11 (PMC = EL1/EL0)
168+
// bit 5~8: 0b1111 (BAS = AArch64)
169+
// others: 0
170+
dbg.arch.dbg_bcr[i] = 0b1 | (0b11 << 1) | (0b1111 << 5);
171+
// DBGBVR_EL1 (Debug Breakpoint Value Registers, D13.3.3):
172+
// bit 2~52: VA[2:52]
173+
dbg.arch.dbg_bvr[i] = (!0u64 >> 11) & addr.0;
174+
}
175+
176+
vcpu_fd.set_guest_debug(&dbg)?;
177+
178+
Ok(())
179+
}
180+
181+
/// Bits in a Vcpu pstate for IRQ
182+
const IRQ_ENABLE_FLAGS: u64 = 0x80 | 0x40;
183+
/// Register id for pstate
184+
const PSTATE_ID: u64 = arm64_core_reg_id!(KVM_REG_SIZE_U64, offset_of!(user_pt_regs, pstate));
185+
186+
/// Disable IRQ interrupts to avoid getting stuck in a loop while single stepping
187+
///
188+
/// When GDB hits a single breakpoint and resumes it will follow the steps:
189+
/// - Clear SW breakpoint we've hit
190+
/// - Single step
191+
/// - Re-insert the SW breakpoint
192+
/// - Resume
193+
/// However, with IRQ enabled the single step takes us into the IRQ handler so when we resume we
194+
/// immediately hit the SW breapoint we just re-inserted getting stuck in a loop.
195+
fn toggle_interrupts(vcpu_fd: &VcpuFd, enable: bool) -> Result<(), GdbTargetError> {
196+
let mut pstate = get_sys_reg(PSTATE_ID, vcpu_fd)?;
197+
198+
if enable {
199+
pstate |= IRQ_ENABLE_FLAGS;
200+
} else {
201+
pstate &= !IRQ_ENABLE_FLAGS;
202+
}
203+
204+
vcpu_fd.set_one_reg(PSTATE_ID, &pstate.to_le_bytes())?;
205+
206+
Ok(())
33207
}
34208

35209
/// Configures the Vcpu for debugging and sets the hardware breakpoints on the Vcpu
36210
pub fn vcpu_set_debug(
37-
_vcpu_fd: &VcpuFd,
38-
_addrs: &[GuestAddress],
39-
_step: bool,
211+
vcpu_fd: &VcpuFd,
212+
addrs: &[GuestAddress],
213+
step: bool,
40214
) -> Result<(), GdbTargetError> {
41-
unimplemented!()
215+
let mut control = KVM_GUESTDBG_ENABLE | KVM_GUESTDBG_USE_HW | KVM_GUESTDBG_USE_SW_BP;
216+
if step {
217+
control |= KVM_GUESTDBG_SINGLESTEP;
218+
}
219+
220+
toggle_interrupts(vcpu_fd, step)?;
221+
set_kvm_debug(control, vcpu_fd, addrs)
42222
}
43223

44-
/// Injects a BP back into the guest kernel for it to handle, this is particularly useful for the
45-
/// kernels selftesting which can happen during boot.
224+
/// KVM does not support injecting breakpoints on aarch64 so this is a no-op
46225
pub fn vcpu_inject_bp(
47226
_vcpu_fd: &VcpuFd,
48227
_addrs: &[GuestAddress],
49228
_step: bool,
50229
) -> Result<(), GdbTargetError> {
51-
unimplemented!()
230+
Ok(())
52231
}
232+
/// The number of general purpose registers
233+
const GENERAL_PURPOSE_REG_COUNT: usize = 31;
234+
/// The number of core registers we read from the Vcpu
235+
const CORE_REG_COUNT: usize = 33;
236+
/// Stores the register ids of registers to be read from the Vcpu
237+
const CORE_REG_IDS: [u64; CORE_REG_COUNT] = {
238+
let mut regs = [0; CORE_REG_COUNT];
239+
let mut idx = 0;
240+
241+
let reg_offset = offset_of!(kvm_regs, regs);
242+
let mut off = reg_offset;
243+
while idx < GENERAL_PURPOSE_REG_COUNT {
244+
regs[idx] = arm64_core_reg_id!(KVM_REG_SIZE_U64, off);
245+
idx += 1;
246+
off += std::mem::size_of::<u64>();
247+
}
248+
249+
regs[idx] = arm64_core_reg_id!(KVM_REG_SIZE_U64, offset_of!(user_pt_regs, sp));
250+
idx += 1;
251+
252+
regs[idx] = arm64_core_reg_id!(KVM_REG_SIZE_U64, offset_of!(user_pt_regs, pc));
253+
regs
254+
};
53255

54256
/// Reads the registers for the Vcpu
55-
pub fn read_registers(_vcpu_fd: &VcpuFd, _regs: &mut CoreRegs) -> Result<(), GdbTargetError> {
56-
unimplemented!()
257+
pub fn read_registers(vcpu_fd: &VcpuFd, regs: &mut CoreRegs) -> Result<(), GdbTargetError> {
258+
let mut register_vec = Aarch64RegisterVec::default();
259+
get_registers(vcpu_fd, &CORE_REG_IDS, &mut register_vec)?;
260+
261+
let mut registers = register_vec.iter();
262+
263+
for i in 0..GENERAL_PURPOSE_REG_COUNT {
264+
regs.x[i] = registers
265+
.next()
266+
.ok_or(GdbTargetError::ReadRegisterVecError)?
267+
.value();
268+
}
269+
270+
regs.sp = registers
271+
.next()
272+
.ok_or(GdbTargetError::ReadRegisterVecError)?
273+
.value();
274+
275+
regs.pc = registers
276+
.next()
277+
.ok_or(GdbTargetError::ReadRegisterVecError)?
278+
.value();
279+
280+
Ok(())
57281
}
58282

59283
/// Writes to the registers for the Vcpu
60-
pub fn write_registers(_vcpu_fd: &VcpuFd, _regs: &CoreRegs) -> Result<(), GdbTargetError> {
61-
unimplemented!()
284+
pub fn write_registers(vcpu_fd: &VcpuFd, regs: &CoreRegs) -> Result<(), GdbTargetError> {
285+
let kreg_off = offset_of!(kvm_regs, regs);
286+
let mut off = kreg_off;
287+
for i in 0..GENERAL_PURPOSE_REG_COUNT {
288+
vcpu_fd.set_one_reg(
289+
arm64_core_reg_id!(KVM_REG_SIZE_U64, off),
290+
&regs.x[i].to_le_bytes(),
291+
)?;
292+
off += std::mem::size_of::<u64>();
293+
}
294+
295+
let off = offset_of!(user_pt_regs, sp);
296+
vcpu_fd.set_one_reg(
297+
arm64_core_reg_id!(KVM_REG_SIZE_U64, off + kreg_off),
298+
&regs.sp.to_le_bytes(),
299+
)?;
300+
301+
let off = offset_of!(user_pt_regs, pc);
302+
vcpu_fd.set_one_reg(
303+
arm64_core_reg_id!(KVM_REG_SIZE_U64, off + kreg_off),
304+
&regs.pc.to_le_bytes(),
305+
)?;
306+
307+
Ok(())
62308
}

src/vmm/src/gdb/arch/x86.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use vm_memory::GuestAddress;
88

99
use crate::gdb::target::GdbTargetError;
1010
use crate::logger::error;
11+
use crate::Vmm;
1112

1213
/// Sets the 9th (Global Exact Breakpoint enable) and the 10th (always 1) bits for the DR7 debug
1314
/// control register
@@ -30,11 +31,11 @@ pub fn get_instruction_pointer(vcpu_fd: &VcpuFd) -> Result<u64, GdbTargetError>
3031
}
3132

3233
/// Translates a virtual address according to the vCPU's current address translation mode.
33-
pub fn translate_gva(vcpu_fd: &VcpuFd, gva: u64) -> Result<u64, GdbTargetError> {
34+
pub fn translate_gva(vcpu_fd: &VcpuFd, gva: u64, _vmm: &Vmm) -> Result<u64, GdbTargetError> {
3435
let tr = vcpu_fd.translate_gva(gva)?;
3536

3637
if tr.valid == 0 {
37-
return Err(GdbTargetError::KvmGvaTranslateError);
38+
return Err(GdbTargetError::GvaTranslateError);
3839
}
3940

4041
Ok(tr.physical_address)

0 commit comments

Comments
 (0)