mirror of
https://github.com/capstone-engine/llvm-capstone.git
synced 2025-02-04 16:26:46 +00:00
Adapt the ObjC stepping algorithm to deal with "selector-stubs" in clang.
Clang is adding a feature to ObjC code generation, where instead of calling objc_msgSend directly with an object & selector, it generates a stub that gets passed only the object and the stub figures out the selector. This patch adds support for following that dispatch method into the implementation function.
This commit is contained in:
parent
2a2149c754
commit
833882b327
@ -1554,7 +1554,7 @@ AppleObjCRuntimeV2::DynamicClassInfoExtractor::GetClassInfoUtilityFunctionImpl(
|
||||
if (!utility_fn_or_error) {
|
||||
LLDB_LOG_ERROR(
|
||||
log, utility_fn_or_error.takeError(),
|
||||
"Failed to get utility function for implementation lookup: {0}");
|
||||
"Failed to get utility function for dynamic info extractor: {0}");
|
||||
return {};
|
||||
}
|
||||
|
||||
@ -1684,7 +1684,7 @@ AppleObjCRuntimeV2::SharedCacheClassInfoExtractor::
|
||||
if (!utility_fn_or_error) {
|
||||
LLDB_LOG_ERROR(
|
||||
log, utility_fn_or_error.takeError(),
|
||||
"Failed to get utility function for implementation lookup: {0}");
|
||||
"Failed to get utility function for shared class info extractor: {0}");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,7 @@
|
||||
#include "lldb/Utility/Log.h"
|
||||
|
||||
#include "llvm/ADT/STLExtras.h"
|
||||
#include "llvm/ADT/ScopeExit.h"
|
||||
|
||||
#include "Plugins/LanguageRuntime/ObjC/ObjCLanguageRuntime.h"
|
||||
|
||||
@ -45,239 +46,138 @@ const char *AppleObjCTrampolineHandler::g_lookup_implementation_function_name =
|
||||
"__lldb_objc_find_implementation_for_selector";
|
||||
const char *AppleObjCTrampolineHandler::
|
||||
g_lookup_implementation_with_stret_function_code =
|
||||
" \n\
|
||||
extern \"C\" \n\
|
||||
{ \n\
|
||||
extern void *class_getMethodImplementation(void *objc_class, void *sel); \n\
|
||||
extern void *class_getMethodImplementation_stret(void *objc_class, \n\
|
||||
void *sel); \n\
|
||||
extern void * object_getClass (id object); \n\
|
||||
extern void * sel_getUid(char *name); \n\
|
||||
extern int printf(const char *format, ...); \n\
|
||||
} \n\
|
||||
extern \"C\" void * __lldb_objc_find_implementation_for_selector ( \n\
|
||||
void *object, \n\
|
||||
void *sel, \n\
|
||||
int is_stret, \n\
|
||||
int is_super, \n\
|
||||
int is_super2, \n\
|
||||
int is_fixup, \n\
|
||||
int is_fixed, \n\
|
||||
int debug) \n\
|
||||
{ \n\
|
||||
struct __lldb_imp_return_struct \n\
|
||||
{ \n\
|
||||
void *class_addr; \n\
|
||||
void *sel_addr; \n\
|
||||
void *impl_addr; \n\
|
||||
}; \n\
|
||||
\n\
|
||||
struct __lldb_objc_class { \n\
|
||||
void *isa; \n\
|
||||
void *super_ptr; \n\
|
||||
}; \n\
|
||||
struct __lldb_objc_super { \n\
|
||||
void *receiver; \n\
|
||||
struct __lldb_objc_class *class_ptr; \n\
|
||||
}; \n\
|
||||
struct __lldb_msg_ref { \n\
|
||||
void *dont_know; \n\
|
||||
void *sel; \n\
|
||||
}; \n\
|
||||
\n\
|
||||
struct __lldb_imp_return_struct return_struct; \n\
|
||||
\n\
|
||||
if (debug) \n\
|
||||
printf (\"\\n*** Called with obj: 0x%p sel: 0x%p is_stret: %d is_super: %d, \"\n\
|
||||
\"is_super2: %d, is_fixup: %d, is_fixed: %d\\n\", \n\
|
||||
object, sel, is_stret, is_super, is_super2, is_fixup, is_fixed);\n\
|
||||
if (is_super) \n\
|
||||
{ \n\
|
||||
if (is_super2) \n\
|
||||
{ \n\
|
||||
return_struct.class_addr = ((__lldb_objc_super *) object)->class_ptr->super_ptr;\n\
|
||||
} \n\
|
||||
else \n\
|
||||
{ \n\
|
||||
return_struct.class_addr = ((__lldb_objc_super *) object)->class_ptr;\n\
|
||||
} \n\
|
||||
} \n\
|
||||
else \n\
|
||||
{ \n\
|
||||
// This code seems a little funny, but has its reasons... \n\
|
||||
\n\
|
||||
// The call to [object class] is here because if this is a \n\
|
||||
// class, and has not been called into yet, we need to do \n\
|
||||
// something to force the class to initialize itself. \n\
|
||||
// Then the call to object_getClass will actually return the \n\
|
||||
// correct class, either the class if object is a class \n\
|
||||
// instance, or the meta-class if it is a class pointer. \n\
|
||||
void *class_ptr = (void *) [(id) object class]; \n\
|
||||
return_struct.class_addr = (id) object_getClass((id) object); \n\
|
||||
if (debug) \n\
|
||||
{ \n\
|
||||
if (class_ptr == object) \n\
|
||||
{ \n\
|
||||
printf (\"Found a class object, need to use the meta class %p -> %p\\n\",\n\
|
||||
class_ptr, return_struct.class_addr); \n\
|
||||
} \n\
|
||||
else \n\
|
||||
{ \n\
|
||||
printf (\"[object class] returned: %p object_getClass: %p.\\n\", \n\
|
||||
class_ptr, return_struct.class_addr); \n\
|
||||
} \n\
|
||||
} \n\
|
||||
} \n\
|
||||
\n\
|
||||
if (is_fixup) \n\
|
||||
{ \n\
|
||||
if (is_fixed) \n\
|
||||
{ \n\
|
||||
return_struct.sel_addr = ((__lldb_msg_ref *) sel)->sel; \n\
|
||||
} \n\
|
||||
else \n\
|
||||
{ \n\
|
||||
char *sel_name = (char *) ((__lldb_msg_ref *) sel)->sel; \n\
|
||||
return_struct.sel_addr = sel_getUid (sel_name); \n\
|
||||
if (debug) \n\
|
||||
printf (\"\\n*** Got fixed up selector: %p for name %s.\\n\",\n\
|
||||
return_struct.sel_addr, sel_name); \n\
|
||||
} \n\
|
||||
} \n\
|
||||
else \n\
|
||||
{ \n\
|
||||
return_struct.sel_addr = sel; \n\
|
||||
} \n\
|
||||
\n\
|
||||
if (is_stret) \n\
|
||||
{ \n\
|
||||
return_struct.impl_addr = \n\
|
||||
class_getMethodImplementation_stret (return_struct.class_addr, \n\
|
||||
return_struct.sel_addr); \n\
|
||||
} \n\
|
||||
else \n\
|
||||
{ \n\
|
||||
return_struct.impl_addr = \n\
|
||||
class_getMethodImplementation (return_struct.class_addr, \n\
|
||||
return_struct.sel_addr); \n\
|
||||
} \n\
|
||||
if (debug) \n\
|
||||
printf (\"\\n*** Returning implementation: %p.\\n\", \n\
|
||||
return_struct.impl_addr); \n\
|
||||
\n\
|
||||
return return_struct.impl_addr; \n\
|
||||
} \n\
|
||||
";
|
||||
R"(
|
||||
if (is_stret) {
|
||||
return_struct.impl_addr =
|
||||
class_getMethodImplementation_stret (return_struct.class_addr,
|
||||
return_struct.sel_addr);
|
||||
} else {
|
||||
return_struct.impl_addr =
|
||||
class_getMethodImplementation (return_struct.class_addr,
|
||||
return_struct.sel_addr);
|
||||
}
|
||||
if (debug)
|
||||
printf ("\n*** Returning implementation: %p.\n",
|
||||
return_struct.impl_addr);
|
||||
|
||||
return return_struct.impl_addr;
|
||||
}
|
||||
)";
|
||||
const char *
|
||||
AppleObjCTrampolineHandler::g_lookup_implementation_no_stret_function_code =
|
||||
" \n\
|
||||
extern \"C\" \n\
|
||||
{ \n\
|
||||
extern void *class_getMethodImplementation(void *objc_class, void *sel); \n\
|
||||
extern void * object_getClass (id object); \n\
|
||||
extern void * sel_getUid(char *name); \n\
|
||||
extern int printf(const char *format, ...); \n\
|
||||
} \n\
|
||||
extern \"C\" void * __lldb_objc_find_implementation_for_selector (void *object, \n\
|
||||
void *sel, \n\
|
||||
int is_stret, \n\
|
||||
int is_super, \n\
|
||||
int is_super2, \n\
|
||||
int is_fixup, \n\
|
||||
int is_fixed, \n\
|
||||
int debug) \n\
|
||||
{ \n\
|
||||
struct __lldb_imp_return_struct \n\
|
||||
{ \n\
|
||||
void *class_addr; \n\
|
||||
void *sel_addr; \n\
|
||||
void *impl_addr; \n\
|
||||
}; \n\
|
||||
\n\
|
||||
struct __lldb_objc_class { \n\
|
||||
void *isa; \n\
|
||||
void *super_ptr; \n\
|
||||
}; \n\
|
||||
struct __lldb_objc_super { \n\
|
||||
void *receiver; \n\
|
||||
struct __lldb_objc_class *class_ptr; \n\
|
||||
}; \n\
|
||||
struct __lldb_msg_ref { \n\
|
||||
void *dont_know; \n\
|
||||
void *sel; \n\
|
||||
}; \n\
|
||||
\n\
|
||||
struct __lldb_imp_return_struct return_struct; \n\
|
||||
\n\
|
||||
if (debug) \n\
|
||||
printf (\"\\n*** Called with obj: 0x%p sel: 0x%p is_stret: %d is_super: %d, \" \n\
|
||||
\"is_super2: %d, is_fixup: %d, is_fixed: %d\\n\", \n\
|
||||
object, sel, is_stret, is_super, is_super2, is_fixup, is_fixed); \n\
|
||||
if (is_super) \n\
|
||||
{ \n\
|
||||
if (is_super2) \n\
|
||||
{ \n\
|
||||
return_struct.class_addr = ((__lldb_objc_super *) object)->class_ptr->super_ptr; \n\
|
||||
} \n\
|
||||
else \n\
|
||||
{ \n\
|
||||
return_struct.class_addr = ((__lldb_objc_super *) object)->class_ptr; \n\
|
||||
} \n\
|
||||
} \n\
|
||||
else \n\
|
||||
{ \n\
|
||||
// This code seems a little funny, but has its reasons... \n\
|
||||
// The call to [object class] is here because if this is a class, and has not been called into \n\
|
||||
// yet, we need to do something to force the class to initialize itself. \n\
|
||||
// Then the call to object_getClass will actually return the correct class, either the class \n\
|
||||
// if object is a class instance, or the meta-class if it is a class pointer. \n\
|
||||
void *class_ptr = (void *) [(id) object class]; \n\
|
||||
return_struct.class_addr = (id) object_getClass((id) object); \n\
|
||||
if (debug) \n\
|
||||
{ \n\
|
||||
if (class_ptr == object) \n\
|
||||
{ \n\
|
||||
printf (\"Found a class object, need to return the meta class %p -> %p\\n\", \n\
|
||||
class_ptr, return_struct.class_addr); \n\
|
||||
} \n\
|
||||
else \n\
|
||||
{ \n\
|
||||
printf (\"[object class] returned: %p object_getClass: %p.\\n\", \n\
|
||||
class_ptr, return_struct.class_addr); \n\
|
||||
} \n\
|
||||
} \n\
|
||||
} \n\
|
||||
\n\
|
||||
if (is_fixup) \n\
|
||||
{ \n\
|
||||
if (is_fixed) \n\
|
||||
{ \n\
|
||||
return_struct.sel_addr = ((__lldb_msg_ref *) sel)->sel; \n\
|
||||
} \n\
|
||||
else \n\
|
||||
{ \n\
|
||||
char *sel_name = (char *) ((__lldb_msg_ref *) sel)->sel; \n\
|
||||
return_struct.sel_addr = sel_getUid (sel_name); \n\
|
||||
if (debug) \n\
|
||||
printf (\"\\n*** Got fixed up selector: %p for name %s.\\n\",\n\
|
||||
return_struct.sel_addr, sel_name); \n\
|
||||
} \n\
|
||||
} \n\
|
||||
else \n\
|
||||
{ \n\
|
||||
return_struct.sel_addr = sel; \n\
|
||||
} \n\
|
||||
\n\
|
||||
return_struct.impl_addr = \n\
|
||||
class_getMethodImplementation (return_struct.class_addr, \n\
|
||||
return_struct.sel_addr); \n\
|
||||
if (debug) \n\
|
||||
printf (\"\\n*** Returning implementation: 0x%p.\\n\", \n\
|
||||
return_struct.impl_addr); \n\
|
||||
\n\
|
||||
return return_struct.impl_addr; \n\
|
||||
} \n\
|
||||
";
|
||||
R"(
|
||||
return_struct.impl_addr =
|
||||
class_getMethodImplementation (return_struct.class_addr,
|
||||
return_struct.sel_addr);
|
||||
if (debug)
|
||||
printf ("\n*** getMethodImpletation for addr: 0x%p sel: 0x%p result: 0x%p.\n",
|
||||
return_struct.class_addr, return_struct.sel_addr, return_struct.impl_addr);
|
||||
|
||||
return return_struct.impl_addr;
|
||||
}
|
||||
)";
|
||||
|
||||
const char
|
||||
*AppleObjCTrampolineHandler::g_lookup_implementation_function_common_code =
|
||||
R"(
|
||||
extern "C"
|
||||
{
|
||||
extern void *class_getMethodImplementation(void *objc_class, void *sel);
|
||||
extern void *class_getMethodImplementation_stret(void *objc_class, void *sel);
|
||||
extern void * object_getClass (id object);
|
||||
extern void * sel_getUid(char *name);
|
||||
extern int printf(const char *format, ...);
|
||||
}
|
||||
extern "C" void *
|
||||
__lldb_objc_find_implementation_for_selector (void *object,
|
||||
void *sel,
|
||||
int is_str_ptr,
|
||||
int is_stret,
|
||||
int is_super,
|
||||
int is_super2,
|
||||
int is_fixup,
|
||||
int is_fixed,
|
||||
int debug)
|
||||
{
|
||||
struct __lldb_imp_return_struct {
|
||||
void *class_addr;
|
||||
void *sel_addr;
|
||||
void *impl_addr;
|
||||
};
|
||||
|
||||
struct __lldb_objc_class {
|
||||
void *isa;
|
||||
void *super_ptr;
|
||||
};
|
||||
struct __lldb_objc_super {
|
||||
void *receiver;
|
||||
struct __lldb_objc_class *class_ptr;
|
||||
};
|
||||
struct __lldb_msg_ref {
|
||||
void *dont_know;
|
||||
void *sel;
|
||||
};
|
||||
|
||||
struct __lldb_imp_return_struct return_struct;
|
||||
|
||||
if (debug)
|
||||
printf ("\n*** Called with obj: %p sel: %p is_str_ptr: %d "
|
||||
"is_stret: %d is_super: %d, "
|
||||
"is_super2: %d, is_fixup: %d, is_fixed: %d\n",
|
||||
object, sel, is_str_ptr, is_stret,
|
||||
is_super, is_super2, is_fixup, is_fixed);
|
||||
|
||||
if (is_str_ptr) {
|
||||
if (debug)
|
||||
printf("*** Turning string: '%s'", sel);
|
||||
sel = sel_getUid((char *)sel);
|
||||
if (debug)
|
||||
printf("*** into sel to %p", sel);
|
||||
}
|
||||
if (is_super) {
|
||||
if (is_super2) {
|
||||
return_struct.class_addr
|
||||
= ((__lldb_objc_super *) object)->class_ptr->super_ptr;
|
||||
} else {
|
||||
return_struct.class_addr = ((__lldb_objc_super *) object)->class_ptr;
|
||||
}
|
||||
if (debug)
|
||||
printf("*** Super, class addr: %p\n", return_struct.class_addr);
|
||||
} else {
|
||||
// This code seems a little funny, but has its reasons...
|
||||
// The call to [object class] is here because if this is a class, and has
|
||||
// not been called into yet, we need to do something to force the class to
|
||||
// initialize itself.
|
||||
// Then the call to object_getClass will actually return the correct class,
|
||||
// either the class if object is a class instance, or the meta-class if it
|
||||
// is a class pointer.
|
||||
void *class_ptr = (void *) [(id) object class];
|
||||
return_struct.class_addr = (id) object_getClass((id) object);
|
||||
if (debug) {
|
||||
if (class_ptr == object) {
|
||||
printf ("Found a class object, need to return the meta class %p -> %p\n",
|
||||
class_ptr, return_struct.class_addr);
|
||||
} else {
|
||||
printf ("[object class] returned: %p object_getClass: %p.\n",
|
||||
class_ptr, return_struct.class_addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (is_fixup) {
|
||||
if (is_fixed) {
|
||||
return_struct.sel_addr = ((__lldb_msg_ref *) sel)->sel;
|
||||
} else {
|
||||
char *sel_name = (char *) ((__lldb_msg_ref *) sel)->sel;
|
||||
return_struct.sel_addr = sel_getUid (sel_name);
|
||||
if (debug)
|
||||
printf ("\n*** Got fixed up selector: %p for name %s.\n",
|
||||
return_struct.sel_addr, sel_name);
|
||||
}
|
||||
} else {
|
||||
return_struct.sel_addr = sel;
|
||||
}
|
||||
)";
|
||||
|
||||
AppleObjCTrampolineHandler::AppleObjCVTables::VTableRegion::VTableRegion(
|
||||
AppleObjCVTables *owner, lldb::addr_t header_addr)
|
||||
@ -676,7 +576,6 @@ const char *AppleObjCTrampolineHandler::g_opt_dispatch_names[] = {
|
||||
AppleObjCTrampolineHandler::AppleObjCTrampolineHandler(
|
||||
const ProcessSP &process_sp, const ModuleSP &objc_module_sp)
|
||||
: m_process_wp(), m_objc_module_sp(objc_module_sp),
|
||||
m_lookup_implementation_function_code(nullptr),
|
||||
m_impl_fn_addr(LLDB_INVALID_ADDRESS),
|
||||
m_impl_stret_fn_addr(LLDB_INVALID_ADDRESS),
|
||||
m_msg_forward_addr(LLDB_INVALID_ADDRESS) {
|
||||
@ -729,17 +628,24 @@ AppleObjCTrampolineHandler::AppleObjCTrampolineHandler(
|
||||
get_impl_name.AsCString());
|
||||
}
|
||||
return;
|
||||
} else if (m_impl_stret_fn_addr == LLDB_INVALID_ADDRESS) {
|
||||
}
|
||||
|
||||
// We will either set the implementation to the _stret or non_stret version,
|
||||
// so either way it's safe to start filling the m_lookup_..._code here.
|
||||
m_lookup_implementation_function_code.assign(
|
||||
g_lookup_implementation_function_common_code);
|
||||
|
||||
if (m_impl_stret_fn_addr == LLDB_INVALID_ADDRESS) {
|
||||
// It there is no stret return lookup function, assume that it is the same
|
||||
// as the straight lookup:
|
||||
m_impl_stret_fn_addr = m_impl_fn_addr;
|
||||
// Also we will use the version of the lookup code that doesn't rely on the
|
||||
// stret version of the function.
|
||||
m_lookup_implementation_function_code =
|
||||
g_lookup_implementation_no_stret_function_code;
|
||||
m_lookup_implementation_function_code.append(
|
||||
g_lookup_implementation_no_stret_function_code);
|
||||
} else {
|
||||
m_lookup_implementation_function_code =
|
||||
g_lookup_implementation_with_stret_function_code;
|
||||
m_lookup_implementation_function_code.append(
|
||||
g_lookup_implementation_with_stret_function_code);
|
||||
}
|
||||
|
||||
// Look up the addresses for the objc dispatch functions and cache
|
||||
@ -806,7 +712,7 @@ AppleObjCTrampolineHandler::SetupDispatchFunction(Thread &thread,
|
||||
// First stage is to make the ClangUtility to hold our injected function:
|
||||
|
||||
if (!m_impl_code) {
|
||||
if (m_lookup_implementation_function_code != nullptr) {
|
||||
if (!m_lookup_implementation_function_code.empty()) {
|
||||
auto utility_fn_or_error = exe_ctx.GetTargetRef().CreateUtilityFunction(
|
||||
m_lookup_implementation_function_code,
|
||||
g_lookup_implementation_function_name, eLanguageTypeC, exe_ctx);
|
||||
@ -891,12 +797,42 @@ AppleObjCTrampolineHandler::GetStepThroughDispatchPlan(Thread &thread,
|
||||
|
||||
DispatchFunction vtable_dispatch = {"vtable", false, false, false,
|
||||
DispatchFunction::eFixUpFixed};
|
||||
// The selector specific stubs are a wrapper for objc_msgSend. They don't get
|
||||
// passed a SEL, but instead the selector string is encoded in the stub
|
||||
// name, in the form:
|
||||
// objc_msgSend$SelectorName
|
||||
// and the stub figures out the uniqued selector. If we find ourselves in
|
||||
// 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 look and see if we are in one of the known ObjC
|
||||
// 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
|
||||
// function has that name:
|
||||
Address func_addr;
|
||||
Target &target = thread.GetProcess()->GetTarget();
|
||||
llvm::StringRef sym_name;
|
||||
const DispatchFunction *this_dispatch = nullptr;
|
||||
|
||||
if (target.ResolveLoadAddress(curr_pc, func_addr)) {
|
||||
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();
|
||||
// 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.
|
||||
|
||||
const DispatchFunction *this_dispatch = FindDispatchFunction(curr_pc);
|
||||
if (!in_selector_stub)
|
||||
this_dispatch = FindDispatchFunction(curr_pc);
|
||||
|
||||
// Next check to see if we are in a vtable region:
|
||||
|
||||
@ -910,11 +846,15 @@ AppleObjCTrampolineHandler::GetStepThroughDispatchPlan(Thread &thread,
|
||||
}
|
||||
}
|
||||
|
||||
// Since we set this_dispatch in both the vtable & sel specific stub cases
|
||||
// this if will be used for all three of those cases.
|
||||
if (this_dispatch) {
|
||||
Log *log = GetLog(LLDBLog::Step);
|
||||
|
||||
// We are decoding a method dispatch. First job is to pull the
|
||||
// arguments out:
|
||||
// arguments out. If we are in a regular stub, we get self & selector,
|
||||
// but if we are in a selector-specific stub, we'll have to get that from
|
||||
// the string sym_name.
|
||||
|
||||
lldb::StackFrameSP thread_cur_frame = thread.GetStackFrameAtIndex(0);
|
||||
|
||||
@ -944,11 +884,17 @@ 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 (this_dispatch->stret_return) {
|
||||
// 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) {
|
||||
obj_index = 1;
|
||||
sel_index = 2;
|
||||
argument_values.PushValue(void_ptr_value);
|
||||
@ -975,14 +921,16 @@ AppleObjCTrampolineHandler::GetStepThroughDispatchPlan(Thread &thread,
|
||||
}
|
||||
|
||||
ExecutionContext exe_ctx(thread.shared_from_this());
|
||||
Process *process = exe_ctx.GetProcessPtr();
|
||||
// isa_addr will store the class pointer that the method is being
|
||||
// dispatched to - so either the class directly or the super class
|
||||
// if this is one of the objc_msgSendSuper flavors. That's mostly
|
||||
// used to look up the class/selector pair in our cache.
|
||||
|
||||
lldb::addr_t isa_addr = LLDB_INVALID_ADDRESS;
|
||||
lldb::addr_t sel_addr =
|
||||
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();
|
||||
|
||||
// Figure out the class this is being dispatched to and see if
|
||||
@ -998,14 +946,14 @@ AppleObjCTrampolineHandler::GetStepThroughDispatchPlan(Thread &thread,
|
||||
// to dig the super out of the class and use that.
|
||||
|
||||
Value super_value(*(argument_values.GetValueAtIndex(obj_index)));
|
||||
super_value.GetScalar() += process->GetAddressByteSize();
|
||||
super_value.GetScalar() += process_sp->GetAddressByteSize();
|
||||
super_value.ResolveValue(&exe_ctx);
|
||||
|
||||
if (super_value.GetScalar().IsValid()) {
|
||||
|
||||
// isa_value now holds the class pointer. The second word of the
|
||||
// class pointer is the super-class pointer:
|
||||
super_value.GetScalar() += process->GetAddressByteSize();
|
||||
super_value.GetScalar() += process_sp->GetAddressByteSize();
|
||||
super_value.ResolveValue(&exe_ctx);
|
||||
if (super_value.GetScalar().IsValid())
|
||||
isa_addr = super_value.GetScalar().ULongLong();
|
||||
@ -1024,7 +972,7 @@ AppleObjCTrampolineHandler::GetStepThroughDispatchPlan(Thread &thread,
|
||||
// this structure.
|
||||
|
||||
Value super_value(*(argument_values.GetValueAtIndex(obj_index)));
|
||||
super_value.GetScalar() += process->GetAddressByteSize();
|
||||
super_value.GetScalar() += process_sp->GetAddressByteSize();
|
||||
super_value.ResolveValue(&exe_ctx);
|
||||
|
||||
if (super_value.GetScalar().IsValid()) {
|
||||
@ -1060,20 +1008,22 @@ AppleObjCTrampolineHandler::GetStepThroughDispatchPlan(Thread &thread,
|
||||
// Okay, we've got the address of the class for which we're resolving this,
|
||||
// let's see if it's in our cache:
|
||||
lldb::addr_t impl_addr = LLDB_INVALID_ADDRESS;
|
||||
|
||||
// If this is a regular dispatch, look up the sel in our addr to sel cache:
|
||||
if (isa_addr != LLDB_INVALID_ADDRESS) {
|
||||
if (log) {
|
||||
LLDB_LOGF(log,
|
||||
"Resolving call for class - 0x%" PRIx64
|
||||
" and selector - 0x%" PRIx64,
|
||||
isa_addr, sel_addr);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
// If it is a selector-specific stub dispatch, look in the string cache:
|
||||
|
||||
if (impl_addr != LLDB_INVALID_ADDRESS) {
|
||||
// Yup, it was in the cache, so we can run to that address directly.
|
||||
@ -1091,20 +1041,52 @@ AppleObjCTrampolineHandler::GetStepThroughDispatchPlan(Thread &thread,
|
||||
ValueList dispatch_values;
|
||||
|
||||
// We've will inject a little function in the target that takes the
|
||||
// object, selector and some flags,
|
||||
// object, selector/selector string and some flags,
|
||||
// and figures out the implementation. Looks like:
|
||||
// void *__lldb_objc_find_implementation_for_selector (void *object,
|
||||
// void *sel,
|
||||
// int
|
||||
// is_str_ptr,
|
||||
// int is_stret,
|
||||
// int is_super,
|
||||
// int is_super2,
|
||||
// int is_fixup,
|
||||
// int is_fixed,
|
||||
// int debug)
|
||||
// If we don't have an actual SEL, but rather a string version of the
|
||||
// selector WE injected, set is_str_ptr to true, and sel to the address
|
||||
// of the string.
|
||||
// So set up the arguments for that call.
|
||||
|
||||
dispatch_values.PushValue(*(argument_values.GetValueAtIndex(obj_index)));
|
||||
dispatch_values.PushValue(*(argument_values.GetValueAtIndex(sel_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);
|
||||
}
|
||||
|
||||
Value flag_value;
|
||||
CompilerType clang_int_type =
|
||||
@ -1114,6 +1096,12 @@ AppleObjCTrampolineHandler::GetStepThroughDispatchPlan(Thread &thread,
|
||||
// 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;
|
||||
dispatch_values.PushValue(flag_value);
|
||||
|
||||
if (this_dispatch->stret_return)
|
||||
flag_value.GetScalar() = 1;
|
||||
else
|
||||
@ -1158,7 +1146,8 @@ AppleObjCTrampolineHandler::GetStepThroughDispatchPlan(Thread &thread,
|
||||
dispatch_values.PushValue(flag_value);
|
||||
|
||||
ret_plan_sp = std::make_shared<AppleThreadPlanStepThroughObjCTrampoline>(
|
||||
thread, *this, dispatch_values, isa_addr, sel_addr);
|
||||
thread, *this, dispatch_values, isa_addr, sel_addr, sel_str_addr,
|
||||
sym_name);
|
||||
if (log) {
|
||||
StreamString s;
|
||||
ret_plan_sp->GetDescription(&s, eDescriptionLevelFull);
|
||||
|
@ -38,11 +38,11 @@ public:
|
||||
public:
|
||||
enum FixUpState { eFixUpNone, eFixUpFixed, eFixUpToFix };
|
||||
|
||||
const char *name;
|
||||
bool stret_return;
|
||||
bool is_super;
|
||||
bool is_super2;
|
||||
FixUpState fixedup;
|
||||
const char *name = nullptr;
|
||||
bool stret_return = false;
|
||||
bool is_super = false;
|
||||
bool is_super2 = false;
|
||||
FixUpState fixedup = eFixUpNone;
|
||||
};
|
||||
|
||||
lldb::addr_t SetupDispatchFunction(Thread &thread,
|
||||
@ -52,9 +52,19 @@ public:
|
||||
const DispatchFunction &)>);
|
||||
|
||||
private:
|
||||
/// These hold the code for the function that finds the implementation of
|
||||
/// an ObjC message send given the class & selector and the kind of dispatch.
|
||||
/// There are two variants depending on whether the platform uses a separate
|
||||
/// _stret passing convention (e.g. Intel) or not (e.g. ARM). The difference
|
||||
/// is only at the very end of the function, so the code is broken into the
|
||||
/// common prefix and the suffix, which get composed appropriately before
|
||||
/// the function gets compiled.
|
||||
/// \{
|
||||
static const char *g_lookup_implementation_function_name;
|
||||
static const char *g_lookup_implementation_function_common_code;
|
||||
static const char *g_lookup_implementation_with_stret_function_code;
|
||||
static const char *g_lookup_implementation_no_stret_function_code;
|
||||
/// \}
|
||||
|
||||
class AppleObjCVTables {
|
||||
public:
|
||||
@ -144,7 +154,7 @@ private:
|
||||
MsgsendMap m_opt_dispatch_map;
|
||||
lldb::ProcessWP m_process_wp;
|
||||
lldb::ModuleSP m_objc_module_sp;
|
||||
const char *m_lookup_implementation_function_code;
|
||||
std::string m_lookup_implementation_function_code;
|
||||
std::unique_ptr<UtilityFunction> m_impl_code;
|
||||
std::mutex m_impl_function_mutex;
|
||||
lldb::addr_t m_impl_fn_addr;
|
||||
|
@ -32,13 +32,15 @@ using namespace lldb_private;
|
||||
AppleThreadPlanStepThroughObjCTrampoline::
|
||||
AppleThreadPlanStepThroughObjCTrampoline(
|
||||
Thread &thread, AppleObjCTrampolineHandler &trampoline_handler,
|
||||
ValueList &input_values, lldb::addr_t isa_addr, lldb::addr_t sel_addr)
|
||||
ValueList &input_values, lldb::addr_t isa_addr, lldb::addr_t sel_addr,
|
||||
lldb::addr_t sel_str_addr, llvm::StringRef sel_str)
|
||||
: ThreadPlan(ThreadPlan::eKindGeneric,
|
||||
"MacOSX Step through ObjC Trampoline", thread, eVoteNoOpinion,
|
||||
eVoteNoOpinion),
|
||||
m_trampoline_handler(trampoline_handler),
|
||||
m_args_addr(LLDB_INVALID_ADDRESS), m_input_values(input_values),
|
||||
m_isa_addr(isa_addr), m_sel_addr(sel_addr), m_impl_function(nullptr) {}
|
||||
m_isa_addr(isa_addr), m_sel_addr(sel_addr), m_impl_function(nullptr),
|
||||
m_sel_str_addr(sel_str_addr), m_sel_str(sel_str) {}
|
||||
|
||||
// Destructor
|
||||
AppleThreadPlanStepThroughObjCTrampoline::
|
||||
@ -126,8 +128,10 @@ bool AppleThreadPlanStepThroughObjCTrampoline::ShouldStop(Event *event_ptr) {
|
||||
}
|
||||
}
|
||||
|
||||
// Second stage, if all went well with the function calling, then fetch the
|
||||
// target address, and queue up a "run to that address" plan.
|
||||
// Second stage, if all went well with the function calling, get the
|
||||
// implementation function address, and queue up a "run to that address" plan.
|
||||
Log *log = GetLog(LLDBLog::Step);
|
||||
|
||||
if (!m_run_to_sp) {
|
||||
Value target_addr_value;
|
||||
ExecutionContext exc_ctx;
|
||||
@ -142,7 +146,6 @@ bool AppleThreadPlanStepThroughObjCTrampoline::ShouldStop(Event *event_ptr) {
|
||||
}
|
||||
Address target_so_addr;
|
||||
target_so_addr.SetOpcodeLoadAddress(target_addr, exc_ctx.GetTargetPtr());
|
||||
Log *log = GetLog(LLDBLog::Step);
|
||||
if (target_addr == 0) {
|
||||
LLDB_LOGF(log, "Got target implementation of 0x0, stopping.");
|
||||
SetPlanComplete();
|
||||
@ -174,13 +177,25 @@ bool AppleThreadPlanStepThroughObjCTrampoline::ShouldStop(Event *event_ptr) {
|
||||
ObjCLanguageRuntime *objc_runtime =
|
||||
ObjCLanguageRuntime::Get(*GetThread().GetProcess());
|
||||
assert(objc_runtime != nullptr);
|
||||
if (m_sel_str_addr != LLDB_INVALID_ADDRESS) {
|
||||
// Cache the string -> implementation and free the string in the target.
|
||||
Status dealloc_error =
|
||||
GetThread().GetProcess()->DeallocateMemory(m_sel_str_addr);
|
||||
// For now just log this:
|
||||
if (dealloc_error.Fail())
|
||||
LLDB_LOG(log, "Failed to deallocate the sel str at {0} - error: {1}",
|
||||
m_sel_str_addr, dealloc_error);
|
||||
objc_runtime->AddToMethodCache(m_isa_addr, m_sel_str, target_addr);
|
||||
LLDB_LOG(log,
|
||||
"Adding \\{isa-addr={0}, sel-addr={1}\\} = addr={2} to cache.",
|
||||
m_isa_addr, m_sel_str, target_addr);
|
||||
} else {
|
||||
objc_runtime->AddToMethodCache(m_isa_addr, m_sel_addr, target_addr);
|
||||
LLDB_LOGF(log,
|
||||
"Adding {isa-addr=0x%" PRIx64 ", sel-addr=0x%" PRIx64
|
||||
"} = addr=0x%" PRIx64 " to cache.",
|
||||
m_isa_addr, m_sel_addr, target_addr);
|
||||
|
||||
// Extract the target address from the value:
|
||||
}
|
||||
|
||||
m_run_to_sp = std::make_shared<ThreadPlanRunToAddress>(
|
||||
GetThread(), target_so_addr, false);
|
||||
|
@ -24,7 +24,8 @@ class AppleThreadPlanStepThroughObjCTrampoline : public ThreadPlan {
|
||||
public:
|
||||
AppleThreadPlanStepThroughObjCTrampoline(
|
||||
Thread &thread, AppleObjCTrampolineHandler &trampoline_handler,
|
||||
ValueList &values, lldb::addr_t isa_addr, lldb::addr_t sel_addr);
|
||||
ValueList &values, lldb::addr_t isa_addr, lldb::addr_t sel_addr,
|
||||
lldb::addr_t sel_str_addr, llvm::StringRef sel_str);
|
||||
|
||||
~AppleThreadPlanStepThroughObjCTrampoline() override;
|
||||
|
||||
@ -70,6 +71,13 @@ private:
|
||||
FunctionCaller *m_impl_function; /// This is a pointer to a impl function that
|
||||
/// is owned by the client that pushes this
|
||||
/// plan.
|
||||
lldb::addr_t m_sel_str_addr; /// If this is not LLDB_INVALID_ADDRESS then it
|
||||
/// is the address we wrote the selector string
|
||||
/// to. We need to deallocate it when the
|
||||
/// function call is done.
|
||||
std::string m_sel_str; /// This is the string we wrote to memory - we
|
||||
/// use it for caching, but only if
|
||||
/// m_sel_str_addr is non-null.
|
||||
};
|
||||
|
||||
class AppleThreadPlanStepThroughDirectDispatch: public ThreadPlanStepOut {
|
||||
|
@ -37,7 +37,7 @@ char ObjCLanguageRuntime::ID = 0;
|
||||
ObjCLanguageRuntime::~ObjCLanguageRuntime() = default;
|
||||
|
||||
ObjCLanguageRuntime::ObjCLanguageRuntime(Process *process)
|
||||
: LanguageRuntime(process), m_impl_cache(),
|
||||
: LanguageRuntime(process), m_impl_cache(), m_impl_str_cache(),
|
||||
m_has_new_literals_and_indexing(eLazyBoolCalculate),
|
||||
m_isa_to_descriptor(), m_hash_to_isa_map(), m_type_size_cache(),
|
||||
m_isa_to_descriptor_stop_id(UINT32_MAX), m_complete_class_cache(),
|
||||
@ -75,6 +75,18 @@ void ObjCLanguageRuntime::AddToMethodCache(lldb::addr_t class_addr,
|
||||
ClassAndSel(class_addr, selector), impl_addr));
|
||||
}
|
||||
|
||||
void ObjCLanguageRuntime::AddToMethodCache(lldb::addr_t class_addr,
|
||||
llvm::StringRef sel_str,
|
||||
lldb::addr_t impl_addr) {
|
||||
Log *log = GetLog(LLDBLog::Step);
|
||||
|
||||
LLDB_LOG(log, "Caching: class {0} selector {1} implementation {2}.",
|
||||
class_addr, sel_str, impl_addr);
|
||||
|
||||
m_impl_str_cache.insert(std::pair<ClassAndSelStr, lldb::addr_t>(
|
||||
ClassAndSelStr(class_addr, sel_str), impl_addr));
|
||||
}
|
||||
|
||||
lldb::addr_t ObjCLanguageRuntime::LookupInMethodCache(lldb::addr_t class_addr,
|
||||
lldb::addr_t selector) {
|
||||
MsgImplMap::iterator pos, end = m_impl_cache.end();
|
||||
@ -84,6 +96,15 @@ lldb::addr_t ObjCLanguageRuntime::LookupInMethodCache(lldb::addr_t class_addr,
|
||||
return LLDB_INVALID_ADDRESS;
|
||||
}
|
||||
|
||||
lldb::addr_t ObjCLanguageRuntime::LookupInMethodCache(lldb::addr_t class_addr,
|
||||
llvm::StringRef sel_str) {
|
||||
MsgImplStrMap::iterator pos, end = m_impl_str_cache.end();
|
||||
pos = m_impl_str_cache.find(ClassAndSelStr(class_addr, sel_str));
|
||||
if (pos != end)
|
||||
return (*pos).second;
|
||||
return LLDB_INVALID_ADDRESS;
|
||||
}
|
||||
|
||||
lldb::TypeSP
|
||||
ObjCLanguageRuntime::LookupInCompleteClassCache(ConstString &name) {
|
||||
CompleteClassMap::iterator complete_class_iter =
|
||||
|
@ -22,6 +22,7 @@
|
||||
#include "lldb/Symbol/CompilerType.h"
|
||||
#include "lldb/Symbol/Type.h"
|
||||
#include "lldb/Target/LanguageRuntime.h"
|
||||
#include "lldb/Utility/ConstString.h"
|
||||
#include "lldb/lldb-private.h"
|
||||
|
||||
class CommandObjectObjC_ClassTable_Dump;
|
||||
@ -242,11 +243,19 @@ public:
|
||||
|
||||
virtual bool HasReadObjCLibrary() = 0;
|
||||
|
||||
// These two methods actually use different caches. The only time we'll
|
||||
// cache a sel_str is if we found a "selector specific stub" for the selector
|
||||
// and conversely we only add to the SEL cache if we saw a regular dispatch.
|
||||
lldb::addr_t LookupInMethodCache(lldb::addr_t class_addr, lldb::addr_t sel);
|
||||
lldb::addr_t LookupInMethodCache(lldb::addr_t class_addr,
|
||||
llvm::StringRef sel_str);
|
||||
|
||||
void AddToMethodCache(lldb::addr_t class_addr, lldb::addr_t sel,
|
||||
lldb::addr_t impl_addr);
|
||||
|
||||
void AddToMethodCache(lldb::addr_t class_addr, llvm::StringRef sel_str,
|
||||
lldb::addr_t impl_addr);
|
||||
|
||||
TypeAndOrName LookupInClassNameCache(lldb::addr_t class_addr);
|
||||
|
||||
void AddToClassNameCache(lldb::addr_t class_addr, const char *name,
|
||||
@ -343,20 +352,22 @@ protected:
|
||||
}
|
||||
|
||||
private:
|
||||
// We keep a map of <Class,Selector>->Implementation so we don't have to call
|
||||
// the resolver function over and over.
|
||||
// We keep two maps of <Class,Selector>->Implementation so we don't have
|
||||
// to call the resolver function over and over.
|
||||
// The first comes from regular obj_msgSend type dispatch, and maps the
|
||||
// class + uniqued SEL value to an implementation.
|
||||
// The second comes from the "selector-specific stubs", which are always
|
||||
// of the form _objc_msgSend$SelectorName, so we don't know the uniqued
|
||||
// selector, only the string name.
|
||||
|
||||
// FIXME: We need to watch for the loading of Protocols, and flush the cache
|
||||
// for any
|
||||
// class that we see so changed.
|
||||
|
||||
struct ClassAndSel {
|
||||
ClassAndSel() {
|
||||
sel_addr = LLDB_INVALID_ADDRESS;
|
||||
class_addr = LLDB_INVALID_ADDRESS;
|
||||
}
|
||||
ClassAndSel() = default;
|
||||
|
||||
ClassAndSel(lldb::addr_t in_sel_addr, lldb::addr_t in_class_addr)
|
||||
ClassAndSel(lldb::addr_t in_class_addr, lldb::addr_t in_sel_addr)
|
||||
: class_addr(in_class_addr), sel_addr(in_sel_addr) {}
|
||||
|
||||
bool operator==(const ClassAndSel &rhs) {
|
||||
@ -379,11 +390,35 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
lldb::addr_t class_addr;
|
||||
lldb::addr_t sel_addr;
|
||||
lldb::addr_t class_addr = LLDB_INVALID_ADDRESS;
|
||||
lldb::addr_t sel_addr = LLDB_INVALID_ADDRESS;
|
||||
};
|
||||
|
||||
struct ClassAndSelStr {
|
||||
ClassAndSelStr() = default;
|
||||
|
||||
ClassAndSelStr(lldb::addr_t in_class_addr, llvm::StringRef in_sel_name)
|
||||
: class_addr(in_class_addr), sel_name(in_sel_name) {}
|
||||
|
||||
bool operator==(const ClassAndSelStr &rhs) {
|
||||
return class_addr == rhs.class_addr && sel_name == rhs.sel_name;
|
||||
}
|
||||
|
||||
bool operator<(const ClassAndSelStr &rhs) const {
|
||||
if (class_addr < rhs.class_addr)
|
||||
return true;
|
||||
else if (class_addr > rhs.class_addr)
|
||||
return false;
|
||||
else
|
||||
return ConstString::Compare(sel_name, rhs.sel_name);
|
||||
}
|
||||
|
||||
lldb::addr_t class_addr = LLDB_INVALID_ADDRESS;
|
||||
ConstString sel_name;
|
||||
};
|
||||
|
||||
typedef std::map<ClassAndSel, lldb::addr_t> MsgImplMap;
|
||||
typedef std::map<ClassAndSelStr, lldb::addr_t> MsgImplStrMap;
|
||||
typedef std::map<ObjCISA, ClassDescriptorSP> ISAToDescriptorMap;
|
||||
typedef std::multimap<uint32_t, ObjCISA> HashToISAMap;
|
||||
typedef ISAToDescriptorMap::iterator ISAToDescriptorIterator;
|
||||
@ -391,6 +426,7 @@ private:
|
||||
typedef ThreadSafeDenseMap<void *, uint64_t> TypeSizeCache;
|
||||
|
||||
MsgImplMap m_impl_cache;
|
||||
MsgImplStrMap m_impl_str_cache;
|
||||
LazyBool m_has_new_literals_and_indexing;
|
||||
ISAToDescriptorMap m_isa_to_descriptor;
|
||||
HashToISAMap m_hash_to_isa_map;
|
||||
|
@ -57,7 +57,7 @@ class ObjCDataFormatterKVO(ObjCDataFormatterTestCase):
|
||||
' 21 key/value pairs'
|
||||
])
|
||||
|
||||
lldbutil.run_break_set_by_regexp(self, 'setAtoms')
|
||||
lldbutil.run_break_set_by_symbol(self, '-[Molecule setAtoms:]')
|
||||
|
||||
self.runCmd("continue")
|
||||
self.expect("frame variable _cmd", substrs=['setAtoms:'])
|
||||
|
Loading…
x
Reference in New Issue
Block a user