From 4f6080f818ab339a8dfc83be33364be9a828d426 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:27:50 +0000 Subject: [PATCH 1/7] Initial plan From 591d6cfe2faf7565ee5803c7497bc2c2d5983aa6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:36:06 +0000 Subject: [PATCH 2/7] Add hardware breakpoint type enum and standardized interface Co-authored-by: xusheng6 <94503187+xusheng6@users.noreply.github.com> --- core/adapters/esrevenadapter.cpp | 68 +++++++++++++++++++-- core/adapters/esrevenadapter.h | 6 +- core/adapters/gdbadapter.cpp | 68 +++++++++++++++++++-- core/adapters/gdbadapter.h | 6 +- core/adapters/lldbadapter.cpp | 100 +++++++++++++++++++++++++++++++ core/adapters/lldbadapter.h | 4 ++ core/debugadapter.cpp | 14 +++++ core/debugadapter.h | 4 ++ core/debuggercommon.h | 10 ++++ 9 files changed, 270 insertions(+), 10 deletions(-) diff --git a/core/adapters/esrevenadapter.cpp b/core/adapters/esrevenadapter.cpp index ccc6a3a5..af77d981 100644 --- a/core/adapters/esrevenadapter.cpp +++ b/core/adapters/esrevenadapter.cpp @@ -1068,20 +1068,80 @@ bool EsrevenAdapter::StepOverReverse() return status != InternalError; } -bool EsrevenAdapter::AddHardwareWriteBreakpoint(uint64_t address) +bool EsrevenAdapter::AddHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size) { if (m_isTargetRunning || !m_rspConnector) return false; - return this->m_rspConnector->TransmitAndReceive(RspData("Z2,{:x},{}", address, 1)).AsString() != "OK"; + std::string command; + switch (type) + { + case HardwareExecuteBreakpoint: + // Z1 = hardware execution breakpoint + command = fmt::format("Z1,{:x},{}", address, size); + break; + case HardwareReadBreakpoint: + // Z3 = hardware read watchpoint + command = fmt::format("Z3,{:x},{}", address, size); + break; + case HardwareWriteBreakpoint: + // Z2 = hardware write watchpoint + command = fmt::format("Z2,{:x},{}", address, size); + break; + case HardwareAccessBreakpoint: + // Z4 = hardware access watchpoint (read/write) + command = fmt::format("Z4,{:x},{}", address, size); + break; + default: + return false; + } + + return m_rspConnector->TransmitAndReceive(RspData(command)).AsString() == "OK"; } -bool EsrevenAdapter::RemoveHardwareWriteBreakpoint(uint64_t address) + +bool EsrevenAdapter::RemoveHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size) { if (m_isTargetRunning || !m_rspConnector) return false; - return this->m_rspConnector->TransmitAndReceive(RspData("Z2,{:x},{}", address, 1)).AsString() != "OK"; + std::string command; + switch (type) + { + case HardwareExecuteBreakpoint: + // z1 = remove hardware execution breakpoint + command = fmt::format("z1,{:x},{}", address, size); + break; + case HardwareReadBreakpoint: + // z3 = remove hardware read watchpoint + command = fmt::format("z3,{:x},{}", address, size); + break; + case HardwareWriteBreakpoint: + // z2 = remove hardware write watchpoint + command = fmt::format("z2,{:x},{}", address, size); + break; + case HardwareAccessBreakpoint: + // z4 = remove hardware access watchpoint (read/write) + command = fmt::format("z4,{:x},{}", address, size); + break; + default: + return false; + } + + return m_rspConnector->TransmitAndReceive(RspData(command)).AsString() == "OK"; +} + + +bool EsrevenAdapter::AddHardwareWriteBreakpoint(uint64_t address) +{ + // Delegate to new standardized method + return AddHardwareBreakpoint(address, HardwareWriteBreakpoint, 1); +} + +bool EsrevenAdapter::RemoveHardwareWriteBreakpoint(uint64_t address) +{ + // Delegate to new standardized method + return RemoveHardwareBreakpoint(address, HardwareWriteBreakpoint, 1); } bool EsrevenAdapter::StepReturnReverse() diff --git a/core/adapters/esrevenadapter.h b/core/adapters/esrevenadapter.h index d2f10845..9f4ad42c 100644 --- a/core/adapters/esrevenadapter.h +++ b/core/adapters/esrevenadapter.h @@ -156,7 +156,11 @@ namespace BinaryNinjaDebugger bool ResumeThread(std::uint32_t tid) override; DebugBreakpoint AddBreakpoint(const ModuleNameAndOffset& address, unsigned long breakpoint_type = 0) override; - // Temporary internal methods + // Hardware breakpoint and watchpoint support + bool AddHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size = 1) override; + bool RemoveHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size = 1) override; + + // Legacy methods - kept for backward compatibility bool AddHardwareWriteBreakpoint(uint64_t address); bool RemoveHardwareWriteBreakpoint(uint64_t address); diff --git a/core/adapters/gdbadapter.cpp b/core/adapters/gdbadapter.cpp index b7f66bed..e9bdef2c 100644 --- a/core/adapters/gdbadapter.cpp +++ b/core/adapters/gdbadapter.cpp @@ -1116,20 +1116,80 @@ bool GdbAdapter::StepOverReverse() return status != InternalError; } -bool GdbAdapter::AddHardwareWriteBreakpoint(uint64_t address) +bool GdbAdapter::AddHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size) { if (m_isTargetRunning || !m_rspConnector) return false; - return this->m_rspConnector->TransmitAndReceive(RspData("Z2,{:x},{}", address, 1)).AsString() != "OK"; + std::string command; + switch (type) + { + case HardwareExecuteBreakpoint: + // Z1 = hardware execution breakpoint + command = fmt::format("Z1,{:x},{}", address, size); + break; + case HardwareReadBreakpoint: + // Z3 = hardware read watchpoint + command = fmt::format("Z3,{:x},{}", address, size); + break; + case HardwareWriteBreakpoint: + // Z2 = hardware write watchpoint + command = fmt::format("Z2,{:x},{}", address, size); + break; + case HardwareAccessBreakpoint: + // Z4 = hardware access watchpoint (read/write) + command = fmt::format("Z4,{:x},{}", address, size); + break; + default: + return false; + } + + return m_rspConnector->TransmitAndReceive(RspData(command)).AsString() == "OK"; } -bool GdbAdapter::RemoveHardwareWriteBreakpoint(uint64_t address) + +bool GdbAdapter::RemoveHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size) { if (m_isTargetRunning || !m_rspConnector) return false; - return this->m_rspConnector->TransmitAndReceive(RspData("Z2,{:x},{}", address, 1)).AsString() != "OK"; + std::string command; + switch (type) + { + case HardwareExecuteBreakpoint: + // z1 = remove hardware execution breakpoint + command = fmt::format("z1,{:x},{}", address, size); + break; + case HardwareReadBreakpoint: + // z3 = remove hardware read watchpoint + command = fmt::format("z3,{:x},{}", address, size); + break; + case HardwareWriteBreakpoint: + // z2 = remove hardware write watchpoint + command = fmt::format("z2,{:x},{}", address, size); + break; + case HardwareAccessBreakpoint: + // z4 = remove hardware access watchpoint (read/write) + command = fmt::format("z4,{:x},{}", address, size); + break; + default: + return false; + } + + return m_rspConnector->TransmitAndReceive(RspData(command)).AsString() == "OK"; +} + + +bool GdbAdapter::AddHardwareWriteBreakpoint(uint64_t address) +{ + // Delegate to new standardized method + return AddHardwareBreakpoint(address, HardwareWriteBreakpoint, 1); +} + +bool GdbAdapter::RemoveHardwareWriteBreakpoint(uint64_t address) +{ + // Delegate to new standardized method + return RemoveHardwareBreakpoint(address, HardwareWriteBreakpoint, 1); } bool GdbAdapter::StepReturnReverse() diff --git a/core/adapters/gdbadapter.h b/core/adapters/gdbadapter.h index ebcd2e0f..40d2dae9 100644 --- a/core/adapters/gdbadapter.h +++ b/core/adapters/gdbadapter.h @@ -156,7 +156,11 @@ namespace BinaryNinjaDebugger bool ResumeThread(std::uint32_t tid) override; DebugBreakpoint AddBreakpoint(const ModuleNameAndOffset& address, unsigned long breakpoint_type = 0) override; - // Temporary internal methods + // Hardware breakpoint and watchpoint support + bool AddHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size = 1) override; + bool RemoveHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size = 1) override; + + // Legacy methods - kept for backward compatibility bool AddHardwareWriteBreakpoint(uint64_t address); bool RemoveHardwareWriteBreakpoint(uint64_t address); diff --git a/core/adapters/lldbadapter.cpp b/core/adapters/lldbadapter.cpp index a2b79dfb..977b00f7 100644 --- a/core/adapters/lldbadapter.cpp +++ b/core/adapters/lldbadapter.cpp @@ -908,6 +908,16 @@ std::vector LldbAdapter::GetFramesOfThread(uint32_t tid) DebugBreakpoint LldbAdapter::AddBreakpoint(const std::uintptr_t address, unsigned long breakpoint_type) { + // Check if this is a hardware breakpoint type + if (breakpoint_type == HardwareExecuteBreakpoint) + { + if (AddHardwareBreakpoint(address, HardwareExecuteBreakpoint)) + return DebugBreakpoint(address, 0, true); // Use 0 as ID for hardware breakpoints for now + else + return DebugBreakpoint {}; + } + + // Default software breakpoint SBBreakpoint bp = m_target.BreakpointCreateByAddress(address); if (!bp.IsValid()) return DebugBreakpoint {}; @@ -984,6 +994,96 @@ std::vector LldbAdapter::GetBreakpointList() const } +bool LldbAdapter::AddHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size) +{ + if (!m_targetActive) + return false; + + switch (type) + { + case HardwareExecuteBreakpoint: + { + // Use LLDB command to set hardware execution breakpoint + std::string command = fmt::format("breakpoint set --address 0x{:x} -H", address); + auto result = InvokeBackendCommand(command); + return result.find("Breakpoint") != std::string::npos; + } + case HardwareReadBreakpoint: + { + // Use LLDB watchpoint command for read + std::string command = fmt::format("watchpoint set expression -w read -s {} -- 0x{:x}", size, address); + auto result = InvokeBackendCommand(command); + return result.find("Watchpoint") != std::string::npos; + } + case HardwareWriteBreakpoint: + { + // Use LLDB watchpoint command for write + std::string command = fmt::format("watchpoint set expression -w write -s {} -- 0x{:x}", size, address); + auto result = InvokeBackendCommand(command); + return result.find("Watchpoint") != std::string::npos; + } + case HardwareAccessBreakpoint: + { + // Use LLDB watchpoint command for read/write + std::string command = fmt::format("watchpoint set expression -w read_write -s {} -- 0x{:x}", size, address); + auto result = InvokeBackendCommand(command); + return result.find("Watchpoint") != std::string::npos; + } + default: + return false; + } +} + + +bool LldbAdapter::RemoveHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size) +{ + if (!m_targetActive) + return false; + + switch (type) + { + case HardwareExecuteBreakpoint: + { + // Find and delete hardware breakpoint at address + for (size_t i = 0; i < m_target.GetNumBreakpoints(); i++) + { + auto bp = m_target.GetBreakpointAtIndex(i); + if (bp.IsHardware()) + { + for (size_t j = 0; j < bp.GetNumLocations(); j++) + { + auto location = bp.GetLocationAtIndex(j); + auto bpAddress = location.GetAddress().GetLoadAddress(m_target); + if (address == bpAddress) + { + return m_target.BreakpointDelete(bp.GetID()); + } + } + } + } + return false; + } + case HardwareReadBreakpoint: + case HardwareWriteBreakpoint: + case HardwareAccessBreakpoint: + { + // Find and delete watchpoint at address + for (size_t i = 0; i < m_target.GetNumWatchpoints(); i++) + { + auto wp = m_target.GetWatchpointAtIndex(i); + if (wp.GetWatchAddress() == address) + { + return m_target.DeleteWatchpoint(wp.GetID()); + } + } + return false; + } + default: + return false; + } +} + + static intx::uint512 SBValueToUint512(lldb::SBValue& reg_val) { using namespace lldb; using namespace intx; diff --git a/core/adapters/lldbadapter.h b/core/adapters/lldbadapter.h index e1f42984..f8588a08 100644 --- a/core/adapters/lldbadapter.h +++ b/core/adapters/lldbadapter.h @@ -94,6 +94,10 @@ namespace BinaryNinjaDebugger { std::vector GetBreakpointList() const override; + // Hardware breakpoint and watchpoint support + bool AddHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size = 1) override; + bool RemoveHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size = 1) override; + std::unordered_map ReadAllRegisters() override; DebugRegister ReadRegister(const std::string& reg) override; diff --git a/core/debugadapter.cpp b/core/debugadapter.cpp index 52360905..c32d16b6 100644 --- a/core/debugadapter.cpp +++ b/core/debugadapter.cpp @@ -205,3 +205,17 @@ bool DebugAdapter::SetTTDPosition(const TTDPosition& position) // Default implementation returns false for adapters that don't support TTD return false; } + + +bool DebugAdapter::AddHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size) +{ + // Default implementation returns false for adapters that don't support hardware breakpoints + return false; +} + + +bool DebugAdapter::RemoveHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size) +{ + // Default implementation returns false for adapters that don't support hardware breakpoints + return false; +} diff --git a/core/debugadapter.h b/core/debugadapter.h index 548c023a..3938baa3 100644 --- a/core/debugadapter.h +++ b/core/debugadapter.h @@ -264,6 +264,10 @@ namespace BinaryNinjaDebugger { virtual std::vector GetBreakpointList() const = 0; + // Hardware breakpoint and watchpoint support + virtual bool AddHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size = 1); + virtual bool RemoveHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size = 1); + virtual std::unordered_map ReadAllRegisters() = 0; virtual DebugRegister ReadRegister(const std::string& reg) = 0; diff --git a/core/debuggercommon.h b/core/debuggercommon.h index 22fafc3f..458acbd5 100644 --- a/core/debuggercommon.h +++ b/core/debuggercommon.h @@ -152,4 +152,14 @@ namespace BinaryNinjaDebugger { TTDCallEvent() : threadId(0), uniqueThreadId(0), functionAddress(0), returnAddress(0), returnValue(0), hasReturnValue(false) {} }; + + // Breakpoint types - used to specify the type of breakpoint to set + enum DebugBreakpointType + { + SoftwareBreakpoint = 0, // Default software breakpoint + HardwareExecuteBreakpoint = 1, // Hardware execution breakpoint + HardwareReadBreakpoint = 2, // Hardware read watchpoint + HardwareWriteBreakpoint = 3, // Hardware write watchpoint + HardwareAccessBreakpoint = 4 // Hardware read/write watchpoint + }; }; // namespace BinaryNinjaDebugger From 8a18bc454798af963b900a6599885f257f66fec1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:42:44 +0000 Subject: [PATCH 3/7] Add API and FFI support for hardware breakpoints Co-authored-by: xusheng6 <94503187+xusheng6@users.noreply.github.com> --- api/debuggerapi.h | 15 +++++++++++++++ api/debuggercontroller.cpp | 12 ++++++++++++ api/ffi.h | 16 ++++++++++++++++ core/ffi.cpp | 28 ++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+) diff --git a/api/debuggerapi.h b/api/debuggerapi.h index 2d3cf9e5..b8a63cff 100644 --- a/api/debuggerapi.h +++ b/api/debuggerapi.h @@ -362,6 +362,17 @@ namespace BinaryNinjaDebuggerAPI { }; + // Breakpoint types - used to specify the type of breakpoint to set + enum DebugBreakpointType + { + SoftwareBreakpoint = 0, // Default software breakpoint + HardwareExecuteBreakpoint = 1, // Hardware execution breakpoint + HardwareReadBreakpoint = 2, // Hardware read watchpoint + HardwareWriteBreakpoint = 3, // Hardware write watchpoint + HardwareAccessBreakpoint = 4 // Hardware read/write watchpoint + }; + + struct ModuleNameAndOffset { std::string module; @@ -647,6 +658,10 @@ namespace BinaryNinjaDebuggerAPI { bool ContainsBreakpoint(uint64_t address); bool ContainsBreakpoint(const ModuleNameAndOffset& breakpoint); + // Hardware breakpoint and watchpoint support + bool AddHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size = 1); + bool RemoveHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size = 1); + uint64_t IP(); uint64_t GetLastIP(); bool SetIP(uint64_t address); diff --git a/api/debuggercontroller.cpp b/api/debuggercontroller.cpp index 859995ab..3495cadb 100644 --- a/api/debuggercontroller.cpp +++ b/api/debuggercontroller.cpp @@ -766,6 +766,18 @@ bool DebuggerController::ContainsBreakpoint(const ModuleNameAndOffset& breakpoin } +bool DebuggerController::AddHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size) +{ + return BNDebuggerAddHardwareBreakpoint(m_object, address, (BNDebugBreakpointType)type, size); +} + + +bool DebuggerController::RemoveHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size) +{ + return BNDebuggerRemoveHardwareBreakpoint(m_object, address, (BNDebugBreakpointType)type, size); +} + + uint64_t DebuggerController::RelativeAddressToAbsolute(const ModuleNameAndOffset& address) { return BNDebuggerRelativeAddressToAbsolute(m_object, address.module.c_str(), address.offset); diff --git a/api/ffi.h b/api/ffi.h index b2f489ae..d91887cb 100644 --- a/api/ffi.h +++ b/api/ffi.h @@ -218,6 +218,16 @@ extern "C" } BNDebugAdapterTargetStatus; + typedef enum BNDebugBreakpointType + { + BNSoftwareBreakpoint = 0, // Default software breakpoint + BNHardwareExecuteBreakpoint = 1, // Hardware execution breakpoint + BNHardwareReadBreakpoint = 2, // Hardware read watchpoint + BNHardwareWriteBreakpoint = 3, // Hardware write watchpoint + BNHardwareAccessBreakpoint = 4 // Hardware read/write watchpoint + } BNDebugBreakpointType; + + typedef enum BNDebuggerEventType { LaunchEventType, @@ -522,6 +532,12 @@ extern "C" DEBUGGER_FFI_API bool BNDebuggerContainsRelativeBreakpoint( BNDebuggerController* controller, const char* module, uint64_t offset); + // Hardware breakpoint and watchpoint support + DEBUGGER_FFI_API bool BNDebuggerAddHardwareBreakpoint(BNDebuggerController* controller, uint64_t address, + BNDebugBreakpointType type, size_t size); + DEBUGGER_FFI_API bool BNDebuggerRemoveHardwareBreakpoint(BNDebuggerController* controller, uint64_t address, + BNDebugBreakpointType type, size_t size); + DEBUGGER_FFI_API uint64_t BNDebuggerGetIP(BNDebuggerController* controller); DEBUGGER_FFI_API uint64_t BNDebuggerGetLastIP(BNDebuggerController* controller); DEBUGGER_FFI_API bool BNDebuggerSetIP(BNDebuggerController* controller, uint64_t address); diff --git a/core/ffi.cpp b/core/ffi.cpp index c3f77a86..1c8b1b5b 100644 --- a/core/ffi.cpp +++ b/core/ffi.cpp @@ -897,6 +897,34 @@ bool BNDebuggerContainsRelativeBreakpoint(BNDebuggerController* controller, cons } +bool BNDebuggerAddHardwareBreakpoint(BNDebuggerController* controller, uint64_t address, BNDebugBreakpointType type, size_t size) +{ + DebuggerState* state = controller->object->GetState(); + if (!state) + return false; + + DebugAdapter* adapter = state->GetAdapter(); + if (!adapter) + return false; + + return adapter->AddHardwareBreakpoint(address, (DebugBreakpointType)type, size); +} + + +bool BNDebuggerRemoveHardwareBreakpoint(BNDebuggerController* controller, uint64_t address, BNDebugBreakpointType type, size_t size) +{ + DebuggerState* state = controller->object->GetState(); + if (!state) + return false; + + DebugAdapter* adapter = state->GetAdapter(); + if (!adapter) + return false; + + return adapter->RemoveHardwareBreakpoint(address, (DebugBreakpointType)type, size); +} + + uint64_t BNDebuggerRelativeAddressToAbsolute(BNDebuggerController* controller, const char* module, uint64_t offset) { DebuggerState* state = controller->object->GetState(); From b7a45ba7eb94c61a24cd7bfec9f7c0914e3a449f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:45:33 +0000 Subject: [PATCH 4/7] Update documentation with hardware breakpoint API usage Co-authored-by: xusheng6 <94503187+xusheng6@users.noreply.github.com> --- docs/guide/index.md | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/docs/guide/index.md b/docs/guide/index.md index 932ad24b..4c6a4450 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -504,9 +504,44 @@ dbg.execute_backend_command('image list') ### Hardware Breakpoints/Watchpoints -Hardware breakpoints and watchpoints are very useful and we plan to add better support for it soon. It is tracked by -this [issue](https://github.com/Vector35/debugger/issues/53). For now, we can run a backend command directly to set -hardware breakpoints/watchpoints. +Hardware breakpoints and watchpoints are now supported through both the debugger API and direct backend commands. + +#### Using the Debugger API + +Hardware breakpoints can be set using the following methods in Python: + +```python +from debugger import DebuggerController, DebugBreakpointType + +# Get the controller for your binary view +controller = DebuggerController(bv) + +# Set hardware execution breakpoint +controller.add_hardware_breakpoint(0x12345678, DebugBreakpointType.HardwareExecuteBreakpoint) + +# Set hardware read watchpoint (1 byte) +controller.add_hardware_breakpoint(0x12345678, DebugBreakpointType.HardwareReadBreakpoint, 1) + +# Set hardware write watchpoint (4 bytes) +controller.add_hardware_breakpoint(0x12345678, DebugBreakpointType.HardwareWriteBreakpoint, 4) + +# Set hardware access (read/write) watchpoint (8 bytes) +controller.add_hardware_breakpoint(0x12345678, DebugBreakpointType.HardwareAccessBreakpoint, 8) + +# Remove hardware breakpoint +controller.remove_hardware_breakpoint(0x12345678, DebugBreakpointType.HardwareExecuteBreakpoint) +``` + +The supported breakpoint types are: +- `SoftwareBreakpoint`: Regular software breakpoint (default) +- `HardwareExecuteBreakpoint`: Hardware execution breakpoint +- `HardwareReadBreakpoint`: Hardware read watchpoint +- `HardwareWriteBreakpoint`: Hardware write watchpoint +- `HardwareAccessBreakpoint`: Hardware read/write watchpoint + +#### Using Backend Commands + +For cases where you need more control or the API is not available, you can use backend commands directly. #### WinDbg/DbgEng From 7b54f45eea90c9d7c6f0d9435304808450f35e3a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:47:45 +0000 Subject: [PATCH 5/7] Complete hardware breakpoint implementation with examples Co-authored-by: xusheng6 <94503187+xusheng6@users.noreply.github.com> --- core/adapters/esrevenadapter.cpp | 9 ++ core/adapters/gdbadapter.cpp | 9 ++ docs/examples/hardware_breakpoints.py | 184 ++++++++++++++++++++++++++ 3 files changed, 202 insertions(+) create mode 100644 docs/examples/hardware_breakpoints.py diff --git a/core/adapters/esrevenadapter.cpp b/core/adapters/esrevenadapter.cpp index af77d981..39fbe669 100644 --- a/core/adapters/esrevenadapter.cpp +++ b/core/adapters/esrevenadapter.cpp @@ -411,6 +411,15 @@ DebugBreakpoint EsrevenAdapter::AddBreakpoint(const std::uintptr_t address, unsi DebugBreakpoint(address)) != this->m_debugBreakpoints.end()) return {}; + // Handle hardware breakpoint types + if (breakpoint_type != SoftwareBreakpoint) + { + if (AddHardwareBreakpoint(address, (DebugBreakpointType)breakpoint_type)) + return DebugBreakpoint(address, 0, true); // Use 0 as ID for hardware breakpoints for now + else + return DebugBreakpoint{}; + } + /* TODO: replace %d with the actual breakpoint size as it differs per architecture */ size_t kind = 1; if (m_remoteArch == "aarch64") diff --git a/core/adapters/gdbadapter.cpp b/core/adapters/gdbadapter.cpp index e9bdef2c..b0f2670c 100644 --- a/core/adapters/gdbadapter.cpp +++ b/core/adapters/gdbadapter.cpp @@ -409,6 +409,15 @@ DebugBreakpoint GdbAdapter::AddBreakpoint(const std::uintptr_t address, unsigned DebugBreakpoint(address)) != this->m_debugBreakpoints.end()) return {}; + // Handle hardware breakpoint types + if (breakpoint_type != SoftwareBreakpoint) + { + if (AddHardwareBreakpoint(address, (DebugBreakpointType)breakpoint_type)) + return DebugBreakpoint(address, 0, true); // Use 0 as ID for hardware breakpoints for now + else + return DebugBreakpoint{}; + } + /* TODO: replace %d with the actual breakpoint size as it differs per architecture */ size_t kind = 1; if (m_remoteArch == "aarch64") diff --git a/docs/examples/hardware_breakpoints.py b/docs/examples/hardware_breakpoints.py new file mode 100644 index 00000000..09c2148d --- /dev/null +++ b/docs/examples/hardware_breakpoints.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +""" +Example script demonstrating hardware breakpoint usage + +This script shows how to use the new hardware breakpoint functionality +introduced in the debugger. +""" + +try: + from binaryninja import load + from debugger import DebuggerController, DebugBreakpointType +except ImportError: + from binaryninja import load + from binaryninja.debugger import DebuggerController, DebugBreakpointType + + +def hardware_breakpoint_example(binary_path): + """ + Example showing how to use hardware breakpoints + """ + # Load the binary + bv = load(binary_path) + if not bv: + print(f"Failed to load binary: {binary_path}") + return + + # Get the debugger controller + controller = DebuggerController(bv) + + print("Setting up hardware breakpoints...") + + # Example 1: Hardware execution breakpoint at entry point + entry_point = bv.entry_point + print(f"Setting hardware execution breakpoint at entry point: 0x{entry_point:x}") + success = controller.add_hardware_breakpoint( + entry_point, + DebugBreakpointType.HardwareExecuteBreakpoint + ) + if success: + print("✓ Hardware execution breakpoint set successfully") + else: + print("✗ Failed to set hardware execution breakpoint") + + # Example 2: Hardware write watchpoint on a data address + # In a real scenario, you'd find a data address from your binary analysis + data_address = 0x1000 # Example address + print(f"Setting hardware write watchpoint at 0x{data_address:x} (4 bytes)") + success = controller.add_hardware_breakpoint( + data_address, + DebugBreakpointType.HardwareWriteBreakpoint, + 4 # Watch 4 bytes + ) + if success: + print("✓ Hardware write watchpoint set successfully") + else: + print("✗ Failed to set hardware write watchpoint") + + # Example 3: Hardware read watchpoint + print(f"Setting hardware read watchpoint at 0x{data_address + 8:x} (8 bytes)") + success = controller.add_hardware_breakpoint( + data_address + 8, + DebugBreakpointType.HardwareReadBreakpoint, + 8 # Watch 8 bytes + ) + if success: + print("✓ Hardware read watchpoint set successfully") + else: + print("✗ Failed to set hardware read watchpoint") + + # Example 4: Hardware access (read/write) watchpoint + print(f"Setting hardware access watchpoint at 0x{data_address + 16:x} (1 byte)") + success = controller.add_hardware_breakpoint( + data_address + 16, + DebugBreakpointType.HardwareAccessBreakpoint, + 1 # Watch 1 byte + ) + if success: + print("✓ Hardware access watchpoint set successfully") + else: + print("✗ Failed to set hardware access watchpoint") + + print("\nLaunching target...") + stop_reason = controller.launch_and_wait() + print(f"Target stopped with reason: {stop_reason}") + + # Continue execution to test breakpoints + print("Continuing execution...") + stop_reason = controller.go_and_wait() + print(f"Target stopped with reason: {stop_reason}") + + # Clean up - remove hardware breakpoints + print("\nCleaning up hardware breakpoints...") + + controller.remove_hardware_breakpoint( + entry_point, + DebugBreakpointType.HardwareExecuteBreakpoint + ) + + controller.remove_hardware_breakpoint( + data_address, + DebugBreakpointType.HardwareWriteBreakpoint, + 4 + ) + + controller.remove_hardware_breakpoint( + data_address + 8, + DebugBreakpointType.HardwareReadBreakpoint, + 8 + ) + + controller.remove_hardware_breakpoint( + data_address + 16, + DebugBreakpointType.HardwareAccessBreakpoint, + 1 + ) + + print("Hardware breakpoints removed") + + # Quit the debugger + controller.quit_and_wait() + print("Debugging session ended") + + +def backend_command_example(binary_path): + """ + Example showing how to use hardware breakpoints via backend commands + (useful for advanced scenarios or when the API is not sufficient) + """ + bv = load(binary_path) + controller = DebuggerController(bv) + + print("Using backend commands for hardware breakpoints...") + + # Launch the target first + controller.launch_and_wait() + + # For LLDB adapter: + if controller.get_adapter_type() == "LLDB": + print("Using LLDB commands:") + + # Hardware execution breakpoint + result = controller.send_command("breakpoint set --address 0x100000000 -H") + print(f"LLDB hardware execution breakpoint: {result}") + + # Hardware write watchpoint + result = controller.send_command("watchpoint set expression -w write -s 4 -- 0x100001000") + print(f"LLDB hardware write watchpoint: {result}") + + # List breakpoints and watchpoints + result = controller.send_command("breakpoint list") + print(f"LLDB breakpoints: {result}") + + result = controller.send_command("watchpoint list") + print(f"LLDB watchpoints: {result}") + + # For GDB RSP adapter: + elif "GDB" in controller.get_adapter_type(): + print("Using GDB RSP commands:") + + # Hardware execution breakpoint (Z1) + result = controller.send_command("Z1,100000000,1") + print(f"GDB hardware execution breakpoint: {result}") + + # Hardware write watchpoint (Z2) + result = controller.send_command("Z2,100001000,4") + print(f"GDB hardware write watchpoint: {result}") + + controller.quit_and_wait() + + +if __name__ == "__main__": + import sys + + if len(sys.argv) != 2: + print("Usage: python hardware_breakpoints.py ") + sys.exit(1) + + binary_path = sys.argv[1] + + print("=== Hardware Breakpoint API Example ===") + hardware_breakpoint_example(binary_path) + + print("\n=== Backend Command Example ===") + backend_command_example(binary_path) \ No newline at end of file From 7cb6ddde6a368320bf491d3f69c04265a9276eb5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Sep 2025 06:15:18 +0000 Subject: [PATCH 6/7] Add hardware breakpoint support to DbgEng adapter and UI enhancements Co-authored-by: xusheng6 <94503187+xusheng6@users.noreply.github.com> --- api/debuggerapi.h | 1 + core/adapters/dbgengadapter.cpp | 93 +++++++++++++++- core/adapters/dbgengadapter.h | 4 + core/adapters/esrevenadapter.cpp | 4 +- core/adapters/gdbadapter.cpp | 4 +- core/adapters/lldbadapter.cpp | 4 +- core/debugadapter.h | 8 +- ui/breakpointswidget.cpp | 90 ++++++++++++--- ui/breakpointswidget.h | 8 +- ui/hardwarebreakpointdialog.cpp | 186 +++++++++++++++++++++++++++++++ ui/hardwarebreakpointdialog.h | 58 ++++++++++ 11 files changed, 430 insertions(+), 30 deletions(-) create mode 100644 ui/hardwarebreakpointdialog.cpp create mode 100644 ui/hardwarebreakpointdialog.h diff --git a/api/debuggerapi.h b/api/debuggerapi.h index b8a63cff..26b6d1df 100644 --- a/api/debuggerapi.h +++ b/api/debuggerapi.h @@ -359,6 +359,7 @@ namespace BinaryNinjaDebuggerAPI { uint64_t offset; uint64_t address; bool enabled; + DebugBreakpointType type = SoftwareBreakpoint; }; diff --git a/core/adapters/dbgengadapter.cpp b/core/adapters/dbgengadapter.cpp index f58635f9..dd467649 100644 --- a/core/adapters/dbgengadapter.cpp +++ b/core/adapters/dbgengadapter.cpp @@ -996,6 +996,15 @@ bool DbgEngAdapter::ResumeThread(std::uint32_t tid) DebugBreakpoint DbgEngAdapter::AddBreakpoint(const std::uintptr_t address, unsigned long breakpoint_flags) { + // Handle hardware breakpoint types + if (breakpoint_flags != SoftwareBreakpoint) + { + if (AddHardwareBreakpoint(address, (DebugBreakpointType)breakpoint_flags)) + return DebugBreakpoint(address, 0, true, (DebugBreakpointType)breakpoint_flags); + else + return DebugBreakpoint{}; + } + IDebugBreakpoint2* debug_breakpoint {}; /* attempt to read at breakpoint location to confirm its valid */ @@ -1021,7 +1030,7 @@ DebugBreakpoint DbgEngAdapter::AddBreakpoint(const std::uintptr_t address, unsig if (debug_breakpoint->SetFlags(DEBUG_BREAKPOINT_ENABLED | breakpoint_flags) != S_OK) return {}; - const auto new_breakpoint = DebugBreakpoint(address, id, true); + const auto new_breakpoint = DebugBreakpoint(address, id, true, SoftwareBreakpoint); this->m_debug_breakpoints.push_back(new_breakpoint); return new_breakpoint; @@ -1125,6 +1134,88 @@ std::vector DbgEngAdapter::GetBreakpointList() const return {}; } + +bool DbgEngAdapter::AddHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size) +{ + if (!m_dbgengInitialized) + return false; + + std::string command; + switch (type) + { + case HardwareExecuteBreakpoint: + // ba e
: hardware execution breakpoint + command = fmt::format("ba e{} 0x{:x}", size, address); + break; + case HardwareReadBreakpoint: + // ba r
: hardware read breakpoint + command = fmt::format("ba r{} 0x{:x}", size, address); + break; + case HardwareWriteBreakpoint: + // ba w
: hardware write breakpoint + command = fmt::format("ba w{} 0x{:x}", size, address); + break; + case HardwareAccessBreakpoint: + // ba a
: hardware access (read/write) breakpoint + command = fmt::format("ba a{} 0x{:x}", size, address); + break; + default: + return false; + } + + // Execute the command and check if it succeeded + auto result = InvokeBackendCommand(command); + // DbgEng typically returns an empty string or specific success message for successful ba commands + // If the command fails, it usually contains an error message + return result.find("error") == std::string::npos && result.find("Error") == std::string::npos; +} + + +bool DbgEngAdapter::RemoveHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size) +{ + if (!m_dbgengInitialized) + return false; + + // List all breakpoints to find the ID of the hardware breakpoint at this address + auto result = InvokeBackendCommand("bl"); + + // Parse the breakpoint list to find the ID + // DbgEng breakpoint list format is typically: + // 0 e
+ // 1 r
etc. + std::stringstream ss(result); + std::string line; + + while (std::getline(ss, line)) + { + // Look for lines containing our address + if (line.find(fmt::format("{:x}", address)) != std::string::npos) + { + // Extract breakpoint ID (first number in the line) + std::istringstream iss(line); + std::string id_str; + if (iss >> id_str) + { + try + { + int bp_id = std::stoi(id_str); + // Remove the breakpoint using bc (breakpoint clear) command + auto clear_result = InvokeBackendCommand(fmt::format("bc {}", bp_id)); + return clear_result.find("error") == std::string::npos && + clear_result.find("Error") == std::string::npos; + } + catch (...) + { + // Continue searching if this line doesn't contain a valid ID + continue; + } + } + } + } + + return false; +} + void DbgEngAdapter::ApplyBreakpoints() { for (const auto bp : m_pendingBreakpoints) diff --git a/core/adapters/dbgengadapter.h b/core/adapters/dbgengadapter.h index 6e7f1eaf..aa7db2d9 100644 --- a/core/adapters/dbgengadapter.h +++ b/core/adapters/dbgengadapter.h @@ -200,6 +200,10 @@ namespace BinaryNinjaDebugger { std::vector GetBreakpointList() const override; + // Hardware breakpoint and watchpoint support + bool AddHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size = 1) override; + bool RemoveHardwareBreakpoint(uint64_t address, DebugBreakpointType type, size_t size = 1) override; + std::string GetRegisterNameByIndex(std::uint32_t index) const; std::unordered_map ReadAllRegisters() override; DebugRegister ReadRegister(const std::string& reg) override; diff --git a/core/adapters/esrevenadapter.cpp b/core/adapters/esrevenadapter.cpp index 39fbe669..7b3c8faf 100644 --- a/core/adapters/esrevenadapter.cpp +++ b/core/adapters/esrevenadapter.cpp @@ -415,7 +415,7 @@ DebugBreakpoint EsrevenAdapter::AddBreakpoint(const std::uintptr_t address, unsi if (breakpoint_type != SoftwareBreakpoint) { if (AddHardwareBreakpoint(address, (DebugBreakpointType)breakpoint_type)) - return DebugBreakpoint(address, 0, true); // Use 0 as ID for hardware breakpoints for now + return DebugBreakpoint(address, 0, true, (DebugBreakpointType)breakpoint_type); else return DebugBreakpoint{}; } @@ -430,7 +430,7 @@ DebugBreakpoint EsrevenAdapter::AddBreakpoint(const std::uintptr_t address, unsi if (this->m_rspConnector->TransmitAndReceive(RspData("Z0,{:x},{}", address, kind)).AsString() != "OK" ) return DebugBreakpoint{}; - const auto new_breakpoint = DebugBreakpoint(address, this->m_internalBreakpointId++, true); + const auto new_breakpoint = DebugBreakpoint(address, this->m_internalBreakpointId++, true, SoftwareBreakpoint); this->m_debugBreakpoints.push_back(new_breakpoint); return new_breakpoint; diff --git a/core/adapters/gdbadapter.cpp b/core/adapters/gdbadapter.cpp index b0f2670c..89b7b910 100644 --- a/core/adapters/gdbadapter.cpp +++ b/core/adapters/gdbadapter.cpp @@ -413,7 +413,7 @@ DebugBreakpoint GdbAdapter::AddBreakpoint(const std::uintptr_t address, unsigned if (breakpoint_type != SoftwareBreakpoint) { if (AddHardwareBreakpoint(address, (DebugBreakpointType)breakpoint_type)) - return DebugBreakpoint(address, 0, true); // Use 0 as ID for hardware breakpoints for now + return DebugBreakpoint(address, 0, true, (DebugBreakpointType)breakpoint_type); else return DebugBreakpoint{}; } @@ -428,7 +428,7 @@ DebugBreakpoint GdbAdapter::AddBreakpoint(const std::uintptr_t address, unsigned if (this->m_rspConnector->TransmitAndReceive(RspData("Z0,{:x},{}", address, kind)).AsString() != "OK" ) return DebugBreakpoint{}; - const auto new_breakpoint = DebugBreakpoint(address, this->m_internalBreakpointId++, true); + const auto new_breakpoint = DebugBreakpoint(address, this->m_internalBreakpointId++, true, SoftwareBreakpoint); this->m_debugBreakpoints.push_back(new_breakpoint); return new_breakpoint; diff --git a/core/adapters/lldbadapter.cpp b/core/adapters/lldbadapter.cpp index 977b00f7..ecad2cfb 100644 --- a/core/adapters/lldbadapter.cpp +++ b/core/adapters/lldbadapter.cpp @@ -912,7 +912,7 @@ DebugBreakpoint LldbAdapter::AddBreakpoint(const std::uintptr_t address, unsigne if (breakpoint_type == HardwareExecuteBreakpoint) { if (AddHardwareBreakpoint(address, HardwareExecuteBreakpoint)) - return DebugBreakpoint(address, 0, true); // Use 0 as ID for hardware breakpoints for now + return DebugBreakpoint(address, 0, true, HardwareExecuteBreakpoint); else return DebugBreakpoint {}; } @@ -922,7 +922,7 @@ DebugBreakpoint LldbAdapter::AddBreakpoint(const std::uintptr_t address, unsigne if (!bp.IsValid()) return DebugBreakpoint {}; - return DebugBreakpoint(address, bp.GetID(), bp.IsEnabled()); + return DebugBreakpoint(address, bp.GetID(), bp.IsEnabled(), SoftwareBreakpoint); } diff --git a/core/debugadapter.h b/core/debugadapter.h index 3938baa3..e2e38fc5 100644 --- a/core/debugadapter.h +++ b/core/debugadapter.h @@ -118,12 +118,14 @@ namespace BinaryNinjaDebugger { std::uintptr_t m_address {}; unsigned long m_id {}; bool m_is_active {}; + DebugBreakpointType m_type = SoftwareBreakpoint; - DebugBreakpoint(std::uintptr_t address, unsigned long id, bool active) : - m_address(address), m_id(id), m_is_active(active) + DebugBreakpoint(std::uintptr_t address, unsigned long id, bool active, DebugBreakpointType type = SoftwareBreakpoint) : + m_address(address), m_id(id), m_is_active(active), m_type(type) {} - DebugBreakpoint(std::uintptr_t address) : m_address(address) {} + DebugBreakpoint(std::uintptr_t address, DebugBreakpointType type = SoftwareBreakpoint) : + m_address(address), m_type(type) {} DebugBreakpoint() {} diff --git a/ui/breakpointswidget.cpp b/ui/breakpointswidget.cpp index 0ab904dd..b3337dc8 100644 --- a/ui/breakpointswidget.cpp +++ b/ui/breakpointswidget.cpp @@ -17,7 +17,10 @@ limitations under the License. #include #include #include +#include +#include #include "breakpointswidget.h" +#include "hardwarebreakpointdialog.h" #include "ui.h" #include "menus.h" #include "fmt/format.h" @@ -26,11 +29,31 @@ using namespace BinaryNinjaDebuggerAPI; using namespace BinaryNinja; using namespace std; -BreakpointItem::BreakpointItem(bool enabled, const ModuleNameAndOffset location, uint64_t address) : - m_enabled(enabled), m_location(location), m_address(address) +BreakpointItem::BreakpointItem(bool enabled, const ModuleNameAndOffset location, uint64_t address, DebugBreakpointType type) : + m_enabled(enabled), m_location(location), m_address(address), m_type(type) {} +std::string BreakpointItem::typeString() const +{ + switch (m_type) + { + case SoftwareBreakpoint: + return "Software"; + case HardwareExecuteBreakpoint: + return "Hardware Exec"; + case HardwareReadBreakpoint: + return "Hardware Read"; + case HardwareWriteBreakpoint: + return "Hardware Write"; + case HardwareAccessBreakpoint: + return "Hardware Access"; + default: + return "Unknown"; + } +} + + bool BreakpointItem::operator==(const BreakpointItem& other) const { return (m_enabled == other.enabled()) && (m_location == other.location()) && (m_address == other.address()); @@ -132,6 +155,14 @@ QVariant DebugBreakpointsListModel::data(const QModelIndex& index, int role) con return QVariant(text); } + case DebugBreakpointsListModel::TypeColumn: + { + QString text = QString::fromStdString(item->typeString()); + if (role == Qt::SizeHintRole) + return QVariant((qulonglong)text.size()); + + return QVariant(text); + } } return QVariant(); } @@ -153,6 +184,8 @@ QVariant DebugBreakpointsListModel::headerData(int column, Qt::Orientation orien return "Location"; case DebugBreakpointsListModel::AddressColumn: return "Remote Address"; + case DebugBreakpointsListModel::TypeColumn: + return "Type"; } return QVariant(); } @@ -195,6 +228,7 @@ void DebugBreakpointsItemDelegate::paint( // case DebugBreakpointsListModel::EnabledColumn: case DebugBreakpointsListModel::LocationColumn: case DebugBreakpointsListModel::AddressColumn: + case DebugBreakpointsListModel::TypeColumn: { painter->setFont(m_font); painter->setPen(option.palette.color(QPalette::WindowText).rgba()); @@ -340,26 +374,46 @@ void DebugBreakpointsWidget::add() if (!view) return; - uint64_t address = 0; - if (!ViewFrame::getAddressFromInput(frame, view, address, - frame->getCurrentOffset(), "Add Breakpoint", "The address of the breakpoint:", true)) + // Show options for software or hardware breakpoint + QMenu menu(this); + QAction* softwareAction = menu.addAction("Software Breakpoint"); + QAction* hardwareAction = menu.addAction("Hardware Breakpoint..."); + + QAction* chosen = menu.exec(QCursor::pos()); + if (!chosen) return; - bool isAbsoluteAddress = false; - auto controller = DebuggerController::GetController(view); - if (controller->IsConnected()) - isAbsoluteAddress = true; - - if (isAbsoluteAddress) + if (chosen == softwareAction) { - m_controller->AddBreakpoint(address); + // Original software breakpoint logic + uint64_t address = 0; + if (!ViewFrame::getAddressFromInput(frame, view, address, + frame->getCurrentOffset(), "Add Breakpoint", "The address of the breakpoint:", true)) + return; + + bool isAbsoluteAddress = false; + auto controller = DebuggerController::GetController(view); + if (controller->IsConnected()) + isAbsoluteAddress = true; + + if (isAbsoluteAddress) + { + m_controller->AddBreakpoint(address); + } + else + { + std::string filename = m_controller->GetInputFile(); + uint64_t offset = address - m_controller->GetViewFileSegmentsStart(); + ModuleNameAndOffset info = {filename, offset}; + m_controller->AddBreakpoint(info); + } } - else + else if (chosen == hardwareAction) { - std::string filename = m_controller->GetInputFile(); - uint64_t offset = address - m_controller->GetViewFileSegmentsStart(); - ModuleNameAndOffset info = {filename, offset}; - m_controller->AddBreakpoint(info); + // Hardware breakpoint dialog + uint64_t suggestedAddress = frame->getCurrentOffset(); + HardwareBreakpointDialog dialog(this, m_controller, suggestedAddress); + dialog.exec(); } } @@ -392,7 +446,7 @@ void DebugBreakpointsWidget::updateContent() ModuleNameAndOffset info; info.module = bp.module; info.offset = bp.offset; - bps.emplace_back(bp.enabled, info, bp.address); + bps.emplace_back(bp.enabled, info, bp.address, bp.type); } m_model->updateRows(bps); diff --git a/ui/breakpointswidget.h b/ui/breakpointswidget.h index 0c717d2f..33e98fca 100644 --- a/ui/breakpointswidget.h +++ b/ui/breakpointswidget.h @@ -38,12 +38,15 @@ class BreakpointItem bool m_enabled; ModuleNameAndOffset m_location; uint64_t m_address; + DebugBreakpointType m_type; public: - BreakpointItem(bool enabled, const ModuleNameAndOffset location, uint64_t remoteAddress); + BreakpointItem(bool enabled, const ModuleNameAndOffset location, uint64_t remoteAddress, DebugBreakpointType type = SoftwareBreakpoint); bool enabled() const { return m_enabled; } ModuleNameAndOffset location() const { return m_location; } uint64_t address() const { return m_address; } + DebugBreakpointType type() const { return m_type; } + std::string typeString() const; bool operator==(const BreakpointItem& other) const; bool operator!=(const BreakpointItem& other) const; bool operator<(const BreakpointItem& other) const; @@ -67,6 +70,7 @@ class DebugBreakpointsListModel : public QAbstractTableModel //EnabledColumn, LocationColumn, AddressColumn, + TypeColumn, }; DebugBreakpointsListModel(QWidget* parent, ViewFrame* view); @@ -82,7 +86,7 @@ class DebugBreakpointsListModel : public QAbstractTableModel virtual int columnCount(const QModelIndex& parent = QModelIndex()) const override { (void)parent; - return 2; + return 3; } BreakpointItem getRow(int row) const; virtual QVariant data(const QModelIndex& i, int role) const override; diff --git a/ui/hardwarebreakpointdialog.cpp b/ui/hardwarebreakpointdialog.cpp new file mode 100644 index 00000000..b4f3a85a --- /dev/null +++ b/ui/hardwarebreakpointdialog.cpp @@ -0,0 +1,186 @@ +/* +Copyright 2020-2025 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "hardwarebreakpointdialog.h" +#include +#include + +HardwareBreakpointDialog::HardwareBreakpointDialog(QWidget* parent, DbgRef controller, uint64_t suggestedAddress) : + QDialog(parent), m_controller(controller), m_suggestedAddress(suggestedAddress) +{ + setWindowTitle("Add Hardware Breakpoint"); + setModal(true); + resize(400, 200); + + // Create form layout + QFormLayout* formLayout = new QFormLayout(); + + // Address input + m_addressEdit = new QLineEdit(); + if (suggestedAddress != 0) + m_addressEdit->setText(QString("0x%1").arg(suggestedAddress, 0, 16)); + formLayout->addRow("Address:", m_addressEdit); + + // Type selection + m_typeCombo = new QComboBox(); + m_typeCombo->addItem("Hardware Execute", static_cast(HardwareExecuteBreakpoint)); + m_typeCombo->addItem("Hardware Read", static_cast(HardwareReadBreakpoint)); + m_typeCombo->addItem("Hardware Write", static_cast(HardwareWriteBreakpoint)); + m_typeCombo->addItem("Hardware Access (Read/Write)", static_cast(HardwareAccessBreakpoint)); + formLayout->addRow("Type:", m_typeCombo); + + // Size selection (for watchpoints) + m_sizeSpin = new QSpinBox(); + m_sizeSpin->setMinimum(1); + m_sizeSpin->setMaximum(8); + m_sizeSpin->setValue(1); + m_sizeSpin->setSuffix(" byte(s)"); + // Only allow powers of 2 + m_sizeSpin->setSpecialValueText("1 byte"); + formLayout->addRow("Size:", m_sizeSpin); + + // Help label + m_helpLabel = new QLabel(); + m_helpLabel->setWordWrap(true); + m_helpLabel->setStyleSheet("QLabel { color: gray; font-size: 10px; }"); + formLayout->addRow(m_helpLabel); + + // Button box + m_buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + + // Main layout + QVBoxLayout* mainLayout = new QVBoxLayout(); + mainLayout->addLayout(formLayout); + mainLayout->addWidget(m_buttonBox); + setLayout(mainLayout); + + // Connect signals + connect(m_buttonBox, &QDialogButtonBox::accepted, this, &HardwareBreakpointDialog::addBreakpoint); + connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(m_addressEdit, &QLineEdit::textChanged, this, &HardwareBreakpointDialog::validateInput); + connect(m_typeCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &HardwareBreakpointDialog::typeChanged); + + // Initial setup + typeChanged(); + validateInput(); +} + +uint64_t HardwareBreakpointDialog::getAddress() const +{ + QString text = m_addressEdit->text().trimmed(); + if (text.startsWith("0x") || text.startsWith("0X")) + text = text.mid(2); + + bool ok; + uint64_t address = text.toULongLong(&ok, 16); + return ok ? address : 0; +} + +DebugBreakpointType HardwareBreakpointDialog::getType() const +{ + return static_cast(m_typeCombo->currentData().toInt()); +} + +size_t HardwareBreakpointDialog::getSize() const +{ + return static_cast(m_sizeSpin->value()); +} + +void HardwareBreakpointDialog::addBreakpoint() +{ + uint64_t address = getAddress(); + if (address == 0) + { + QMessageBox::warning(this, "Invalid Address", "Please enter a valid hexadecimal address."); + return; + } + + DebugBreakpointType type = getType(); + size_t size = getSize(); + + // Validate size for powers of 2 + if (type != HardwareExecuteBreakpoint && (size & (size - 1)) != 0) + { + QMessageBox::warning(this, "Invalid Size", "Watchpoint size must be a power of 2 (1, 2, 4, or 8 bytes)."); + return; + } + + if (m_controller) + { + bool success = m_controller->AddHardwareBreakpoint(address, type, size); + if (success) + { + accept(); + } + else + { + QMessageBox::warning(this, "Failed to Add Breakpoint", + "Failed to add hardware breakpoint. The target may not support hardware breakpoints or all hardware breakpoint slots may be in use."); + } + } +} + +void HardwareBreakpointDialog::validateInput() +{ + uint64_t address = getAddress(); + bool valid = (address != 0); + + m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(valid); + + if (!valid && !m_addressEdit->text().isEmpty()) + { + m_helpLabel->setText("Please enter a valid hexadecimal address (e.g., 0x401000)"); + m_helpLabel->setStyleSheet("QLabel { color: red; font-size: 10px; }"); + } + else + { + typeChanged(); // Update help text + } +} + +void HardwareBreakpointDialog::typeChanged() +{ + DebugBreakpointType type = getType(); + + // Enable/disable size control based on type + bool needSize = (type != HardwareExecuteBreakpoint); + m_sizeSpin->setEnabled(needSize); + + // Update help text + QString helpText; + switch (type) + { + case HardwareExecuteBreakpoint: + helpText = "Hardware execution breakpoint will trigger when the CPU executes code at the specified address."; + m_sizeSpin->setValue(1); // Execution breakpoints are always 1 byte + break; + case HardwareReadBreakpoint: + helpText = "Hardware read watchpoint will trigger when the CPU reads from the specified memory range."; + break; + case HardwareWriteBreakpoint: + helpText = "Hardware write watchpoint will trigger when the CPU writes to the specified memory range."; + break; + case HardwareAccessBreakpoint: + helpText = "Hardware access watchpoint will trigger when the CPU reads from or writes to the specified memory range."; + break; + default: + helpText = ""; + break; + } + + m_helpLabel->setText(helpText); + m_helpLabel->setStyleSheet("QLabel { color: gray; font-size: 10px; }"); +} \ No newline at end of file diff --git a/ui/hardwarebreakpointdialog.h b/ui/hardwarebreakpointdialog.h new file mode 100644 index 00000000..41458810 --- /dev/null +++ b/ui/hardwarebreakpointdialog.h @@ -0,0 +1,58 @@ +/* +Copyright 2020-2025 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "debuggerapi.h" + +using namespace BinaryNinjaDebuggerAPI; + +class HardwareBreakpointDialog : public QDialog +{ + Q_OBJECT + +private: + DbgRef m_controller; + QLineEdit* m_addressEdit; + QComboBox* m_typeCombo; + QSpinBox* m_sizeSpin; + QLabel* m_helpLabel; + QDialogButtonBox* m_buttonBox; + + uint64_t m_suggestedAddress; + +public: + HardwareBreakpointDialog(QWidget* parent, DbgRef controller, uint64_t suggestedAddress = 0); + + uint64_t getAddress() const; + DebugBreakpointType getType() const; + size_t getSize() const; + +private Q_SLOTS: + void addBreakpoint(); + void validateInput(); + void typeChanged(); +}; \ No newline at end of file From 906391daf0e0f0ac4ea98a2a626995db27f54de3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Sep 2025 06:17:52 +0000 Subject: [PATCH 7/7] Add comprehensive hardware breakpoint support for all debug adapters with UI integration Co-authored-by: xusheng6 <94503187+xusheng6@users.noreply.github.com> --- breakpoint_widget_enhanced.png | Bin 0 -> 37935 bytes hardware_breakpoint_dialog.png | Bin 0 -> 11251 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 breakpoint_widget_enhanced.png create mode 100644 hardware_breakpoint_dialog.png diff --git a/breakpoint_widget_enhanced.png b/breakpoint_widget_enhanced.png new file mode 100644 index 0000000000000000000000000000000000000000..00897fac5a3f36981304aacf19a03befe164b5d3 GIT binary patch literal 37935 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 NOi1cwzM!_({{WHwX`}!E literal 0 HcmV?d00001 diff --git a/hardware_breakpoint_dialog.png b/hardware_breakpoint_dialog.png new file mode 100644 index 0000000000000000000000000000000000000000..89694aad8cbb6331064a224afe9f0a1821dccb35 GIT binary patch literal 11251 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