| // Copyright 2018 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 <gtest/gtest.h> |
| |
| #include "src/developer/debug/debug_agent/arch.h" |
| #include "src/developer/debug/debug_agent/debugged_thread.h" |
| #include "src/developer/debug/debug_agent/hardware_breakpoint.h" |
| #include "src/developer/debug/debug_agent/local_stream_backend.h" |
| #include "src/developer/debug/debug_agent/mock_object_provider.h" |
| #include "src/developer/debug/debug_agent/mock_process.h" |
| #include "src/developer/debug/debug_agent/mock_process_breakpoint.h" |
| #include "src/developer/debug/debug_agent/mock_thread_exception.h" |
| #include "src/developer/debug/debug_agent/mock_thread_handle.h" |
| #include "src/developer/debug/debug_agent/object_provider.h" |
| #include "src/developer/debug/debug_agent/software_breakpoint.h" |
| #include "src/developer/debug/debug_agent/watchpoint.h" |
| |
| namespace debug_agent { |
| |
| namespace { |
| |
| // Dependencies ----------------------------------------------------------------------------------- |
| |
| constexpr uint64_t kWatchpointLength = 8; |
| |
| // TODO(brettw) this should be removed and the necessary mocks provided by the MockThreadHandle. |
| class MockBreakpointArchProvider : public arch::ArchProvider { |
| public: |
| debug_ipc::ExceptionType DecodeExceptionType(const DebuggedThread&, |
| uint32_t exception_type) override { |
| return exception_type_; |
| } |
| |
| bool IsBreakpointInstruction(zx::process& process, uint64_t address) override { |
| for (uint64_t addr : breakpoints_) { |
| if (addr == address) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| void AppendBreakpoint(uint64_t addr) { breakpoints_.push_back(addr); } |
| |
| uint64_t BreakpointInstructionForSoftwareExceptionAddress(uint64_t exception_addr) override { |
| return exception_addr; |
| } |
| |
| std::pair<debug_ipc::AddressRange, int> InstructionForWatchpointHit( |
| const DebugRegisters&) const override { |
| return {{exception_addr_, exception_addr_ + kWatchpointLength}, slot_}; |
| } |
| |
| void set_exception_addr(uint64_t addr) { exception_addr_ = addr; } |
| void set_exception_type(debug_ipc::ExceptionType e) { exception_type_ = e; } |
| void set_slot(int slot) { slot_ = slot; } |
| |
| private: |
| uint64_t exception_addr_ = 0; |
| int slot_ = -1; |
| |
| std::vector<uint64_t> breakpoints_; |
| debug_ipc::ExceptionType exception_type_ = debug_ipc::ExceptionType::kLast; |
| }; |
| |
| class TestProcess : public MockProcess { |
| public: |
| TestProcess(DebugAgent* debug_agent, zx_koid_t koid, std::string name, |
| std::shared_ptr<arch::ArchProvider> arch_provider, |
| std::shared_ptr<ObjectProvider> object_provider) |
| : MockProcess(debug_agent, koid, std::move(name), std::move(arch_provider), |
| std::move(object_provider)) {} |
| |
| SoftwareBreakpoint* FindSoftwareBreakpoint(uint64_t address) const override { |
| auto it = software_breakpoints_.find(address); |
| if (it == software_breakpoints_.end()) |
| return nullptr; |
| return it->second.get(); |
| } |
| |
| HardwareBreakpoint* FindHardwareBreakpoint(uint64_t address) const override { |
| auto it = hardware_breakpoints_.find(address); |
| if (it == hardware_breakpoints_.end()) |
| return nullptr; |
| return it->second.get(); |
| } |
| |
| Watchpoint* FindWatchpoint(const debug_ipc::AddressRange& range) const override { |
| for (auto& [r, watchpoint] : watchpoints_) { |
| if (r.Contains(range)) |
| return watchpoint.get(); |
| } |
| |
| return nullptr; |
| } |
| |
| void AppendSofwareBreakpoint(Breakpoint* breakpoint, uint64_t address) { |
| software_breakpoints_[address] = |
| std::make_unique<MockSoftwareBreakpoint>(breakpoint, this, address); |
| } |
| |
| void AppendHardwareBreakpoint(Breakpoint* breakpoint, uint64_t address, |
| std::shared_ptr<arch::ArchProvider> arch_provider) { |
| hardware_breakpoints_[address] = std::make_unique<MockHardwareBreakpoint>( |
| breakpoint, this, address, std::move(arch_provider)); |
| } |
| |
| void AppendWatchpoint(Breakpoint* breakpoint, debug_ipc::AddressRange range, |
| std::shared_ptr<arch::ArchProvider> arch_provider) { |
| watchpoints_[range] = std::make_unique<Watchpoint>( |
| debug_ipc::BreakpointType::kWrite, breakpoint, this, std::move(arch_provider), range); |
| } |
| |
| private: |
| std::map<uint64_t, std::unique_ptr<MockSoftwareBreakpoint>> software_breakpoints_; |
| std::map<uint64_t, std::unique_ptr<MockHardwareBreakpoint>> hardware_breakpoints_; |
| WatchpointMap watchpoints_; |
| }; |
| |
| class TestStreamBackend : public LocalStreamBackend { |
| public: |
| void HandleNotifyException(debug_ipc::NotifyException exception) override { |
| exceptions_.push_back(std::move(exception)); |
| } |
| |
| const std::vector<debug_ipc::NotifyException>& exceptions() const { return exceptions_; } |
| |
| private: |
| std::vector<debug_ipc::NotifyException> exceptions_; |
| }; |
| |
| 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 {} |
| }; |
| |
| // Helpers ----------------------------------------------------------------------------------------- |
| |
| struct TestContext { |
| std::shared_ptr<MockBreakpointArchProvider> arch_provider; |
| std::shared_ptr<LimboProvider> limbo_provider; |
| std::shared_ptr<MockObjectProvider> object_provider; |
| |
| std::unique_ptr<DebugAgent> debug_agent; |
| std::unique_ptr<TestStreamBackend> backend; |
| }; |
| |
| TestContext CreateTestContext() { |
| TestContext context; |
| |
| // Mock the system. |
| context.arch_provider = std::make_shared<MockBreakpointArchProvider>(); |
| context.limbo_provider = std::make_shared<LimboProvider>(nullptr); |
| context.object_provider = CreateDefaultMockObjectProvider(); |
| |
| // Create the debug agent. |
| SystemProviders providers; |
| providers.arch_provider = context.arch_provider; |
| providers.limbo_provider = context.limbo_provider; |
| providers.object_provider = context.object_provider; |
| context.debug_agent = std::make_unique<DebugAgent>(nullptr, std::move(providers)); |
| |
| // Create the connection to the debug agent. |
| context.backend = std::make_unique<TestStreamBackend>(); |
| context.debug_agent->Connect(&context.backend->stream()); |
| |
| return context; |
| } |
| |
| std::pair<const MockProcessObject*, const MockThreadObject*> GetProcessThread( |
| const MockObjectProvider& object_provider, const std::string& process_name, |
| const std::string& thread_name) { |
| auto* process = object_provider.ProcessByName(process_name); |
| FX_DCHECK(process); |
| auto* thread = process->GetThread(thread_name); |
| FX_DCHECK(thread); |
| |
| return {process, thread}; |
| } |
| |
| // If |thread| is null, it means a process-wide breakpoint. |
| debug_ipc::ProcessBreakpointSettings CreateLocation(zx_koid_t process_koid, zx_koid_t thread_koid, |
| uint64_t address) { |
| debug_ipc::ProcessBreakpointSettings location = {}; |
| location.process_koid = process_koid; |
| location.thread_koid = thread_koid; |
| location.address = address; |
| |
| return location; |
| } |
| |
| // If |thread| is null, it means a process-wide breakpoint. |
| debug_ipc::ProcessBreakpointSettings CreateLocation(zx_koid_t process_koid, zx_koid_t thread_koid, |
| debug_ipc::AddressRange range) { |
| debug_ipc::ProcessBreakpointSettings location = {}; |
| location.process_koid = process_koid; |
| location.thread_koid = thread_koid; |
| location.address_range = range; |
| |
| return location; |
| } |
| |
| // Tests ------------------------------------------------------------------------------------------- |
| |
| TEST(DebuggedThreadBreakpoint, NormalException) { |
| TestContext context = CreateTestContext(); |
| |
| // Create a process from our mocked object hierarchy. |
| auto [proc_object, thread_object] = |
| GetProcessThread(*context.object_provider, "job121-p2", "second-thread"); |
| TestProcess process(context.debug_agent.get(), proc_object->koid, proc_object->name, |
| context.arch_provider, context.object_provider); |
| |
| auto owning_thread_handle = |
| std::make_unique<MockThreadHandle>(process.koid(), thread_object->koid); |
| MockThreadHandle* mock_thread_handle = owning_thread_handle.get(); |
| |
| // Create the thread that will be on an exception. |
| DebuggedThread::CreateInfo create_info = {}; |
| create_info.process = &process; |
| create_info.koid = thread_object->koid; |
| create_info.handle = std::move(owning_thread_handle); |
| create_info.arch_provider = context.arch_provider; |
| create_info.object_provider = context.object_provider; |
| DebuggedThread thread(context.debug_agent.get(), std::move(create_info)); |
| |
| // Set the exception information the arch provider is going to return. |
| constexpr uint64_t kAddress = 0xdeadbeef; |
| context.arch_provider->set_exception_addr(kAddress); |
| context.arch_provider->set_exception_type(debug_ipc::ExceptionType::kPageFault); |
| |
| // The current thread address should agree with the exception. |
| GeneralRegisters regs; |
| regs.set_ip(kAddress); |
| mock_thread_handle->SetGeneralRegisters(regs); |
| mock_thread_handle->set_state( |
| ThreadHandle::State(debug_ipc::ThreadRecord::BlockedReason::kException)); |
| |
| // Trigger the exception. |
| zx_exception_info exception_info = {}; |
| exception_info.pid = proc_object->koid; |
| exception_info.tid = thread_object->koid; |
| exception_info.type = ZX_EXCP_FATAL_PAGE_FAULT; |
| thread.OnException(std::make_unique<MockThreadException>(), exception_info); |
| |
| // We should've received an exception notification. |
| ASSERT_EQ(context.backend->exceptions().size(), 1u); |
| { |
| EXPECT_EQ(context.backend->exceptions()[0].type, debug_ipc::ExceptionType::kPageFault); |
| EXPECT_EQ(context.backend->exceptions()[0].hit_breakpoints.size(), 0u); |
| |
| auto& thread_record = context.backend->exceptions()[0].thread; |
| EXPECT_EQ(thread_record.process_koid, proc_object->koid); |
| EXPECT_EQ(thread_record.thread_koid, thread_object->koid); |
| EXPECT_EQ(thread_record.state, debug_ipc::ThreadRecord::State::kBlocked); |
| EXPECT_EQ(thread_record.blocked_reason, debug_ipc::ThreadRecord::BlockedReason::kException); |
| EXPECT_EQ(thread_record.stack_amount, debug_ipc::ThreadRecord::StackAmount::kMinimal); |
| } |
| } |
| |
| TEST(DebuggedThreadBreakpoint, SWBreakpoint) { |
| TestContext context = CreateTestContext(); |
| |
| // Create a process from our mocked object hierarchy. |
| auto [proc_object, thread_object] = |
| GetProcessThread(*context.object_provider, "job121-p2", "second-thread"); |
| TestProcess process(context.debug_agent.get(), proc_object->koid, proc_object->name, |
| context.arch_provider, context.object_provider); |
| |
| auto owning_thread_handle = |
| std::make_unique<MockThreadHandle>(process.koid(), thread_object->koid); |
| MockThreadHandle* mock_thread_handle = owning_thread_handle.get(); |
| |
| // Create the thread that will be on an exception. |
| DebuggedThread::CreateInfo create_info = {}; |
| create_info.process = &process; |
| create_info.koid = thread_object->koid; |
| create_info.handle = std::move(owning_thread_handle); |
| create_info.arch_provider = context.arch_provider; |
| create_info.object_provider = context.object_provider; |
| DebuggedThread thread(context.debug_agent.get(), std::move(create_info)); |
| |
| // Set the exception information the arch provider is going to return. |
| constexpr uint64_t kAddress = 0xdeadbeef; |
| context.arch_provider->set_exception_addr(kAddress); |
| context.arch_provider->set_exception_type(debug_ipc::ExceptionType::kSoftware); |
| |
| // The current thread address should agree with the exception. |
| GeneralRegisters regs; |
| regs.set_ip(kAddress); |
| mock_thread_handle->SetGeneralRegisters(regs); |
| mock_thread_handle->set_state( |
| ThreadHandle::State(debug_ipc::ThreadRecord::BlockedReason::kException)); |
| |
| // Trigger the exception. |
| zx_exception_info exception_info = {}; |
| exception_info.pid = proc_object->koid; |
| exception_info.tid = thread_object->koid; |
| exception_info.type = ZX_EXCP_SW_BREAKPOINT; |
| thread.OnException(std::make_unique<MockThreadException>(), exception_info); |
| |
| // We should've received an exception notification. |
| ASSERT_EQ(context.backend->exceptions().size(), 1u); |
| { |
| auto& exception = context.backend->exceptions()[0]; |
| |
| EXPECT_EQ(exception.type, debug_ipc::ExceptionType::kSoftware) |
| << debug_ipc::ExceptionTypeToString(exception.type); |
| EXPECT_EQ(exception.hit_breakpoints.size(), 0u); |
| |
| auto& thread_record = exception.thread; |
| EXPECT_EQ(thread_record.process_koid, proc_object->koid); |
| EXPECT_EQ(thread_record.thread_koid, thread_object->koid); |
| EXPECT_EQ(thread_record.state, debug_ipc::ThreadRecord::State::kBlocked); |
| EXPECT_EQ(thread_record.blocked_reason, debug_ipc::ThreadRecord::BlockedReason::kException); |
| EXPECT_EQ(thread_record.stack_amount, debug_ipc::ThreadRecord::StackAmount::kMinimal); |
| } |
| |
| // Add a breakpoint on that address. |
| constexpr uint32_t kBreakpointId = 1000; |
| MockProcessDelegate process_delegate; |
| auto breakpoint = std::make_unique<Breakpoint>(&process_delegate); |
| debug_ipc::BreakpointSettings settings = {}; |
| settings.id = kBreakpointId; |
| settings.type = debug_ipc::BreakpointType::kSoftware; |
| settings.locations.push_back(CreateLocation(proc_object->koid, 0, kAddress)); |
| breakpoint->SetSettings(settings); |
| |
| process.AppendSofwareBreakpoint(breakpoint.get(), kAddress); |
| context.arch_provider->AppendBreakpoint(kAddress); |
| |
| // Throw the same breakpoint exception. |
| thread.OnException(std::make_unique<MockThreadException>(), exception_info); |
| |
| // We should've received an exception notification with hit breakpoints. |
| ASSERT_EQ(context.backend->exceptions().size(), 2u); |
| { |
| auto& exception = context.backend->exceptions()[1]; |
| |
| EXPECT_EQ(exception.type, debug_ipc::ExceptionType::kSoftware) |
| << debug_ipc::ExceptionTypeToString(exception.type); |
| ASSERT_EQ(exception.hit_breakpoints.size(), 1u); |
| EXPECT_EQ(exception.hit_breakpoints[0].id, breakpoint->stats().id); |
| EXPECT_EQ(breakpoint->stats().hit_count, 1u); |
| |
| auto& thread_record = exception.thread; |
| EXPECT_EQ(thread_record.process_koid, proc_object->koid); |
| EXPECT_EQ(thread_record.thread_koid, thread_object->koid); |
| EXPECT_EQ(thread_record.state, debug_ipc::ThreadRecord::State::kBlocked); |
| EXPECT_EQ(thread_record.blocked_reason, debug_ipc::ThreadRecord::BlockedReason::kException); |
| EXPECT_EQ(thread_record.stack_amount, debug_ipc::ThreadRecord::StackAmount::kMinimal); |
| } |
| } |
| |
| TEST(DebuggedThreadBreakpoint, HWBreakpoint) { |
| TestContext context = CreateTestContext(); |
| |
| // Create a process from our mocked object hierarchy. |
| auto [proc_object, thread_object] = |
| GetProcessThread(*context.object_provider, "job121-p2", "second-thread"); |
| TestProcess process(context.debug_agent.get(), proc_object->koid, proc_object->name, |
| context.arch_provider, context.object_provider); |
| |
| auto owning_thread_handle = |
| std::make_unique<MockThreadHandle>(process.koid(), thread_object->koid); |
| MockThreadHandle* mock_thread_handle = owning_thread_handle.get(); |
| |
| // Create the thread that will be on an exception. |
| DebuggedThread::CreateInfo create_info = {}; |
| create_info.process = &process; |
| create_info.koid = thread_object->koid; |
| create_info.handle = std::move(owning_thread_handle); |
| create_info.arch_provider = context.arch_provider; |
| create_info.object_provider = context.object_provider; |
| DebuggedThread thread(context.debug_agent.get(), std::move(create_info)); |
| |
| // Set the exception information the arch provider is going to return. |
| constexpr uint64_t kAddress = 0xdeadbeef; |
| context.arch_provider->set_exception_addr(kAddress); |
| context.arch_provider->set_exception_type(debug_ipc::ExceptionType::kHardware); |
| |
| // The current thread address should agree with the exception. |
| GeneralRegisters regs; |
| regs.set_ip(kAddress); |
| mock_thread_handle->SetGeneralRegisters(regs); |
| mock_thread_handle->set_state( |
| ThreadHandle::State(debug_ipc::ThreadRecord::BlockedReason::kException)); |
| |
| // Add a breakpoint on that address. |
| constexpr uint32_t kBreakpointId = 1000; |
| MockProcessDelegate process_delegate; |
| auto breakpoint = std::make_unique<Breakpoint>(&process_delegate); |
| debug_ipc::BreakpointSettings settings = {}; |
| settings.id = kBreakpointId; |
| settings.type = debug_ipc::BreakpointType::kHardware; |
| settings.locations.push_back(CreateLocation(proc_object->koid, 0, kAddress)); |
| breakpoint->SetSettings(settings); |
| |
| process.AppendHardwareBreakpoint(breakpoint.get(), kAddress, context.arch_provider); |
| |
| // Trigger the exception. |
| zx_exception_info exception_info = {}; |
| exception_info.pid = proc_object->koid; |
| exception_info.tid = thread_object->koid; |
| exception_info.type = ZX_EXCP_HW_BREAKPOINT; |
| thread.OnException(std::make_unique<MockThreadException>(), exception_info); |
| |
| // We should've received an exception notification. |
| ASSERT_EQ(context.backend->exceptions().size(), 1u); |
| { |
| auto& exception = context.backend->exceptions()[0]; |
| |
| EXPECT_EQ(exception.type, debug_ipc::ExceptionType::kHardware) |
| << debug_ipc::ExceptionTypeToString(exception.type); |
| EXPECT_EQ(exception.hit_breakpoints.size(), 1u); |
| EXPECT_EQ(exception.hit_breakpoints[0].id, breakpoint->stats().id); |
| EXPECT_EQ(breakpoint->stats().hit_count, 1u); |
| |
| auto& thread_record = exception.thread; |
| EXPECT_EQ(thread_record.process_koid, proc_object->koid); |
| EXPECT_EQ(thread_record.thread_koid, thread_object->koid); |
| EXPECT_EQ(thread_record.state, debug_ipc::ThreadRecord::State::kBlocked); |
| EXPECT_EQ(thread_record.blocked_reason, debug_ipc::ThreadRecord::BlockedReason::kException); |
| EXPECT_EQ(thread_record.stack_amount, debug_ipc::ThreadRecord::StackAmount::kMinimal); |
| } |
| } |
| |
| TEST(DebuggedThreadBreakpoint, Watchpoint) { |
| TestContext context = CreateTestContext(); |
| |
| // Create a process from our mocked object hierarchy. |
| auto [proc_object, thread_object] = |
| GetProcessThread(*context.object_provider, "job121-p2", "second-thread"); |
| TestProcess process(context.debug_agent.get(), proc_object->koid, proc_object->name, |
| context.arch_provider, context.object_provider); |
| |
| auto owning_thread_handle = |
| std::make_unique<MockThreadHandle>(process.koid(), thread_object->koid); |
| MockThreadHandle* mock_thread_handle = owning_thread_handle.get(); |
| |
| // Create the thread that will be on an exception. |
| DebuggedThread::CreateInfo create_info = {}; |
| create_info.process = &process; |
| create_info.koid = thread_object->koid; |
| create_info.handle = std::move(owning_thread_handle); |
| create_info.arch_provider = context.arch_provider; |
| create_info.object_provider = context.object_provider; |
| DebuggedThread thread(context.debug_agent.get(), std::move(create_info)); |
| |
| // Add a watchpoint. |
| const debug_ipc::AddressRange kRange = {0x1000, 0x1000 + kWatchpointLength}; |
| MockProcessDelegate process_delegate; |
| Breakpoint breakpoint(&process_delegate); |
| |
| constexpr uint32_t kBreakpointId = 1000; |
| debug_ipc::BreakpointSettings settings = {}; |
| settings.id = kBreakpointId; |
| settings.type = debug_ipc::BreakpointType::kWrite; |
| settings.locations.push_back(CreateLocation(proc_object->koid, 0, kRange)); |
| breakpoint.SetSettings(settings); |
| |
| process.AppendWatchpoint(&breakpoint, kRange, context.arch_provider); |
| |
| // Set the exception information the arch provider is going to return. |
| const uint64_t kAddress = kRange.begin(); |
| constexpr uint64_t kSlot = 0; |
| context.arch_provider->set_exception_type(debug_ipc::ExceptionType::kWatchpoint); |
| context.arch_provider->set_exception_addr(kAddress); |
| context.arch_provider->set_slot(kSlot); |
| |
| // The current thread address should agree with the exception. |
| GeneralRegisters regs; |
| regs.set_ip(kAddress); |
| mock_thread_handle->SetGeneralRegisters(regs); |
| mock_thread_handle->set_state( |
| ThreadHandle::State(debug_ipc::ThreadRecord::BlockedReason::kException)); |
| |
| // Trigger the exception. |
| zx_exception_info exception_info = {}; |
| exception_info.pid = proc_object->koid; |
| exception_info.tid = thread_object->koid; |
| exception_info.type = ZX_EXCP_HW_BREAKPOINT; |
| thread.OnException(std::make_unique<MockThreadException>(), exception_info); |
| |
| // We should've received an exception notification. |
| { |
| auto& exception = context.backend->exceptions()[0]; |
| |
| EXPECT_EQ(exception.type, debug_ipc::ExceptionType::kWatchpoint) |
| << debug_ipc::ExceptionTypeToString(exception.type); |
| EXPECT_EQ(exception.hit_breakpoints.size(), 1u); |
| EXPECT_EQ(exception.hit_breakpoints[0].id, breakpoint.stats().id); |
| EXPECT_EQ(breakpoint.stats().hit_count, 1u); |
| |
| auto& thread_record = exception.thread; |
| EXPECT_EQ(thread_record.process_koid, proc_object->koid); |
| EXPECT_EQ(thread_record.thread_koid, thread_object->koid); |
| EXPECT_EQ(thread_record.state, debug_ipc::ThreadRecord::State::kBlocked); |
| EXPECT_EQ(thread_record.blocked_reason, debug_ipc::ThreadRecord::BlockedReason::kException); |
| EXPECT_EQ(thread_record.stack_amount, debug_ipc::ThreadRecord::StackAmount::kMinimal); |
| } |
| } |
| |
| } // namespace |
| } // namespace debug_agent |