Handle accelerated ObjC Class method dispatchs. (#188392)
On Darwin, clang added a new objc class method dispatch, turned on by the flag: `-fobjc-msgsend-class-selector-stubs`. These stubs are like the accelerated stubs added previously for method selectors. This patch adds support for stepping through them. I had previously handled the method stubs by figuring out the object -> class and then used the selector encoded in the stub name. But that depended on all the stubs setting the object into arg1 before calling the stub, which not all the stubs do anymore. Since all the stubs do some work and then call objc_msgSend, instead of trying to predict what the stub is going to do, I just let it run to the objc_msgSend and then figure out how to go from there - at that point arg1 and arg2 are correctly set, so I can use the regular objc_msgSend trampoline for that. That change meant I was no longer using the "find the implementation" method that was passing in the selector string that we pulled from the stub name, so this patch also removes that.
This commit is contained in:
parent
1d48918ef2
commit
3c8e9499ac
@ -1186,6 +1186,16 @@ def skipUnlessBoundsSafety(func):
|
||||
return skipTestIfFn(is_compiler_with_bounds_safety)(func)
|
||||
|
||||
|
||||
def skipUnlessCompilerSupports(flag):
|
||||
"""Decorate the item to skip the test unless the compiler supports this flag."""
|
||||
|
||||
def does_compiler_support_flag():
|
||||
if not _compiler_supports(lldbplatformutil.getCompiler(), flag):
|
||||
return f"Compiler does not support flag {flag}"
|
||||
return None
|
||||
|
||||
return skipTestIfFn(does_compiler_support_flag)
|
||||
|
||||
def skipIfAsan(func):
|
||||
"""Skip this test if the environment is set up to run LLDB *itself* under ASAN."""
|
||||
return skipTestIfFn(is_running_under_asan)(func)
|
||||
|
||||
@ -803,8 +803,6 @@ AppleObjCTrampolineHandler::GetStepThroughDispatchPlan(Thread &thread,
|
||||
// one of these stubs, we strip off the selector string and pass that to the
|
||||
// implementation finder function, which looks up the SEL (you have to do this
|
||||
// in process) and passes that to the runtime lookup function.
|
||||
DispatchFunction sel_stub_dispatch = {"sel-specific-stub", false, false,
|
||||
false, DispatchFunction::eFixUpNone};
|
||||
|
||||
// First step is to see if we're in a selector-specific dispatch stub.
|
||||
// Those are of the form _objc_msgSend$<SELECTOR>, so see if the current
|
||||
@ -818,19 +816,24 @@ AppleObjCTrampolineHandler::GetStepThroughDispatchPlan(Thread &thread,
|
||||
const Symbol *curr_sym = func_addr.CalculateSymbolContextSymbol();
|
||||
if (curr_sym)
|
||||
sym_name = curr_sym->GetName().GetStringRef();
|
||||
|
||||
if (!sym_name.empty() && !sym_name.consume_front("objc_msgSend$"))
|
||||
sym_name = {};
|
||||
else
|
||||
this_dispatch = &sel_stub_dispatch;
|
||||
}
|
||||
bool in_selector_stub = !sym_name.empty();
|
||||
|
||||
// objc has introduced new accelerated dispatch stubs which figure out the
|
||||
// selector and in some cases the object in one way or another, then call
|
||||
// objc_msgSend. If we're in one of those stubs, we can use "step through
|
||||
// direct dispatch" plan to get to the actual dispatch.
|
||||
if (!sym_name.empty() && (sym_name.consume_front("objc_msgSend$")
|
||||
|| sym_name.consume_front("objc_msgSendClass$"))) {
|
||||
ret_plan_sp = std::make_shared<AppleThreadPlanStepThroughDirectDispatch>(
|
||||
thread, *this);
|
||||
return ret_plan_sp;
|
||||
}
|
||||
|
||||
// Second step is to look and see if we are in one of the known ObjC
|
||||
// dispatch functions. We've already compiled a table of same, so
|
||||
// consult it.
|
||||
|
||||
if (!in_selector_stub)
|
||||
this_dispatch = FindDispatchFunction(curr_pc);
|
||||
this_dispatch = FindDispatchFunction(curr_pc);
|
||||
|
||||
// Next check to see if we are in a vtable region:
|
||||
|
||||
@ -882,17 +885,11 @@ AppleObjCTrampolineHandler::GetStepThroughDispatchPlan(Thread &thread,
|
||||
int obj_index;
|
||||
int sel_index;
|
||||
|
||||
// If this is a selector-specific stub then just push one value, 'cause
|
||||
// we only get the object.
|
||||
// If this is a struct return dispatch, then the first argument is
|
||||
// the return struct pointer, and the object is the second, and
|
||||
// the selector is the third.
|
||||
// Otherwise the object is the first and the selector the second.
|
||||
if (in_selector_stub) {
|
||||
obj_index = 0;
|
||||
sel_index = 1;
|
||||
argument_values.PushValue(void_ptr_value);
|
||||
} else if (this_dispatch->stret_return) {
|
||||
if (this_dispatch->stret_return) {
|
||||
obj_index = 1;
|
||||
sel_index = 2;
|
||||
argument_values.PushValue(void_ptr_value);
|
||||
@ -926,10 +923,9 @@ AppleObjCTrampolineHandler::GetStepThroughDispatchPlan(Thread &thread,
|
||||
|
||||
lldb::addr_t isa_addr = LLDB_INVALID_ADDRESS;
|
||||
lldb::addr_t sel_addr = LLDB_INVALID_ADDRESS;
|
||||
// If we are not in a selector stub, get the sel address from the arguments.
|
||||
if (!in_selector_stub)
|
||||
sel_addr =
|
||||
argument_values.GetValueAtIndex(sel_index)->GetScalar().ULongLong();
|
||||
// Get the sel address from the arguments.
|
||||
sel_addr =
|
||||
argument_values.GetValueAtIndex(sel_index)->GetScalar().ULongLong();
|
||||
|
||||
// Figure out the class this is being dispatched to and see if
|
||||
// we've already cached this method call, If so we can push a
|
||||
@ -1011,15 +1007,9 @@ AppleObjCTrampolineHandler::GetStepThroughDispatchPlan(Thread &thread,
|
||||
ObjCLanguageRuntime *objc_runtime =
|
||||
ObjCLanguageRuntime::Get(*thread.GetProcess());
|
||||
assert(objc_runtime != nullptr);
|
||||
if (!in_selector_stub) {
|
||||
LLDB_LOG(log, "Resolving call for class - {0} and selector - {1}",
|
||||
isa_addr, sel_addr);
|
||||
impl_addr = objc_runtime->LookupInMethodCache(isa_addr, sel_addr);
|
||||
} else {
|
||||
LLDB_LOG(log, "Resolving call for class - {0} and selector - {1}",
|
||||
isa_addr, sym_name);
|
||||
impl_addr = objc_runtime->LookupInMethodCache(isa_addr, sym_name);
|
||||
}
|
||||
LLDB_LOG(log, "Resolving call for class - {0} and selector - {1}",
|
||||
isa_addr, sel_addr);
|
||||
impl_addr = objc_runtime->LookupInMethodCache(isa_addr, sel_addr);
|
||||
}
|
||||
// If it is a selector-specific stub dispatch, look in the string cache:
|
||||
|
||||
@ -1058,46 +1048,18 @@ AppleObjCTrampolineHandler::GetStepThroughDispatchPlan(Thread &thread,
|
||||
|
||||
dispatch_values.PushValue(*(argument_values.GetValueAtIndex(obj_index)));
|
||||
lldb::addr_t sel_str_addr = LLDB_INVALID_ADDRESS;
|
||||
if (!in_selector_stub) {
|
||||
// If we don't have a selector string, push the selector from arguments.
|
||||
dispatch_values.PushValue(
|
||||
*(argument_values.GetValueAtIndex(sel_index)));
|
||||
} else {
|
||||
// Otherwise, inject the string into the target, and push that value for
|
||||
// the sel argument.
|
||||
Status error;
|
||||
sel_str_addr = process_sp->AllocateMemory(
|
||||
sym_name.size() + 1, ePermissionsReadable | ePermissionsWritable,
|
||||
error);
|
||||
if (sel_str_addr == LLDB_INVALID_ADDRESS || error.Fail()) {
|
||||
LLDB_LOG(log,
|
||||
"Could not allocate memory for selector string {0}: {1}",
|
||||
sym_name, error);
|
||||
return ret_plan_sp;
|
||||
}
|
||||
process_sp->WriteMemory(sel_str_addr, sym_name.str().c_str(),
|
||||
sym_name.size() + 1, error);
|
||||
if (error.Fail()) {
|
||||
LLDB_LOG(log, "Could not write string to address {0}", sel_str_addr);
|
||||
return ret_plan_sp;
|
||||
}
|
||||
Value sel_ptr_value(void_ptr_value);
|
||||
sel_ptr_value.GetScalar() = sel_str_addr;
|
||||
dispatch_values.PushValue(sel_ptr_value);
|
||||
}
|
||||
// Push the selector from arguments.
|
||||
dispatch_values.PushValue(*(argument_values.GetValueAtIndex(sel_index)));
|
||||
|
||||
Value flag_value;
|
||||
CompilerType clang_int_type =
|
||||
scratch_ts_sp->GetBuiltinTypeForEncodingAndBitSize(
|
||||
lldb::eEncodingSint, 32);
|
||||
flag_value.SetValueType(Value::ValueType::Scalar);
|
||||
// flag_value.SetContext (Value::eContextTypeClangType, clang_int_type);
|
||||
flag_value.SetCompilerType(clang_int_type);
|
||||
|
||||
if (in_selector_stub)
|
||||
flag_value.GetScalar() = 1;
|
||||
else
|
||||
flag_value.GetScalar() = 0;
|
||||
// We are passing in a sel addr now a string pointer in all cases for now.
|
||||
flag_value.GetScalar() = 0;
|
||||
dispatch_values.PushValue(flag_value);
|
||||
|
||||
if (this_dispatch->stret_return)
|
||||
@ -1140,7 +1102,7 @@ AppleObjCTrampolineHandler::GetStepThroughDispatchPlan(Thread &thread,
|
||||
if (log && log->GetVerbose())
|
||||
flag_value.GetScalar() = 1;
|
||||
else
|
||||
flag_value.GetScalar() = 0; // FIXME - Set to 0 when debugging is done.
|
||||
flag_value.GetScalar() = 0;
|
||||
dispatch_values.PushValue(flag_value);
|
||||
|
||||
ret_plan_sp = std::make_shared<AppleThreadPlanStepThroughObjCTrampoline>(
|
||||
@ -1153,20 +1115,19 @@ AppleObjCTrampolineHandler::GetStepThroughDispatchPlan(Thread &thread,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, check if we have hit an "optimized dispatch" function. This will
|
||||
|
||||
// Next, check if we have hit an "optimized dispatch" function. This will
|
||||
// either directly call the base implementation or dispatch an objc_msgSend
|
||||
// if the method has been overridden. So we just do a "step in/step out",
|
||||
// setting a breakpoint on objc_msgSend, and if we hit the msgSend, we
|
||||
// will automatically step in again. That's the job of the
|
||||
// setting a breakpoint on objc_msgSend, and if we hit the msgSend, we
|
||||
// will automatically step in again. That's the job of the
|
||||
// AppleThreadPlanStepThroughDirectDispatch.
|
||||
if (!this_dispatch && !ret_plan_sp) {
|
||||
MsgsendMap::iterator pos;
|
||||
pos = m_opt_dispatch_map.find(curr_pc);
|
||||
if (pos != m_opt_dispatch_map.end()) {
|
||||
const char *opt_name = g_opt_dispatch_names[(*pos).second];
|
||||
ret_plan_sp = std::make_shared<AppleThreadPlanStepThroughDirectDispatch>(
|
||||
thread, *this, opt_name);
|
||||
thread, *this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -240,8 +240,7 @@ bool AppleThreadPlanStepThroughObjCTrampoline::WillStop() { return true; }
|
||||
|
||||
AppleThreadPlanStepThroughDirectDispatch ::
|
||||
AppleThreadPlanStepThroughDirectDispatch(
|
||||
Thread &thread, AppleObjCTrampolineHandler &handler,
|
||||
llvm::StringRef dispatch_func_name)
|
||||
Thread &thread, AppleObjCTrampolineHandler &handler)
|
||||
: ThreadPlanStepOut(thread, nullptr, true /* first instruction */, false,
|
||||
eVoteNoOpinion, eVoteNoOpinion,
|
||||
0 /* Step out of zeroth frame */,
|
||||
@ -250,9 +249,7 @@ AppleThreadPlanStepThroughDirectDispatch ::
|
||||
,
|
||||
true /* Run to branch for inline step out */,
|
||||
false /* Don't gather the return value */),
|
||||
m_trampoline_handler(handler),
|
||||
m_dispatch_func_name(std::string(dispatch_func_name)),
|
||||
m_at_msg_send(false) {
|
||||
m_trampoline_handler(handler), m_at_msg_send(false) {
|
||||
// Set breakpoints on the dispatch functions:
|
||||
auto bkpt_callback = [&] (lldb::addr_t addr,
|
||||
const AppleObjCTrampolineHandler
|
||||
@ -290,8 +287,7 @@ void AppleThreadPlanStepThroughDirectDispatch::GetDescription(
|
||||
s->PutCString("Step through ObjC direct dispatch function.");
|
||||
break;
|
||||
default:
|
||||
s->Printf("Step through ObjC direct dispatch '%s' using breakpoints: ",
|
||||
m_dispatch_func_name.c_str());
|
||||
s->Printf("Step through ObjC direct dispatch using breakpoints: ");
|
||||
bool first = true;
|
||||
for (auto bkpt_sp : m_msgSend_bkpts) {
|
||||
if (!first) {
|
||||
|
||||
@ -83,8 +83,7 @@ private:
|
||||
class AppleThreadPlanStepThroughDirectDispatch: public ThreadPlanStepOut {
|
||||
public:
|
||||
AppleThreadPlanStepThroughDirectDispatch(Thread &thread,
|
||||
AppleObjCTrampolineHandler &handler,
|
||||
llvm::StringRef dispatch_func_name);
|
||||
AppleObjCTrampolineHandler &handler);
|
||||
|
||||
~AppleThreadPlanStepThroughDirectDispatch() override;
|
||||
|
||||
@ -106,8 +105,6 @@ protected:
|
||||
bool DoPlanExplainsStop(Event *event_ptr) override;
|
||||
|
||||
AppleObjCTrampolineHandler &m_trampoline_handler;
|
||||
std::string m_dispatch_func_name; /// Which dispatch function we're stepping
|
||||
/// through.
|
||||
lldb::ThreadPlanSP m_objc_step_through_sp; /// When we hit an objc_msgSend,
|
||||
/// we'll use this plan to get to
|
||||
/// its target.
|
||||
|
||||
@ -11,40 +11,41 @@ class TestObjCClassMethod(TestBase):
|
||||
# Call super's setUp().
|
||||
TestBase.setUp(self)
|
||||
# Find the line numbers to break inside main().
|
||||
self.main_source = "class.m"
|
||||
self.break_line = line_number(self.main_source, "// Set breakpoint here.")
|
||||
self.main_source = lldb.SBFileSpec("class.m")
|
||||
|
||||
SHARED_BUILD_TESTCASE = False
|
||||
NO_DEBUG_INFO_TESTCASE = True
|
||||
|
||||
@skipUnlessDarwin
|
||||
@skipUnlessCompilerSupports("-fobjc-msgsend-class-selector-stubs")
|
||||
@add_test_categories(["pyapi"])
|
||||
def test_with_python_api(self):
|
||||
def test_without_class_stubs(self):
|
||||
self.do_test_with_python_api("-fno-objc-msgsend-class-selector-stubs")
|
||||
|
||||
@skipUnlessDarwin
|
||||
@skipUnlessCompilerSupports("-fobjc-msgsend-class-selector-stubs")
|
||||
@add_test_categories(["pyapi"])
|
||||
def test_using_class_stubs(self):
|
||||
self.do_test_with_python_api("-fobjc-msgsend-class-selector-stubs")
|
||||
|
||||
def do_test_with_python_api(self, compiler_flags):
|
||||
"""Test calling functions in class methods."""
|
||||
self.build()
|
||||
exe = self.getBuildArtifact("a.out")
|
||||
d = {}
|
||||
if len(compiler_flags):
|
||||
d["CFLAGS_EXTRAS"] = compiler_flags
|
||||
|
||||
target = self.dbg.CreateTarget(exe)
|
||||
self.assertTrue(target, VALID_TARGET)
|
||||
self.build(dictionary=d)
|
||||
|
||||
bpt = target.BreakpointCreateByLocation(self.main_source, self.break_line)
|
||||
self.assertTrue(bpt, VALID_BREAKPOINT)
|
||||
|
||||
# Now launch the process, and do not stop at entry point.
|
||||
process = target.LaunchSimple(None, None, self.get_process_working_directory())
|
||||
|
||||
self.assertTrue(process, PROCESS_IS_VALID)
|
||||
|
||||
# The stop reason of the thread should be breakpoint.
|
||||
thread_list = lldbutil.get_threads_stopped_at_breakpoint(process, bpt)
|
||||
|
||||
# Make sure we stopped at the first breakpoint.
|
||||
self.assertNotEqual(len(thread_list), 0, "No thread stopped at our breakpoint.")
|
||||
self.assertEqual(
|
||||
len(thread_list), 1, "More than one thread stopped at our breakpoint."
|
||||
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
|
||||
self, "Set a breakpoint here", self.main_source
|
||||
)
|
||||
|
||||
# Now make sure we can call a function in the class method we've
|
||||
# stopped in.
|
||||
frame = thread_list[0].GetFrameAtIndex(0)
|
||||
frame = thread.GetFrameAtIndex(0)
|
||||
self.assertTrue(frame, "Got a valid frame 0 frame.")
|
||||
|
||||
# First check that we can call a class method:
|
||||
cmd_value = frame.EvaluateExpression(
|
||||
'(int)[Foo doSomethingWithString:@"Hello"]'
|
||||
)
|
||||
@ -54,3 +55,10 @@ class TestObjCClassMethod(TestBase):
|
||||
print("cmd_value has the value %d" % cmd_value.GetValueAsUnsigned())
|
||||
self.assertTrue(cmd_value.IsValid())
|
||||
self.assertEqual(cmd_value.GetValueAsUnsigned(), 5)
|
||||
|
||||
# Now check that we can step INTO class methods:
|
||||
thread.StepInto()
|
||||
frame = thread.GetFrameAtIndex(0)
|
||||
self.assertEqual(
|
||||
frame.name, "+[Foo doSomethingWithString:]", "Stopped in class method"
|
||||
)
|
||||
|
||||
@ -20,5 +20,6 @@
|
||||
|
||||
int main()
|
||||
{
|
||||
return 0; // Set breakpoint here.
|
||||
[Foo doSomethingWithString:@"Set a breakpoint here"];
|
||||
return 0;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user