Recently, I was debugging an interesting issue: a program crashes whenever it tries to call a particular function from a dynamic library. It was not clear how to debug this issue. Eventually, I did resolve the problem: the key was in the dynamic linker, dyld.
In this article, I want to make a short intro on where to start if you have a similar issue. It is by no means an exhaustive guide, but rather a starting point that could have saved me a few hours would I have known this information before.
Inspect the dyld from inside
The source code of dyld is generally available but is outdated. At the time of writing, you can get the source code for macOS High Sierra, 10.13.6, from here. If you are running the latest version of macOS, 10.14, Mojave, then your last resort is binaries shipped with the OS. Though, the source code from previous versions is still helpful. So to get the full picture, I recommend doing the following:
- Get the latest version of the source code from https://opensource.apple.com/
- Use disassembler (Hopper ftw) to inspect dyld binaries:
- Disable SIP (optional) and run you binary under
lldb. You can set breakpoints on all dyld related functions:
br set -r dyldor
br set -r dyld3for dyld3 only.
During debugging, please be ready to jump a lot between the source code and the three libraries mentioned in step two.
Inspect the dyld from the outside
There are a few other options to observe the behavior of dyld without looking at the code, source or binary. You will also need to disable SIP if you want to exercise it on systems apps. All the options are controlled via environment variables. These are the ones I found the most useful:
DYLD_PRINT_APIS: documented, prints a nice trace of almost everything that is happening inside of dyld. Here is an example output:
It looks cryptic, but it greatly helps to understand the program execution flow.
DYLD_PRINT_LIBRARIES, documented, prints all the dynamic libraries that are being loaded during the app startup. Here is an example output:
dyld: loaded: /usr/lib/libiconv.2.dylib
dyld: loaded: /System/Library/Frameworks/Security.framework/Versions/A/Security
dyld: loaded: /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
dyld: loaded: /usr/lib/libz.1.dylib
dyld: loaded: /usr/lib/libSystem.B.dylib
dyld: loaded: /usr/lib/libresolv.9.dylib
dyld: loaded: /usr/lib/system/libcache.dylib
dyld: loaded: /usr/lib/system/libcommonCrypto.dylib
dyld: loaded: /usr/lib/system/libcompiler_rt.dylib
DYLD_PRINT_WARNINGS, undocumented, may print some useful information. Currently, it definitely prints some info about dyld3 closures. An example output:
dyld: found closure 0x7ffff48ae9ac (size=844) in dyld shared cache
dyld: closure 0x7ffff48ae9ac not used because DYLD_FRAMEWORK_PATH changed
DYLD_*_PATH, documented, changes the order of directories where dyld will search for dynamic libraries. The nice side effect of using these variables is that their presence disables the dyld3 closure cache. So if your suspect is dyld3 closures, then export any of the
DYLD_*_PATHvariables to disable them. Some examples:
For more info, please consult the dyld man page (
man dyld) or dig through the code, source or binary.
Debugging such thing as dyld is a nontrivial task, but it is indeed possible. If you know any other hints or tricks, please share them.