How To Create an Xcode Plugin: Part 2/3
Continue your exploration of app internals as you learn about developing an Xcode plugin with more LLDB, swizzling, and Dtrace in the second of this three-part tutorial series. By Derek Selander.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How To Create an Xcode Plugin: Part 2/3
25 mins
Update note: This tutorial has only been tested with Xcode 6.3.2 — if you’re working with another version of Xcode, your experience might not match up perfectly with this tutorial.
Welcome to Part 2 of this three-part tutorial series on custom Xcode plugins! In Part 1 of this series, you were treated to a glimpse of Xcode’s underlying classes through NSNotification
properties and injected code into the private class DVTBezelAlertPanel
. In addition, you added a NSMenuItem
to the menu to persist your preference of enabling Rayrolling in Xcode.
In this part, you’ll continue to build out the Rayrolling plugin you started in Part 1 — if you didn’t work through the first part of the tutorial, or just want to start afresh, you can download the finished project from the first part here. You’ll take a deep dive into the tools available for you to explore Xcode and, with your newfound knowledge, modify Xcode’s title bar so it showcases the lyrics of Ray’s very own hit song Never Gonna Live You Up. :]
Getting Started
Open Xcode and Terminal and position your windows on the desktop so you can see both of them at the same time:
Since you’ve graduated from plugin n00b to ¡L33T P1|_|gin m4$Ter!, you’ll use LLDB via Terminal for your Xcode explorations; you no longer need to attach Xcode to an new instance of Xcode to see how things work under the hood.
LLDB’s BFF, Dtrace
One of the best tools for exploring Xcode is Dtrace, which is a wickedly awesome debugging tool and the workhorse behind Instruments. It’s an incredibly useful tool — provided you know how to wield it.
First, a “Hello World” tour of Dtrace is in order. You’ll create a script that will keep a running count of all the classes that begin with IDE and increment the count each time you call a class or instance method for that particular class. Dtrace will then dump this data when you exit the script.
Launch Xcode, then type the following into a fresh tab in Terminal:
sudo dtrace -n 'objc$target:IDE*::entry { @[probemod] = count(); }' -p `pgrep -xo Xcode`
Although you won’t see any output at first, Dtrace is silently generating a trace of all method calls. Head back to Xcode and play around with it a bit; open some files and click on a few items. Then navigate back to Terminal and press Control + C to terminate the script. The contents of the data will be dumped out into Terminal:
Pretty cool, eh? :] There’s quite a bit you can do with Dtrace, but this tutorial won’t cover the full scope of what you can do. Instead, a quick anatomy of a Dtrace program will help get you started:
Note that you use the $target keyword to match the process ID. You specify the target through the p or c option flags
-
Probe Description: Consists of a provider, module, function, and name separated by colons. Omitting any of these items will cause the Probe Description to include all matches. You can use the * or ? operators for pattern matching.
- Provider: The group that contains the set of classes and functions, such as profile, fbt, or io. For this particular tutorial, you’ll primarily use the objc provider to hook into Objective-C method calls.
- Module: In Objective-C, this section is where you specify the class name you wish to observe.
- Function: The part of the probe description that can specify the function name that you wish to observe.
- Name: Although there are different names available based upon your Provider selection, you will only use entry or return, which will match a probe description for the start or the end of a function.
Note that you use the $target keyword to match the process ID. You specify the target through the p or c option flags
-
Predicate: An optional expression to evaluate if the action is a candidate for execution. Think of it as the content of an
if
block. - Action: The action to perform. This could be as simple as printing something to the console, or performing more advanced functions.
Much like the LLDB command image lookup -rn {Regex Query}
, you can use Dtrace to dump classes and methods in a particular process using the -l
flag.
To see a quick example of this, launch Safari, then type the following in Terminal:
sudo dtrace -ln 'objc$target::-*ecret*:entry' -p `pgrep -xn Safari`
The above Dtrace script prints out all the instance methods that have the string ecret contained in a method name. You supply the entry probe description name as all methods have an entry and return, so you’re basically omitting duplicates for your search query.
Now that you’ve covered the basics of Dtrace, it’s time to use it to hunt down NSViews
of interest. Since there are a ton of views in Xcode, you’d quickly be overwhelmed using LLDB trying to figure out which view is which. Even with LLDB’s breakpoint conditions, debugging something this common in an application can be an ugly process.
Fortunately, being smart with Dtrace will help you immensely. You’ll use Dtrace to hunt down the NSViews
that make up Xcode’s titlebar. But how would you do that?
Here’s one way: when a mouse stops moving or clicks down on an NSView
, hitTest:
fires, which returns the deepest subview within that point. You’ll use Dtrace along with this method to determine which NSView
you should use to explore the potential superview
and subviews
.
Run the following command in Terminal:
sudo dtrace -qn 'objc$target:NSView:-hitTest?:return /arg1 != 0/ { printf("UIView: 0x%x\n", arg1); }' -p `pgrep -xo Xcode`
Once the script is running, make sure Xcode is the first responder by clicking somewhere within its window. Move your cursor around the Xcode window; as soon as you stop moving your mouse, Dtrace spits out a memory address multiple times. This is because the hitTest:
method is fired on multiple NSView
s in the view hierarchy.
Navigate to the Xcode title bar and click on the titlebar. Select the most recent address that appeared in Terminal and copy it to your clipboard.
Open a new tab in Terminal, launch LLDB, attach it to Xcode, then print the address you copied over from Dtrace like so:
> lldb
(lldb) pro attach -n Xcode
...
(lldb) po {The Address you copied from dtrace}
...
You’ll see some output similar to the following:
Depending on the spot you clicked in Xcode, you’ll hit one of one of several views. Explore the superview
or the subviews
of the memory address until you reach IDEActivityView
.
Once you find the reference of IDEActivityView
, make sure that this NSView
is actually the one you want.
Type the following In LLDB:
(lldb) po [{IDEActivityView Address} setHidden:YES]
...
(lldb) c
...
The Xcode title view is now hidden, which shows that this is the the view you want to maninpulate.
Use LLDB to unhide this view:
(lldb) pro i
(lldb) po [{IDEActivityView Address} setHidden:NO]
(lldb) c
Here’s the flow in LLDB, for reference:
Based on past experience, you know that the contents of this title view changes when you build or stop running a particular program. You can observe this functionality with Dtrace. The IDEActivity prefix is a pretty unique naming convention; you can observe all classes that begin with IDEActivity to see all related things happening behind the scenes.
Back in Terminal, stop the Dtrace program by pressing Control + C and then paste and execute the following Dtrace script into Terminal:
sudo dtrace -qn 'objc$target:IDEActivity*::entry { printf("%c[%s %s]\n", probefunc[0], probemod, (string)&probefunc[1]); @num[probemod] = count(); }' -p `pgrep -xn Xcode`
This prints out every called method whose classname begins with IDEActivity. Once you exit this program, it will also print the count of how often a particular class’s methods were called.
Start up your Dtrace program, build and run a project in Xcode, then stop the project. Note that the text changes in the title view, then stop the Dtrace program and view the results:
Look over the information carefully; the solution to how IDEActivityView
and friends operate is right there in the console output, but it’s a lot of information to plow through, isn’t it?
Fortunately, you can selectively limit the information displayed to you. Browse through the classes and see if there are any that you can selectively explore. Perhaps IDEActivityReport* would be a good candidate, since it knocks out several classes that look related.
Augment the Dtrace script so it now looks like this:
sudo dtrace -qn 'objc$target:IDEActivityReport*::entry { printf("%c[%s %s]\n", probefunc[0], probemod, (string)&probefunc[1]); @num[probemod] = count(); }' -p `pgrep -xn Xcode`
Go through the motion of running and stopping Xcode while keeping a close eye on the console. Stop the Dtrace script once you’ve stopped Xcode. Are there any classes that look like they could be candidates for further exploration?
IDEActivityReportStringSegment
looks interesting. Narrow your script to focus only on this class; take note of the probemod
to probefunc
change:
sudo dtrace -qn 'objc$target:IDEActivityReportStringSegment::entry { printf("%c[%s %s]\n", probefunc[0], probemod, (string)&probefunc[1]); @num[probefunc] = count(); }' -p `pgrep -xn Xcode`
Go through the build, run Xcode, stop Xcode, stop Dtrace motions once again and look at the count of the methods executed by class instances of IDEActivityReportStringSegment
. It seems that initWithString:priority:frontSeparator:backSeparator:
and initWithString:priority:
look like good items to explore.
Open up a fresh LLDB session and run the following:
(lldb) pro attach -n Xcode
(lldb) rb 'IDEActivityReportStringSegment\ initWithString'
Breakpoint 9: 2 locations.
(lldb) br command add
Enter your debugger command(s). Type 'DONE' to end.
> po NSLog(@"customidentifier %s %@", $rsi, $rdx)
> c
> DONE
(lldb) c
Here you create a custom command that executes whenever you call any method that begins with initWithString
and belongs to the IDEActivityReportStringSegment
class. This custom command prints the Selector method and the contents of self
, which is the instance of IDEActivityReportStringSegment
, to the console.
In addition, you tagged the NSLog
statement to contain the word customidentifier
so you can easily hunt it down in the system console.
Go to the system console now and create a grep‘d tail searching for customidentifier. Create a new Terminal tab using ⌘ + t and type the following:
tail -f /var/log/system.log | grep customidentifier
Build and run in Xcode in order to populate the IDEActivityReportStringSegment
changes. This prints out all the messages you added in your custom LLDB command hook:
Comparing the output from the console to the output from Xcode’s titlebar view shows that these are the items you are in fact looking for! :]