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

11. Assembly Register Calling Convention
Written by Derek Selander

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 scary, 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 a 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:

pushq   %rbx
subq    $0x228, %rsp 
movq    %rdi, %rbx 

In this block of assembly, you see three opcodes, pushq, subq, and movq. 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 rbx, rsp, rdi, and rbp. The % before each tells you this is a register.

In addition, you can also find a numeric constant in hexadecimal shown as 0x228. The $ before this constant tells you it’s an absolute number.

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.

Note: In the above example, take note there are a bunch of %’s and $’s that precede the registers and constants. This is how the disassembler formats the assembly. However, there are two main ways that assembly can be showcased. The first is Intel assembly, and the second is AT&T assembly.

By default, Apple’s disassembler tools ship with assembly displayed in the AT&T format, as it is in the example above. Although this is a good format to work with, it can be a little hard on the eyes. In the next chapter, you’ll change the assembly format to Intel, and will work exclusively with Intel assembly syntax from there on out.

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 is the architecture most likely used on your macOS computer, unless you are running an “ancient” Macintosh.

uname -m 

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

NSString *name = @"Zoltan";
NSLog(@"Hello world, I am %@. I’m %d, and I live in %@.", name, 30, @"my father’s basement");
RDI = @"Hello world, I am %@. I’m %d, and I live in %@.";
RSI = @"Zoltan";
RDX = 30;
RCX = @"my father’s basement";
NSLog(RDI, RSI, RDX, RCX);

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:@"Clowns will eat me"];
NSString *helloWorldString; 
helloWorldString = objc_msgSend(@"Can't Sleep; ", "stringByAppendingString:", @"Clowns will eat me");

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
RDI = UIViewControllerInstance 
RSI = "viewDidLoad"
objc_msgSend(RDI, RSI)
(lldb) po $rdi 
<Registers.ViewController: 0x6080000c13b0>
(lldb) po $rsi 
140735181830794
(lldb) po (char *)$rsi 
"viewDidLoad"
(lldb) po (SEL)$rsi
(lldb) b -[NSResponder mouseUp:]
(lldb) continue

(lldb) po $rdi 
<NSView: 0x608000120140>
(lldb) po $rdx 
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 [$rdx class]
(lldb) breakpoint delete
About to delete all breakpoints, do you want to do that?: [Y/n] 
(lldb) breakpoint set -o true -S "-[NSWindow mouseDown:]"
(lldb) continue
(lldb) po [$rdi 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: 1, two: 2, three: 3, four: 4,
                            five: 5, six: 6, seven: 7,
                            eight: 8, nine: 9, ten: 10)
(lldb) register read -f d 
General Purpose Registers:
       rax = 106377750908800
       rbx = 106377750908800
       rcx = 4
       rdx = 3
       rdi = 1
       rsi = 2
       rbp = 140732920750896
       rsp = 140732920750776
        r8 = 5
        r9 = 6
       r10 = 4294981504  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) -> Swift.String at ViewController.swift:39
       r11 = 105827995224480
       r12 = 4314945952
       r13 = 106377750908800
       r14 = 88
       r15 = 4314945952
       rip = 4294981504  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) -> Swift.String at ViewController.swift:39
    rflags = 518
        cs = 43
        fs = 0
        gs = 0

RAX, the return register

But wait — there’s more! So far, you’ve learned how six 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 rax -fd 
     rax = 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 X (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 $rdx = 0x0000618000644080" -G1
(lldb) continue

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