diff --git a/lldb/test/API/commands/watchpoints/unaligned-watchpoint/Makefile b/lldb/test/API/commands/watchpoints/unaligned-watchpoint/Makefile new file mode 100644 --- /dev/null +++ b/lldb/test/API/commands/watchpoints/unaligned-watchpoint/Makefile @@ -0,0 +1,3 @@ +C_SOURCES := main.c + +include Makefile.rules diff --git a/lldb/test/API/commands/watchpoints/unaligned-watchpoint/TestUnalignedWatchpoint.py b/lldb/test/API/commands/watchpoints/unaligned-watchpoint/TestUnalignedWatchpoint.py new file mode 100644 --- /dev/null +++ b/lldb/test/API/commands/watchpoints/unaligned-watchpoint/TestUnalignedWatchpoint.py @@ -0,0 +1,55 @@ +""" +Watch one byte in the middle of a doubleword, mutate the +entire doubleword including the watched byte. On AArch64 +the trap address is probably the start of the doubleword, +instead of the address of our watched byte. Test that lldb +correctly associates this watchpoint trap with our watchpoint. +""" + + + +import lldb +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +from lldbsuite.test import lldbutil + + +class UnalignedWatchpointTestCase(TestBase): + NO_DEBUG_INFO_TESTCASE = True + @skipIfOutOfTreeDebugserver + def test_unaligned_watchpoint(self): + """Test an unaligned watchpoint triggered by a larger aligned write.""" + 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) + + self.expect("watchpoint set variable a.buf[2]") + + self.runCmd("process continue") + + # We should be stopped again due to the watchpoint (write type), but + # only once. The stop reason of the thread should be watchpoint. + self.expect("thread list", STOPPED_DUE_TO_WATCHPOINT, + substrs=['stopped', + 'stop reason = watchpoint']) + + # Use the '-v' option to do verbose listing of the watchpoint. + # The hit count should now be 1. + self.expect("watchpoint list -v", + substrs=['hit_count = 1']) + + self.runCmd("process continue") + + # We should be stopped again due to the watchpoint (write type), but + # only once. The stop reason of the thread should be watchpoint. + self.expect("thread list", STOPPED_DUE_TO_WATCHPOINT, + substrs=['stopped', + 'stop reason = watchpoint']) + + # Use the '-v' option to do verbose listing of the watchpoint. + # The hit count should now be 1. + self.expect("watchpoint list -v", + substrs=['hit_count = 2']) diff --git a/lldb/test/API/commands/watchpoints/unaligned-watchpoint/main.c b/lldb/test/API/commands/watchpoints/unaligned-watchpoint/main.c new file mode 100644 --- /dev/null +++ b/lldb/test/API/commands/watchpoints/unaligned-watchpoint/main.c @@ -0,0 +1,13 @@ +#include +#include +int main() { + union { + uint8_t buf[8]; + uint64_t val; + } a; + a.val = 0; // break here + for (int i = 0; i < 5; i++) { + a.val = i; + printf("a.val is %lu\n", a.val); + } +} diff --git a/lldb/tools/debugserver/source/DNBBreakpoint.h b/lldb/tools/debugserver/source/DNBBreakpoint.h --- a/lldb/tools/debugserver/source/DNBBreakpoint.h +++ b/lldb/tools/debugserver/source/DNBBreakpoint.h @@ -122,7 +122,9 @@ DNBBreakpoint *Add(nub_addr_t addr, nub_size_t length, bool hardware); bool Remove(nub_addr_t addr); DNBBreakpoint *FindByAddress(nub_addr_t addr); + const DNBBreakpoint *FindNearestWatchpoint(nub_addr_t addr) const; const DNBBreakpoint *FindByAddress(nub_addr_t addr) const; + const DNBBreakpoint *FindByHardwareIndex(uint32_t idx) const; size_t FindBreakpointsThatOverlapRange(nub_addr_t addr, nub_addr_t size, std::vector &bps); diff --git a/lldb/tools/debugserver/source/DNBBreakpoint.cpp b/lldb/tools/debugserver/source/DNBBreakpoint.cpp --- a/lldb/tools/debugserver/source/DNBBreakpoint.cpp +++ b/lldb/tools/debugserver/source/DNBBreakpoint.cpp @@ -82,6 +82,45 @@ return NULL; } +const DNBBreakpoint * +DNBBreakpointList::FindByHardwareIndex(uint32_t idx) const { + for (const auto &pos : m_breakpoints) + if (pos.second.GetHardwareIndex() == idx) + return &pos.second; + + return nullptr; +} + +const DNBBreakpoint * +DNBBreakpointList::FindNearestWatchpoint(nub_addr_t addr) const { + // Exact match + for (const auto &pos : m_breakpoints) { + if (pos.second.IsEnabled()) { + nub_addr_t start_addr = pos.second.Address(); + nub_addr_t end_addr = start_addr + pos.second.ByteSize(); + if (addr >= start_addr && addr <= end_addr) + return &pos.second; + } + } + + // Find watchpoint nearest to this address + // before or after the watched region of memory + const DNBBreakpoint *closest = nullptr; + uint32_t best_match = UINT32_MAX; + for (const auto &pos : m_breakpoints) { + if (pos.second.IsEnabled()) { + nub_addr_t start_addr = pos.second.Address(); + nub_addr_t end_addr = start_addr + pos.second.ByteSize(); + uint32_t delta = addr < start_addr ? start_addr - addr : addr - end_addr; + if (delta < best_match) { + closest = &pos.second; + best_match = delta; + } + } + } + return closest; +} + // Finds the next breakpoint at an address greater than or equal to "addr" size_t DNBBreakpointList::FindBreakpointsThatOverlapRange( nub_addr_t addr, nub_addr_t size, std::vector &bps) { diff --git a/lldb/tools/debugserver/source/DNBDefs.h b/lldb/tools/debugserver/source/DNBDefs.h --- a/lldb/tools/debugserver/source/DNBDefs.h +++ b/lldb/tools/debugserver/source/DNBDefs.h @@ -234,7 +234,8 @@ eStopTypeInvalid = 0, eStopTypeSignal, eStopTypeException, - eStopTypeExec + eStopTypeExec, + eStopTypeWatchpoint }; enum DNBMemoryPermissions { @@ -264,6 +265,37 @@ nub_size_t data_count; nub_addr_t data[DNB_THREAD_STOP_INFO_MAX_EXC_DATA]; } exception; + + // eStopTypeWatchpoint + struct { + // The trigger address from the mach exception + // (likely the contents of the FAR register) + nub_addr_t mach_exception_addr; + + // The trigger address, adjusted to be the start + // address of one of the existing watchpoints for + // lldb's benefit. + nub_addr_t addr; + + // The watchpoint hardware index. + uint32_t hw_idx; + + // If the esr_fields bitfields have been filled in. + bool esr_fields_set; + struct { + uint32_t + iss; // "ISS encoding for an exception from a Watchpoint exception" + uint32_t wpt; // Watchpoint number + bool wptv; // Watchpoint number Valid + bool wpf; // Watchpoint might be false-positive + bool fnp; // FAR not Precise + bool vncr; // watchpoint from use of VNCR_EL2 reg by EL1 + bool fnv; // FAR not Valid + bool cm; // Cache maintenance + bool wnr; // Write not Read + uint32_t dfsc; // Data Fault Status Code + } esr_fields; + } watchpoint; } details; }; diff --git a/lldb/tools/debugserver/source/MacOSX/MachException.cpp b/lldb/tools/debugserver/source/MacOSX/MachException.cpp --- a/lldb/tools/debugserver/source/MacOSX/MachException.cpp +++ b/lldb/tools/debugserver/source/MacOSX/MachException.cpp @@ -158,6 +158,19 @@ return true; } +#if defined(__arm64__) || defined(__aarch64__) + if (exc_type == EXC_BREAKPOINT && exc_data[0] == EXC_ARM_DA_DEBUG && + exc_data.size() > 1) { + stop_info->reason = eStopTypeWatchpoint; + stop_info->details.watchpoint.mach_exception_addr = exc_data[1]; + stop_info->details.watchpoint.addr = INVALID_NUB_ADDRESS; + if (exc_data.size() > 2) { + stop_info->details.watchpoint.hw_idx = exc_data[2]; + } + return true; + } +#endif + // We always stop with a mach exceptions stop_info->reason = eStopTypeException; // Save the EXC_XXXX exception type diff --git a/lldb/tools/debugserver/source/MacOSX/MachProcess.h b/lldb/tools/debugserver/source/MacOSX/MachProcess.h --- a/lldb/tools/debugserver/source/MacOSX/MachProcess.h +++ b/lldb/tools/debugserver/source/MacOSX/MachProcess.h @@ -383,6 +383,9 @@ void PrivateResume(); void StopProfileThread(); + void RefineWatchpointStopInfo(nub_thread_t tid, + struct DNBThreadStopInfo *stop_info); + uint32_t Flags() const { return m_flags; } nub_state_t DoSIGSTOP(bool clear_bps_and_wps, bool allow_running, uint32_t *thread_idx_ptr); diff --git a/lldb/tools/debugserver/source/MacOSX/MachProcess.mm b/lldb/tools/debugserver/source/MacOSX/MachProcess.mm --- a/lldb/tools/debugserver/source/MacOSX/MachProcess.mm +++ b/lldb/tools/debugserver/source/MacOSX/MachProcess.mm @@ -1277,6 +1277,8 @@ if (m_thread_list.GetThreadStoppedReason(tid, stop_info)) { if (m_did_exec) stop_info->reason = eStopTypeExec; + if (stop_info->reason == eStopTypeWatchpoint) + RefineWatchpointStopInfo(tid, stop_info); return true; } return false; @@ -1420,6 +1422,135 @@ m_profile_events.ResetEvents(eMachProcessProfileCancel); } +/// return 1 if bit position \a bit is set in \a value +static uint32_t bit(uint32_t value, uint32_t bit) { + return (value >> bit) & 1u; +} + +// return the bitfield "value[msbit:lsbit]". +static uint64_t bits(uint64_t value, uint32_t msbit, uint32_t lsbit) { + assert(msbit >= lsbit); + uint64_t shift_left = sizeof(value) * 8 - 1 - msbit; + value <<= + shift_left; // shift anything above the msbit off of the unsigned edge + value >>= shift_left + lsbit; // shift it back again down to the lsbit + // (including undoing any shift from above) + return value; // return our result +} + +void MachProcess::RefineWatchpointStopInfo( + nub_thread_t tid, struct DNBThreadStopInfo *stop_info) { + const DNBBreakpoint *wp = m_watchpoints.FindNearestWatchpoint( + stop_info->details.watchpoint.mach_exception_addr); + if (wp) { + stop_info->details.watchpoint.addr = wp->Address(); + stop_info->details.watchpoint.hw_idx = wp->GetHardwareIndex(); + DNBLogThreadedIf(LOG_WATCHPOINTS, + "MachProcess::RefineWatchpointStopInfo " + "mach exception addr 0x%llx moved in to nearest " + "watchpoint, 0x%llx-0x%llx", + stop_info->details.watchpoint.mach_exception_addr, + wp->Address(), wp->Address() + wp->ByteSize() - 1); + } else { + stop_info->details.watchpoint.addr = + stop_info->details.watchpoint.mach_exception_addr; + } + + stop_info->details.watchpoint.esr_fields_set = false; + std::optional esr, far; + nub_size_t num_reg_sets = 0; + const DNBRegisterSetInfo *reg_sets = GetRegisterSetInfo(tid, &num_reg_sets); + for (nub_size_t set = 0; set < num_reg_sets; set++) { + if (reg_sets[set].registers == NULL) + continue; + for (uint32_t reg = 0; reg < reg_sets[set].num_registers; ++reg) { + if (strcmp(reg_sets[set].registers[reg].name, "esr") == 0) { + DNBRegisterValue reg_value; + if (GetRegisterValue(tid, set, reg, ®_value)) { + esr = reg_value.value.uint64; + } + } + if (strcmp(reg_sets[set].registers[reg].name, "far") == 0) { + DNBRegisterValue reg_value; + if (GetRegisterValue(tid, set, reg, ®_value)) { + far = reg_value.value.uint64; + } + } + } + } + + if (esr && far) { + if (*far != stop_info->details.watchpoint.mach_exception_addr) { + // AFAIK the kernel is going to put the FAR value in the mach + // exception, if they don't match, it's interesting enough to log it. + DNBLogThreadedIf(LOG_WATCHPOINTS, + "MachProcess::RefineWatchpointStopInfo mach exception " + "addr 0x%llx but FAR register has value 0x%llx", + stop_info->details.watchpoint.mach_exception_addr, *far); + } + uint32_t exception_class = bits(*esr, 31, 26); + + // "Watchpoint exception from a lower Exception level" + if (exception_class == 0b110100) { + stop_info->details.watchpoint.esr_fields_set = true; + // Documented in the ARM ARM A-Profile Dec 2022 edition + // Section D17.2 ("General system control registers"), + // Section D17.2.37 "ESR_EL1, Exception Syndrome Register (EL1)", + // "Field Descriptions" + // "ISS encoding for an exception from a Watchpoint exception" + uint32_t iss = bits(*esr, 23, 0); + stop_info->details.watchpoint.esr_fields.iss = iss; + stop_info->details.watchpoint.esr_fields.wpt = + bits(iss, 23, 18); // Watchpoint number + stop_info->details.watchpoint.esr_fields.wptv = + bit(iss, 17); // Watchpoint number Valid + stop_info->details.watchpoint.esr_fields.wpf = + bit(iss, 16); // Watchpoint might be false-positive + stop_info->details.watchpoint.esr_fields.fnp = + bit(iss, 15); // FAR not Precise + stop_info->details.watchpoint.esr_fields.vncr = + bit(iss, 13); // watchpoint from use of VNCR_EL2 reg by EL1 + stop_info->details.watchpoint.esr_fields.fnv = + bit(iss, 10); // FAR not Valid + stop_info->details.watchpoint.esr_fields.cm = + bit(iss, 6); // Cache maintenance + stop_info->details.watchpoint.esr_fields.wnr = + bit(iss, 6); // Write not Read + stop_info->details.watchpoint.esr_fields.dfsc = + bits(iss, 5, 0); // Data Fault Status Code + + DNBLogThreadedIf(LOG_WATCHPOINTS, + "ESR watchpoint fields parsed: " + "iss = 0x%x, wpt = %u, wptv = %d, wpf = %d, fnp = %d, " + "vncr = %d, fnv = %d, cm = %d, wnr = %d, dfsc = 0x%x", + stop_info->details.watchpoint.esr_fields.iss, + stop_info->details.watchpoint.esr_fields.wpt, + stop_info->details.watchpoint.esr_fields.wptv, + stop_info->details.watchpoint.esr_fields.wpf, + stop_info->details.watchpoint.esr_fields.fnp, + stop_info->details.watchpoint.esr_fields.vncr, + stop_info->details.watchpoint.esr_fields.fnv, + stop_info->details.watchpoint.esr_fields.cm, + stop_info->details.watchpoint.esr_fields.wnr, + stop_info->details.watchpoint.esr_fields.dfsc); + + if (stop_info->details.watchpoint.esr_fields.wptv) { + DNBLogThreadedIf(LOG_WATCHPOINTS, + "Watchpoint Valid field true, " + "finding startaddr of watchpoint %d", + stop_info->details.watchpoint.esr_fields.wpt); + stop_info->details.watchpoint.hw_idx = + stop_info->details.watchpoint.esr_fields.wpt; + const DNBBreakpoint *wp = m_watchpoints.FindByHardwareIndex( + stop_info->details.watchpoint.esr_fields.wpt); + if (wp) { + stop_info->details.watchpoint.addr = wp->Address(); + } + } + } + } +} + bool MachProcess::StartProfileThread() { DNBLogThreadedIf(LOG_PROCESS, "MachProcess::%s ( )", __FUNCTION__); // Create the thread that profiles the inferior and reports back if enabled diff --git a/lldb/tools/debugserver/source/RNBRemote.cpp b/lldb/tools/debugserver/source/RNBRemote.cpp --- a/lldb/tools/debugserver/source/RNBRemote.cpp +++ b/lldb/tools/debugserver/source/RNBRemote.cpp @@ -2841,11 +2841,21 @@ InitializeRegisters(); if (g_reg_entries != NULL) { + auto interesting_regset = [](int regset) -> bool { +#if defined(__arm64__) || defined(__aarch64__) + // GPRs and exception registers, helpful for debugging + // from packet logs. + return regset == 1 || regset == 3; +#else + return regset == 1; +#endif + }; + DNBRegisterValue reg_value; for (uint32_t reg = 0; reg < g_num_reg_entries; reg++) { // Expedite all registers in the first register set that aren't // contained in other registers - if (g_reg_entries[reg].nub_info.set == 1 && + if (interesting_regset(g_reg_entries[reg].nub_info.set) && g_reg_entries[reg].nub_info.value_regs == NULL) { if (!DNBThreadGetRegisterValueByID( pid, tid, g_reg_entries[reg].nub_info.set, @@ -2860,6 +2870,51 @@ if (did_exec) { ostrm << "reason:exec;"; + } else if (tid_stop_info.reason == eStopTypeWatchpoint) { + ostrm << "reason:watchpoint;"; + ostrm << "description:"; + std::ostringstream wp_desc; + wp_desc << tid_stop_info.details.watchpoint.addr << " "; + wp_desc << tid_stop_info.details.watchpoint.hw_idx << " "; + wp_desc << tid_stop_info.details.watchpoint.mach_exception_addr; + append_hexified_string(ostrm, wp_desc.str()); + ostrm << ";"; + + // Temporarily, print all of the fields we've parsed out of the ESR + // on a watchpoint exception. Normally this is something we would + // log for LOG_WATCHPOINTS only, but this was implemented from the + // ARM ARM spec and hasn't been exercised on real hardware that can + // set most of these fields yet. It may need to be debugged in the + // future, so include all of these purely for debugging by reading + // the packet logs; lldb isn't using these fields. + ostrm << "watch_addr:" << std::hex + << tid_stop_info.details.watchpoint.addr << ";"; + ostrm << "me_watch_addr:" << std::hex + << tid_stop_info.details.watchpoint.mach_exception_addr << ";"; + ostrm << "wp_hw_idx:" << std::hex + << tid_stop_info.details.watchpoint.hw_idx << ";"; + if (tid_stop_info.details.watchpoint.esr_fields_set) { + ostrm << "wp_esr_iss:" << std::hex + << tid_stop_info.details.watchpoint.esr_fields.iss << ";"; + ostrm << "wp_esr_wpt:" << std::hex + << tid_stop_info.details.watchpoint.esr_fields.wpt << ";"; + ostrm << "wp_esr_wptv:" + << tid_stop_info.details.watchpoint.esr_fields.wptv << ";"; + ostrm << "wp_esr_wpf:" + << tid_stop_info.details.watchpoint.esr_fields.wpf << ";"; + ostrm << "wp_esr_fnp:" + << tid_stop_info.details.watchpoint.esr_fields.fnp << ";"; + ostrm << "wp_esr_vncr:" + << tid_stop_info.details.watchpoint.esr_fields.vncr << ";"; + ostrm << "wp_esr_fnv:" + << tid_stop_info.details.watchpoint.esr_fields.fnv << ";"; + ostrm << "wp_esr_cm:" << tid_stop_info.details.watchpoint.esr_fields.cm + << ";"; + ostrm << "wp_esr_wnr:" + << tid_stop_info.details.watchpoint.esr_fields.wnr << ";"; + ostrm << "wp_esr_dfsc:" << std::hex + << tid_stop_info.details.watchpoint.esr_fields.dfsc << ";"; + } } else if (tid_stop_info.details.exception.type) { ostrm << "metype:" << std::hex << tid_stop_info.details.exception.type << ';'; @@ -5475,6 +5530,20 @@ } break; + case eStopTypeWatchpoint: { + reason_value = "watchpoint"; + thread_dict_sp->AddIntegerItem("watchpoint", + tid_stop_info.details.watchpoint.addr); + thread_dict_sp->AddIntegerItem( + "me_watch_addr", + tid_stop_info.details.watchpoint.mach_exception_addr); + std::ostringstream wp_desc; + wp_desc << tid_stop_info.details.watchpoint.addr << " "; + wp_desc << tid_stop_info.details.watchpoint.hw_idx << " "; + wp_desc << tid_stop_info.details.watchpoint.mach_exception_addr; + thread_dict_sp->AddStringItem("description", wp_desc.str()); + } break; + case eStopTypeExec: reason_value = "exec"; break;