Translating an ARM iOS App to Intel macOS Using Bitcode

May 18 2019

When Apple introduced Bitcode and made it mandatory on watchOS and tvOS, they kinda hand-waved away questions about why it existed, with nebulous claims about it being useful to tune-up binaries by utilizing the latest compiler improvements.

Since then, Bitcode has proven instrumental in the seamless overnight transition of watchOS to 64-bit, where developers didn't even need to recompile their apps on the store as Apple did it transparently for them so they could run on the Apple Watch Series 4. You likely didn't even notice a transition had taken place.

What is Bitcode? Well, bitcode with a small b- is an architecture-specific intermediate representation used by LLVM, and capital-B Bitcode pertains to a set of features allowing you to embed this representation in your Mach-O binary and the mechanisms by which you can provide it to Apple in your App Store submissions. It's not as flexible as source code, but it's far more flexible than a built binary, with metadata and annotations for the compiler. In practice, you (or Apple) can easily take the Bitcode blobs from your app and recompile them into a fully-functioning copy of your app. Going from armv7 to armv7s, or arm64 to arm64e is a piece of cake, and saves developers having to recompile an ever-fatter binary of their own every time Apple tweaks their ARM chips. Bitcode has long-since been used by Apple in its OpenGL drivers such that the driver can optimize on the fly for the various GPU architectures Apple supports.

We have seen Microsoft use static recompilation to great effect on Xbox One, giving it access to a whole library of originally-PowerPC Xbox 360 games, all without developer intervention or access to the source code. And that's without an intermediary like Bitcode to trivialize the process.

Of course, the specter of macOS on ARM has been in the public psyche for many years now, and many have pondered whether Bitcode will make this transition more straightforward. The commonly held belief is that Bitcode is not suited to massive architectural changes like moving between Intel and ARM.

I was unconvinced, so I decided to test the theory!


Firstly, we need an Objective-C Hello World app with Bitcode; Bitcode is usually only included when building an archive for the App Store, so we need to force its inclusion in a regular build. You can use the -fembed-bitcode flag or a custom build setting:

BITCODE_GENERATION_MODE = bitcode

Build your binary for Generic iOS Device, or an attached device, like normal. Bitcode doesn't seem to be embedded in arm64e builds (i.e. if you have an A12-based device), so you might want to turn off Xcode’s ‘compile active architectures only’ setting and build for arm64 directly.

Using a tool called ebcutil, you can very easily extract all the Bitcode objects from your compiled binary.

ebcutil -a arm64 -e path/to/MyApp.app/MyApp

Then, for each Bitcode object, recompile it for Intel.

for f in *;
    do clang -arch x86_64 -c -Xclang -disable-llvm-passes -emit-llvm -x ir -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk $f -o $f.o;
done

Now, you want to link your compiled blobs back into a binary.

clang -arch x86_64 -mios-version-min=12.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk *.o -o path/to/MyApp.app/MyApp

If this succeeds, you now have an Intel version of your originally-arm64 app! You should be able to drop it directly into an iOS Simulator window to install and verify it runs.

This is a very important proof: you can statically translate binaries between Intel and ARM if they include Bitcode. It really works!


⚠️ Gotchas for more-complex projects

ARC appears to use some inline assembly, which means you'll need to disable ARC for a project for arm64-to-x86 translation to succeed right now.

Certain kinds of Blocks, like completion handlers, also seem to trip up the compiler with instructions it refuses to accept; if you see an X87 error this is likely your issue.

Why Objective-C? Well Swift was designed with ARC in mind and thus I dont believe there is a way to avoid the aforementioned inline assembly, so recompilation will fail currently.


Let’s take it a step further: let’s use marzipanify to convert this Intel iOS app into a Mac app that can run using Marzipan.

Translated app on macOS

That was easy!

This means, in theory, that if Apple wanted every iOS app on the App Store to run on the Mac, today or in the future, they have a mechanism to do so transparently and without needing developers to update or recompile their apps.


So, what if the Mac switched to using ARM chips instead of Intel? Well, as you can see, Apple could use Bitcode to translate every Bitcode-enabled app on the Mac App Store, without consulting developers, so it would be ready to go on day one. This kind of power means Apple needn’t preannounce an ARM switch a year ahead of time, and also means a technology like Rosetta may be completely unnecessary this time round.

Obviously, we’re not there yet: Apple doesn't enable Bitcode for submissions to the Mac App Store today, and today’s Bitcode may not be ideal for such an architectural translation. If I were Apple, I would make sure those two things change soon, and surely mandate Bitcode for all Marzipan apps in macOS 10.15.