Skip to content

Commit

Permalink
Add AArch64 MASK watchpoint support in debugserver
Browse files Browse the repository at this point in the history
Add suport for MASK style watchpoints on AArch64 in debugserver
on Darwin systems, for watching power-of-2 sized memory ranges.
More work needed in lldb before this can be exposed to the user
(because they will often try watching memory ranges that are not
exactly power-of-2 in size/alignment) but this is the first part
of adding that capability.

Differential Revision: https://reviews.llvm.org/D149792
rdar://108233371
  • Loading branch information
jasonmolenda committed May 4, 2023
1 parent 4fac08f commit 2e16e41
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
C_SOURCES := main.c

include Makefile.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""
Watch larger-than-8-bytes regions of memory, confirm that
writes to those regions are detected.
"""



import lldb
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
from lldbsuite.test import lldbutil

class UnalignedWatchpointTestCase(TestBase):

def continue_and_report_stop_reason(self, process, iter_str):
process.Continue()
self.assertIn(process.GetState(), [lldb.eStateStopped, lldb.eStateExited],
iter_str)
thread = process.GetSelectedThread()
return thread.GetStopReason()


NO_DEBUG_INFO_TESTCASE = True
# debugserver on AArch64 has this feature.
@skipIf(archs=no_match(['arm64', 'arm64e', 'aarch64']))
@skipUnlessDarwin

# debugserver only gained the ability to watch larger regions
# with this patch.
@skipIfOutOfTreeDebugserver

def test_large_watchpoint(self):
"""Test watchpoint that covers a large region of memory."""
self.build()
self.main_source_file = lldb.SBFileSpec("main.c")
(target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(self,
"break here", self.main_source_file)

frame = thread.GetFrameAtIndex(0)

array_addr = frame.GetValueForVariablePath("array").GetValueAsUnsigned()

# watch 256 uint32_t elements in the middle of the array,
# don't assume that the heap allocated array is aligned
# to a 1024 byte boundary to begin with, force alignment.
wa_256_addr = ((array_addr + 1024) & ~(1024-1))
err = lldb.SBError()
wp = target.WatchAddress(wa_256_addr, 1024, False, True, err)
self.assertTrue(wp.IsValid())
self.assertSuccess(err)

c_count = 0
reason = self.continue_and_report_stop_reason(process, "continue #%d" % c_count)
while reason == lldb.eStopReasonWatchpoint:
c_count = c_count + 1
reason = self.continue_and_report_stop_reason(process, "continue #%d" % c_count)
self.assertLessEqual(c_count, 16)

self.assertEqual(c_count, 16)
16 changes: 16 additions & 0 deletions lldb/test/API/functionalities/watchpoint/large-watchpoint/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main() {
const int count = 65535;
int *array = (int*) malloc(sizeof (int) * count);
memset (array, 0, count * sizeof (int));

puts ("break here");

for (int i = 0; i < count - 16; i += 16)
array[i] += 10;

puts ("done, exiting.");
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,15 @@ def test_watch_address_with_invalid_watch_size(self):
"pointee",
value.GetValueAsUnsigned(0),
value.GetType().GetPointeeType())
# Watch for write to *g_char_ptr.
error = lldb.SBError()
watchpoint = target.WatchAddress(
value.GetValueAsUnsigned(), 365, False, True, error)
self.assertFalse(watchpoint)
self.expect(error.GetCString(), exe=False,
substrs=['watch size of %d is not supported' % 365])

# debugserver on Darwin AArch64 systems can watch large regions
# of memory via https://reviews.llvm.org/D149792 , don't run this
# test there.
if self.getArchitecture() not in ['arm64', 'arm64e', 'arm64_32']:
# Watch for write to *g_char_ptr.
error = lldb.SBError()
watchpoint = target.WatchAddress(
value.GetValueAsUnsigned(), 365, False, True, error)
self.assertFalse(watchpoint)
self.expect(error.GetCString(), exe=False,
substrs=['watch size of %d is not supported' % 365])
156 changes: 125 additions & 31 deletions lldb/tools/debugserver/source/MacOSX/arm64/DNBArchImplARM64.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,6 @@
#define WCR_LOAD ((uint32_t)(1u << 3))
#define WCR_STORE ((uint32_t)(1u << 4))

// Enable breakpoint, watchpoint, and vector catch debug exceptions.
// (MDE bit in the MDSCR_EL1 register. Equivalent to the MDBGen bit in
// DBGDSCRext in Aarch32)
#define MDE_ENABLE ((uint32_t)(1u << 15))

// Single instruction step
// (SS bit in the MDSCR_EL1 register)
#define SS_ENABLE ((uint32_t)(1u))
Expand Down Expand Up @@ -807,8 +802,6 @@ uint32_t DNBArchMachARM64::EnableHardwareBreakpoint(nub_addr_t addr,
(uint64_t)m_state.dbg.__bvr[i],
(uint32_t)m_state.dbg.__bcr[i]);

// The kernel will set the MDE_ENABLE bit in the MDSCR_EL1 for us
// automatically, don't need to do it here.
kret = SetDBGState(also_set_on_task);

DNBLogThreadedIf(LOG_WATCHPOINTS,
Expand Down Expand Up @@ -848,9 +841,10 @@ DNBArchMachARM64::AlignRequestedWatchpoint(nub_addr_t requested_addr,

/// Round up \a requested_size to the next power-of-2 size, at least 8
/// bytes
/// requested_size == 3 -> aligned_size == 8
/// requested_size == 13 -> aligned_size == 16
/// requested_size == 16 -> aligned_size == 16
/// requested_size == 8 -> aligned_size == 8
/// requested_size == 9 -> aligned_size == 16
/// requested_size == 15 -> aligned_size == 16
/// requested_size == 192 -> aligned_size == 256
/// Could be `std::bit_ceil(aligned_size)` when we build with C++20?
aligned_size = 1ULL << (addr_bit_size - __builtin_clzll(aligned_size - 1));

Expand Down Expand Up @@ -922,7 +916,7 @@ uint32_t DNBArchMachARM64::EnableHardwareWatchpoint(nub_addr_t addr,
if (wps[0].aligned_size <= 8)
return SetBASWatchpoint(wps[0], read, write, also_set_on_task);
else
return INVALID_NUB_HW_INDEX;
return SetMASKWatchpoint(wps[0], read, write, also_set_on_task);
}

// We have multiple WatchpointSpecs
Expand Down Expand Up @@ -1024,9 +1018,6 @@ uint32_t DNBArchMachARM64::SetBASWatchpoint(DNBArchMachARM64::WatchpointSpec wp,
(uint64_t)m_state.dbg.__wvr[i],
(uint32_t)m_state.dbg.__wcr[i]);

// The kernel will set the MDE_ENABLE bit in the MDSCR_EL1 for us
// automatically, don't need to do it here.

kret = SetDBGState(also_set_on_task);
// DumpDBGState(m_state.dbg);

Expand All @@ -1042,6 +1033,81 @@ uint32_t DNBArchMachARM64::SetBASWatchpoint(DNBArchMachARM64::WatchpointSpec wp,
return INVALID_NUB_HW_INDEX;
}

uint32_t
DNBArchMachARM64::SetMASKWatchpoint(DNBArchMachARM64::WatchpointSpec wp,
bool read, bool write,
bool also_set_on_task) {
const uint32_t num_hw_watchpoints = NumSupportedHardwareWatchpoints();

// Read the debug state
kern_return_t kret = GetDBGState(false);
if (kret != KERN_SUCCESS)
return INVALID_NUB_HW_INDEX;

// Check to make sure we have the needed hardware support
uint32_t i = 0;

for (i = 0; i < num_hw_watchpoints; ++i) {
if ((m_state.dbg.__wcr[i] & WCR_ENABLE) == 0)
break; // We found an available hw watchpoint slot
}
if (i == num_hw_watchpoints) {
DNBLogThreadedIf(LOG_WATCHPOINTS,
"DNBArchMachARM64::"
"SetMASKWatchpoint(): All "
"hardware resources (%u) are in use.",
num_hw_watchpoints);
return INVALID_NUB_HW_INDEX;
}

DNBLogThreadedIf(LOG_WATCHPOINTS,
"DNBArchMachARM64::"
"SetMASKWatchpoint() "
"set hardware register %d to MASK watchpoint "
"aligned start address 0x%llx, aligned size %zu",
i, wp.aligned_start, wp.aligned_size);

// Clear any previous LoHi joined-watchpoint that may have been in use
LoHi[i] = 0;

// MASK field is the number of low bits that are masked off
// when comparing the address with the DBGWVR<n>_EL1 values.
// If aligned size is 16, that means we ignore low 4 bits, 0b1111.
// popcount(16 - 1) give us the correct value of 4.
// 2GB is max watchable region, which is 31 bits (low bits 0x7fffffff
// masked off) -- a MASK value of 31.
const uint64_t mask = __builtin_popcountl(wp.aligned_size - 1) << 24;
// A '0b11111111' BAS value needed for mask watchpoints plus a
// nonzero mask value.
const uint64_t not_bas_wp = 0xff << 5;

m_state.dbg.__wvr[i] = wp.aligned_start;
m_state.dbg.__wcr[i] = mask | not_bas_wp | S_USER | // Stop only in user mode
(read ? WCR_LOAD : 0) | // Stop on read access?
(write ? WCR_STORE : 0) | // Stop on write access?
WCR_ENABLE; // Enable this watchpoint;

DNBLogThreadedIf(LOG_WATCHPOINTS,
"DNBArchMachARM64::SetMASKWatchpoint() "
"adding watchpoint on address 0x%llx with control "
"register value 0x%llx",
(uint64_t)m_state.dbg.__wvr[i],
(uint64_t)m_state.dbg.__wcr[i]);

kret = SetDBGState(also_set_on_task);

DNBLogThreadedIf(LOG_WATCHPOINTS,
"DNBArchMachARM64::"
"SetMASKWatchpoint() "
"SetDBGState() => 0x%8.8x.",
kret);

if (kret == KERN_SUCCESS)
return i;

return INVALID_NUB_HW_INDEX;
}

bool DNBArchMachARM64::ReenableHardwareWatchpoint(uint32_t hw_index) {
// If this logical watchpoint # is actually implemented using
// two hardware watchpoint registers, re-enable both of them.
Expand All @@ -1068,14 +1134,11 @@ bool DNBArchMachARM64::ReenableHardwareWatchpoint_helper(uint32_t hw_index) {

DNBLogThreadedIf(LOG_WATCHPOINTS,
"DNBArchMachARM64::"
"SetBASWatchpoint( %u ) - WVR%u = "
"ReenableHardwareWatchpoint_helper( %u ) - WVR%u = "
"0x%8.8llx WCR%u = 0x%8.8llx",
hw_index, hw_index, (uint64_t)m_state.dbg.__wvr[hw_index],
hw_index, (uint64_t)m_state.dbg.__wcr[hw_index]);

// The kernel will set the MDE_ENABLE bit in the MDSCR_EL1 for us
// automatically, don't need to do it here.

kret = SetDBGState(false);

return (kret == KERN_SUCCESS);
Expand Down Expand Up @@ -1177,30 +1240,61 @@ uint32_t DNBArchMachARM64::GetHardwareWatchpointHit(nub_addr_t &addr) {
uint32_t i, num = NumSupportedHardwareWatchpoints();
for (i = 0; i < num; ++i) {
nub_addr_t wp_addr = GetWatchAddress(debug_state, i);
uint32_t byte_mask = bits(debug_state.__wcr[i], 12, 5);

DNBLogThreadedIf(LOG_WATCHPOINTS,
"DNBArchImplARM64::"
"GetHardwareWatchpointHit() slot: %u "
"(addr = 0x%llx; byte_mask = 0x%x)",
i, static_cast<uint64_t>(wp_addr), byte_mask);
"(addr = 0x%llx, WCR = 0x%llx)",
i, wp_addr, debug_state.__wcr[i]);

if (!IsWatchpointEnabled(debug_state, i))
continue;

if (bits(wp_addr, 48, 3) != bits(addr, 48, 3))
continue;
// DBGWCR<n>EL1.BAS are the bits of the doubleword that are watched
// with a BAS watchpoint.
uint32_t bas_bits = bits(debug_state.__wcr[i], 12, 5);
// DBGWCR<n>EL1.MASK is the number of bits that are masked off the
// virtual address when comparing to DBGWVR<n>_EL1.
uint32_t mask = bits(debug_state.__wcr[i], 28, 24);

// Sanity check the byte_mask
uint32_t lsb = LowestBitSet(byte_mask);
if (lsb < 0)
continue;
const bool is_bas_watchpoint = mask == 0;

uint64_t byte_to_match = bits(addr, 2, 0);
DNBLogThreadedIf(
LOG_WATCHPOINTS,
"DNBArchImplARM64::"
"GetHardwareWatchpointHit() slot: %u %s",
i, is_bas_watchpoint ? "is BAS watchpoint" : "is MASK watchpoint");

if (byte_mask & (1 << byte_to_match)) {
addr = wp_addr + lsb;
return i;
if (is_bas_watchpoint) {
if (bits(wp_addr, 48, 3) != bits(addr, 48, 3))
continue;
} else {
if (bits(wp_addr, 48, mask) == bits(addr, 48, mask)) {
DNBLogThreadedIf(LOG_WATCHPOINTS,
"DNBArchImplARM64::"
"GetHardwareWatchpointHit() slot: %u matched MASK "
"ignoring %u low bits",
i, mask);
return i;
}
}

if (is_bas_watchpoint) {
// Sanity check the bas_bits
uint32_t lsb = LowestBitSet(bas_bits);
if (lsb < 0)
continue;

uint64_t byte_to_match = bits(addr, 2, 0);

if (bas_bits & (1 << byte_to_match)) {
addr = wp_addr + lsb;
DNBLogThreadedIf(LOG_WATCHPOINTS,
"DNBArchImplARM64::"
"GetHardwareWatchpointHit() slot: %u matched BAS",
i);
return i;
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ class DNBArchMachARM64 : public DNBArchProtocol {
bool write, bool also_set_on_task) override;
uint32_t SetBASWatchpoint(WatchpointSpec wp, bool read, bool write,
bool also_set_on_task);
uint32_t SetMASKWatchpoint(WatchpointSpec wp);
uint32_t SetMASKWatchpoint(WatchpointSpec wp, bool read, bool write,
bool also_set_on_task);
bool DisableHardwareWatchpoint(uint32_t hw_break_index,
bool also_set_on_task) override;
bool DisableHardwareWatchpoint_helper(uint32_t hw_break_index,
Expand Down

0 comments on commit 2e16e41

Please sign in to comment.