Основы программирования на ассемблере: Быстрый старт

Этот вводный курс поможет понять принципы работы процессора и преодолеть ограничения языков высокого уровня [rus-linux.net](https://rus-linux.net/MyLDP/algol/get_started_with_assembly_language_1.html). Вы изучите архитектуру x86-64, базовый синтаксис NASM и напишете свои первые низкоуровневые программы [xakep.ru](https://xakep.ru/2020/06/16/asm-course-2/).

1. Архитектура процессора и установка инструментария NASM [metanit.com](https://metanit.com/assembler/nasm)

Архитектура процессора и установка инструментария NASM metanit.com

Архитектура Intel x86-64

Программирование на ассемблере требует понимания того, как устроено «железо». В отличие от языков высокого уровня (Python, Java, C++), где детали скрыты за абстракциями, ассемблер работает напрямую с ресурсами процессора. Современные компьютеры, будь то серверы, ноутбуки или десктопы, преимущественно построены на архитектуре фон Неймана.

!Три блока: Центральный процессор (CPU), Память (Memory) и Устройства ввода-вывода (I/O), соединенные системной шиной (System Bus)

Эта архитектура состоит из трех ключевых компонентов:

  • Центральный процессор (ЦП/CPU): «Мозг» компьютера, выполняющий инструкции.
  • Память: Хранит как данные, так и сами программы.
  • Устройства ввода/вывода (I/O): Клавиатура, диск, монитор, сеть.
  • Все эти компоненты общаются через системную шину, которая передает адреса, данные и управляющие сигналы. Когда вы пишете код на ассемблере, вы управляете именно процессором, заставляя его перемещать данные между регистрами, памятью и портами ввода-вывода.

    Современным стандартом для персональных компьютеров является архитектура Intel x86-64. Это 64-битное расширение классической 32-битной архитектуры x86. Она поддерживает обратную совместимость, что позволяет запускать старые программы на новом оборудовании.

    Регистры процессора

    Регистры — это сверхбыстрая память, находящаяся внутри самого процессора. Это «рабочий стол» процессора: прежде чем выполнить операцию (сложение, сравнение, логическое И), данные обычно нужно поместить в регистры.

    В архитектуре x86-64 доступно 16 регистров общего назначения (General Purpose Registers — GPR). Каждый из них имеет размер 64 бита.

    Основные 64-битные регистры

    * RAX (Accumulator): Часто используется для арифметики и возврата значений из функций. * RBX (Base): Базовый регистр, часто используется как указатель на данные. * RCX (Counter): Счетчик, используется в циклах. * RDX (Data): Используется в арифметике и вводе-выводе. * RSP (Stack Pointer): Указывает на вершину стека (важнейший регистр для управления памятью). * RBP (Base Pointer): Указывает на базу кадра стека (используется при вызове функций). * RSI (Source Index): Индекс источника (для операций копирования). * RDI (Destination Index): Индекс назначения (для операций копирования). * R8 — R15: Дополнительные регистры, добавленные в 64-битной архитектуре.

    Доступ к частям регистров

    Одной из ключевых особенностей x86 является возможность обращаться к частям одного и того же регистра. Это наследие эволюции от 8-битных процессоров к 64-битным.

    Рассмотрим на примере регистра RAX (64 бита): * RAX: Полный 64-битный регистр. * EAX: Младшие 32 бита регистра RAX. * AX: Младшие 16 бит регистра RAX (или младшие 16 бит EAX). * AH: Старшие 8 бит регистра AX. * AL: Младшие 8 бит регистра AX.

    !Схема вложенности: длинный прямоугольник RAX (64 бита), внутри него справа EAX (32 бита), внутри EAX справа AX (16 бит), внутри AX деление на AH (8 бит) и AL (8 бит)

    Это правило работает для регистров A, B, C, D. Например, для счетчика: RCX -> ECX -> CX -> CH / CL. Для новых регистров (R8-R15) суффиксы другие: R8 (64 бита), R8D (32 бита), R8W (16 бит), R8B (8 бит).

    Изменение младшей части регистра (например, AL) меняет и значение всего регистра RAX. Однако запись в 32-битную часть (EAX) на 64-битных процессорах обычно обнуляет старшую половину RAX.

    Единицы информации

    В ассемблере вы постоянно работаете с битами и байтами.

    где — один байт, минимально адресуемая единица памяти, а — восемь бит (нулей или единиц), составляющих этот байт.

    Разрядность процессора определяет, с какими числами он может работать за одну операцию и сколько памяти может адресовать. 64-битный процессор теоретически может адресовать:

    где — это 18 446 744 073 709 551 616 уникальных адресов, что значительно превышает объемы существующей оперативной памяти.

    Установка инструментария NASM

    Для превращения исходного кода (текста) в машинный код нам нужен компилятор. Мы будем использовать NASM (Netwide Assembler). Это популярный, бесплатный и кроссплатформенный инструмент.

    Важно понимать цепочку создания программы:

  • Исходный код (.asm): Текст программы.
  • Компиляция (NASM): Превращает .asm в объектный файл (.obj или .o). Это машинный код, но еще не готовая программа (нет адресов системных библиотек).
  • Компоновка (Linker): Объединяет объектный файл с системными библиотеками и создает исполняемый файл (.exe или без расширения в Linux/macOS).
  • Установка на Windows

  • Перейдите на официальный сайт nasm.us и выберите актуальную версию (например, 2.16.01).
  • Зайдите в папку win64 и скачайте ZIP-архив (например, nasm-2.16.01-win64.zip). Использование инсталлятора не обязательно.
  • Распакуйте архив в удобное место, например, в C:\NASM.
  • Настройка переменных среды (обязательно):
  • * Откройте поиск Windows и введите «Изменение системных переменных среды». * Нажмите кнопку «Переменные среды». * В списке «Системные переменные» (нижнее окно) найдите строку Path и нажмите «Изменить». * Нажмите «Создать» и вставьте путь к папке с nasm.exe (например, C:\NASM). * Сохраните изменения (ОК -> ОК).

    Чтобы проверить установку, откройте командную строку (cmd) и введите:

    Если вы увидите версию (например, NASM version 2.16.01), значит, все настроено верно.

    Установка на macOS

    На macOS проще всего использовать пакетный менеджер Homebrew.

  • Откройте Терминал.
  • Если Homebrew не установлен, установите его командой с brew.sh.
  • Введите команду:
  • После установки проверьте версию командой nasm -v.

    Также для компоновки (создания запускаемых файлов) вам понадобятся инструменты разработчика Xcode. Если они не установлены, система предложит их установить при первой попытке компиляции, или вы можете вызвать установку вручную:

    Установка на Linux

    В большинстве дистрибутивов Linux NASM доступен в стандартных репозиториях.

    Для Ubuntu/Debian:

    Для Fedora/Red Hat:

    Для компоновки в Linux обычно используется ld (часть пакета binutils) или gcc, которые часто уже предустановлены.

    Итоги

    * Процессор взаимодействует с памятью и устройствами через регистры и шину данных. * Регистры — это сверхбыстрая память внутри ЦП. В x86-64 основные регистры имеют размер 64 бита (начинаются на R, например RAX). * Регистры имеют вложенную структуру: к младшим частям 64-битного регистра можно обращаться как к 32-битным (EAX), 16-битным (AX) и 8-битным (AL) регистрам. * NASM — это ассемблер, который переводит код в объектный файл. Для получения исполняемой программы необходим этап компоновки (linking). * Для работы необходимо добавить путь к NASM в переменные среды (Path), чтобы вызывать его из командной строки.

    2. Структура программы: секции кода, метки и первый запуск [metanit.com](https://metanit.com/assembler/nasm/1.4.php)

    Структура программы: секции кода, метки и первый запуск metanit.com

    Любая программа на ассемблере — это не просто набор инструкций, а структурированный документ, который должен быть понятен и компилятору (NASM), и компоновщику (Linker). Чтобы процессор начал выполнение кода, ему нужно знать, где этот код находится и с какой команды начинать.

    Основные элементы программы

    Минимальная программа на NASM состоит из трех ключевых элементов: директивы видимости, секции кода и метки точки входа.

    1. Директива global

    Чтобы операционная система могла запустить программу, компоновщик должен знать, где находится начало исполнения. Для этого используется директива global.

    Эта строка сообщает компоновщику, что метка _start должна быть видимой извне (за пределами текущего файла). Без этой директивы метка осталась бы локальной, и компоновщик выдал бы ошибку «entry point not found».

    2. Секция кода (.text)

    Память программы делится на сегменты (секции). Код программы — инструкции, которые выполняет процессор — должен находиться в специальной секции, защищенной от записи (чтобы программа случайно не переписала сама себя), но доступной для чтения и исполнения.

    Все инструкции процессора (mov, add, syscall) пишутся строго после этой директивы.

    3. Метки (Labels)

    Метка — это человекочитаемое имя для определенного адреса в памяти. В ассемблере нет переменных или функций в привычном понимании, есть только адреса. Метка заканчивается двоеточием.

    Когда вы пишете _start:, вы говорите ассемблеру: «Запомни адрес этой инструкции под именем _start». Это стандартное имя для точки входа в Linux-программах (аналог main в C++).

    4. Комментарии

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

    Первая программа: Корректный выход

    Самая простая программа не выводит текст (это сложнее, чем кажется), а просто корректно завершает работу, сообщая операционной системе код возврата. Рассмотрим код для Linux x86-64.

    Создайте файл hello.asm и вставьте следующий код:

    Разбор инструкций

  • mov rax, 60: Инструкция mov (move) копирует число 60 в регистр RAX. В Linux регистр RAX используется для передачи номера системного вызова. 60 — это номер функции «завершить программу».
  • mov rdi, 22: В регистр RDI мы помещаем аргумент для функции завершения — код возврата. Это число, которое программа вернет системе.
  • syscall: Эта команда передает управление от вашей программы ядру операционной системы. Ядро смотрит в RAX, видит там 60 и понимает: «Нужно закрыть эту программу с кодом из RDI».
  • Код возврата обычно находится в диапазоне:

    где — код возврата, означает успешное завершение, а числа от до обычно сигнализируют о различных ошибках.

    Процесс создания исполняемого файла

    Код на ассемблере не запускается напрямую. Он проходит два этапа превращения.

    !Цепочка превращения исходного кода в работающую программу

    1. Компиляция (Ассемблирование)

    NASM превращает текстовый файл .asm в объектный файл .o. Объектный файл содержит машинный код, но в нем еще не расставлены финальные адреса системных библиотек.

    Команда для Linux:

    * -f elf64: указывает формат файла (Executable and Linkable Format, 64-bit). * -o hello.o: имя выходного файла.

    2. Компоновка (Линковка)

    Компоновщик (Linker, обычно ld) берет объектный файл и создает готовый к запуску файл, связывая его с системными вызовами.

    Команда для Linux:

    Теперь в папке появится файл hello (без расширения), который можно запустить.

    3. Запуск и проверка

    Запустите программу:

    Программа ничего не выведет на экран, так как мы не просили её печатать. Но мы можем проверить код возврата. В Linux код возврата последней программы хранится в переменной ? nasm global _start section .text _start: mov rax, 22 ; Код возврата ret ; Возврат управления (работает не во всех окружениях) bash nasm -f win64 hello.asm -o hello.obj nasm global _start section .text _start: mov rax, 0x2000001 ; 0x2000001 - exit на macOS mov rdi, 22 syscall bash nasm -f macho64 hello.asm -o hello.o ld hello.o -o hello -lSystem -syslibroot xcrun -sdk macosx --show-sdk-path `

    Итоги

  • Программа на ассемблере обязательно содержит секцию .text (код) и директиву global для точки входа.
  • Метка (_start:`) — это имя адреса памяти, с которого начинается выполнение.
  • Процесс создания программы состоит из двух шагов: компиляция (NASM создает объектный файл) и компоновка (Linker создает исполняемый файл).
  • Для завершения программы используется системный вызов (syscall) или инструкция возврата, передающая код завершения (например, 0 для успеха).