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

11. Assembly Register Calling Convention
Written by Walter Tyree

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Now that you’ve gained a basic understanding of how to maneuver around the debugger, it’s time to take a step down the executable Jenga tower and explore the 1s and 0s that make up your source code. This section will focus on the low-level aspects of debugging.

In this chapter, you’ll look at registers the CPU uses and explore and modify parameters passed into function calls. You’ll also learn about common Apple computer architectures and how their registers are used within a function. This is known as an architecture’s calling convention.

Knowing how assembly works and how a specific architecture’s calling convention works is an extremely important skill to have. It lets you observe function parameters you don’t have the source code for and lets you modify the parameters passed into a function. In addition, it’s sometimes even better to go to the assembly level because your source code could have different or unknown names for variables you’re not aware of.

For example, let’s say you always wanted to know the second parameter of a function call, regardless of what the parameter’s name is. Knowledge of assembly gives you a great base layer to manipulate and observe parameters in functions.

Assembly 101

Wait, so what’s assembly again?

Have you ever stopped in a function you didn’t have source code for, and saw an onslaught of memory addresses followed by cryptic, short commands? Did you huddle in a ball and quietly whisper to yourself you’ll never look at this dense stuff again? Well… that stuff is known as assembly!

Here’s a picture of a backtrace in Xcode, which showcases the assembly of a function within the Simulator.

Looking at the image above, the assembly can be broken into several parts. Each line in an assembly instruction contains an opcode, which can be thought of as an extremely simple instruction for the computer.

So what does an opcode look like? An opcode is an instruction that performs a simple task on the computer. For example, consider the following snippet of assembly:

stp    x0, x1, [sp]
sub    x0, x8, #0x3
mov    x8, x20

In this nonsense block of assembly, you see three opcodes, stp, sub, and mov. Think of the opcode items as the action to perform. The things following the opcode are the source and destination labels. That is, these are the items the opcode acts upon.

In the above example, there are several registers, shown as x0, x8, x1, and sp. Most registers begin with x or r but there are some special use registers like sp, fp and xzr.

In addition, you can also find a numeric constant in hexadecimal shown as 0x3. The # before this constant tells you it’s an absolute number. A hexadecimal number by itself is almost always a memory location.

There’s no need to know what this code is doing at the moment, since you’ll first need to learn about the registers and calling convention of functions. Then you’ll learn more about the opcodes and write your own assembly in a future chapter. Remember, though, the focus in this book is to be able to read and follow assembly to help in debugging, not to write a bunch of assembly.

x86_64 vs ARM64

As a developer for Apple platforms, there are two primary architectures you’ll deal with when learning assembly: x86_64 architecture and arm64 architecture. x86_64 was the architecture used on macOS computers with “Intel” CPUs. ARM64 is the architecture used on iOS devices and macOS computers with “Apple Silicon”.

uname -m

arm64 Register Calling Convention

Your CPU uses a set of registers in order to manipulate data in your running program. These are storage holders, just like the RAM in your computer. However they’re located on the CPU itself very close to the parts of the CPU that need them. So these parts of the CPU can access these registers incredibly quickly. Also, there are a finite number of registers.

let fName = "Zoltan"
0x1044757a8 <+124>: adrp   x0, 8
0x1044757ac <+128>: add    x0, x0, #0x770            ; "Zoltan"
0x1044757b0 <+132>: mov    w8, #0x6
0x1044757b4 <+136>: mov    x1, x8
0x1044757b8 <+140>: mov    w8, #0x1
0x1044757bc <+144>: str    w8, [sp, #0x4]
0x1044757c0 <+148>: and    w2, w8, #0x1
0x1044757c4 <+152>: bl     0x10447b3f0               ; symbol stub for: Swift.String.init(_builtinStringLiteral: Builtin.RawPointer, utf8CodeUnitCount: Builtin.Word, isASCII: Builtin.Int1) -> Swift.String

Objective-C and Registers

As you learned in the previous section, registers use a specific calling convention. You can take that same knowledge and apply it to other languages as well.

[UIApplication sharedApplication];
id UIApplicationClass = [UIApplication class];
objc_msgSend(UIApplicationClass, "sharedApplication");
NSString *helloWorldString = [@"Can't Sleep; " stringByAppendingString:@"Coffee for dessert was unwise."];
NSString *helloWorldString;
helloWorldString = objc_msgSend(@"Can't Sleep; ", "stringByAppendingString:", @"Coffee for dessert was unwise.");

Putting Theory to Practice

For this section, you’ll be using a project supplied in this chapter’s resource bundle called Registers.

(lldb) register read
x0 = UIViewControllerInstance
x1 = "viewDidLoad"
objc_msgSend(x0, x1)
(lldb) po $x0
<Registers.ViewController: 0x6080000c13b0>
(lldb) po $x1
8211036373
(lldb) po (char *)$x1
"viewDidLoad"
(lldb) po (SEL)$x1
(lldb) b -[NSResponder mouseUp:]
(lldb) continue

(lldb) po $x0
<NSView: 0x11d62e010>
(lldb) po $x2
NSEvent: type=LMouseUp loc=(351.672,137.914) time=175929.4 flags=0 win=0x6100001e0400 winNum=8622 ctxt=0x0 evNum=10956 click=1 buttonNumber=0 pressure=0 deviceID:0x300000014400000 subtype=NSEventSubtypeTouch
(lldb) po [$x2 class]
(lldb) breakpoint delete
About to delete all breakpoints, do you want to do that?: [Y/n]
(lldb) b "-[NSWindow mouseDown:]"
(lldb) continue
(lldb) po [$x0 setBackgroundColor:[NSColor redColor]]
(lldb) continue

Swift and Registers

When exploring registers in Swift you’ll hit three hurdles that make assembly debugging harder than it is in Objective-C.

func executeLotsOfArguments(one: Int, two: Int, three: Int,
                            four: Int, five: Int, six: Int,
                            seven: Int, eight: Int, nine: Int,
                            ten: Int) {
    print("arguments are: \(one), \(two), \(three),
          \(four), \(five), \(six), \(seven),
          \(eight), \(nine), \(ten)")
}
self.executeLotsOfArguments(
  one: 31, two: 32, three: 33, four: 34,
  five: 35, six: 36, seven: 37,
  eight: 38, nine: 39, ten: 40)
(lldb) register read -f d
General Purpose Registers:
        x0 = 1
        x1 = 8419065840  libswiftCore.dylib`type metadata for Any + 8
        x2 = 33
        x3 = 34
        x4 = 35
        x5 = 36
        x6 = 37
        x7 = 38
        x8 = 1
        x9 = 40
       x10 = 39
       x11 = 32
       x12 = 8419006000  libswiftCore.dylib`protocol witness table for Swift.Int : Swift.CustomStringConvertible in Swift
       x13 = 105553126854192
       x14 = 2161727825501079277 (0x000000010411c6ed) (void *)0x01f4c6b578000000
       x15 = 8401620944  (void *)0x00000001f4c71400: NSResponder
       x16 = 8401620944  (void *)0x00000001f4c71400: NSResponder
       x17 = -424042045551510852 (0x0000000199127abc) libobjc.A.dylib`-[NSObject release]
       x18 = 0
       x19 = 105553139451872
       x20 = 6103686920
       x21 = 105553160866960
       x22 = 0
       x23 = 4294967300
       x24 = 1
       x25 = 8434082064  @"Found circular dependency when loading dependencies for %@ and %@"
       x26 = 5192684800
       x27 = 21474836484
       x28 = 8359120896  AppKit`_OBJC_PROTOCOL_REFERENCE_$_NSSecureCoding
        fp = 6103687040
        lr = 4363202980  Registers`Registers.ViewController.viewDidLoad() -> () + 192 at ViewController.swift:61:3
        sp = 6103686528
        pc = 4363203312  Registers`Registers.ViewController.executeLotsOfArguments(one: Swift.Int, two: Swift.Int, three: Swift.Int, four: Swift.Int, five: Swift.Int, six: Swift.Int, seven: Swift.Int, eight: Swift.Int, nine: Swift.Int, ten: Swift.Int) -> () + 228 at ViewController.swift:67:13
      cpsr = 1610616832
(lldb) disassemble

The Return Register

But wait — there’s more! So far, you’ve seen how registers are called in a function, but what about return values?

func executeLotsOfArguments(one: Int, two: Int, three: Int,
                            four: Int, five: Int, six: Int,
                            seven: Int, eight: Int, nine: Int,
                            ten: Int) -> Int {
    print("arguments are: \(one), \(two), \(three), \(four),
          \(five), \(six), \(seven), \(eight), \(nine), \(ten)")
    return 100
}
override func viewDidLoad() {
    super.viewDidLoad()
    _ = self.executeLotsOfArguments(one: 1, two: 2,
          three: 3, four: 4, five: 5, six: 6, seven: 7,
          eight: 8, nine: 9, ten: 10)
}
(lldb) finish
(lldb) re re x0 -fd
     x0 = 100

Changing Around Values in Registers

In order to solidify your understanding of registers, you’ll modify registers in an already-compiled application.

xcrun simctl list
iPhone 14 (DE1F3042-4033-4A69-B0BF-FD71713CFBF6) (Shutdown)
open /Applications/Xcode.app/Contents/Developer/Applications/Simulator.app --args -CurrentDeviceUDID DE1F3042-4033-4A69-B0BF-FD71713CFBF6
lldb -n SpringBoard
(lldb) p/x @"Yay! Debugging"
(__NSCFString *) $3 = 0x0000618000644080 @"Yay! Debugging!"
(lldb) br set -n  "-[UILabel setText:]" -C "po $x2 = 0x0000618000644080" -G1
(lldb) continue

Key Points

  • Architectures define a calling convention which dictates where parameters to a function and its return value are stored.
  • In Objective-C, the x0 register is the reference of the calling NSObject, x1 is the Selector, x2 is the first parameter and so on.
  • In Swift, there’s still not a consistent register calling convention. For right now, the reference to “self” in a class is passed on the stack allowing the parameters to start with the x2 register. But who knows how long this will last and what crazy changes will take place as Swift evolves.
  • The x0 register is used for return values in functions regardless of whether you’re working with Objective-C or Swift.
  • Make sure you use the Objective-C context when printing registers with $.

Where to Go From Here?

Whew! That was a long one, wasn’t it? Sit back and take a break with your favorite form of liquid; you’ve earned it.

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 accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now