The LLDB Debugger - Part 3: Watchpoint
lldb debug watchpoint Estimated reading time: 8 minutesDebugging - is not just a process for finding and fixing bugs - for me, this is a great way to find out how actually the program works. To do so, we should be able to detect any change at any memory address.
Often, breakpoints
can help a lot within this task, but for some cases, such technique simply can’t help us - imagine a case, when we would like to detect a moment, when some constant is read from the memory, of some variable is write to memory. In other words - when some value at a certain memory address is read or written. Breakpoints have no power here. Of cause, accessors (like getter and setter) can help a lot in some cases, but, not always.
Watchpoint instead, does not require some instruction in code to be set, all that needs for them - is an address in memory which we would like to monitor and inspect.
Articles in this series:
- The LLDB Debugger - Part 1: Basics
- The LLDB Debugger - Part 2: Breakpoints
- The LLDB Debugger - Part 3: Watchpoint
watch the memory
context
Thus watchpoint
works with memory address and monitor memory change, we should somehow obtain the address of a variable or use the name of the variable that is available in the current scope.
If we talking about the name of the variable - everything is quite simple - just use its name. But, if we would like to get a notification when some ivar
changes (for example in Obj-C), we should use a memory address. To deal with it, we can use the next command:
(lldb) language objc class-table dump <ClassName> -v
Let’s play a bit. First, let’s create a class:
class SomeClass {
var myVariable: String?
var mySecondVariable: Int?
}
then, we can create an instance and play a bit with this command:
let classObj = SomeClass()
classObj.mySecondVariable = 2
classObj.myVariable = "Hello world!"
The first try:
(lldb) language objc class-table dump SomeClass -v
isa = 0x10219c210 name = _TtC7testApp9SomeClass instance size = 41 num ivars = 0 superclass = _TtCs12_SwiftObject
Let’t add superclass for SomeClass
as NSObject
and repeat operation:
(lldb) language objc class-table dump SomeClass -v
isa = 0x1007381f0 name = _TtC7testApp9SomeClass instance size = 33 num ivars = 0 superclass = NSObject
instance method name = init type = @16@0:8
instance method name = .cxx_destruct type = v16@0:8
As u can see, swift doesn’t provide direct access to ivars
, so an alternative to watchpoints
here is willGet
and willSet
.
Within Obj-C, we can see offset for each ivar
from the base address. To check this out, let’s create a pure Obj-C class:
DemoClass.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface DemoClass : NSObject
@property (nonatomic, copy) NSString *someVariable;
@end
NS_ASSUME_NONNULL_END
DemoClass.m
#import "DemoClass.h"
@implementation DemoClass
@end
And repeating the same operation:
(lldb) po demoClass
<DemoClass: 0x600000794610>
(lldb) language objc class-table dump DemoClass -v
isa = 0x102c54330 name = DemoClass instance size = 16 num ivars = 1 superclass = NSObject
ivar name = _someVariable type = id size = 8 offset = 8
instance method name = setSomeVariable: type = v24@0:8@16
instance method name = someVariable type = @16@0:8
instance method name = .cxx_destruct type = v16@0:8
Now, we can see that ivar
_someVariable
has size 8 and offset 8. Using this information we can grab the base address and add this offset to get ivar
memory address.
(lldb) p/x 0x600000794610 + 8
(long) $1 = 0x0000600000794618
// or
(lldb) print/x 0x600000794610 + 8
(long) $1 = 0x0000600000794618
p
(short forlldb
that allows u to format types in a certain manner./x
meanhex
. Here is the full format list available inlldb
.
Here, we grab the base address of the object in the heap and add an offset equal to 8 (according to class-dumb info). As result, we got the address of ivar
, which holds the NSString
value.
adding the watchpoint
To add a watchpoint
we can use the next command:
w e s -- 0x0000600000794618
w e s
is short forwatchpoint expression set
-lldb
commands can be invoked with a short version of first symbols from commands if there is no conflict within other commands.
Output:
(lldb) w s e -- 0x0000600000794618
Watchpoint created: Watchpoint 1: addr = 0x600000794618 size = 8 state = enabled type = w
new value: -5147790661703735024
And here one more helpful command - list
. These commands similar to the one used within breakpoints
- simply return the list of available watchpoints
:
(lldb) watchpoint list
Number of supported hardware watchpoints: 4
Current watchpoints:
Watchpoint 1: addr = 0x600000794618 size = 8 state = enabled type = w
new value: -5147790661703735024
Now, we can test this. To do so we should simply update the value of someVariable
:
(lldb) e -l objc -O -- [((DemoClass *)0x600000794610) setSomeVariable: @"Hello there!"];
The result will be interrupted due to the existing watchpoint
:
error: Execution was interrupted, reason: watchpoint 1.
The process has been returned to the state before expression evaluation.
As u can see, our watchpoint
works as expected. To be more concrete - u can try to change the variable by any other action (for example button press) - the code will be interrupted and paused on created watchpoint
.
Also, if we check the current value of the someVariable
- it’s updated as expected:
(lldb) e -l objc -O -- [(DemoClass *)0x600000794610 someVariable];
Hello there!
We also can check variable directly:
(lldb) po *((__unsafe_unretained NSString **)(0x600000794618));
Hello there!
we used here double dereference, because we passing a pointer to
NSString *
, and this pointer also a pointer…for debugging purpose we can ommit
__unsafe_unretained
and simply call*(( NSString **)(0x600000794618));
more about
__unsafe_unretained
. Here also a good explanation by Brad Larson
As u can see, watchpoint
is a powerful tool for monitoring any memory-related changes in objects and variables. Above I mention that we can monitor either address of memory either variable - keep in mind, that under variable I mean that we can set watchpoint
even to some ivar
inside another object that is currently available in context:
w s variable <objec->ivar>
Again,
w s
is just a short forwatchpoint set
options
set type
We can also configure few options for watchpoint set expression
:
-s <byte-size> ( --size <byte-size> )
Number of bytes to use to watch a region.
Values: 1 | 2 | 4 | 8
-w <watch-type> ( --watch <watch-type> )
Specify the type of watching to perform.
Values: read | write | read_write
example:
w s e -w write -s 8 -- 0x0000600001414298
set conditions
Another good moment - we can add a condition to watchpoint
as it was done within breakpoint
previously. To do so just create watchpoint
and then modify it using -c
flag:
(lldb) w l
Number of supported hardware watchpoints: 4
Current watchpoints:
Watchpoint 1: addr = 0x600000b2c568 size = 8 state = enabled type = w
old value: -8671835945046180749
new value: -8671835945046279053
(lldb) w modify 1 -c '([*(( NSString **)(0x0000600000b2c568)) isEqual:@"15"])'
1 watchpoints modified.
(lldb) w l
Number of supported hardware watchpoints: 4
Current watchpoints:
Watchpoint 1: addr = 0x600000b2c568 size = 8 state = enabled type = w
old value: -8671835945046180749
new value: -8671835945046279053
condition = '([*(( NSString **)(0x0000600000b2c568)) isEqual:@"15"])'
Here, we list all watchpoints
, then modify the one using condition:
“stop when someVariable
in demoClass
instance object isEqual to 15”.
I added a button and increment
tapCount
value, and insomeVariable
storedstringRepresentation
oftapCount
value.
When this condition is true
, watchpoint is called:
Watchpoint 1 hit:
old value: -8671835945046279053
new value: -8671835945046246285
(lldb) po ([*(( NSString **)(0x0000600000b2c568)) isEqual:@"15"])
true
(lldb) po (NSString *)-8671835945046246285
15

edit
Editing a watchpoint
can be done using modify
command. There are not many ways to edit it:
-c <expr> ( --condition <expr> )
The watchpoint stops only if this condition expression evaluates to true.
Real example of modification already shown above :] .
list
Listing available watchpoints
also can be done using similar command list
:
(lldb) w l
//or
(lldb) watchpoint list
delete
To delete a watchpoint
simple use the same-name command:
(lldb) watchpoint delete 1
(lldb) w delete 1
to delete all - simply omit the last param
understanding context
Setting the watchpoint
is just a half of the job - another part is to understand whats causes the change.
To do so we can use same commands as we used for breakpoints
(thread backtrace
or frame variable <name>
, etc) (read about breakpoints here).
One more good point to mention is when watchpoint hit, we will see a specific message:
Watchpoint 1 hit:
old value: -8671887165918691125
new value: -8671835945045623701
This is nothing but values. To check the values:
(lldb) po (NSString *)-8671887165918691125
Hello
(lldb) po (NSString *)-8671835945045623701
Hello there!
In addition, we can use few more useful commands:
(lldb) disassemble -m -F intel
On my M1 mac I got an error - unsupported flavor:
(lldb) disassemble -m -F intel
error: Disassembler flavors are currently only supported for x86 and x86_64 targets.
read more about this command here
Articles in this series:
- The LLDB Debugger - Part 1: Basics
- The LLDB Debugger - Part 2: Breakpoints
- The LLDB Debugger - Part 3: Watchpoint
Resources
Share on: