Beware of ARMv6-only iOS libraries

There I was, one day at work, trying to figure out why this iOS project wouldn’t link, giving the error “Undefined symbols for architecture armv7:” with the missing symbols being the entry points of a third-party library that was just added. Which made no sense at all, as the library did define these symbols (I checked using otool -vS), and the library was properly pulled in by a subproject, which was itself properly pulled in by the app (I checked with the log of commands executed by Xcode). I already tried cleaning all projects three times, nuking all the caches I could think of, and removing and reinserting the external library, to no avail. I was at a loss for ideas.

This is an actual issue that I want to warn you about because it could affect any iOS developer, so I’ll cut to the chase: the issue was that the third-party library was only built for ARMv6 (or only built for for the ARM_ALL subtype, the same issue occurs in the end) while I was trying to build an app with an ARMv7 slice.

This can happen with libraries that haven’t been updated recently, or that work fine in ARMv6 for most people (I’ll explain why in a minute) so are not built for both ARMv6 and ARMv7. If this happens to you (you can check with otool -vhf <library> | less; notice that in this context, “ALL” does NOT mean “both ARMv6 and ARMv7”), demand from the supplier of the library that you be provided a library that has both ARMv6 and ARMv7 slices; and if you have no choice but to take matters into your own hands (as I had to do), it’s possible to build a simple tool (everything you need is in mach-o/loader.h and mach/machine.h) that will manipulate the ARMv6 object files so that they become ARMv7 (the code in fact remains unchanged) so that you can craft a suitable library with both ARMv6 and ARMv7 slices.

What’s interesting is that the issue only becomes apparent in a specific configuration: if the external library is referenced not by the app target, but by a library target which is itself used by the app. iOS apps projects often have only one target, as iOS apps are generally simpler than Mac apps, but it’s not outlandish for more complex iOS projects to have multiple targets which depend on each other, in which case you could encounter the issue.

What is happening here is that ARMv6 and ARMv7 are treated separately enough by the iOS toolchain that they are in practice treated as different architectures altogether, a bit like x86 and ppc are when building Universal Binaries (and this is no fluke, there are plenty of ARM-specific patches in the source to enforce this behavior). What this means among other things is that libtool (the tool which builds libraries) will refuse to mix together ARMv6 and ARMv7 object files; instead, it will combine them separately, then put the results side by side in a fat library.

So in our case what happens is that the object files from the third-party library, being ARMv6, are put together with the ARMv6 object files (of which there may be none, if you’re building ARMv7-only) in the ARMv6 slice of the library target, while the ARMv7 object files are put together in the ARMv7 slice, which therefore does not include anything from the third-party library. Then when the app target is built, more precisely when its ARMv7 slice is built, the linker will find the ARMv7 slice of the library from the target, and will not look in the ARMv6 slice (as it is expected to define the same symbols, so the linker would find duplicate definitions if it tried); so the third-party library object files are never found, referenced symbols are undefined, and you wonder what is going on.

This does not happen if the third-party library is referenced directly by the app target, as then the linker will find this library with only an ARMv6 slice, in which case there is an exception to the segregation rules: since there is no ARMv7 version, these ARMv6 object files will be searched for symbols and linked in to satisfy the dependencies instead, even though the linker is building for ARMv7; the linker is the only one allowed to mix together ARMv6 and ARMv7 object files. So the ARMv6-only library is working in this case, which may lead the supplier of the library to believe the library does not need to be updated to have an ARMv7 slice, while in fact it does need to, otherwise it will cause problems for people who have more complex project structures.

do not think I am too prude to be spelling out the f-word here; I am just saving f-bombs on this blog for when they are really worth it↩