25.
Script Bridging with SBValue & Memory
Written by Derek Selander
So far, when evaluating JIT code (i.e. Objective-C, Swift, C, etc. code that’s executed through your Python script), you’ve used a small set of APIs to evaluate the code.
For example, you’ve used SBDebugger and SBCommandReturnObject’s HandleCommand method to evaluate code. SBDebugger’s HandleCommand goes straight to stderr, while you have a little more control over where the SBCommandReturnObject result ends up. Once evaluated, you had to manually parse the return output for anything of interest. This manual searching of the output from the JIT code is a bit unsightly. Nobody likes stringly typed things!
So, it’s time to talk about a new class in the lldb Python module, SBValue, and how it can simplify the parsing of JIT code output. Open up the Xcode project named Allocator in the starter folder for this chapter. This is a simple application which dynamically generates classes based upon input from a text field.
This is accomplished by taking the string from the text field and using it as an input to the NSClassFromString function. If a valid class is returned, it’s initialized using the plain old init method. Otherwise, an error is spat out.
Build and run the application on any iOS 12 Simulator. You’ll make zero modifications to this project, yet you’ll explore object layouts in memory through SBValue, as well as manually with pointers through LLDB.
A detour down memory layout lane
To truly appreciate the power of the SBValue class, you’re going to explore the memory layout of three unique objects within the Allocator application. You’ll start with an Objective-C class, then explore a Swift class with no superclass, then finally explore a Swift class that inherits from NSObject.
All three of these classes have three properties with the following order:
- A
UIColorcalledeyeColor. - A language specific string (
String/NSString) calledfirstName. - A language specific string (
String/NSString) calledlastName.
Each instance of these classes is initialized with the same values. They are:
-
eyeColorwill beUIColor.brownor[UIColor brownColor]depending on language. -
firstNamewill be"Derek"or@"Derek"depending on language. -
lastNamewill be"Selander"or@"Selander"depending on language.
Objective-C memory layout
You’ll explore the Objective-C class first, as it’s the foundation for how these objects are laid out in memory. Jump over to the DSObjectiveCObject.h and take a look at it. Here it is for your reference:
@interface DSObjectiveCObject : NSObject
@property (nonatomic, strong) UIColor *eyeColor;
@property (nonatomic, strong) NSString *firstName;
@property (nonatomic, strong) NSString *lastName;
@end
As mentioned earlier, there are three properties: eyeColor, firstName, and lastName in that order.
Jump over to the implementation file DSObjectiveCObject.m and give it a gander to understand what’s happening when this Objective-C object is initialized:
@implementation DSObjectiveCObject
- (instancetype)init
{
self = [super init];
if (self) {
self.eyeColor = [UIColor brownColor];
self.firstName = @"Derek";
self.lastName = @"Selander";
}
return self;
}
@end
Nothing too crazy. The properties will be initialized to the values just described above.
When this is compiled, this Objective-C class will actually look like a C struct. The compiler will create a struct similar to the following pseudocode:
struct DSObjectiveCObject {
Class isa;
UIColor *eyeColor;
NSString *firstName
NSString *lastName
}
Take note of the Class isa variable as the first parameter. This is the magic behind an Objective-C class being considered an Objective-C class. This isa value is always the first value in an object instance’s memory layout, and is a pointer to the class the object is an instance of. After that, the properties are added to this struct in the order they were written in your source code.
Let’s see this in action through LLDB. Perform the following steps:
-
Make sure the DSObjectiveCObject is selected in the
UIPickerView. -
Tap on the Allocate Class button.
-
Once the reference address is spat out in the console, copy that address to your clipboard.
-
Pause execution and bring up the LLDB console window.
An instance of the DSObjectiveCObject has been created. You’ll now use LLDB to spelunk into offsets of this object’s contents.
Copy the memory address from the console output and make sure po’ing it will give you a valid reference (e.g. you’re not stopped on a Swift stack frame when printing out this address).
For my case, I got the pointer 0x600000031f80. As always, yours will be different. Print out the address through LLDB:
(lldb) po 0x600000031f80
You should get this expected line of output:
<DSObjectiveCObject: 0x600000031f80>
Since this can be treated as a C struct, you’ll start spelunking into offsets of this pointer’s contents.
In the LLDB console, type the following (replacing the pointer with yours):
(lldb) po *(id *)(0x600000031f80)
This casts the memory address to a pointer-to-an-id and then dereferences it. This will therefore give you access the object’s isa pointer.
You should see this output:
DSObjectiveCObject
That’s the class object’s description, as expected.
Let’s look at another way of viewing this memory. Use the x command (aka examine, a port from GDBs popularity with this command) to jump to the starting pointer, then po it. Enter the following:
(lldb) x/gx 0x600000031f80
This command says the following:
- Examine the memory (
x) - Print out the size of a giant word, (64 bits, or 8 bytes) (
g) - Finally, format it in hexadecimal (
x).
If, hypothetically, you only wanted to view the first byte at this location in binary instead, you could type x/bt 0x600000031f80 instead. This would be interpreted as examine (x), a byte (b) in binary (t). The examine command is definitely one of those nice commands to keep in your toolkit when exploring memory.
You’ll see the following output (or at least, similar output, as the values will be different for you):
0x600000031f80: 0x0000000108b06568
This gives you output that tells you the value at memory address 0x600000031f80 contains 0x0000000108b06568. Well, it does for me!
Jumping back to the task at hand, take the address printed out by the x/gx command and print out this new address using po.
(lldb) po 0x0000000108b06568
Once again, this will print out the isa class, which is the DSObjectiveCObject class. This is an alternative way to print out the isa instance, which might give more insight into what’s happening. However, that took two LLDB commands instead of one, so you’ll stick to dereferencing the pointer and not use the x/gx command.
Let’s jump a little further into the eyeColor property. In the LLDB console:
(lldb) po *(id *)(0x600000031f80 + 0x8)
This says “start at 0x600000031f80 (or equivalent), go up 8 bytes and get the contents pointed at by this pointer.” You’ll get the following output:
UIExtendedSRGBColorSpace 0.6 0.4 0.2 1
How did I get to the number 8? Try this out in LLDB:
(lldb) po sizeof(Class)
The isa variable is of type Class. So by knowing how big a Class is, you know how much space that takes up in the struct, and therefore you know the offset of eyeColor.
Note: When working with 64-bit architectures (x64 or ARM64), all pointers will be 8 bytes. In addition, the
Classclass itself is a pointer to a C struct not defined in the headers. This means in 64-bit architecture, all you need to do to move between different pointers is to jump by 8 bytes!There are types which are different sizes in bytes, such as
int,short,booland others, and the compiler may pad that memory to fit into a predefined size. However, there’s no need to worry about that for now, since thisDSObjectiveCObjectclass only contains pointers toNSObjectsubclasses, along with theClassobject held in theisavariable.
Keep on going. Increment the offset by another 8 bytes in LLDB:
(lldb) po *(id *)(0x600000031f80 + 0x10)
You’re adding another 8 to get 0x10 in hexadecimal (or 16 in decimal). You’ll get @"Derek", which is the contents of the firstName property. Increment by yet another 8 bytes to get the lastName property:
(lldb) po *(id *)(0x600000031f80 + 0x18)
You’ll get @"Selander". Cool, right? Let’s visually revisit what you just did to hammer this home:
You started at a base address that pointed to the instance of DSObjectiveCObject. For this particular example, this starting address is at 0x600000031f80. You started by dereferencing this pointer, which gave you the isa variable, then you jumped by offsets of 8 bytes to the next Objective-C property, dereferenced the pointer at that offset, cast it to type id and spat it out to the console.
Spelunking memory is a fun and instructional way to see what’s happening behind the scenes. This lets you appreciate the SBValue class even more. But you’re not at the point of talking about the SBValue class, as you still have two more classes to explore. The first is a Swift class with no superclass, and the second is a Swift class which inherits from NSObject. You’ll explore the non superclass Swift object first.
Swift memory layout with no superclass
Note: It’s worth mentioning right up front: the Swift ABI is still fluctuating. This means the information below could change before the Swift ABI completes. The day a new version of Xcode breaks the information in the following section, feel free to complain in ALL CAPS in the forums!
Time to explore a Swift class with no superclass! In the Allocator project, jump to ASwiftClass.swift and take a look at what’s there.
class ASwiftClass {
let eyeColor = UIColor.brown
let firstName = "Derek"
let lastName = "Selander"
required init() { }
}
Here, you have the Swift equivalent for DSObjectiveCObject with the obvious “Swifty” changes.
Again, you can imagine this Swift class as a C struct with some interesting differences from its Objective-C counterpart. Check out the following pseudocode:
struct ASwiftClass {
Class isa;
// Simplified, see "InlineRefCounts"
// in https://github.com/apple/swift
uintptr_t refCounts;
UIColor *eyeColor;
// Simplified, see "_StringGuts"
// in https://github.com/apple/swift
struct _StringCore {
uintptr_t _object; // packed bits for string type
uintptr_t rawBits; // raw data
} firstName;
struct _StringCore {
uintptr_t _object; // packed bits for string type
uintptr_t rawBits; // raw data
} lastName;
}
Pretty interesting right? You still have that isa variable as the first parameter.
After the isa variable, there’s an eight byte variable reserved for reference counting and alignment called refCounts. This differs to your typical Objective-C object which doesn’t contain this varible at this offset.
Next, you have the normal UIColor, but that’s where this ASwiftClass struct goes completely off the rails.
A Swift String is a very interesting “object”. In fact, a Swift String is a struct within the ASwiftClass struct. You can think of a Swift string as sort of a facade design pattern that hides different types of Swift string types based upon if they are hardcoded, Cocoa, use ASCII, etc. To make it even more interesting, the types and layout will differ if Swift is compiled for a 32-bit or 64-bit platform. For the sake of simplicity, only the 64-bit platform will be discussed.
For 64-bit platforms, the memory layout of a Swift String comprises 16 bytes with the structural layout depending on the type of String. That is, you will need to first determine the String’s type before you can correctly parse the String’s contents.
So how can one determine the type? By consulting the documentation available on the Swift repo!
This documentation is taken from the https://github.com/apple/swift/blob/master/stdlib/public/core/StringObject.swift for Swift 4.2
// ## _StringObject bit layout
//
// x86-64 and arm64: (one 64-bit word)
// +---+---+---|---+------+------------------------------------------+
// + t | v | o | w | uuuu | payload (56 bits) |
// +---+---+---|---+------+------------------------------------------+
// most significant bit least significatn bit
//
// where t: is-a-value, i.e. a tag bit that says not to perform ARC
// v: sub-variant bit, i.e. set for isCocoa or isSmall
// o: is-opaque, i.e. opaque vs contiguously stored strings
// w: width indicator bit (0: ASCII, 1: UTF-16)
// u: unused bits
//
// payload is:
// isNative: the native StringStorage object
// isCocoa: the Cocoa object
// isOpaque & !isCocoa: the _OpaqueString object
// isUnmanaged: the pointer to code units
// isSmall: opaque bits used for inline storage // TODO: use them!
//
From the documentation, the t, v, o, w bits are used to help determine the type of Swift String. The following 4 u bits will be used by the specific string type. That is, the first 4 bits of the _object variable for the _StringCore struct mentioned above will give you this information.
The layout of the Swift String struct makes the assembly calling convention rather interesting. If you pass a String to a function, it will actually pass in two parameters (and use two registers) instead of a pointer to a struct containing the two parameters (in one register). Don’t believe me? Check it out yourself when you’re done with this chapter!
Back to LLDB and jumping through an object.
Clear the LLDB screen with a ⌘ + K, then resume the application through LLDB or the Xcode GUI.
You’re going to do the exact same thing with the ASwiftClass that you did with DSObjectiveCObject. Use the developer/designer “approved” UIPickerView and select Allocator.SwiftClass. Remember, to correctly reference a Swift class (i.e. in NSClassFromString and friends), you need the module name prepended to the classname with a period separating the two.
Tap the Allocate Class button and copy the memory address spat out to the console.
You’ll get something similar to the following:
<Allocator.ASwiftClass: 0x61800009d830>
Usually, Swift hides the pointer in the description and debugDescription methods, but there’s something sneaky compiled into this project that you’ll come across in a second.
For now, grab that memory address and stick it in the clipboard.
First use LLDB to ensure it’s valid, by po-ing it:
(lldb) po 0x61800009d830
If you get something different than the following, you should be more than somewhat surprised:
<Allocator.ASwiftClass: 0x61800009d830>
Even though this is a pure Swift object, you were able to get the dynamic description in the Objective-C context. That means you can climb the class hierarchy to see the parent class!
(lldb) po [0x61800009d830 superclass]
You’ll get an interesting class with the name of:
SwiftObject
You’ll explore this class more in a second. For now, start jumping around in memory. Dereference the pointer’s address and prove to yourself that the first parameter is that isa Class variable:
(lldb) po *(id *)0x61800009d830
You’ll get Allocator.ASwiftClass. Now check out that reference counter variable:
(lldb) po *(id *)(0x61800009d830 + 0x8)
You’ll get something similar to the following:
0x0000000000000002
It’s clear that the address here is not a Objective-C address, since *(id *)0x0000000200000004 would point to a class if it were a valid instance/class. Instead, this is the reference counter unique to Swift classes. Let’s see how this thing works.
Use LLDB to manually retain this class:
(lldb) po [0x61800009d830 retain]
Press the up arrow twice to retrieve the previous command and execute it again:
(lldb) po *(id *)(0x61800009d830 + 0x8)
You’ll now get a slightly different number:
0x0000000200000002
Notice the middle hex value jumped up by 2. Shooting from the hip, this giant word should actually be viewed as 2 separate integer (32-bit) fields instead of one 64-bit field. See if release’ing this reference brings the count back down:
(lldb) po [0x61800009d830 release]
Yep, up arrow twice again, then Enter.
(lldb) po *(id *)(0x61800009d830 + 0x8)
You’ll get your happy, original value:
0x0000000000000002
Now that you’ve got past the isa and the refCounts it’s time to turn your attention to those lovely properties in the ASwiftClass instance.
Clear the screen to start fresh, then increment your offset amount in LLDB.
(lldb) po *(id *)(0x61800009d830 + 0x10)
You’ll get the internal representation of UIColor’s brown:
UIExtendedSRGBColorSpace 0.6 0.4 0.2 1
Jump another 8 bytes and start exploring the firstName object’s structure. You’re going to change around how you view the display of the data. Remember those first 4 bits of a Swift String that determine the type? You will examine those now.
Type the following:
(lldb) x/gt '0x61800009d830 + 0x18'
You’ll get the following:
0x600003c3aa18: 0b1110010100000000000000000000000000000000000000000000000000000000
Check out those first four bits on the far left. You can tell that:
- bit 0(t): This object is not reference counted with ARC, which explains why the value initially had zero when executing the
retainmethod earlier. - bit 1(v): The variant is set, for this case, the String is internally known as a Swift small String. More on that in a second.
- bit 2(o): This instance is stored as an opaque string
- bit 3(w): The value is not set, which means that ASCII is used for this reference.
This String reference is a small String, which is a Swift String that takes up less than 15 bytes. That means all of the contents can be referenced inside of the Swift String struct (try saying that 3 times fast). If the string was greater than 15 bytes, a pointer would be needed to reference the data instead of just packing it into the 16 byte struct (15 bytes for data, 1 byte for type and string length).
The layout of the small String types can be found here: https://github.com/apple/swift/blob/master/stdlib/public/core/SmallString.swift
Here is a simplified C layout of the UTF-8 small Swift String:
typedef struct {
char spillover[7];
char bits; // msb (tvow) bit types, lsb (uuuu) string length
char start[8]; // start address of String
} SmallUTF8String;
In this struct, if a string has a length greater than 8 bytes, the spillover variable is used to start the remaining characters. There’s also the bits value, which stores the type as well as count on the lower 4 bits.
Use LLDB to explore the layout of the firstName variable at the start offset in LLDB:
(lldb) x/s '0x61800009d830 + 0x20'
"Derek"
Since the String contains the value “Derek”, the start offset can be used.
What about the length of the string? You can easily view the data by typing the following:
(lldb) x/gx '0x61800009d830 + 0x18'
You’ll see something like:
0x61800009d848: 0xe500000000000000
That 5 is the value you want. This is the lower 4 bits in the most significant side (represented by the bits variable in the SmallUTF8String struct). To truly isolate this you can execute the following:
p/d *(int *)(0x61800009d830 + 0x18 + 7) & 0xf
This will give the String’s length, which has to be 0-15 due to the limiation length of a small Swift string.
No need to go after the lastName property. You’ve got the idea of how this works. If you did want to hunt for that on your own time, you would want to start at offset 0x28 of the ASwiftClass instance.
Swift memory layout with NSObject superclass
Final one. You know the drill, so we’ll speed this one up a bit and skip the actual debugging session.
Check out the sourcecode for ASwiftNSObjectClass.swift:
class ASwiftNSObjectClass: NSObject {
let eyeColor = UIColor.brown
let firstName = "Derek"
let lastName = "Selander"
required override init() { }
}
It’s the same thing as the ASwiftClass, except it inherits from NSObject instead of from nothing.
So is there any difference in the generated C struct pseudocode?
struct ASwiftNSObjectClass {
Class isa;
UIColor *eyeColor;
struct _StringCore {
uintptr_t _object;
uintptr_t rawBits;
} firstName;
struct _StringCore {
uintptr_t _object;
uintptr_t rawBits;
} lastName;
}
Almost! The only difference is that the ASwiftNSObjectClass instance is missing the refCounts variable at offset 0x8, the rest of the layout in memory will be the same.
Let’s skip the debugging session and just talk about what will happen when you try retain’ing an instance of this class: the refCounts variable will not be modified. This makes sense because Objective-C has its own implementation of retain/release that’s different from the Swift implementation.
You can finally look at the SBValue class I’ve been itching to describe to you!
SBValue
Yay! Time to talk about this awesome class.
SBValue is responsible for interpreting the parsed expressions from your JIT code. Think of SBValue as a representation that lets you explore the members within your object, just as you did above, but without all that ugly dereferencing. Within the SBValue instance, you can easily access all members of your struct… er, I mean, your Objective-C or Swift classes.
Within the SBTarget and SBFrame class, there’s a method named EvaluateExpression, which will take your expression as a Python str and return an SBValue instance. In addition, there’s an optional second parameter that lets you specify how you want your code to be parsed. You’ll start without the optional second parameter, and explore it later.
Jump back into the LLDB console and make sure the Allocator project is still running. Make sure the LLDB console is up (i.e. the program is paused), clear the console and type the following:
(lldb) po [DSObjectiveCObject new]
You’ll get something similar to the following:
<DSObjectiveCObject: 0x61800002eec0>
This ensures you can create a valid instance of a DSObjectiveCObject.
This code works, so you can apply it to the EvaluateExpression method of either the global SBTarget or SBFrame instance:
(lldb) script lldb.frame.EvaluateExpression('[DSObjectiveCObject new]')
You’ll get the usual cryptic output with the class but no context to describe what this does:
<lldb.SBValue; proxy of <Swig Object of type 'lldb::SBValue *' at 0x10ac78b10> >
You’ve got to use print to get context for these classes:
(lldb) script print lldb.target.EvaluateExpression('[DSObjectiveCObject new]')
You’ll get your happy debugDescription you’ve become accustomed to.
(DSObjectiveCObject *) $2 = 0x0000618000034280
Note: If you mistype something, you’ll still get an instance of
SBValue, so make sure it’s printed out the item you expect it should. For example, if you mistyped the JIT code, you might get something like** = <could not resolve type>**from theSBValue.You can verify the
SBValuesucceeded by checking theSBErrorinstance within yourSBValue. If yourSBvaluewas namedsbval, you could dosbval.GetError().Success(), or more simplysbval.error.success.
Modify this command so you’re assigning it to the variable a inside the Python context:
(lldb) script a = lldb.target.EvaluateExpression('[DSObjectiveCObject new]')
Now apply the Python print function to the a variable:
(lldb) script print a
Again, you’ll get something similar to the following:
(DSObjectiveCObject *) $0 = 0x0000608000033260
Great! You have a SBValue instance stored at a and are already knowledgeable about the memory layout of the DSObjectiveCObject. You know a is holding a SBValue that is a pointer to the DSObjectiveCObject class.
You can grab the description of the DSObjectiveCObject class by using the GetDescription(), or more simply description property of SBValue.
Type the following:
(lldb) script print a.description
You’ll see something similar to the following:
<DSObjectiveCObject: 0x608000033260>
You can also get the value property, which returns a Python String containing the address of this instance:
(lldb) script print a.value
Just the value this time:
0x0000608000033260
Copy the output of a.value and ensure po’ing this pointer gives you the original, correct reference:
(lldb) po 0x0000608000033260
Yup:
<DSObjectiveCObject: 0x608000033260>
If you want the address expressed in a Python number instead of a Python str, you can use the signed or unsigned property:
(lldb) script print a.signed
Like this:
106102872289888
Formatting the number to hexadecimal will produce the pointer to this instance of DSObjectiveCObject:
(lldb) p/x 106102872289888
And you’re back to where you were before:
(long) $3 = 0x0000608000033260
Exploring properties through SBValue offsets
What about those properties stuffed inside that DSObjectiveCObject instance? Let’s explore those!
Use the GetNumChildren method available to SBValue to get its child count:
(lldb) script print a.GetNumChildren()
You’ll get 4 (or potentially 3 depending on the version of LLDB/run conditions which hides an instance’s isa variable)
You can think children as just an array. There’s a special API to traverse the children in a class called GetChildAtIndex, so you can explore children 0-3 in LLDB.
Child 0:
(lldb) script print a.GetChildAtIndex(0)
(NSObject) NSObject = {
isa = DSObjectiveCObject
}
Child 1:
(lldb) script print a.GetChildAtIndex(1)
(UICachedDeviceRGBColor *) _eyeColor = 0x0000608000070e00
Child 2:
(lldb) script print a.GetChildAtIndex(2)
(__NSCFConstantString *) _firstName = 0x000000010db83368 @"Derek"
Child 3:
(lldb) script print a.GetChildAtIndex(3)
(__NSCFConstantString *) _lastName = 0x000000010db83388 @"Selander"
Each of these will return a SBValue in itself, so you can explore that object even further if you desired. Take the firstName property into account. Type the following to just get the description:
(lldb) script print a.GetChildAtIndex(2).description
Derek
It’s important to remember the Python variable a is a pointer to an object. Type the following:
(lldb) script a.size
8
This will print out a value saying a is 8 bytes long. But you want to get to the actual content! Fortunately, the SBValue has a deref property that returns another SBValue. Explore the output with the size property:
(lldb) script a.deref.size
This returns the value 32 since it makes up the isa, eyeColor, firstName, and lastName, each of them being 8 bytes long themselves as they are all pointers.
Here’s another way to look at what the deref property is doing. Explore the SBType class (you can look that one up yourself) of the SBValue.
(lldb) script print a.type.name
You’ll get this:
DSObjectiveCObject *
Now do the same thing through the deref property:
(lldb) script print a.deref.type.name
You’ll now get the normal class:
DSObjectiveCObject
Viewing raw data through SBValue
You can even dump the raw data out with the data property in SBValue! This is represented by a class named SBData, which is yet another class you can check out on your own.
Print out the data of the pointer to DSObjectiveCObject:
(lldb) script print a.data
This will print out the physical bytes that make up the object. Again, this is the pointer to DSObjectiveCObject, not the object itself.
60 32 03 00 80 60 00 00 `2...`..
Remember, each byte can be represented as two digits in hexadecimal.
Do you remember covering the little-endian formatting in Chapter 12, “Assembly & Memory,” and how the raw data is reversed?
Compare this with the value property of SBValue.
(lldb) script print a.value
Which will give you the expected 0x0000608000033260
Notice how the values have been flipped. For example, the final two hex digits of my pointer are the first grouping (aka byte) in the raw data. In my case, the raw data contains 0x60 as the first value, while the pointer contains 0x60 as the final value.
Use the deref property to grab all the bytes that make up this DSObjectiveCObject.
(lldb) script print a.deref.data
f0 54 b8 0d 01 00 00 00 00 0e 07 00 80 60 00 00 .T...........`..
68 33 b8 0d 01 00 00 00 88 33 b8 0d 01 00 00 00 h3.......3......
This is yet another way to visualize what is happening. You were jumping 8 bytes each time when you were spelunking in memory with that cute po *(id*)(0x0000608000033260 + multiple_of_8) command.
SBExpressionOptions
As mentioned when discussing the EvaluateExpression API, there’s an optional second parameter that will take an instance of type SBExpressionOptions. You can use this command to pass in specific options for the JIT execution.
In LLDB, clear the screen, start fresh and type the following:
(lldb) script options = lldb.SBExpressionOptions()
You’ll get no output upon success. Next, type:
(lldb) script options.SetLanguage(lldb.eLanguageTypeSwift)
SBExpressionOptions has a method named SetLanguage (when in doubt, use gdocumentation SBExpressionOptions), which takes an LLDB module enum of type lldb::LanguageType. The LLDB authors have a convention for sticking an “e” before an enum, the enum name, then the unique value.
This sets the options to evaluate the code as Swift instead of whatever the default is, based on the language type of SBFrame.
Now tell the options variable to interpret the JIT code as a of type ID (i.e. po, instead of p):
(lldb) script options.SetCoerceResultToId()
SetCoerceResultToId takes an optional Boolean, which determines if it should be interpreted as an id or not. By default, this is set to True.
To recap what you did here: you set the options to parse this expression using the Python API instead of the options passed to us through the expression command.
For example, SBExpressionOptions you’ve declared so far is pretty much equivalent to the following options in the expression command:
expression -lswift -O -- your_expression_here
Next, create an instance of the ASwiftClass method only using the expression command. If this works, you’ll try out the same expression in the EvaluateExpression command. In LLDB type the following:
(lldb) e -lswift -O -- ASwiftClass()
You’ll get an ugly little error for output…
error: <EXPR>:3:1: error: use of unresolved identifier 'ASwiftClass'
ASwiftClass()
^~~~~~~~~~~
Oh yeah, — you need to import the Allocator module to make Swift play nicely in the debugger.
In LLDB:
(lldb) e -lswift -- import Allocator
Note: This is a problem many LLDB users complain about: LLDB can’t properly evaluate code that should be able to execute. Adding this import logic will modify LLDB’s Swift expression prefix, which is basically a set of header files that a referenced right before you JIT code is evaluated.
LLDB can’t see the class
ASwiftClassin the JIT code when you’re stopped in the non-Swift debugging context. This means you need to append the headers to the expression prefix that belongs to the Allocator module.There’s a great explanation from one of the LLDB authors about this very problem here: http://stackoverflow.com/questions/19339493/why-cant-lldb-evaluate-this-expression.
Execute the previous command again. Up arrow twice then Enter:
(lldb) e -lswift -O -- ASwiftClass()
You’ll get a reference to an instance of the ASwiftClass().
Now that you know this works, use the EvaluateExpression method with the options parameter as the second parameter this time and assign the output to the variable b, like so:
(lldb) script b = lldb.target.EvaluateExpression('ASwiftClass()', options)
If everything went well, you’ll get a reference to a SBValue in the b Python variable.
Note: It’s worth pointing out some properties of
SBValuewill not play nicely with Swift. For example, dereferencing a Swift object withSBValue’sdereforaddress_ofproperty will not work properly. You can coerce this pointer to an Objective-C reference by casting the pointer as aSwiftObject, and everything will then work fine. Like I said, they make you work for it when you’re trying to go after pointers in Swift!
Referencing variables by name with SBValue
Referencing child SBValues via GetChildAtIndex from SBValue is a rather ho-hum way to navigate to an object in memory. What if the author of this class added a property before eyeColor that totally screwed up your offset logic when traversing this SBValue?
Fortunately, SBValue has yet another method that lets you reference instance variables by name instead of by offset: GetValueForExpressionPath.
Jump back to LLDB and type the following:
(lldb) script print b.GetValueForExpressionPath('.firstName')
You can keep drilling down into the child’s own struct if you wish:
How did I obtain the name of child SBValues? If you had no clue of the name for the child SBValue, all you have to do is get to the child using the GetIndexOfChild API, then use the name property on that SBValue child.
For example, if I didn’t know the name of the UIColor property found in the b SBValue, I could do the following:
lldb.value
One final cool thing you can do is create a Python reference that contains the SBValue’s properties as the Python object’s properties (wait… what?). Think of this as an object through which you can reference variables using Python properties instead of Strings.
Back in the console, instantiate a new value object from your b SBValue:
(lldb) script c = lldb.value(b)
This will create the special LLDB Python object of type value. Now you can reference its instance variables just like you would a normal object!
Type the following into LLDB:
(lldb) script print c.firstName
You can also cast the child object back into a SBValue so you can query it or apply it to a for loop, like so:
(lldb) script print c.firstName.sbvalue.signed
Again, if you don’t know the name of a child SBValue, use the GetChildAtIndex API to get the child and get its name from the name property.
Note: Although the
lldb.valueclass is awesome, this comes at a cost. It’s rather expensive to create and access properties through this type of class. If you are parsing a hugeNSArray(orArray<Any>, for you Swifties), using this class will definitely slow you down. Play around with it and find the sweet spot between speed and convenience.
Where to go from here?
Holy cow… how dense was that chapter!? Fortunately you have come full circle. You can use the options provided by your custom command to dynamically generate your JIT script code. From the return value of your JIT code, you can write scripts that have custom logic based upon the return SBValue that is parsed through the EvaluateExpression APIs.
This can unlock some amazing scripts for you. In any process to which you can attach LLDB, you can run your own custom code and handle your own custom return values within your Python script. There’s no need to deal with signing issues or loading of frameworks or anything like that.
The remaining chapters in this section will focus on the composition of some creative scripts and how they can make your debugging (or reverse engineering) life much simpler. Theory time is over. It’s time for some fun!