// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause //go:build linux && loong64 package debug import ( "fmt" "syscall" "unsafe" ) // Hardware watchpoint support via the NT_LOONGARCH_HW_WATCH regset. // // The kernel's PTRACE_POKEUSER on loong64 accepts only the user_pt_regs // indices 0-34 (GPRs, orig_a0, era, badv, per // arch/loongarch/include/uapi/asm/ptrace.h), so there is no debug-register // window to poke. The real interface is PTRACE_GETREGSET/SETREGSET on // NT_LOONGARCH_HW_WATCH (0xa06, include/uapi/linux/elf.h) with struct // user_watch_state_v2 (arch/loongarch/include/uapi/asm/ptrace.h): a dbg_info // word followed by 14 slots of {addr u64, mask u64, ctrl u32, pad u32}. // hw_break_get puts the slot count in the low byte of dbg_info // (arch/loongarch/kernel/ptrace.c, ptrace_hbp_get_resource_info) and // hw_break_set ignores dbg_info, reading addr, mask and ctrl per slot. const ntLoongHWWatch = 0xa06 // loongWatchState mirrors the kernel's struct user_watch_state_v2. type loongWatchState struct { DbgInfo uint64 DbgRegs [14]loongWatchReg } type loongWatchReg struct { Addr uint64 Mask uint64 Ctrl uint32 Pad uint32 } // Control word bit layout (arch/loongarch/include/asm/hw_breakpoint.h): // bits 1-4 privilege enables (CTRL_PLV3_ENABLE, 0x10, covers user mode), // bits 8-9 access type (LOAD 1<<0, STORE 1<<1), bits 10-11 length // (0=8 bytes, 1=4, 2=2, 3=1, inverted like the hardware FWP cfg). const ( loongCtrlPLV3Enable = 0x10 loongTypeLoad = 1 << 8 loongTypeStore = 2 << 8 loongLenShift = 10 ) // WatchpointType selects what triggers the watchpoint. type WatchpointType int const ( WatchWrite WatchpointType = 1 WatchRead WatchpointType = 3 ) // maxWatchpoints reports the slot capacity of the regset struct; the number // the hardware actually provides is read from dbg_info at arm time. func maxWatchpoints() int { return len(loongWatchState{}.DbgRegs) } // archWatchpointAddr resolves the address of the watchpoint that fired: // the loongarch kernel already reports the accessed address as si_addr. func archWatchpointAddr(s *Session, siAddr uint64) uint64 { return siAddr } func (s *Session) FindFreeWatchpointSlot() int { for i := range maxWatchpoints() { if !s.wpSlots[i] { return i } } return -1 } func (s *Session) IsWatchpointSlotUsed(slot int) bool { if slot < 0 || slot >= maxWatchpoints() { return false } return s.wpSlots[slot] } // SetWatchpoint installs a hardware watchpoint on the given address. func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error { if slot < 0 || slot >= maxWatchpoints() { return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints()-1) } if s.wpSlots[slot] { return fmt.Errorf("debug: watchpoint slot %d already in use", slot) } var ctrlType uint32 switch typ { case WatchWrite: ctrlType = loongTypeStore case WatchRead: ctrlType = loongTypeLoad | loongTypeStore } var lenBits uint32 switch size { case 1: lenBits = 3 case 2: lenBits = 2 case 4: lenBits = 1 case 8: lenBits = 0 default: return fmt.Errorf("debug: watchpoint size must be 1, 2, 4, or 8") } state, err := s.getLoongWatchState() if err != nil { return fmt.Errorf("debug: read watchpoint state: %w", err) } if uint64(slot) >= state.DbgInfo&0xff { return fmt.Errorf("debug: slot %d exceeds available watchpoints (%d)", slot, state.DbgInfo&0xff) } state.DbgRegs[slot].Addr = addr state.DbgRegs[slot].Mask = 0 state.DbgRegs[slot].Ctrl = loongCtrlPLV3Enable | ctrlType | lenBits<= maxWatchpoints() { return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints()-1) } if !s.wpSlots[slot] { return fmt.Errorf("debug: watchpoint slot %d is not in use", slot) } state, err := s.getLoongWatchState() if err != nil { return err } state.DbgRegs[slot].Addr = 0 state.DbgRegs[slot].Mask = 0 state.DbgRegs[slot].Ctrl = 0 if err := s.setLoongWatchState(state); err != nil { return err } s.wpSlots[slot] = false return nil } func (s *Session) ClearAllWatchpoints() error { for slot := range maxWatchpoints() { if s.wpSlots[slot] { if err := s.ClearWatchpoint(slot); err != nil { return err } } } return nil } func (s *Session) getLoongWatchState() (*loongWatchState, error) { var state loongWatchState iovec := syscall.Iovec{ Base: (*byte)(unsafe.Pointer(&state)), Len: uint64(unsafe.Sizeof(state)), } _, _, errno := syscall.Syscall6( syscall.SYS_PTRACE, uintptr(syscall.PTRACE_GETREGSET), uintptr(s.pid), uintptr(ntLoongHWWatch), uintptr(unsafe.Pointer(&iovec)), 0, 0, ) if errno != 0 { return nil, errno } return &state, nil } func (s *Session) setLoongWatchState(state *loongWatchState) error { iovec := syscall.Iovec{ Base: (*byte)(unsafe.Pointer(state)), Len: uint64(unsafe.Sizeof(*state)), } _, _, errno := syscall.Syscall6( syscall.SYS_PTRACE, uintptr(syscall.PTRACE_SETREGSET), uintptr(s.pid), uintptr(ntLoongHWWatch), uintptr(unsafe.Pointer(&iovec)), 0, 0, ) if errno != 0 { return errno } return nil }