GNU ассемблер под MacOS Intel x86-64

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

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

На операционной системе MacOS на платформе Intel x86-64 (так называемая архитектура Darwin) мы можем использовать ассемблер Clang, который во много работает аналогично GNU ассемблеру AS и поддерживает все те же команды. Второй способ представляет установка непосредственно самого GNU ассемблера AS со всем комплектом компиляторов GCC. Поскольку первый способ наиболее простой и наиболее распространен, то рассмотрим сначала его.

И также обращаю внимание, что данном случае речь идет именно о MacOS на платформе Intel x86-64, а не MacOS M1|M2 на платформе ARM.

Установка Clang

Прежде всего необходимо установить и настроить компилятор 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 % 

Установка GCC

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

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

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

brew install gcc
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850