| // 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 "src/developer/debug/debug_agent/debugged_process.h" | |
| #include <inttypes.h> | |
| #include <lib/fit/defer.h> | |
| #include <lib/syslog/cpp/macros.h> | |
| #include <zircon/syscalls/exception.h> | |
| #include <utility> | |
| #include "src/developer/debug/debug_agent/align.h" | |
| #include "src/developer/debug/debug_agent/breakpoint.h" | |
| #include "src/developer/debug/debug_agent/debug_agent.h" | |
| #include "src/developer/debug/debug_agent/debugged_thread.h" | |
| #include "src/developer/debug/debug_agent/elf_utils.h" | |
| #include "src/developer/debug/debug_agent/exception_handle.h" | |
| #include "src/developer/debug/debug_agent/hardware_breakpoint.h" | |
| #include "src/developer/debug/debug_agent/process_breakpoint.h" | |
| #include "src/developer/debug/debug_agent/software_breakpoint.h" | |
| #include "src/developer/debug/debug_agent/thread_handle.h" | |
| #include "src/developer/debug/debug_agent/time.h" | |
| #include "src/developer/debug/debug_agent/watchpoint.h" | |
| #include "src/developer/debug/ipc/protocol.h" | |
| #include "src/developer/debug/ipc/records.h" | |
| #include "src/developer/debug/shared/logging/logging.h" | |
| #include "src/developer/debug/shared/platform_message_loop.h" | |
| #include "src/developer/debug/shared/zx_status.h" | |
| #include "src/lib/fxl/strings/string_printf.h" | |
| #if defined(__linux__) | |
| #include "src/developer/debug/debug_agent/linux_utils.h" | |
| #endif | |
| namespace debug_agent { | |
| namespace { | |
| std::vector<char> ReadSocketInput(BufferedStdioHandle* buffer) { | |
| FX_DCHECK(buffer->IsValid()); | |
| constexpr size_t kReadSize = 1024; // Read in 1K chunks. | |
| std::vector<char> data; | |
| auto& stream = buffer->stream(); | |
| while (true) { | |
| char buf[kReadSize]; | |
| size_t read_amount = stream.Read(buf, kReadSize); | |
| data.insert(data.end(), buf, buf + read_amount); | |
| if (read_amount < kReadSize) | |
| break; | |
| } | |
| return data; | |
| } | |
| // Meant to be used in debug logging. | |
| std::string LogPreamble(const DebuggedProcess* process) { | |
| return fxl::StringPrintf("[P: %lu (%s)] ", process->koid(), | |
| process->process_handle().GetName().c_str()); | |
| } | |
| void LogRegisterBreakpoint(debug::FileLineFunction location, DebuggedProcess* process, | |
| Breakpoint* bp, uint64_t address) { | |
| if (!debug::IsDebugLoggingActive()) | |
| return; | |
| std::stringstream ss; | |
| ss << LogPreamble(process) << "Setting breakpoint " << bp->settings().id << " (" | |
| << bp->settings().name << ") on 0x" << std::hex << address; | |
| if (bp->settings().one_shot) | |
| ss << " (one shot)"; | |
| DEBUG_LOG_WITH_LOCATION(Process, location) << ss.str(); | |
| } | |
| } // namespace | |
| // DebuggedProcessCreateInfo ----------------------------------------------------------------------- | |
| DebuggedProcessCreateInfo::DebuggedProcessCreateInfo(std::unique_ptr<ProcessHandle> handle) | |
| : handle(std::move(handle)) {} | |
| // DebuggedProcess --------------------------------------------------------------------------------- | |
| DebuggedProcess::DebuggedProcess(DebugAgent* debug_agent) : debug_agent_(debug_agent) {} | |
| DebuggedProcess::~DebuggedProcess() { | |
| if (process_handle_) { | |
| DetachFromProcess(); | |
| } | |
| } | |
| void DebuggedProcess::DetachFromProcess() { | |
| DEBUG_LOG(Process) << LogPreamble(this) << "DetachFromProcess"; | |
| // 1. Remove installed software breakpoints. We need to tell each thread that this will happen. | |
| for (auto& [address, breakpoint] : software_breakpoints_) { | |
| for (auto& [thread_koid, thread] : threads_) { | |
| thread->WillDeleteProcessBreakpoint(breakpoint.get()); | |
| } | |
| } | |
| for (auto& [address, breakpoint] : hardware_breakpoints_) { | |
| for (auto& thread_koid : breakpoint->installed_threads()) { | |
| auto thread = GetThread(thread_koid); | |
| if (!thread) { | |
| // Might have raced with thread destruction. | |
| continue; | |
| } | |
| thread->WillDeleteProcessBreakpoint(breakpoint.get()); | |
| } | |
| } | |
| // Clear the resources. | |
| software_breakpoints_.clear(); | |
| hardware_breakpoints_.clear(); | |
| watchpoints_.clear(); | |
| // 2. Resume threads. Technically a 0'ed request would work, but being explicit is future-proof. | |
| debug_ipc::ResumeRequest resume_request = {}; | |
| resume_request.how = debug_ipc::ResumeRequest::How::kResolveAndContinue; | |
| resume_request.ids.push_back({.process = koid(), .thread = 0}); | |
| OnResume(resume_request); | |
| // 3. Unbind from notifications (this will detach from the process). | |
| process_handle_->Detach(); | |
| } | |
| debug::Status DebuggedProcess::Init(DebuggedProcessCreateInfo create_info) { | |
| ProcessHandle::AttachConfig config; | |
| // Any attach priority above kMinimal means we try to take the exception channel. | |
| config.claim_exception_channel = | |
| create_info.priority > debug_ipc::AttachConfig::Priority::kMinimal; | |
| if (debug::Status status = create_info.handle->Attach(this, config); status.has_error()) | |
| return status; | |
| process_handle_ = std::move(create_info.handle); | |
| from_limbo_ = create_info.from_limbo; | |
| is_weakly_attached_ = create_info.priority == debug_ipc::AttachConfig::Priority::kWeak; | |
| if (create_info.stdio.out.is_valid()) | |
| SetStdout(std::move(create_info.stdio.out)); | |
| if (create_info.stdio.err.is_valid()) | |
| SetStderr(std::move(create_info.stdio.err)); | |
| // Update module list. | |
| module_list_.Update(process_handle()); | |
| #if defined(__linux__) | |
| dl_debug_addr_ = linux::GetLdSoDebugAddress(koid()); | |
| if (dl_debug_addr_) { | |
| DEBUG_LOG(Process) << "Got ld.so debug address = " << std::hex << dl_debug_addr_; | |
| if (uint64_t bp_addr = linux::GetLdSoBreakpointAddress(koid())) { | |
| DEBUG_LOG(Process) << "Got ld.so breakpoint address = " << std::hex << bp_addr; | |
| loader_breakpoint_ = std::make_unique<Breakpoint>(debug_agent_, true); | |
| if (auto err = loader_breakpoint_->SetSettings("Internal shared library load breakpoint", | |
| koid(), bp_addr); | |
| err.has_error()) { | |
| DEBUG_LOG(Process) << LogPreamble(this) | |
| << "Could not set shared library load breakpoint: " << err.message(); | |
| // Continue even in the error case: we can continue with most things working even if the | |
| // loader breakpoint fails for some reason. | |
| } | |
| } else { | |
| DEBUG_LOG(Process) << LogPreamble(this) | |
| << "Could not set shared library load breakpoint address."; | |
| } | |
| } else { | |
| DEBUG_LOG(Process) << LogPreamble(this) << "Can't get ld.so debug address."; | |
| } | |
| // Linux doesn't give thread notifications for the initial thread, so grab it now. | |
| PopulateCurrentThreads(); | |
| #endif | |
| return debug::Status(); | |
| } | |
| debug::Status DebuggedProcess::AttachNow() { | |
| // This object should have already been initialized. | |
| FX_DCHECK(process_handle_); | |
| return process_handle_->Attach(this, ProcessHandle::AttachConfig()); | |
| } | |
| bool DebuggedProcess::IsAttached() const { | |
| // This object should have already been initialized. | |
| FX_DCHECK(process_handle_); | |
| return process_handle_->IsAttached(); | |
| } | |
| void DebuggedProcess::OnResume(const debug_ipc::ResumeRequest& request) { | |
| if (request.ids.empty()) { | |
| // Empty thread ID list means resume all threads. | |
| suspend_new_threads_ = false; | |
| for (auto& [thread_koid, thread] : threads_) | |
| thread->ClientResume(request); | |
| } else { | |
| for (const debug_ipc::ProcessThreadId& id : request.ids) { | |
| if (id.process != koid()) { | |
| // The request may contain resume requests for more than one process. | |
| continue; | |
| } | |
| suspend_new_threads_ = false; | |
| if (!id.thread) { | |
| // A 0 thread koid will resume all threads of the given process. | |
| for (auto& [thread_koid, thread] : threads_) { | |
| thread->ClientResume(request); | |
| } | |
| } else if (DebuggedThread* thread = GetThread(id.thread)) { | |
| // Might be not found if there is a race between the thread exiting and the client sending | |
| // the request. | |
| thread->ClientResume(request); | |
| } | |
| } | |
| } | |
| } | |
| void DebuggedProcess::OnReadMemory(const debug_ipc::ReadMemoryRequest& request, | |
| debug_ipc::ReadMemoryReply* reply) { | |
| reply->blocks = process_handle_->ReadMemoryBlocks(request.address, request.size); | |
| // Remove any breakpoint instructions we've inserted. | |
| // | |
| // If there are a lot of ProcessBreakpoints this will get slow. If we find we have 100's of | |
| // breakpoints an auxiliary data structure could be added to find overlapping breakpoints faster. | |
| for (const auto& [addr, bp] : software_breakpoints_) { | |
| // Generally there will be only one block. If we start reading many megabytes that cross | |
| // mapped memory boundaries, a top-level range check would be a good idea to avoid unnecessary | |
| // iteration. | |
| for (auto& block : reply->blocks) { | |
| bp->FixupMemoryBlock(&block); | |
| } | |
| } | |
| } | |
| void DebuggedProcess::OnKill(const debug_ipc::KillRequest& request, debug_ipc::KillReply* reply) { | |
| // Stop observing before killing the process to avoid getting exceptions after we stopped | |
| // listening to them. | |
| process_handle_->Detach(); | |
| // Since we're being killed, we treat this process as not having any more | |
| // threads. This makes cleanup code more straightforward, as there are no | |
| // threads to resume/handle. | |
| threads_.clear(); | |
| reply->status = process_handle_->Kill(); | |
| } | |
| void DebuggedProcess::OnSaveMinidump(const debug_ipc::SaveMinidumpRequest& request, | |
| debug_ipc::SaveMinidumpReply* reply) { | |
| reply->status = process_handle_->SaveMinidump(GetThreads(), &reply->core_data); | |
| } | |
| DebuggedThread* DebuggedProcess::GetThread(zx_koid_t thread_koid) const { | |
| auto found_thread = threads_.find(thread_koid); | |
| if (found_thread == threads_.end()) | |
| return nullptr; | |
| return found_thread->second.get(); | |
| } | |
| std::vector<DebuggedThread*> DebuggedProcess::GetThreads() const { | |
| std::vector<DebuggedThread*> threads; | |
| threads.reserve(threads_.size()); | |
| for (auto& kv : threads_) | |
| threads.emplace_back(kv.second.get()); | |
| return threads; | |
| } | |
| void DebuggedProcess::PopulateCurrentThreads() { | |
| for (auto& thread : process_handle_->GetChildThreads()) { | |
| // We should never populate the same thread twice. | |
| zx_koid_t thread_koid = thread->GetKoid(); | |
| if (threads_.find(thread_koid) != threads_.end()) | |
| continue; | |
| auto new_thread = std::make_unique<DebuggedThread>(debug_agent_, this, std::move(thread)); | |
| DebuggedThread* new_thread_ptr = new_thread.get(); // Save for notification. | |
| threads_.emplace(thread_koid, std::move(new_thread)); | |
| // Notify the client. | |
| new_thread_ptr->SendThreadNotification(); | |
| } | |
| } | |
| std::vector<debug_ipc::ThreadRecord> DebuggedProcess::GetThreadRecords() const { | |
| std::vector<debug_ipc::ThreadRecord> result; | |
| for (const auto& pair : threads_) | |
| result.push_back(pair.second->GetThreadRecord(debug_ipc::ThreadRecord::StackAmount::kMinimal)); | |
| return result; | |
| } | |
| bool DebuggedProcess::HandleLoaderBreakpoint(uint64_t address) { | |
| // The loader breakpoint is a hardcodeed breakpoint with a known address. | |
| if (address != process_handle().GetLoaderBreakpointAddress()) | |
| return false; | |
| if (module_list_.Update(process_handle())) { | |
| if (!is_weakly_attached_) { | |
| DEBUG_LOG(Process) << "Got loader breakpoint with new module, suspending and sending list."; | |
| SuspendAndSendModules(); | |
| } | |
| } else { | |
| DEBUG_LOG(Process) << "Got loader breakpoint but no module list change, continuing."; | |
| } | |
| return true; | |
| } | |
| void DebuggedProcess::SuspendAndSendModules() { | |
| // Suspend all threads while the module list is being sent. The client will resume the threads | |
| // once it's loaded symbols and processed breakpoints (this may take a while and we'd like to | |
| // get any breakpoints as early as possible). | |
| ClientSuspendAllThreads(); | |
| // Notify the client of any libraries. | |
| debug_ipc::NotifyModules notify; | |
| notify.process_koid = koid(); | |
| notify.modules = module_list_.modules(); | |
| notify.timestamp = GetNowTimestamp(); | |
| DEBUG_LOG(Process) << LogPreamble(this) << "Sending " << notify.modules.size() << " modules."; | |
| debug_agent_->SendNotification(notify); | |
| } | |
| SoftwareBreakpoint* DebuggedProcess::FindSoftwareBreakpoint(uint64_t address) const { | |
| auto it = software_breakpoints_.find(address); | |
| if (it == software_breakpoints_.end()) | |
| return nullptr; | |
| return it->second.get(); | |
| } | |
| HardwareBreakpoint* DebuggedProcess::FindHardwareBreakpoint(uint64_t address) const { | |
| auto it = hardware_breakpoints_.find(address); | |
| if (it == hardware_breakpoints_.end()) | |
| return nullptr; | |
| return it->second.get(); | |
| } | |
| Watchpoint* DebuggedProcess::FindWatchpoint(const debug::AddressRange& range) const { | |
| auto it = watchpoints_.lower_bound(range); | |
| if (it == watchpoints_.end()) | |
| return nullptr; | |
| for (; it != watchpoints_.end(); it++) { | |
| if (it->first.Contains(range)) | |
| return it->second.get(); | |
| } | |
| return nullptr; | |
| } | |
| debug::Status DebuggedProcess::RegisterBreakpoint(Breakpoint* bp, uint64_t address) { | |
| LogRegisterBreakpoint(FROM_HERE, this, bp, address); | |
| switch (bp->settings().type) { | |
| case debug_ipc::BreakpointType::kSoftware: | |
| return RegisterSoftwareBreakpoint(bp, address); | |
| case debug_ipc::BreakpointType::kHardware: | |
| return RegisterHardwareBreakpoint(bp, address); | |
| case debug_ipc::BreakpointType::kReadWrite: | |
| case debug_ipc::BreakpointType::kWrite: | |
| FX_NOTREACHED() << "Watchpoints are registered through RegisterWatchpoint."; | |
| // TODO(donosoc): Reactivate once the transition is complete. | |
| return debug::Status("Watchpoints are registered through RegisterWatchpoint."); | |
| case debug_ipc::BreakpointType::kLast: | |
| FX_NOTREACHED(); | |
| return debug::Status("Invalid breakpoint type."); | |
| } | |
| FX_NOTREACHED(); | |
| } | |
| void DebuggedProcess::UnregisterBreakpoint(Breakpoint* bp, uint64_t address) { | |
| DEBUG_LOG(Process) << LogPreamble(this) << "Unregistering breakpoint " << bp->settings().id | |
| << " (" << bp->settings().name << ")."; | |
| switch (bp->settings().type) { | |
| case debug_ipc::BreakpointType::kSoftware: | |
| return UnregisterSoftwareBreakpoint(bp, address); | |
| case debug_ipc::BreakpointType::kHardware: | |
| return UnregisterHardwareBreakpoint(bp, address); | |
| case debug_ipc::BreakpointType::kReadWrite: | |
| case debug_ipc::BreakpointType::kWrite: | |
| FX_NOTREACHED() << "Watchpoints are unregistered through UnregisterWatchpoint."; | |
| return; | |
| case debug_ipc::BreakpointType::kLast: | |
| FX_NOTREACHED(); | |
| return; | |
| } | |
| FX_NOTREACHED(); | |
| } | |
| debug::Status DebuggedProcess::RegisterWatchpoint(Breakpoint* bp, | |
| const debug::AddressRange& range) { | |
| FX_DCHECK(debug_ipc::IsWatchpointType(bp->settings().type)) | |
| << "Breakpoint type must be kWatchpoint, got: " | |
| << debug_ipc::BreakpointTypeToString(bp->settings().type); | |
| // NOTE: Even though the watchpoint system can handle un-aligned ranges, there is no way for | |
| // an exception to determine which byte access actually triggered the exception. This means | |
| // that watchpoint installed and nominal ranges should be the same. | |
| // | |
| // We make that check here and fail early if the range is not correctly aligned. | |
| auto aligned_range = AlignRange(range); | |
| if (!aligned_range.has_value() || aligned_range.value() != range) | |
| return debug::Status("Watchpoint range must be aligned."); | |
| auto it = watchpoints_.find(range); | |
| if (it == watchpoints_.end()) { | |
| auto watchpoint = std::make_unique<Watchpoint>(bp->settings().type, bp, this, range); | |
| if (auto status = watchpoint->Init(); status.has_error()) | |
| return status; | |
| watchpoints_[range] = std::move(watchpoint); | |
| return debug::Status(); | |
| } else { | |
| return it->second->RegisterBreakpoint(bp); | |
| } | |
| } | |
| void DebuggedProcess::UnregisterWatchpoint(Breakpoint* bp, const debug::AddressRange& range) { | |
| FX_DCHECK(debug_ipc::IsWatchpointType(bp->settings().type)) | |
| << "Breakpoint type must be kWatchpoint, got: " | |
| << debug_ipc::BreakpointTypeToString(bp->settings().type); | |
| auto it = watchpoints_.find(range); | |
| if (it == watchpoints_.end()) | |
| return; | |
| Watchpoint* watchpoint = it->second.get(); | |
| bool still_used = watchpoint->UnregisterBreakpoint(bp); | |
| if (!still_used) { | |
| for (auto& [thread_koid, thread] : threads_) { | |
| thread->WillDeleteProcessBreakpoint(watchpoint); | |
| } | |
| } | |
| watchpoints_.erase(it); | |
| } | |
| void DebuggedProcess::EnqueueStepOver(ProcessBreakpoint* process_breakpoint, | |
| DebuggedThread* thread) { | |
| // Passing the thread will delete any previous queuing of the same thread. Otherwise the thread | |
| // will be recusrsively waiting for itself and can never make progress. | |
| PruneStepOverQueue(thread); | |
| StepOverTicket ticket = {}; | |
| ticket.process_breakpoint = process_breakpoint->GetWeakPtr(); | |
| ticket.thread = thread->GetWeakPtr(); | |
| step_over_queue_.push_back(std::move(ticket)); | |
| DEBUG_LOG(Process) << LogPreamble(this) << "[PB: 0x" << std::hex << process_breakpoint->address() | |
| << "] Enqueing thread " << std::dec << thread->koid() | |
| << " for step over. Queue size: " << step_over_queue_.size(); | |
| // If the queue already had an element, we wait until that element is done. | |
| if (step_over_queue_.size() > 1u) | |
| return; | |
| // This is the first ticket in the queue. We start executing it immediatelly. | |
| process_breakpoint->ExecuteStepOver(thread); | |
| } | |
| void DebuggedProcess::OnBreakpointFinishedSteppingOver() { | |
| { | |
| // We always tell the current breakpoint to finish the stepping over after starting the new one. | |
| // This will free the other suspended threads, letting the new stepping over thread continue. | |
| // | |
| // We need to do this *after* starting the following breakpoint, because otherwise we introduce | |
| // a window where threads are unsuspeded between breakpoints. | |
| auto prev_ticket = step_over_queue_.front(); | |
| auto post_execute_breakpoint = fit::defer([prev_ticket = std::move(prev_ticket)]() { | |
| if (!prev_ticket.is_valid()) | |
| return; | |
| prev_ticket.process_breakpoint->StepOverCleanup(prev_ticket.thread.get()); | |
| }); | |
| // Pop the previous ticket (the post-complete action is already set with the deferred action). | |
| step_over_queue_.pop_front(); | |
| // If there are still elements in the queue, we execute the next one (the queue is pruned so we | |
| // know the next one is valid). | |
| PruneStepOverQueue(nullptr); | |
| if (!step_over_queue_.empty()) { | |
| auto& ticket = step_over_queue_.front(); | |
| ticket.process_breakpoint->ExecuteStepOver(ticket.thread.get()); | |
| return; | |
| } | |
| } | |
| } | |
| void DebuggedProcess::OnProcessTerminated() { | |
| DEBUG_LOG(Process) << LogPreamble(this) << "Terminating."; | |
| debug_ipc::NotifyProcessExiting notify; | |
| notify.process_koid = koid(); | |
| notify.return_code = process_handle_->GetReturnCode(); | |
| notify.timestamp = GetNowTimestamp(); | |
| debug_agent_->SendNotification(notify); | |
| debug_agent_->RemoveDebuggedProcess(koid()); | |
| // "THIS" IS NOW DELETED. | |
| } | |
| void DebuggedProcess::OnThreadStarting(std::unique_ptr<ExceptionHandle> exception) { | |
| auto thread_handle = exception->GetThreadHandle(); | |
| zx_koid_t thread_id = thread_handle->GetKoid(); | |
| DEBUG_LOG(Process) << LogPreamble(this) << "Thread starting with koid " << thread_id; | |
| if (threads_.find(thread_id) != threads_.end()) { | |
| // This is possible when `DebugAgent::AttachToExistingProcess` in the following order: | |
| // - We create an exception channel. | |
| // - A thread starts and the notification is delivered to the exception channel, | |
| // but we haven't got a chance to look into it. | |
| // - We PopulateCurrentThreads(). | |
| // - We process the pending notifications and OnThreadStarting() the same thread | |
| // populated just now. | |
| return; | |
| } | |
| auto new_thread = std::make_unique<DebuggedThread>(debug_agent_, this, std::move(thread_handle)); | |
| if (suspend_new_threads_) { | |
| new_thread->ClientSuspend(true); | |
| } | |
| // Notify the client. | |
| new_thread->SendThreadNotification(); | |
| threads_.emplace(thread_id, std::move(new_thread)); | |
| } | |
| void DebuggedProcess::OnThreadExiting(std::unique_ptr<ExceptionHandle> exception) { | |
| auto excepting_thread_handle = exception->GetThreadHandle(); | |
| zx_koid_t thread_id = excepting_thread_handle->GetKoid(); | |
| DEBUG_LOG(Process) << LogPreamble(this) << " Thread exiting with koid " << thread_id; | |
| // Clean up our DebuggedThread object. | |
| auto found_thread = threads_.find(thread_id); | |
| if (found_thread != threads_.end()) { | |
| threads_.erase(thread_id); | |
| } | |
| // The thread will currently be in a "Dying" state. For it to complete its | |
| // lifecycle it must be resumed. | |
| exception.reset(); | |
| // Notify the client. Can't call GetThreadRecord since the thread doesn't exist any more. | |
| debug_ipc::NotifyThreadExiting notify; | |
| notify.record.id = {.process = koid(), .thread = thread_id}; | |
| notify.record.state = debug_ipc::ThreadRecord::State::kDead; | |
| notify.timestamp = GetNowTimestamp(); | |
| debug_agent_->SendNotification(notify); | |
| } | |
| void DebuggedProcess::OnException(std::unique_ptr<ExceptionHandle> exception) { | |
| auto excepting_thread_handle = exception->GetThreadHandle(); | |
| zx_koid_t thread_id = excepting_thread_handle->GetKoid(); | |
| DebuggedThread* thread = GetThread(thread_id); | |
| if (!thread) { | |
| // The faulting thread should be suspended in the exception that we're currently holding, so we | |
| // should be able to find it after synchronizing, but if we don't then we cannot continue. | |
| PopulateCurrentThreads(); | |
| thread = GetThread(thread_id); | |
| if (!thread) { | |
| LOGS(Error) << "Exception on thread " << thread_id << " which we don't know about."; | |
| return; | |
| } | |
| } | |
| // If we get here thread should always be valid. | |
| FX_DCHECK(thread); | |
| thread->OnException(std::move(exception)); | |
| } | |
| void DebuggedProcess::OnProcessStarting(std::unique_ptr<ProcessHandle> new_process_handle) { | |
| // This is called only on Linux when we observe a fork. Fuchsia new processes are detected by | |
| // the job. | |
| #if defined(__linux__) | |
| // The new process will be a copy of the current one, including all software breakpoints | |
| // installed in this one. In response to this notification, we will either resume and detach from | |
| // the forked process (in which case we better not leave those software breakpoint instructions | |
| // in), or we will continue with debugging it and breakpoint system will reinstall the breakpoints | |
| // (in which case it will get incorrect "old data" under the breakpoint unless we put the data | |
| // back now). | |
| // | |
| // Either way, undo all software breakpoints in the new process that referred to our current one. | |
| for (const auto& [addr, sw_bp] : software_breakpoints_) { | |
| sw_bp->UninstallFromMemorySpace(*new_process_handle); | |
| } | |
| debug_agent_->OnProcessChanged(DebugAgent::ProcessChangedHow::kStarting, | |
| std::move(new_process_handle)); | |
| #endif | |
| } | |
| void DebuggedProcess::OnAddressSpace(const debug_ipc::AddressSpaceRequest& request, | |
| debug_ipc::AddressSpaceReply* reply) { | |
| reply->map = process_handle_->GetAddressSpace(request.address); | |
| } | |
| void DebuggedProcess::OnModules(debug_ipc::ModulesReply* reply) { | |
| // When the client explicitly requests modules, we are no longer weakly attached. | |
| is_weakly_attached_ = false; | |
| // Since the client requested the modules explicitly, force update our cache in case something | |
| // changed unexpectedly. | |
| module_list_.Update(process_handle()); | |
| reply->modules = module_list_.modules(); | |
| } | |
| void DebuggedProcess::OnWriteMemory(const debug_ipc::WriteMemoryRequest& request, | |
| debug_ipc::WriteMemoryReply* reply) { | |
| size_t actual = 0; | |
| // TODO(brettw) replace reply with a serialized Status. | |
| if (debug::Status status = process_handle_->WriteMemory(request.address, request.data.data(), | |
| request.data.size(), &actual); | |
| status.has_error()) { | |
| reply->status = status; | |
| } else if (actual != request.data.size()) { | |
| // Convert partial writes to errors. | |
| reply->status = debug::Status("Partial write of " + std::to_string(actual) + | |
| " bytes instead of " + std::to_string(request.data.size())); | |
| } else { | |
| reply->status = debug::Status(); | |
| } | |
| } | |
| void DebuggedProcess::OnLoadInfoHandleTable(const debug_ipc::LoadInfoHandleTableRequest& request, | |
| debug_ipc::LoadInfoHandleTableReply* reply) { | |
| auto result = process_handle_->GetHandles(); | |
| if (result.is_error()) { | |
| // TODO(brettw) replace reply with a serialized status. | |
| reply->status = result.error_value(); | |
| } else { | |
| reply->status = debug::Status(); | |
| reply->handles = std::move(std::move(result).value()); | |
| } | |
| } | |
| void DebuggedProcess::InjectThreadForTest(std::unique_ptr<DebuggedThread> thread) { | |
| zx_koid_t koid = thread->koid(); | |
| threads_[koid] = std::move(thread); | |
| } | |
| std::vector<debug_ipc::ProcessThreadId> DebuggedProcess::ClientSuspendAllThreads( | |
| zx_koid_t except_thread) { | |
| DEBUG_LOG(Process) << LogPreamble(this) << "DebuggedProcess::ClientSuspendAllThreads"; | |
| // Also suspend new threads. | |
| suspend_new_threads_ = true; | |
| std::vector<debug_ipc::ProcessThreadId> suspended_thread_ids; | |
| // Issue the suspension order for all the threads. | |
| for (auto& [thread_koid, thread] : threads_) { | |
| // Do an asynchronous suspend. We'll wait for the suspension at the bottom. If there is more | |
| // than one thread this allows waiting for each to complete in parallel instead of series. | |
| // | |
| // Here we explitly check for something already suspended, even if re-suspending it is a no-op, | |
| // because we don't want to report its state as changed. | |
| if (thread_koid != except_thread && !thread->is_client_suspended()) { | |
| suspended_thread_ids.push_back({.process = koid(), .thread = thread_koid}); | |
| thread->ClientSuspend(false); | |
| } | |
| } | |
| // Wait on the notification for each thread. | |
| auto deadline = DebuggedThread::DefaultSuspendDeadline(); | |
| for (const debug_ipc::ProcessThreadId& id : suspended_thread_ids) { | |
| if (DebuggedThread* thread = GetThread(id.thread)) | |
| thread->thread_handle().WaitForSuspension(deadline); | |
| } | |
| return suspended_thread_ids; | |
| } | |
| void DebuggedProcess::SetStdout(OwnedStdioHandle handle) { | |
| stdout_ = std::make_unique<BufferedStdioHandle>(std::move(handle)); | |
| // We bind |this| into the callbacks. This is OK because the DebuggedProcess | |
| // owns both sockets, meaning that it's assured to outlive the sockets. | |
| stdout_->set_data_available_callback([this]() { OnStdout(false); }); | |
| stdout_->set_error_callback([this]() { OnStdout(true); }); | |
| if (!stdout_->Start()) { | |
| LOGS(Warn) << "Could not listen on stdout for process " << process_handle_->GetName(); | |
| stdout_.reset(); | |
| } | |
| } | |
| void DebuggedProcess::SetStderr(OwnedStdioHandle handle) { | |
| stderr_ = std::make_unique<BufferedStdioHandle>(std::move(handle)); | |
| stderr_->set_data_available_callback([this]() { OnStderr(false); }); | |
| stderr_->set_error_callback([this]() { OnStderr(true); }); | |
| if (!stderr_->Start()) { | |
| LOGS(Warn) << "Could not listen on stderr for process " << process_handle_->GetName(); | |
| stderr_.reset(); | |
| } | |
| } | |
| void DebuggedProcess::OnStdout(bool close) { | |
| if (close) { | |
| // Note: in the "close" case, the stdout_ object will already be internally reset. | |
| DEBUG_LOG(Process) << LogPreamble(this) << "stdout closed."; | |
| stdout_.reset(); | |
| return; | |
| } | |
| auto data = ReadSocketInput(stdout_.get()); | |
| FX_DCHECK(!data.empty()); | |
| DEBUG_LOG(Process) << LogPreamble(this) | |
| << "Got stdout: " << std::string(data.data(), data.size()); | |
| SendIO(debug_ipc::NotifyIO::Type::kStdout, std::move(data)); | |
| } | |
| void DebuggedProcess::OnStderr(bool close) { | |
| if (close) { | |
| // Note: in the "close" case, the stderr_ object will already be internally reset. | |
| DEBUG_LOG(Process) << LogPreamble(this) << "stderr closed."; | |
| stderr_.reset(); | |
| return; | |
| } | |
| auto data = ReadSocketInput(stderr_.get()); | |
| FX_DCHECK(!data.empty()); | |
| DEBUG_LOG(Process) << LogPreamble(this) | |
| << "Got stderr: " << std::string(data.data(), data.size()); | |
| SendIO(debug_ipc::NotifyIO::Type::kStderr, std::move(data)); | |
| } | |
| void DebuggedProcess::SendIO(debug_ipc::NotifyIO::Type type, const std::vector<char>& data) { | |
| // We send the IO message in chunks. | |
| auto it = data.begin(); | |
| size_t size = data.size(); | |
| while (size > 0) { | |
| size_t chunk_size = size; | |
| if (chunk_size >= debug_ipc::NotifyIO::kMaxDataSize) | |
| chunk_size = debug_ipc::NotifyIO::kMaxDataSize; | |
| auto end = it + chunk_size; | |
| std::string msg(it, end); | |
| it = end; | |
| size -= chunk_size; | |
| debug_ipc::NotifyIO notify; | |
| notify.process_koid = koid(); | |
| notify.type = type; | |
| // We tell whether this is a piece of a bigger message. | |
| notify.more_data_available = size > 0; | |
| notify.data = std::move(msg); | |
| notify.timestamp = GetNowTimestamp(); | |
| debug_agent_->SendNotification(notify); | |
| } | |
| } | |
| void DebuggedProcess::PruneStepOverQueue(DebuggedThread* optional_thread) { | |
| std::deque<StepOverTicket> good_tickets; | |
| for (auto& ticket : step_over_queue_) { | |
| if (!ticket.is_valid()) | |
| continue; | |
| if (optional_thread && ticket.thread && ticket.thread.get() == optional_thread) | |
| continue; // Delete everything from this thread. | |
| good_tickets.push_back(std::move(ticket)); | |
| } | |
| step_over_queue_ = std::move(good_tickets); | |
| } | |
| debug::Status DebuggedProcess::RegisterSoftwareBreakpoint(Breakpoint* bp, uint64_t address) { | |
| auto found = software_breakpoints_.find(address); | |
| if (found == software_breakpoints_.end()) { | |
| auto breakpoint = std::make_unique<SoftwareBreakpoint>(bp, this, address); | |
| if (auto status = breakpoint->Init(); status.has_error()) | |
| return status; | |
| software_breakpoints_[address] = std::move(breakpoint); | |
| return debug::Status(); | |
| } else { | |
| return found->second->RegisterBreakpoint(bp); | |
| } | |
| } | |
| void DebuggedProcess::UnregisterSoftwareBreakpoint(Breakpoint* bp, uint64_t address) { | |
| auto found = software_breakpoints_.find(address); | |
| if (found == software_breakpoints_.end()) { | |
| return; | |
| } | |
| bool still_used = found->second->UnregisterBreakpoint(bp); | |
| if (!still_used) { | |
| for (auto& pair : threads_) | |
| pair.second->WillDeleteProcessBreakpoint(found->second.get()); | |
| software_breakpoints_.erase(found); | |
| } | |
| } | |
| debug::Status DebuggedProcess::RegisterHardwareBreakpoint(Breakpoint* bp, uint64_t address) { | |
| auto found = hardware_breakpoints_.find(address); | |
| if (found == hardware_breakpoints_.end()) { | |
| auto breakpoint = std::make_unique<HardwareBreakpoint>(bp, this, address); | |
| if (auto status = breakpoint->Init(); status.has_error()) | |
| return status; | |
| hardware_breakpoints_[address] = std::move(breakpoint); | |
| return debug::Status(); | |
| } else { | |
| return found->second->RegisterBreakpoint(bp); | |
| } | |
| } | |
| void DebuggedProcess::UnregisterHardwareBreakpoint(Breakpoint* bp, uint64_t address) { | |
| auto found = hardware_breakpoints_.find(address); | |
| if (found == hardware_breakpoints_.end()) { | |
| return; | |
| } | |
| bool still_used = found->second->UnregisterBreakpoint(bp); | |
| if (!still_used) { | |
| for (auto& pair : threads_) | |
| pair.second->WillDeleteProcessBreakpoint(found->second.get()); | |
| hardware_breakpoints_.erase(found); | |
| } | |
| } | |
| } // namespace debug_agent |