Skip to content

Commit 3ed8fce

Browse files
committed
feat: Support GDB debugging on ARM
Add support for debugging aarch64 with gdb
1 parent 7fc9270 commit 3ed8fce

File tree

5 files changed

+299
-52
lines changed

5 files changed

+299
-52
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: 249 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,287 @@
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+
const DESC_MASK: u64 = (!0u64 >> (64 - 48)) & !(!0u64 >> (64 - 12));
66+
67+
/// Translates a virtual address according to the Vcpu's current address translation mode.
68+
///
69+
/// To simplify the implementation we've made some assumptions about the paging setup
70+
/// here we just assert firstly paging is setup and these assumptions are correct
71+
pub fn translate_gva(vcpu_fd: &VcpuFd, gva: u64, vmm: &Vmm) -> Result<u64, GdbTargetError> {
72+
// Check this virtual address is in kernel space
73+
if extract_bits_64!(gva, 55, 1) == 0 {
74+
return Err(GdbTargetError::GvaTranslateError);
75+
}
76+
77+
// Translation control register
78+
let tcr_el1: u64 = get_sys_reg(TCR_EL1, vcpu_fd)?;
79+
80+
// If this is 0 then translation is not yet ready
81+
if extract_bits_64!(tcr_el1, 16, 6) == 0 {
82+
return Ok(gva);
83+
}
84+
85+
// Check 4KB pages are being used
86+
if extract_bits_64!(tcr_el1, 30, 2) != 2 {
87+
return Err(GdbTargetError::GvaTranslateError);
88+
}
89+
90+
// ID_AA64MMFR0_EL1 provides information about the implemented memory model and memory
91+
// management, check this is a physical address size we support
92+
if (get_sys_reg(ID_AA64MMFR0_EL1, vcpu_fd)? & 0b1111) != 5 {
93+
return Err(GdbTargetError::GvaTranslateError);
94+
}
95+
96+
let page_indicies = [
97+
(gva >> 36) & 0xFFF,
98+
(gva >> 27) & 0xFFF,
99+
(gva >> 18) & 0xFFF,
100+
(gva >> 9) & 0xFFF,
101+
];
102+
103+
// Transition table base register used for initial table lookup
104+
// take the bottom 48 bits from the register value
105+
let mut pte: u64 = get_sys_reg(TTBR1_EL1, vcpu_fd)? & (!0u64 >> (64 - 48));
106+
let mut level = 0;
107+
108+
while level < 4 {
109+
// Clear the bottom 3 bits from this address
110+
let address = read_address(vmm, (pte | page_indicies[level]) & PTE_ADDRESS_MASK)?;
111+
pte = address & DESC_MASK;
112+
113+
// If this is a valid table entry and we aren't at the end of the page tables
114+
// then loop again and check next level
115+
if (address & 2 != 0) && (level < 3) {
116+
level += 1;
117+
continue;
118+
}
119+
break;
120+
}
121+
122+
let page_size = 1u64 << ((9 * (4 - level)) + 3);
123+
// Clear bottom bits of page size
124+
pte &= !(page_size - 1);
125+
pte |= gva & (page_size - 1);
126+
Ok(pte)
24127
}
25128

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

35189
/// Configures the Vcpu for debugging and sets the hardware breakpoints on the Vcpu
36190
pub fn vcpu_set_debug(
37-
_vcpu_fd: &VcpuFd,
38-
_addrs: &[GuestAddress],
39-
_step: bool,
191+
vcpu_fd: &VcpuFd,
192+
addrs: &[GuestAddress],
193+
step: bool,
40194
) -> Result<(), GdbTargetError> {
41-
unimplemented!()
195+
let mut control = KVM_GUESTDBG_ENABLE | KVM_GUESTDBG_USE_HW | KVM_GUESTDBG_USE_SW_BP;
196+
if step {
197+
control |= KVM_GUESTDBG_SINGLESTEP;
198+
}
199+
200+
toggle_interrupts(vcpu_fd, step)?;
201+
set_kvm_debug(control, vcpu_fd, addrs)
42202
}
43203

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.
204+
/// KVM does not support injecting breakpoints on aarch64 so this is a no-op
46205
pub fn vcpu_inject_bp(
47206
_vcpu_fd: &VcpuFd,
48207
_addrs: &[GuestAddress],
49208
_step: bool,
50209
) -> Result<(), GdbTargetError> {
51-
unimplemented!()
210+
Ok(())
52211
}
53212

213+
/// The number of core registers we read from the Vcpu
214+
const CORE_REG_COUNT: usize = 33;
215+
/// Stores the register ids of ids to be read fron the Vcpu
216+
const CORE_REG_IDS: [u64; CORE_REG_COUNT] = {
217+
let mut regs = [0; CORE_REG_COUNT];
218+
let mut idx = 0;
219+
220+
let reg_offset = offset_of!(kvm_regs, regs);
221+
let mut off = reg_offset;
222+
while idx < 31 {
223+
regs[idx] = arm64_core_reg_id!(KVM_REG_SIZE_U64, off);
224+
idx += 1;
225+
off += std::mem::size_of::<u64>();
226+
}
227+
228+
regs[idx] = arm64_core_reg_id!(KVM_REG_SIZE_U64, offset_of!(user_pt_regs, sp));
229+
idx += 1;
230+
231+
regs[idx] = arm64_core_reg_id!(KVM_REG_SIZE_U64, offset_of!(user_pt_regs, pc));
232+
regs
233+
};
234+
54235
/// Reads the registers for the Vcpu
55-
pub fn read_registers(_vcpu_fd: &VcpuFd, _regs: &mut CoreRegs) -> Result<(), GdbTargetError> {
56-
unimplemented!()
236+
pub fn read_registers(vcpu_fd: &VcpuFd, regs: &mut CoreRegs) -> Result<(), GdbTargetError> {
237+
let mut register_vec = Aarch64RegisterVec::default();
238+
get_registers(vcpu_fd, &CORE_REG_IDS, &mut register_vec)?;
239+
240+
let mut registers = register_vec.iter();
241+
242+
for i in 0..31 {
243+
regs.x[i] = registers
244+
.next()
245+
.ok_or(GdbTargetError::ReadRegisterVecError)?
246+
.value();
247+
}
248+
249+
regs.sp = registers
250+
.next()
251+
.ok_or(GdbTargetError::ReadRegisterVecError)?
252+
.value();
253+
254+
regs.pc = registers
255+
.next()
256+
.ok_or(GdbTargetError::ReadRegisterVecError)?
257+
.value();
258+
259+
Ok(())
57260
}
58261

59262
/// Writes to the registers for the Vcpu
60-
pub fn write_registers(_vcpu_fd: &VcpuFd, _regs: &CoreRegs) -> Result<(), GdbTargetError> {
61-
unimplemented!()
263+
pub fn write_registers(vcpu_fd: &VcpuFd, regs: &CoreRegs) -> Result<(), GdbTargetError> {
264+
let kreg_off = offset_of!(kvm_regs, regs);
265+
let mut off = kreg_off;
266+
for i in 0..31 {
267+
vcpu_fd.set_one_reg(
268+
arm64_core_reg_id!(KVM_REG_SIZE_U64, off),
269+
&regs.x[i].to_le_bytes(),
270+
)?;
271+
off += std::mem::size_of::<u64>();
272+
}
273+
274+
let off = offset_of!(user_pt_regs, sp);
275+
vcpu_fd.set_one_reg(
276+
arm64_core_reg_id!(KVM_REG_SIZE_U64, off + kreg_off),
277+
&regs.sp.to_le_bytes(),
278+
)?;
279+
280+
let off = offset_of!(user_pt_regs, pc);
281+
vcpu_fd.set_one_reg(
282+
arm64_core_reg_id!(KVM_REG_SIZE_U64, off + kreg_off),
283+
&regs.pc.to_le_bytes(),
284+
)?;
285+
286+
Ok(())
62287
}

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)