7.
Image
Written by Walter Tyree
If you’ve read this far, you now have a solid foundation in debugging. You can find and attach to a process of interest, efficiently create regular expression breakpoints to cover a wide range of culprits, navigate the stack frame and tweak variables using the expression command.
It’s time to explore one of the best tools for finding code of interest through the powers of lldb. You’ve already seen it a few times, but in this chapter, you’ll take a deep dive into the image command.
The image command is an alias for the target modules subcommand. The image command specializes in querying information about code that lives within a module. “Module” is a generic term for executable code, like an executable or a shared library. Examples of shared libraries include frameworks like UIKit for iOS or dynamic libraries like libSystem.B.dylib. A module can apply to a shared library on disk or code that’s loaded into a process.
Listing Modules
You’ll continue using the Signals project. Fire up the project in Xcode and then build & run on a simulator.
Note: You might be wondering why you keep running on the simulator and not a device. It’s just easier and faster. You don’t have to worry about certificate permissions and the connection to the device and things like that. Also, the processor on your computer is, hopefully, faster than the one on your phone, so stopping and starting the app won’t take as long. If you want to run on device, most everything with the Signals app will work the same though. Go ahead.
At any point, suspend the program and type the following in lldb:
(lldb) image list
This command lists all the modules currently loaded. You’ll see a lot! For such a simple program to run in memory, all those modules had to be loaded into the process!
The start of the list should look something like the following:
[ 0] 88B10840-E223-3B24-B89F-922AFF796077 0x00000001027ac000 /Users/lolz/Library/Developer/Xcode/DerivedData/Signals-bugxmcyqgcvqzfdfkgoztexuneae/Build/Products/Debug-iphonesimulator/Signals.app/Signals
[ 1] 86A8BA48-8BB4-3B30-9CDA-051F73C74F44 0x0000000102a80000 /usr/lib/dyld
[ 2] CC38080D-7C09-357A-B552-DB1B456781CE 0x00000001029dc000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/dyld_sim
[ 3] 4597B93E-E778-3BB5-BB66-E4344A5F2FDB 0x00000001c7be2000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libBacktraceRecording.dylib
[ 4] 14F79286-A67E-30FE-B786-E5A978D6DB0D 0x00000001c7bf5000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libMainThreadChecker.dylib
...
Like some other commands, there’s a -b switch to make the output “brief”. Enter the following command:
(lldb) image list -b
Now you just get the names of each module.
The first module is the main executable, Signals. The second module is the dynamic link editor or, more simply, dyld. dyld is responsible for loading any code into memory and executes code well before any of your code has a chance to start running.
You can filter out modules by specifying their name. Type the following into lldb:
(lldb) image list Foundation
The output will look similar to the following:
[ 0] E14C8373-8298-3E74-AFC5-61DDDC413228 0x00000001806ea000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/Foundation.framework/Foundation
This is a useful way to find information about just the modules you want. The output has a few interesting pieces to note:
- The module’s UUID prints first:
E14C8373-8298-3E74-AFC5-61DDDC413228. The UUID is important for hunting down symbolic information and uniquely identifies the version of the Foundation framework. - Following the UUID is the load address:
0x00000001806ea000. This identifies where the module loads into the executable’s process space. - Finally, you have the full path to the module’s location on the disk.
Note: Some modules won’t be at the physical location they claim to be on the disk. This is likely because they’re part of the dyld shared cache, or
dsc.dscpacks hundreds — sometimes thousands — of shared libraries together. Starting in macOS Monterey, Apple no longer includes the separated dynamic libraries on disk, leaving onlydscto explore if you’re not spelunking in memory. You’ll learn more aboutdscat the end of this chapter.
Take a deeper dive into another common module, UIKit. Type the following into lldb:
(lldb) image dump symtab -s address UIKit
Symtab, file = /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/UIKit.framework/UIKit, num_symbols = 3 (sorted by address):
Debug symbol
|Synthetic symbol
||Externally Visible
|||
Index UserID DSX Type File Address/Value Load Address Size Flags Name
------- ------ --- --------------- ------------------ ------------------ ------------------ ---------- ----------------------------------
[ 0] 0 Data 0x0000000000000fc8 0x00000001ac4ebfc8 0x0000000000000030 0x001e0000 UIKitVersionString
[ 1] 1 Data 0x0000000000000ff8 0x00000001ac4ebff8 0x0000000000000008 0x001e0000 UIKitVersionNumber
This dumps all the symbol table information available for UIKit. Remember, from a code standpoint, UIKit is a wrapper for the private UIKitCore module. As you can see, the UIKit module doesn’t include many symbols due to the sharing of code for Mac Catalyst. Repeat the same action for UIKitCore:
(lldb) image dump symtab UIKitCore -s address
It’s more output than you can shake a stick at! This command sorts the output by the addresses of each function thanks to the -s address argument. The lldb command above is comparable to dumping the symbol table information via nm, but instead, it happens in memory. Although not applicable to iOS Simulator shared libraries, this is convenient because you won’t be able to run the nm command on libraries packed into the dsc since nm expects an actual file on disk.
The image dump output has a lot of useful information, but your eyes will likely hurt scrolling through UIKitCore’s symbol table in its entirety. You need a way to effectively query the UIKitCore module.
The image lookup command is perfect for narrowing your search. Type the following into lldb:
(lldb) image lookup -n "-[UIViewController viewDidLoad]"
You’ll get output similar to the following:
1 match found in /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore:
Address: UIKitCore[0x00000000004b9278] (UIKitCore.__TEXT.__text + 4943316)
Summary: UIKitCore`-[UIViewController viewDidLoad]
This dumps out information relating solely to UIViewController‘s viewDidLoad instance method. The -n option searches for functions or symbols. On this computer, the viewDidLoad method is located at offset 0x00000000004b9278 of the UIKitCore file on the disk and is found in the UIKitCore.__TEXT.__text section of UIKitCore. The __TEXT.__text section is an R-X mapped section of memory where executable code typically lives. You’ll learn about the Mach-O components in the “Low Level” section of this book.
Typing the full -[UIViewController viewDidLoad] method can be a little tedious, and this can only dump out methods where you already know the name of the symbol.
This is where regular expressions come into play again. The -r option lets you perform a regular expression query. Type the following into lldb:
(lldb) image lookup -rn UIViewController
Not only does this dump out all methods containing the phrase “UIViewController”, but it also spits out results like UIViewControllerBuiltinTransitionViewAnimator since it contains “UIViewController”. You can be smart with the regular expression query, just like when you were setting breakpoints, to only spit out UIViewController methods. Type the following into lldb:
(lldb) image lookup -rn '\[UIViewController\ '
This is good, but what about categories? They come in the form of (+|-)[UIViewController(CategoryName) methodName]. Search for all UIViewController category methods:
(lldb) image lookup -rn '\[UIViewController\('
Searching for the presence of the parenthesis immediately following the Objective-C class name returns category methods for that particular class. Not only does this print out both public and undocumented APIs, but it also gives you hints to the methods the UIViewController class overrides from its parent classes.
Note:
image lookup‘s output can be a little jarring to read when there are many search results. In the upcoming chapters, you’ll look at ways to manipulatelldbto improve readability via command aliases and usinglldb’s Script Bridging interface. You can get a taste of what “pretty” output looks like with the following zinger:
script print("\n".join([i.GetSymbol().GetName() + "\n" for i in lldb.target.FindGlobalFunctions("\[UIViewController\(", 0, lldb.eMatchTypeRegex)]))
You can also limit your search queries to a specific module by appending the module name as the final argument to your search query. If you wanted to see all the implementations of viewDidLoad implemented in UIKitCore, you’d type the following:
(lldb) image lookup -rn viewDidLoad\]$ UIKitCore
Using the regular expression syntax, the command above looks for any viewDidLoad methods. The \]$ syntax dictates the final character in the symbol’s name is a closing bracket. This is a nice addition because Objective-C blocks could be implemented in a viewDidLoad method. When that happens, the symbol name for the block’s function would be the name of the symbol for the function where the block is created with the phrase __block_invoke appended, along with an increasing number if multiple blocks are implemented in the function.
Note: Regarding
lldb‘s command interface, there’s a subtle difference between searching for code in a module —image lookup— versus breaking in code for a module —breakpoint set. If you wanted to search for all blocks in the Commons framework that contain the phrase_block_invokein their symbols, you’d useimage lookup -rn _block_invoke Commons. If you wanted to make a breakpoint for every block in the Commons framework, you’d userb appendSignal.*block_invoke -s Commons. Take note of the-sargument versus the space.
Note: It’s worth mentioning that a private symbol’s name can be stripped out of code in a shared library. A private symbol is a symbol that can’t be linked to from another module — i.e., dictated from the
privatekeyword in Swift or thestaticsymbol declaration in C. The presence of a private symbol’s name in the symbol table hints that Apple didn’t care to remove it. Iflldbcan infer a symbol at an address but can’t find a name for it,lldbnames the symbol___lldb_unnamed_symbol<Counter>$$<Module>whereCounterincreases for every unknown symbol name in the module. See how many stripped, private symbols there are with animage lookup -rn ___lldb.
Swift Symbol Naming
Swift, like C++, generates mangled symbol names depending on the context/attributes of the originating source code. Compiling a simple C file and then comparing it to Swift best illustrates this. In Terminal, compile and view a function in C code:
% echo "void somefunction(void) {}" > /tmp/mangling_test.c
% clang -o /tmp/mangling_test /tmp/mangling_test.c -shared
% nm /tmp/mangling_test
If all goes well, nm will give you this output:
0000000000003fb4 T _somefunction
The nm command reads the symbol table embedded in the binary, which contains _somefunction. It’s common for all C compilers to prepend an underscore to the function name for its symbol. Notice how somefunction appears the same in the C source code and the symbol table.
Now, in Terminal, compare how Swift generates the same function name:
% echo "func somefunction() {}" > /tmp/mangling_test.swift
% swiftc -o /tmp/mangling_test /tmp/mangling_test.swift -emit-library
% nm /tmp/mangling_test
This time, nm will give you longer output:
0000000000003fb0 t _$s13mangling_test12somefunctionyyF
0000000000003fb4 s ___swift_reflection_version
The resulting symbol is now named _$s13mangling_test12somefunctionyyF. The module name mangling_test was included in the somefunction symbol. In addition to an underscore, the $s substring appears at the beginning of the name, hinting that the stable Swift ABI was likely used to compile the symbol.
To view the unmangled names, pipe the output into the swift-demangle command like so:
% nm /tmp/mangling_test | xcrun swift-demangle
Now, swift-demangle undoes all of the mangling:
0000000000003fb0 t mangling_test.somefunction() -> ()
0000000000003fb4 s ___swift_reflection_version
lldb provides a similar demangling command you can use in a process to unmangle names:
(lldb) language swift demangle _$s13mangling_test12somefunctionyyF
_$s13mangling_test12somefunctionyyF ---> mangling_test.somefunction() -> ()
This mangling is important for both searching for code via lldb’s image lookup as well as setting breakpoints discussed in Chapter 4: “Stopping in Code”. You can search for this Swift symbol using the mangled name or unmangled name. Although the Swift ABI has now solidified, the LLDB authors still occasionally tweak how to query the symbol. For example, with lldb-1400.0.38.17 (Xcode 14.2), the following search queries will resolve to the same s13mangling_test12somefunctionyyF symbol:
(lldb) image lookup -n somefunction
(lldb) image lookup -n mangling_test.somefunction
(lldb) image lookup -n $s13mangling_test12somefunctionyyF
More Swift Naming Conventions
Understanding how Swift mangles symbols can help you identify clever breakpoints in your source code.
Swift symbols using the stable ABI begin with a $s in the symbol name, which you can learn more about from Apple’s Swift GitHub repo. Here’s an example of how to look for Swift code in the Commons module of the Signal project using Swift mangled naming:
(lldb) image lookup -rn ^\$s Commons
Expanding on the regex above, you can find relevant Swift symbols in a mixed language codebase. Do this by appending a wildcard followed by class names, methods or functions to the prefix above, such as the following:
(lldb) image lookup -rn ^\$s.*viewDidLoad`
Without going into the full details of Swift symbol mangling, the type of object the symbol represents is usually encoded as a one- or two-character encoding following the symbol name. Zero or more entities following the type can help explain the type of symbol.
For example, imagine the Swift file Bleh.swift contains the following code:
class SomeClass {
var heresAVariable: Int = 0
}
The compiler generates several methods pertaining to using and manipulating heresAVariable, such as getting/setting/modifying/initialization. Now, narrow the focus to just the mangled getter symbol, which has the following mangled name:
_$s4Bleh9SomeClassC14heresAVariableSivg
Immediately following the $s is the number 4, indicating the module length of Bleh. 9 is the length of the class, SomeClass. The C immediately following SomeClass indicates it’s of type class. 14 gives the length of heresAVariable. Si is a Swift type of Int. The v indicates it’s a variable, and the g means it’s a getter function.
Using this knowledge, you can create a regular expression breakpoint on all the getters of a class/struct with the following lldb expression:
(lldb) rb \$s.*vg$
The command above uses the mangling convention to search for a getter, g, of a variable, v.
Note: Earlier in this book, you used a regular expression and searched for the phrases
getterorsetterfor breakpoints. However, that has the potential downside of false positives for breakpoints that you wouldn’t want to stop on. Using the mangled names allows more precise control of your breakpoints. But this comes at the price of learning the implementation details of how the compiler generates Swift code.
It’s worth repeating: Regular expression breakpoints are useful — especially when working with Swift. :]
Undocumented Debugging Methods
The image lookup command also does a great job searching for undocumented methods. Apple has included undocumented APIs for internal debugging that can also be used by those who are willing to dig through the symbol tables to find these helpful APIs.
Here are a couple interesting APIs pulled from UIKitCore for inspecting an Objective-C object’s contents:
-
-[NSObject(IvarDescription) _ivarDescription]dumps the raw ivars used to store values in Objective-C properties. -
-[NSObject(IvarDescription) _propertyDescription]lists the property declarations for that class. -
-[NSObject(IvarDescription) _shortMethodDescription]lists the methods implemented by that class. -
-[NSObject(IvarDescription) _methodDescription]lists the methods implemented by that class and recursively lists methods of its parent class.
You can also use APIs for inspecting the UIViews and UIViewController hierarchy:
-
-[UIViewController _printHierarchy]prints the entire view hierarchy from the UIViewController’s standpoint. -
+[UIViewController _printHierarchy]prints the entire view hierarchy.
Remember, these APIs are Objective-C-centric, so be sure to use the Objective-C context when executing them if you’re not in an Objective-C stack frame.
For fun, execute this command in the Signals project:
(lldb) exp -lobjc -O -- [[UIApplication sharedApplication] _ivarDescription]
You’ll get a slew of output since UIApplication holds many instance variables behind the scenes. Scan carefully, and find something that interests you.
For example, the private __gestureEnvironment variable holding references to a class called UIGestureEnvironment seems interesting.
You can use what you’ve learned to dump information about this class with the following:
(lldb) image lookup -rn UIGestureEnvironment
Spelunk into any rabbit hole of interest and play around with the private references to an Objective-C class using -[NSObject _shortMethodDescription] and -[NSObject _ivarDescription].
Dyld Shared Cache
When working with all these shared libraries, you may have noticed something a little odd. Open Terminal and type these commands:
% cd /tmp
% touch some_program.swift
% swiftc some_program.swift -framework CoreAudio
This creates a Swift command line executable that doesn’t do anything — the equivalent of an int main() { return 0; } in C — and links in the CoreAudio framework. Verify CoreAudio is linked to swift_program:
% otool -L some_program
The otool command should report something similar to:
some_program:
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.0.0)
/System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio (compatibility version 1.0.0, current version 1.0.0)
otool’s -L option displays the list of required dynamic dependency libraries found in the Mach-O header of some_program.
In macOS — or any recent version of iOS — you won’t be able to find these libraries on the disk, which makes it hard to dig through them for more interesting method names and object properties.
% ls /usr/lib/libobjc.A.dylib
ls: /usr/lib/libobjc.A.dylib: No such file or directory
% ls /usr/lib/libSystem.B.dylib
ls: /usr/lib/libSystem.B.dylib: No such file or directory
% ls /System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio
ls: /System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio: No such file or directory
That’s because Apple moved these libraries/frameworks from their expected locations into the dyld shared cache or dsc. This acts as a “super library”, giving a significant optimization boost when loading processes.
The problem with dsc is you’ll often want to explore these dynamic libraries but don’t have a way to do so if they’re packed away into a structure that seems to change on every major OS release. You can explore several solutions to introspect individual libraries in dsc:
- Use
lldb.lldbknows about thedscformat, allowing you to quickly query information from a loaded library. - Use public/private tools to extract the
dscshared library code so you can reinspect them on disk. Althoughdyldis available on Apple’s open source site, Apple often lags behind on releasing the current implementation. You can also check out the ipsw tool that by @blacktop__ created.
Extracting Shared Libraries Without a Third-Party Tool
Apple has shipped a special bundle located at /usr/lib/dsc_extractor.bundle since about Big Sur or so. You can link to this bundle and use it to extract dsc libraries using the publicly exported symbol dyld_shared_cache_extract_dylibs_progress.
The C function signature looks like:
extern int
dyld_shared_cache_extract_dylibs_progress(const char* shared_cache_file_path,
const char* extraction_root_path,
void (^progress)(unsigned current, unsigned total));
You can link and call this function using the dlopen/dlsym APIs. You’ll learn more about these APIs in the “Low Level” section of this book.
Here’s the Swift code to load the bundle and call the dyld_shared_cache_extract_dylibs_progress function. This code is also available with the materials download for this book:
import Foundation
typealias extract_dylibs = @convention(c) (
UnsafePointer<CChar>?,
UnsafePointer<CChar>?, ((UInt32, UInt32) -> Void)?) -> Int32
if CommandLine.argc != 3 {
print("\(String(utf8String: getprogname()) ?? "") <cache_path> <output_path>")
exit(1)
}
guard let handle = dlopen("/usr/lib/dsc_extractor.bundle", RTLD_NOW) else {
print("Couldn't find handle")
exit(1)
}
guard let sym = dlsym(
handle, "dyld_shared_cache_extract_dylibs_progress") else {
print("Can't find dyld_shared_cache_extract_dylibs_progress")
exit(1)
}
let extract_dylibs_func = unsafeBitCast(
sym, to: extract_dylibs.self)
let err = extract_dylibs_func(CommandLine.arguments[1],
CommandLine.arguments[2])
{ cur, total in
print("\(cur)/\(total)")
}
if err != 0 {
print("Something went wrong")
exit(1)
} else {
print("success! files written at \"\(CommandLine.arguments[2])\"")
}
Put this code into a file called dsc_extractor.swift and then run the following:
% swiftc dsc_extractor.swift
This produces the dsc_exctractor executable. The tool expects two arguments:
- A path to a dyld shared cache (
dsc) file. - The output path where it writes the extracted
dsclibrary contents.
You can then use nm and otool to inspect the libraries it produces, outside of linking them to an app and using lldb.
For example, run it with the following:
% dsc_extractor /System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e /tmp/dsc_payload
If you have not updated to macOS Ventura yet then you might not have the shared cache in the /System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/ folder. Instead, try here:
% dsc_extractor /System/Library/dyld/dyld_shared_cache_arm64e /tmp/dsc_payload
If your terminal complains it cannot find the dsc_extractor try again but add ./ to the beginning of the command to remind terminal to look in this directory.
Note: Apple has been moving the dyld shared cache files around as they clean up the file system as part of the migration away from x86. In Ventura, the cache moved to
/System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e. Depending on when you read this, it may have moved again. You can ask in the Kodeco forums for this book, if you cannot find it and need help.
Note: The author created the program above on an M1 Macbook Air, which has the
arm64eCPU variant. You’ll learn more about this CPU architecture in the assembly section. Be aware that all installs of macOS have the arm64e and the x86_64 dyld shared cache files available.
Key Points
-
imageis an alias for thetarget modulessubcommand and lets you inspect the loaded libraries for an app. - Use
image lookup -nto search for modules by name. Add the-rflag to also use regex syntax to narrow your searches. - Swift symbols get mangled by the compiler, but following a pattern, so you can still use regex to match them.
- Use
language swift demangleinlldbto demangle symbols or usexcrun swift-demanglein terminal. - Using
image lookupto search Apple’s code for undocumented methods that may assist you in your debugging. - Apple combines libraries into a shared cache, but you can extract and introspect individual libraries.
Where to Go From Here?
A number of challenges in this chapter can help you get interested in symbol exploration:
- Figure out a pattern using
image lookupto find all Swift closures within the Signals module. Once you do that, create a breakpoint on every Swift closure within the Signals module. How could you create a breakpoint for all Swift closures in a particular file? - Use the
image lookupcommand to figure out which UIKitCore method contains the most Objective-C blocks. - Look at Swift code that can stop on didSet/willSet property helpers or do/try/catch blocks. Can you use a regex pattern to stop on code like this?
- Use
image lookup -rs "Notification$" UIKitCoreto dump all theNSNotificationnames you can observe through UIKitCore and see if there are any interesting undocumented ones worth playing around with.
Need Another Challenge?
Using the private UIKitCore NSObject category method _shortMethodDescription as well as your image lookup -rn command, attach to the SpringBoard process and search for the class responsible for displaying time in the upper-left corner of the status bar. Keep in mind that you’ll need SIP disabled. Change the class’s value to something more amusing. Drill into subviews and see if you can find it using the tools given so far.