| // 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 <inttypes.h> |
| |
| #include <zircon/status.h> |
| #include <zircon/syscalls/exception.h> |
| #include <zircon/syscalls/exception.h> |
| |
| #include "garnet/bin/debug_agent/breakpoint.h" |
| #include "garnet/bin/debug_agent/debugged_thread.h" |
| #include "garnet/bin/debug_agent/process_breakpoint.h" |
| #include "lib/fxl/logging.h" |
| #include "lib/fxl/strings/string_printf.h" |
| |
| namespace debug_agent { |
| |
| // Low-level Declarations ------------------------------------------------------ |
| // Implementations are at the end of the file. |
| |
| class ProcessBreakpoint::SoftwareBreakpoint { |
| public: |
| SoftwareBreakpoint(ProcessBreakpoint*, ProcessMemoryAccessor*); |
| ~SoftwareBreakpoint(); |
| |
| zx_status_t Install(); |
| void Uninstall(); |
| |
| void FixupMemoryBlock(debug_ipc::MemoryBlock* block); |
| |
| private: |
| ProcessBreakpoint* process_bp_; // Not-owning. |
| ProcessMemoryAccessor* memory_accessor_; // Not-owning. |
| |
| // Set to true when the instruction has been replaced. |
| bool installed_ = false; |
| |
| // Previous memory contents before being replaced with the break instruction. |
| arch::BreakInstructionType previous_data_ = 0; |
| }; |
| |
| class ProcessBreakpoint::HardwareBreakpoint { |
| public: |
| HardwareBreakpoint(ProcessBreakpoint*); |
| ~HardwareBreakpoint(); |
| |
| // Checks if any of the installations need to be added/removed. |
| zx_status_t Update(const std::set<zx_koid_t>& thread_koids); |
| // Uninstall all the threads. |
| zx_status_t Uninstall(); |
| |
| const std::set<zx_koid_t>& installed_threads() const { |
| return installed_threads_; |
| } |
| |
| private: |
| // Install/Uninstall a particular thread. |
| zx_status_t Install(zx_koid_t thread_koid); |
| zx_status_t Uninstall(zx_koid_t thread_koid); |
| |
| ProcessBreakpoint* process_bp_; // Not-owning. |
| std::set<zx_koid_t> installed_threads_; |
| }; |
| |
| namespace { |
| |
| // A given set of breakpoints have a number of locations, which could target |
| // different threads. We need to get all the threads that are targeted to |
| // this particular location. |
| std::set<zx_koid_t> HWThreadsTargeted(const ProcessBreakpoint& pb) { |
| std::set<zx_koid_t> ids; |
| bool all_threads = false; |
| for (Breakpoint* bp : pb.breakpoints()) { |
| // We only care about hardware breakpoints. |
| if (bp->settings().type != debug_ipc::BreakpointType::kHardware) |
| continue; |
| |
| for (auto& location : bp->settings().locations) { |
| // We only install for locations that match this process breakpoint. |
| if (location.address != pb.address()) |
| continue; |
| |
| auto thread_id = location.thread_koid; |
| if (thread_id == 0) { |
| all_threads = true; |
| break; |
| } else { |
| ids.insert(thread_id); |
| } |
| } |
| |
| // No need to continue searching if a breakpoint wants all threads. |
| if (all_threads) |
| break; |
| } |
| |
| // If all threads are required, add them all. |
| if (all_threads) { |
| for (DebuggedThread* thread : pb.process()->GetThreads()) |
| ids.insert(thread->koid()); |
| } |
| |
| return ids; |
| } |
| |
| } // namespace |
| |
| // ProcessBreakpoint Implementation -------------------------------------------- |
| |
| ProcessBreakpoint::ProcessBreakpoint(Breakpoint* breakpoint, |
| DebuggedProcess* process, |
| ProcessMemoryAccessor* memory_accessor, |
| uint64_t address) |
| : process_(process), memory_accessor_(memory_accessor), address_(address) { |
| breakpoints_.push_back(breakpoint); |
| } |
| |
| ProcessBreakpoint::~ProcessBreakpoint() { Uninstall(); } |
| |
| zx_status_t ProcessBreakpoint::Init() { return Update(); } |
| |
| zx_status_t ProcessBreakpoint::RegisterBreakpoint(Breakpoint* breakpoint) { |
| // Shouldn't get duplicates. |
| FXL_DCHECK(std::find(breakpoints_.begin(), breakpoints_.end(), breakpoint) == |
| breakpoints_.end()); |
| breakpoints_.push_back(breakpoint); |
| // Check if we need to install/uninstall a breakpoint. |
| return Update(); |
| } |
| |
| bool ProcessBreakpoint::UnregisterBreakpoint(Breakpoint* breakpoint) { |
| auto found = std::find(breakpoints_.begin(), breakpoints_.end(), breakpoint); |
| if (found == breakpoints_.end()) { |
| FXL_NOTREACHED(); // Should always be found. |
| } else { |
| breakpoints_.erase(found); |
| } |
| // Check if we need to install/uninstall a breakpoint. |
| Update(); |
| return !breakpoints_.empty(); |
| } |
| |
| void ProcessBreakpoint::FixupMemoryBlock(debug_ipc::MemoryBlock* block) { |
| if (software_breakpoint_) |
| software_breakpoint_->FixupMemoryBlock(block); |
| } |
| |
| void ProcessBreakpoint::OnHit( |
| debug_ipc::BreakpointType exception_type, |
| std::vector<debug_ipc::BreakpointStats>* hit_breakpoints) { |
| hit_breakpoints->clear(); |
| for (Breakpoint* breakpoint : breakpoints_) { |
| // Only care for breakpoints that match the exception type. |
| if (breakpoint->settings().type != exception_type) |
| continue; |
| |
| breakpoint->OnHit(); |
| hit_breakpoints->push_back(breakpoint->stats()); |
| } |
| } |
| |
| void ProcessBreakpoint::BeginStepOver(zx_koid_t thread_koid) { |
| // Shouldn't be recursively stepping over a breakpoint from the same thread. |
| FXL_DCHECK(thread_step_over_.find(thread_koid) == thread_step_over_.end()); |
| |
| if (!CurrentlySteppingOver()) { |
| // This is the first thread to attempt to step over the breakpoint (there |
| // could theoretically be more than one). |
| Uninstall(); |
| } |
| thread_step_over_[thread_koid] = StepStatus::kCurrent; |
| } |
| |
| bool ProcessBreakpoint::BreakpointStepHasException( |
| zx_koid_t thread_koid, debug_ipc::NotifyException::Type exception_type) { |
| auto found_thread = thread_step_over_.find(thread_koid); |
| if (found_thread == thread_step_over_.end()) { |
| // Shouldn't be getting these notifications from a thread not currently |
| // doing a step-over. |
| FXL_NOTREACHED(); |
| return false; |
| } |
| StepStatus step_status = found_thread->second; |
| thread_step_over_.erase(found_thread); |
| |
| // When the last thread is done stepping over, put the breakpoint back. |
| if (step_status == StepStatus::kCurrent && !CurrentlySteppingOver()) |
| Update(); |
| |
| // Now check if this exception was likely caused by successfully stepping |
| // over the breakpoint, or something else (the stepped |
| // instruction crashed or something). |
| // TODO(donosoc): Handle HW breakpoint case. |
| return exception_type == debug_ipc::NotifyException::Type::kSingleStep; |
| } |
| |
| bool ProcessBreakpoint::CurrentlySteppingOver() const { |
| for (const auto& pair : thread_step_over_) { |
| if (pair.second == StepStatus::kCurrent) |
| return true; |
| } |
| return false; |
| } |
| |
| zx_status_t ProcessBreakpoint::Update() { |
| // Software breakpoints remain installed as long as even one remains active, |
| // regardless of which threads are targeted. |
| int sw_bp_count = 0; |
| for (Breakpoint* bp : breakpoints_) { |
| if (bp->settings().type == debug_ipc::BreakpointType::kSoftware) |
| sw_bp_count++; |
| } |
| |
| if (sw_bp_count == 0 && software_breakpoint_) { |
| software_breakpoint_.reset(); |
| } else if (sw_bp_count > 0 && !software_breakpoint_) { |
| software_breakpoint_ = |
| std::make_unique<SoftwareBreakpoint>(this, memory_accessor_); |
| zx_status_t status = software_breakpoint_->Install(); |
| if (status != ZX_OK) |
| return status; |
| } |
| |
| // Hardware breakpoints are different. We need to remove for all the threads |
| // that are not covered anymore. |
| std::set<zx_koid_t> threads = HWThreadsTargeted(*this); |
| |
| if (threads.empty()) { |
| hardware_breakpoint_.reset(); |
| } else { |
| if (!hardware_breakpoint_) { |
| hardware_breakpoint_ = std::make_unique<HardwareBreakpoint>(this); |
| } |
| return hardware_breakpoint_->Update(threads); |
| } |
| |
| return ZX_OK; |
| } |
| |
| void ProcessBreakpoint::Uninstall() { |
| software_breakpoint_.reset(); |
| hardware_breakpoint_.reset(); |
| } |
| |
| // ProcessBreakpoint::SoftwareBreakpoint Implementation ------------------------ |
| |
| ProcessBreakpoint::SoftwareBreakpoint::SoftwareBreakpoint( |
| ProcessBreakpoint* process_bp, ProcessMemoryAccessor* memory_accessor) |
| : process_bp_(process_bp), memory_accessor_(memory_accessor) {} |
| |
| ProcessBreakpoint::SoftwareBreakpoint::~SoftwareBreakpoint() { Uninstall(); } |
| |
| zx_status_t ProcessBreakpoint::SoftwareBreakpoint::Install() { |
| FXL_DCHECK(!installed_); |
| |
| uint64_t address = process_bp_->address(); |
| |
| // Read previous instruction contents. |
| size_t actual = 0; |
| zx_status_t status = memory_accessor_->ReadProcessMemory( |
| address, &previous_data_, sizeof(arch::BreakInstructionType), &actual); |
| if (status != ZX_OK) |
| return status; |
| if (actual != sizeof(arch::BreakInstructionType)) |
| return ZX_ERR_UNAVAILABLE; |
| |
| // Replace with breakpoint instruction. |
| status = memory_accessor_->WriteProcessMemory( |
| address, &arch::kBreakInstruction, sizeof(arch::BreakInstructionType), |
| &actual); |
| if (status != ZX_OK) |
| return status; |
| if (actual != sizeof(arch::BreakInstructionType)) |
| return ZX_ERR_UNAVAILABLE; |
| |
| installed_ = true; |
| return ZX_OK; |
| } |
| |
| void ProcessBreakpoint::SoftwareBreakpoint::Uninstall() { |
| if (!installed_) |
| return; // Not installed. |
| |
| uint64_t address = process_bp_->address(); |
| |
| // If the breakpoint was previously installed it means the memory address |
| // was valid and writable, so we generally expect to be able to do the same |
| // write to uninstall it. But it could have been unmapped during execution |
| // or even remapped with something else. So verify that it's still a |
| // breakpoint instruction before doing any writes. |
| arch::BreakInstructionType current_contents = 0; |
| size_t actual = 0; |
| zx_status_t status = memory_accessor_->ReadProcessMemory( |
| address, ¤t_contents, sizeof(arch::BreakInstructionType), &actual); |
| if (status != ZX_OK || actual != sizeof(arch::BreakInstructionType)) |
| return; // Probably unmapped, safe to ignore. |
| |
| if (current_contents != arch::kBreakInstruction) { |
| fprintf(stderr, |
| "Warning: Debug break instruction unexpectedly replaced " |
| "at %" PRIX64 "\n", |
| address); |
| return; // Replaced with something else, ignore. |
| } |
| |
| status = memory_accessor_->WriteProcessMemory( |
| address, &previous_data_, sizeof(arch::BreakInstructionType), &actual); |
| if (status != ZX_OK || actual != sizeof(arch::BreakInstructionType)) { |
| fprintf(stderr, "Warning: unable to remove breakpoint at %" PRIX64 ".", |
| address); |
| } |
| |
| installed_ = false; |
| } |
| |
| void ProcessBreakpoint::SoftwareBreakpoint::FixupMemoryBlock( |
| debug_ipc::MemoryBlock* block) { |
| if (block->data.empty()) |
| return; // Nothing to do. |
| FXL_DCHECK(static_cast<size_t>(block->size) == block->data.size()); |
| |
| size_t src_size = sizeof(arch::BreakInstructionType); |
| const uint8_t* src = reinterpret_cast<uint8_t*>(&previous_data_); |
| |
| // Simple implementation to prevent boundary errors (ARM instructions are |
| // 32-bits and could be hanging partially off either end of the requested |
| // buffer). |
| for (size_t i = 0; i < src_size; i++) { |
| uint64_t dest_address = process_bp_->address() + i; |
| if (dest_address >= block->address && |
| dest_address < block->address + block->size) |
| block->data[dest_address - block->address] = src[i]; |
| } |
| } |
| |
| bool ProcessBreakpoint::SoftwareBreakpointInstalled() const { |
| return software_breakpoint_ != nullptr; |
| } |
| |
| bool ProcessBreakpoint::HardwareBreakpointInstalled() const { |
| return hardware_breakpoint_ != nullptr && |
| !hardware_breakpoint_->installed_threads().empty(); |
| } |
| |
| // ProcessBreakpoint::HardwareBreakpoint Implementation ------------------------ |
| |
| ProcessBreakpoint::HardwareBreakpoint::HardwareBreakpoint( |
| ProcessBreakpoint* process_bp) |
| : process_bp_(process_bp) {} |
| |
| ProcessBreakpoint::HardwareBreakpoint::~HardwareBreakpoint() { |
| Uninstall(); |
| } |
| |
| zx_status_t ProcessBreakpoint::HardwareBreakpoint::Update( |
| const std::set<zx_koid_t>& thread_ids) { |
| // We get a snapshot of which threads are already installed. |
| auto current_threads = installed_threads_; |
| |
| // Uninstall pass. |
| for (zx_koid_t thread_id : current_threads) { |
| // The ProcessBreakpoint not longer tracks this. Remove. |
| if (thread_ids.count(thread_id) == 0) { |
| zx_status_t res = Uninstall(thread_id); |
| if (res != ZX_OK) |
| return res; |
| installed_threads_.erase(thread_id); |
| } |
| } |
| |
| // Install pass. |
| for (zx_koid_t thread_id : thread_ids) { |
| // If it's already installed, ignore. |
| if (installed_threads_.count(thread_id) > 0) |
| continue; |
| |
| // If it's new, install. |
| if (installed_threads_.count(thread_id) == 0) { |
| zx_status_t res = Install(thread_id); |
| if (res != ZX_OK) |
| return res; |
| installed_threads_.insert(thread_id); |
| } |
| } |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t ProcessBreakpoint::HardwareBreakpoint::Install( |
| zx_koid_t thread_id) { |
| uint64_t address = process_bp_->address(); |
| // We need to install this new thread. |
| DebuggedThread* thread = process_bp_->process()->GetThread(thread_id); |
| if (!thread) { |
| FXL_LOG(WARNING) << fxl::StringPrintf( |
| "Warning: installing HW breakpoint for nonexistent thread %u at " |
| "%" PRIX64 "\n", |
| static_cast<uint32_t>(thread_id), address); |
| return ZX_OK; |
| } |
| |
| zx_status_t res = |
| arch::ArchProvider::Get().InstallHWBreakpoint(&thread->thread(), address); |
| // TODO: Do we want to remove all other locations when one fails? |
| if (res != ZX_OK) { |
| FXL_LOG(WARNING) << fxl::StringPrintf( |
| "Warning: Error installing HW breakpoint for thread %u at " |
| "%" PRIX64 ": %s\n", |
| static_cast<uint32_t>(thread_id), address, zx_status_get_string(res)); |
| return res; |
| } |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t ProcessBreakpoint::HardwareBreakpoint::Uninstall() { |
| for (zx_koid_t thread_id : installed_threads_) { |
| zx_status_t res = Uninstall(thread_id); |
| if (res != ZX_OK) |
| return res; |
| } |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t ProcessBreakpoint::HardwareBreakpoint::Uninstall( |
| zx_koid_t thread_id) { |
| uint64_t address = process_bp_->address(); |
| DebuggedThread* thread = process_bp_->process()->GetThread(thread_id); |
| if (!thread) { |
| FXL_LOG(WARNING) << fxl::StringPrintf( |
| "Warning: removed HW breakpoint for nonexistent thread %u at %" PRIX64 |
| "\n", |
| static_cast<uint32_t>(thread_id), address); |
| // TODO: What to do in this case? |
| return ZX_OK; |
| } |
| |
| zx_status_t res = arch::ArchProvider::Get().UninstallHWBreakpoint( |
| &thread->thread(), address); |
| if (res != ZX_OK) { |
| FXL_LOG(WARNING) << fxl::StringPrintf( |
| "Warning: Error removing HW breakpoint for thread %u at %" PRIX64 |
| ": %s\n", |
| static_cast<uint32_t>(thread_id), address, zx_status_get_string(res)); |
| return res; |
| } |
| |
| return ZX_OK; |
| } |
| |
| } // namespace debug_agent |