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

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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.

class ViewController: UIViewController {
  override func viewDidLoad() { 
SomeTarget.ViewController.viewDidLoad() -> ()

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.

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

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

DTrace variables & control flow

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

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.

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

    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]);

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:

  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 */

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

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

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

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.

int b = 10;
int a = 0;

if (b == 10) {
  a = 5;
} else {
  a = 6;
b = 10;
a = 0;
a = b == 10 ? 5 : 6
int b = 10;
int a = 0;
if (b == 10) {
b = 10; 
a = 0;
a = b == 10 ? a + 1 : a
#!/usr/sbin/dtrace -s
#pragma D option quiet  

  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

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!

int open(const char *path, int oflag, ...);
int open_nocancel(const char *path, int flags, mode_t mode);
sudo dtrace -n 'syscall::open:entry { printf("%s", copyinstr(arg0)); }'

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

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.

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

DTrace & destructive actions

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


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.

echo ~/troll.png
echo ~/troll.png | wc -m
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); }'

Other destructive actions

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

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.

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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now