Skip to content

Commit cc83362

Browse files
committed
Clang added a new feature to the ObjC compiler that will translate method
calls to commonly un-overridden methods into a function that checks whether the method is overridden anywhere and if not directly dispatches to the NSObject implementation. That means if you do override any of these methods, "step-in" will not step into your code, since we hit the wrapper function, which has no debug info, and immediately step out again. Add code to recognize these functions as "trampolines" and a thread plan that will get us from the function to the user code, if overridden. <rdar://problem/54404114> Differential Revision: https://reviews.llvm.org/D73225
1 parent 568c400 commit cc83362

File tree

9 files changed

+566
-54
lines changed

9 files changed

+566
-54
lines changed

lldb/include/lldb/Target/ThreadPlan.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,8 +461,12 @@ class ThreadPlan : public std::enable_shared_from_this<ThreadPlan>,
461461
virtual void WillPop();
462462

463463
// This pushes a plan onto the plan stack of the current plan's thread.
464+
// Also sets the plans to private and not master plans. A plan pushed by
465+
// another thread plan is never either of the above.
464466
void PushPlan(lldb::ThreadPlanSP &thread_plan_sp) {
465467
m_thread.PushPlan(thread_plan_sp);
468+
thread_plan_sp->SetPrivate(false);
469+
thread_plan_sp->SetIsMasterPlan(false);
466470
}
467471

468472
ThreadPlanKind GetKind() const { return m_kind; }

lldb/include/lldb/Target/ThreadPlanStepInRange.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ class ThreadPlanStepInRange : public ThreadPlanStepRange,
4949

5050
bool IsVirtualStep() override;
5151

52+
// Plans that are implementing parts of a step in might need to follow the
53+
// behavior of this plan w.r.t. StepThrough. They can get that from here.
54+
static uint32_t GetDefaultFlagsValue() {
55+
return s_default_flag_values;
56+
}
57+
5258
protected:
5359
static bool DefaultShouldStopHereCallback(ThreadPlan *current_plan,
5460
Flags &flags,
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
OBJC_SOURCES := stepping-tests.m
2+
LD_EXTRAS := -lobjc -framework Foundation
3+
4+
include Makefile.rules
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""Test stepping through ObjC method dispatch in various forms."""
2+
3+
from __future__ import print_function
4+
5+
6+
import lldb
7+
from lldbsuite.test.decorators import *
8+
from lldbsuite.test.lldbtest import *
9+
from lldbsuite.test import lldbutil
10+
11+
12+
class TestObjCDirectDispatchStepping(TestBase):
13+
14+
mydir = TestBase.compute_mydir(__file__)
15+
NO_DEBUG_INFO_TESTCASE = True
16+
17+
def setUp(self):
18+
# Call super's setUp().
19+
TestBase.setUp(self)
20+
# Find the line numbers that we will step to in main:
21+
self.main_source = lldb.SBFileSpec("stepping-tests.m")
22+
23+
@skipUnlessDarwin
24+
@add_test_categories(['pyapi', 'basic_process'])
25+
def test_with_python_api(self):
26+
"""Test stepping through the 'direct dispatch' optimized method calls."""
27+
self.build()
28+
29+
(target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(self,
30+
"Stop here to start stepping",
31+
self.main_source)
32+
stop_bkpt = target.BreakpointCreateBySourceRegex("// Stop Location [0-9]+", self.main_source)
33+
self.assertEqual(stop_bkpt.GetNumLocations(), 15)
34+
35+
# Here we step through all the overridden methods of OverridesALot
36+
# The last continue will get us to the call ot OverridesInit.
37+
for idx in range(2,16):
38+
thread.StepInto()
39+
func_name = thread.GetFrameAtIndex(0).GetFunctionName()
40+
self.assertTrue("OverridesALot" in func_name, "%d'th step did not match name: %s"%(idx, func_name))
41+
stop_threads = lldbutil.continue_to_breakpoint(process, stop_bkpt)
42+
self.assertEqual(len(stop_threads), 1)
43+
self.assertEqual(stop_threads[0], thread)
44+
45+
thread.StepInto()
46+
func_name = thread.GetFrameAtIndex(0).GetFunctionName()
47+
self.assertEqual(func_name, "-[OverridesInit init]", "Stopped in [OverridesInit init]")
48+
49+
50+
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#import <Foundation/Foundation.h>
2+
3+
@interface OverridesALot: NSObject
4+
5+
- (void)boring;
6+
7+
@end
8+
9+
@implementation OverridesALot
10+
11+
+ (id)alloc {
12+
NSLog(@"alloc");
13+
return [super alloc];
14+
}
15+
16+
+ (id)allocWithZone: (NSZone *)z {
17+
NSLog(@"allocWithZone:");
18+
return [super allocWithZone: z];
19+
}
20+
21+
+ (id)new {
22+
NSLog(@"new");
23+
return [super new];
24+
}
25+
26+
- (id)init {
27+
NSLog(@"init");
28+
return [super init];
29+
}
30+
31+
- (id)self {
32+
NSLog(@"self");
33+
return [super self];
34+
}
35+
36+
+ (id)class {
37+
NSLog(@"class");
38+
return [super class];
39+
}
40+
41+
- (BOOL)isKindOfClass: (Class)c {
42+
NSLog(@"isKindOfClass:");
43+
return [super isKindOfClass: c];
44+
}
45+
46+
- (BOOL)respondsToSelector: (SEL)s {
47+
NSLog(@"respondsToSelector:");
48+
return [super respondsToSelector: s];
49+
}
50+
51+
- (id)retain {
52+
NSLog(@"retain");
53+
return [super retain];
54+
}
55+
56+
- (oneway void)release {
57+
NSLog(@"release");
58+
[super release];
59+
}
60+
61+
- (id)autorelease {
62+
NSLog(@"autorelease");
63+
return [super autorelease];
64+
}
65+
66+
- (void)boring {
67+
NSLog(@"boring");
68+
}
69+
70+
@end
71+
72+
@interface OverridesInit: NSObject
73+
74+
- (void)boring;
75+
76+
@end
77+
78+
@implementation OverridesInit
79+
80+
- (id)init {
81+
NSLog(@"init");
82+
return [super init];
83+
}
84+
85+
@end
86+
87+
int main() {
88+
id obj;
89+
90+
// First make an object of the class that overrides everything,
91+
// and make sure we step into all the methods:
92+
93+
obj = [OverridesALot alloc]; // Stop here to start stepping
94+
[obj release]; // Stop Location 2
95+
96+
obj = [OverridesALot allocWithZone: NULL]; // Stop Location 3
97+
[obj release]; // Stop Location 4
98+
99+
obj = [OverridesALot new]; // Stop Location 5
100+
[obj release]; // Stop Location 6
101+
102+
obj = [[OverridesALot alloc] init]; // Stop Location 7
103+
[obj self]; // Stop Location 8
104+
[obj isKindOfClass: [OverridesALot class]]; // Stop Location 9
105+
[obj respondsToSelector: @selector(hello)]; // Stop Location 10
106+
[obj retain]; // Stop Location 11
107+
[obj autorelease]; // Stop Location 12
108+
[obj boring]; // Stop Location 13
109+
[obj release]; // Stop Location 14
110+
111+
// Now try a class that only overrides init but not alloc, to make
112+
// sure we get into the second method in a combined call:
113+
114+
obj = [[OverridesInit alloc] init]; // Stop Location 15
115+
116+
return 0; // Stop Location 15
117+
}

0 commit comments

Comments
 (0)