Heads up... You're reading this book for free, with parts of this chapter shown beyond this point astext.
You’ve learned how to create breakpoints on executable code; that is, memory that has read and execute permissions. But using only breakpoints leaves out an important component to debugging — you can monitor when the instruction pointer executes an address, but you can’t monitor when memory is being read or written to. You can’t monitor value changes to instantiated Swift objects on the heap, nor can you monitor reads to a particular address (say, a hardcoded string) in memory. This is where a watchpoint comes into play.
A watchpoint is a special type of breakpoint that can monitor reads or writes to a particular value in memory and is not limited to executable code as are breakpoints. However, there are limitations to using watchpoints: there are a finite amount of watchpoints permitted per architecture (typically 4) and the “watched” size of memory usually caps out at 8 bytes.
Watchpoint best practices
Like all debugging techniques, a watchpoint is a type of tool in the debugging toolbox. You’ll likely not use this tool very often, but it can be extremely useful in certain situations. Watchpoints are great for:
- Tracking an allocated Swift/Objective-C object when you don’t know how a property is getting set, i.e. via direct ivar access, Objective-C property setter method, Swift property setter method, hardcoded offset access, or other methods.
- Monitoring when a hardcoded string is being utilized, such as in a
- Monitor the instruction pointer for a particular type of assembly instruction.
Finding a property’s offset
Watchpoints are great for discovering how a particular piece of memory is being written to. A pratical example of this is when a value is written to a previously allocated instance created from the heap, such as in an Objective-C/Swift class.
(lldb) language objc class-table dump UnixSignalHandler -v
isa = 0x10e843d90 name = UnixSignalHandler instance size = 56 num ivars = 4 superclass = NSObject ivar name = source type = id size = 8 offset = 24 ivar name = _shouldEnableSignalHandling type = bool size = 1 offset = 32 ivar name = _signals type = id size = 8 offset = 40 ivar name = _sharedUserDefaults type = id size = 8 offset = 48 instance method name = setShouldEnableSignalHandling: type = v20@0:8B16 ...
(lldb) p/x 0x6000024d0f40 + 32 (long) $0 = 0x00006000024d0f60
(lldb) watchpoint set expression -s 1 -w write -- 0x00006000024d0f60
What caused the watchpoint
What exactly caused the watchpoint to be triggered? Caffeinate up, you’ll be looking at a bit of assembly now. To find out, use LLDB to disassemble the current method.
(lldb) disassemble -F intel -m
0x100c04be7 <+39>: mov byte ptr [rsi + rdi], al
*(BOOL *)(rsi + rdi) = al
(lldb) p/x $rsi + $rdi
(lldb) watchpoint list
(lldb) po $rsi + $rdi - 32 <UnixSignalHandler: 0x6000024d0f40>
self->_shouldEnableSignalHandling = shouldEnableSignalHandling;
The Xcode GUI watchpoint equivalent
Xcode provides a GUI for setting watchpoints. You could perform the equivalent of the above methods by setting a breakpoint on the creation method of the
UnixSignalHandler singleton, then set a watchpoint via the GUI. First though, you need to delete the previous watchpoint.
(lldb) watchpoint delete About to delete all watchpoints, do you want to do that?: [Y/n] Y All watchpoints removed. (1 watchpoints) (lldb) c Process 68247 resuming
Other watchpoint tidbits
Fortunately, the syntax for watchpoints is very similar to the syntax for breakpoints. You can
modify them just as you would using LLDB’s breakpoint syntax.
(lldb) watchpoint list -b Number of supported hardware watchpoints: 4 Current watchpoints: Watchpoint 2: addr = 0x60000274ee20 size = 1 state = enabled type = w
(lldb) watchpoint modify 2 -c '*(BOOL*)0x60000274ee20 == 0'
(lldb) watchpoint modify 2
(lldb) watchpoint command add 2 Enter your debugger command(s). Type 'DONE' to end. > bt 5 > continue > DONE
(lldb) watchpoint command delete 2
Where to go from here?
Watchpoints tend to play very nicely with those who understand how an executable is laid out in memory. This layout, known as Mach-O, will be discussed in detail in Chapter 18, “Hello, Mach-O”. Combining this knowledge with watchpoints, you can watch when strings are referenced, or when static pointers are intialized, without having to tediously track the locations at runtime.