Chapters

Hide chapters

Advanced Apple Debugging & Reverse Engineering

Third Edition · iOS 12 · Swift 4.2 · Xcode 10

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section III: Low Level

Section 3: 7 chapters
Show chapters Hide chapters

Section IV: Custom LLDB Commands

Section 4: 8 chapters
Show chapters Hide chapters

30. Intermediate DTrace
Written by Derek Selander

This chapter will act as a grab-bag of more DTrace fundamentals, destructive actions (yay!), as well as how to use DTrace with Swift. I’ll get you excited first before going into theory. I’ll start with how to use DTrace with Swift then go into the sleep-inducing concepts that will make your eyes water. Nah, trust me, this will be fun!

In this chapter, you’ll learn additional ways DTrace can profile code, as well as how to augment existing code without laying a finger on the actual executable itself. Magic!

Getting started

We’re not done picking on Ray Wenderlich. Included in this chapter is yet another movie-title inspired project with Ray’s name spliced into it.

Open up the Finding Ray application in the starter directory for this chapter. No need to do anything special for setup. Build and run the project on the iPhone X simulator.

The majority of this project is written in Swift, though many Swift subclasses inherit from NSObject as they need to be visually displayed (if it’s an on-screen component, it must inherit from UIView, which inherits from NSObject, meaning Objective-C)

DTrace is agnostic to whatever Swift code inherits from whatever class as it’s all the same to DTrace. You can still profile Objective-C code subclassed by a Swift object so long as it inherits from NSObject using the objc$target provider. The downside to this approach is if there are any new methods implemented or any overridden methods implemented by the Swift class, you’ll not see them in any Objective-C probes.

DTrace & Swift in theory

Let’s talk about how one can use DTrace to profile Swift code. There are some pros along with some cons that should be taken into consideration.

First, the happy news: Swift works well with DTrace modules! This means it’s very easy to filter out Swift code based on the particular module it’s implemented in. The module (aka the probemod) will likely be the name of your target in Xcode which contains the Swift code (unless you’ve changed the target name in Xcode’s build settings).

This means you can filter the following Swift code implemented in the SomeTarget module like so:

pid$target:SomeTarget::entry

This will set a probe on the start of every single function implemented inside the SomeTarget module. Since the pid$target goes after all the non-Objective-C code, this probe will pick C & C++ code as well, but as you’ll see in a second, that’s easy to filter out with a well-designed query.

Now for the bad news. Since the information about the module is taken up, the Swift classname and function name all go into the DTrace function section (aka probefunc) for a Swift method. This means you need to be a little more creative with your DTrace querying.

In the previous iteration of Swift (Swift 3), the probefunc Swift names returned by DTrace were the mangled Swift names, but that’s no longer applicable in Swift 4! DTrace now uses the unmangled Swift names in the output!

So without further ado, let’s look at a quick example of a Swift DTrace probe.

Imagine you have a subclass of UIViewController named ViewController which only overrides viewDidLoad. Like so:

class ViewController: UIViewController {
  override func viewDidLoad() { 
    super.viewDidLoad()
  }
}

If you want to create a breakpoint on this function, the fullname to this breakpoint would be the following:

SomeTarget.ViewController.viewDidLoad() -> ()

No surprise there; you’ve beaten that concept to death in Section 1. If you wanted to search for every viewDidLoad implemented by Swift in the SomeTarget target (catchy name, right?), you could create a DTrace probe description that looks like the following:

pid$target:SomeTarget:*viewDidLoad*:entry

This effectively says, “So long as SomeTarget and viewDidLoad are in the function section, gimme the probe.”

Time to try this theory out in the Finding Ray application.

DTrace & Swift in Practice

If the Finding Ray application is not already running, spark it up. iPhone X Plus Simulator. You know what’s up.

Create a fresh window in Terminal and type the following:

sudo dtrace -n 'pid$target:Finding?Ray::entry' -p `pgrep "Finding Ray"`

I chose an Xcode project name that has a space on purpose. Take note of what you need to do to resolve spaces in an Xcode target when using a DTrace script. The probemod section uses a ? as a placeholder wildcard character for the space. In addition, you need to surround your query when pgrep’ing for the process name, otherwise it won’t work.

After you’ve finished typing your password, you’ll get ~240 probe entry hits for all the non-Objective-C functions inside the Finding Ray module.

Click on Ray and drag him around in the Simulator while keeping an eye on all the methods that are getting hit in the Terminal.

There’s still a bit too much noise. You only want the Swift functions to only be displayed. No need to see the probe ID nor the CPU columns.

Kill the DTrace script and replace it with the following:

sudo dtrace -qn 'pid$target:Finding?Ray::entry { printf("%s\n", probefunc); } ' -p `pgrep "Finding Ray"` 

It’s subtle, but you’ve added the -q (or --quiet) option. This will tell DTrace to not display the number of probes you’ve found, nor to display its default output when a probe gets hit. Fortunately, you’ve also added a printf statement to spit out the probefunc manually instead.

Wait for DTrace to start up, then drag again.

Much prettier. Unfortunately, you’re still getting some methods the Swift compiler generated that I didn’t write. You don’t want to see any code the Swift compiler has created; you only want to see code I wrote in my Swift classes.

Kill the previous DTrace script and augment this probe description to only contain code that you’ve implemented, and not that of the Swift compiler:

sudo dtrace -qn 'pid$target:Finding?Ray::entry { printf("%s\n", probefunc); } ' -p `pgrep "Finding Ray"` | grep -E "^[^@].*\."

Jump over to the Simulator and drag Ray around. Notice the difference?

QuickTouchPanGestureRecognizer.delaysTouchesBegan.getter
ViewController.handleGesture(panGesture:)
ViewController.dynamicAnimator.getter
ViewController.snapBehavior.getter
ViewController.containerView.getter
MotionView.animate(isSelected:)

This is piping the output to grep which is using a regular expression query to say return anything that doesn’t contain a “@” and contains a period in the output. This essentially is saying dodn’t return any @objc bridging methods and a period is gauranteed in any Swift code you write thanks to module namespacing.

One final addition. Augment the script to remove the grep filtering, and instead trace all Swift function entries and exits in the “Finding Ray” module, and use DTrace’s flowindent option.

The flowindent option will properly indent function entries and returns.

sudo dtrace -qFn 'pid$target:Finding?Ray::*r* { printf("%s\n", probefunc); } ' -p `pgrep "Finding Ray"` 

There are a couple of items to note on this one. You’ve added the -F option for flowindent. Check out the name section in the probe description, *r*. What does this do?

From a DTrace standpoint, most functions in a process have entry, return and function offsets for every assembly instruction. These offsets are given in hexadecimal. This says “give me any name that contains the letter ‘r’.”

This returns both the entry & return in the probe description name, but omits any function offsets since assembly only goes as high as f. Clever, eh?

With both the enter & return probes of each Swift function enabled, you can clearly see what functions are being executed and where they’re being executed from.

Wait for DTrace to start, then drag Ray Wenderlich’s face around. You’ll get pretty output that looks like this:

Hehehe… thought you would get a kick out of that one!

DTrace variables & control flow

You’ll jump into a bit of theory now, which you’ll need for the remainder of this section.

DTrace has several ways to create and reference variables in your script. All of them have their own pros and cons as they battle between speed and convenience of use in DTrace.

Scalar variables

The first way to create a variable is to use a scalar variable. These are simple variables that can take only take items of fixed size. You don’t need to declare the type of scalar variables, or any variables for that matter in your DTrace scripts.

I tend to lean towards using a scalar variable in DTrace scripts to represent a Boolean value, which is due to the limited conditional logic with DTrace — you only have predicates and ternary operators to really branch your logic.

For example, here is a practical case to use a scalar variable:

#!/usr/sbin/dtrace -s
#pragma D option quiet  

dtrace:::BEGIN
{
    isSet = 0;
    object = 0;
}
objc$target:NSObject:-init:return / isSet == 0 /
{
    object = arg1;
    isSet = 1;
}
objc$target:::entry / isSet && object == arg0 /
{
    printf("0x%p %c[%s %s]\n", arg0, probefunc[0], probemod, (string)&probefunc[1]);
}

This script declares two scalar variables: the isSet scalar variable will check and see if the object scalar variable has been set. If not, the script will set the the next object to the object variable. This script will trace all Objective-C method calls that are being used on the object variable.

Clause-local variables

The next step up are clause-local variables. These are denoted by the word this-> used right before the variable name and can take any type of value, including char*’s. Clause-local variables can survive across the same probe. If you you try to reference them on a different probe, it won’t work. For example, consider the following:

pid$target::objc_msgSend:entry 
{
  this->object = arg0;  
}

pid$target::objc_msgSend:entry / this->object != 0 / {
  /* Do some logic here */
}

obc$target:::entry {
  this-f = this->object; /* Won’t work since different probe */
}

I tend to stick with clause-local variables as much as I can since they’re quite fast and I don’t have to manually free them like I do with the next type of variable…

Thread-local variables

Thread-local variables offer the most flexibility at the price of speed. Additionally, you have to manually release them, otherwise you’ll leak memory. Thread-local variables can be used by preceding the variable name with self->.

The nice thing about thread-local variables is they can be used in different probes, like so:

objc$target:NSObject:init:entry {
  self->a = arg0;
}

objc$target::-dealloc:entry / arg0 == self->a / {
  self->a = 0; 
}

This will assign self->a to whatever object is being initialized. When this object is released, you’ll need to manually release it as well by setting a to 0.

With variables in DTrace out of the way, let’s talk about how you can use variables to execute conditional logic.

DTrace conditions

DTrace has extremely limited conditional logic built in. There’s no such thing as the if/else-statement in DTrace! This is a conscious decision, because a DTrace script is designed to be fast.

However, it does present a problem for you when you want to conditionally perform logic based upon a particular probe, or information contained within that probe.

To get around this limitation, there are two notable methods you can use to perform conditional logic.

The first workaround is to use a ternary operator.

Consider the following contrived Objective-C logic:

int b = 10;
int a = 0;

if (b == 10) {
  a = 5;
} else {
  a = 6;
}

This can be rewritten in DTrace to use a ternary operator:

b = 10;
a = 0;
a = b == 10 ? 5 : 6

Here’s another example of conditional logic with no else-statement:

int b = 10;
int a = 0;
if (b == 10) {
  a++;
}

In DTrace form, this would look like:

b = 10; 
a = 0;
a = b == 10 ? a + 1 : a

The other solution to this is to use multiple DTrace clauses along with a predicate. The first DTrace clause will setup the information needed by the second clause to see if it should perform the action in the predicate.

I know you probably forgot all the terminology for these DTrace components so let’s also look at an example for this.

For example, let’s say you wanted to trace every call in between the start and stop of a function. Typically, I would recommend just setting a DTrace script to catch everything and then use LLDB to execute the command. But what if you wanted to do this solely in DTrace?

For this particular example, you want to trace all Objective-C method calls being executed by -[UIViewController initWithNibName:bundle:] with the following DTrace script:

#!/usr/sbin/dtrace -s
#pragma D option quiet  

dtrace:::BEGIN
{
  trace = 0;
}

objc$target:target:UIViewController:-initWithNibName?bundle?:entry {
  trace = 1
}

objc$target:target:::entry / trace / {
  printf("%s\n", probefunc);
}

objc$target:target:UIViewController:-initWithNibName?bundle?:return {
  trace = 0
}

As soon as the initWithNibName:bundle: is entered, the trace variable is set. From there on out, every single Objective-C method is displayed until initWithNibName:bundle: returns.

Not being able to use loops and conditions can appear annoying at first when writing DTrace scripts, but think of not relying on the common programming idioms you’ve become accustomed to as a nice brain teaser.

Time for another big discussion: inspecting process memory in your DTrace scripts.

Inspecting process memory

It may come as surprise, but the DTrace scripts you’ve been writing are actually executed in the kernel itself. This is why they’re so fast and also why you don’t need to change any code in an already compiled program to perform dynamic tracing. The kernel has direct access!

DTrace has probes all over your computer. There are probes in the kernel, there’s probes in userland, there’s even probes to describe the crossing between the kernel and userland (and vice versa) using the fbt provider.

Here’s a visualization showing a very very small percentage of the DTrace probes on your computer.

Narrow down your focus to just two probes of the thousands by exploring the open system call and the open_nocancel system call. Both of these functions are implemented in the kernel and are responsible for any type of file openings for reading, writing, or both.

The system open has the following function signature:

int open(const char *path, int oflag, ...);

Internally, open will sometimes call the open_nocancel, which has the following function signature:

int open_nocancel(const char *path, int flags, mode_t mode);

Both of these functions contain a char* as the first parameter. You’ve already grabbed parameters from functions before in DTrace probes using arg0 and arg1. What you haven’t done yet is dereference those pointers to look at their data. Just as in the previous chapters with SBValue, you can spelunk in memory with DTrace and even get the string representation of this first parameter in the open system calls.

There’s one gotcha though. A DTrace script executes in the kernel. The argX parameters are given to you, but these are pointers to the value in the address space of the program. However, DTrace runs in the kernel. So you need to manually copy whatever data you’re reading into the kernel’s memory space.

This is done through the copyin and copyinstr functions. copyin will take an address with the amount of bytes you want to read, while the copyinstr expects to copy a char* representation. In the case of the open family of system calls, you could read the first parameter as a string with the following DTrace clause:

sudo dtrace -n 'syscall::open:entry { printf("%s", copyinstr(arg0)); }'

For example, if a process whose PID was 12345 was attempting to open "/Applications/SomeApp.app/", DTrace could read this first parameter using copyinstr(arg0).

For this particular example, DTrace will read in arg0, which for this example equals 0x7fff58034300. With the copyinstr function, the 0x7fff58034300 memory address will be dereferenced to grab the char* representation for the pathname, "/Applications/SomeApp.app/".

Playing with open syscalls

With the knowledge you need to inspect process memory, create a DTrace script that monitors the open family of system calls. In Terminal, type the following:

sudo dtrace -qn 'syscall::open*:entry { printf("%s opened %s\n", execname, copyinstr(arg0)); ustack(); }'

This will print the contents of open (or open_nocancel) along with the program that called the open* system call with the userland stack trace that was responsible for the call.

Isn’t DTrace awesome!?

Augment your open family of system calls to only focus on the Finding Ray process.

sudo dtrace -qn 'syscall::open*:entry / execname == "Finding Ray" / { printf("%s opened %s\n", execname, copyinstr(arg0)); ustack(); }'

Note: The actions you perform with DTrace can sometimes produce errors to stderr in Terminal. Depending on the error, you can get around this by creating checks for appropriate input with a DTrace predicate, or you can filter your probe description query with less probes. An alternative to this is to ignore all errors produced by DTrace by adding 2>/dev/null in your DTrace one-liner. This effectively tells your DTrace one-liner to pipe any stderr content (2 is the standard error file descriptor) to be ignored. I often use this solution to cast a wide net on probes that can be error-prone, but ignore any errors that my tracing produces.

Rebuild an launch the application.

Stack traces will now only be displayed on any open* system call being called from the Finding Ray application. Play around with the app in the Simulator a bit and see if you can make it output something!

Filtering open syscalls by paths

Inside the Finding Ray project, I remember I used the image named Ray.pdf for something, but I can’t remember where. Good thing I have DTrace along with grep to hunt down the location of where Ray.pdf is being opened.

Kill your current DTrace script and modify the script so it pipes stderr straight to hell. While you’re doing that, append a grep query to it so it looks like:

sudo dtrace -qn 'syscall::open*:entry / execname == "Finding Ray" / { printf("%s opened %s\n", execname, copyinstr(arg0)); ustack(); }' 2>/dev/null | grep Ray.png -A40

This pipes all stderr to nowhere, stdout to grep and searches for any references to the Ray.png image. If there’s a hit, print out the next 40 lines.

Note: There’s actually a pretty awesome DTrace script called opensnoop found in /usr/bin/ on your computer which has many options for monitoring the open family of system calls and is wayyyyyyyy easier to use than writing these scripts. But you wouldn’t learn anything if I just gave you the easy way out, right? Check out this script on your own time, with a good ol’ man opensnoop. You won’t be disappointed in what it can do.

There’s a more elegant way to do this without relying on piping (well, more elegant in my opinion). You can use the predicate section of the DTrace clause to search the userland char* input for the Ray.png string.

You’ll use the strstr DTrace function to do this check. This function takes two strings and returns a pointer to the first occurrence of the second string in the first string. If it can’t find an occurrence, it will return NULL. This means you can check if this function equals NULL in the predicate to search for a path which contains Ray.png!

Augment your increasingly ugly — er, complex DTrace script to look like the following:

sudo dtrace -qn 'syscall::open*:entry / execname == "Finding Ray" && strstr(copyinstr(arg0), "Ray.png") != NULL / { printf("%s opened %s\n", execname, copyinstr(arg0)); ustack(); }' 2>/dev/null

Build and rerun the application.

You threw out the grep piping and replaced it with a conditional check in the predicate for anything containing the name Ray.png that’s opened in the Finding Ray process.

In addition, you’ve easily pinpointed the stack trace responsible for opening the Ray.png image.

DTrace & destructive actions

Note: What I am about to show you is very dangerous.

Let me repeat that: This next bit is very dangerous.

If you screw up a command you could lose some of your beloved images. Follow along only at your own risk!

In fact, to be safe, please close any applications that pertain to using photos (i.e. Photos, PhotoShop, etc). Neither I, nor the publisher are legally responsible for anything that could happen on your computer.

You have been warned!

Heh… I bet that above legal section made you nervous.

You’ll use DTrace to perform a destructive action. That is, normally DTrace will only monitor your computer, but now you’ll actually alter logic in your program.

You’ll monitor the open family of system calls that are executed by the Finding Ray app. If one of the open system calls contain the phrase .png in its first parameter (aka the parameter of type char* to the path it’s opening), you’ll replace that argument with a different PNG image.

This can all be accomplished with the copyout and copyoutstr DTrace commands. You’ll use the copyoutstr explicitly for this example. You’ll notice these name are similar to copyin and copyinstr. The in and out in this context refer to the direction in which you’re copying data, either into where DTrace can read it, or out to where the process can read it.

In the projects directory, there’s a standalone image named troll.png. Create a new window in Finder with ⌘ + N, then navigate to your home directory by pressing ⌘ + Shift + H. Drop troll.png into this directory (feel free to remove it when this chapter is done). There’s a method to this madness — just bear with me!

Why did you need to do this? You’re about to write to memory in an existing program. There’s only a finite amount of space that is already allocated for this string in the program’s memory.

This will likely be some long string because you’re in the iPhone Simulator and your process (mostly) reads images found in its own sandbox.

Do you remember searching for Ray.png? Here’s that full path on my computer. Yours will obviously be different.

/Users/derekselander/Library/Developer/CoreSimulator/Devices/97F8BE2C-4547-470C-955F-3654A8347C41/data/Containers/Bundle/Application/102BDE66-79CB-453C-BA71-4062B2BC5297/Finding Ray.app/Ray.png

The plan of attack is to use DTrace with a shorter path to an image, which will result in something like this in the program’s memory:

/Users/derekselander/troll.png\0veloper/CoreSimulator/Devices/97F8BE2C-4547-470C-955F-3654A8347C41/data/Containers/Bundle/Application/102BDE66-79CB-453C-BA71-4062B2BC5297/Finding Ray.app/Ray.png

You see that \0 in there? That’s the NULL terminator for char*. So essentially this string is really just:

/Users/derekselander/troll.png

Because that’s how NULL terminated strings work!

Getting your path length

When writing data out, you’ll need to figure out how many chars your fullpath is to the troll.png. I know the length of mine, but unfortunately, I don’t know your name nor the name of your computer’s home directory.

Type the following in Terminal:

echo ~/troll.png

This will be dump the fullpath to the troll.png image. Hold onto this for a second as you’ll paste this into your script. Also figure out how many characters this is in Terminal:

echo ~/troll.png | wc -m

In my case, /Users/derekselander/troll.png is 31 char’s. But here’s the gotcha: You need to account for the null terminator. This means the total length I need to insert my new string needs to be an existing char* of length 32 or greater.

The arg0 in open* is pointing to something in memory. If you were to write in this location with something longer than this string, then this could corrupt memory and kill the program. Obviously, you don’t want this, so what you’ll do is stick troll.png in a directory that has a shorter character count.

You’ll also perform checks via the DTrace predicate to ensure you have enough room as well. C’mon, you’re a thorough and diligent programmer, right?

Type the following in Terminal, replacing /Users/derekselander and 32 with your values:

sudo dtrace -wn 'syscall::open*:entry / execname == "Finding Ray" && arg0 > 0xfffffffe && strstr(copyinstr(arg0), ".png") != NULL && strlen(copyinstr(arg0)) >= 32 / { this->a = "/Users/derekselander/troll.png"; copyoutstr(this->a, arg0, 32); }'

Rebuild and run Finding Ray while this new DTrace script is active.

Provided you’ve executed everything correctly, each time the Finding Ray process tries to open a file that contains the phrase “.png”, you’ll return troll.png instead.

Other destructive actions

In addition to copyoutstr and copyout, DTrace has some other destructive actions worth noting:

  • stop(void): This will freeze the currently running userland process (given by the pid built-in argument). This is ideal if you want to stop execution of a userland program, attach LLDB to it and explore it further.

  • raise(int signal): This will raise a signal to the process responsible for a probe.

  • system(string program, …): This lets you execute a command just as if you were in Terminal. This has the added benefit of letting you access all the DTrace built-in variables, such as execname and probemod, to use in a printf-style formatting.

I encourage you to explore these destructive actions (especially the stop() action) on your own time. That being said, be careful with that system function. You can do a lot of damage really easily if used incorrectly.

Where to go from here?

There are many powerful DTrace scripts on your macOS machine. You can hunt for them using the man -k dtrace, then systematically man’ing what each script does. In addition, you can learn a lot by studying the code in them. Remember, these are scripts, not compiled executables, so source-code is fair game.

Also, be very careful with destructive actions. That being said, you can put Ray Wenderlich everywhere on your computer:

Isn’t that what you’ve always wanted?

In all seriousness, you can do some pretty crazy stuff to your computer and gain a lot of insight using DTrace.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.