milen.me
milen.me

Apple Linker Magic & Swift Runtime

Permalink | RSS

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 in 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 of os12.1 would match a deployment target of iOS 12.1.
  • The action can be one of the following: add, hide, weak, install_name and compatibility_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 <stdio.h>

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.

← Back to Writings

Any opinions and viewpoints expressed, explicitly or implicitly, are not endorsed by and do not represent any of my previous, current or future employers.