Svartsinn1
Краткий гайд по ассемблеру

Самиздат: [Регистрация] [Найти] [Рейтинги] [Обсуждения] [Новинки] [Обзоры] [Помощь|Техвопросы]
Links
Кожевенное мастерство: сумки, ремни своими руками Юристы. Круглосуточно
 Ваша оценка:

Современный курс по ассемблеру ARM64 для macOS

Ядро XNU, драйверы устройств, виртуальная машина JavaScriptCore, криптографические функции Security framework, рантайм Swift, компилятор LLVM. Что общего у этих компонентов macOS? Все они работают на процессорах ARM64, которые Apple ставит в свои компьютеры с 2020 года. Ассемблер не умер. Он перестал быть массовым навыком, но остался фундаментом, на котором стоит любая программа, любой компилятор и любая операционная система.

Если вы знаете C и хотите понять, во что превращается ваш код после компиляции, этот курс для вас. C прячет от вас регистры процессора, конвенции вызова, выравнивание стека и машинные инструкции. Ассемблер раскрывает их. Прелесть ARM64 в том, что это архитектура с чистым дизайном: 31 универсальный регистр, фиксированная длина инструкции 4 байта, предсказуемое кодирование. Поняв ARM64, вы начинаете видеть, как процессор выполняет программу, как компилятор размещает переменные в регистрах и почему высокоуровневые абстракции стоят именно столько машинных тактов, сколько стоят.

C даёт вам управление памятью. Ассемблер даёт управление процессором. Каждая инструкция в ARM64 соответствует одной операции, которую выполняет железо. Нет скрытого вызова функций, нет неявного выделения памяти, нет магии компилятора. Вы видите, как значение перемещается из регистра в регистр, как условный переход меняет поток выполнения, как стек растёт вниз при вызове функции. Это уровень абстракции, ниже которого лежит только сам кремний.

Apple Silicon - это не просто ещё один ARM-чип. Это платформа со своими правилами: Mach-O вместо ELF, BSD-подобные системные вызовы вместо Linux, регистр X16 для номеров системных вызовов вместо X8, префикс подчёркивания для имён функций из C. Изучение ассемблера на macOS - это одновременно изучение ARM64 и изучение платформы Darwin. Этот курс охватывает обе стороны: архитектуру процессора и специфику операционной системы.

Курс разбит на 25 тем. Каждая тема - один принцип: краткая теория, практическое задание с рабочим кодом, домашнее задание для самостоятельной работы. Пройдя их, вы не просто выучите синтаксис ассемблера. Вы поймёте, как процессор работает с памятью, как операционная система принимает системные вызовы и почему компилятор генерирует именно такие инструкции, какие генерирует.

Зачем учить ассемблер, если есть C

C транслируется в ассемблер. Каждая строка C превращается в несколько инструкций процессора. Компилятор решает, какие регистры использовать, как разместить локальные переменные, как вызывать функции. Вы пишете int x = 42;, а компилятор решает, положить ли 42 в регистр W0 или в стек. Вы пишете return a + b;, а компилятор генерирует ADD W0, W1, W2 и RET. Знание ассемблера превращает вас из человека, который доверяет компилятору, в человека, который понимает, что компилятор делает и почему.

Ассемблер остаётся языком выбора для задач, где важны три вещи: точный контроль над производительностью, понимание бинарного кода и работа с железом на самом низком уровне. В мире оптимизации горячих циклов, реверс-инжиниринга бинарных файлов, разработки эксплойтов и аудита безопасности альтернатив ассемблеру практически нет. Даже при работе на C часто приходится читать сгенерированный ассемблерный код, чтобы понять, почему программа тормозит или падает.

Есть и образовательный аргумент. ARM64 - это архитектура, в которой каждая инструкция делает ровно то, что написано. Нет микрокода, нет скрытых состояний, нет магии. Это делает ARM64 идеальным инструментом для изучения того, как процессор работает. Вы видите, где данные лежат, как они перемещаются между регистрами, почему конвейер процессора может дать просадку на ветвлении и как условные инструкции помогают этого избежать.

Apple Silicon и архитектура ARM64

Apple Silicon - это семейство процессоров M1, M2, M3, M4, которые Apple ставит в свои компьютеры с 2020 года. Все они построены на архитектуре ARM64, она же AArch64. Это 64-битная версия архитектуры ARM, которая используется в большинстве смартфонов, планшетов и встраиваемых систем мира. Apple Silicon - не единственный ARM64-процессор на рынке, но он отличается высокой производительностью, эффективным энергопотреблением и тесной интеграцией с macOS.

Архитектура ARM64 характеризуется несколькими ключевыми свойствами. 31 универсальный регистр общего назначения (X0-X30), каждый 64 бита. Фиксированная длина инструкции - 4 байта. Load-store архитектура: арифметические операции работают только с регистрами, доступ к памяти - через отдельные инструкции LDR и STR. Конвенция вызова AAPCS64 определяет, какие регистры используются для аргументов, возвращаемых значений и сохранения контекста. Все это делает ARM64 более предсказуемым и читаемым, чем x86-64 с его десятилетиями обратной совместимости.

Apple добавила к стандартной архитектуре ARM64 несколько расширений. arm64e - версия с Pointer Authentication Codes (PAC), которая защищает от атак на управление потоком выполнения. NEON - SIMD-расширение для параллельной обработки данных. AMX - матричное расширение, доступное в macOS 14 и новее. Эти расширения не обязательны для изучения, но знание об их существовании помогает понимать, почему тот или иной код генерируется именно так.

Между Linux ARM64 и macOS ARM64 есть существенные различия. Linux использует формат ELF, macOS - Mach-O. Linux хранит номера системных вызовов в X8, macOS - в X16. Linux вызывает ядро через SVC #0, macOS - через SVC #0x80. Названия секций различаются: .text в ELF, __TEXT,__text в Mach-O. Эти отличия не меняют саму архитектуру процессора, но влияют на то, как пишется ассемблерный код для каждой платформы.

Развёртывание окружения на macOS

macOS поставляется со встроенным ассемблером, который является частью LLVM. Команда as в терминале вызывает Clang-интегрированный ассемблер, а не GNU assembler. Это важно: синтаксис LLVM-ассемблера немного отличается от GNU assembler, и код, написанный для Linux, может потребовать адаптации. Вам не нужно устанавливать отдельный ассемблер - всё уже есть в системе.

Для начала установите средства командной строки Xcode. Откройте терминал и выполните:

xcode-select --install

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

as --version

Вы увидите версию LLVM, обычно 15.x или 16.x на современных macOS. Для ARM64 не нужно указывать архитектуру вручную на Apple Silicon - система сама использует arm64 по умолчанию. На Intel Mac можно кросс-компилировать через флаг -arch arm64, но для полноценной работы нужен Apple Silicon Mac.

Для сборки программ также понадобится линкер ld, который входит в средства командной строки. Проверьте его наличие:

ld -v

Альтернативный путь - использовать clang напрямую. Clang может ассемблировать и линковать за один шаг, что упрощает сборку небольших программ. Мы будем использовать оба подхода: as + ld для понимания процесса и clang для быстрой сборки.

Visual Studio Code: настройка для ассемблера

Visual Studio Code - бесплатный редактор, который можно настроить для работы с ассемблером. Установите VS Code с официального сайта, затем откройте панель расширений (Cmd+Shift+X) и найдите "ARM Assembly" от Xiao Cheng. Это расширение добавляет подсветку синтаксиса для ARM64. Также полезно расширение "Hex Editor" для просмотра бинарных файлов.

Создайте папку для проекта, откройте её в VS Code и создайте файл hello.s. Расширение .s (или .S для препроцессора) - стандартное расширение для ассемблерных файлов на macOS. Введите простой код:

.section __TEXT,__text
.global _main
_main:
MOV X0, #0
RET

Для компиляции и запуска в VS Code создайте задачу сборки. Нажмите Cmd+Shift+P, введите "Tasks: Configure Default Build Task" и выберите "Others". VS Code создаст файл .vscode/tasks.json. Отредактируйте его:

{
  "version": "2.0.0",
  "tasks": [{
    "label": "ARM64: assemble and link",
    "type": "shell",
    "command": "clang",
    "args": [
      "-arch", "arm64",
      "-o", "${fileDirname}/${fileBasenameNoExtension}",
      "${file}"
    ],
    "group": { "kind": "build", "isDefault": true }
  }]
}

Теперь по Cmd+Shift+B файл будет ассемблироваться и линковаться в один шаг. Для запуска используйте терминал: ./hello. Для отладки создайте launch.json через меню Run - Add Configuration - LLDB.

Современные средства отладки

На macOS стандартный отладчик - это LLDB, часть LLVM. Он заменяет GDB, который традиционно используется на Linux. LLDB интегрирован в VS Code через расширение CodeLLDB, но его можно использовать и из терминала. Основные команды LLDB для ARM64:

lldb ./hello # запустить отладчик
breakpoint set --name _main # поставить точку останова
run # запустить программу
stepi # выполнить одну инструкцию
register read # показать все регистры
register read x0 x1 x16 # показать конкретные регистры
memory read $x1 # прочитать память по адресу
disassemble --name _main # дизассемблировать функцию
continue # продолжить выполнение

LLDB на macOS поддерживает все возможности для работы с ARM64: просмотр регистров, дизассемблирование, точки останова на инструкциях, пошаговое выполнение. Для просмотра бинарных файлов используется otool - нативный инструмент macOS. Команда otool -tv program дизассемблирует исполняемый файл, otool -h program показывает заголовок Mach-O, otool -L program показывает динамические библиотеки.

Ещё один полезный инструмент - objdump из LLVM. Он работает с Mach-O через флаг --macho: objdump --macho -d program. Для просмотра символов используется nm program. Эти инструменты помогают понять, как устроен бинарный файл, какие секции в нём есть и какие функции экспортируются.

Apple ABI: отличия от стандартного ARM64

Apple следует стандарту AAPCS64 (ARM Architecture Procedure Call Standard for 64-bit Architecture), но добавляет несколько платформенных правил. Регистр X18 зарезервирован Apple - не используйте его в своём коде. Регистр FP (X29) должен всегда указывать на валидный frame record - это требование для работы отладчика и разворота стека. Регистр LR (X30) хранит адрес возврата и автоматически сохраняется при вызове функции через BL.

Конвенция вызова на macOS ARM64: первые 8 аргументов функции передаются в регистрах X0-X7 (или W0-W7 для 32-битных значений). Возвращаемое значение - в X0. Если аргументов больше восьми, остальные передаются через стек. Регистры X0-X7 не сохраняются при вызове - функция callee может их перезаписать. Регистры X19-X28 сохраняются - функция должна сохранить их перед использованием и восстановить перед возвратом.

Для variadic-функций (printf, scanf) Apple отходит от стандарта. На Linux ARM64 variadic-аргументы передаются в регистрах, на macOS - через стек. Это означает, что вызов printf из ассемблера на macOS требует укладки аргументов в стек, а не в регистры. Это важное отличие, которое легко пропустить при переносе кода с Linux.

Структура курса

Курс состоит из 25 тем. Каждая тема построена по схеме: краткая теория (синтаксис, механика, отличия от C), практика (рабочий код, который нужно набрать и запустить) и домашнее задание. Темы идут от простых к сложным. Первые пять - это базовый синтаксис и регистры. Дальше начинается специфика ARM64: адресация памяти, стек, вызов функций. Финальные темы посвящены оптимизации, SIMD и взаимодействию с C.

Рекомендуется проходить темы последовательно. Каждая последующая тема опирается на предыдущую. Код всех примеров собирается командой clang -arch arm64 file.s -o program. Если что-то не собирается - проверьте версию ассемблера: as --version. Для полной поддержки всех возможностей нужен LLVM 14 или новее.

АспектCARM64 Assembly
Уровеньвысокоуровневый (по сравнению с ассемблером)машинный код один-к-одному
Регистрыскрыты, управляются компиляторомосновной ресурс, ручное управление
Памятьчерез указатели, malloc/freeLDR/STR, адреса в регистрах
Функцииимя, аргументы, returnметка, BL/RET, X0 для результата
Управление потокомif/else/while/forB/BL/B.cond, условные инструкции
Скоростьзависит от оптимизаций компиляторамаксимальная, ручная оптимизация
Читаемостьвысокая, абстракциинизкая, детальное управление
Портабельностьвысокая (перекомпиляция)зависит от архитектуры процессора

Тема 1. Архитектура ARM64 и Apple Silicon

Теория

ARM64 - это 64-битная архитектура процессора, разработанная компанией ARM Holdings. Она используется в большинстве мобильных устройств, встраиваемых системах и, с 2020 года, в компьютерах Apple. Архитектура ARM64 относится к типу load-store: арифметические операции выполняются только над регистрами, а доступ к памяти осуществляется через отдельные инструкции загрузки и сохранения. Это отличается от CISC-архитектур вроде x86-64, где одна инструкция может одновременно читать память, выполнять операцию и записывать результат обратно в память.

Apple Silicon - это реализация ARM64 от Apple. Процессоры M1, M2, M3, M4 построены на архитектуре ARMv8-A и ARMv9-A. Они поддерживают все стандартные инструкции ARM64 плюс дополнительные расширения: NEON для SIMD-операций, Pointer Authentication для безопасности, AMX для матричных вычислений. Для программиста на ассемблере Apple Silicon - это стандартный ARM64 процессор с некоторыми платформенными особенностями, которые диктует macOS.

Фиксированная длина инструкции - 4 байта - ключевая особенность ARM64. Каждая инструкция кодируется ровно 32 битами. Это упрощает декодирование и делает дизассемблирование однозначным. В x86-64 инструкции имеют переменную длину от 1 до 15 байтов, что усложняет разбор. В ARM64 вы всегда знаете, где начинается следующая инструкция - через 4 байта.

Практика

// Проверка архитектуры вашего Mac
// Выполните в терминале:
//   uname -m
// Ожидаемый вывод на Apple Silicon:
//   arm64

// Простая программа, которая возвращает 42
// Файл: arch.s

.section __TEXT,__text
.global _main
.align 2

_main:
    MOV W0, #42     // W0 = 42 (32-битный регистр)
    RET             // вернуться в вызывающий код

Сборка: clang -arch arm64 arch.s -o arch. Запуск: ./arch; echo $?. Вывод: 42. Команда echo $? показывает код возврата последней программы, который помещается в W0.

Домашнее задание

Напишите программу, которая возвращает код 0. Затем измените её, чтобы возвращать 7. Проверьте через echo $?. Откройте бинарный файл в otool -tv ./arch и найдите вашу инструкцию MOV W0, #42.

Тема 2. Регистры ARM64: X и W

Теория

ARM64 имеет 31 регистр общего назначения: X0-X30. Каждый из них 64 бита. Регистр X31 в инструкциях обозначает SP (стек-указатель) или ноль, в зависимости от контекста. Первые 8 регистров (X0-X7) используются для передачи аргументов в функции. X0 также хранит возвращаемое значение. X29 - это FP (frame pointer), X30 - LR (link register), X31 - SP.

Каждый 64-битный регистр X имеет 32-битный аналог W. Регистр W0 - это младшие 32 бита регистра X0. Использование W вместо X означает, что операция выполняется над 32 битами, а старшие 32 бита регистра обнуляются. Это важно при работе с int в C, который на ARM64 обычно 4 байта. Если вы используете X-регистр для 32-битного значения, старшие 32 бита могут содержать мусор.

Регистры делятся на две группы по конвенции вызова. X0-X18 - временные регистры (scratch), функция-калышей может их перезаписать без сохранения. X19-X28 - сохраняемые регистры (callee-saved), функция должна сохранить их перед использованием и восстановить перед возвратом. На macOS X18 зарезервирован Apple - не используйте его.

Практика

// Регистры X и W: разница между 64-битным и 32-битным доступом
// Файл: registers.s

.section __TEXT,__text
.global _main
.align 2

_main:
    MOV X0, #0          // 64-битный: X0 = 0
    MOVK X0, #0x1234, LSL #16  // сдвиг и установка: X0 = 0x12340000
    MOVK X0, #0x5678, LSL #32  // X0 = 0x5678_1234_0000

    MOV W1, #0xAB       // 32-битный: W1 = 0xAB, X1 = 0x000000AB
    MOV W2, #0xCD       // W2 = 0xCD

    ADD W3, W1, W2      // W3 = W1 + W2 = 0x178 (с переполнением)

    MOV W0, #0          // код возврата 0
    RET

Сборка и запуск аналогичны. Откройте в LLDB: lldb ./registers. Поставьте точку останова на _main, запустите, выполняйте по инструкции через stepi, после каждой инструкции проверяйте регистры через register read x0 x1 x2 x3. Вы увидите, как MOV W1, #0xAB обнуляет старшие 32 бита X1, а MOV X0, #0 работает с полными 64 битами.

Домашнее задание

Загрузите в X0 число 0xDEADBEEF через три инструкции MOV/MOVK. Затем загрузите его же в W0 и объясните, почему значение отличается. Используйте LLDB для проверки значений регистров.

Тема 3. Первая программа: Hello World

Теория

Программа Hello World на ассемблере macOS ARM64 требует понимания системных вызовов. Системный вызов - это способ запросить ядро операционной системы выполнить действие: записать в файл, прочитать файл, выделить память, завершить процесс. На macOS системные вызовы основаны на BSD-конвенции, которая отличается от Linux.

На macOS ARM64 номер системного вызова помещается в регистр X16 (не X8, как на Linux). Аргументы - в X0-X5. Системный вызов инициируется инструкцией SVC #0x80 (не SVC #0, как на Linux). Номера системных вызовов macOS: write = 4, read = 3, exit = 1, open = 5, close = 6. Это BSD-номера, а не Linux generic.

Символы в Mach-O имеют свои особенности. Имя точки входа по умолчанию - _main (с подчёркиванием), если вы линкуете с libSystem. Если вы хотите использовать _start без стандартной библиотеки, нужно указать -e _start при линковке. Имена функций из C-библиотек имеют префикс подчёркивания: printf в C становится _printf в ассемблере.

Практика

// Hello World через системный вызов write
// Файл: hello.s

.section __TEXT,__text
.global _main
.align 2

_main:
    // Сохраняем frame pointer и link register
    STP X29, X30, [SP, #-16]!
    MOV X29, SP

    // write(1, msg, 14)
    MOV X16, #4              // syscall: write
    MOV X0, #1               // fd = stdout
    ADR X1, msg              // адрес строки
    MOV X2, #14              // длина
    SVC #0x80                // вызов ядра

    // exit(0)
    MOV X16, #1              // syscall: exit
    MOV X0, #0               // статус 0
    SVC #0x80

    // Восстанавливаем регистры (для порядка, хотя exit не вернётся)
    LDP X29, X30, [SP], #16
    RET

.section __TEXT,__const
.align 2
msg:
    .ascii "Hello, World!\n"

Сборка: clang -arch arm64 hello.s -o hello. Запуск: ./hello. Вывод: Hello, World!. Инструкция ADR X1, msg загружает адрес метки msg в регистр X1. STP и LDP сохраняют и восстанавливают пару регистров на стеке - это стандартный пролог и эпилог функции.

Домашнее задание

Измените программу, чтобы она выводила две строки: "Hello, " и "World!\n" - через два отдельных вызова write. Подсчитайте общее количество байтов, записанных в stdout, и выведите это число в код возврата.

Тема 4. Сборка: as, ld, clang

Теория

Процесс сборки ассемблерной программы на macOS состоит из двух этапов: ассемблирование (превращение текста в объектный файл Mach-O) и линковка (объединение объектных файлов в исполняемый файл). Команда as ассемблирует, ld линкует. Команда clang может выполнить оба шага за один вызов.

Полный процесс выглядит так. Сначала as -arch arm64 -o hello.o hello.s создаёт объектный файл. Затем ld -arch arm64 -o hello hello.o -l System -syslibroot $(xcrun -sdk macosx --show-sdk-path) -e _main линкует с системной библиотекой. Флаг -l System подключает libSystem.dylib, который предоставляет стандартные функции. Флаг -syslibroot указывает путь к SDK macOS, где лежат системные библиотеки.

В повседневной работе проще использовать clang, который автоматически вызывает as и ld с правильными параметрами. Но понимание раздельных шагов важно для отладки и для работы с многофайловыми проектами. Если линковка не удаётся, часто помогает выполнить шаги раздельно и посмотреть, на каком этапе возникает проблема.

Практика

# Сборка через раздельные шаги

# Шаг 1: ассемблирование
as -arch arm64 -o hello.o hello.s

# Шаг 2: линковка с libSystem
ld -arch arm64 -o hello hello.o \
   -l System \
   -syslibroot $(xcrun -sdk macosx --show-sdk-path) \
   -e _main

# Шаг 3: запуск
./hello

# Альтернатива: всё в один шаг через clang
clang -arch arm64 hello.s -o hello

# Проверка типа файла
file hello
# Вывод: Mach-O 64-bit executable arm64

# Просмотр заголовка Mach-O
otool -h hello

# Дизассемблирование
otool -tv hello

Команда file подтверждает, что создан Mach-O исполняемый файл для arm64. otool -h показывает заголовок с magic-числом 0xFEEDFACF для 64-битного Mach-O. otool -tv дизассемблирует код и показывает ваши инструкции.

Домашнее задание

Соберите программу из темы 3 через раздельные шаги as и ld. Затем соберите через clang. Сравните размер получившихся бинарных файлов. Объясните, почему размер может отличаться.

Тема 5. Mach-O: формат исполняемых файлов macOS

Теория

Mach-O - это формат исполняемых файлов, используемый в macOS, iOS и других операционных системах Apple. Он заменяет ELF, который используется в Linux. Mach-O имеет другую структуру: заголовок Mach header, затем load commands (команды загрузки), затем сегменты с секциями. Каждый сегмент может содержать несколько секций.

Стандартные секции Mach-O для ARM64: __TEXT,__text - исполняемый код, __TEXT,__const - константы только для чтения, __DATA,__data - инициализированные данные, __DATA,__bss - неинициализированные данные. В ELF это .text, .rodata, .data, .bss. При написании ассемблерного кода для macOS используйте синтаксис Mach-O секций.

Apple Silicon использует размер страницы 16 КБ, а не 4 КБ, как в большинстве ARM64-систем. Это влияет на выравнивание сегментов в Mach-O и на работу с виртуальной памятью. Для ассемблерных программ это обычно не критично, но может повлиять на директиву .align. Используйте .align 2 для выравнивания инструкций на 4 байта (2 в степени 2 = 4).

Практика

// Структура Mach-O: секции и сегменты
// Файл: macho.s

.section __TEXT,__text       // исполняемый код
.global _main
.align 2

_main:
    ADRP X0, const_data@PAGE
    ADD  X0, X0, const_data@PAGEOFF
    LDR  W1, [X0]             // загрузить 32-битное значение
    MOV  W0, W1               // вернуть его
    RET

.section __TEXT,__const      // константы
.align 2
const_data:
    .word 0xCAFEBABE          // 32-битная константа

.section __DATA,__data        // изменяемые данные
.align 3
mutable_data:
    .quad 0                   // 64-битное значение, инициализировано нулём

После сборки изучите структуру: otool -l macho | head -80 покажет load commands и секции. Вы увидите сегменты __TEXT и __DATA с их секциями. Команда nm macho покажет символы _main, const_data, mutable_data и их адреса.

Домашнее задание

Создайте программу с тремя секциями: __TEXT,__text (код), __TEXT,__const (константа), __DATA,__data (изменяемая переменная). Код должен загрузить константу, прибавить к изменяемой переменной и вернуть результат. Проверьте через otool -l, что все три секции присутствуют в бинарном файле.

Тема 6. Системные вызовы macOS и Darwin

Теория

Системный вызов - это интерфейс между пользовательской программой и ядром операционной системы. На macOS ядро XNU (основанное на Mach и BSD) предоставляет системные вызовы в стиле BSD. Это означает, что номера и семантика вызовов отличаются от Linux, хотя многие имена совпадают. Для программиста на ассемблере это означает, что код, написанный для Linux ARM64, не будет работать на macOS без адаптации.

Конвенция системных вызовов на macOS ARM64: номер вызова в X16, аргументы в X0-X5, вызов через SVC #0x80. Результат возвращается в X0. При ошибке устанавливается флаг переноса (carry flag), и X0 содержит положительный код ошибки. Это отличается от Linux, где при ошибке X0 содержит отрицательный код.

Основные системные вызовы macOS: exit (1), read (3), write (4), open (5), close (6), getpid (20). Полный список находится в файле syscalls.master в исходном коде XNU, который доступен на GitHub. Номера считаются приватными и могут меняться, но на практике остаются стабильными между версиями macOS.

Практика

// Системные вызовы: write и exit
// Файл: syscalls.s

.section __TEXT,__text
.global _main
.align 2

_main:
    STP X29, X30, [SP, #-16]!
    MOV X29, SP

    // write(1, msg, 5)
    MOV X16, #4           // syscall write
    MOV X0, #1            // stdout
    ADR X1, msg
    MOV X2, #5            // длина
    SVC #0x80

    // exit(0)
    MOV X16, #1           // syscall exit
    MOV X0, #0
    SVC #0x80

    LDP X29, X30, [SP], #16
    RET

.section __TEXT,__const
msg:
    .ascii "Hello\n"

Программа вызывает write для вывода строки в stdout, затем exit для завершения. Обратите внимание: после SVC #0x80 можно проверить флаг переноса через B.CC для обработки ошибок. В данном примере обработка опущена для простоты.

Домашнее задание

Напишите программу, которая открывает файл (через open, syscall 5), читает из него 100 байтов (через read, syscall 3), выводит их в stdout (через write, syscall 4) и закрывает файл (через close, syscall 6). Обрабатывайте ошибки: если open вернул ошибку, выводите сообщение и завершайтесь с кодом 1.

Тема 7. Адресация памяти: ADRP и ADD

Теория

На macOS ARM64 для загрузки адресов используется пара инструкций ADRP и ADD. ADRP загружает адрес страницы (4 КБ или 16 КБ на Apple Silicon), на которой находится символ. ADD добавляет смещение внутри страницы. Вместе они позволяют адресовать любой символ в диапазоне Ђ4 ГБ от текущего счётчика команд. Это отличается от ADR, который имеет диапазон всего Ђ1 МБ.

Макросы @PAGE и @PAGEOFF - это расширения LLVM-ассемблера для macOS. @PAGE возвращает старшие биты адреса (номер страницы), @PAGEOFF - младшие биты (смещение внутри страницы). На Linux GNU assembler использует другой синтаксис: :lo12: для смещения. При переносе кода между платформами нужно учитывать это различие.

Для адресации кода внутри одной функции часто достаточно ADR, так как диапазон Ђ1 МБ покрывает большинство функций. Для доступа к глобальным данным в секциях __TEXT,__const или __DATA,__data используется ADRP + ADD, потому что данные могут находиться далеко от кода в бинарном файле.

Практика

// ADRP + ADD: загрузка адреса глобальной переменной
// Файл: addressing.s

.section __DATA,__data
.align 3
counter:
    .quad 41             // 64-битная переменная, инициализирована 41

.section __TEXT,__text
.global _main
.align 2

_main:
    STP X29, X30, [SP, #-16]!
    MOV X29, SP

    // Загружаем адрес counter
    ADRP X0, counter@PAGE      // X0 = страница counter
    ADD  X0, X0, counter@PAGEOFF // X0 = точный адрес counter

    // Читаем значение
    LDR  X1, [X0]              // X1 = *counter = 41

    // Инкрементируем
    ADD  X1, X1, #1           // X1 = 42

    // Записываем обратно
    STR  X1, [X0]             // *counter = 42

    // Возвращаем значение
    MOV  X0, X1

    LDP X29, X30, [SP], #16
    RET

Программа загружает адрес глобальной переменной counter, читает её значение, увеличивает на 1 и записывает обратно. LDR X1, [X0] загружает 64-битное значение из памяти по адресу в X0 в регистр X1. STR X1, [X0] сохраняет значение из X1 по тому же адресу.

Домашнее задание

Создайте массив из 5 целых чисел в секции __DATA,__data. Напишите программу, которая загружает адрес массива через ADRP + ADD, обходит все элементы, суммирует их и возвращает сумму. Используйте LDR для чтения каждого элемента и арифметику указателей для перехода к следующему.

Тема 8. Арифметические инструкции

Теория

ARM64 предоставляет полный набор арифметических инструкций: ADD (сложение), SUB (вычитание), MUL (умножение), SDIV (деление со знаком), UDIV (деление без знака). Все они работают с регистрами. Операнды могут быть регистр и константа (immediate), или два регистра. Деление на ноль не вызывает исключения - результат равен нулю.

Инструкция ADD может устанавливать флаги условий, если использовать суффикс S: ADDS. Флаги хранятся в регистре состояния процессора (CPSR) и используются условными ветвлениями. Основные флаги: N (negative), Z (zero), C (carry), V (overflow). После ADDS W0, W1, W2 флаг Z устанавливается, если результат равен нулю, флаг N - если результат отрицательный.

Инструкция MUL умножает два 64-битных регистра и возвращает младшие 64 бита результата. Для полного 128-битного произведения используются SMULH (со знаком) и UMULH (без знака). Инструкция MADD умножает и складывает: MADD X0, X1, X2, X3 выполняет X0 = X1 * X2 + X3, что полезно для вычисления адресов и полиномиальных вычислений.

Практика

// Арифметика: сложение, вычитание, умножение, деление
// Файл: arithmetic.s

.section __TEXT,__text
.global _main
.align 2

_main:
    MOV W0, #10          // W0 = 10
    MOV W1, #3           // W1 = 3

    ADD W2, W0, W1       // W2 = 10 + 3 = 13
    SUB W3, W0, W1       // W3 = 10 - 3 = 7
    MUL W4, W0, W1       // W4 = 10 * 3 = 30
    SDIV W5, W0, W1      // W5 = 10 / 3 = 3

    // MADD: умножение со сложением
    MOV W6, #5
    MADD W7, W0, W1, W6  // W7 = 10 * 3 + 5 = 35

    // ADDS с установкой флагов
    MOV W8, #0
    MOV W9, #0
    ADDS W8, W8, #0      // устанавливает флаг Z (результат 0)

    // Возвращаем 0
    MOV W0, #0
    RET

Соберите и откройте в LLDB. Выполняйте по инструкции через stepi, после каждой проверяйте регистры: register read w0 w1 w2 w3 w4 w5 w6 w7 w8. Вы увидите, как каждая инструкция меняет ровно один регистр. После ADDS W8, W8, #0 проверьте флаги: register read cpsr - бит Z должен быть установлен.

Домашнее задание

Вычислите факториал числа 5 в регистрах, используя только MUL и MOV. Результат должен оказаться в W0. Проверьте, что 5! = 120. Затем вычислите факториал числа 10 и объясните, почему результат не помещается в 32-битный регистр W0.

Тема 9. Управление потоком: ветвления и условия

Теория

Управление потоком выполнения в ARM64 осуществляется через условные и безусловные ветвления. Безусловная ветвь B label переходит на метку без сохранения адреса возврата. Ветвь с сохранением BL label сохраняет адрес следующей инструкции в X30 (LR) и переходит - это вызов функции. Возврат из функции - RET, который переходит по адресу в X30.

Условные ветвления используют флаги условий. После инструкции CMP W0, W1 устанавливаются флаги, и условная ветвь B.EQ label переходит, если значения равны. Основные условия: EQ (равно), NE (не равно), GT (больше), LT (меньше), GE (больше или равно), LE (меньше или равно). Для беззнакового сравнения используются HS (выше или равно) и LO (ниже).

ARM64 также поддерживает условные инструкции с суффиксом: ADD W0, W1, W2, EQ выполнится, только если флаг EQ установлен. Это позволяет писать безветвлевой код, который часто быстрее, потому что избегает промахов предсказателя ветвлений. Но условные инструкции занимают ресурсы и могут быть медленнее, если условие почти всегда истинно или ложно.

Практика

// Ветвления: if/else через CMP и B.cond
// Файл: branches.s

.section __TEXT,__text
.global _main
.align 2

_main:
    MOV W0, #7           // число для проверки
    MOV W1, #5           // порог

    CMP W0, W1           // сравниваем W0 и W1
    B.GT greater         // если W0 > W1, перейти

    // W0 <= W1
    MOV W2, #0           // результат 0
    B done

greater:
    MOV W2, #1           // результат 1

done:
    // Возвращаем W2
    MOV W0, W2
    RET

Программа сравнивает два числа и возвращает 1, если первое больше, и 0 в противном случае. Инструкция CMP W0, W1 вычисляет W0 - W1 и устанавливает флаги, не сохраняя результат. Ветвь B.GT проверяет флаги и переходит, если W0 больше W1 (с учётом знака).

Домашнее задание

Напишите функцию, которая определяет, является ли число чётным или нечётным, используя TST W0, #1 (проверка младшего бита) и условную ветвь. Верните 0 для чётного и 1 для нечётного. Проверьте на числах 4 и 7.

Тема 10. Условные инструкции и безветвлевой код

Теория

Условные инструкции - отличительная черта ARM. Инструкция может выполняться или нет, в зависимости от флагов условий. Например, MOV W0, W1, EQ копирует W1 в W0 только если флаг EQ установлен. Это позволяет реализовывать простые условные операции без ветвлений, что улучшает производительность на процессорах с длинным конвейером.

Безветвлевой код особенно эффективен для коротких операций выбора. Например, нахождение максимума двух чисел через условное движение: CMP W0, W1; CSEL W2, W0, W1, GT. Инструкция CSEL (conditional select) выбирает один из двух регистров в зависимости от условия. Если W0 > W1, W2 = W0, иначе W2 = W1. Это одна инструкция вместо ветвления, и она не вызывает промах предсказателя.

Не все операции выгодно делать безветвлевыми. Если ветвление хорошо предсказывается (например, почти всегда выбирается один путь), обычная ветвь будет быстрее. Условные инструкции занимают исполнительные ресурсы процессора даже когда условие ложно. Баланс между ветвлением и безветвлевым кодом - одна из тонкостей оптимизации на ARM64.

Практика

// CSEL: выбор максимума без ветвления
// Файл: csel.s

.section __TEXT,__text
.global _main
.align 2

_main:
    MOV W0, #42          // первое число
    MOV W1, #17           // второе число

    CMP W0, W1           // сравниваем
    CSEL W2, W0, W1, GT  // W2 = (W0 > W1) ? W0 : W1

    // CSEL для минимума
    CSEL W3, W0, W1, LT  // W3 = (W0 < W1) ? W0 : W1

    // Абсолютное значение через CSNEG
    MOV W4, #-5
    CMP W4, #0
    CSNEG W4, W4, W4, LT // если W4 < 0, W4 = -W4

    MOV W0, W2           // возвращаем максимум
    RET

Инструкция CSNEG (conditional select negate) выбирает между регистром и его отрицанием в зависимости от условия. Это компактный способ вычисления абсолютного значения без ветвления. Все три операции - максимум, минимум, модуль - выполняются без единой ветви.

Домашнее задание

Напишите функцию clamp, которая ограничивает значение в диапазоне [min, max] с использованием только условных инструкций (CSEL, CSMIN, CSMAX если доступны, или комбинации CMP + CSEL). Проверьте на значениях: 5 с диапазоном [10, 20] должно вернуть 10; 25 с диапазоном [10, 20] должно вернуть 20; 15 с диапазоном [10, 20] должно вернуть 15.

Тема 11. Стек и вызов функций

Теория

Стек в ARM64 растёт вниз: от больших адресов к меньшим. Регистр SP (stack pointer) указывает на вершину стека. При вызове функции адрес возврата сохраняется в регистре LR (X30). Функция, которая вызывает другие функции, должна сохранить LR на стеке, потому что следующий BL перезапишет его. Это базовое правило, нарушение которого приводит к зависанию или падению программы.

Apple требует, чтобы стек был выровнен на 16 байт перед любым вызовом функции. Это значит, что SP должен быть кратен 16, когда выполняется BL. Несоблюдение этого правила вызывает исключение выравнивания. Для выравнивания используйте STP и LDP с шагом 16, которые автоматически работают с парами регистров.

Стандартный пролог функции на macOS ARM64: STP X29, X30, [SP, #-16]! (сохранить FP и LR, уменьшить SP на 16), MOV X29, SP (установить FP на новую вершину стека). Эпилог: LDP X29, X30, [SP], #16 (восстановить FP и LR, увеличить SP на 16), RET. Этот паттерн встречается в каждой нетривиальной функции.

Практика

// Стек и вызов функции
// Файл: stack.s

.section __TEXT,__text
.global _main
.align 2

// Функция add: W0 = W0 + W1
add_numbers:
    ADD W0, W0, W1     // результат в W0
    RET

_main:
    // Пролог
    STP X29, X30, [SP, #-16]!
    MOV X29, SP

    // Подготовка аргументов
    MOV W0, #15
    MOV W1, #27

    // Вызов функции
    BL add_numbers

    // W0 теперь содержит 42

    // Эпилог
    LDP X29, X30, [SP], #16
    RET

Инструкция BL add_numbers сохраняет адрес следующей инструкции в X30 и переходит на add_numbers. Функция add_numbers выполняет сложение и возвращает через RET, который переходит по адресу в X30. Главная функция сохраняет X30 на стеке, потому что без этого BL перезаписал бы адрес возврата в систему.

Домашнее задание

Напишите программу с двумя функциями: square (возводит число в квадрат) и sum_of_squares (сумма квадратов двух чисел, вызывает square дважды). _main должен вызывать sum_of_squares с аргументами 3 и 4 и возвращать результат (9 + 16 = 25). Все функции должны сохранять и восстанавливать FP и LR.

Тема 12. Конвенция вызова AAPCS64 на macOS

Теория

Конвенция вызова AAPCS64 (ARM Architecture Procedure Call Standard for 64-bit) определяет, как функции передают аргументы и возвращают значения. На macOS Apple следует AAPCS64 с небольшими платформенными дополнениями. Первые 8 аргументов передаются в регистрах X0-X7. Возвращаемое значение - в X0. Если аргументов больше 8, остальные передаются через стек.

Регистры X0-X7 - временные (caller-saved). Вызывающая функция должна сохранить их, если они нужны после вызова. Регистры X19-X28 - сохраняемые (callee-saved). Вызываемая функция должна сохранить их перед использованием и восстановить перед возвратом. На macOS X18 зарезервирован Apple, не используйте его.

Регистры X8-X15 - также временные, но имеют специальные роли в некоторых контекстах. X8 используется как indirect result location register (для больших возвращаемых значений). X9-X15 - полностью временные, могут использоваться как scratch. При написании функции, которая вызывает другие функции, помните, что любые из X0-X15 могут быть перезаписаны.

Практика

// Конвенция вызова: 4 аргумента в регистрах
// Файл: aapcs.s

.section __TEXT,__text
.global _main
.align 2

// Функция: W0 = W0 * W1 + W2 * W3
linear_combination:
    // Сохраняем callee-saved, если нужны
    // (здесь не нужны, мы не используем X19-X28)
    
    MUL W4, W0, W1       // W4 = W0 * W1
    MUL W5, W2, W3       // W5 = W2 * W3
    ADD W0, W4, W5       // W0 = W4 + W5
    RET                  // результат в W0

_main:
    STP X29, X30, [SP, #-16]!
    MOV X29, SP

    // Аргументы: 2 * 3 + 4 * 5 = 26
    MOV W0, #2
    MOV W1, #3
    MOV W2, #4
    MOV W3, #5

    BL linear_combination

    // W0 = 26

    LDP X29, X30, [SP], #16
    RET

Функция linear_combination принимает 4 аргумента в W0-W3 и возвращает результат в W0. Она использует временные регистры W4 и W5 для промежуточных вычислений - это безопасно, потому что они caller-saved и мы находимся в вызываемой функции. Если бы linear_combination вызывала другую функцию, W4 и W5 могли бы быть перезаписаны.

Домашнее задание

Напишите функцию avg4, которая принимает 4 числа в W0-W3 и возвращает их среднее арифметическое в W0. Используйте ADD для суммы и SDIV для деления. Вызовите из _main с аргументами 10, 20, 30, 40 и проверьте, что результат равен 25.

Тема 13. Работа с памятью: LDR и STR

Теория

Инструкции LDR (load register) и STR (store register) - основные инструкции работы с памятью в ARM64. LDR X0, [X1] загружает 64-битное значение из памяти по адресу в X1 в регистр X0. STR X0, [X1] сохраняет 64-битное значение из X0 в память по адресу в X1. Размер данных определяется суффиксом: LDRB (1 байт), LDRH (2 байта), LDR W (4 байта), LDR X (8 байтов).

ARM64 поддерживает несколько режимов адресации. Базовый: LDR X0, [X1] - адрес в X1. Со смещением: LDR X0, [X1, #8] - адрес X1 + 8. Прединдексация: LDR X0, [X1, #8]! - адрес X1 + 8, и X1 обновляется до X1 + 8. Постиндексация: LDR X0, [X1], #8 - адрес X1, затем X1 обновляется до X1 + 8. Прединдексация и постиндексация удобны для обхода массивов.

Для работы со стеком есть парные инструкции STP (store pair) и LDP (load pair), которые сохраняют и загружают два регистра одновременно. Они эффективнее, чем две отдельные инструкции STR и LDR, и автоматически работают с 16-байтным выравниванием. Используйте их для пролога и эпилога функций.

Практика

// LDR/STR: чтение и запись памяти
// Файл: memory.s

.section __DATA,__data
.align 3
array:
    .quad 10, 20, 30, 40, 50    // массив из 5 int64

.section __TEXT,__text
.global _main
.align 2

_main:
    STP X29, X30, [SP, #-16]!
    MOV X29, SP

    // Загружаем адрес массива
    ADRP X1, array@PAGE
    ADD  X1, X1, array@PAGEOFF

    // Читаем третий элемент (индекс 2, смещение 16)
    LDR  X2, [X1, #16]          // X2 = 30

    // Удваиваем его
    LSL  X2, X2, #1             // X2 = 60

    // Записываем обратно
    STR  X2, [X1, #16]          // array[2] = 60

    // Обход массива через постиндексацию
    MOV  X3, #0                 // индекс
    MOV  X4, #0                 // сумма
    MOV  X5, #5                 // количество элементов

loop:
    LDR  X6, [X1], #8           // X6 = *X1; X1 += 8
    ADD  X4, X4, X6             // сумма += X6
    ADD  X3, X3, #1             // индекс++
    CMP  X3, X5
    B.LT loop

    // X4 = сумма всех элементов

    LDP X29, X30, [SP], #16
    RET

Программа демонстрирует три режима адресации. Прямая адресация со смещением для чтения конкретного элемента. Запись обратно в память. Постиндексация LDR X6, [X1], #8 для обхода массива: после каждой загрузки X1 автоматически увеличивается на 8 (размер одного элемента).

Домашнее задание

Создайте массив из 10 байтов в секции __DATA,__data. Напишите программу, которая обходит массив с использованием LDRB и постиндексации, находит максимальный байт и возвращает его в W0. Инициализируйте массив значениями от 1 до 10 и проверьте, что результат равен 10.

Тема 14. Циклы и итерации

Теория

Цикл в ассемблере реализуется через метку, тело цикла, изменение счётчика и условную ветвь обратно на метку. ARM64 не имеет специальной инструкции цикла вроде LOOP в x86. Цикл for из C превращается в последовательность: инициализация счётчика, метка начала, тело, инкремент, сравнение, условная ветвь.

Цикл while реализуется так: метка начала, проверка условия, условная ветвь на конец, тело, безусловная ветвь на начало. Цикл do-while: метка начала, тело, проверка условия, условная ветвь на начало. Разница между while и do-while - в месте проверки условия: до тела или после.

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

Практика

// Цикл: сумма чисел от 1 до N
// Файл: loop.s

.section __TEXT,__text
.global _main
.align 2

_main:
    STP X29, X30, [SP, #-16]!
    MOV X29, SP

    MOV W0, #0           // сумма = 0
    MOV W1, #1           // счётчик = 1
    MOV W2, #100         // N = 100

loop_start:
    ADD W0, W0, W1       // сумма += счётчик
    ADD W1, W1, #1       // счётчик++
    CMP W1, W2           // сравнить с N
    B.LE loop_start      // если счётчик <= N, продолжать

    // W0 = 5050 (сумма 1..100)

    LDP X29, X30, [SP], #16
    RET

Цикл выполняет 100 итераций. Каждая итерация: ADD (1 такт), ADD (1 такт), CMP (1 такт, объединяется с ветвью), B.LE (1 такт при правильном предсказании). Всего 4 инструкции на итерацию, 400 инструкций на весь цикл. На современном Apple Silicon это менее 100 наносекунд.

Домашнее задание

Напишите цикл, который вычисляет сумму чётных чисел от 2 до 100. Используйте инкремент на 2: ADD W1, W1, #2. Сравните производительность с версией, которая проверяет каждый чётный бит через TST W1, #1. Какая быстрее и почему?

Тема 15. Массивы и строки

Теория

Массив в ассемблере - это последовательность элементов одинакового размера в памяти. Доступ к элементу выполняется через базовый адрес плюс смещение: LDR W0, [X1, X2, LSL #2] загружает элемент массива int по индексу в X2, где X1 - базовый адрес. Сдвиг LSL #2 умножает индекс на 4 (размер int в байтах). Для 64-битных элементов используйте LSL #3 (умножение на 8).

Строка в ассемблере - это массив байтов. ASCII-символы занимают по 1 байту. Для чтения символа используйте LDRB. Для сравнения строк нужно обойти оба массива и сравнить байты до нулевого терминатора. В C это делается через strcmp, в ассемблере вы пишете этот цикл вручную.

На macOS строки можно размещать в секции __TEXT,__const с помощью директивы .ascii или .asciz (с нулевым терминатором). Длина строки вычисляется как разность адресов: msg_len = . - msg. Директива . обозначает текущую позицию, поэтому . - msg даёт количество байтов от метки msg до текущей позиции.

Практика

// Строки: вычисление длины и преобразование в верхний регистр
// Файл: strings.s

.section __TEXT,__const
msg:
    .asciz "Hello, ARM64!"    // строка с нулевым терминатором

.section __TEXT,__text
.global _main
.align 2

_main:
    STP X29, X30, [SP, #-16]!
    MOV X29, SP

    ADRP X0, msg@PAGE
    ADD  X0, X0, msg@PAGEOFF

    // Вычисление длины строки
    MOV X1, X0                // X1 = указатель
strlen_loop:
    LDRB W2, [X1]            // W2 = текущий байт
    CBZ W2, strlen_done      // если 0, конец строки
    ADD X1, X1, #1           // следующий байт
    B strlen_loop
strlen_done:
    SUB X3, X1, X0           // X3 = длина строки

    // Преобразование в верхний регистр
    MOV X1, X0               // сброс указателя
upper_loop:
    LDRB W2, [X1]            // читаем символ
    CBZ W2, upper_done       // конец строки
    // Если 'a' <= W2 <= 'z', вычесть 32
    CMP W2, #'a'
    B.LT upper_skip
    CMP W2, #'z'
    B.GT upper_skip
    SUB W2, W2, #32          // в верхний регистр
    STRB W2, [X1]            // записываем обратно
upper_skip:
    ADD X1, X1, #1
    B upper_loop
upper_done:
    MOV W0, W3               // возвращаем длину

    LDP X29, X30, [SP], #16
    RET

Инструкция CBZ (compare and branch if zero) - компактная форма проверки на ноль. Она заменяет CMP W2, #0 + B.EQ. Сравнение с символьными константами #'a' и #'z' работает, потому что ASCII-коды букв идут подряд: 'a' = 97, 'z' = 122. Разница между строчной и прописной буквой - 32.

Домашнее задание

Напишите функцию strcmp_asm, которая сравнивает две строки и возвращает 0, если они равны, отрицательное число, если первая меньше, и положительное, если первая больше. Адресы строк передаются в X0 и X1. Результат - в W0. Протестируйте на строках "hello" и "world".

Тема 16. Функции и процедуры

Теория

Функция в ассемблере - это метка, после которой идёт код, заканчивающийся инструкцией RET. Вызов функции - BL label, который сохраняет адрес возврата в X30 и переходит на метку. Возврат - RET, который переходит по адресу в X30. Если функция вызывает другие функции, она должна сохранить X30 на стеке, потому что BL перезапишет его.

Локальные переменные функции размещаются на стеке. Пролог функции выделяет место на стеке, вычитая из SP размер, нужный для локальных переменных. Эпилог освобождает это место, прибавляя обратно. Например, для двух локальных 64-битных переменных: SUB SP, SP, #16 в прологе, ADD SP, SP, #16 в эпилоге. Доступ к переменным - через STR и LDR с смещением от SP.

Функции с большим количеством локальных переменных или глубокой рекурсией могут переполнить стек. Стек на macOS по умолчанию ограничен 8 МБ для главного потока. Для рекурсивных функций с глубоким стеком это ограничение может стать проблемой. В C это решается через ulimit -s или программную итерацию вместо рекурсии.

Практика

// Функция с локальными переменными на стеке
// Файл: functions.s

.section __TEXT,__text
.global _main
.align 2

// Функция: W0 = W0 * W0 + local1 * local2
compute:
    // Пролог с локальными переменными
    STP X29, X30, [SP, #-32]!  // 32 байт: 16 для FP/LR, 16 для локальных
    MOV X29, SP

    // Локальные переменные по смещениям от SP
    STR W0, [SP, #16]           // local1 = W0 (первый аргумент)
    MOV W1, #3
    STR W1, [SP, #20]           // local2 = 3

    // W0 * W0
    LDR W2, [SP, #16]           // W2 = local1
    MUL W3, W2, W2              // W3 = W2 * W2

    // local1 * local2
    LDR W4, [SP, #16]
    LDR W5, [SP, #20]
    MUL W6, W4, W5              // W6 = local1 * local2

    // Результат
    ADD W0, W3, W6              // W0 = W3 + W6

    // Эпилог
    LDP X29, X30, [SP], #32
    RET

_main:
    STP X29, X30, [SP, #-16]!
    MOV X29, SP

    MOV W0, #5                  // аргумент
    BL compute                 // W0 = 5*5 + 5*3 = 40

    LDP X29, X30, [SP], #16
    RET

Функция compute выделяет 32 байта на стеке: 16 для сохранения FP и LR, 16 для двух локальных 32-битных переменных. Локальные переменные хранятся по смещениям #16 и #20 от SP. Директива STP X29, X30, [SP, #-32]! одновременно уменьшает SP на 32 и сохраняет два регистра.

Домашнее задание

Напишите функцию swap, которая принимает адреса двух переменных (в X0 и X1) и обменивает их значения. Функция должна загрузить значения по адресам, обменять их и записать обратно. Вызовите из _main с двумя переменными на стеке.

Тема 17. Рекурсия

Теория

Рекурсия в ассемблере - это функция, которая вызывает сама себя через BL. Каждый вызов создаёт новый фрейм на стеке с сохранённым LR и локальными переменными. При возврате из рекурсии фреймы освобождаются в обратном порядке. Рекурсия работает, потому что каждый вызов имеет свой набор сохранённых регистров и локальных переменных на стеке.

Классический пример рекурсии - факториал. Факториал n равен n, умноженному на факториал (n-1). Базовый случай: факториал 0 равен 1. В ассемблере функция получает n в W0, проверяет базовый случай, если n равно 0, возвращает 1. Иначе уменьшает n на 1, вызывает себя, умножает результат на исходное n.

Рекурсия требует сохранения LR перед рекурсивным вызовом, потому что BL перезапишет его. Каждый уровень рекурсии добавляет минимум 16 байт на стек (для FP и LR). Для факториала 10 это 160 байт - ничтожно мало. Для факториала 1000 это 16 КБ, что тоже приемлемо. Но для неконтролируемой рекурсии переполнение стека наступает быстро.

Практика

// Рекурсия: факториал
// Файл: recursion.s

.section __TEXT,__text
.global _main
.align 2

// Функция: W0 = factorial(W0)
factorial:
    // Пролог
    STP X29, X30, [SP, #-16]!
    MOV X29, SP

    // Базовый случай: если W0 <= 1, вернуть 1
    CMP W0, #1
    B.GT recurse

    MOV W0, #1               // factorial(0) = factorial(1) = 1
    B fact_done

recurse:
    // Сохраняем n
    MOV W19, W0              // W19 = n (callee-saved)

    // Рекурсивный вызов: factorial(n-1)
    SUB W0, W0, #1
    BL factorial
    // W0 = factorial(n-1)

    // Умножаем: n * factorial(n-1)
    MUL W0, W19, W0          // W0 = n * factorial(n-1)

fact_done:
    // Эпилог
    LDP X29, X30, [SP], #16
    RET

_main:
    STP X29, X30, [SP, #-16]!
    MOV X29, SP

    MOV W0, #5               // 5!
    BL factorial             // W0 = 120

    LDP X29, X30, [SP], #16
    RET

Регистр W19 используется для сохранения n перед рекурсивным вызовом. W19 - callee-saved, поэтому factorial может его использовать, но должна сохранить перед использованием. В данном случае мы не сохраняем W19 явно, потому что полагаемся на то, что вызывающая функция (_main) не ожидает его сохранения. В более строгом коде нужно сохранить W19 в прологе и восстановить в эпилоге.

Домашнее задание

Напишите рекурсивную функцию Фибоначчи: fib(0) = 0, fib(1) = 1, fib(n) = fib(n-1) + fib(n-2). Вызовите fib(10) и проверьте, что результат равен 55. Сравните производительность с итеративной версией. Почему рекурсивная версия медленнее и как это исправить?

Тема 18. Взаимодействие с C: вызов C из ассемблера

Теория

На macOS ARM64 ассемблерный код может вызывать функции из C-библиотек. libSystem.dylib предоставляет стандартные функции: printf, malloc, free, puts, exit. Для вызова C-функции из ассемблера используйте BL _function_name, где имя функции имеет префикс подчёркивания. Это специфика macOS: в C функция называется printf, в ассемблере - _printf.

При вызове C-функций нужно соблюдать конвенцию вызова AAPCS64. Первые 8 аргументов в X0-X7. Возвращаемое значение в X0. Стек выровнен на 16 байт. Для variadic-функций (printf, scanf) macOS требует укладки аргументов в стек, а не в регистры. Это важное отличие от Linux, где variadic-аргументы передаются в регистрах.

Для вызова printf на macOS нужно: загрузить форматную строку в X0 через ADRP+ADD, уложить остальные аргументы в стек, вызвать BL _printf. После вызова очистить стек. Форматная строка должна быть в секции __TEXT,__const и заканчиваться нулевым байтом.

Практика

// Вызов printf из ассемблера
// Файл: call_c.s

.section __TEXT,__const
.align 2
fmt:
    .asciz "x = %d, y = %d, sum = %d\n"

.section __TEXT,__text
.global _main
.align 2

_main:
    STP X29, X30, [SP, #-16]!
    MOV X29, SP

    MOV W19, #15          // x (callee-saved)
    MOV W20, #27          // y (callee-saved)
    ADD W21, W19, W20     // sum = 42

    // Подготовка аргументов для printf
    // printf(fmt, x, y, sum)
    // На macOS variadic-аргументы кладутся в стек

    SUB SP, SP, #32       // место для 4 аргументов (16-байт выравнивание)
    STR W21, [SP, #0]     // sum (3-й аргумент после fmt)
    STR W20, [SP, #8]     // y
    STR W19, [SP, #16]    // x

    ADRP X0, fmt@PAGE
    ADD  X0, X0, fmt@PAGEOFF

    BL _printf

    ADD SP, SP, #32       // очистка стека

    MOV W0, #0            // код возврата

    LDP X29, X30, [SP], #16
    RET

Программа вызывает printf с форматной строкой и тремя аргументами. Аргументы укладываются в стек, потому что printf - variadic-функция. Выравнивание стека на 16 байт соблюдается: 32 байта - кратно 16. После вызова стек очищается прибавлением 32 к SP.

Домашнее задание

Напишите программу, которая вызывает printf для вывода строки "Hello from assembly!" без аргументов. Затем вызовите puts для вывода другой строки. Объясните разницу между printf и puts с точки зрения вызова из ассемблера.

Тема 19. Взаимодействие с C: вызов ассемблера из C

Теория

Ассемблерные функции можно вызывать из C-кода. Для этого нужно: объявить функцию с внешней linkage в C, реализовать её в ассемблере с правильным именем (с префиксом подчёркивания на macOS), и собирать оба файла вместе. C-компилятор и ассемблер создают объектные файлы, которые линкер объединяет в один исполняемый файл.

Ассемблерная функция должна соблюдать конвенцию вызова AAPCS64. Аргументы приходят в X0-X7, результат возвращается в X0. Если функция использует callee-saved регистры (X19-X28), она должна сохранить их в прологе и восстановить в эпилоге. Регистры X0-X18 можно использовать свободно, но помните, что X18 зарезервирован Apple.

Для сборки многофайлового проекта используйте clang с обоими файлами: clang -arch arm64 main.c functions.s -o program. Clang автоматически вызовет ассемблер для .s файла и линкер для объединения. C-функции могут вызывать ассемблерные функции и наоборот без ограничений.

Практика

Файл main.c:

#include <stdio.h>

// Объявление внешней функции
extern int add_asm(int a, int b);
extern int factorial_asm(int n);

int main(void) {
    printf("add_asm(10, 20) = %d\n", add_asm(10, 20));
    printf("factorial_asm(5) = %d\n", factorial_asm(5));
    return 0;
}

Файл functions.s:

.section __TEXT,__text
.global _add_asm
.global _factorial_asm
.align 2

// int add_asm(int a, int b)
_add_asm:
    ADD W0, W0, W1
    RET

// int factorial_asm(int n)
_factorial_asm:
    STP X29, X30, [SP, #-16]!
    MOV X29, SP

    CMP W0, #1
    B.GT fact_recurse

    MOV W0, #1
    B fact_end

fact_recurse:
    MOV W19, W0
    SUB W0, W0, #1
    BL _factorial_asm
    MUL W0, W19, W0

fact_end:
    LDP X29, X30, [SP], #16
    RET

Сборка: clang -arch arm64 main.c functions.s -o program. Запуск: ./program. Вывод:

add_asm(10, 20) = 30
factorial_asm(5) = 120

Домашнее задание

Напишите ассемблерную функцию strlen_asm(const char *s), которая вычисляет длину строки. Объявите её в C, вызовите из main и сравните результат с strlen из стандартной библиотеки. Сборка: clang main.c strlen.s -o test.

Тема 20. Указатели и структуры

Теория

Указатель в ассемблере - это просто адрес в регистре. Нет специальных типов указателей, как в C. Регистр X0 может содержать адрес чего угодно: массива, структуры, функции. Тип данных определяется тем, как вы обращаетесь к памяти: LDRB для байта, LDR W для 32-битного int, LDR X для 64-битного long. Программист отвечает за правильный размер доступа.

Структура в C размещается в памяти последовательно, с возможным выравниванием полей. На ARM64 выравнивание по умолчанию: 1 байт для char, 2 для short, 4 для int, 8 для long и указателей. Поля идут в порядке объявления. Для доступа к полю структуры используйте базовый адрес плюс смещение поля.

Например, структура struct Point { int x; int y; } занимает 8 байтов. Поле x по смещению 0, поле y по смещению 4. Доступ: LDR W0, [X1] для x, LDR W0, [X1, #4] для y, где X1 содержит адрес структуры. Если структура передаётся по указателю, сначала загрузите указатель в регистр, затем обращайтесь к полям по смещению.

Практика

Файл struct_test.c:

#include <stdio.h>

struct Point {
    int x;
    int y;
};

extern int distance_squared(struct Point *p1, struct Point *p2);

int main(void) {
    struct Point a = {3, 4};
    struct Point b = {6, 8};
    printf("distance^2 = %d\n", distance_squared(&a, &b));
    return 0;
}

Файл struct_test.s:

// int distance_squared(struct Point *p1, struct Point *p2)
// X0 = p1, X1 = p2
// struct Point { int x (offset 0); int y (offset 4); }

.section __TEXT,__text
.global _distance_squared
.align 2

_distance_squared:
    // Загружаем координаты первой точки
    LDR W2, [X0]          // p1->x
    LDR W3, [X0, #4]      // p1->y

    // Загружаем координаты второй точки
    LDR W4, [X1]          // p2->x
    LDR W5, [X1, #4]      // p2->y

    // Разности
    SUB W2, W4, W2        // dx = p2->x - p1->x
    SUB W3, W5, W3        // dy = p2->y - p1->y

    // Квадраты
    MUL W2, W2, W2        // dx * dx
    MUL W3, W3, W3        // dy * dy

    // Сумма
    ADD W0, W2, W3        // dx^2 + dy^2

    RET

Сборка: clang -arch arm64 struct_test.c struct_test.s -o struct_test. Запуск: ./struct_test. Вывод: distance^2 = 25. Функция получает указатели на две структуры Point и вычисляет квадрат расстояния между ними. Смещения полей (0 и 4) соответствуют структуре C.

Домашнее задание

Определите структуру struct Rect { int x, y, width, height; }. Напишите ассемблерную функцию int rect_area(struct Rect *r), которая возвращает площадь прямоугольника. Напишите функцию int rect_contains(struct Rect *r, int px, int py), которая возвращает 1, если точка (px, py) внутри прямоугольника, и 0 в противном случае.

Тема 21. NEON и SIMD-инструкции

Теория

NEON - это SIMD-расширение ARM (Single Instruction Multiple Data), которое позволяет выполнять одну операцию над несколькими данными одновременно. NEON-регистры (V0-V31) имеют ширину 128 бит и могут рассматриваться как массивы: 16 байтов, 8 halfwords, 4 words, 2 doublewords. Одна инструкция может сложить 4 пары 32-битных чисел за один такт.

NEON полезен для обработки медиа: графики, аудио, видео, научных вычислений. Например, конвертация RGBA-пикселя в grayscale: нужно умножить 4 компонента на коэффициенты и сложить. С NEON это одна инструкция вместо четырёх. Умножение матриц, свёртка, БПФ - все эти алгоритмы ускоряются в 2-4 раза при правильном использовании SIMD.

Apple Silicon полностью поддерживает NEON. В macOS 14 и новее также доступно расширение AMX (Apple Matrix Extension) для матричных вычислений. NEON-инструкции имеют суффиксы, обозначающие тип данных: .16B (16 байтов), .4S (4 слова), .2D (2 doubleword). Например, ADD V0.4S, V1.4S, V2.4S складывает 4 пары 32-битных чисел одновременно.

Практика

// NEON: сложение 4 пар чисел одновременно
// Файл: neon.s

.section __TEXT,__const
.align 4
vec_a:
    .word 10, 20, 30, 40     // 4 x 32-битных числа
vec_b:
    .word 1, 2, 3, 4

.section __TEXT,__text
.global _main
.align 2

_main:
    STP X29, X30, [SP, #-16]!
    MOV X29, SP

    // Загружаем векторы
    ADRP X0, vec_a@PAGE
    ADD  X0, X0, vec_a@PAGEOFF
    LDR  Q0, [X0]            // Q0 = 128-битный регистр

    ADRP X1, vec_b@PAGE
    ADD  X1, X1, vec_b@PAGEOFF
    LDR  Q1, [X1]

    // NEON-сложение: 4 пары 32-битных чисел
    ADD  V2.4S, V0.4S, V1.4S  // V2 = {11, 22, 33, 44}

    // Сохраняем результат
    ADRP X2, vec_a@PAGE
    ADD  X2, X2, vec_a@PAGEOFF
    STR  Q2, [X2]            // перезаписываем vec_a

    // Возвращаем первый элемент
    MOV  W0, V2.S[0]         // W0 = 11

    LDP X29, X30, [SP], #16
    RET

Инструкция LDR Q0, [X0] загружает 128 бит из памяти в NEON-регистр Q0. ADD V2.4S, V0.4S, V1.4S складывает четыре 32-битных элемента одновременно. MOV W0, V2.S[0] извлекает первый 32-битный элемент из вектора. Одна инструкция ADD выполнила работу четырёх обычных ADD.

Домашнее задание

Напишите программу, которая умножает два 4-элементных вектора через NEON: MUL V2.4S, V0.4S, V1.4S. Инициализируйте векторы значениями и проверьте результат. Затем напишите скалярную версию (через обычные MUL) и сравните количество инструкций.

Тема 22. Оптимизация кода

Теория

Оптимизация ассемблерного кода начинается с измерений. Преждевременная оптимизация без профилирования часто ухудшает код. На macOS для измерения времени используется mach_absolute_time() из Mach API, или утилита time в терминале. Для точного профилирования - Instruments.app от Apple, которая показывает такты процессора на каждую функцию.

Основные приёмы оптимизации на ARM64: уменьшение количества инструкций в горячих циклах, использование условных инструкций вместо ветвлений, использование NEON для параллельной обработки, правильное расположение данных в памяти (cache-friendly), устранение зависимостей между инструкциями для параллельного выполнения. Современные процессоры ARM64 могут выполнять несколько инструкций за такт, если они независимы.

Компилятор C с оптимизацией -O2 или -O3 часто генерирует код, который трудно превзойти вручную. Но в специфических случаях ручной ассемблер может быть быстрее: криптографические функции, обработка медиа, эмуляторы, JIT-компиляторы. Перед написанием ручного ассемблера изучите сгенерированный компилятором код через clang -S -O3 file.c.

Практика

// Оптимизация: сумма массива через развёртывание цикла
// Файл: optimize.s

.section __DATA,__data
.align 4
array:
    .word 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12

.section __TEXT,__text
.global _main
.align 2

_main:
    STP X29, X30, [SP, #-16]!
    MOV X29, SP

    ADRP X0, array@PAGE
    ADD  X0, X0, array@PAGEOFF

    MOV W1, #0              // сумма
    MOV W2, #12             // количество элементов
    MOV W3, #0              // индекс

// Обычный цикл (4 инструкции на итерацию)
sum_loop:
    LDR W4, [X0, X3, LSL #2]
    ADD W1, W1, W4
    ADD W3, W3, #1
    CMP W3, W2
    B.LT sum_loop

    // W1 = 78 (сумма 1..12)

    LDP X29, X30, [SP], #16
    RET

Базовая версия цикла - 5 инструкций на итерацию (LDR, ADD, ADD, CMP, B). Развёртывание цикла на 2 итерации уменьшает количество ветвлений вдвое: обрабатываем по 2 элемента за итерацию, ветвление только каждые 2 элемента. Это улучшает производительность на 10-20% за счёт лучшего использования конвейера процессора.

Домашнее задание

Напишите две версии суммирования массива: базовую и с развёртыванием цикла на 4. Измерьте время выполнения обеих версий на массиве из 100000 элементов. Используйте clock() из C или mach_absolute_time(). Сравните результаты.

Тема 23. Дизассемблирование и обратная разработка

Теория

Дизассемблирование - это превращение бинарного кода обратно в ассемблер. На macOS для этого используется otool -tv или objdump --macho -d. Дизассемблирование помогает понять, как работает скомпилированная программа, найти ошибки, проанализировать поведение библиотек и проверить, что компилятор сгенерировал ожидаемый код.

Для изучения кода C-функции используйте clang -S -O2 file.c -o file.s. Это сгенерирует ассемблерный код, который компилятор создаёт для вашего C-файла. Сравнение с вашим ручным ассемблером покажет, где компилятор делает лучше (обычно - почти везде) и где вы можете улучшить (специфические случаи).

Для обратной разработки бинарников без исходного кода используются специализированные инструменты: Hopper Disassembler, IDA Pro, Ghidra. Они предоставляют графический интерфейс, декомпиляцию в C-подобный псевдокод и анализ потока управления. Для базового анализа достаточно otool и nm в терминале.

Практика

# Дизассемблирование программы

# Компиляция C-файла с оптимизацией
clang -arch arm64 -O2 -S -o factorial.s factorial.c

# Просмотр сгенерированного ассемблера
cat factorial.s

# Дизассемблирование бинарного файла
otool -tv ./program

# Использование objdump
objdump --macho -d ./program

# Просмотр символов
nm ./program

# Просмотр строк в бинарнике
strings ./program | head -20

# Просмотр зависимостей от динамических библиотек
otool -L ./program

Команда clang -S показывает, как компилятор превращает ваш C-код в ассемблер. Вы увидите, как компилятор размещает переменные, как он оптимизирует циклы, как он встраивает (inline) функции. Это лучший учебный материал для понимания связи между C и ассемблером.

Домашнее задание

Напишите простую C-функцию (например, вычисление факториала) и скомпилируйте её с -O0, -O1, -O2 и -O3. Сравните сгенерированный ассемблерный код. Какие оптимизации компилятор применяет на каждом уровне? Сколько инструкций в каждой версии?

Тема 24. arm64e и Pointer Authentication

Теория

arm64e - это расширение ARM64 с Pointer Authentication Codes (PAC), которое Apple использует начиная с чипа A12 (и всех Apple Silicon M-серии). PAC добавляет криптографическую подпись к указателям и адресам возврата. Это защищает от атак, основанных на подмене адресов: ROP (Return-Oriented Programming), JOP (Jump-Oriented Programming) и других.

При включённом PAC адрес возврата в LR подписывается специальной инструкцией PACIASP перед сохранением на стек. При возврате подпись проверяется инструкцией RETAA (или AUTIASP + RET). Если подпись не совпадает, процесс падает с SIGSEGV. Это делает атаки на управление потоком выполнения значительно сложнее.

Для большинства ассемблерного кода на macOS PAC прозрачен: система обрабатывает его автоматически. Но если вы пишете низкоуровневый код, который работает с указателями напрямую, или используете arm64e-цель, нужно учитывать PAC. На macOS по умолчанию системные библиотеки используют arm64e, а пользовательские приложения - arm64 (без PAC). Universal binaries могут содержать оба варианта.

Практика

// Проверка архитектуры бинарного файла
// Выполните в терминале:

# Проверка архитектуры Python3
lipo -info /usr/bin/python3
# Вывод: x86_64 arm64e

# Извлечение arm64e-среза
lipo -extract arm64e /usr/bin/python3 -o python3_arm64e

# Проверка вашего бинарника
file ./hello
# Вывод: Mach-O 64-bit executable arm64

# Дизассемблирование arm64e-бинарника
# Ищите инструкции PACIASP, AUTIASP, RETAA
otool -tv /usr/bin/python3 | grep -A2 "PACIASP\|RETAA" | head -20

В arm64e-коде вы увидите инструкции PACIASP в прологах функций и RETAA или AUTIASP в эпилогах. Это признаки работы Pointer Authentication. В пользовательских arm64-бинарниках этих инструкций нет.

Домашнее задание

Скомпилируйте простую C-программу для arm64 и arm64e: clang -arch arm64e -o test_arm64e test.c и clang -arch arm64 -o test_arm64 test.c. Сравните размер бинарников и дизассемблируйте обе версии. Найдите инструкции PAC в arm64e-версии.

Тема 25. Современные возможности: атомики, барьеры, WFE

Теория

ARM64 предоставляет инструкции для многопоточного программирования: атомарные операции, барьеры памяти, ожидание события. Атомарные операции выполняются неделимо: LDXR (load exclusive) и STXR (store exclusive) реализуют compare-and-swap. Инструкция STXR возвращает статус: 0 если успешно, 1 если не удалось (другой поток изменил значение между LDXR и STXR).

Барьеры памяти гарантируют порядок доступа к памяти в многопоточной среде. DMB (data memory barrier) гарантирует, что все операции доступа к памяти до барьера завершены до операций после. DSB (data synchronization barrier) - более сильный: процессор ждёт, пока все операции до барьера не завершатся полностью. ISB (instruction synchronization barrier) сбрасывает конвейер и гарантирует, что последующие инструкции загружаются заново.

Инструкция WFE (wait for event) переводит процессор в режим ожидания до получения события от другого ядра. SEV (send event) отправляет событие всем ядрам. Это используется для эффективной синхронизации: вместо цикла ожидания (busy-wait) процессор засыпает и просыпается по сигналу. На Apple Silicon WFE и SEV поддерживаются, но обычно программисты используют высокоуровневые примитивы (мьютексы, очереди) вместо прямых инструкций.

Практика

// Атомарный инкремент через LDXR/STXR
// Файл: atomic.s

.section __DATA,__data
.align 3
counter:
    .quad 0

.section __TEXT,__text
.global _atomic_increment
.align 2

// void atomic_increment(long *addr, long value)
// X0 = addr, X1 = value
_atomic_increment:
    // Пролог
    STP X29, X30, [SP, #-16]!
    MOV X29, SP

atomic_loop:
    LDXR X2, [X0]          // эксклюзивная загрузка
    ADD  X2, X2, X1        // прибавляем значение
    STXR W3, X2, [X0]      // эксклюзивная запись
    CBNZ W3, atomic_loop   // если не удалось (W3 != 0), повторить

    // Эпилог
    LDP X29, X30, [SP], #16
    RET

.global _main
_main:
    STP X29, X30, [SP, #-16]!
    MOV X29, SP

    ADRP X0, counter@PAGE
    ADD  X0, X0, counter@PAGEOFF
    MOV  X1, #1

    // Инкрементируем 5 раз
    BL _atomic_increment
    BL _atomic_increment
    BL _atomic_increment
    BL _atomic_increment
    BL _atomic_increment

    // Читаем результат
    LDR  X0, [X0]          // X0 = 5

    LDP X29, X30, [SP], #16
    RET

Инструкция LDXR X2, [X0] загружает значение эксклюзивно: процессор запоминает, что этот адрес был прочитан. STXR W3, X2, [X0] пытается записать эксклюзивно: если адрес не был изменён другим ядром после LDXR, запись успешна и W3 = 0. Если изменён - W3 = 1, и нужно повторить цикл. CBNZ (compare and branch if non-zero) проверяет статус и зацикливается при неудаче.

Домашнее задание

Напишите функцию atomic_compare_and_swap(long *addr, long expected, long new_value), которая заменяет значение по адресу на new_value, только если текущее значение равно expected. Верните 1 при успехе и 0 при неудаче. Используйте LDXR и STXR. Протестируйте из C.

Что дальше

25 тем этого курса - не предел, а фундамент. Дальше идёт системное программирование: работа с Mach API, порты, сообщения, виртуальная память, нити. Затем - углубление в архитектуру: конвейер процессора, кэш-линейки, векторизация, предсказание ветвлений. Потом - безопасность: эксплойты, ROP-цепочки, защита PAC, reverse engineering.

Ассемблер - не конечная точка, а отправная. После ассемблера легче понять, как компилятор оптимизирует код, как JIT-компиляторы генерируют машинный код на лету, как работают эмуляторы и виртуальные машины. Знание ARM64 делает вас лучшим C-программистом: вы понимаете, что стоит за каждым int и pointer, и почему компилятор выбирает одни инструкции вместо других.

Главное, что даёт этот курс, - не синтаксис, а модель мышления. Вы начинаете видеть программу как поток инструкций в процессоре, управляемый регистрами и адресами памяти. Это уровень абстракции, на котором компьютер больше не чёрный ящик. И именно он отличает программиста, который понимает машину, от программиста, который умеет пользоваться инструментами.

Учите ассемблер. Не потому что он модный, а потому что он показывает, как работает процессор. А процессор - это то, что выполняет вашу программу каждую наносекунду.


 Ваша оценка:

Связаться с программистом сайта.

Новые книги авторов СИ, вышедшие из печати:
О.Болдырева "Крадуш. Чужие души" М.Николаев "Вторжение на Землю"

Как попасть в этoт список

Кожевенное мастерство | Сайт "Художники" | Доска об'явлений "Книги"