Apple Linker Magic & Swift Runtime
Before Swift was ABI-stable, apps on Apple platforms had to embed the Swift runtime in their app bundles regardless of the targeted OS version.
With the release of the ABI-stable Swift 5, Apple started including the runtime as part of its OSes. Consequently, apps using Swift 5 deploying to OSes containing the runtime do not need to bundle it anymore.
This article explores how apps link differently against the runtime depending on the deployment target.
TL;DR
It’s best to read the article in full but if you’re short on time, here’s a summary (spoiler alert):
- Apple’s linker,
ld64
, recognises specially named symbols which change its linking behaviour. - Those magic symbols can customise behaviour on a per-deployment target basis.
- The Swift runtime leverages those magic symbols to change its install name.
- “asm labels” are used to define such magic symbols which start without a underscore.
The Mystery Behaviour
The obvious question arises about what happens when you compile apps for specific deployment targets, some of which guarantee the presence of an ABI-stable runtime while others do not.
For example, iOS 13 ships with an ABI-stable runtime while iOS 11 does not. Let’s see what happens when we compile an iOS app with different deployment targets.
iOS 13
The binary is linked against the Swift runtime using the following install names:
/usr/lib/swift/libswiftCore.dylib (compatibility version 1.0.0, current version 1100.2.255)
/usr/lib/swift/libswiftFoundation.dylib (compatibility version 1.0.0, current version 0.0.0)
/usr/lib/swift/libswiftObjectiveC.dylib (compatibility version 1.0.0, current version 0.0.0)
iOS 11
When linked against iOS 11, the install names change to:
@rpath/libswiftCore.dylib (compatibility version 1.0.0, current version 1100.2.255)
@rpath/libswiftFoundation.dylib (compatibility version 1.0.0, current version 0.0.0)
@rpath/libswiftObjectiveC.dylib (compatibility version 1.0.0, current version 0.0.0)
The only difference between the linker invocations is the addition of -Xlinker -rpath -Xlinker /usr/lib/swift
to the Clang driver. This will simply add /usr/lib/swift
as a runtime path for the app binary which will be used for dylib resolution.
Most importantly, both linker invocations link against the same Swift runtime dylibs. But notice that the install names are different! How can that be?
The Mystery of the Magic Symbols
How can you link against the same dylib, which has a single install name, but any linked binaries record different dylib install names?
After a bit of digging, you might notice some strangely named symbols in libswiftCore
, like:
$ld$install_name$os11.0$@rpath/libswiftCore.dylib
$ld$install_name$os11.1$@rpath/libswiftCore.dylib
$ld$install_name$os11.2$@rpath/libswiftCore.dylib
...
$ld$install_name$os12.0$@rpath/libswiftCore.dylib
$ld$install_name$os12.1$@rpath/libswiftCore.dylib
Those are suspicious for three reasons:
- They do not begin with an underscore (platform calling convention decoration).
- They seem to provide values for the install name which we are observing.
- They stop exactly before an ABI-stable Swift runtime shipped in iOS 12.2.
Where do they come from and how do they work?
Swift Runtime Source Code
After a bit of digging, we can find the source code in magic-symbols-for-install-name.c:
#define RPATH_INSTALL_name_DIRECTIVE_IMPL2(name, major, minor) \
SWIFT_RUNTIME_EXPORT const char install_name_ ## major ## _ ## minor \
__asm("$ld$install_name$os" #major "." #minor "$@rpath/lib" #name ".dylib"); \
const char install_name_ ## major ## _ ## minor = 0;
#define RPATH_INSTALL_name_DIRECTIVE_IMPL(name, major, minor) \
RPATH_INSTALL_name_DIRECTIVE_IMPL2(name, major, minor)
#define RPATH_INSTALL_name_DIRECTIVE(major, minor) \
RPATH_INSTALL_name_DIRECTIVE_IMPL(SWIFT_TARGET_LIBRARY_name, major, minor)
#elif TARGET_OS_IPHONE
...
RPATH_INSTALL_name_DIRECTIVE(12, 1)
...
When the preprocessor expands it, it generates the following code, where SWIFT_TARGET_LIBRARY_NAME
can be swiftCore
.
const char install_name_12_1 __asm("$ld$install_name$os" "12" "." "1" "$@rpath/lib" "SWIFT_TARGET_LIBRARY_name" ".dylib");
const char install_name_12_1 = 0;
After applying string concatenation which the compiler performs, it’s equivalent to:
const char install_name_12_1 __asm("$ld$install_name$os12.1$@rpath/libswiftCore.dylib");
const char install_name_12_1 = 0;
asm Labels
The ability to provide the name in assembler for symbols is a GNU extensions which Clang supports as well. It’s known as “asm labels” or “asm-on-declarations”. A custom name can be specified by appending __asm("name")
after a variable or function declaration. According to the GNU docs:
On systems where an underscore is normally prepended to the name of a C variable, this feature allows you to define names for the linker that do not start with an underscore.
If you’re using Rust, you can leverage an undocumented escape hatch in LLVM to disable platform calling convention decoration:
#[export_name = "\x01bar"]
pub extern fn foo() {
println!("foo");
}
#[export_name = "\x01no_underscore"]
static mut BAZ: i64 = 5;
The Magic Trick
We have now discovered how the linker changes the install name depending on the deployment target:
- The Swift runtime libraries create specially named symbols.
- Those specially named symbols are used by the linker to adjust the install name depending on the deployment target.
Swift Runtime Behaviour
When the deployment target is iOS 12.1 or earlier, the Swift 5 runtime libraries will have install names that are @rpath
-relative and /usr/lib/swift
will also be added as an rpath
for the app binary itself. The latter is done so that multiple apps that use the same Swift runtime can share dylibs shipped with the OS to minimise memory usage.
When the deployment target is iOS 12.2 and above, the Swift 5 runtime libraries will have absolute install names beginning with /usr/lib/swift
and no extra rpath
will be added.
Note that in all cases, @executable_path/Frameworks
will be an rpath
for the app binary, so that on iOS 12.1 or below, the bundled Swift runtime can be used as a fallback if the OS does not bundle a compatible one.
Apple Linker Magic
ld64
sources are published by Apple, so we can see how the linker actually works. The code can be found in macho_dylib_file.cpp inside the method File<A>::addSymbol()
. It defines the magic format as $ld$ <action> $ <condition> $ <symbol-name>
.
- The
condition
is matched exactly against the deployment target. A condition ofos12.1
would match a deployment target of iOS 12.1. - The
action
can be one of the following:add
,hide
,weak
,install_name
andcompatibility_version
. symbol-name
is the payload for the actions.
Actions
The linker supports the following actions:
add
: treats the symbol as being exported, even if not defined. If the symbol is referenced, linking will succeed.hide
: hides a symbol, as if it does not exist. If the symbol is strongly referenced, linking will fail.weak
: If no weak imports are allowed, hides the symbol. Only changes behaviour if linking against a library using-no_weak_imports
. Note that, generally, weak vs strong linkage is a determined by the binary which is linked against a library, not the library itself.install_name
: overrides the install name.compatibility_version
: overrides the compatibility version.
Swift Runtime Location
If you want to investigate things, be aware of how the linker locates the Swift runtime libraries. Xcode passes an appropriate parameter for -isysroot
which the Clang driver translates to the linker option -syslibroot
when calling ld64
. The docs define it as:
-syslibroot rootdir
Prepend rootdir to all search paths when searching for libraries or frameworks.
At the time of writing, the linker invocation includes -syslibroot /Applications/Xcode.app/.../Developer/SDKs/iPhoneOS13.2.sdk -L/usr/lib/swift
. This means that when it searches for the Swift runtime, it will search in /Applications/Xcode.app/.../Developer/SDKs/iPhoneOS13.2.sdk/usr/lib/swift
, rather than in /usr/lib/swift
on the host system.
If you look inside that directory, you will find the Swift runtime text-based stubs ( .tbd
) which define the public interface of the dylibs on the target system.
Code Samples
If you would like to play around with the code, the easiest way is to create two files: lib.c
and main.c
. For example:
// == lib.c ==
// clang -shared lib.c -o libLinkerTest.dylib
int a = 15;
// Even though `a` is defined above, it will be hidden if linking against 10.12
int b asm("$ld$hide$os10.12$_a");
int b = 10;
// == main.c ==
// clang main.c -lLinkerTest -L. -target x86_64-apple-macos10.12
#include
extern int a;
int main() {
printf("%d\n", a);
return 0;
}
Install Names
If you would like to inspect the install name of a dylib, use otool -l name.dylib
and find the LC_ID_DYLIB
load command.
References
- Evolving Swift On Apple Platforms After ABI Stability
- MachO-Explorer: A graphical Mach-O viewer for macOS.
- Frameworks and Weak Linking
- Dynamic Library Design Guidelines