Files
2026-09-29 21:22:14 +03:00

58 KiB
Raw Permalink Blame History

Методичка по языку ассемблера x86-64

Семинар 1: Окружение, инструменты, первая программа

Содержание

  1. Необходимые инструменты
  2. Командная оболочка и окружение выполнения
  3. Регулярные выражения и grep
  4. Переменные окружения
  5. Модель памяти процесса x86-64
  6. Цикл выполнения инструкций
  7. Компиляция программ на ассемблере
  8. Разбор программы Hello World
  9. Системные вызовы Linux x86-64
  10. Регистры процессора x86-64
  11. Отладчик GDB
  12. Типичные ошибки и их решение
  13. Практические задания
  14. Справочник: часто используемые конструкции
  15. Полезные ссылки и документация

1. Необходимые инструменты

Базовый набор инструментов

Для работы с ассемблером x86-64 в Linux потребуются:

  • NASM (Netwide Assembler) — ассемблер, переводящий мнемоники в машинный код
  • ld (GNU linker) — компоновщик, создающий исполняемые файлы из объектных файлов
  • GDB (GNU Debugger) — отладчик для пошагового выполнения и анализа программ
  • make — утилита для автоматизации сборки
  • grep, sed, awk — инструменты обработки текста

Установка в Ubuntu/Debian

sudo apt update
sudo apt install nasm gdb build-essential

Установка в Fedora/RHEL

sudo dnf install nasm gdb make gcc

Проверка установки

nasm -version    # Должна показать версию NASM
ld --version     # Должна показать версию GNU ld
gdb --version    # Должна показать версию GDB

Виртуальные машины

Если вы работаете на Windows или macOS, рекомендуется использовать виртуальную машину с Linux:

  • VirtualBox + Ubuntu 22.04 LTS
  • VMware Workstation Player + Fedora
  • WSL2 (Windows Subsystem for Linux) — для Windows 10/11

Важно: Убедитесь, что виртуализация включена в BIOS/UEFI, и что VM имеет доступ к 64-битным инструкциям процессора.


2. Командная оболочка и окружение выполнения

Стандартные потоки ввода-вывода

Каждый процесс в Unix/Linux имеет три предопределённых потока:

Поток Дескриптор Назначение Обычное устройство
stdin 0 Стандартный ввод Клавиатура
stdout 1 Стандартный вывод Терминал
stderr 2 Стандартный поток ошибок Терминал

Перенаправление потоков

# Перенаправление stdout в файл (перезапись)
./program > output.txt

# Перенаправление stdout в файл (добавление)
./program >> output.txt

# Перенаправление stderr в файл
./program 2> errors.txt

# Перенаправление и stdout, и stderr в один файл
./program > all.txt 2>&1

# Перенаправление stdin из файла
./program < input.txt

# Конвейер (pipe): stdout первой программы → stdin второй
./program1 | ./program2

Пример: разделение потоков

Создайте тестовый скрипт test.sh:

#!/bin/bash
echo "Это сообщение в stdout"
echo "Это сообщение в stderr" >&2

Запустите с перенаправлением:

chmod +x test.sh
./test.sh > stdout.txt 2> stderr.txt
cat stdout.txt  # Покажет только stdout
cat stderr.txt  # Покажет только stderr

Диаграмма: процесс и стандартные потоки

graph LR
    A[Клавиатура] -->|stdin fd=0| B[Процесс]
    B -->|stdout fd=1| C[Терминал]
    B -->|stderr fd=2| C
    B -->|write fd=3| D[Файл data.txt]

3. Регулярные выражения и grep

Основные метасимволы

Символ Значение Пример Что найдёт
. Любой символ a.c abc, a5c, a c
* 0 или более повторений ab*c ac, abc, abbc
+ 1 или более повторений ab+c abc, abbc (но не ac)
? 0 или 1 повторение ab?c ac, abc
^ Начало строки ^Hello строки, начинающиеся с Hello
$ Конец строки world$ строки, заканчивающиеся на world
[] Класс символов [abc] a, b или c
[^] Отрицание класса [^0-9] любой нецифровой символ
| Альтернатива (или) cat|dog cat или dog
() Группировка (ab)+ ab, abab, ababab

Примеры использования grep

# Поиск строк, содержащих слово "error" (регистронезависимо)
grep -i "error" /var/log/syslog

# Поиск строк, начинающихся с цифры
grep "^[0-9]" file.txt

# Поиск email-адресов (упрощённый паттерн)
grep -E "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}" file.txt

# Показать номера строк
grep -n "function" program.c

# Инвертированный поиск (строки БЕЗ паттерна)
grep -v "^#" config.conf

# Рекурсивный поиск в директории
grep -r "TODO" ./src/

# Подсчёт совпадений
grep -c "error" logfile.txt

Расширенные регулярные выражения (ERE)

Для использования расширенного синтаксиса добавьте флаг -E:

# Найти числа от 100 до 199
grep -E "1[0-9]{2}" file.txt

# Найти IP-адреса (упрощённый паттерн)
grep -E "([0-9]{1,3}\.){3}[0-9]{1,3}" file.txt

# Найти строки с 3 или более повторяющимися буквами
grep -E "([a-z])\1{2,}" file.txt

4. Переменные окружения

Что такое переменные окружения?

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

Просмотр переменных окружения

# Показать все переменные окружения
env

# Показать конкретную переменную
echo $PATH
echo $HOME
echo $USER

# Показать все переменные, включая локальные переменные оболочки
set

Важные переменные окружения

Переменная Назначение Пример значения
PATH Список директорий для поиска исполняемых файлов /usr/local/bin:/usr/bin:/bin
HOME Домашняя директория пользователя /home/username
USER Имя текущего пользователя username
SHELL Путь к текущей оболочке /bin/bash
PWD Текущая рабочая директория /home/username/projects
LANG Локаль системы en_US.UTF-8
LD_LIBRARY_PATH Пути поиска динамических библиотек /usr/local/lib:/usr/lib

Установка и экспорт переменных

# Локальная переменная (только в текущей оболочке)
MY_VAR="hello"

# Экспорт переменной (доступна дочерним процессам)
export MY_VAR="hello"

# Одной командой
export MY_VAR="hello"

# Временная переменная только для одной команды
MY_VAR="hello" ./program

# Добавление в PATH
export PATH="$PATH:/new/directory"

Постоянные переменные окружения

Для постоянного сохранения переменных добавьте их в конфигурационные файлы:

  • ~/.bashrc — для bash (интерактивные non-login сессии)
  • ~/.bash_profile или ~/.profile — для login сессий
  • ~/.zshrc — для zsh

Пример в ~/.bashrc:

export MY_PROJECT_PATH="$HOME/projects/asm"
export PATH="$PATH:$MY_PROJECT_PATH/bin"

После изменения выполните:

source ~/.bashrc

5. Модель памяти процесса x86-64

Адресное пространство процесса

В x86-64 Linux каждый процесс имеет виртуальное адресное пространство размером до 2^48 байт (256 ТБ в теории, практически меньше). Память организована в несколько сегментов:

graph TD
    A[0x00007FFFFFFFFFFF<br/>Высокие адреса] --> B[Stack<br/>Стек растёт вниз]
    B --> C[...]
    C --> D[Memory Mapping<br/>mmap, разделяемые библиотеки]
    D --> E[...]
    E --> F[Heap<br/>Куча растёт вверх]
    F --> G[BSS<br/>Неинициализированные данные]
    G --> H[Data<br/>Инициализированные данные]
    H --> I[Text<br/>Код программы read-only]
    I --> J[0x0000000000400000<br/>Низкие адреса]

Секции исполняемого файла ELF

Секция Права доступа Содержимое Размер определяется
.text r-x Машинный код программы На этапе компиляции
.rodata r-- Константы, строковые литералы На этапе компиляции
.data rw- Инициализированные глобальные переменные На этапе компиляции
.bss rw- Неинициализированные глобальные переменные На этапе компиляции (обнуляется при загрузке)
stack rw- Локальные переменные, адреса возврата Динамически (растёт при вызовах функций)
heap rw- Динамически выделяемая память (malloc) Динамически (по запросу программы)

Визуализация секций программы

graph LR
    A[hello.asm исходник] --> B[NASM ассемблер]
    B --> C[hello.o объектный файл]
    C --> D[ld компоновщик]
    D --> E[hello исполняемый ELF]
    
    subgraph "Секции в памяти при запуске"
    F[.text: инструкции]
    G[.data: msg + len]
    H[.bss: пусто]
    I[stack: начальный фрейм]
    end

Пример: просмотр секций программы

# Скомпилируйте hello.asm
nasm -f elf64 hello.asm
ld -o hello hello.o

# Просмотр секций
readelf -S hello

# Просмотр размеров секций
size hello

# Дизассемблирование секции .text
objdump -d -M intel hello

Типичный вывод size:

   text    data     bss     dec     hex filename
    181      13       0     194      c2 hello
  • text (181 байт) — код программы
  • data (13 байт) — строка "Hello, World!\n"
  • bss (0 байт) — нет неинициализированных данных

Пример объявления секций в NASM

section .data          ; Инициализированные данные
    msg db "Hello", 0xA
    len equ $ - msg
    counter dq 42      ; 64-битное число

section .bss           ; Неинициализированные данные
    buffer resb 100    ; Резерв 100 байт
    number resq 1      ; Резерв 8 байт (qword)

section .text          ; Код программы
    global _start

_start:
    ; Ваш код здесь

Важное замечание о Stack и Heap

  • Stack (стек) используется для:

    • Локальных переменных функций
    • Адресов возврата из функций
    • Сохранённых значений регистров
    • Аргументов функций (сверх 6 аргументов в System V ABI)
  • Heap (куча) используется для:

    • Динамического выделения памяти (malloc, mmap)
    • Больших структур данных, размер которых не известен на этапе компиляции
    • Данных, которые должны жить дольше, чем функция, создавшая их

В ассемблере вы обычно работаете только со стеком (через регистры rsp, rbp) и статическими секциями (.data, .bss). Для работы с кучей нужно вызывать системные вызовы brk/mmap или библиотечные функции C.


6. Цикл выполнения инструкций

Fetch-Decode-Execute Cycle

Процессор выполняет программу в бесконечном цикле из трёх фаз:

graph LR
    A[FETCH<br/>Загрузить инструкцию<br/>из памяти по адресу RIP] --> B[DECODE<br/>Декодировать<br/>опкод и операнды]
    B --> C[EXECUTE<br/>Выполнить операцию<br/>обновить регистры/память]
    C --> D[Увеличить RIP<br/>на длину инструкции]
    D --> A

Детальное объяснение фаз

  1. FETCH (Выборка)

    • Процессор читает байты инструкции из памяти по адресу, на который указывает регистр RIP (Instruction Pointer)
    • В x86-64 инструкции имеют переменную длину: от 1 до 15 байт
    • Процессор использует кэш инструкций (I-cache) для ускорения выборки
  2. DECODE (Декодирование)

    • Декодер разбирает байты инструкции:
      • Префиксы (опционально)
      • Опкод (код операции)
      • ModR/M байт (режим адресации и операнды)
      • SIB байт (scale-index-base, для сложной адресации)
      • Смещение и immediate-значения
    • Определяет, какие исполнительные блоки нужны (ALU, FPU, Load/Store Unit)
  3. EXECUTE (Выполнение)

    • Исполнительные блоки выполняют операцию:
      • Арифметико-логическое устройство (ALU) для вычислений
      • Блок загрузки/сохранения для операций с памятью
      • Блок управления потоком для переходов
    • Обновляются регистры назначения и флаги
  4. Обновление RIP

    • RIP увеличивается на длину выполненной инструкции
    • Для инструкций перехода (jump, call, ret) RIP устанавливается в целевой адрес
    • Для условных переходов RIP изменяется только если условие истинно

Пример: пошаговое выполнение

Рассмотрим последовательность инструкций:

mov rax, 5      ; Загрузить 5 в rax
add rax, 3      ; Добавить 3 к rax
mov rbx, rax    ; Скопировать rax в rbx

Предположим, код загружен по адресу 0x401000.

Цикл RIP Инструкция FETCH DECODE EXECUTE Результат
1 0x401000 mov rax, 5 Читает 7 байт Опкод 48 C7 C0 05 00 00 00 rax ← 5 rax=5, RIP=0x401007
2 0x401007 add rax, 3 Читает 4 байта Опкод 48 83 C0 03 rax ← rax + 3 rax=8, RIP=0x40100B
3 0x40100B mov rbx, rax Читает 3 байта Опкод 48 89 C3 rbx ← rax rbx=8, RIP=0x40100E

Современные оптимизации

Современные процессоры x86-64 (Intel Core, AMD Ryzen) используют:

  • Конвейеризацию (Pipelining): несколько инструкций находятся на разных стадиях одновременно
  • Суперскалярность: несколько исполнительных блоков выполняют инструкции параллельно
  • Out-of-Order Execution: инструкции выполняются не по порядку, если это безопасно
  • Предсказание переходов (Branch Prediction): процессор угадывает, будет ли выполнен условный переход

Однако логическая модель остаётся последовательной: программа ведёт себя так, как будто инструкции выполняются строго по порядку.


7. Компиляция программ на ассемблере

Двухэтапный процесс сборки

graph LR
    A[hello.asm<br/>Исходный код] --> B[nasm]
    B --> C[hello.o<br/>Объектный файл<br/>машинный код + метаданные]
    C --> D[ld]
    D --> E[hello<br/>Исполняемый файл ELF]

Этап 1: Ассемблирование (NASM)

nasm -f elf64 hello.asm -o hello.o

Опции:

  • -f elf64 — формат вывода: ELF 64-bit (для Linux x86-64)
  • -o hello.o — имя выходного объектного файла
  • -g — добавить отладочную информацию (для GDB)
  • -F dwarf — формат отладочной информации DWARF
  • -l hello.lst — создать листинг (listing) файл с адресами и машинными кодами

Пример с отладочной информацией:

nasm -f elf64 -g -F dwarf hello.asm -o hello.o

Что делает NASM:

  1. Разбирает синтаксис исходного кода
  2. Преобразует мнемоники в опкоды (например, mov rax, 1 → 48 C7 C0 01 00 00 00)
  3. Разрешает метки (заменяет имена меток на адреса или смещения)
  4. Создаёт объектный файл в формате ELF с секциями .text, .data, .bss
  5. Добавляет таблицу символов и релокации (для последующей компоновки)

Этап 2: Компоновка (ld)

ld -o hello hello.o

Опции:

  • -o hello — имя выходного исполняемого файла
  • -m elf_x86_64 — целевая платформа (обычно определяется автоматически)
  • -dynamic-linker /lib64/ld-linux-x86-64.so.2 — для динамической линковки с библиотеками C
  • -lc — линковка с libc (если используете функции C)
  • -e _start — точка входа (по умолчанию _start)

Что делает ld:

  1. Объединяет секции из всех объектных файлов (если их несколько)
  2. Разрешает внешние символы (например, вызовы функций из других файлов)
  3. Выполняет релокацию (заменяет временные адреса на окончательные)
  4. Создаёт заголовок ELF и таблицы программных заголовков
  5. Записывает исполняемый файл

Проверка результата

# Проверить тип файла
file hello
# Вывод: hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV)...

# Сделать исполняемым (если ld не установил права)
chmod +x hello

# Запустить
./hello

Makefile для автоматизации

Создайте файл Makefile:

# Переменные
ASM = nasm
LD = ld
ASMFLAGS = -f elf64 -g -F dwarf
LDFLAGS =

# Цель по умолчанию
all: hello

# Правило для объектных файлов
%.o: %.asm
	$(ASM) $(ASMFLAGS) $< -o $@

# Правило для исполняемого файла
hello: hello.o
	$(LD) $(LDFLAGS) $< -o $@

# Очистка
clean:
	rm -f *.o hello

# Запуск
run: hello
	./hello

# Отладка
debug: hello
	gdb ./hello

.PHONY: all clean run debug

Использование:

make          # Собрать программу
make run      # Собрать и запустить
make debug    # Собрать и запустить в GDB
make clean    # Удалить сгенерированные файлы

8. Разбор программы Hello World

Полный исходный код

section .data
    msg db "Hello, World!", 0xA    ; Строка с переводом строки (0xA = '\n')
    len equ $ - msg                ; Вычислить длину строки

section .text
    global _start

_start:
    ; Системный вызов write(1, msg, len)
    mov rax, 1          ; syscall номер: sys_write
    mov rdi, 1          ; arg1: file descriptor (stdout)
    mov rsi, msg        ; arg2: указатель на буфер
    mov rdx, len        ; arg3: количество байт
    syscall             ; Выполнить системный вызов

    ; Системный вызов exit(0)
    mov rax, 60         ; syscall номер: sys_exit
    xor rdi, rdi        ; arg1: exit code = 0
    syscall             ; Выполнить системный вызов

Пошаговый разбор секции .data

section .data
  • Директива секции: все последующие объявления будут помещены в секцию .data (инициализированные данные)
    msg db "Hello, World!", 0xA
  • msg — метка (адрес начала строки)
  • db (define byte) — директива объявления байтов
  • "Hello, World!" — ASCII-строка (13 байт)
  • 0xA — шестнадцатеричное значение 10 (код символа \n — перевод строки)
  • Итого: 14 байт данных
    len equ $ - msg
  • len — константа времени ассемблирования (не выделяет память)
  • equ — директива определения константы
  • $ — текущий адрес (адрес сразу после строки)
  • $ - msg — разность адресов = длина строки
  • Результат: len = 14

Пошаговый разбор секции .text

section .text
  • Секция кода (исполняемая, read-only)
    global _start
  • Объявляет символ _start глобальным (видимым компоновщику)
  • Компоновщик ld использует _start как точку входа по умолчанию
_start:
  • Метка: адрес первой инструкции программы

Системный вызов write

    mov rax, 1          ; syscall номер: sys_write
    mov rdi, 1          ; arg1: file descriptor (stdout)
    mov rsi, msg        ; arg2: указатель на буфер
    mov rdx, len        ; arg3: количество байт
    syscall             ; Выполнить системный вызов

Регистры для системных вызовов (System V ABI):

Регистр Назначение В данном примере
rax Номер системного вызова 1 (sys_write)
rdi 1-й аргумент 1 (stdout)
rsi 2-й аргумент Адрес строки msg
rdx 3-й аргумент Длина строки 14
r10 4-й аргумент —
r8 5-й аргумент —
r9 6-й аргумент —

После syscall:

  • Управление передаётся ядру Linux
  • Ядро выполняет sys_write(1, msg, 14)
  • Строка "Hello, World!\n" выводится в терминал
  • Управление возвращается в программу
  • rax содержит результат (количество записанных байт или код ошибки)

Системный вызов exit

    mov rax, 60         ; syscall номер: sys_exit
    xor rdi, rdi        ; arg1: exit code = 0
    syscall             ; Выполнить системный вызов
  • sys_exit(0) завершает процесс с кодом возврата 0 (успех)
  • xor rdi, rdi — идиоматический способ обнулить регистр (быстрее и короче, чем mov rdi, 0)

Проверка кода возврата в bash:

./hello
echo $?    # Выведет 0

Таблица инструкций с машинными кодами

Адрес (пример) Машинный код Инструкция Описание
0x401000 48 C7 C0 01 00 00 00 mov rax, 1 Загрузить 1 в rax
0x401007 48 C7 C7 01 00 00 00 mov rdi, 1 Загрузить 1 в rdi
0x40100E 48 BE xx xx xx xx xx xx xx xx mov rsi, msg Загрузить адрес msg в rsi
0x401018 48 C7 C2 0E 00 00 00 mov rdx, 14 Загрузить 14 в rdx
0x40101F 0F 05 syscall Системный вызов
0x401021 48 C7 C0 3C 00 00 00 mov rax, 60 Загрузить 60 в rax
0x401028 48 31 FF xor rdi, rdi Обнулить rdi
0x40102B 0F 05 syscall Системный вызов

Обратите внимание:

  • Инструкция syscall кодируется всего двумя байтами: 0F 05
  • mov с 64-битным immediate занимает 10 байт (префикс REX.W 48 + опкод + 8 байт данных)
  • xor rdi, rdi занимает всего 3 байта

Эксперимент: изменение строки

Попробуйте изменить программу:

section .data
    msg db "Hello, Assembly!", 0xA, 0    ; Добавлен null-terminator (для совместимости с C)
    len equ $ - msg - 1                   ; Не считаем null-terminator

section .text
    global _start

_start:
    mov rax, 1
    mov rdi, 1
    mov rsi, msg
    mov rdx, len
    syscall

    mov rax, 60
    xor rdi, rdi
    syscall

Соберите и запустите:

nasm -f elf64 hello.asm && ld -o hello hello.o && ./hello

9. Системные вызовы Linux x86-64

Что такое системный вызов?

Системный вызов (syscall) — это интерфейс между программой в пользовательском пространстве (user space) и ядром операционной системы (kernel space). Это единственный легальный способ для программы:

  • Взаимодействовать с устройствами ввода-вывода
  • Работать с файловой системой
  • Управлять процессами и потоками
  • Выделять память
  • Взаимодействовать с сетью

Переход в режим ядра

sequenceDiagram
    participant U as User Space<br/>(программа)
    participant C as CPU
    participant K as Kernel Space<br/>(ядро Linux)
    
    U->>C: syscall (0x0F 0x05)
    Note over C: Сохранить контекст
    C->>K: Передать управление<br/>Ring 3 → Ring 0
    Note over K: Проверить права<br/>Выполнить операцию
    K->>C: Вернуть результат в RAX
    Note over C: Восстановить контекст
    C->>U: Продолжить выполнение

**Кольца защиты (Protection Rings):**
- **Ring 0** (kernel mode): полный доступ ко всему оборудованию, выполнение привилегированных инструкций
- **Ring 3** (user mode): ограниченный доступ, не может напрямую взаимодействовать с устройствами

### Механизм вызова

**Инструкция `syscall` (x86-64):**
```nasm
syscall    ; 0x0F 0x05 в машинном коде

Передача параметров:

Параметр Регистр Описание
Номер syscall rax Идентификатор системного вызова (0-331+)
Аргумент 1 rdi Первый параметр
Аргумент 2 rsi Второй параметр
Аргумент 3 rdx Третий параметр
Аргумент 4 r10 Четвёртый параметр (не rcx!)
Аргумент 5 r8 Пятый параметр
Аргумент 6 r9 Шестой параметр
Возвращаемое значение rax Результат выполнения

Важно: в отличие от calling convention функций, системные вызовы используют r10 вместо rcx для 4-го аргумента (rcx используется ядром для сохранения return address).

Основные системные вызовы

1. read (номер 0)

Чтение данных из файлового дескриптора.

mov rax, 0          ; syscall номер: read
mov rdi, 0          ; fd = 0 (stdin)
mov rsi, buffer     ; указатель на буфер
mov rdx, 128        ; максимальное количество байт для чтения
syscall
; rax содержит количество прочитанных байт (или -errno при ошибке)

2. write (номер 1)

Запись данных в файловый дескриптор.

mov rax, 1          ; syscall номер: write
mov rdi, 1          ; fd = 1 (stdout)
mov rsi, message    ; указатель на данные
mov rdx, 13         ; количество байт для записи
syscall
; rax содержит количество записанных байт

3. exit (номер 60)

Завершение процесса.

mov rax, 60         ; syscall номер: exit
mov rdi, 0          ; код возврата (0 = успех)
syscall
; не возвращается

Стандартные файловые дескрипторы

Номер Имя Назначение
0 stdin Стандартный ввод
1 stdout Стандартный вывод
2 stderr Стандартный вывод ошибок

Коды ошибок

При ошибке системный вызов возвращает отрицательное значение -errno:

Код Макрос Описание
-1 EPERM Операция не разрешена
-2 ENOENT Файл или каталог не существует
-9 EBADF Неверный файловый дескриптор
-14 EFAULT Неверный адрес памяти
-22 EINVAL Неверный аргумент

Проверка ошибок:

syscall
test rax, rax       ; Проверить знак результата
js .error           ; Если отрицательное → ошибка
; Успех
jmp .continue

.error:
neg rax             ; Преобразовать -errno в errno
; rax содержит код ошибки (положительный)

Полная таблица системных вызовов

Смотрите docs/system-calls.md или https://filippo.io/linux-syscall-table/

Популярные системные вызовы:

  • 0: read
  • 1: write
  • 2: open
  • 3: close
  • 9: mmap (выделение памяти)
  • 12: brk (изменение размера кучи)
  • 39: getpid
  • 57: fork
  • 59: execve
  • 60: exit

10. Регистры общего назначения

Иерархия регистров

В x86-64 регистры имеют многоуровневую структуру для обратной совместимости:

graph TD
    A[RAX 64 бит<br/>0x0123456789ABCDEF] --> B[EAX 32 бит младших<br/>0x89ABCDEF]
    B --> C[AX 16 бит младших<br/>0xCDEF]
    C --> D[AH 8 бит старших<br/>0xCD]
    C --> E[AL 8 бит младших<br/>0xEF]
    
    style A fill:#4ecdc4
    style B fill:#95e1d3
    style C fill:#f38181
    style D fill:#ffd93d
    style E fill:#6bcf7f

Полный список регистров общего назначения

64-бит 32-бит 16-бит 8-бит (low) 8-бит (high) Назначение в calling convention
rax eax ax al ah Возвращаемое значение, accumulator
rbx ebx bx bl bh Callee-saved, base
rcx ecx cx cl ch 4-й аргумент, counter
rdx edx dx dl dh 3-й аргумент, data
rsi esi si sil — 2-й аргумент, source index
rdi edi di dil — 1-й аргумент, destination index
rsp esp sp spl — Stack pointer (указатель стека)
rbp ebp bp bpl — Base pointer (база фрейма)
r8 r8d r8w r8b — 5-й аргумент
r9 r9d r9w r9b — 6-й аргумент
r10 r10d r10w r10b — Caller-saved
r11 r11d r11w r11b — Caller-saved
r12 r12d r12w r12b — Callee-saved
r13 r13d r13w r13b — Callee-saved
r14 r14d r14w r14b — Callee-saved
r15 r15d r15w r15b — Callee-saved

Важно: запись в младшую 32-битную часть (eax, ebx, ...) обнуляет старшие 32 бита регистра!

mov rax, 0x123456789ABCDEF  ; rax = 0x123456789ABCDEF
mov eax, 0x11111111         ; rax = 0x0000000011111111 (старшие биты обнулены!)

Специальные регистры

rip (Instruction Pointer):

  • Указывает на адрес следующей инструкции
  • Изменяется автоматически при выполнении
  • Можно изменить через jmp, call, ret
  • Используется для RIP-relative адресации

rflags (Флаги процессора):

  • 64-битный регистр состояния
  • Содержит флаги результата операций (ZF, SF, CF, OF, ...)
  • Изменяется автоматически арифметическими и логическими операциями

Примеры использования разных размеров

; Работа с байтами
mov al, 0xFF        ; al = 255, остальная часть rax не изменяется
mov ah, 0x12        ; ah = 18, al и остальное не изменяется

; Работа с 16-битными значениями
mov ax, 0x1234      ; ax = 0x1234, старшие 48 бит rax не изменяются

; 32-битная запись обнуляет старшие биты
mov rax, -1         ; rax = 0xFFFFFFFFFFFFFFFF (все биты = 1)
mov eax, 0          ; rax = 0x0000000000000000 (старшие 32 бита обнулились!)

; 8 и 16-битная запись НЕ обнуляет старшие биты
mov rax, -1         ; rax = 0xFFFFFFFFFFFFFFFF
mov al, 0           ; rax = 0xFFFFFFFFFFFFFF00 (только младший байт обнулён)

11. Арифметические и логические операции

Арифметические инструкции

Инструкция Описание Пример Эффект
add dst, src Сложение add rax, rbx rax = rax + rbx
sub dst, src Вычитание sub rax, 10 rax = rax - 10
inc dst Инкремент (+1) inc rcx rcx = rcx + 1
dec dst Декремент (-1) dec rdx rdx = rdx - 1
imul dst, src Знаковое умножение imul rax, rbx rax = rax * rbx
mul src Беззнаковое умножение mul rbx rdx:rax = rax * rbx
idiv src Знаковое деление idiv rbx rax = rdx:rax / rbx, rdx = остаток
div src Беззнаковое деление div rcx rax = rdx:rax / rcx, rdx = остаток
neg dst Смена знака neg rax rax = -rax

Примечание о умножении/делении:

  • mul/div работают с парой rdx:rax (128-битный результат/делимое)
  • imul с двумя операндами работает как обычная операция
  • Перед делением нужно расширить делимое: cqo (sign-extend rax → rdx:rax)
; Умножение: результат может не поместиться в 64 бита
mov rax, 0x8000000000000000
mov rbx, 2
mul rbx             ; rdx:rax = rax * rbx
                    ; rax = 0, rdx = 1 (результат = 2^64)

; Деление: делимое в rdx:rax
mov rax, 100        ; Делимое = 100
cqo                 ; Расширить rax в rdx:rax (rdx = 0 или -1)
mov rbx, 7
idiv rbx            ; rax = 100 / 7 = 14, rdx = 100 % 7 = 2

Логические операции

Инструкция Описание Пример Эффект
and dst, src Логическое И and rax, 0xFF rax = rax & 0xFF
or dst, src Логическое ИЛИ or rax, rbx rax = rax | rbx
xor dst, src Исключающее ИЛИ xor rax, rax rax = 0 (обнуление)
not dst Логическое НЕ not rax rax = ~rax (инверсия)
test op1, op2 Тест (AND без записи) test rax, rax Установить флаги по rax & rax
cmp op1, op2 Сравнение (SUB без записи) cmp rax, 10 Установить флаги по rax - 10

Популярные идиомы:

; Обнуление регистра (быстрее mov rax, 0)
xor rax, rax        ; rax = 0, обнуляет CF и OF

; Проверка на ноль
test rax, rax       ; ZF = 1 если rax == 0
jz .is_zero

; Извлечение младшего байта
and rax, 0xFF       ; Обнулить все биты кроме младших 8

; Установка бита
or rax, 0x80        ; Установить бит 7

; Сброс бита
and rax, ~0x80      ; Сбросить бит 7

; Инверсия бита
xor rax, 0x80       ; Инвертировать бит 7

Сдвиги и вращения

Инструкция Описание Пример Эффект
shl dst, count Логический сдвиг влево shl rax, 3 rax = rax << 3 (× 8)
shr dst, count Логический сдвиг вправо shr rax, 2 rax = rax >> 2 (÷ 4, беззнаковое)
sal dst, count Арифметический сдвиг влево sal rax, 1 То же что shl
sar dst, count Арифметический сдвиг вправо sar rax, 1 rax = rax >> 1 (÷ 2, знаковое)
rol dst, count Циклический сдвиг влево rol rax, 8 Биты циклически влево
ror dst, count Циклический сдвиг вправо ror rax, 4 Биты циклически вправо

Различие между shr и sar:

; Беззнаковый сдвиг (shr)
mov rax, -8         ; rax = 0xFFFFFFFFFFFFFFF8
shr rax, 1          ; rax = 0x7FFFFFFFFFFFFFFC (= 9223372036854775804)

; Знаковый сдвиг (sar) сохраняет знаковый бит
mov rax, -8         ; rax = 0xFFFFFFFFFFFFFFF8 (= -8)
sar rax, 1          ; rax = 0xFFFFFFFFFFFFFFFC (= -4, знак сохранён!)

Быстрое умножение/деление на степени двойки:

; Умножить на 8 (2^3)
shl rax, 3          ; Быстрее чем imul rax, 8

; Разделить на 4 (2^2)
shr rax, 2          ; Для беззнакового
sar rax, 2          ; Для знакового (сохранит знак)

12. Флаги процессора и условные переходы

Регистр RFLAGS

Основные флаги (биты регистра rflags):

Флаг Бит Имя Устанавливается когда
CF 0 Carry Flag Перенос при сложении / заём при вычитании (unsigned)
PF 2 Parity Flag Чётное количество единиц в младшем байте
ZF 6 Zero Flag Результат равен нулю
SF 7 Sign Flag Результат отрицательный (старший бит = 1)
OF 11 Overflow Flag Переполнение при знаковых операциях

Как устанавливаются флаги

; Арифметические операции устанавливают флаги
mov rax, 5
sub rax, 5          ; ZF = 1 (результат 0)

mov rax, 10
sub rax, 20         ; SF = 1 (результат отрицательный)
                    ; CF = 1 (заём при unsigned вычитании)

; test и cmp устанавливают флаги без изменения операндов
test rax, rax       ; Выполнить rax & rax, установить флаги
                    ; ZF = 1 если rax == 0

cmp rax, 10         ; Выполнить rax - 10, установить флаги
                    ; ZF = 1 если rax == 10
                    ; CF = 1 если rax < 10 (unsigned)

Условные переходы

После сравнения двух значений (cmp a, b):

Условие Unsigned Signed Проверяемые флаги
a == b je (jump if equal) je ZF = 1
a != b jne (not equal) jne ZF = 0
a < b jb (below) jl (less) CF = 1 / SF ≠ OF
a <= b jbe (below/equal) jle (less/equal) CF = 1 or ZF = 1 / ZF = 1 or SF ≠ OF
a > b ja (above) jg (greater) CF = 0 and ZF = 0 / ZF = 0 and SF = OF
a >= b jae (above/equal) jge (greater/equal) CF = 0 / SF = OF

Проверка отдельных флагов:

Инструкция Условие Флаги
jz / je Ноль / равно ZF = 1
jnz / jne Не ноль / не равно ZF = 0
js Знак отрицательный SF = 1
jns Знак положительный SF = 0
jc Перенос CF = 1
jnc Нет переноса CF = 0
jo Переполнение OF = 1
jno Нет переполнения OF = 0

Примеры использования

Проверка диапазона (0-9 для ASCII цифр):

; Проверить, что символ в al — цифра '0'-'9'
cmp al, '0'
jb .not_digit       ; Если al < '0' → не цифра (unsigned)
cmp al, '9'
ja .not_digit       ; Если al > '9' → не цифра (unsigned)
; Здесь: это цифра
jmp .is_digit

.not_digit:
; Обработка не-цифры

.is_digit:
; Обработка цифры

Знаковое сравнение:

; Проверить, что rax находится в диапазоне [-100, 100]
cmp rax, -100
jl .out_of_range    ; Если rax < -100 (signed)
cmp rax, 100
jg .out_of_range    ; Если rax > 100 (signed)
; Здесь: -100 <= rax <= 100

Цикл с счётчиком:

    mov rcx, 10     ; Счётчик = 10
.loop:
    ; Тело цикла
    
    dec rcx         ; Уменьшить счётчик
    jnz .loop       ; Повторить, пока rcx != 0
    
; Выход из цикла

13. Типичные ошибки начинающих

Ошибка 1: Забыли указать размер секции .bss

Неправильно:

section .bss
    buffer:         ; ❌ Не указан размер!

Правильно:

section .bss
    buffer: resb 128    ; ✓ Резервировать 128 байт
    counter: resq 1     ; ✓ Резервировать 1 qword (8 байт)

Ошибка 2: Использовали неправильный размер операндов

Неправильно:

mov rax, [buffer]       ; Загружает 8 байт
mov byte [buffer], rax  ; ❌ Пытается записать 8 байт в 1 байт!

Правильно:

mov al, [buffer]        ; ✓ Загрузить 1 байт
mov byte [buffer], al   ; ✓ Записать 1 байт

; Или явно указать размер
movzx rax, byte [buffer]  ; Загрузить байт с zero-extend

Ошибка 3: Забыли про обнуление старших битов при 32-битной записи

Проблема:

mov rax, 0xFFFFFFFFFFFFFFFF
mov eax, 0              ; ❌ rax станет 0, а не 0xFFFFFFFF00000000!

Объяснение: запись в 32-битную часть (eax) обнуляет старшие 32 бита автоматически.

Ошибка 4: Неправильно вычислили длину строки

Неправильно:

section .data
    msg: db "Hello", 10     ; Строка из 6 байт (5 букв + '\n')

section .text
    mov rdx, 5              ; ❌ Длина = 5, но должна быть 6!
    ; В результате '\n' не выведется

Правильно:

section .data
    msg: db "Hello", 10
    msg_len equ $ - msg     ; ✓ Автоматически вычислить длину

section .text
    mov rdx, msg_len        ; ✓ Правильная длина = 6

Ошибка 5: Перепутали signed и unsigned переходы

Неправильно:

; Проверка ASCII кода (беззнаковое значение)
cmp al, '0'
jl .not_digit           ; ❌ jl для signed сравнения!

Правильно:

; ASCII коды — беззнаковые
cmp al, '0'
jb .not_digit           ; ✓ jb (below) для unsigned

Напоминание:

  • jl/jg/jle/jge — для signed (знаковых) сравнений
  • jb/ja/jbe/jae — для unsigned (беззнаковых) сравнений

Ошибка 6: Не обработали ошибку системного вызова

Неправильно:

mov rax, 0
mov rdi, 0
mov rsi, buffer
mov rdx, 100
syscall
; ❌ Не проверили, что rax >= 0 (может быть -errno)
mov [bytes_read], rax

Правильно:

syscall
test rax, rax
js .read_error          ; ✓ Если отрицательное → ошибка

; Успех
mov [bytes_read], rax
jmp .continue

.read_error:
neg rax                 ; Преобразовать -errno в errno
; Обработать ошибку (rax содержит код ошибки)

Ошибка 7: Забыли про завершающий ноль в строках C

Проблема: при передаче строк в функции C (printf, strlen) они ожидают завершающий \0.

Неправильно:

section .data
    msg: db "Hello"         ; ❌ Нет '\0'!
    
extern strlen
    mov rdi, msg
    call strlen             ; strlen может читать за границу

Правильно:

section .data
    msg: db "Hello", 0      ; ✓ Завершающий ноль

14. Отладка с GDB

Основные команды

Запуск программы:

gdb ./program
(gdb) break _start          # Точка останова на метке
(gdb) run                   # Запустить программу

Пошаговое выполнение:

(gdb) stepi                 # Выполнить одну инструкцию (step into)
(gdb) nexti                 # Следующая инструкция (step over)
(gdb) continue              # Продолжить до следующего breakpoint
(gdb) finish                # Выполнить до выхода из функции

Просмотр состояния:

(gdb) info registers        # Показать все регистры
(gdb) info registers rax rbx rsp  # Конкретные регистры
(gdb) print $rax            # Вывести значение rax
(gdb) print/x $rax          # В hex формате
(gdb) print/t $rax          # В бинарном формате
(gdb) print/d $rax          # В десятичном (со знаком)

Просмотр памяти:

(gdb) x/10gx $rsp           # 10 qword в hex от адреса rsp
(gdb) x/20bx $rsi           # 20 байт в hex от адреса rsi
(gdb) x/s $rdi              # Строку (до '\0') по адресу rdi
(gdb) x/10i $rip            # 10 инструкций от текущего rip

Формат команды x (examine):

  • Формат: x/[count][format][size] address
  • count: количество единиц
  • format: o(octal), x(hex), d(decimal), u(unsigned), t(binary), i(instruction), c(char), s(string)
  • size: b(byte), h(halfword-2), w(word-4), g(giant-8)

Точки останова:

(gdb) break _start          # На метке
(gdb) break *0x401000       # На конкретном адресе
(gdb) info breakpoints      # Список всех breakpoints
(gdb) delete 1              # Удалить breakpoint №1
(gdb) disable 2             # Временно отключить breakpoint №2

Дизассемблирование:

(gdb) disassemble           # Дизассемблировать текущую функцию
(gdb) disassemble _start    # Конкретную метку
(gdb) set disassembly-flavor intel  # Использовать Intel синтаксис

Пример сессии отладки

$ gdb ./hello
(gdb) break _start
Breakpoint 1 at 0x401000

(gdb) run
Starting program: /path/to/hello

Breakpoint 1, 0x0000000000401000 in _start ()

(gdb) disassemble
   0x0000000000401000 <_start+0>:    mov    eax,0x1
   0x0000000000401005 <_start+5>:    mov    edi,0x1
=> 0x000000000040100a <_start+10>:   movabs rsi,0x402000
   ...

(gdb) info registers rax rdi rsi rdx
rax            0x0                 0
rdi            0x0                 0
rsi            0x0                 0
rdx            0x0                 0

(gdb) stepi
(gdb) info registers rax
rax            0x1                 1

(gdb) stepi
(gdb) info registers rdi
rdi            0x1                 1

(gdb) stepi
(gdb) info registers rsi
rsi            0x402000            4202496

(gdb) x/s $rsi
0x402000:       "hello, world!\n"

(gdb) continue
hello, world!
[Inferior 1 (process 12345) exited normally]

Полезные настройки

Файл ~/.gdbinit:

set disassembly-flavor intel
set pagination off
set print pretty on

# Показывать регистры после каждого шага
define hook-stop
    info registers rip rax rbx rcx rdx rsi rdi rsp rbp
end

15. Справочник

Директивы NASM

Директива Описание Пример
section .data Секция инициализированных данных
section .bss Секция неинициализированных данных
section .text Секция кода
global symbol Экспортировать символ global _start
extern symbol Импортировать внешний символ extern printf
equ Определить константу BUF_SIZE equ 128
%define Макроопределение %define STDOUT 1

Объявление данных

Директива Размер Описание Пример
db 1 байт Define byte db 0x41, 'A', 10
dw 2 байта Define word dw 0x1234
dd 4 байта Define double word dd 0x12345678
dq 8 байт Define quad word dq 0x123456789ABCDEF
resb 1 байт Reserve bytes resb 100
resw 2 байта Reserve words resw 50
resd 4 байта Reserve double words resd 25
resq 8 байт Reserve quad words resq 10

Системные вызовы (краткая таблица)

rax Имя rdi rsi rdx Описание
0 read fd buf* count Чтение
1 write fd buf* count Запись
2 open path* flags mode Открыть файл
3 close fd — — Закрыть файл
60 exit code — — Завершить процесс

Шаблон минимальной программы

; Минимальная программа hello world
section .data
    msg: db 'Hello, World!', 10
    msg_len equ $ - msg

section .text
global _start

_start:
    ; write(1, msg, msg_len)
    mov rax, 1
    mov rdi, 1
    mov rsi, msg
    mov rdx, msg_len
    syscall
    
    ; exit(0)
    mov rax, 60
    xor rdi, rdi
    syscall

Полезные ссылки


Конец методички для семинара 1