⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/debug/
breakpoint.rs

1//! GDB Breakpoint and Watchpoint Management
2//!
3//! Implements INT3 software breakpoints (Z0/z0), hardware watchpoints via
4//! x86_64 debug registers DR0-DR3 (Z2/z2 write, Z3/z3 read, Z4/z4 access),
5//! and single-step via EFLAGS TF bit.
6
7#[cfg(feature = "alloc")]
8use alloc::vec::Vec;
9
10/// Maximum software breakpoints
11const MAX_SW_BREAKPOINTS: usize = 256;
12
13/// Maximum hardware watchpoints (DR0-DR3)
14const MAX_HW_WATCHPOINTS: usize = 4;
15
16/// Software breakpoint entry
17#[derive(Debug, Clone, Copy, Default)]
18struct SwBreakpoint {
19    addr: u64,
20    original_byte: u8,
21    active: bool,
22}
23
24/// Hardware watchpoint type
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub(crate) enum WatchpointType {
27    Write,
28    Read,
29    Access,
30}
31
32/// Hardware watchpoint entry
33#[derive(Debug, Clone, Copy)]
34struct HwWatchpoint {
35    addr: u64,
36    len: u8,
37    wp_type: WatchpointType,
38    active: bool,
39}
40
41impl Default for HwWatchpoint {
42    fn default() -> Self {
43        Self {
44            addr: 0,
45            len: 1,
46            wp_type: WatchpointType::Write,
47            active: false,
48        }
49    }
50}
51
52/// Breakpoint manager state
53struct BreakpointManager {
54    sw_breakpoints: [SwBreakpoint; MAX_SW_BREAKPOINTS],
55    sw_count: usize,
56    hw_watchpoints: [HwWatchpoint; MAX_HW_WATCHPOINTS],
57}
58
59impl BreakpointManager {
60    const fn new() -> Self {
61        Self {
62            sw_breakpoints: [SwBreakpoint {
63                addr: 0,
64                original_byte: 0,
65                active: false,
66            }; MAX_SW_BREAKPOINTS],
67            sw_count: 0,
68            hw_watchpoints: [HwWatchpoint {
69                addr: 0,
70                len: 1,
71                wp_type: WatchpointType::Write,
72                active: false,
73            }; MAX_HW_WATCHPOINTS],
74        }
75    }
76
77    fn insert_sw_breakpoint(&mut self, addr: u64) -> bool {
78        // Check for existing
79        for bp in &self.sw_breakpoints[..self.sw_count] {
80            if bp.addr == addr && bp.active {
81                return true; // Already set
82            }
83        }
84
85        if self.sw_count >= MAX_SW_BREAKPOINTS {
86            return false;
87        }
88
89        // Read original byte and replace with INT3 (0xCC)
90        let ptr = addr as *mut u8;
91        // SAFETY: GDB has requested a breakpoint at this address. We trust that
92        // GDB provides valid code addresses. The original byte is preserved for
93        // restoration when the breakpoint is removed.
94        let original = unsafe { core::ptr::read_volatile(ptr) };
95        unsafe {
96            core::ptr::write_volatile(ptr, 0xCC);
97        }
98
99        self.sw_breakpoints[self.sw_count] = SwBreakpoint {
100            addr,
101            original_byte: original,
102            active: true,
103        };
104        self.sw_count += 1;
105        true
106    }
107
108    fn remove_sw_breakpoint(&mut self, addr: u64) -> bool {
109        for bp in &mut self.sw_breakpoints[..self.sw_count] {
110            if bp.addr == addr && bp.active {
111                // Restore original byte
112                let ptr = addr as *mut u8;
113                // SAFETY: Restoring the original instruction byte that was saved
114                // when the breakpoint was inserted.
115                unsafe {
116                    core::ptr::write_volatile(ptr, bp.original_byte);
117                }
118                bp.active = false;
119                return true;
120            }
121        }
122        false
123    }
124
125    fn insert_hw_watchpoint(&mut self, addr: u64, len: u8, wp_type: WatchpointType) -> bool {
126        // Find free DR slot
127        for (idx, wp) in self.hw_watchpoints.iter_mut().enumerate() {
128            if !wp.active {
129                wp.addr = addr;
130                wp.len = len;
131                wp.wp_type = wp_type;
132                wp.active = true;
133
134                #[cfg(all(target_arch = "x86_64", target_os = "none"))]
135                set_debug_register(idx, addr, len, wp_type);
136                #[cfg(not(all(target_arch = "x86_64", target_os = "none")))]
137                let _ = idx;
138
139                return true;
140            }
141        }
142        false
143    }
144
145    fn remove_hw_watchpoint(&mut self, addr: u64, wp_type: WatchpointType) -> bool {
146        for (idx, wp) in self.hw_watchpoints.iter_mut().enumerate() {
147            if wp.active && wp.addr == addr && wp.wp_type == wp_type {
148                wp.active = false;
149
150                #[cfg(all(target_arch = "x86_64", target_os = "none"))]
151                clear_debug_register(idx);
152                #[cfg(not(all(target_arch = "x86_64", target_os = "none")))]
153                let _ = idx;
154
155                return true;
156            }
157        }
158        false
159    }
160}
161
162static BP_MANAGER: spin::Mutex<BreakpointManager> = spin::Mutex::new(BreakpointManager::new());
163
164// ---------------------------------------------------------------------------
165// Debug register manipulation (x86_64 bare-metal only)
166// ---------------------------------------------------------------------------
167
168#[cfg(all(target_arch = "x86_64", target_os = "none"))]
169fn set_debug_register(idx: usize, addr: u64, len: u8, wp_type: WatchpointType) {
170    // SAFETY: Writing x86_64 debug registers DR0-DR3 and DR7 to configure hardware
171    // breakpoints.
172    unsafe {
173        // Set address in DR0-DR3
174        match idx {
175            0 => core::arch::asm!("mov dr0, {}", in(reg) addr, options(nostack)),
176            1 => core::arch::asm!("mov dr1, {}", in(reg) addr, options(nostack)),
177            2 => core::arch::asm!("mov dr2, {}", in(reg) addr, options(nostack)),
178            3 => core::arch::asm!("mov dr3, {}", in(reg) addr, options(nostack)),
179            _ => return,
180        }
181
182        // Configure DR7
183        let mut dr7: u64;
184        core::arch::asm!("mov {}, dr7", out(reg) dr7, options(nostack));
185
186        // Enable local breakpoint for this slot
187        dr7 |= 1 << (idx * 2);
188
189        // Set condition bits (bits 16-17 for DR0, 20-21 for DR1, etc.)
190        let condition = match wp_type {
191            WatchpointType::Write => 0b01,  // Write only
192            WatchpointType::Read => 0b11,   // Read/Write (x86 has no read-only)
193            WatchpointType::Access => 0b11, // Read/Write
194        };
195        let cond_shift = 16 + (idx * 4);
196        dr7 &= !(0b11 << cond_shift);
197        dr7 |= condition << cond_shift;
198
199        // Set length bits
200        let len_bits = match len {
201            1 => 0b00,
202            2 => 0b01,
203            4 => 0b11,
204            8 => 0b10,
205            _ => 0b00,
206        };
207        let len_shift = 18 + (idx * 4);
208        dr7 &= !(0b11 << len_shift);
209        dr7 |= len_bits << len_shift;
210
211        core::arch::asm!("mov dr7, {}", in(reg) dr7, options(nostack));
212    }
213}
214
215#[cfg(all(target_arch = "x86_64", target_os = "none"))]
216fn clear_debug_register(idx: usize) {
217    // SAFETY: Reading and writing x86_64 DR7 to disable a hardware breakpoint slot.
218    unsafe {
219        let mut dr7: u64;
220        core::arch::asm!("mov {}, dr7", out(reg) dr7, options(nostack));
221
222        // Disable local breakpoint
223        dr7 &= !(1 << (idx * 2));
224
225        // Clear condition and length
226        let cond_shift = 16 + (idx * 4);
227        dr7 &= !(0b1111 << cond_shift);
228
229        core::arch::asm!("mov dr7, {}", in(reg) dr7, options(nostack));
230    }
231}
232
233// ---------------------------------------------------------------------------
234// RSP Z/z command handlers
235// ---------------------------------------------------------------------------
236
237fn parse_z_command(data: &[u8]) -> Option<(u8, u64, u64)> {
238    // Format: type,addr,kind
239    if data.is_empty() {
240        return None;
241    }
242
243    let parts: Vec<&[u8]> = data.split(|&b| b == b',').collect();
244    if parts.len() < 3 {
245        return None;
246    }
247
248    let bp_type = super::gdb_stub::hex_digit(parts[0][0])?;
249    let addr = super::gdb_stub::parse_hex_u64(parts[1])?;
250    let kind = super::gdb_stub::parse_hex_u64(parts[2])?;
251
252    Some((bp_type, addr, kind))
253}
254
255/// Handle Z (insert breakpoint/watchpoint) command
256pub(crate) fn handle_insert(data: &[u8]) -> Option<Vec<u8>> {
257    let (bp_type, addr, kind) = parse_z_command(data)?;
258    let mut mgr = BP_MANAGER.lock();
259
260    match bp_type {
261        // Software breakpoint
262        0 => {
263            if mgr.insert_sw_breakpoint(addr) {
264                Some(b"OK".to_vec())
265            } else {
266                Some(b"E01".to_vec())
267            }
268        }
269        // Hardware breakpoint (use sw bp as fallback)
270        1 => {
271            if mgr.insert_sw_breakpoint(addr) {
272                Some(b"OK".to_vec())
273            } else {
274                Some(b"E01".to_vec())
275            }
276        }
277        // Write watchpoint
278        2 => {
279            if mgr.insert_hw_watchpoint(addr, kind as u8, WatchpointType::Write) {
280                Some(b"OK".to_vec())
281            } else {
282                Some(b"E01".to_vec())
283            }
284        }
285        // Read watchpoint
286        3 => {
287            if mgr.insert_hw_watchpoint(addr, kind as u8, WatchpointType::Read) {
288                Some(b"OK".to_vec())
289            } else {
290                Some(b"E01".to_vec())
291            }
292        }
293        // Access watchpoint
294        4 => {
295            if mgr.insert_hw_watchpoint(addr, kind as u8, WatchpointType::Access) {
296                Some(b"OK".to_vec())
297            } else {
298                Some(b"E01".to_vec())
299            }
300        }
301        _ => None,
302    }
303}
304
305/// Handle z (remove breakpoint/watchpoint) command
306pub(crate) fn handle_remove(data: &[u8]) -> Option<Vec<u8>> {
307    let (bp_type, addr, _kind) = parse_z_command(data)?;
308    let mut mgr = BP_MANAGER.lock();
309
310    match bp_type {
311        0 | 1 => {
312            if mgr.remove_sw_breakpoint(addr) {
313                Some(b"OK".to_vec())
314            } else {
315                Some(b"E01".to_vec())
316            }
317        }
318        2 => {
319            if mgr.remove_hw_watchpoint(addr, WatchpointType::Write) {
320                Some(b"OK".to_vec())
321            } else {
322                Some(b"E01".to_vec())
323            }
324        }
325        3 => {
326            if mgr.remove_hw_watchpoint(addr, WatchpointType::Read) {
327                Some(b"OK".to_vec())
328            } else {
329                Some(b"E01".to_vec())
330            }
331        }
332        4 => {
333            if mgr.remove_hw_watchpoint(addr, WatchpointType::Access) {
334                Some(b"OK".to_vec())
335            } else {
336                Some(b"E01".to_vec())
337            }
338        }
339        _ => None,
340    }
341}
342
343// ---------------------------------------------------------------------------
344// Tests
345// ---------------------------------------------------------------------------
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    #[test]
352    fn test_breakpoint_manager_creation() {
353        let mgr = BreakpointManager::new();
354        assert_eq!(mgr.sw_count, 0);
355        for wp in &mgr.hw_watchpoints {
356            assert!(!wp.active);
357        }
358    }
359
360    #[test]
361    fn test_watchpoint_type_eq() {
362        assert_eq!(WatchpointType::Write, WatchpointType::Write);
363        assert_ne!(WatchpointType::Write, WatchpointType::Read);
364        assert_ne!(WatchpointType::Read, WatchpointType::Access);
365    }
366
367    #[test]
368    fn test_parse_z_command() {
369        let result = parse_z_command(b"0,401000,1");
370        assert!(result.is_some());
371        let (tp, addr, kind) = result.unwrap();
372        assert_eq!(tp, 0);
373        assert_eq!(addr, 0x401000);
374        assert_eq!(kind, 1);
375    }
376
377    #[test]
378    fn test_parse_z_command_watchpoint() {
379        let result = parse_z_command(b"2,7ffe1234,4");
380        assert!(result.is_some());
381        let (tp, addr, kind) = result.unwrap();
382        assert_eq!(tp, 2);
383        assert_eq!(addr, 0x7FFE1234);
384        assert_eq!(kind, 4);
385    }
386
387    #[test]
388    fn test_parse_z_command_invalid() {
389        assert!(parse_z_command(b"").is_none());
390        assert!(parse_z_command(b"0").is_none());
391    }
392
393    #[test]
394    fn test_hw_watchpoint_slots() {
395        let mut mgr = BreakpointManager::new();
396        // Fill all 4 slots
397        for i in 0..4 {
398            assert!(mgr.insert_hw_watchpoint(0x1000 + i * 8, 4, WatchpointType::Write));
399        }
400        // 5th should fail
401        assert!(!mgr.insert_hw_watchpoint(0x2000, 4, WatchpointType::Write));
402
403        // Remove one and try again
404        assert!(mgr.remove_hw_watchpoint(0x1000, WatchpointType::Write));
405        assert!(mgr.insert_hw_watchpoint(0x2000, 4, WatchpointType::Write));
406    }
407
408    #[test]
409    fn test_remove_nonexistent_watchpoint() {
410        let mut mgr = BreakpointManager::new();
411        assert!(!mgr.remove_hw_watchpoint(0xDEAD, WatchpointType::Write));
412    }
413
414    #[test]
415    fn test_sw_breakpoint_default() {
416        let bp = SwBreakpoint::default();
417        assert_eq!(bp.addr, 0);
418        assert_eq!(bp.original_byte, 0);
419        assert!(!bp.active);
420    }
421
422    #[test]
423    fn test_hw_watchpoint_default() {
424        let wp = HwWatchpoint::default();
425        assert_eq!(wp.addr, 0);
426        assert_eq!(wp.len, 1);
427        assert_eq!(wp.wp_type, WatchpointType::Write);
428        assert!(!wp.active);
429    }
430
431    #[test]
432    fn test_handle_insert_parse() {
433        // Test parsing only (not actual insertion since that touches memory)
434        let result = parse_z_command(b"2,deadbeef,8");
435        assert!(result.is_some());
436        let (tp, addr, kind) = result.unwrap();
437        assert_eq!(tp, 2);
438        assert_eq!(addr, 0xDEADBEEF);
439        assert_eq!(kind, 8);
440    }
441}