blob: c380ea2902414f8b0b69114a30dfe3d9ac94b4a8 [file] [log] [blame]
// 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 "garnet/bin/debug_agent/debugged_thread.h"
#include <inttypes.h>
#include <zircon/syscalls/debug.h>
#include <zircon/syscalls/exception.h>
#include <memory>
#include "garnet/bin/debug_agent/arch.h"
#include "garnet/bin/debug_agent/debug_agent.h"
#include "garnet/bin/debug_agent/debugged_process.h"
#include "garnet/bin/debug_agent/process_breakpoint.h"
#include "garnet/bin/debug_agent/process_info.h"
#include "garnet/bin/debug_agent/unwind.h"
#include "garnet/lib/debug_ipc/agent_protocol.h"
#include "garnet/lib/debug_ipc/helper/message_loop_zircon.h"
#include "garnet/lib/debug_ipc/helper/stream_buffer.h"
#include "garnet/lib/debug_ipc/helper/zx_status.h"
#include "garnet/lib/debug_ipc/message_reader.h"
#include "garnet/lib/debug_ipc/message_writer.h"
#include "lib/fxl/logging.h"
namespace debug_agent {
DebuggedThread::DebuggedThread(DebuggedProcess* process, zx::thread thread,
zx_koid_t koid, ThreadCreationOption option)
: debug_agent_(process->debug_agent()),
process_(process),
thread_(std::move(thread)),
koid_(koid) {
switch (option) {
case ThreadCreationOption::kRunningKeepRunning:
// do nothing
break;
case ThreadCreationOption::kSuspendedKeepSuspended:
suspend_reason_ = SuspendReason::kException;
break;
case ThreadCreationOption::kSuspendedShouldRun:
debug_ipc::MessageLoopZircon::Current()->ResumeFromException(thread_, 0);
}
}
DebuggedThread::~DebuggedThread() {}
void DebuggedThread::OnException(uint32_t type) {
suspend_reason_ = SuspendReason::kException;
debug_ipc::NotifyException notify;
notify.type = arch::ArchProvider::Get().DecodeExceptionType(*this, type);
if (current_breakpoint_) {
// The current breakpoint is set only when stopped at a breakpoint or when
// single-stepping over one. We're not going to get an exception for a
// thread when stopped, so hitting this exception means the breakpoint is
// done being stepped over. The breakpoint will tell us if the exception
// was from a normal completion of the breakpoint step, or whether
// something else went wrong while stepping.
bool completes_bp_step =
current_breakpoint_->BreakpointStepHasException(koid_, notify.type);
current_breakpoint_ = nullptr;
if (completes_bp_step &&
run_mode_ == debug_ipc::ResumeRequest::How::kContinue) {
// This step was an internal thing to step over the breakpoint in
// service of continuing from a breakpoint. Transparently resume the
// thread since the client didn't request the step. The step
// (non-continue) cases will be handled below in the normal flow since
// we just finished a step.
ResumeForRunMode();
return;
}
// Something else went wrong while stepping (the instruction with the
// breakpoint could have crashed). Fall through to dispatching the
// exception to the client.
current_breakpoint_ = nullptr;
}
zx_thread_state_general_regs regs;
thread_.read_state(ZX_THREAD_STATE_GENERAL_REGS, &regs, sizeof(regs));
switch (type) {
case ZX_EXCP_SW_BREAKPOINT:
notify.type = debug_ipc::NotifyException::Type::kSoftware;
if (UpdateForSoftwareBreakpoint(&regs, &notify.hit_breakpoints) ==
OnStop::kIgnore)
return;
break;
case ZX_EXCP_HW_BREAKPOINT: {
if (notify.type == debug_ipc::NotifyException::Type::kSingleStep) {
if (run_mode_ == debug_ipc::ResumeRequest::How::kContinue) {
// This could be due to a race where the user was previously single
// stepping and then requested a continue before the single stepping
// completed. It could also be a breakpoint that was deleted while
// in the process of single-stepping over it. In both cases, the
// least confusing thing is to resume automatically.
ResumeForRunMode();
return;
}
// When stepping in a range, automatically continue as long as we're
// still in range.
if (run_mode_ == debug_ipc::ResumeRequest::How::kStepInRange &&
*arch::ArchProvider::Get().IPInRegs(&regs) >=
step_in_range_begin_ &&
*arch::ArchProvider::Get().IPInRegs(&regs) < step_in_range_end_) {
ResumeForRunMode();
return;
}
} else if (notify.type == debug_ipc::NotifyException::Type::kHardware) {
if (UpdateForHardwareBreakpoint(&regs, &notify.hit_breakpoints) ==
OnStop::kIgnore)
return;
} else {
FXL_NOTREACHED() << "Unexpected hw exception type: "
<< static_cast<uint32_t>(notify.type);
}
break;
}
default:
notify.type = debug_ipc::NotifyException::Type::kGeneral;
break;
}
notify.process_koid = process_->koid();
FillThreadRecord(debug_ipc::ThreadRecord::StackAmount::kMinimal, &regs,
&notify.thread);
// Send notification.
debug_ipc::MessageWriter writer;
debug_ipc::WriteNotifyException(notify, &writer);
debug_agent_->stream()->Write(writer.MessageComplete());
// Keep the thread suspended for the client.
// TODO(brettw) suspend other threads in the process and other debugged
// processes as desired.
}
bool DebuggedThread::Pause() {
if (suspend_reason_ == SuspendReason::kNone) {
if (thread_.suspend(&suspend_token_) == ZX_OK) {
suspend_reason_ = SuspendReason::kOther;
return true;
}
}
return false;
}
void DebuggedThread::Resume(const debug_ipc::ResumeRequest& request) {
run_mode_ = request.how;
step_in_range_begin_ = request.range_begin;
step_in_range_end_ = request.range_end;
ResumeForRunMode();
}
void DebuggedThread::FillThreadRecord(
debug_ipc::ThreadRecord::StackAmount stack_amount,
const zx_thread_state_general_regs* optional_regs,
debug_ipc::ThreadRecord* record) const {
debug_agent::FillThreadRecord(process_->process(), process_->dl_debug_addr(),
thread_, stack_amount, optional_regs, record);
}
void DebuggedThread::ReadRegisters(
const std::vector<debug_ipc::RegisterCategory::Type>& cats_to_get,
std::vector<debug_ipc::RegisterCategory>* out) const {
out->clear();
for (const auto& cat_type : cats_to_get) {
auto& cat = out->emplace_back();
cat.type = cat_type;
zx_status_t status = arch::ArchProvider::Get().ReadRegisters(
cat_type, thread_, &cat.registers);
if (status != ZX_OK) {
out->pop_back();
FXL_LOG(ERROR) << "Could not get register state for category: "
<< debug_ipc::RegisterCategory::TypeToString(cat_type);
}
}
}
zx_status_t DebuggedThread::WriteRegisters(
const std::vector<debug_ipc::Register>& regs) {
// We use a map to keep track of which categories will change.
std::map<debug_ipc::RegisterCategory::Type, debug_ipc::RegisterCategory>
categories;
bool rip_change = false;
debug_ipc::RegisterID rip_id = GetSpecialRegisterID(
arch::ArchProvider::Get().GetArch(), debug_ipc::SpecialRegisterType::kIP);
// We append each register to the correct category to be changed.
for (const debug_ipc::Register& reg : regs) {
auto cat_type = debug_ipc::RegisterCategory::RegisterIDToCategory(reg.id);
if (cat_type == debug_ipc::RegisterCategory::Type::kNone) {
FXL_LOG(WARNING) << "Attempting to change register without category: "
<< RegisterIDToString(reg.id);
continue;
}
// We are changing the RIP, meaning that we're not going to jump over a
// breakpoint.
if (reg.id == rip_id)
rip_change = true;
auto& category = categories[cat_type];
category.type = cat_type;
category.registers.push_back(reg);
}
for (const auto& [cat_type, cat] : categories) {
FXL_DCHECK(cat_type != debug_ipc::RegisterCategory::Type::kNone);
zx_status_t res = arch::ArchProvider::Get().WriteRegisters(cat, &thread_);
if (res != ZX_OK) {
FXL_LOG(WARNING) << "Could not write category "
<< debug_ipc::RegisterCategory::TypeToString(cat_type)
<< ": " << debug_ipc::ZxStatusToString(res);
}
}
// If the debug agent wrote to the thread IP directly, then current state is
// no longer valid. Specifically, if we're currently on a breakpoint, we have
// to now know the fact that we're no longer in a breakpoint.
//
// This is necessary to avoid the single-stepping logic that the thread does
// when resuming from a breakpoint.
current_breakpoint_ = nullptr;
return ZX_OK;
}
void DebuggedThread::SendThreadNotification() const {
debug_ipc::ThreadRecord record;
FillThreadRecord(debug_ipc::ThreadRecord::StackAmount::kMinimal, nullptr,
&record);
debug_ipc::NotifyThread notify;
notify.process_koid = process_->koid();
notify.record = record;
debug_ipc::MessageWriter writer;
debug_ipc::WriteNotifyThread(
debug_ipc::MsgHeader::Type::kNotifyThreadStarting, notify, &writer);
debug_agent_->stream()->Write(writer.MessageComplete());
}
void DebuggedThread::WillDeleteProcessBreakpoint(ProcessBreakpoint* bp) {
if (current_breakpoint_ == bp)
current_breakpoint_ = nullptr;
}
DebuggedThread::OnStop DebuggedThread::UpdateForSoftwareBreakpoint(
zx_thread_state_general_regs* regs,
std::vector<debug_ipc::BreakpointStats>* hit_breakpoints) {
uint64_t breakpoint_address =
arch::ArchProvider::Get()
.BreakpointInstructionForSoftwareExceptionAddress(
*arch::ArchProvider::Get().IPInRegs(regs));
ProcessBreakpoint* found_bp =
process_->FindProcessBreakpointForAddr(breakpoint_address);
if (found_bp) {
// Our software breakpoint.
UpdateForHitProcessBreakpoint(debug_ipc::BreakpointType::kSoftware,
found_bp, regs, hit_breakpoints);
// The found_bp could have been deleted if it was a one-shot, so must
// not be dereferenced below this.
found_bp = nullptr;
} else {
// Hit a software breakpoint that doesn't correspond to any current
// breakpoint.
if (arch::ArchProvider::Get().IsBreakpointInstruction(process_->process(),
breakpoint_address)) {
// The breakpoint is a hardcoded instruction in the program code. In
// this case we want to continue from the following instruction since
// the breakpoint instruction will never go away.
*arch::ArchProvider::Get().IPInRegs(regs) =
arch::ArchProvider::Get().NextInstructionForSoftwareExceptionAddress(
*arch::ArchProvider::Get().IPInRegs(regs));
zx_status_t status =
thread_.write_state(ZX_THREAD_STATE_GENERAL_REGS, regs,
sizeof(zx_thread_state_general_regs));
if (status != ZX_OK) {
fprintf(stderr, "Warning: could not update IP on thread, error = %d.",
static_cast<int>(status));
}
if (!process_->dl_debug_addr() && process_->RegisterDebugState()) {
// This breakpoint was the explicit breakpoint ld.so executes to
// notify us that the loader is ready. Send the current module list
// and silently keep this thread stopped. The client will explicitly
// resume this thread when it's ready to continue (it will need to
// load symbols for the modules and may need to set breakpoints based
// on them).
std::vector<uint64_t> paused_threads;
paused_threads.push_back(koid());
process_->SendModuleNotification(std::move(paused_threads));
return OnStop::kIgnore;
}
} else {
// Not a breakpoint instruction. Probably the breakpoint instruction
// used to be ours but its removal raced with the exception handler.
// Resume from the instruction that used to be the breakpoint.
*arch::ArchProvider::Get().IPInRegs(regs) = breakpoint_address;
// Don't automatically continue execution here. A race for this should
// be unusual and maybe something weird happened that caused an
// exception we're not set up to handle. Err on the side of telling the
// user about the exception.
}
}
return OnStop::kSendNotification;
}
DebuggedThread::OnStop DebuggedThread::UpdateForHardwareBreakpoint(
zx_thread_state_general_regs* regs,
std::vector<debug_ipc::BreakpointStats>* hit_breakpoints) {
uint64_t breakpoint_address =
arch::ArchProvider::Get()
.BreakpointInstructionForHardwareExceptionAddress(
*arch::ArchProvider::Get().IPInRegs(regs));
ProcessBreakpoint* found_bp =
process_->FindProcessBreakpointForAddr(breakpoint_address);
if (!found_bp) {
// Hit a hw debug exception that doesn't belong to any ProcessBreakpoint.
// This is probably a race between the removal and the exception handler.
// Send a notification.
*arch::ArchProvider::Get().IPInRegs(regs) = breakpoint_address;
return OnStop::kSendNotification;
}
UpdateForHitProcessBreakpoint(debug_ipc::BreakpointType::kHardware, found_bp,
regs, hit_breakpoints);
// The ProcessBreakpoint could've been deleted if it was a one-shot, so must
// not be derefereced below this.
found_bp = nullptr;
return OnStop::kSendNotification;
}
void DebuggedThread::UpdateForHitProcessBreakpoint(
debug_ipc::BreakpointType exception_type,
ProcessBreakpoint* process_breakpoint, zx_thread_state_general_regs* regs,
std::vector<debug_ipc::BreakpointStats>* hit_breakpoints) {
current_breakpoint_ = process_breakpoint;
process_breakpoint->OnHit(exception_type, hit_breakpoints);
// When the program hits one of our breakpoints, set the IP back to
// the exact address that triggered the breakpoint. When the thread
// resumes, this is the address that it will resume from (after
// putting back the original instruction), and will be what the client
// wants to display to the user.
*arch::ArchProvider::Get().IPInRegs(regs) = process_breakpoint->address();
zx_status_t status = thread_.write_state(
ZX_THREAD_STATE_GENERAL_REGS, regs, sizeof(zx_thread_state_general_regs));
if (status != ZX_OK) {
fprintf(stderr, "Warning: could not update IP on thread, error = %d.",
static_cast<int>(status));
}
// Delete any one-shot breakpoints. Since there can be multiple Breakpoints
// (some one-shot, some not) referring to the current ProcessBreakpoint,
// this operation could delete the ProcessBreakpoint or it could not. If it
// does, our observer will be told and current_breakpoint_ will be cleared.
for (const auto& stats : *hit_breakpoints) {
if (stats.should_delete)
process_->debug_agent()->RemoveBreakpoint(stats.breakpoint_id);
}
}
void DebuggedThread::ResumeForRunMode() {
// If we jumped, once we resume we reset the status.
if (suspend_reason_ == SuspendReason::kException) {
// Note: we could have a valid suspend token here in addition to the
// exception if the suspension races with the delivery of the exception.
if (current_breakpoint_) {
// Going over a breakpoint always requires a single-step first. Then we
// continue according to run_mode_.
SetSingleStep(true);
current_breakpoint_->BeginStepOver(koid_);
} else {
// All non-continue resumptions require single stepping.
SetSingleStep(run_mode_ != debug_ipc::ResumeRequest::How::kContinue);
}
suspend_reason_ = SuspendReason::kNone;
debug_ipc::MessageLoopZircon::Current()->ResumeFromException(thread_, 0);
} else if (suspend_reason_ == SuspendReason::kOther) {
// A breakpoint should only be current when it was hit which will be
// caused by an exception.
FXL_DCHECK(!current_breakpoint_);
// All non-continue resumptions require single stepping.
SetSingleStep(run_mode_ != debug_ipc::ResumeRequest::How::kContinue);
// The suspend token is holding the thread suspended, releasing it will
// resume (if nobody else has the thread suspended).
suspend_reason_ = SuspendReason::kNone;
FXL_DCHECK(suspend_token_.is_valid());
suspend_token_.reset();
}
}
void DebuggedThread::SetSingleStep(bool single_step) {
zx_thread_state_single_step_t value = single_step ? 1 : 0;
// This could fail for legitimate reasons, like the process could have just
// closed the thread.
thread_.write_state(ZX_THREAD_STATE_SINGLE_STEP, &value, sizeof(value));
}
const char* DebuggedThread::SuspendReasonToString(SuspendReason reason) {
switch (reason) {
case SuspendReason::kNone:
return "None";
case SuspendReason::kException:
return "Exception";
case SuspendReason::kOther:
return "Other";
}
FXL_NOTREACHED();
return "";
}
} // namespace debug_agent