Chapters

Hide chapters

Advanced Apple Debugging & Reverse Engineering

Fourth Edition · iOS 16, macOS 13.3 · Swift 5.8, Python 3 · Xcode 14

Section I: Beginning LLDB Commands

Section 1: 10 chapters
Show chapters Hide chapters

Section IV: Custom LLDB Commands

Section 4: 8 chapters
Show chapters Hide chapters

29. Intermediate DTrace
Written by Walter Tyree

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

pid$target:SomeTarget::entry
class ViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
  }
}
SomeTarget.ViewController.viewDidLoad() -> ()
pid$target:SomeTarget:*viewDidLoad*:entry

DTrace & Swift in Practice

If the Finding Ray application is not already running, spark it up! iPhone 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 "^[^@].*\."
QuickTouchPanGestureRecognizer.touchesBegan(_:with:)
QuickTouchPanGestureRecognizer.gestureRecognizerShouldBegin(_:)
QuickTouchPanGestureRecognizer.shouldRequireFailure(of:)
QuickTouchPanGestureRecognizer.shouldRequireFailure(of:)
QuickTouchPanGestureRecognizer.canPrevent(_:)
QuickTouchPanGestureRecognizer.delaysTouchesBegan.getter
QuickTouchPanGestureRecognizer.delaysTouchesBegan.getter
ViewController.handleGesture(panGesture:)
ViewController.dynamicAnimator.getter
ViewController.snapBehavior.getter
ViewController.containerView.getter
MotionView.animate(isSelected:)
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 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  

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

Clause-Local Variables

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

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

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) {
  a++;
}
b = 10;
a = 0;
a = b == 10 ? a + 1 : a
#!/usr/sbin/dtrace -s
#pragma D option quiet  

dtrace:::BEGIN
{
  tracing = 0;
}

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

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

objc$target:UIViewController:-initWithNibName?bundle?:return {
  tracing = 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.png for something, but I can’t remember where. Good thing I have DTrace along with grep to hunt down the location of where Ray.png 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.

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

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/virtualadmin/troll.png"; copyoutstr(this->a, arg0, 31); }'

Other Destructive Actions

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

Key Points

  • Because Swift sits on top of C and Objective-C for now, DTrace can easily trace Swift applications.
  • Use the -q flag with DTrace to limit some of the output.
  • Filter output through grep to limit the volume of output even further.
  • The -F switch will indent function entries and returns.
  • Scalar variables have limited scope but don’t slow execution.
  • Use this-> when working with clause-local variables which can live in different places across a single probe.
  • Use self-> to designate thread-local variables which live the longest, remember to release them though or you’ll leak memory.
  • DTrace uses ternary operators to address the lack of proper conditional branching.
  • DTrace probes can read from and write to memory addresses, potentially causing destruction.

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