| // Copyright 2019 The Fuchsia Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. | |
| #include "src/developer/debug/debug_agent/watchpoint.h" | |
| #include <zircon/status.h> | |
| #include <gtest/gtest.h> | |
| #include "src/developer/debug/debug_agent/arch.h" | |
| #include "src/developer/debug/debug_agent/breakpoint.h" | |
| #include "src/developer/debug/debug_agent/mock_process.h" | |
| #include "src/developer/debug/debug_agent/mock_thread.h" | |
| namespace debug_agent { | |
| namespace { | |
| using AddressRange = ::debug_ipc::AddressRange; | |
| class MockProcessDelegate : public Breakpoint::ProcessDelegate { | |
| public: | |
| zx_status_t RegisterBreakpoint(Breakpoint*, zx_koid_t, uint64_t) override { return ZX_OK; } | |
| void UnregisterBreakpoint(Breakpoint* bp, zx_koid_t process_koid, uint64_t address) override {} | |
| zx_status_t RegisterWatchpoint(Breakpoint*, zx_koid_t, const debug_ipc::AddressRange&) override { | |
| return ZX_OK; | |
| } | |
| void UnregisterWatchpoint(Breakpoint*, zx_koid_t, const debug_ipc::AddressRange&) override {} | |
| }; | |
| // If |thread| is null, it means a process-wide breakpoint. | |
| debug_ipc::ProcessBreakpointSettings CreateLocation(const MockProcess& process, | |
| const MockThread* thread, | |
| const debug_ipc::AddressRange& range) { | |
| debug_ipc::ProcessBreakpointSettings location = {}; | |
| location.process_koid = process.koid(); | |
| if (thread) | |
| location.thread_koid = thread->koid(); | |
| location.address = 0; | |
| location.address_range = range; | |
| return location; | |
| } | |
| bool ContainsKoids(const Watchpoint& watchpoint, const std::vector<zx_koid_t>& koids) { | |
| auto& installed_koids = watchpoint.installed_threads(); | |
| if (installed_koids.size() != koids.size()) { | |
| ADD_FAILURE() << "Expected " << koids.size() << " koids, got " << installed_koids.size(); | |
| return false; | |
| } | |
| for (zx_koid_t koid : koids) { | |
| if (installed_koids.count(koid) == 0) { | |
| ADD_FAILURE() << "Expected koid " << koid << " to be present."; | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| // Tests ------------------------------------------------------------------------------------------- | |
| const AddressRange kAddressRange = {0x1000, 0x2000}; | |
| TEST(Watchpoint, SimpleInstallAndRemove) { | |
| MockProcess process(nullptr, 0x1, "process"); | |
| MockThread* thread1 = process.AddThread(0x1001); | |
| MockProcessDelegate process_delegate; | |
| debug_ipc::BreakpointSettings settings; | |
| settings.type = debug_ipc::BreakpointType::kWrite; | |
| settings.locations.push_back(CreateLocation(process, thread1, kAddressRange)); | |
| Breakpoint breakpoint1(&process_delegate); | |
| breakpoint1.SetSettings(settings); | |
| Watchpoint watchpoint(debug_ipc::BreakpointType::kWrite, &breakpoint1, &process, kAddressRange); | |
| ASSERT_EQ(watchpoint.breakpoints().size(), 1u); | |
| // Update should install one thread | |
| ASSERT_EQ(watchpoint.Update(), ZX_OK); | |
| ASSERT_TRUE(ContainsKoids(watchpoint, {thread1->koid()})); | |
| EXPECT_EQ(thread1->mock_thread_handle().WatchpointInstallCount(kAddressRange), 1u); | |
| EXPECT_EQ(thread1->mock_thread_handle().TotalWatchpointUninstallCalls(), 0u); | |
| // Binding again to the same breakpoint should fail. | |
| ASSERT_EQ(watchpoint.RegisterBreakpoint(&breakpoint1), ZX_ERR_ALREADY_BOUND); | |
| ASSERT_EQ(watchpoint.breakpoints().size(), 1u); | |
| // Unregistering the breakpoint should issue an uninstall. As there are no more breakpoints, | |
| // unregistering should return false. | |
| ASSERT_FALSE(watchpoint.UnregisterBreakpoint(&breakpoint1)); | |
| ASSERT_EQ(watchpoint.breakpoints().size(), 0u); | |
| ASSERT_TRUE(ContainsKoids(watchpoint, {})); | |
| EXPECT_EQ(thread1->mock_thread_handle().WatchpointUninstallCount(kAddressRange), 1u); | |
| // Registering again should add the breakpoint again. | |
| ASSERT_EQ(watchpoint.RegisterBreakpoint(&breakpoint1), ZX_OK); | |
| ASSERT_EQ(watchpoint.breakpoints().size(), 1u); | |
| ASSERT_TRUE(ContainsKoids(watchpoint, {thread1->koid()})); | |
| EXPECT_EQ(thread1->mock_thread_handle().WatchpointInstallCount(kAddressRange), 2u); | |
| // Create two other threads. | |
| MockThread* thread2 = process.AddThread(0x1002); | |
| MockThread* thread3 = process.AddThread(0x1003); | |
| // Create a breakpoint that targets the second thread with the correct size. | |
| // Registering the breakpoint should only add one more install. | |
| debug_ipc::BreakpointSettings settings2; | |
| settings2.type = debug_ipc::BreakpointType::kWrite; | |
| settings2.locations.push_back(CreateLocation(process, thread2, kAddressRange)); | |
| debug_ipc::AddressRange address_range2(kAddressRange.begin(), kAddressRange.end() + 8); | |
| settings2.locations.push_back(CreateLocation(process, thread3, address_range2)); | |
| Breakpoint breakpoint2(&process_delegate); | |
| breakpoint2.SetSettings(settings2); | |
| ASSERT_EQ(watchpoint.RegisterBreakpoint(&breakpoint2), ZX_OK); | |
| ASSERT_EQ(watchpoint.breakpoints().size(), 2u); | |
| ASSERT_TRUE(ContainsKoids(watchpoint, {thread1->koid(), thread2->koid()})); | |
| EXPECT_EQ(thread2->mock_thread_handle().WatchpointInstallCount(kAddressRange), 1u); | |
| // Removing the first breakpoint should remove the first install. | |
| // Returning true means that there are more breakpoints registered. | |
| ASSERT_TRUE(watchpoint.UnregisterBreakpoint(&breakpoint1)); | |
| ASSERT_EQ(watchpoint.breakpoints().size(), 1u); | |
| ASSERT_TRUE(ContainsKoids(watchpoint, {thread2->koid()})); | |
| EXPECT_EQ(thread1->mock_thread_handle().WatchpointUninstallCount(kAddressRange), 2u); | |
| // Add a location for another address to the already bound breakpoint. | |
| // Add a location to the already bound breakpoint. | |
| settings2.locations.push_back(CreateLocation(process, thread3, kAddressRange)); | |
| breakpoint2.SetSettings(settings2); | |
| // Updating should've only installed for the third thread. | |
| ASSERT_EQ(watchpoint.Update(), ZX_OK); | |
| ASSERT_TRUE(ContainsKoids(watchpoint, {thread2->koid(), thread3->koid()})); | |
| EXPECT_EQ(thread3->mock_thread_handle().WatchpointInstallCount(kAddressRange), 1u); | |
| // Create moar threads. | |
| MockThread* thread4 = process.AddThread(0x1004); | |
| MockThread* thread5 = process.AddThread(0x1005); | |
| MockThread* thread6 = process.AddThread(0x1006); | |
| // Create a breakpoint that spans all locations. | |
| debug_ipc::BreakpointSettings settings3; | |
| settings3.type = debug_ipc::BreakpointType::kWrite; | |
| settings3.locations.push_back(CreateLocation(process, nullptr, kAddressRange)); | |
| Breakpoint breakpoint3(&process_delegate); | |
| breakpoint3.SetSettings(settings3); | |
| // Registering the breakpoint should add a breakpoint for all threads, but only updating the ones | |
| // that are not currently installed. | |
| ASSERT_EQ(watchpoint.RegisterBreakpoint(&breakpoint3), ZX_OK); | |
| ASSERT_EQ(watchpoint.breakpoints().size(), 2u); | |
| ASSERT_TRUE(ContainsKoids(watchpoint, {thread1->koid(), thread2->koid(), thread3->koid(), | |
| thread4->koid(), thread5->koid(), thread6->koid()})); | |
| EXPECT_EQ(thread4->mock_thread_handle().WatchpointInstallCount(kAddressRange), 1u); | |
| EXPECT_EQ(thread5->mock_thread_handle().WatchpointInstallCount(kAddressRange), 1u); | |
| EXPECT_EQ(thread6->mock_thread_handle().WatchpointInstallCount(kAddressRange), 1u); | |
| // Removing the other breakpoint should not remove installs. | |
| ASSERT_TRUE(watchpoint.UnregisterBreakpoint(&breakpoint2)); | |
| ASSERT_EQ(watchpoint.breakpoints().size(), 1u); | |
| ASSERT_TRUE(ContainsKoids(watchpoint, {thread1->koid(), thread2->koid(), thread3->koid(), | |
| thread4->koid(), thread5->koid(), thread6->koid()})); | |
| EXPECT_EQ(thread1->mock_thread_handle().TotalWatchpointUninstallCalls(), 2u); | |
| EXPECT_EQ(thread3->mock_thread_handle().TotalWatchpointUninstallCalls(), 0u); | |
| EXPECT_EQ(thread4->mock_thread_handle().TotalWatchpointUninstallCalls(), 0u); | |
| EXPECT_EQ(thread5->mock_thread_handle().TotalWatchpointUninstallCalls(), 0u); | |
| EXPECT_EQ(thread6->mock_thread_handle().TotalWatchpointUninstallCalls(), 0u); | |
| // Removing the last breakpoint should remove all the installations. | |
| // Returns false because there are no more registered breakpoints. | |
| ASSERT_FALSE(watchpoint.UnregisterBreakpoint(&breakpoint3)); | |
| ASSERT_EQ(watchpoint.breakpoints().size(), 0u); | |
| ASSERT_TRUE(ContainsKoids(watchpoint, {})); | |
| EXPECT_EQ(thread1->mock_thread_handle().TotalWatchpointUninstallCalls(), 3u); | |
| EXPECT_EQ(thread2->mock_thread_handle().TotalWatchpointUninstallCalls(), 1u); | |
| EXPECT_EQ(thread3->mock_thread_handle().TotalWatchpointUninstallCalls(), 1u); | |
| EXPECT_EQ(thread4->mock_thread_handle().TotalWatchpointUninstallCalls(), 1u); | |
| EXPECT_EQ(thread5->mock_thread_handle().TotalWatchpointUninstallCalls(), 1u); | |
| EXPECT_EQ(thread6->mock_thread_handle().TotalWatchpointUninstallCalls(), 1u); | |
| } | |
| TEST(Watchpoint, InstalledRanges) { | |
| MockProcess process(nullptr, 0x1, "process"); | |
| MockThread* thread1 = process.AddThread(0x1001); | |
| MockProcessDelegate process_delegate; | |
| debug_ipc::BreakpointSettings settings; | |
| settings.type = debug_ipc::BreakpointType::kWrite; | |
| settings.locations.push_back(CreateLocation(process, thread1, kAddressRange)); | |
| Breakpoint breakpoint1(&process_delegate); | |
| breakpoint1.SetSettings(settings); | |
| Watchpoint watchpoint(debug_ipc::BreakpointType::kWrite, &breakpoint1, &process, kAddressRange); | |
| ASSERT_EQ(watchpoint.breakpoints().size(), 1u); | |
| const debug_ipc::AddressRange kSubRange = {0x900, 0x2100}; | |
| constexpr int kSlot = 1; | |
| thread1->mock_thread_handle().set_watchpoint_range_to_return(kSubRange); | |
| thread1->mock_thread_handle().set_watchpoint_slot_to_return(kSlot); | |
| // Update should install one thread | |
| ASSERT_EQ(watchpoint.Update(), ZX_OK); | |
| ASSERT_TRUE(ContainsKoids(watchpoint, {thread1->koid()})); | |
| // There should be an install with a different sub range. | |
| auto installs = thread1->mock_thread_handle().watchpoint_installs(); | |
| ASSERT_EQ(installs.size(), 1u); | |
| EXPECT_EQ(installs[0].address_range, kAddressRange); | |
| EXPECT_EQ(installs[0].type, debug_ipc::BreakpointType::kWrite); | |
| EXPECT_EQ(thread1->mock_thread_handle().TotalWatchpointUninstallCalls(), 0u); | |
| // The installed and actual range should be differnt. | |
| { | |
| auto& installations = watchpoint.installed_threads(); | |
| ASSERT_EQ(installations.size(), 1u); | |
| auto it = installations.find(thread1->koid()); | |
| ASSERT_NE(it, installations.end()); | |
| EXPECT_EQ(it->second.range, kSubRange); | |
| EXPECT_EQ(it->second.slot, kSlot); | |
| } | |
| } | |
| TEST(Watchpoint, MatchesException) { | |
| MockProcess process(nullptr, 0x1, "process"); | |
| MockThread* thread1 = process.AddThread(0x1001); | |
| MockThread* thread2 = process.AddThread(0x1002); | |
| MockProcessDelegate process_delegate; | |
| debug_ipc::BreakpointSettings settings; | |
| settings.type = debug_ipc::BreakpointType::kWrite; | |
| settings.locations.push_back(CreateLocation(process, thread1, kAddressRange)); | |
| Breakpoint breakpoint1(&process_delegate); | |
| breakpoint1.SetSettings(settings); | |
| Watchpoint watchpoint(debug_ipc::BreakpointType::kWrite, &breakpoint1, &process, kAddressRange); | |
| ASSERT_EQ(watchpoint.breakpoints().size(), 1u); | |
| const debug_ipc::AddressRange kSubRange = {0x900, 0x2100}; | |
| constexpr int kSlot = 1; | |
| thread1->mock_thread_handle().set_watchpoint_range_to_return(kSubRange); | |
| thread1->mock_thread_handle().set_watchpoint_slot_to_return(kSlot); | |
| // Update should install one thread | |
| ASSERT_EQ(watchpoint.Update(), ZX_OK); | |
| ASSERT_TRUE(ContainsKoids(watchpoint, {thread1->koid()})); | |
| // There should be an install with a different sub range. | |
| auto installs = thread1->mock_thread_handle().watchpoint_installs(); | |
| ASSERT_EQ(installs.size(), 1u); | |
| EXPECT_EQ(installs[0].address_range, kAddressRange); | |
| EXPECT_EQ(thread1->mock_thread_handle().TotalWatchpointUninstallCalls(), 0u); | |
| // The installed and actual range should be differnt. | |
| { | |
| auto& installations = watchpoint.installed_threads(); | |
| ASSERT_EQ(installations.size(), 1u); | |
| auto it = installations.find(thread1->koid()); | |
| ASSERT_NE(it, installations.end()); | |
| EXPECT_EQ(it->second.range, kSubRange); | |
| EXPECT_EQ(it->second.slot, kSlot); | |
| // Only same slot and within range should work. | |
| EXPECT_FALSE(watchpoint.MatchesException(thread1->koid(), kSubRange.begin() - 1, kSlot)); | |
| EXPECT_TRUE(watchpoint.MatchesException(thread1->koid(), kSubRange.begin(), kSlot)); | |
| EXPECT_TRUE(watchpoint.MatchesException(thread1->koid(), kSubRange.begin() + 1, kSlot)); | |
| EXPECT_TRUE(watchpoint.MatchesException(thread1->koid(), kSubRange.end() - 1, kSlot)); | |
| EXPECT_FALSE(watchpoint.MatchesException(thread1->koid(), kSubRange.end(), kSlot)); | |
| // Different slot should fail. | |
| EXPECT_FALSE(watchpoint.MatchesException(thread1->koid(), kSubRange.begin(), kSlot + 1)); | |
| // Different thread should fail. | |
| EXPECT_FALSE(watchpoint.MatchesException(thread2->koid(), kSubRange.begin(), kSlot)); | |
| } | |
| } | |
| TEST(Watchpoint, DifferentTypes) { | |
| MockProcess process(nullptr, 0x1, "process"); | |
| MockThread* thread1 = process.AddThread(0x1001); | |
| MockProcessDelegate process_delegate; | |
| debug_ipc::BreakpointSettings settings; | |
| settings.locations.push_back(CreateLocation(process, thread1, kAddressRange)); | |
| { | |
| Breakpoint breakpoint1(&process_delegate); | |
| settings.type = debug_ipc::BreakpointType::kWrite; | |
| breakpoint1.SetSettings(settings); | |
| Watchpoint watchpoint(debug_ipc::BreakpointType::kWrite, &breakpoint1, &process, kAddressRange); | |
| ASSERT_EQ(watchpoint.breakpoints().size(), 1u); | |
| ASSERT_EQ(watchpoint.Update(), ZX_OK); | |
| ASSERT_TRUE(ContainsKoids(watchpoint, {thread1->koid()})); | |
| // There should be an install with a different sub range. | |
| auto installs = thread1->mock_thread_handle().watchpoint_installs(); | |
| ASSERT_EQ(installs.size(), 1u); | |
| EXPECT_EQ(installs[0].address_range, kAddressRange); | |
| EXPECT_EQ(installs[0].type, debug_ipc::BreakpointType::kWrite); | |
| } | |
| { | |
| Breakpoint breakpoint1(&process_delegate); | |
| settings.type = debug_ipc::BreakpointType::kReadWrite; | |
| breakpoint1.SetSettings(settings); | |
| Watchpoint watchpoint(debug_ipc::BreakpointType::kReadWrite, &breakpoint1, &process, | |
| kAddressRange); | |
| ASSERT_EQ(watchpoint.breakpoints().size(), 1u); | |
| ASSERT_EQ(watchpoint.Update(), ZX_OK); | |
| ASSERT_TRUE(ContainsKoids(watchpoint, {thread1->koid()})); | |
| // There should be an install with a different sub range. | |
| auto installs = thread1->mock_thread_handle().watchpoint_installs(); | |
| ASSERT_EQ(installs.size(), 2u); | |
| EXPECT_EQ(installs[1].address_range, kAddressRange); | |
| EXPECT_EQ(installs[1].type, debug_ipc::BreakpointType::kReadWrite); | |
| } | |
| } | |
| } // namespace | |
| } // namespace debug_agent |