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.

Leave a rating/review
Save for later
Share

Dtrace your way through Xcode for fun and profit!

Dtrace your way through Xcode for fun and profit!

Dtrace your way through Xcode for fun and profit!

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. :]

Plugin_Swizzled_Titlebar

Getting Started

Open Xcode and Terminal and position your windows on the desktop so you can see both of them at the same time:

View_Organization

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:

Dtrace_Hello_World

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:

Dtrace_breakdown_v2

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.

Note: If you want to learn more about Dtrace, check out this excellent article from the Big Nerd Ranch Folks as well as this Dtrace book. Dtrace is a complex tool, so don’t be too frustrated if you don’t understand everything about it in this quick introduction.

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 NSViews 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:

Dtrace_hitTest_2

Note: Right now, Xcode is now paused via LLDB in Terminal. You can pause Xcode to start the debugger at any time by typing process interrupt or just pro i. In addition, you can resume Xcode at any time by typing continue or just c. Make sure you are ALWAYS aware of the state of LLDB when playing with Xcode, because you might think Xcode is being unresponsive when it’s simply paused in the debugger.

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:

LLDB_setHidden

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:

Dtrace_IDEActivity

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:

LLDB_String_Hunting

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! :]

Contributors

Over 300 content creators. Join our team.