На операционной системе MacOS на платформе Intel x86-64 (так называемая архитектура Darwin) мы можем использовать ассемблер Clang, который во много работает аналогично GNU ассемблеру AS и поддерживает все те же команды. Второй способ представляет установка непосредственно самого GNU ассемблера AS со всем комплектом компиляторов GCC. Поскольку первый способ наиболее простой и наиболее распространен, то рассмотрим сначала его.
И также обращаю внимание, что данном случае речь идет именно о MacOS на платформе Intel x86-64, а не MacOS M1|M2 на платформе ARM.
Прежде всего необходимо установить и настроить компилятор clang, который будет использоваться для компиляции программы. Для установки clang необходимо установить Xcode Command Line Tools. Эти инструменты устанавливаются вместе с XCode, поэтому самый простой способ установки компилятора - это установка XCode.
После установки XCode следует проверить корректность установки компилятора Clang. Для этого откроем терминал и выполним команду
clang -v
. Терминал должен отобразить версию clang, наподобие:
eugene@MacBook-Pro-Eugene % 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 %
Если консоль не распознает команду 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 установлен, мы можем приступать к написанию первой программы на ассемблере. Определим в файловой системе файл, который пусть будет называться hello.s и будет иметь следующий код:
.globl _start .data message: .asciz "Hello METANIT.COM!\n" # текст выводимого сообщения .equ len, .-message .text _start: leaq message(%rip), %rsi # в RSI - адрес строки movq $1, %rdi # в RDI - дексриптор вывода в стандартный поток (консоль) movq $len, %rdx # в RDX - длина строки movq $0x2000004, %rax # в RAX - номер функции для вывода в поток syscall # вызываем функцию Linux movq $0x2000001, %rax syscall
Рассмотрим поэтапно данный код. Вначале идет директива .global:
.global _start
Данная директива делает видимой извне определенную метку программы. В данном случае метку _start
, которая является точкой входа в программу. Благодаря этому компоновщик при компоновке программы в исполняемый файл сможет увидеть данную метку.
Затем расположена секция данных
.data message: .asciz "Hello METANIT.COM!\n" # текст выводимого сообщения .equ len, .-message
В этой секции размещаем саму строку, которая проецируется на метку message
:
message: .asciz "Hello METANIT.COM!\n"
Для определения строки применяется директива .asciz, после которой в двойных кавычках располагается сама строка.
Чтобы знать длину строку (для ее вывода на экран), определяем с помощью директивы .equ константу len
, значением которой будет результат
выражения .-message
. Точка указывает на текущий адрес. То есть от текущего адреса отнимаем адрес переменной message. Таким образом, мы получим
количество байта от начала адреса message до ее конца - размер message в байтах.
Далее идет секция кода, которая называется .text, и в ней первым делом определена метка
_start, на которую собственно и проецируется программа. То есть эта метка - точка входа в программу. Название метки - произвольное, но обычно это или _start
или _main
.text _start:
И теперь нам надо вывести строку из секции .data на консоль. Для взаимодействия с ресурсами операционной системы применяются системные вызовы (syscalls). В частности, для вывода на консоль нам нужно обратиться к системному вызову с номером 4 (каждый системный вызов имеет определенный номер) и передать ему некоторое значения.
Список всех системных вызовов, а также их номера, названия функций и параметры можно посмотреть на странице https://opensource.apple.com/source/xnu/xnu-1504.3.12/bsd/kern/syscalls.master
В целом MacOS на Intel x86-64 следует тем же условностям, что и Linux:
Номер системного вызова помещается в регистр %rax
Параметры для системных вызовов передаются через регистры %rdi, %rsi, %rdx, %rcx в порядке следования, то есть значение для первого параметра передается через регистр %rdi, значение для второго - через регистр %rsi и так далее. Остальные параметры передаются через стек
Регистр %rax содержит код возврата системного вызова - результат функции
Сначала с помощью инструкции leaq в регистр %rsi помещаем адрес выводимой строки - строки message:
leaq message(%rip), %rsi # в RSI - адрес строки
Причем важно, что здесь мы используем адресацию относительно регистра %rip, и адреса строки определяется как message(%rip)
Далее с помощью инструкции movq помещаем в регистр %rdi число 1:
movq $1, %rdi # в RDI - дексриптор вывода в стандартный поток (консоль)
Это число указывает, что мы будем выводить данные в стандартный поток вывода, грубо говоря на консоль.
Далее помещаем значение константы len, которая хранит размер строки в байтах, в регистр RDX (в строке ASCII каждый символ занимает 1 байт):
movq $len, %rdx # в RDX - длина строки
Далее в регистр %rax помещаем номер системного вызова, который собственно и будет выводить строку на консоль:
movq $0x2000004, %rax # в RAX - номер функции для вывода в поток
Формально это системный вызов с номером 4 или системная функция write
. Но по факту полный номер - 0x2000004
,
то есть 0x2000000 плюс номер функции.
Итак, мы все параметры установили, теперь вызовем сам системный вызов с помощью инструкции syscall
syscall
После выполнения данной инструкции на консоль будет выведена нужная нам строка. После этого нам надо завершить программу. Для завершения программы применяется системный вызов 1:
movq $0x2000001, %rax syscall
В целом программа выглядит также, как и для Linux, только для обращения к данным применяется адресация относительно регистра %rip и номера системных вызовов немного отличаются.
Для компиляции программы перейдем в терминале к папке, где располагается файл hello.s, и выполним следующую команду
as hello.s -o hello.o
Данная команда обращается к ассемблеру as, получает файл с исходным кодом "hello.s" и компилирует его в объектный файл hello.o.
После этого в папке с исходным файлом появится объектный файл hello.o. Теперь его надо скомпилировать в исполняемый файл. Для этого в терминале выполним следующую команду
ld -o hello hello.o -l System -syslibroot `xcrun -sdk macosx --show-sdk-path` -e _start
Здесь программе компоновщика (линкера) передается набор опций. Вкратце пробежимся по ним:
-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
После выполнения этой команды в текущей папке появится также исполняемый файл hello. И мы можем запустить его на выполнение командой
./hello
и на консоль должна быть выведена строка.
Полный вывод:
eugene@MacBook-Pro-Eugene asm % as -o hello.o hello.s eugene@MacBook-Pro-Eugene asm % ld -o hello hello.o -lSystem -syslibroot `xcrun -sdk macosx --show-sdk-path` -e _start eugene@MacBook-Pro-Eugene asm % ./hello Hello METANIT.COM! eugene@MacBook-Pro-Eugene asm %
В качестве альтернативы компилятору и ассемблеру Clang, можно использовать стандартный GNU ассемблер AS из набора компиляторов GCC. Установка GCC обычно производится с помощью менеджера пакетов Homebrew. Соответственно сначала надо установить сам менеджер, выполнив в терминале следующую команду:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Затем устанавливаем сам пакет GCC с помощью следующей команды:
brew install gcc