
Полная версия:
Ассемблер ARM64

Андрей Ипполитов
Ассемблер ARM64
Об авторе
Андрей – родился в Москве и с раннего возраста увлекся программированием. После получения образования в «МГУПС», он много экспериментировал с разными языками программирования. Автор живет в Подмосковье, помимо писательства, активно занимается созданием программы kitasm. Его жизненная философия вдохновляет читателей на программирование.
Сейчас Андрей работает над новыми проектами и продолжает радовать своих поклонников свежими сюжетами и идеями.
Пролог
Ночь в лаборатории была тёплой и тихой – свет индикаторов на панелях мерцал, как звёзды в искусственном небе. За столом, у старого терминала с потёртым шрифтом на экране, сидел человек, для которого цифры и биты были больше, чем просто знаки: они были языком мышления, тайной, которую стоило расшифровать.
Он вспомнил свои первые шаги: как нерешительно вводил команды, наблюдая за реакцией машины, как одна простая инструкция могла изменить поток исполнения, как каждая метка и каждое смещение были крошечными рычагами управления вселенной микропроцессора. Ассемблер казался тогда заклинанием – строгой рифмовкой операторов и регистров, где ошибка в одном символе могла превратить систему в молчаливого монстра.
Эта книга – не о магии и не о забытом ремесле, а о языке, который позволяет взглянуть в сердце вычислений. Здесь нет волшебных трюков, только чистая логика и точность: как строятся инструкции, как передаются данные, как осуществляется контроль потока, как из простых операций вырастает сложная программа. Ассемблер – это алхимия простых операций, превращающая электрические импульсы в смысл.
Читатель встретит здесь не сухие описания и не абстрактные теории, а практику: примеры, отладочные приёмы, шаблоны решения типовых задач и объяснения, почему тот или иной подход оказывается эффективнее. Мы пройдём путь от базовых команд к реальным программам, от простейших циклов до управления прерываниями, работы с памятью и оптимизации исполнения.
Если вы пришли из высокоуровневых языков – приготовьтесь к смене парадигмы. Ассемблер потребует точности мышления, понимания архитектуры и умения мыслить в терминах машинных состояний. Но взамен он даст необычную, почти интимную близость к машине: вы научитесь слышать её ритм и предугадывать её поведение.
Если же вы уже знакомы с ассемблером – возможно, вы найдёте здесь новые взгляды и полезные приёмы, способные сделать код компактнее, быстрее и яснее.
Перед началом: выключите лишние шумы, подготовьте отлаживающую среду и запаситесь терпением. Путь будет нелёгким, но каждое пройденное испытание откроет новую грань понимания того, как устроен мир компьютеров – от стека вызовов до мельчайших переключений флагов.
Добро пожаловать в мир, где байты говорят правду.
Введение
Добро пожаловать в мир низкоуровневого программирования, где каждая инструкция имеет значение, а аппаратное обеспечение раскрывает свои самые сокровенные тайны! Эта книга – ваш проводник в увлекательное путешествие по архитектуре ARM64 и языку ассемблера, ориентированному на платформу macOS. Если вы когда-либо задумывались, как на самом деле работают программы, что скрывается за магией высокоуровневых языков, или стремитесь получить максимальную производительность от своего Mac, то эта книга для вас.
Почему ARM64 и MacOS?
Современные устройства Apple, от iPhone и iPad до MacBook и Mac Pro, работают под управлением процессоров на базе архитектуры ARM64. Это означает, что понимание ARM64 – это ключ к пониманию сердца большинства устройств, которыми мы пользуемся каждый день. macOS, как операционная система, тесно интегрирована с этой архитектурой, предоставляя уникальные возможности и инструменты для разработки на ассемблере.
Что такое Assembler?
Assembler (или язык ассемблера) – язык программирования, который находится на очень низком уровне. Он представляет собой почти прямое отображение машинных инструкций, которые процессор выполняет напрямую. В отличие от высокоуровневых языков, таких как Python, Java или C++, где вы работаете с абстракциями и командами, ассемблер требует от вас понимания таких вещей, как:
•
Регистры: Крошечные, сверхбыстрые ячейки памяти внутри процессора, используемые для временного хранения данных и адресов.
•
Инструкции: Фундаментальные операции, которые процессор может выполнять, такие как сложение, вычитание, перемещение данных, условные переходы и вызовы функций.
•
Память: Адресация ячеек основной памяти, где хранятся данные и код программы.
•
Архитектура набора команд: Набор всех инструкций, которые может выполнять конкретный процессор.
Почему стоит изучать Assembler на ARM64 MacOS?
Хотя ассемблер может показаться сложным и трудоемким, его изучение открывает двери к:
•
Глубокому пониманию работы компьютера: Вы начнете видеть, как программы взаимодействуют с аппаратным обеспечением на самом фундаментальном уровне.
•
Оптимизации производительности: Для критически важных участков кода, где важна каждая наносекунда, ассемблер позволяет добиться максимальной скорости выполнения.
•
Разработке операционных систем и драйверов: Эти области часто требуют прямого взаимодействия с аппаратным обеспечением.
•
Реверс-инжинирингу и анализу вредоносного ПО: Понимание ассемблера – неотъемлемая часть изучения того, как программы работают “изнутри”, что важно для безопасности.
•
Созданию кросс-платформенных решений: Знание
ARM64
пригодится вам при работе с различными устройствами, использующими эту архитектуру.
•
Отладке сложных проблем: Иногда только понимание ассемблерного кода может помочь выявить и исправить трудноуловимые ошибки.
Что вы узнаете из этой книги:
В этой книге мы последовательно разберем:
•
Основы архитектуры
ARM64:
Понимание регистровой модели, режимов выполнения и общих концепций.
•
Базовые инструкции
ARM64:
Изучение основных операций для работы с данными, арифметики, логики и управления потоком выполнения.
•
Системные вызовы
macOS:
Как ваша программа взаимодействует с операционной системой для выполнения таких задач, как ввод/вывод, выделение памяти и работа с файлами.
•
Процесс сборки и отладки: Использование инструментов, доступных на
macOS
, для компиляции и отладки ассемблерного кода. Практические примеры: От простых программ до более сложных задач, демонстрирующих применение полученных знаний.
•
Особенности
ARM64
на
macOS:
Специфические моменты, связанные с работой на этой платформе.
Кому предназначена эта книга:
•
Начинающим программистам: Если вы хотите копнуть глубже, чем просто написание скриптов.
•
Опытным разработчикам: Желающим расширить свои знания и освоить новые парадигмы программирования.
•
Системным администраторам и
DevOps-
специалистам: Интересующимся внутренним устройством систем.
•
Студентам технических специальностей: Изучающим информатику, компьютерную инженерию или смежные области.
Предварительные знания:
Для успешного освоения материала желательно иметь базовое представление о принципах работы компьютеров и программирования. Знакомство с каким-либо высокоуровневым языком программирования также будет плюсом, так как вы сможете проводить параллели и лучше понимать абстракции.
Путь к мастерству:
Изучение ассемблера – это марафон, а не спринт. Не ожидайте, что вы станете экспертом за одну ночь. Главное – это терпение, практика и последовательность. Я предлагаю вам вместе шаг за шагом разбирать каждую тему, решать практические задачи и экспериментировать.
Готовы ли вы к этому захватывающему путешествию? Откройте для себя мир, где код оживает, и где вы контролируете каждое действие процессора.
Далее, по умолчанию, все относится к ассемблеру GAS для MacOS на чипе ARM64 (M chip)
Прежде чем начать
Прежде чем писать код, нам нужно убедиться, что у нас есть все необходимое. На macOS для разработки на ассемблере нам потребуются:
Xcode Command Line Tools: Это набор утилит командной строки, который включает в себя компилятор (ассемблер) as и компоновщик ld, а также отладчик lldb. Если они еще не установлены, вы можете сделать это, выполнив в Терминале:
$ xcode-select —install
Затем установим IDE. IDE расшифровывается как интегрированная среда разработки (Integrated Development Environment). Это программное обеспечение, которое предоставляет разработчикам следующие функции:
•
Редактор кода: удобный инструмент для написания и редактирования программного кода.
•
Компилятор или интерпретатор: преобразует написанный код в исполняемый файл.
•
Отладчик: помогает находить и устранять ошибки в коде.
IDE значительно упрощает процесс разработки, собирая все необходимые инструменты в одном приложении. Мы скачаем и установим программу kitasm. Для установки пройдите на сайт www.kitasm.site и загрузите последнюю версию. Программа платная, но можно скачать демо версию, она тоже подойдет. Затем разархивируйте пакет с программой, войдите в терминал и введите команду:
$ sudo xattr -r -d com.apple.quarantine kitasm.app
Эта команда разрешит запуск программы, загруженной из интернета. Теперь запустим kitasm.
Программа kitasm
Слева панель для отображения меток и переменных, справа панель для вывода регистров дебаггером. Посередине простой редактор кода с подсвечиваемым синтаксисом. Внизу окно для вывода логов.
Глава 1 Первая программа
Создание первой программы с использованием ассемблера – увлекательный процесс, который позволяет увидеть, как работает низкоуровневое программирование. Давайте создадим простую программу, которая выводит текст на экран.
Hello world
Откроем kitasm и напишем следующий код:
.global _start
.text
_start:
; Системный вызов для записи (write)
; x0 = файловый дескриптор (1 – stdout)
; x1 = указатель на буфер с сообщением
; x2 = длина сообщения
mov x0, 1 ; stdout
adr x1, message ; Указатель на строку message
mov x2, #13 ; Длина строки "Hello, ARM64!\n"
mov x16, 4 ; Номер системного вызова SYS_write
svc #0 ; Вызов ядра
; Системный вызов для завершения программы (exit)
; x0 = код возврата (0 – успешно)
mov x0, 0 ; Код возврата 0
mov x16, 1 ; Номер системного вызова SYS_exit
svc #0 ; Вызов ядра
message:
.ascii "Hello, World!\n"
Теперь сохраним в «Hello, World.s» и запустим его на выполнение. Для этого нажимаем правую кнопку мыши и выбираем пункт «run». Если все прошло успешно
внизу программы kitasm, в окне input/output вы увидите вывод фразы «Hello world!». Также там будет показан выход из программы с параметром 0. Это означает, что программа завершилась без ошибок.
Hello, World!
End program. Exitcode: 0
В папке куда вы сохранили, появится исполняемый файл «helloworld». Его можно запустить, написав в терминале команду:
$ ./helloworld
Комментарии в ассемблере обозначаются символом «;» или
«//«
Поздравляю! Вы только что написали и запустили свою первую программу на ассемблере. Этот простой пример иллюстрирует базовые принципы взаимодействия с операционной системой и работу с системными вызовами на низком уровне. Дальнейшее изучение ассемблера открывает множество возможностей для оптимизации кода и понимания работы компьютеров на более глубоком уровне.
Разберем код построчно:
•
.global
start
: Эта директива сообщает компоновщику, что метка _
start
является глобальной. В
macOS
точка входа в программу (начало исполнения) обычно называется _
start
.
•
.text
: Эта директива указывает, что далее следует секция кода, где будут располагаться исполняемые инструкции.
•
_
start
:
Это метка. В ассемблере метки используются для обозначения адресов инструкций или данных. К ним можно обращаться для переходов или вызовов
.
•
mov x0, 1
: Инструкция
mov
(
move
) перемещает значение 1 в регистр
x
0. Регистры
x
0-
x
30 используются для передачи аргументов системным вызовам и функциям. Для системного вызова
write
,
x
0 содержит файловый дескриптор. 1 означает стандартный вывод (
stdout
), куда обычно выводятся данные на консоль.
•
adr x1, message:
Инструкция
adr
(
address
) загружает в регистр x1 адрес метки
message. x1
будет содержать указатель на строку, которую мы хотим вывести.
•
mov x2, #13
: Загружаем число 13 в регистр
x
2. Это длина нашей строки “
Hello,
World
!\n”. Символ новой строки
\n
также занимает один байт
.
•
mov x16, 4
: для выполнения системных вызовов используется регистр
x
16. Номер
4
соответствует системному вызову
SYS_write.
•
svc #0
: Инструкция
svc
(
supervisor
call
) инициирует системный вызов. Она передает управление ядру операционной системы, которое выполнит запрошенную операцию (в данном случае, запись данных в
stdout
).
#0
указывает
,
что это стандартный вызов
.
•
mov x0, 0
: Для системного вызова
exit (
завершение программы), регистр
x
0 содержит код возврата. 0 означает
,
что программа завершилась успешно
.
•
mov x16, 1
: Номер 1 соответствует системному вызову
SYS
_
exit
.
•
message:
: Метка для нашей строки.
•
.ascii "Hello,
World
!\n"
: Директива .
ascii
определяет строку, состоящую из
ASCII
–символов.
\n
– это символ новой строки
.
Глава 2 Синтаксис ассемблера
Эта глава даёт практический и структурированный обзор синтаксиса ARM64 (AArch64) для GAS. Пояснения охватывают секции, директивы, регистры, инструкции, адресацию, соглашение о вызовах и типичные идиомы. Основные понятия (регистры, память, инструкции)
Байт – это базовая единица измерения информации в компьютерах, состоящая из 8 бит. Каждый бит может принимать значение 0 или 1, поэтому байт может представлять 256 различных комбинаций (от 0 до 255 в десятичной системе) или в шестнадцатеричной системе (от 0 до FF)
D9
Hex D
Hex 9
1
1
0
1
1
0
0
1
Байт состоит из 8 битов (нули и единицы)
Числа
Числа в программировании на языке ассемблера играют важную роль, поскольку они используются для представления данных, адресов и значений в программе. В ассемблере числа могут быть представлены в различных форматах и использоваться в разных контекстах.
Представление чисел
В ассемблере числа могут быть представлены в следующих форматах:
•
Десятичные числа:
Представляются в десятичной системе счисления, например: 10.
•
Шестнадцатеричные числа: Представляются в шестнадцатеричной системе счисления, например: 0
x
10 или 10
h
.
•
Двоичные числа:
Представляются в двоичной системе счисления, хотя напрямую синтаксис для двоичных чисел не поддерживается.
Типы чисел
•
Целые числа:
Представляются как 8-битные, 16-битные, 32-битные или 64-битные значения.
•
Дробные числа:
Не поддерживаются напрямую в
ARM64
ассемблере, но могут быть представлены с помощью специальных библиотек или реализаций.
Представление чисел в памяти
Числа в памяти представляются в виде двоичных или шестнадцатеричных кодов. Например:
•
8-битное целое число:
`0x10
•
64-битное целое число:
`0x0000000000000010
Операции с числами
В ассемблере поддерживаются различные операции с числами, такие как:
•
Сложение:
ADD X0, X1, X2 ; X0 = X1 + X2
•
Вычитание:
SUB X0, X1, X2 ; X0 = X1 – X2
•
Умножение:
MUL X0, X1, X2 ; X0 = X1 * X2
•
Деление:
UDIV X0, X1, X2 ; X0 = X1 / X2 (
беззнаковое)
Пример использования чисел
section .data
num1: .quad 10
num2: .quad 20
section .text
global _start
_start:
; Загрузка чисел в регистры
LDR X0, =num1 ; X0 = 10
LDR X1, =num2 ; X1 = 20
; Сложение
ADD X2, X0, X1 ; X2 = 30
; Вычитание
SUB X3, X2, X0 ; X3 = 20
RET
Числовые константы могут быть использованы напрямую в коде или объявлены с помощью директив.
.equ MY_CONST, 100
MOV X0, #MY_CONST ; X0 = 100
Числа играют ключевую роль в представлении данных и выполнении арифметических операций. Понимание того, как правильно использовать и представлять числа в различных форматах, необходимо для написания эффективных и корректных программ на языке ассемблера.
Переменные
В программировании на языке ассемблера, переменные и метки играют ключевую роль в обозначении и использовании данных в программе. Понимание того, как правильно использовать переменные и метки, необходимо для написания эффективных и корректных программ.
Переменные в ассемблере представляют собой области памяти, зарезервированные для хранения данных. Объявление переменной в ассемблере обычно включает в себя резервирование памяти под нее и возможное присвоение ей начального значения.
Объявление переменных
Переменные в ассемблере объявляются с помощью директив, таких как .byte, .half, .word, .quad и др., которые определяют размер переменной в байтах.
•
.byte: определяет байт (8 бит)
•
.half:
определяет полуслово (16 бит)
•
.word:
определяет слово (32 бита)
•
.dword:
определяет двойное слово (64 бита)
•
.
quad
: определяет двойное слово (64 бита)
•
.asciz:
определяет строку ASCII, завершающуюся нулевым байтом
•
.ascii:
определяет строку ASCII, не завершающуюся нулевым байтом
section .data my_byte: .byte 10
my_word: .word 20
my_quad: .quad 30
Метки
Метки в ассемблере используются для обозначения определенных мест в программе или адресов памяти. Они могут быть использованы для перехода к определенному участку кода или для ссылки на переменные.
Метки обычно записываются с помощью идентификатора и двоеточия:
my_label: ; Код
Типы меток
•
Метки перехода: Используются для указания места в программе, к которому можно перейти с помощью инструкции перехода (например
, B, BL).
•
Метки адреса: Используются для обозначения адресов переменных или данных.
section .data
var1: .quad 10
str1: .asciz "Hello, World!"
section .text
global _start
_start:
; Загрузка значения переменной
LDR X0, =var1 ; X0 = адрес var1
; Загрузка значения из памяти
LDR X1, [X0] ; X1 = значение по адресу var1 = 10
; Использование метки для перехода
my_loop:
; Код цикла
B my_loop ; Переход на my_loop
; Использование метки для данных
LDR X2, =str1 ; X2 = адрес str1
RET
Директива .global позволяет сделать метку или переменную доступной из других объектных файлов при линковке.
.global my_variable my_variable: .quad 100
Переменные и метки являются фундаментальными понятиями в программировании на языке ассемблера. Правильное использование переменных для хранения и манипуляции данными, а также меток для обозначения адресов и переходов, необходимо для создания эффективных и корректных программ.
Константы
Константы в программировании на языке ассемблера играют важную роль, поскольку они представляют собой неизменяемые значения, используемые в программе. В ассемблере константы могут быть представлены различными способами, в зависимости от их типа и назначения.
Типы констант
•
Числовые константы: Это наиболее простой тип констант, представляющий собой конкретное числовое значение. Числовые константы могут быть заданы в различных системах счисления, таких как десятичная, шестнадцатеричная, двоичная.
•
Адресные константы: Используются для представления адресов памяти. Они часто применяются для указания адресов функций, переменных или меток.
•
ASCII
–строки: Строки, представленные в виде последовательности
ASCII
–символов.
Представление констант
•
Числовые константы
:
•
Десятичные числа записываются как обычно, например: 10.
•
Шестнадцатеричные числа начинаются с префикса
0x,
например: 0
x
10.
•
Двоичные числа можно представить, используя соответствующую двоичную запись, хотя напрямую синтаксис для двоичных чисел не поддерживается, их можно выразить через эквивалентные десятичные или шестнадцатеричные значения.
В ассемблере константы можно использовать в различных контекстах, таких как:
•
Непосредственные значения: 10 использоваться в инструкциях, например,
MOV
X
0, #10 загружает значение 10 в регистр
X
0.
•
Адреса: Константы могут использоваться для задания адресов, к которым будут обращаться инструкции загрузки/сохранения, например
, LDR X0, =my_var
загружает адрес метки
my_var
в регистр
X
0.
Пример использования констант
; Пример использования констант section .data
my_str: .asciz "Hello, World!" ; ASCII-строка
num: .quad 10 ; 64-битная константа
section .text
global _start
_start:
; Использование непосредственного значения
MOV X0, #5 ; X0 = 5
; Использование адресной константы
LDR X1, =num ; X1 = адрес num
; Загрузка значения из памяти
LDR X2, [X1] ; X2 = значение по адресу num = 10
; Использование ASCII-строки
LDR X3, =my_str ; X3 = адрес my_str
RET
Константы в ассемблере являются важным элементом программирования, позволяющим использовать фиксированные значения и адреса в программе. Понимание того, как правильно использовать и представлять константы, необходимо для написания эффективных и корректных программ на языке ассемблера.
Регистры
Регистром называют небольшую, очень быструю ячейку памяти, встроенную в процессор. Он хранит данные, которые процессор использует непосредственно при выполнении инструкций (операнды, адреса, флаги и т.п.).
ARM64 – 64‑битная архитектура, в которой основной набор регистров называется General‑Purpose Registers (GPR). Они обозначаются X0‑X30 (64‑битные) и их 32‑битные части – W0‑W30. Кроме них существуют специальные регистры, используемые системой, стеком и управлением процессором.
X0 (64 бита)
63 … 32 (старшие)
31 … 0 (младшие)
LSR x1, x0, #32
W0 (32 bits)
X0 – 64‑битный общий регистр процессора ARM64 (AArch64). Таким образом, X0 представляет собой 64‑битный контейнер, где младшие 32 бита образуют отдельный регистр W0, а старшие 32 бита доступны только в инструкции LSR x1, x0, #32.



