24.
Script Bridging With SBValue & Memory
Written by Walter Tyree
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 and hinders you making anything usefully complex. Nobody likes stringly typed things!
So, it’s time to learn about another 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 that 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, it generates an error.
Build and run the application. 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 compiles, this Objective-C class actually looks like a C struct. The compiler creates 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 appear 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 appears 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).
In 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 output similar to the following:
<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 object_getClass(0x600000031f80)
This will give you the value of the isa class, therefore the type of the object. You cannot reference the isa pointer directly, because it doesn’t exist as a normal pointer. Instead you have to go through the object_getClass function to obtain the isa pointer.
In about 2012 or so, when Apple started really moving to 64-bit as the future, someone noticed a whole bunch of space in isa pointer that would never be used. So, in the name of optimization, it got repurposed. Now metadata about the instance gets encoded in the formerly unused bits. You can look at the header for objc-object.h to see how they are being used. Things like retain count, whether the object is currently being deallocated, does it have associated objects and more are encoded in the bits. All of this is a long way to say: use object_getClass instead of isa when you want to see the class of an object.
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/gt 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 binary (
t).
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: 0b0000000100000000000000000000000100000100101110111100111101010001
This gives you output that tells you the value at memory address 0x600000031f80 which is all of that isa metadata. You could cross reference it with the objc-object header to decode everything, but that’s an exercise left for you.
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 the metadata 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 metadata, 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 is still evolving. Though Swift achieved ABI stability in version 5 and module stability in version 5.1, things are still changing and evolving as Swift begins to support other CPUs and operating systems. So, if you find that something has changed, be sure to check the forums as we will all be adapting.
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 _StringObject {
uintptr_t _countAndFlagBits; // packed bits for string type
uintptr_t _object; // raw data
} firstName;
struct _StringObject {
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 now this ASwiftClass struct goes completely off the rails.
A Swift String is a very interesting “object”. In fact, a String is a struct within the ASwiftClass struct. You can think of String as sort of a facade design pattern that hides different types of 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 the string being stored is longer or shorter than 16 bytes or compiled for 32-bit or 64-bit platforms.
For 64-bit platforms, the memory layout of a Swift String comprises 16 bytes with the structural layout depending on the type of String. To help you think it through, here is an excellent ASCII art diagram to demonstrate the layout of a “small” Swift string.
This documentation is taken from the https://github.com/apple/swift/blob/master/stdlib/public/core/StringObject.swift.
On 64-bit platforms, small strings have the following per-byte layout. When stored in memory (little-endian), their first character (‘a’) is in the lowest address and their top-nibble and count is in the highest address.
┌───────────────┬─────────────────────────────┐
│ _countAndFlags │ _object │
├─┬─┬─┬─┬─┬─┬─┬─┼─┬─┬──┬──┬──┬──┬──┬──────────┤
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │ 11 │ 12 │ 13 │ 14 │ 15 │
├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼──┼──┼──┼──┼──┼──────────┤
│ a │ b │ c │ d │ e │ f │ g │ h │ i │ j │ k │ l │ m │ n │ o │ 1x10 count │
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴──┴──┴──┴──┴──┴──────────┘
From the documentation, you can see that if a regular string will fit in a 16 byte spot, it gets stored directly and the last byte is the length count. However, if it becomes one byte longer, or is Unicode or somehow “special” it magically gets stored differently. Again from the StringObject documentation:
All non-small forms share the same structure for the other half of the bits (i.e. non-object bits) as a word containing code unit count and various performance flags. The top 16 bits are nonessential flags; these aren’t critical for correct operation, but they may provide additional guarantees that allow more efficient operation or more reliable detection of runtime errors. The lower 48 bits contain the code unit count (aka endIndex).
┌──────┬──────┬──────┬──────┬──────┬──────────┬────────┐
│ b63 │ b62 │ b61 │ b60 │ b59 │ b58:48 │ b47:0 │
├──────┼──────┼──────┼──────┼──────┼──────────┼────────┤
│ ASCII│ NFC │native│ tail │ UTF8 │ reserved │ count │
└──────┴──────┴──────┴──────┴──────┴──────────┴────────┘
So, for a larger strings, or a non-ASCII or plain-C string, the string itself is stored somewhere else and the 16 bytes just contain some metadata and the pointer to the “real” string.
The layout of the 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. For this next part, you’ll use the graphical tools Xcode provides to sift through the memory. Remember from before, that many of the debug tools Xcode provides are just graphical wrappers around LLDB. Start by placing a GUI breakpoint in ViewController.swift right after the Swift class gets created:
else if let clsSwift = cls as? ASwiftClass.Type {
let object = clsSwift.init()
You’re going to inspect the object, so make sure the GUI breakpoint is after this code, but before the end of this scope.
With the breakpoint set, ensure that the app is running. You’ll 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 wait for your breakpoint to hit. Now, arrange your Xcode window panes to you can see the variables view and have at least a little bit of space below the stack trace in the Debug navigator. Next, open the details of the object variable. Your screen should look something like the picture:
The hex number on the object line is its memory address. Right-click on the memory address to bring up the context menu and select View memory of “object”. Now, you’ll see the actual memory. At this point, I usually resize my Xcode window so that the memory display wraps at a reasonable place and the addresses in the gray column make sense.
You’ll get something similar to the following:
Here you can see the Xcode window is resized to show 8 bytes per row and you can see:
- The memory address of the object itself
- The memory for the
eyeColorproperty - The memory for
firstName - The memory for
lastName - A running list of all of the memory locations you visit, so you can quickly jump back to them.
Recall from before that in the first part of the class, where the isa variable lives, Apple now puts metadata. One of the things you can see is the retain count byte. Using the location of your object type this into the lldb console:
exp -l objc -O -- [0x6000002ec400 retain]
Because you used a GUI breakpoint in Swift code, you’re in a Swift context, so you have to jump to the Objective-C context. For fun, use the up arrow and execute the command a few more times. Now right-click on the object variable and View memory of “object” to refresh the memory map. If you’ve resized Xcode to show 8 bytes per row, you should see the retain count in the middle of the second row. Now execute a few release commands and make it go down:
exp -l objc -O -- [0x6000002ec400 release]
After a few release commands, View memory of “object” again and it will have changed. Wheeee! :]
The memory for eyeColor is interesting. The eyeColor value should be at offset 0x10. This would be at 0x6000002ec410 in this example. However notice that in the variable view in the screenshot, the eyeColor object is at 0x00006000002ec480. Looking at the memory map at the 0x10 offset (i.e. 0x6000002ec410) you can see that is what is actually stored at the 0x10 offset is the memory address of the “real” data where the UIColor is. This is because UIColor is a class and Swift is using a pointer to reference the object.
Looking back at the variable view, notice that firstName and lastName don’t get memory addresses. However, you can see them offset by 0x18 and 0x28 from the base. Because they are both shorter than 16 bytes, they get stored inline and the last nibble is the length. The string “Derek” has a count of 5 and “Selander” has a count of 8. In the variables view click the to display details of firstName, you’ll see the _guts and then another _object and then the _countAndFlagBits and _object. The _countAndFlagBits and the _object are the two 8 byte pieces of the string struct.
In fact, you can use the type formatting from way back in Chapter 5, “Expression”, to prove that the UInt64 for _coundAndFlagBits is the original string.
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! Hopefully, this exercise shows you why using an abstraction like SBValue in your code instead of trying to go directly to the data is going to be curcial in more complex LLDB scripts.
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. If it’s paused, resume it. The GUI breakpoint from before put you in a Swift context, you want an ObjectiveC context, so you want to pause the Allocator project using the pause program execution button of Xcode. 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 *) $3 = 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) $5 = 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 of 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 for example, type the following to get just 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
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 Jim Ingham, one of the LLDB authors, about this very problem an an answer to this StackOverflow Question.
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.
Key Points
- The isa variable’s unused bits are being repurposed for other things, so use
object_getClassto find out what kind of object you’re exploring. - Use the
xcommand in lldb or the View Memory debug workflow to see the actual bytes for an object. - Class objects in Swift and Objective-C offset properties by
0x8or0x10depending on the type. Pointers are always of size0x8. - Depending on its properties, a Swift
Stringmay be stored in the class directly, or somewhere else in memory. This can change at runtime as the string changes. - Swift classes that do not inherit from
NSObjectkeep their own retain count. -
SBValueabstracts different data into something with a common interface. - Use
GetNumChildren()to explore the properties of the objectSBValuerepresents. - Use the
.dataproperty ofSBValueto get back to the raw bytes of an object. - Use
SBExpressionOptionsto set options that impact theEvalutateExpressioncommand. - The
lldb.value()creates a special Python object to help you avoid having to stringly type properties.
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!