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
Source code
To understand what’s going on within DYLD you’d need to look at the source code, but also at the disassembly.
The source code is generally available on GitHub, but might be slightly outdated if you are running the latest version of macOS.Binaries
The binaries you are interested in are/usr/bin/dyld
and/usr/lib/system/libdyld.dylib
. Since macOS switched do dyld cache, you’d need to use dyld-shared-cache-extractor to extract all the binaries. Once you get the libraries you can disassemble them (Hopper ftw) and start debugging.LLDB
Run your binary and add a breakpoint based on a regex (e.g. `br set -r dyld`) and hit `run`
During debugging, please be ready to jump a lot between the source code, /usr/lib/dyld
and /usr/lib/system/libdyld.dylib
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:_dyld_register_func_for_add_image(0x7fff7696ab92)
_dyld_get_image_slide(0x1000f1000)
_dyld_register_func_for_add_image(0x7fff7689cd98)
_dyld_get_image_slide(0x1000f1000)
_dyld_register_func_for_add_image(0x7fff76be67cb)
dyld_image_path_containing_address(0x7fff75221000)
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_*_PATH:
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 theDYLD_*_PATH
variables to disable them. Some examples:export DYLD_FRAMEWORK_PATH=
export DYLD_LIBRARY_PATH=
For more info, please consult the dyld man page (man dyld
) or dig through the code, source or binary.
Summary
Debugging such a thing as dyld is a nontrivial task, but it is indeed possible. If you know any other hints or tricks, please share them.
Happy debugging!