Skip to content

Commit 20c1e14

Browse files
committed
feat: Support GDB debugging on ARM
Add support for debugging aarch64 with gdb Signed-off-by: Jack Thomson <jackabt@amazon.com>
1 parent 4b79156 commit 20c1e14

File tree

5 files changed

+305
-48
lines changed

5 files changed

+305
-48
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ 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+
arm64_sys_reg!(TTBR1_EL1, 3, 0, 2, 0, 1);
103+
arm64_sys_reg!(TCR_EL1, 3, 0, 2, 0, 2);
104+
arm64_sys_reg!(ID_AA64MMFR0_EL1, 3, 0, 0, 7, 0);
105+
102106
/// Vector lengths pseudo-register
103107
/// TODO: this can be removed after https://github.com/rust-vmm/kvm-bindings/pull/89
104108
/// 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: 261 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,299 @@
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;
920

1021
/// Configures the number of bytes required for a software breakpoint
11-
pub const SW_BP_SIZE: usize = 1;
22+
pub const SW_BP_SIZE: usize = 4;
1223

1324
/// The bytes stored for a software breakpoint
14-
pub const SW_BP: [u8; SW_BP_SIZE] = [0];
25+
pub const SW_BP: [u8; SW_BP_SIZE] = [0, 0, 32, 212];
26+
27+
/// Gets the PC value for a Vcpu
28+
pub fn get_instruction_pointer(vcpu_fd: &VcpuFd) -> Result<u64, GdbTargetError> {
29+
let mut regs = CoreRegs::default();
30+
read_registers(vcpu_fd, &mut regs)?;
1531

16-
/// Gets the RIP value for a Vcpu
17-
pub fn get_instruction_pointer(_vcpu_fd: &VcpuFd) -> Result<u64, GdbTargetError> {
18-
unimplemented!()
32+
Ok(regs.pc)
1933
}
2034

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!()
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+
/// Helper to extract a specific number of bits at an offset from a u64
48+
macro_rules! extract_bits_64 {
49+
($value: tt, $offset: tt, $length: tt) => {
50+
($value >> $offset) & (!0u64 >> (64 - $length))
51+
};
52+
}
53+
54+
/// Mask to clear the last 3 bits from the page table entry
55+
const PTE_ADDRESS_MASK: u64 = !0b111u64;
56+
57+
/// Read a u64 value from a guest memory address
58+
fn read_address(vmm: &Vmm, address: u64) -> Result<u64, GdbTargetError> {
59+
let mut buf = [0; 8];
60+
vmm.guest_memory().read(&mut buf, GuestAddress(address))?;
61+
62+
Ok(u64::from_le_bytes(buf))
63+
}
64+
65+
/// Translates a virtual address according to the Vcpu's current address translation mode.
66+
///
67+
/// To simplify the implementation we've made some assumptions about the paging setup
68+
/// here we just assert firstly paging is setup and these assumptions are correct
69+
pub fn translate_gva(vcpu_fd: &VcpuFd, gva: u64, vmm: &Vmm) -> Result<u64, GdbTargetError> {
70+
// Check this virtual address is in kernel space
71+
if extract_bits_64!(gva, 55, 1) == 0 {
72+
return Err(GdbTargetError::GvaTranslateError);
73+
}
74+
75+
// Translation control register
76+
let tcr_el1: u64 = get_sys_reg(TCR_EL1, vcpu_fd)?;
77+
78+
// If this is 0 then translation is not yet ready
79+
if extract_bits_64!(tcr_el1, 16, 6) == 0 {
80+
return Ok(gva);
81+
}
82+
83+
// Check 4KB pages are being used
84+
if extract_bits_64!(tcr_el1, 30, 2) != 2 {
85+
return Err(GdbTargetError::GvaTranslateError);
86+
}
87+
88+
// ID_AA64MMFR0_EL1 provides information about the implemented memory model and memory
89+
// management, check this is a physical address size we support
90+
let pa_size = match get_sys_reg(ID_AA64MMFR0_EL1, vcpu_fd)? & 0b1111 {
91+
0 => 32,
92+
1 => 36,
93+
2 => 40,
94+
3 => 42,
95+
4 => 44,
96+
5 => 48,
97+
_ => return Err(GdbTargetError::GvaTranslateError),
98+
};
99+
100+
// A mask of the physical address size for a virutal address
101+
let pa_address_mask: u64 = !0u64 >> (64 - pa_size);
102+
// A mask used to take the bottom 12 bits of a value this is as we have a grainsize of 9
103+
// asserted with our 4kb page, plus the offset of 3
104+
let lower_mask: u64 = 0xFFF;
105+
// A mask for a physical address mask with the lower 12 bits cleared
106+
let desc_mask: u64 = pa_address_mask & !lower_mask;
107+
108+
let page_indicies = [
109+
(gva >> 36) & lower_mask,
110+
(gva >> 27) & lower_mask,
111+
(gva >> 18) & lower_mask,
112+
(gva >> 9) & lower_mask,
113+
];
114+
115+
// Transition table base register used for initial table lookup
116+
// take the bottom 48 bits from the register value
117+
let mut address: u64 = get_sys_reg(TTBR1_EL1, vcpu_fd)? & pa_address_mask;
118+
let mut level = 0;
119+
120+
while level < 4 {
121+
// Clear the bottom 3 bits from this address
122+
let pte = read_address(vmm, (address + page_indicies[level]) & PTE_ADDRESS_MASK)?;
123+
address = pte & desc_mask;
124+
125+
// If this is a valid table entry and we aren't at the end of the page tables
126+
// then loop again and check next level
127+
if (pte & 2 != 0) && (level < 3) {
128+
level += 1;
129+
continue;
130+
}
131+
break;
132+
}
133+
134+
let page_size = 1u64 << ((9 * (4 - level)) + 3);
135+
// Clear bottom bits of page size
136+
address &= !(page_size - 1);
137+
address |= gva & (page_size - 1);
138+
Ok(address)
24139
}
25140

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

35201
/// Configures the Vcpu for debugging and sets the hardware breakpoints on the Vcpu
36202
pub fn vcpu_set_debug(
37-
_vcpu_fd: &VcpuFd,
38-
_addrs: &[GuestAddress],
39-
_step: bool,
203+
vcpu_fd: &VcpuFd,
204+
addrs: &[GuestAddress],
205+
step: bool,
40206
) -> Result<(), GdbTargetError> {
41-
unimplemented!()
207+
let mut control = KVM_GUESTDBG_ENABLE | KVM_GUESTDBG_USE_HW | KVM_GUESTDBG_USE_SW_BP;
208+
if step {
209+
control |= KVM_GUESTDBG_SINGLESTEP;
210+
}
211+
212+
toggle_interrupts(vcpu_fd, step)?;
213+
set_kvm_debug(control, vcpu_fd, addrs)
42214
}
43215

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.
216+
/// KVM does not support injecting breakpoints on aarch64 so this is a no-op
46217
pub fn vcpu_inject_bp(
47218
_vcpu_fd: &VcpuFd,
48219
_addrs: &[GuestAddress],
49220
_step: bool,
50221
) -> Result<(), GdbTargetError> {
51-
unimplemented!()
222+
Ok(())
52223
}
53224

225+
/// The number of core registers we read from the Vcpu
226+
const CORE_REG_COUNT: usize = 33;
227+
/// Stores the register ids of ids to be read fron the Vcpu
228+
const CORE_REG_IDS: [u64; CORE_REG_COUNT] = {
229+
let mut regs = [0; CORE_REG_COUNT];
230+
let mut idx = 0;
231+
232+
let reg_offset = offset_of!(kvm_regs, regs);
233+
let mut off = reg_offset;
234+
while idx < 31 {
235+
regs[idx] = arm64_core_reg_id!(KVM_REG_SIZE_U64, off);
236+
idx += 1;
237+
off += std::mem::size_of::<u64>();
238+
}
239+
240+
regs[idx] = arm64_core_reg_id!(KVM_REG_SIZE_U64, offset_of!(user_pt_regs, sp));
241+
idx += 1;
242+
243+
regs[idx] = arm64_core_reg_id!(KVM_REG_SIZE_U64, offset_of!(user_pt_regs, pc));
244+
regs
245+
};
246+
54247
/// Reads the registers for the Vcpu
55-
pub fn read_registers(_vcpu_fd: &VcpuFd, _regs: &mut CoreRegs) -> Result<(), GdbTargetError> {
56-
unimplemented!()
248+
pub fn read_registers(vcpu_fd: &VcpuFd, regs: &mut CoreRegs) -> Result<(), GdbTargetError> {
249+
let mut register_vec = Aarch64RegisterVec::default();
250+
get_registers(vcpu_fd, &CORE_REG_IDS, &mut register_vec)?;
251+
252+
let mut registers = register_vec.iter();
253+
254+
for i in 0..31 {
255+
regs.x[i] = registers
256+
.next()
257+
.ok_or(GdbTargetError::ReadRegisterVecError)?
258+
.value();
259+
}
260+
261+
regs.sp = registers
262+
.next()
263+
.ok_or(GdbTargetError::ReadRegisterVecError)?
264+
.value();
265+
266+
regs.pc = registers
267+
.next()
268+
.ok_or(GdbTargetError::ReadRegisterVecError)?
269+
.value();
270+
271+
Ok(())
57272
}
58273

59274
/// Writes to the registers for the Vcpu
60-
pub fn write_registers(_vcpu_fd: &VcpuFd, _regs: &CoreRegs) -> Result<(), GdbTargetError> {
61-
unimplemented!()
275+
pub fn write_registers(vcpu_fd: &VcpuFd, regs: &CoreRegs) -> Result<(), GdbTargetError> {
276+
let kreg_off = offset_of!(kvm_regs, regs);
277+
let mut off = kreg_off;
278+
for i in 0..31 {
279+
vcpu_fd.set_one_reg(
280+
arm64_core_reg_id!(KVM_REG_SIZE_U64, off),
281+
&regs.x[i].to_le_bytes(),
282+
)?;
283+
off += std::mem::size_of::<u64>();
284+
}
285+
286+
let off = offset_of!(user_pt_regs, sp);
287+
vcpu_fd.set_one_reg(
288+
arm64_core_reg_id!(KVM_REG_SIZE_U64, off + kreg_off),
289+
&regs.sp.to_le_bytes(),
290+
)?;
291+
292+
let off = offset_of!(user_pt_regs, pc);
293+
vcpu_fd.set_one_reg(
294+
arm64_core_reg_id!(KVM_REG_SIZE_U64, off + kreg_off),
295+
&regs.pc.to_le_bytes(),
296+
)?;
297+
298+
Ok(())
62299
}

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)