Первая программа на MacOS

Последнее обновление: 11.10.2023

NASM поддерживает работу на MacOS на архитектуре Intel x86-64 (так называемая архитектура Darwin), поэтому рассмотрим как создать простейшую программу на MacOS.

Установка

На MacOS Intel x86-64 можно установить Nasm разными способами. Первый способ - загрузить все файлы ассемблера с официального сайтаб в частности с адреса https://www.nasm.us/pub/nasm/releasebuilds/2.16.01/macosx/. После загрузки в папке архива мы можем найти файл nasm, который и будет представлять ассемблер. К примеру, я загрузил и распаковал архив в папку /Users/eugene/Documents/nasm-2.16.01/, соответственно для обращения к ассемблеру я могу использовать путь /Users/eugene/Documents/nasm-2.16.01/nasm. Для упрощения работы можно добавить путь в переменные окружения.

Второй способ представляет установка через пакетный менеджер Homebrew. Соответственно сначала надо установить сам менеджер, выполнив в терминале следующую команду:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Затем устанавливаем ассемблер Nasm с помощью следующей команды:

brew install nasm

Вне зависимости, какой мы способ используем для установки, у нас будет один и тот же компилятор.

Но NASM - это только ассемблер - он компилирует код в объектный файл, который содержит машинные инструкции и который понимает компьютер. Однако объектный файл - это не исполняемый файл. Создание исполняемого файла из объектного представляет процесс, который называет компоновка (или линковка - linking). И для этой цели нам потребуется программа компоновщика (еще называемого линковщиком или линкером).

На MacOS наиболее распространенным компоновщиком является компоновщик компилятора Clang - программа ld. Поэтому прежде всего необходимо установить и настроить компилятор 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

Начало работы с NASM

Определим в файловой системе каталог для файлов с исходным кодом и создадим в нем следующий файл hello.asm:

global _start           ; делаем метку метку _start видимой извне

section .text           ; объявление секции кода
align 4                 ; для программ для MacOS необходимо выравнивание
_start:                 ; объявление метки _start - точки входа в программу
    mov rax, 0x02000001 ; 1 - номер системного вызова exit 
    mov rdi, 22         ; произвольный код возврата - 22 
    syscall             ; выполняем системный вызов exit

Рассмотрим поэтапно данный код. Вначале идет директива global:

global _start

Данная директива делает видимой извне определенную метку программы. В данном случае метку _start, которая является точкой входа в программу. Благодаря этому компоновщик при компоновке программы в исполняемый файл сможет увидеть данную метку.

Затем идет секция кода программы, которая и определяет выполняемые программой действия. Для определения секции применяется директива section, после которой указывается имя секции. Причем секция кода программы должна называться .text.

section .text 

Код программы для MacOS должен иметь выравнивание в 4 байта. И для установки выравнивания прописываем директиву align - ей передается количество байтов

align 4

Далее собственно идет код программы. И он начинается с определения метки _start, на которую собственно и проецируется программа. Сама по себе метка представляет произвольное название, после которого идет двоеточие. После двоеточия могут идти инструкции программы или определения данных.

Метка _start выступает в качестве точки входа в программу. Название подобной метки произвольное, но обычно это или _start или _main

Наша программа не производит какой-то феноменальной работы. Все что она делает - это завершается. Да, чтобы программе завершиться, ей надо произвести некоторую работу. А именно нам надо сказать операционной системе, чтобы она завершила программу. Для взаимодействия с операционной системе предназначены системные вызовы. Каждый системный вызов имеет определенный номер и может принимать некоторые данные - параметры. Так, чтобы указать операционной системе, что мы хотим завершить программу, нам надо выполнить системный вызов с номером 1 (который еще называется "exit"). Номер системного вызова передается в регистр rax:

mov rax, 0x02000001

То есть в данном случае мы выполняем инструкцию mov, которая помещает в первый операнд - регистр rax значение из второго операнда - число 0x02000001. Префикс 0x перед числом 02000001 указывает, что данное число - шестнадцатиричное. Хотя номер системного вызова завершения программы - 1, но для macOS по факту полный номер - 0x2000001, то есть 0x2000000 плюс номер вызова.

Системный вызов exit может принимать один параметр - статусный код возврата - некоторое число, которое указывает на статус выполнения программы (успешно завершилась программа или нет и если нет, то почему). В реальности это может быть произвольное число. Первый параметр системных вызовов всегда помещается в регистр rdi. Поэтому следующая инструкция mov помещает в регистр rdi число 22 (число выбрано произвольным образом):

mov rdi, 22

И в конце для выполнения системного вызова выполняется инструкция syscall

syscall

Кроме директив и инструкций, которые определяют действия программы, также следует отметить комментарии. Комментарии начинаются с точки с запятой ;. Комментарии не учитываются при компиляции, никак не влияют на объект или работоспособность программы и нужны лишь в качестве текстового описания отдельных строк или блоков программы.

global _start  ; делаем метку метку _start видимой извне - это текст комментария

Компиляция

Наша программа готова. Теперь скомпилируем ее. Для компиляции перейдем в терминале к месту расположения файла. Если мы загрузили компилятор nasm с официального сайта (по ссылке из начала статьи), то нам надо указать путь к компилятор. В моем случае это путь /Users/eugene/Documents/nasm-2.16.01/nasm, соответственно я выполняю команду

/Users/eugene/Documents/nasm-2.16.01/nasm -f macho64 hello.asm -o hello.o

Здесь ассемблеру передается файл "hello.asm" и с помощью опции -f указывается формат файла, в который мы хотим скомпилировать код. Для MacOS формат файла - macho64.

Опция -o hello.o указывает на имя скомпилированного файла. В результате выполнения этой команды будет создан объектный файл hello.o

Если nasm установлен через Homebrew, то можно просто написать имя файла ассемблера:

nasm -f macho64 hello.asm -o hello.o

Далее скомпонуем объектный файл в исполняемый файл. Для этого выполним команду

ld -o hello hello.o -l System -syslibroot `xcrun -sdk macosx --show-sdk-path` -e _start

Здесь программе компоновщика (линкера) ld передается набор опций. Вкратце пробежимся по ним:

  • -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

Консоль ничего не отобразит, поскольку все, что делает наша программа - это завершается. Однако в нашей программе также устанавливается статусный код возврата - число 22. Получим его, выполнив команду:

echo $?

Компиляция программы и ее запуск полностью:

eugene@MacBook-Pro-Eugene nasm % nasm -f macho64 hello.asm -o hello.o
eugene@MacBook-Pro-Eugene nasm % ld -o hello hello.o -l System -syslibroot `xcrun -sdk macosx --show-sdk-path` -e _start
eugene@MacBook-Pro-Eugene nasm % ./hello
eugene@MacBook-Pro-Eugene nasm % echo $?
22
eugene@MacBook-Pro-Eugene nasm %

Создание первой программы

Теперь создадим более осмысленную программу и для этого изменим файл hello.asm следующим образом:

global _start

section .text   ; секция кода программы
align 4
_start:    
    mov rax, 0x02000004     ; системный вызов функции write
    mov rdi, 1              ; 1 - дескриптор файла стандартного вызова stdout
    lea rsi, [rel message]  ; адрес строки для вывод
    mov rdx, 19    ; количество байтов
    syscall                 ; выполняем системный вызов
    mov rax, 0x02000001     ; 1 - номер системного вызова exit
    syscall                 ; выполняем системный вызов exit

section .data   ; секция данных
message: db "Hello METANIT.COM!",10  ; строка для вывода на консоль

Для вывода строки на консоль, нам надо выполнить еще один системный вызов - вызов номер 4 (он же - системная функция write). Данные, которые будут применяться для вывода, определены в конце программы в секции данных - секции .data:

section .data   ; секция данных
message: db "Hello METANIT.COM!",10  ; строка для вывода на консоль

В секции .data определена метка message, на которую проецируется строка. После метки указывается тип данных. Строка в ассемблере - это просто набор байтов, поэтому имеет тип db. Затем в кавычках определяется собственно выводимая строка - "Hello METANIT.COM!",10. Обратите внимание на 10 после строки - это код символа перевода строки. То есть при выводе мы ожидаем, что будет происходить перевод на другую строку.

Для вывода этой строки на консоль в регистр rax помещаем номер системного вызова - номер 4:

mov rax, 0x02000004

Для вывода нам надо указать в регистре rdi дескриптор стандартного вывода - по умолчанию это число 1

mov rdi, 1

В регистр rsi надо загрузить адрес выводимой строки:

lea rsi, [rel message]

Для загрузки адреса применяется инструкция lea, а для получения адреса имя переменной (строки) помещается в квадратные скобки [rel message]. При загрузке адреса в MacOS применяется адресация относительно регистра rip, поэтому перед названием переменной указывается слово rel.

Кроме того, нам надо указать в регистре rdx сколько байтов строки мы хотим выводить. Каждый символ строки ASCII занимает 1 байт. В нашей строке 19 символов (учитывая символ с числовым кодом 10 в конце строки), поэтому в rdx помещаем число 19:

mov rdx, 19 

Для выполнения системного вызова выполняем инструкцию syscall

Остальные инструкции программы - это выполнение системного вызова завершения программы, который был рассмотрен ранее.

Повторно скомпилируем программу и запустим ее на выполнение:

eugene@MacBook-Pro-Eugene nasm % nasm -f macho64 hello.asm -o hello.o 
eugene@MacBook-Pro-Eugene nasm % ld -o hello hello.o -l System -syslibroot `xcrun -sdk macosx --show-sdk-path` -e _start
eugene@MacBook-Pro-Eugene nasm % ./hello                             
Hello METANIT.COM!
eugene@MacBook-Pro-Eugene nasm % 
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850