
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, butt 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 ThreadPlanStepInstruction 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. 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. I first landed this patch in July 2024 via https://github.com/llvm/llvm-project/pull/96260 but the CI bots and wider testing found a number of test case failures that needed to be updated, I reverted it. I've fixed all of those issues in separate PRs and this change should run cleanly on all the CI bots now. 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;
|
|
}
|