В ноябре 2020 года компания Apple представила серию персональных компьютеров (Mac Mini) и ноутбуков (MacBook Air, MacBook Pro), которые работали на архитектуре Apple Silicon. Основу этой архитектуры составлял чип M1, который применял архитектуру ARM. С этого момента ноутбуки и персональные компьютеры на ARM стали активно распространяться на рынке. По некоторым оценкам доля ПК/ноутбуков на ARM на момент 2023 года состявляет около 15% и, как предполагается, в ближайшие годы будет расти. Из этого количества около 90% занимают компьютеры от фирмы Apple, то есть машины на базе чипов M1 и M2. Что делает актуальным разработку под эти устройства, в том числе с применением ассемблера. Рассмотрим, как создать первую программу на ассемблере ARM64 для MacOS.
Для создания программы на ассемблере ARM64 для MacOS прежде всего необходимо установить и настроить компилятор clang, который будет использоваться для компиляции программы. Для установки clang необходимо установить Xcode Command Line Tools. Эти инструменты устанавливаются вместе с XCode, поэтому самый простой способ установки компилятора - это установка XCode.
После установки XCode следует проверить корректность установки компилятора Clang. Для этого откроем терминал и выполним команду
clang -v
. Терминал должен отобразить версию clang, наподобие:
eugene@MacBook-Pro-Eugene arm64 % clang -v Apple clang version 14.0.3 (clang-1403.0.22.14.1) Target: arm64-apple-darwin22.5.0 Thread model: posix InstalledDir: /Library/Developer/CommandLineTools/usr/bin eugene@MacBook-Pro-Eugene arm64 %
Если консоль не распознает команду clang, и возникает ошибка следующего типа
eugene@MacBook-Pro-Eugene % clang -v xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun eugene@MacBook-Pro-Eugene %
то в этом случае следует явным образом установить Xcode Command Line Tools с помощью следующей команды
xcode-select --install
Итак, когда clang установлен, мы можем приступать к написанию первой программы на ассемблере ARM64. В целом язык ассемблера для Apple Silicon аналогичен стандартному языку ассемблера ARM, который применяется в компиляторах GNU GCC. В свой документации компаниия Apple указывает разве что две особенности:
Регистр X18 зарезервирован, и его не следует использовать
Регистр указателя регистра стека (FP, X29) должен хранить валидное значение.
Однако есть еще ряд особенностей, связанных прежде всего с загрузкой данных, которые далее будут рассмотрены.
Для хранения файлов с исходным кодом определим какую-нибудь папку, например, назовем ее arm64, и в ней создадим файл, который будет называться hello.s и в который поместим следующий код:
// // METANIT.COM. Программа на ассемблере для Mac OS Silicon, которая // выводит на консоль строку "Hello METANIT.COM!" // .global _start // Устанавливаем точку входа в программу для компоновщика .align 2 // Для MacOS требуется выравнивание в 4 байта // _start - точка входа в программу _start: mov X0, #1 // значение 1 представляет стандартный поток вывода (консоль) adr X1, message // передаем адрес строки для вывода на консоль mov X2, #19 // размер строки в байтах mov X16, #4 // номер системного вызова Unix для записи в поток (на консоль) svc #0x80 // вызываем системную функцию с номером 4 // выход из программы mov X0, #0 // устанавливаем код возврата mov X16, #1 // системный вызов 1 завершает программу svc #0x80 // вызываем системную функцию с номером 1 message: .ascii "Hello METANIT.COM!\n"
Рассмотрим поэтапно данный код. Прежде всего стоит отметить, что два слеша // указывают на комментарий. Комментарии служат лишь для разъяснения тех или иных участков программы и на ее компиляцию и выполнение никак не влияют.
Вначале идет директива .global:
.global _start
Данная директива делает видимой извне определенную метку программы. В данном случае метку _start
, которая является точкой входа в программу. Благодаря этому компоновщик при компоновке программы в исполняемый файл сможет увидеть данную метку.
Далее для работы программы на MacOS нам надо установить выравнивание по 4 байтам с помощью директивы .align:
.align 2
Директиве .align
передается степень двойки для установки выравнивания последующиз инструкций. То есть здесь идет выравнивание по 22 = 4 байтам.
Далее идет метка _start, на которую собственно и проецируется программа. То есть эта метка - точка входа в программу. Название метки - произвольное, но обычно это или _start
или _main
Что делает наша программа? Она будет выводить строку на консоль. Прежде всего в конце программы размещаем саму строку, которая проецируется на метку message
:
message: .ascii "Hello METANIT.COM!\n"
Для определения строки применяется директива .ascii, после которой в двойных кавычках располагается сама строка. И теперь нам надо вывести эту строку на консоль. Для взаимодействия с ресурсами операционной системы применяются системные вызовы (syscalls). В частности, для вывода на консоль нам нужно обратиться к системному вызову с номером 4 (каждый системный вызов имеет определенный номер) и передать ему некоторое значения.
Сначала с помощью инструкции MOV помещаем в регистр Х0 число 1:
mov X0, #1
Это число указывает, что мы будем выводить данные в стандартный поток вывода, грубо говоря на консоль.
Затем с помощью инструкции ADR в регистр Х1 помещаем адрес выводимой строки - строки message:
adr X1, message
Далее указываем размер строки в байтах и помещаем этот размер инструкцией MOV в регистр Х2 (в строке ASCII каждый символ занимает 1 байт):
mov X2, #19
Далее в регистр Х16 помещаем номер системного вызова, который собственно и будет выводить строку на консоль:
mov X16, #4
Это системный вызов с номером 4 или системная функция write
.
Итак, мы все параметры установили, теперь вызовем сам системный вызов - для этого применяется инструкция svc, которой в качестве операнда
передается шестнадцатеричное число 0x80
svc #0x80
После выполнения данной инструкции на консоль будет выведена нужная нам строка. После этого нам надо завершить программу. Для завершения программы применяется системный вызов 1. Он принимает один параметр - код возврата, который помещается в регистр Х0:
mov X0, #0
Обычно числовой код 0 говорит об успешном выполнении программы. В реальности же здесь может быть произвольное число. Далее в регистр Х16 помещаем номер системного вызова - 1 (системная функция exit
):
mov X16, #1
И в конце также с помощью инструкции svc обращаемся к этому системному вызову:
svc #0x80
Для компиляции программы перейдем в терминале к папке, где располагается файл hello.s, и выполним следующую команду
as hello.s -o hello.o -arch arm64
Данная команда обращается к ассемблеру as, получает файл с исходным кодом "hello.s" и компилирует его в объектный файл hello.o. Так как clang - многоплатформенный компилятор, то также с помощью флага -arch указываем, что мы компилируем под платформу arm64 (то есть под MacOS Silicon).
После этого в папке с исходным файлом появится объектный файл hello.o. Теперь его надо скомпилировать в исполняемый файл, чтобы мы могли его запускать на том же MacOS ARM64. Для этого в терминале выполним следующую команду
ld -o hello hello.o -l System -syslibroot `xcrun -sdk macosx --show-sdk-path` -e _start -arch arm64
Здесь программе компоновщика (линкера) передается набор опций. Вкратце пробежимся по ним:
-o hello hello.o
Опция -o
указывает, что надо скомпоновать объектный файл hello.o в исполняемый файл hello
-l System
Эта опция указывает компоновщику связать наш исполняемый файл с библиотекой libSystem.dylib. Это делается, чтобы добавить команду загрузки
LC_MAIN
в исполняемый файл.
-syslibroot `xcrun -sdk macosx --show-sdk-path`
Чтобы компоновщик смог найти библиотеку libSystem.dylib
, необходимо сообщить ему, где эту библиотеку найти. Выражение
xcrun -sdk macosx --show-sdk-path
устанавливает путь к используемому для компоновки SDKб а точнее текущую активную версию Xcode.
-e _start
По умолчанию в качестве точки входа в программу компоновщик рассматривает метку _main. В нашей же программу такой точкой входа, с которой начинается программа, является метка _start. И с помощью этого параметра сообщаем компоновщику, что точка входа - _start
-arch arm64
Указывает, что мы компилируем под архитектуру arm64
. Особенно это актуально, если мы компилируем для машины MasOS Silicon (на чипах M1/M2/M3) на компьютерах MacOS на процессоре Intel x86-64. Если же мы компилируем на компьтере MasOS Silicon, то этот параметр можно опустить.
После выполнения этой команды в текущей папке появится также исполняемый файл hello. И мы можем запустить его на выполнение командой
./hello
и на консоль должна быть выведена строка.
Полный вывод:
eugene@MacBook-Pro-Eugene arm64 % as -arch arm64 -o hello.o hello.s eugene@MacBook-Pro-Eugene arm64 % ld -o hello hello.o -lSystem -syslibroot `xcrun -sdk macosx --show-sdk-path` -e _start -arch arm64 eugene@MacBook-Pro-Eugene arm64 % ./hello Hello METANIT.COM! eugene@MacBook-Pro-Eugene arm64 %
Выше указанные команды компиляции и компоновки будут одинаковым образом работать и на MacOS Silicon, и на MacOS Darwin (Intel x86-64), единственное, что на MacOS Darwin мы не сможем запустить приложение, скомпилированное и скомпонованное для ARM64.
Однако если текущий компьютер, на котором выполняется компиляция приложения, работает на платформе Apple Silicon, то есть на той же платформе, для которой компилируется приложение,
то из команд компиляции/компоновки мы можем убрать опцию -arch arm64
:
as -o hello.o hello.s ld -o hello hello.o -lSystem -syslibroot `xcrun -sdk macosx --show-sdk-path` -e _start