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
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Time to Swizzle

Create a new Objective-C Category. Specify the Class as NSObject and name the category Rayrolling IDEActivityReportStringSegment

Add the following code to NSObject+Rayrolling_IDEActivityReportStringSegment.m:

#import "NSObject+Rayrolling_IDEActivityReportStringSegment.h"
#import "NSObject+MethodSwizzler.h"
#import "Rayrolling.h"

@interface NSObject ()

// 1
- (id)initWithString:(id)arg1 priority:(double)arg2 frontSeparator:(id)arg3 backSeparator:(id)arg4;
@end

@implementation NSObject (Rayrolling_IDEActivityReportStringSegment)

// 2
+ (void)load
{
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    [NSClassFromString(@"IDEActivityReportStringSegment") swizzleWithOriginalSelector:@selector(initWithString:priority:frontSeparator:backSeparator:) swizzledSelector:@selector(Rayrolling_initWithString:priority:frontSeparator:backSeparator:) isClassMethod:NO];
  });
}

- (id)Rayrolling_initWithString:(NSString *)string priority:(double)priority frontSeparator:(id)frontSeparator backSeparator:(id)backSeparator
{

  // 3
  static NSArray *lyrics;

  // 4
  static NSInteger index = 0;
  static dispatch_once_t onceToken;

  // 5
  dispatch_once(&onceToken, ^{
    lyrics = @[@"Never gonna live you up.",
               @"Never gonna set you down."];


  });

  // 6
  index = index >= lyrics.count -1 ? 0 : index + 1;

  // 7
  if  ([Rayrolling isEnabled]) {
    return [self Rayrolling_initWithString:lyrics[index] priority:priority frontSeparator:frontSeparator backSeparator:backSeparator];
  }
  return [self Rayrolling_initWithString:string priority:priority frontSeparator:frontSeparator backSeparator:backSeparator];
}

@end

Here’s what’s going on in the code above:

  1. You need to forward declare the private class initializer initWithString:priority:frontSeparator:backSeparator:, otherwise the compiler will be cranky.
  2. load is used to swizzle the private method with the one that will follow below.
  3. Since you’re using a category to swap this method, instance variables are tricky to create in a category. You can add instance variables to existing classes using associated objects, but that’s a whole tutorial unto itself. Therefore, you use a static NSArray to keep the object around once the method has finished.
  4. You use the same static trick to help index survive method calls as well.
  5. Use dispatch_once to initialize the lyrics NSArray.
  6. Increment index and reset it if it gets out of bounds
  7. Check if the plugin is enabled. If so, augment the string parameter; otherwise, use the default string parameter.

As you can see, using the right tools for the job helps you quickly hunt down items of interest. However, there’s always more than one way to find what you’re looking for — and you’ll discover how to also find what you’re looking for through heap analysis.

Revisiting IDEActivityView Through The Heap

It’s time for you to explore Xcode’s IDEActivityView and IDEActivityReportStringSegment again, but you’ll do it in a slightly different fashion.

Until now, you’ve only explored from the top down, where you find a Class, then somehow find its instance(s) in memory, then explore the contents of those properties. It would be ideal to start from the reverse direction and go up the reference chain to find the objects that point to it.

Fortunately, Xcode ships with some nifty Python scripts that allow you to do just that.

Kill any existing LLDB and Dtrace sessions in Terminal and restart Xcode. Start a fresh Xcode and LLDB session. In LLDB perform the usual Xcode-attaching dance and add the following new command script:

(lldb) pro at -n Xcode 
(lldb) command script import lldb.macosx.heap
"malloc_info", "ptr_refs", "cstr_refs", and "objc_refs" commands have been installed, use the "--help" options on these commands for detailed help.

This script loads several incredibly useful functions in the LLDB process.

  • malloc_info: Displays detailed information about the current heap object. Incredibly useful in combination with the MallocStackLogging environment variable.
  • ptr_refs: Given an address in heap memory, this will find other objects that point to this in memory.
  • cstr_refs: Given a char *, finds the occurrences of it in memory.
  • objc_refs: Finds the instances of a particular class in memory.

You’ll look at the easiest to understand one first: objc_refs.

In LLDB, find all the instances of IDEActivityView with the following script:

(lldb) objc_refs -o IDEActivityView
Searching for all instances of classes or subclasses of "IDEActivityView" (isa=0x10fffe650)
0x00007faaee9cbf30: malloc(   304) -> 0x7faaee9cbf30 IDEActivityView.DVTLayerHostingView.NSView.NSResponder.NSObject.isa
<IDEActivityView: 0x7faaee9cbf30>

This prints out all instances of IDEActivityView. The -o option prints the object, and thus the object’s description method)

With one simple command you’ve found the instances in memory that belong to this type of object. This trick also works for subclasses whose superclass you want to find.

Using the memory address spat out by objc_refs, you can explore the instance of IDEActivityView instance variables like so:

(lldb) malloc_info -t 0x7faaee9cbf30
0x00007faaee9cbf30: malloc(   304) -> 0x7faaee9cbf30 IDEActivityView.DVTLayerHostingView.NSView.NSResponder.NSObject.isa (IDEActivityView) *addr = {
  DVTLayerHostingView = {
    NSView = {
      NSResponder = {
...

But wait — that’s not all! You can even get the reference to all the other objects pointing to a particular memory address:

(lldb) ptr_refs  0x7faaee9cbf30
0x00007faaeed9a308: malloc(    16) -> 0x7faaeed9a300 + 8     
0x00007faaeedb4148: malloc(   176) -> 0x7faaeedb4140 + 8      IDEActivityViewBackgroundButton.NSButton.NSControl.NSView.NSResponder._nextResponder
0x00007faaeedb4190: malloc(   176) -> 0x7faaeedb4140 + 80     IDEActivityViewBackgroundButton.NSButton.NSControl._aux.NSObject.isa
0x00007faaeedb4928: malloc(   160) -> 0x7faaeedb4920 + 8      NSView.NSResponder._nextResponder
...
Note: These commands, combined with your im loo -rn {classmethod regex} queries, can help you quickly find and explore instances of classes live in memory. Just make sure that you always pause LLDB first before you try these functions, or else they won’t work!

You can even take it a step further — there is a cool environment variable named MallocStackLogging that takes a backtrace of the stack whenever you instantiate an object. Although many items can point to an object, this can help you figure out who the “ultimate owner” of an object is.

While LLDB is still attached to Xcode, you will kill the Xcode process and relaunch it with the MallocStackLogging environment variable via LLDB:

(lldb) pro kill 
Process 65196 exited with status = 9 (0x00000009)
(lldb) pro launch -v MallocStackLogging=1 -n
Process 67920 launched: '/Applications/Xcode.app/Contents/MacOS/Xcode' (x86_64)

This sets MallocStackLogging to true for every process created by launchd here on out. Be sure to remove this when you are done if you do go this route with the following command:

Note: If you are lazy and are constantly killing LLDB/Xcode sessions, launchctl might prove to be an easier way of launching this environment variable:
launchctl setenv MallocStackLogging 1
launchctl unsetenv MallocStackLogging
launchctl setenv MallocStackLogging 1
launchctl unsetenv MallocStackLogging

To test this all out, make sure the Xcode editor does not have Rayrolling enabled under the Edit menu as below:

Enable_RayRolling

Now, with any project having source code open in Xcode, build and run the project. While the project is actively running, you will see the IDEActivityView (just pretend you don’t know it’s called that for the moment) change its contents as the build completes and runs.

Pause the execution of the program and reload the heap functions if needed with the following:

(lldb) pro i 
(lldb) command script import lldb.macosx.heap
"malloc_info", "ptr_refs", "cstr_refs", and "objc_refs" commands have been installed, use the "--help" options on these commands for detailed help.
Note: If constantly retyping these commands is annoying to you, check out how to set up command aliases in the ~/.lldbinit file. The contents of this file will be called each time you create an instance of LLDB.

Now use the cstr_ref function with the string contents of the Xcode title bar. For example, if the project name was Rayrolling, your title bar would have something like Running Rayrolling : Rayrolling:

(lldb) cstr_ref "Running Rayrolling : Rayrolling" 
0x0000000118cb21d0: malloc(    32) -> 0x118cb21d0
0x000000012079ae21: malloc(    48) -> 0x12079ae10 + 17     __NSCFString1 bytes after __NSCFString
0x00000001207a3bb3: malloc(    64) -> 0x1207a3b90 + 35     __NSCFString19 bytes after __NSCFString
0x00000001223834d1: malloc(    48) -> 0x1223834c0 + 17     __NSCFString1 bytes after __NSCFString
0x000000011f9126a1: malloc(    48) -> 0x11f912690 + 17     __NSCFString1 bytes after __NSCFString

As you can see, the majority of the references are NSString references. You can’t do much with this information by itself, so you will need to figure out how these items were created. Fortunately with MallocStackLogging, this is super easy!

(lldb) cstr_ref "Running Rayrolling : Rayrolling"  -s 

This prints out the stack frame whenever this particular char * is created.

Go through all the stack trace instances. Since these are in memory, the number of memory addresses as well as the address location will be different on your computer depending on how you’ve interacted with Xcode.

By reading through the stack traces, you’ll gain some insight into the grouping of classes that are responsible for this area of Xcode.

For example, depending on when you launched this LLDB query you might see classes such as IDEActivityScrollingTextLayer, IDERunOperation or IDEActivityView, etc.

You can also use the ptr_ref on the output of cstr_ref to see what items are retaining the strings.

Contributors

Over 300 content creators. Join our team.