Apple Linker Magic & Swift Runtime

Published on

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):

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:

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:

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>.

Actions

The linker supports the following actions:

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

← Back to Writings