Рост популярности Apple Silicon привёл к неожиданной проблеме: разработчики пишут ассемблерный код под 64-битную ARM, который отлично работает на Mac, но ломается на всех остальных ARM-устройствах — одноплатниках, серверах под Linux или BSD. Всё из-за того, что они затачивают код под Darwin и Mach-O, забывая про существование ELF.
На самом деле написать переносимый код несложно. Нужно лишь помнить о нескольких различиях между Mach-O и ELF ABI и обходить Apple-специфичные расширения синтаксиса.
Первое и главное: в Mach-O все символы получают префикс с подчёркиванием. Функция unmask в коде на C в ассемблере под Darwin превращается в _unmask. Второе — ELF различает типы данных (STT_FUNC, STT_OBJECT), а в Mach-O такого нет. Директива .type для ELF-целей там не поддерживается.
Есть и мелкие отличия в Platform ABI. Например, регистр x18 зарезервирован в Darwin (и обнуляется при переключении контекста), зарезервирован на Android, но свободно используется в GNU/Linux и Alpine. Если ваш код трогает x18 — он упадёт на Mac.
Вторая крупная ловушка — Apple-специфичные мнемоники для NEON. Они упрощают запись инструкций, но несовместимы с официальным синтаксисом ARM. Вместо короткой записи нужно указывать layout памяти для каждого регистра: eor v2.16b, v2.16b, v0.16b.
Рецепт переносимости прост. Для ABI-деталей — два макроса. Первый прячет подчёркивание для Darwin:
#ifdef __APPLE__
# define PROC_NAME(__proc) _ ## __proc
#else
# define PROC_NAME(__proc) __proc
#endif
Второй макрос опционален — он добавляет .type для ELF, а под clang (где его нет) превращается в пустышку:
#ifdef __clang__
# define TYPE(__proc, __typ)
#else
# define TYPE(__proc, __typ) .type __proc, __typ
#endif
Дальше код пишется как обычно, но с этими макросами:
.global PROC_NAME(unmask)
.align 2
TYPE(unmask, @function)
PROC_NAME(unmask):
Для NEON — просто используйте синтаксис из официального ARM-мануала, а не Apple-сокращения. Следуя этим правилам, вы получите ассемблер, который собирается под любой UNIX на 64-битной ARM.