blob: 18aa72711080e0cd1697a65682d9e7091f71a054 [file] [log] [blame]
// 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 <gtest/gtest.h>
#include "src/developer/debug/debug_agent/breakpoint.h"
#include "src/developer/debug/debug_agent/mock_arch_provider.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 {}
};
class MockWatchpointArchProvider : public MockArchProvider {
public:
struct Installation {
zx_koid_t thread_koid;
AddressRange address_range;
debug_ipc::BreakpointType type;
};
arch::WatchpointInstallationResult InstallWatchpoint(debug_ipc::BreakpointType type,
const zx::thread& thread,
const AddressRange& range) override {
installs_.push_back({thread.get(), range, type});
return arch::WatchpointInstallationResult(ZX_OK, range_to_return_, slot_to_return_);
}
zx_status_t UninstallWatchpoint(const zx::thread& thread, const AddressRange& range) override {
// Uninstalls don't need type.
uninstalls_.push_back({thread.get(), range, debug_ipc::BreakpointType::kLast});
return ZX_OK;
}
const std::vector<Installation>& installs() const { return installs_; }
const std::vector<Installation>& uninstalls() const { return uninstalls_; }
void set_range_to_return(debug_ipc::AddressRange r) { range_to_return_ = r; }
void set_slot_to_return(int slot) { slot_to_return_ = slot; }
private:
std::vector<Installation> installs_;
std::vector<Installation> uninstalls_;
debug_ipc::AddressRange range_to_return_;
int slot_to_return_ = 0;
};
// 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) {
auto arch_provider = std::make_shared<MockWatchpointArchProvider>();
auto object_provider = std::make_shared<ObjectProvider>();
MockProcess process(nullptr, 0x1, "process", arch_provider, object_provider);
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, arch_provider,
kAddressRange);
ASSERT_EQ(watchpoint.breakpoints().size(), 1u);
// Update should install one thread
ASSERT_EQ(watchpoint.Update(), ZX_OK);
ASSERT_TRUE(ContainsKoids(watchpoint, {thread1->koid()}));
ASSERT_EQ(arch_provider->installs().size(), 1u);
EXPECT_EQ(arch_provider->installs()[0].thread_koid, thread1->koid());
EXPECT_EQ(arch_provider->installs()[0].address_range, kAddressRange);
EXPECT_EQ(arch_provider->installs()[0].type, debug_ipc::BreakpointType::kWrite);
EXPECT_EQ(arch_provider->uninstalls().size(), 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, {}));
ASSERT_EQ(arch_provider->installs().size(), 1u);
EXPECT_EQ(arch_provider->uninstalls().size(), 1u);
EXPECT_EQ(arch_provider->uninstalls()[0].thread_koid, thread1->koid());
EXPECT_EQ(arch_provider->uninstalls()[0].address_range, kAddressRange);
// 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()}));
ASSERT_EQ(arch_provider->installs().size(), 2u);
EXPECT_EQ(arch_provider->installs()[1].thread_koid, thread1->koid());
EXPECT_EQ(arch_provider->installs()[1].address_range, kAddressRange);
EXPECT_EQ(arch_provider->installs()[1].type, debug_ipc::BreakpointType::kWrite);
EXPECT_EQ(arch_provider->uninstalls().size(), 1u);
// 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()}));
ASSERT_EQ(arch_provider->installs().size(), 3u);
EXPECT_EQ(arch_provider->installs()[2].thread_koid, thread2->koid());
EXPECT_EQ(arch_provider->installs()[2].address_range, kAddressRange);
EXPECT_EQ(arch_provider->installs()[2].type, debug_ipc::BreakpointType::kWrite);
EXPECT_EQ(arch_provider->uninstalls().size(), 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()}));
ASSERT_EQ(arch_provider->installs().size(), 3u);
EXPECT_EQ(arch_provider->uninstalls().size(), 2u);
EXPECT_EQ(arch_provider->uninstalls()[1].thread_koid, thread1->koid());
EXPECT_EQ(arch_provider->uninstalls()[1].address_range, kAddressRange);
// 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()}));
ASSERT_EQ(arch_provider->installs().size(), 4u);
EXPECT_EQ(arch_provider->installs()[3].thread_koid, thread3->koid());
EXPECT_EQ(arch_provider->installs()[3].address_range, kAddressRange);
EXPECT_EQ(arch_provider->installs()[3].type, debug_ipc::BreakpointType::kWrite);
EXPECT_EQ(arch_provider->uninstalls().size(), 2u);
// 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()}));
ASSERT_EQ(arch_provider->installs().size(), 8u);
EXPECT_EQ(arch_provider->installs()[4].thread_koid, thread1->koid());
EXPECT_EQ(arch_provider->installs()[4].address_range, kAddressRange);
EXPECT_EQ(arch_provider->installs()[4].type, debug_ipc::BreakpointType::kWrite);
EXPECT_EQ(arch_provider->installs()[5].thread_koid, thread4->koid());
EXPECT_EQ(arch_provider->installs()[5].address_range, kAddressRange);
EXPECT_EQ(arch_provider->installs()[5].type, debug_ipc::BreakpointType::kWrite);
EXPECT_EQ(arch_provider->installs()[6].thread_koid, thread5->koid());
EXPECT_EQ(arch_provider->installs()[6].address_range, kAddressRange);
EXPECT_EQ(arch_provider->installs()[6].type, debug_ipc::BreakpointType::kWrite);
EXPECT_EQ(arch_provider->installs()[7].thread_koid, thread6->koid());
EXPECT_EQ(arch_provider->installs()[7].address_range, kAddressRange);
EXPECT_EQ(arch_provider->installs()[7].type, debug_ipc::BreakpointType::kWrite);
EXPECT_EQ(arch_provider->uninstalls().size(), 2u);
// 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()}));
ASSERT_EQ(arch_provider->installs().size(), 8u);
EXPECT_EQ(arch_provider->uninstalls().size(), 2u);
// 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, {}));
ASSERT_EQ(arch_provider->installs().size(), 8u);
EXPECT_EQ(arch_provider->uninstalls().size(), 8u);
EXPECT_EQ(arch_provider->uninstalls()[2].thread_koid, thread1->koid());
EXPECT_EQ(arch_provider->uninstalls()[2].address_range, kAddressRange);
EXPECT_EQ(arch_provider->uninstalls()[3].thread_koid, thread2->koid());
EXPECT_EQ(arch_provider->uninstalls()[3].address_range, kAddressRange);
EXPECT_EQ(arch_provider->uninstalls()[4].thread_koid, thread3->koid());
EXPECT_EQ(arch_provider->uninstalls()[4].address_range, kAddressRange);
EXPECT_EQ(arch_provider->uninstalls()[5].thread_koid, thread4->koid());
EXPECT_EQ(arch_provider->uninstalls()[5].address_range, kAddressRange);
EXPECT_EQ(arch_provider->uninstalls()[6].thread_koid, thread5->koid());
EXPECT_EQ(arch_provider->uninstalls()[6].address_range, kAddressRange);
EXPECT_EQ(arch_provider->uninstalls()[7].thread_koid, thread6->koid());
EXPECT_EQ(arch_provider->uninstalls()[7].address_range, kAddressRange);
}
TEST(Watchpoint, InstalledRanges) {
auto arch_provider = std::make_shared<MockWatchpointArchProvider>();
auto object_provider = std::make_shared<ObjectProvider>();
MockProcess process(nullptr, 0x1, "process", arch_provider, object_provider);
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, arch_provider,
kAddressRange);
ASSERT_EQ(watchpoint.breakpoints().size(), 1u);
const debug_ipc::AddressRange kSubRange = {0x900, 0x2100};
constexpr int kSlot = 1;
arch_provider->set_range_to_return(kSubRange);
arch_provider->set_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.
ASSERT_EQ(arch_provider->installs().size(), 1u);
EXPECT_EQ(arch_provider->installs()[0].thread_koid, thread1->koid());
EXPECT_EQ(arch_provider->installs()[0].address_range, kAddressRange);
EXPECT_EQ(arch_provider->installs()[0].type, debug_ipc::BreakpointType::kWrite);
EXPECT_EQ(arch_provider->uninstalls().size(), 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.status, ZX_OK) << zx_status_get_string(it->second.status);
EXPECT_EQ(it->second.installed_range, kSubRange);
EXPECT_EQ(it->second.slot, kSlot);
}
}
TEST(Watchpoint, MatchesException) {
auto arch_provider = std::make_shared<MockWatchpointArchProvider>();
auto object_provider = std::make_shared<ObjectProvider>();
MockProcess process(nullptr, 0x1, "process", arch_provider, object_provider);
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, arch_provider,
kAddressRange);
ASSERT_EQ(watchpoint.breakpoints().size(), 1u);
const debug_ipc::AddressRange kSubRange = {0x900, 0x2100};
constexpr int kSlot = 1;
arch_provider->set_range_to_return(kSubRange);
arch_provider->set_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.
ASSERT_EQ(arch_provider->installs().size(), 1u);
EXPECT_EQ(arch_provider->installs()[0].thread_koid, thread1->koid());
EXPECT_EQ(arch_provider->installs()[0].address_range, kAddressRange);
EXPECT_EQ(arch_provider->uninstalls().size(), 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.status, ZX_OK) << zx_status_get_string(it->second.status);
EXPECT_EQ(it->second.installed_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) {
auto arch_provider = std::make_shared<MockWatchpointArchProvider>();
auto object_provider = std::make_shared<ObjectProvider>();
MockProcess process(nullptr, 0x1, "process", arch_provider, object_provider);
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, arch_provider,
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.
ASSERT_EQ(arch_provider->installs().size(), 1u);
EXPECT_EQ(arch_provider->installs()[0].thread_koid, thread1->koid());
EXPECT_EQ(arch_provider->installs()[0].address_range, kAddressRange);
EXPECT_EQ(arch_provider->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,
arch_provider, 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.
ASSERT_EQ(arch_provider->installs().size(), 2u);
EXPECT_EQ(arch_provider->installs()[1].thread_koid, thread1->koid());
EXPECT_EQ(arch_provider->installs()[1].address_range, kAddressRange);
EXPECT_EQ(arch_provider->installs()[1].type, debug_ipc::BreakpointType::kReadWrite);
}
}
} // namespace
} // namespace debug_agent