Введение в архитектуру x86, установка окружения и структура программы
Введение в архитектуру x86, установка окружения и структура программы
Добро пожаловать в курс «Основы программирования на ассемблере NASM». Это первая статья, с которой начнется ваше погружение в мир низкоуровневого программирования. Здесь нет магии, скрытой за абстракциями языков высокого уровня вроде Python или Java. Здесь есть только вы, процессор и память.
В этой статье мы разберем, как устроен компьютер с точки зрения программиста, установим необходимые инструменты и напишем нашу первую, пусть и простую, программу.
Что такое язык Ассемблера?
Язык ассемблера (Assembly language) — это человекочитаемое представление машинного кода. Компьютерный процессор не понимает слова «print» или «while». Он понимает только наборы нулей и единиц, которые представляют собой электрические сигналы. Эти наборы называются машинными инструкциями.
Ассемблер — это тонкая прослойка между машинным кодом и программистом. Каждая команда на ассемблере (например, mov или add) обычно соответствует ровно одной машинной инструкции процессора. Это дает вам полный контроль над оборудованием, но и возлагает на вас ответственность за каждый байт и каждый такт процессора.
Мы будем изучать NASM (Netwide Assembler). Это один из самых популярных, гибких и широко используемых ассемблеров для архитектуры x86.
Архитектура x86: Взгляд изнутри
Чтобы писать на ассемблере, нужно понимать, для чего мы пишем. Мы пишем команды для архитектуры x86 (и её 64-битного расширения x86-64). Это архитектура, на которой работают большинство современных персональных компьютеров и серверов (процессоры Intel и AMD).
Ключевые компоненты, с которыми нам предстоит работать:
- Процессор (CPU) — «мозг», выполняющий инструкции.
- Оперативная память (RAM) — «рабочий стол», где хранятся данные и код программы во время выполнения.
- Регистры — сверхбыстрая память, находящаяся прямо внутри процессора.
Регистры процессора
Регистры — это самое важное понятие для начала. Представьте, что оперативная память — это огромный склад, а регистры — это ваши руки. Чтобы что-то сделать с данными (сложить, сравнить), вы должны сначала взять их со склада (памяти) в руки (регистры).
В архитектуре x86 существует несколько типов регистров. Мы начнем с регистров общего назначения. В 32-битной архитектуре они имеют размер 32 бита (4 байта) и начинаются с буквы E (Extended). В 64-битной — начинаются с R и имеют размер 64 бита.
Для простоты понимания в этом курсе мы будем часто обращаться к 64-битным регистрам, так как это современный стандарт, но помнить об их 32-битных «предках».
Основные регистры общего назначения:
| Регистр (64-bit) | Регистр (32-bit) | Назначение |
|---|---|---|
| RAX | EAX | Аккумулятор. Используется для арифметики и возврата значений функций. |
| RBX | EBX | Базовый регистр. Часто используется как указатель на данные. |
| RCX | ECX | Счетчик. Используется в циклах. |
| RDX | EDX | Регистр данных. Используется при умножении/делении и вводе/выводе. |
| RSI | ESI | Индекс источника. Используется при копировании данных. |
| RDI | EDI | Индекс назначения. Используется при копировании данных. |
| RSP | ESP | Указатель стека. Указывает на вершину стека (о стеке поговорим позже). |
| RBP | EBP | Базовый указатель стека. Используется для навигации по стеку. |
Регистры — это самые быстрые ячейки памяти в компьютере. Оптимизация работы с ними — ключ к производительности.
Память и адресация
Память представляет собой огромный массив байтов. Каждый байт имеет свой уникальный адрес. Когда мы говорим процессору «прочитай данные», мы должны указать адрес этих данных.
Размер памяти измеряется в байтах. Связь между битами и байтами выражается формулой:
где — это один байт (минимальная адресуемая единица памяти), а — это восемь бит (нулей или единиц), из которых он состоит.
Установка окружения
Для работы нам понадобятся два инструмента:
- NASM — компилятор (ассемблер), который превратит наш текст в объектный файл.
- Linker (ld) — компоновщик, который превратит объектный файл в исполняемую программу.
Мы будем ориентироваться на работу в среде Linux, так как это «родная» среда для изучения системного программирования. Если у вас Windows, рекомендуется использовать WSL (Windows Subsystem for Linux) или виртуальную машину.
Установка в Linux (Ubuntu/Debian)
Откройте терминал и введите следующие команды:
sudo apt update
sudo apt install nasm build-essential
Пакет build-essential установит необходимые утилиты, включая компоновщик ld.
Проверьте установку:
nasm -v
Вы должны увидеть версию NASM.
Структура программы на NASM
Программа на ассемблере NASM имеет четкую структуру. Она делится на секции (sections). Секции помогают операционной системе понять, как обращаться с разными частями вашей программы.
Стандартная программа состоит из трех основных секций:
-
section .data Здесь объявляются инициализированные данные. Это переменные, которые имеют значение при запуске программы (константы, строки).
-
section .bss Здесь объявляются неинициализированные данные. Это место под переменные, значения которых мы пока не знаем, но резервируем под них память.
-
section .text Это самая важная секция. Здесь находится сам код программы.
Пример шаблона программы
section .data
; Сюда пишем переменные
section .bss
; Сюда пишем резервирование памяти
section .text
global _start ; Объявляем точку входа для линкера
_start:
; Здесь начинается выполнение программы
; ... код ...
Обратите внимание на global _start. Метка _start — это стандартное имя точки входа в программу для линкера ld в Linux. Директива global делает эту метку видимой извне, чтобы линкер мог её найти.
Первая программа: «Hello, World!»
Давайте напишем программу, которая выводит строку «Hello, World!» в терминал и корректно завершается. Мы будем использовать системные вызовы (syscalls) ядра Linux.
Создайте файл hello.asm и вставьте туда следующий код:
section .data
msg db "Hello, World!", 0xA ; Строка байтов. 0xA — это символ новой строки
len equ $ - msg ; Вычисляем длину строки автоматически
section .text
global _start
_start:
; 1. Вывод строки на экран
mov rax, 1 ; Номер системного вызова 'write' (1)
mov rdi, 1 ; Дескриптор файла: 1 = stdout (стандартный вывод)
mov rsi, msg ; Адрес строки, которую хотим вывести
mov rdx, len ; Количество байтов для вывода
syscall ; Просим ядро выполнить команду
; 2. Завершение программы
mov rax, 60 ; Номер системного вызова 'exit' (60)
mov rdi, 0 ; Код возврата (0 означает успех)
syscall ; Просим ядро выполнить команду
Разбор кода
db(define byte) — определяет последовательность байтов.0xA— это код символа переноса строки (\n).equ— определяет константу.- Формула длины строки:
где — длина строки (значение
len), — текущий адрес памяти (обозначается символом$), а — адрес начала строки (меткаmsg). Таким образом, мы вычитаем из текущего адреса адрес начала строки и получаем её длину. mov(move) — команда пересылки данных.mov rax, 1означает «поместить число 1 в регистр RAX».syscall— команда, передающая управление операционной системе для выполнения действия, заданного в регистре RAX.
Компиляция и запуск
Теперь превратим этот код в программу. Выполните в терминале:
-
Ассемблирование (создание объектного файла):
nasm -f elf64 hello.asm -o hello.oФлаг
-f elf64указывает формат файла (Executable and Linkable Format, 64-bit). -
Компоновка (создание исполняемого файла):
ld hello.o -o hello -
Запуск:
./hello
Если вы увидели надпись Hello, World!, поздравляю! Вы написали и запустили свою первую программу на ассемблере.
Заключение
Сегодня мы познакомились с фундаментом: архитектурой x86, регистрами и структурой программы на NASM. Мы узнали, что программа состоит из секций данных и кода, и научились использовать системные вызовы для общения с ОС.
В следующей статье мы углубимся в систему команд: научимся складывать, вычитать и управлять потоком выполнения программы.