
lldb today has two rules: When a thread stops at a BreakpointSite, we set the thread's StopReason to be "breakpoint hit" (regardless if we've actually hit the breakpoint, or if we've merely stopped *at* the breakpoint instruction/point and haven't tripped it yet). And second, when resuming a process, any thread sitting at a BreakpointSite is silently stepped over the BreakpointSite -- because we've already flagged the breakpoint hit when we stopped there originally. In this patch, I change lldb to only set a thread's stop reason to breakpoint-hit when we've actually executed the instruction/triggered the breakpoint. When we resume, we only silently step past a BreakpointSite that we've registered as hit. We preserve this state across inferior function calls that the user may do while stopped, etc. Also, when a user adds a new breakpoint at $pc while stopped, or changes $pc to be the address of a BreakpointSite, we will silently step past that breakpoint when the process resumes. This is purely a UX call, I don't think there's any person who wants to set a breakpoint at $pc and then hit it immediately on resuming. One non-intuitive UX from this change, but I'm convinced it is necessary: If you're stopped at a BreakpointSite that has not yet executed, you `stepi`, you will hit the breakpoint and the pc will not yet advance. This thread has not completed its stepi, and the thread plan is still on the stack. If you then `continue` the thread, lldb will now stop and say, "instruction step completed", one instruction past the BreakpointSite. You can continue a second time to resume execution. I discussed this with Jim, and trying to paper over this behavior will lead to more complicated scenarios behaving non-intuitively. And mostly it's the testsuite that was trying to instruction step past a breakpoint and getting thrown off -- and I changed those tests to expect the new behavior. The bugs driving this change are all from lldb dropping the real stop reason for a thread and setting it to breakpoint-hit when that was not the case. Jim hit one where we have an aarch64 watchpoint that triggers one instruction before a BreakpointSite. On this arch we are notified of the watchpoint hit after the instruction has been unrolled -- we disable the watchpoint, instruction step, re-enable the watchpoint and collect the new value. But now we're on a BreakpointSite so the watchpoint-hit stop reason is lost. Another was reported by ZequanWu in https://discourse.llvm.org/t/lldb-unable-to-break-at-start/78282 we attach to/launch a process with the pc at a BreakpointSite and misbehave. Caroline Tice mentioned it is also a problem they've had with putting a breakpoint on _dl_debug_state. The change to each Process plugin that does execution control is that 1. If we've stopped at a BreakpointSite that has not been executed yet, we will call Thread::SetThreadStoppedAtUnexecutedBP(pc) to record that. When the thread resumes, if the pc is still at the same site, we will continue, hit the breakpoint, and stop again. 2. When we've actually hit a breakpoint (enabled for this thread or not), the Process plugin should call Thread::SetThreadHitBreakpointSite(). When we go to resume the thread, we will push a step-over-breakpoint ThreadPlan before resuming. The biggest set of changes is to StopInfoMachException where we translate a Mach Exception into a stop reason. The Mach exception codes differ in a few places depending on the target (unambiguously), and I didn't want to duplicate the new code for each target so I've tested what mach exceptions we get for each action on each target, and reorganized StopInfoMachException::CreateStopReasonWithMachException to document these possible values, and handle them without specializing based on the target arch. rdar://123942164
382 lines
13 KiB
C++
382 lines
13 KiB
C++
//===-- ScriptedThread.cpp ------------------------------------------------===//
|
|
//
|
|
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
// See https://llvm.org/LICENSE.txt for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#include "ScriptedThread.h"
|
|
|
|
#include "Plugins/Process/Utility/RegisterContextThreadMemory.h"
|
|
#include "Plugins/Process/Utility/StopInfoMachException.h"
|
|
#include "lldb/Target/OperatingSystem.h"
|
|
#include "lldb/Target/Process.h"
|
|
#include "lldb/Target/RegisterContext.h"
|
|
#include "lldb/Target/StopInfo.h"
|
|
#include "lldb/Target/Unwind.h"
|
|
#include "lldb/Utility/DataBufferHeap.h"
|
|
#include "lldb/Utility/LLDBLog.h"
|
|
#include <memory>
|
|
#include <optional>
|
|
|
|
using namespace lldb;
|
|
using namespace lldb_private;
|
|
|
|
void ScriptedThread::CheckInterpreterAndScriptObject() const {
|
|
lldbassert(m_script_object_sp && "Invalid Script Object.");
|
|
lldbassert(GetInterface() && "Invalid Scripted Thread Interface.");
|
|
}
|
|
|
|
llvm::Expected<std::shared_ptr<ScriptedThread>>
|
|
ScriptedThread::Create(ScriptedProcess &process,
|
|
StructuredData::Generic *script_object) {
|
|
if (!process.IsValid())
|
|
return llvm::createStringError(llvm::inconvertibleErrorCode(),
|
|
"Invalid scripted process.");
|
|
|
|
process.CheckScriptedInterface();
|
|
|
|
auto scripted_thread_interface =
|
|
process.GetInterface().CreateScriptedThreadInterface();
|
|
if (!scripted_thread_interface)
|
|
return llvm::createStringError(
|
|
llvm::inconvertibleErrorCode(),
|
|
"Failed to create scripted thread interface.");
|
|
|
|
llvm::StringRef thread_class_name;
|
|
if (!script_object) {
|
|
std::optional<std::string> class_name =
|
|
process.GetInterface().GetScriptedThreadPluginName();
|
|
if (!class_name || class_name->empty())
|
|
return llvm::createStringError(
|
|
llvm::inconvertibleErrorCode(),
|
|
"Failed to get scripted thread class name.");
|
|
thread_class_name = *class_name;
|
|
}
|
|
|
|
ExecutionContext exe_ctx(process);
|
|
auto obj_or_err = scripted_thread_interface->CreatePluginObject(
|
|
thread_class_name, exe_ctx, process.m_scripted_metadata.GetArgsSP(),
|
|
script_object);
|
|
|
|
if (!obj_or_err) {
|
|
llvm::consumeError(obj_or_err.takeError());
|
|
return llvm::createStringError(llvm::inconvertibleErrorCode(),
|
|
"Failed to create script object.");
|
|
}
|
|
|
|
StructuredData::GenericSP owned_script_object_sp = *obj_or_err;
|
|
|
|
if (!owned_script_object_sp->IsValid())
|
|
return llvm::createStringError(llvm::inconvertibleErrorCode(),
|
|
"Created script object is invalid.");
|
|
|
|
lldb::tid_t tid = scripted_thread_interface->GetThreadID();
|
|
|
|
return std::make_shared<ScriptedThread>(process, scripted_thread_interface,
|
|
tid, owned_script_object_sp);
|
|
}
|
|
|
|
ScriptedThread::ScriptedThread(ScriptedProcess &process,
|
|
ScriptedThreadInterfaceSP interface_sp,
|
|
lldb::tid_t tid,
|
|
StructuredData::GenericSP script_object_sp)
|
|
: Thread(process, tid), m_scripted_process(process),
|
|
m_scripted_thread_interface_sp(interface_sp),
|
|
m_script_object_sp(script_object_sp) {}
|
|
|
|
ScriptedThread::~ScriptedThread() { DestroyThread(); }
|
|
|
|
const char *ScriptedThread::GetName() {
|
|
CheckInterpreterAndScriptObject();
|
|
std::optional<std::string> thread_name = GetInterface()->GetName();
|
|
if (!thread_name)
|
|
return nullptr;
|
|
return ConstString(thread_name->c_str()).AsCString();
|
|
}
|
|
|
|
const char *ScriptedThread::GetQueueName() {
|
|
CheckInterpreterAndScriptObject();
|
|
std::optional<std::string> queue_name = GetInterface()->GetQueue();
|
|
if (!queue_name)
|
|
return nullptr;
|
|
return ConstString(queue_name->c_str()).AsCString();
|
|
}
|
|
|
|
void ScriptedThread::WillResume(StateType resume_state) {}
|
|
|
|
void ScriptedThread::ClearStackFrames() { Thread::ClearStackFrames(); }
|
|
|
|
RegisterContextSP ScriptedThread::GetRegisterContext() {
|
|
if (!m_reg_context_sp)
|
|
m_reg_context_sp = CreateRegisterContextForFrame(nullptr);
|
|
return m_reg_context_sp;
|
|
}
|
|
|
|
RegisterContextSP
|
|
ScriptedThread::CreateRegisterContextForFrame(StackFrame *frame) {
|
|
const uint32_t concrete_frame_idx =
|
|
frame ? frame->GetConcreteFrameIndex() : 0;
|
|
|
|
if (concrete_frame_idx)
|
|
return GetUnwinder().CreateRegisterContextForFrame(frame);
|
|
|
|
lldb::RegisterContextSP reg_ctx_sp;
|
|
Status error;
|
|
|
|
std::optional<std::string> reg_data = GetInterface()->GetRegisterContext();
|
|
if (!reg_data)
|
|
return ScriptedInterface::ErrorWithMessage<lldb::RegisterContextSP>(
|
|
LLVM_PRETTY_FUNCTION, "Failed to get scripted thread registers data.",
|
|
error, LLDBLog::Thread);
|
|
|
|
DataBufferSP data_sp(
|
|
std::make_shared<DataBufferHeap>(reg_data->c_str(), reg_data->size()));
|
|
|
|
if (!data_sp->GetByteSize())
|
|
return ScriptedInterface::ErrorWithMessage<lldb::RegisterContextSP>(
|
|
LLVM_PRETTY_FUNCTION, "Failed to copy raw registers data.", error,
|
|
LLDBLog::Thread);
|
|
|
|
std::shared_ptr<RegisterContextMemory> reg_ctx_memory =
|
|
std::make_shared<RegisterContextMemory>(
|
|
*this, 0, *GetDynamicRegisterInfo(), LLDB_INVALID_ADDRESS);
|
|
if (!reg_ctx_memory)
|
|
return ScriptedInterface::ErrorWithMessage<lldb::RegisterContextSP>(
|
|
LLVM_PRETTY_FUNCTION, "Failed to create a register context.", error,
|
|
LLDBLog::Thread);
|
|
|
|
reg_ctx_memory->SetAllRegisterData(data_sp);
|
|
m_reg_context_sp = reg_ctx_memory;
|
|
|
|
return m_reg_context_sp;
|
|
}
|
|
|
|
bool ScriptedThread::LoadArtificialStackFrames() {
|
|
StructuredData::ArraySP arr_sp = GetInterface()->GetStackFrames();
|
|
|
|
Status error;
|
|
if (!arr_sp)
|
|
return ScriptedInterface::ErrorWithMessage<bool>(
|
|
LLVM_PRETTY_FUNCTION, "Failed to get scripted thread stackframes.",
|
|
error, LLDBLog::Thread);
|
|
|
|
size_t arr_size = arr_sp->GetSize();
|
|
if (arr_size > std::numeric_limits<uint32_t>::max())
|
|
return ScriptedInterface::ErrorWithMessage<bool>(
|
|
LLVM_PRETTY_FUNCTION,
|
|
llvm::Twine(
|
|
"StackFrame array size (" + llvm::Twine(arr_size) +
|
|
llvm::Twine(
|
|
") is greater than maximum authorized for a StackFrameList."))
|
|
.str(),
|
|
error, LLDBLog::Thread);
|
|
|
|
StackFrameListSP frames = GetStackFrameList();
|
|
|
|
for (size_t idx = 0; idx < arr_size; idx++) {
|
|
std::optional<StructuredData::Dictionary *> maybe_dict =
|
|
arr_sp->GetItemAtIndexAsDictionary(idx);
|
|
if (!maybe_dict)
|
|
return ScriptedInterface::ErrorWithMessage<bool>(
|
|
LLVM_PRETTY_FUNCTION,
|
|
llvm::Twine(
|
|
"Couldn't get artificial stackframe dictionary at index (" +
|
|
llvm::Twine(idx) + llvm::Twine(") from stackframe array."))
|
|
.str(),
|
|
error, LLDBLog::Thread);
|
|
StructuredData::Dictionary *dict = *maybe_dict;
|
|
|
|
lldb::addr_t pc;
|
|
if (!dict->GetValueForKeyAsInteger("pc", pc))
|
|
return ScriptedInterface::ErrorWithMessage<bool>(
|
|
LLVM_PRETTY_FUNCTION,
|
|
"Couldn't find value for key 'pc' in stackframe dictionary.", error,
|
|
LLDBLog::Thread);
|
|
|
|
Address symbol_addr;
|
|
symbol_addr.SetLoadAddress(pc, &this->GetProcess()->GetTarget());
|
|
|
|
lldb::addr_t cfa = LLDB_INVALID_ADDRESS;
|
|
bool cfa_is_valid = false;
|
|
const bool behaves_like_zeroth_frame = false;
|
|
SymbolContext sc;
|
|
symbol_addr.CalculateSymbolContext(&sc);
|
|
|
|
StackFrameSP synth_frame_sp = std::make_shared<StackFrame>(
|
|
this->shared_from_this(), idx, idx, cfa, cfa_is_valid, pc,
|
|
StackFrame::Kind::Artificial, behaves_like_zeroth_frame, &sc);
|
|
|
|
if (!frames->SetFrameAtIndex(static_cast<uint32_t>(idx), synth_frame_sp))
|
|
return ScriptedInterface::ErrorWithMessage<bool>(
|
|
LLVM_PRETTY_FUNCTION,
|
|
llvm::Twine("Couldn't add frame (" + llvm::Twine(idx) +
|
|
llvm::Twine(") to ScriptedThread StackFrameList."))
|
|
.str(),
|
|
error, LLDBLog::Thread);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ScriptedThread::CalculateStopInfo() {
|
|
StructuredData::DictionarySP dict_sp = GetInterface()->GetStopReason();
|
|
|
|
Status error;
|
|
if (!dict_sp)
|
|
return ScriptedInterface::ErrorWithMessage<bool>(
|
|
LLVM_PRETTY_FUNCTION, "Failed to get scripted thread stop info.", error,
|
|
LLDBLog::Thread);
|
|
|
|
// If we're at a BreakpointSite, mark that we stopped there and
|
|
// need to hit the breakpoint when we resume. This will be cleared
|
|
// if we CreateStopReasonWithBreakpointSiteID.
|
|
if (RegisterContextSP reg_ctx_sp = GetRegisterContext()) {
|
|
addr_t pc = reg_ctx_sp->GetPC();
|
|
if (BreakpointSiteSP bp_site_sp =
|
|
GetProcess()->GetBreakpointSiteList().FindByAddress(pc))
|
|
if (bp_site_sp->IsEnabled())
|
|
SetThreadStoppedAtUnexecutedBP(pc);
|
|
}
|
|
|
|
lldb::StopInfoSP stop_info_sp;
|
|
lldb::StopReason stop_reason_type;
|
|
|
|
if (!dict_sp->GetValueForKeyAsInteger("type", stop_reason_type))
|
|
return ScriptedInterface::ErrorWithMessage<bool>(
|
|
LLVM_PRETTY_FUNCTION,
|
|
"Couldn't find value for key 'type' in stop reason dictionary.", error,
|
|
LLDBLog::Thread);
|
|
|
|
StructuredData::Dictionary *data_dict;
|
|
if (!dict_sp->GetValueForKeyAsDictionary("data", data_dict))
|
|
return ScriptedInterface::ErrorWithMessage<bool>(
|
|
LLVM_PRETTY_FUNCTION,
|
|
"Couldn't find value for key 'data' in stop reason dictionary.", error,
|
|
LLDBLog::Thread);
|
|
|
|
switch (stop_reason_type) {
|
|
case lldb::eStopReasonNone:
|
|
return true;
|
|
case lldb::eStopReasonBreakpoint: {
|
|
lldb::break_id_t break_id;
|
|
data_dict->GetValueForKeyAsInteger("break_id", break_id,
|
|
LLDB_INVALID_BREAK_ID);
|
|
stop_info_sp =
|
|
StopInfo::CreateStopReasonWithBreakpointSiteID(*this, break_id);
|
|
} break;
|
|
case lldb::eStopReasonSignal: {
|
|
uint32_t signal;
|
|
llvm::StringRef description;
|
|
if (!data_dict->GetValueForKeyAsInteger("signal", signal)) {
|
|
signal = LLDB_INVALID_SIGNAL_NUMBER;
|
|
return false;
|
|
}
|
|
data_dict->GetValueForKeyAsString("desc", description);
|
|
stop_info_sp =
|
|
StopInfo::CreateStopReasonWithSignal(*this, signal, description.data());
|
|
} break;
|
|
case lldb::eStopReasonTrace: {
|
|
stop_info_sp = StopInfo::CreateStopReasonToTrace(*this);
|
|
} break;
|
|
case lldb::eStopReasonException: {
|
|
#if defined(__APPLE__)
|
|
StructuredData::Dictionary *mach_exception;
|
|
if (data_dict->GetValueForKeyAsDictionary("mach_exception",
|
|
mach_exception)) {
|
|
llvm::StringRef value;
|
|
mach_exception->GetValueForKeyAsString("type", value);
|
|
auto exc_type =
|
|
StopInfoMachException::MachException::ExceptionCode(value.data());
|
|
|
|
if (!exc_type)
|
|
return false;
|
|
|
|
uint32_t exc_data_size = 0;
|
|
llvm::SmallVector<uint64_t, 3> raw_codes;
|
|
|
|
StructuredData::Array *exc_rawcodes;
|
|
mach_exception->GetValueForKeyAsArray("rawCodes", exc_rawcodes);
|
|
if (exc_rawcodes) {
|
|
auto fetch_data = [&raw_codes](StructuredData::Object *obj) {
|
|
if (!obj)
|
|
return false;
|
|
raw_codes.push_back(obj->GetUnsignedIntegerValue());
|
|
return true;
|
|
};
|
|
|
|
exc_rawcodes->ForEach(fetch_data);
|
|
exc_data_size = raw_codes.size();
|
|
}
|
|
|
|
stop_info_sp = StopInfoMachException::CreateStopReasonWithMachException(
|
|
*this, *exc_type, exc_data_size,
|
|
exc_data_size >= 1 ? raw_codes[0] : 0,
|
|
exc_data_size >= 2 ? raw_codes[1] : 0,
|
|
exc_data_size >= 3 ? raw_codes[2] : 0);
|
|
|
|
break;
|
|
}
|
|
#endif
|
|
stop_info_sp =
|
|
StopInfo::CreateStopReasonWithException(*this, "EXC_BAD_ACCESS");
|
|
} break;
|
|
default:
|
|
return ScriptedInterface::ErrorWithMessage<bool>(
|
|
LLVM_PRETTY_FUNCTION,
|
|
llvm::Twine("Unsupported stop reason type (" +
|
|
llvm::Twine(stop_reason_type) + llvm::Twine(")."))
|
|
.str(),
|
|
error, LLDBLog::Thread);
|
|
}
|
|
|
|
if (!stop_info_sp)
|
|
return false;
|
|
|
|
SetStopInfo(stop_info_sp);
|
|
return true;
|
|
}
|
|
|
|
void ScriptedThread::RefreshStateAfterStop() {
|
|
GetRegisterContext()->InvalidateIfNeeded(/*force=*/false);
|
|
LoadArtificialStackFrames();
|
|
}
|
|
|
|
lldb::ScriptedThreadInterfaceSP ScriptedThread::GetInterface() const {
|
|
return m_scripted_thread_interface_sp;
|
|
}
|
|
|
|
std::shared_ptr<DynamicRegisterInfo> ScriptedThread::GetDynamicRegisterInfo() {
|
|
CheckInterpreterAndScriptObject();
|
|
|
|
if (!m_register_info_sp) {
|
|
StructuredData::DictionarySP reg_info = GetInterface()->GetRegisterInfo();
|
|
|
|
Status error;
|
|
if (!reg_info)
|
|
return ScriptedInterface::ErrorWithMessage<
|
|
std::shared_ptr<DynamicRegisterInfo>>(
|
|
LLVM_PRETTY_FUNCTION, "Failed to get scripted thread registers info.",
|
|
error, LLDBLog::Thread);
|
|
|
|
m_register_info_sp = DynamicRegisterInfo::Create(
|
|
*reg_info, m_scripted_process.GetTarget().GetArchitecture());
|
|
}
|
|
|
|
return m_register_info_sp;
|
|
}
|
|
|
|
StructuredData::ObjectSP ScriptedThread::FetchThreadExtendedInfo() {
|
|
CheckInterpreterAndScriptObject();
|
|
|
|
Status error;
|
|
StructuredData::ArraySP extended_info_sp = GetInterface()->GetExtendedInfo();
|
|
|
|
if (!extended_info_sp || !extended_info_sp->GetSize())
|
|
return ScriptedInterface::ErrorWithMessage<StructuredData::ObjectSP>(
|
|
LLVM_PRETTY_FUNCTION, "No extended information found", error);
|
|
|
|
return extended_info_sp;
|
|
}
|