In my last post, you learned how the compiler compiles the source files. This time around, you’ll see how everything is pulled together.
Now that we’ve compiled all the files, we can link them together.
Here’s the full linker invocation; it’s far shorter than the compilerinvocation:
Ld /Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Products/Debug-iphonesimulator/ImportSwift.app/ImportSwift normal x86_64 cd /Users/jeremy/Workpad/BNR/ImportSwift export IPHONEOS_DEPLOYMENT_TARGET=10.1 export PATH="/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/usr/bin:/Applications/Xcode.app/Contents/Developer/usr/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang -arch x86_64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator10.1.sdk -L/Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Products/Debug-iphonesimulator -F/Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Products/Debug-iphonesimulator -filelist /Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Intermediates/ImportSwift.build/Debug-iphonesimulator/ImportSwift.build/Objects-normal/x86_64/ImportSwift.LinkFileList -Xlinker -rpath -Xlinker @executable_path/Frameworks -mios-simulator-version-min=10.1 -Xlinker -object_path_lto -Xlinker /Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Intermediates/ImportSwift.build/Debug-iphonesimulator/ImportSwift.build/Objects-normal/x86_64/ImportSwift_lto.o -Xlinker -export_dynamic -Xlinker -no_deduplicate -Xlinker -objc_abi_version -Xlinker 2 -fobjc-arc -fobjc-link-runtime -Xlinker -dependency_info -Xlinker /Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Intermediates/ImportSwift.build/Debug-iphonesimulator/ImportSwift.build/Objects-normal/x86_64/ImportSwift_dependency_info.dat -o /Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Products/Debug-iphonesimulator/ImportSwift.app/ImportSwift
Ld $(CONFIGURATION_BUILD_DIR)/$(EXECUTABLE_PATH) normal x86_64
Link the normal build variant executable for x86_64 in place in the
(You might wonder where the “d” is in “link”. The “d” is actually in “load”,and you might still hear a “linker” called a “linker-loader”,but we mostly take the loading bit for granted these days.)
Frontend, architecture, and search path
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang-arch x86_64-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator10.1.sdk
Rather than invoke the linker directly,Xcode opts to let the clang frontend manage all the details for it.In order to do that, it needs to pass many of the options used duringcompilation in order to preserve the same “environment” for linking,so we’ll meet many familiar flags this time around,like
-isysroot for setting the target architectureand the effective system root directory for the baked-in searchpaths.
And more search path munging here.
Listing the input object files
This is new! And as you might gather from that
.LinkFileList path extension, it’s specifically aimed at the linker.
It’s exactly what it looks like: a path to a file that lists all the filesto link. This avoids sticking oodles of file paths on the command line asarguments in their own right. The file itself is just a newline-delimited listof file paths. In this case, it’s really short, because we only compiledtwo object files:
Our first linker-escaped argument: Runtime search path
-Xlinker -rpath -Xlinker @executable_path/Frameworks
This batch of arguments is making sure the dynamic loader
dyld finds embeddedframeworks when it goes to link the app for running.
If you’ve rooted around inside a
.app bundle on disk,you’ll see that there’s an executable, and alongside it, there’s a Frameworksfolder, and in there is where all the embedded frameworks go.
Embedded frameworks get linked in as”eh, you know, @rpath/libwhatever.dylib, it’s on the runtime search path, gofind it, OK?” This batch of argumentsis what makes sure that that embedded Frameworks directory in the app bundlewinds up on that search path.
Syntactically speaking, this is our first linker-pass-through flag.
-Xlinker is a way to tunnel arguments through the
clang driver to theunderlying linker when there’s a flag the linker understandsbut
clang does not.
The args Xcode is trying to pass to the linker are
-rpath @executable_path/Frameworks,but to get both the flag and its argument past
clang,it has to escape both words with
-Xlinker,hence the use of
-Xlinker in front of each intended linker argument.
-rpath flag tells the linker to add the path that is its argumentto the runpath search path list in the final linked output file.The runpath search path list is in turn used by the dynamic loader
dyldto find the dynamic libraries (dylibs) it should link the binary againstwhen the dylib path is runpath-relative.Runpath-relative dylib paths begin with
So, what’s that
@executable_path bit in the argument?That’s yet another magic path symbol for the dynamic linker;it’s used to talk about path relative to whatever the executable isthat dyld is trying to link in order to run.
Deployment target (again)
Yawn, machine-related flag, seen this show already, moving on.
Linker configuration: LTO and Obj-C
-Xlinker -object_path_lto -Xlinker /Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Intermediates/ImportSwift.build/Debug-iphonesimulator/ImportSwift.build/Objects-normal/x86_64/ImportSwift_lto.o-Xlinker -export_dynamic-Xlinker -no_deduplicate-Xlinker -objc_abi_version -Xlinker 2
Here’s a good batch of linker pass-throughs.Let’s elide the
-Xlinker escapes and take a closer look at them one by one.
Persisting any temporary object file used for LTO
This flag tells the linker where to write any temporary object file it needsto create while performing link-time optimization (LTO).Without the flag, it’d pick a spot, do its work, then delete the temporaryfile; with the flag, it leaves the file around, so other tools can pokearound and, say, read out debugging info.
Disabling global inlining and removal
This prevents LTO from inlining or otherwise removing global functions.The thinking behind the name seems to be, “what ends up needing to be exportedis dynamically determined, so don’t assume nothing will notice if you mungeexported identifiers”.This flag would let a plug-in loaded by the appreliably rendezvous with a global symbol declared by the app;many apps might not need this, but it looks like Xcode plays it safe.
Disabling global inlining and removal
This disables the deduplication pass in the linker.The deduplication pass would normally find identical functionsand replace duplicates with an alias of the first copy it encountered.
Funny enough, this is listed in the “Rarely used Options” section of themanpage for
ld. I have the sneaking suspicion this flag gets used a bitmore than that manpage would have you think, seeing as Xcode turns it onautomatically!
Specifying the Obj-C ABI version
This tells the linker which Obj-C ABI version to use.Remember that we earlier told the compiler which to use via
And that’s the end of our unescaped linker flag bundle.Back to flags as they are written.
This is the ARC opt-in flag we saw earlier during compilation.
This flag tells the clang driver to ask the linker to link inthe
objc runtime libraryas well as Foundation.
This is actually redundant with
-fobjc-arc—you can’t do ARC withoutan Obj-C runtime!—but clang was changed tolet that one slide.
Persisting link-time file dependency info
-Xlinker -dependency_info -Xlinker /Users/jeremy/Library/Developer/Xcode/DerivedData/ImportSwift-fvxpitolsctgjmemgmjamfehgmaf/Build/Intermediates/ImportSwift.build/Debug-iphonesimulator/ImportSwift.build/Objects-normal/x86_64/ImportSwift_dependency_info.dat
This tells the linker where to output dependency info.Normally, it doesn’t write this info anywhere.
Dependency info uses an undocumented binary file formatWhen set, the targeted path will be populated with a binary file formatcomprising a single opcode byte and then a NUL-terminated C string.The first such pair is always the linker version that wrote the file.The result appears to effectively encode all the various filepathslinking ended up depending on in various ways; if any of thosefiles changed, you’d know it’s time to relink to update the output binary.
For details, check out Apple’s open source developer tools code for ld64:src/ld/Options.h defines an anonymous enum for the opcodes,starting with
depLinkerVersion,while the end ofsrc/ld/Options.cppdefines the function
dumpDependency, which does the actual writing in thebinary file format. This function is called throughout option handlingto record files opened by
-filelist, and so on.
Specifying the output path
At last, the output path! This is where Xcode tells the linker to leave the linked executable.Notice that this is inside the app bundle in built products directory: this is where you’d run the app from.
Building and running this minimal app produces an ever-exciting blank, white view. You’d never ship this—at least, not since Apple started bundling a flashlight with iOS, you wouldn’t—but the plumbing that works to put it all together is identical to that used in an app you would ship.
Xcode tucks a lot of complexity and underdocumented wisdombehind the “Build & Run” command.Some flags, like
-no_deduplicate,are documented only superficially.What they do is kind of clear, but why you’d want to use them remains obscure,and it’s not clear why Xcode feels it’s necessary to provide them.Other flags, like
-dependency_info, are not documented at all.Explaining them required going to the source,which includedLLVM pull request reviews,a GitHub mirror of the clang Subversion repo,and an attempt to provide version history for the ld64 sourcethat Apple provides only in tarball-snapshot form via itsApple Open Sourcecollection.
We’ve fleshed out the very high-level sketch of what goes into building and linking an Obj-C app. It remains to dig into how an Obj-C app links against Swift code in similar detail. The next time you run into compile or link errors, come back to this article—when you have to lift the curtain, you won’t be overwhelmed, but prepared.
Interested in learning more about our basic and advanced iOS Courses?
Learn from the experts at a Big Nerd Ranch Bootcamp!
Interested in leveling up your coding skills from the same authors of the Big Nerd Ranch Guide? Subscribe to The Frontier today!
Chris Downie and Sam Landfried
Chris Downie and Sam Landfried