From dc822077236cdf8bf439fbd5f1b14e9c4844677b Mon Sep 17 00:00:00 2001 From: AK Date: Tue, 29 Sep 2026 21:22:14 +0300 Subject: [PATCH] ProgLangs added. 29.09.26 --- ЯПы/Семинары/Семинар-1.md | 1511 ++++++++++++++++++++++++++ ЯПы/Семинары/Семинар-2.md | 2106 +++++++++++++++++++++++++++++++++++++ ЯПы/ЯПы.md | 6 + 3 files changed, 3623 insertions(+) create mode 100644 ЯПы/Семинары/Семинар-1.md create mode 100644 ЯПы/Семинары/Семинар-2.md create mode 100644 ЯПы/ЯПы.md diff --git a/ЯПы/Семинары/Семинар-1.md b/ЯПы/Семинары/Семинар-1.md new file mode 100644 index 0000000..f5cc929 --- /dev/null +++ b/ЯПы/Семинары/Семинар-1.md @@ -0,0 +1,1511 @@ +# Методичка по языку ассемблера x86-64 +## Семинар 1: Окружение, инструменты, первая программа + +### Содержание + +1. [Необходимые инструменты](#1-необходимые-инструменты) +2. [Командная оболочка и окружение выполнения](#2-командная-оболочка-и-окружение-выполнения) +3. [Регулярные выражения и grep](#3-регулярные-выражения-и-grep) +4. [Переменные окружения](#4-переменные-окружения) +5. [Модель памяти процесса x86-64](#5-модель-памяти-процесса-x86-64) +6. [Цикл выполнения инструкций](#6-цикл-выполнения-инструкций) +7. [Компиляция программ на ассемблере](#7-компиляция-программ-на-ассемблере) +8. [Разбор программы Hello World](#8-разбор-программы-hello-world) +9. [Системные вызовы Linux x86-64](#9-системные-вызовы-linux-x86-64) +10. [Регистры процессора x86-64](#10-регистры-процессора-x86-64) +11. [Отладчик GDB](#11-отладчик-gdb) +12. [Типичные ошибки и их решение](#12-типичные-ошибки-и-их-решение) +13. [Практические задания](#13-практические-задания) +14. [Справочник: часто используемые конструкции](#справочник-часто-используемые-конструкции) +15. [Полезные ссылки и документация](#полезные-ссылки-и-документация) + +--- + +## 1. Необходимые инструменты + +### Базовый набор инструментов + +Для работы с ассемблером x86-64 в Linux потребуются: + +- **NASM** (Netwide Assembler) — ассемблер, переводящий мнемоники в машинный код +- **ld** (GNU linker) — компоновщик, создающий исполняемые файлы из объектных файлов +- **GDB** (GNU Debugger) — отладчик для пошагового выполнения и анализа программ +- **make** — утилита для автоматизации сборки +- **grep**, **sed**, **awk** — инструменты обработки текста + +### Установка в Ubuntu/Debian + +```bash +sudo apt update +sudo apt install nasm gdb build-essential +``` + +### Установка в Fedora/RHEL + +```bash +sudo dnf install nasm gdb make gcc +``` + +### Проверка установки + +```bash +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 | Стандартный поток ошибок | Терминал | + +### Перенаправление потоков + +```bash +# Перенаправление 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`: + +```bash +#!/bin/bash +echo "Это сообщение в stdout" +echo "Это сообщение в stderr" >&2 +``` + +Запустите с перенаправлением: + +```bash +chmod +x test.sh +./test.sh > stdout.txt 2> stderr.txt +cat stdout.txt # Покажет только stdout +cat stderr.txt # Покажет только stderr +``` + +### Диаграмма: процесс и стандартные потоки + +```mermaid +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 + +```bash +# Поиск строк, содержащих слово "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`: + +```bash +# Найти числа от 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. Переменные окружения + +### Что такое переменные окружения? + +Переменные окружения — это пары ключ-значение, которые хранятся в памяти процесса и наследуются дочерними процессами. Они используются для настройки поведения программ без изменения их кода. + +### Просмотр переменных окружения + +```bash +# Показать все переменные окружения +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` | + +### Установка и экспорт переменных + +```bash +# Локальная переменная (только в текущей оболочке) +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`: + +```bash +export MY_PROJECT_PATH="$HOME/projects/asm" +export PATH="$PATH:$MY_PROJECT_PATH/bin" +``` + +После изменения выполните: + +```bash +source ~/.bashrc +``` + +--- + +## 5. Модель памяти процесса x86-64 + +### Адресное пространство процесса + +В x86-64 Linux каждый процесс имеет виртуальное адресное пространство размером до 2^48 байт (256 ТБ в теории, практически меньше). Память организована в несколько сегментов: + +```mermaid +graph TD + A[0x00007FFFFFFFFFFF
Высокие адреса] --> B[Stack
Стек растёт вниз] + B --> C[...] + C --> D[Memory Mapping
mmap, разделяемые библиотеки] + D --> E[...] + E --> F[Heap
Куча растёт вверх] + F --> G[BSS
Неинициализированные данные] + G --> H[Data
Инициализированные данные] + H --> I[Text
Код программы read-only] + I --> J[0x0000000000400000
Низкие адреса] +``` + +### Секции исполняемого файла ELF + +| Секция | Права доступа | Содержимое | Размер определяется | +|--------|---------------|------------|---------------------| +| `.text` | r-x | Машинный код программы | На этапе компиляции | +| `.rodata` | r-- | Константы, строковые литералы | На этапе компиляции | +| `.data` | rw- | Инициализированные глобальные переменные | На этапе компиляции | +| `.bss` | rw- | Неинициализированные глобальные переменные | На этапе компиляции (обнуляется при загрузке) | +| **stack** | rw- | Локальные переменные, адреса возврата | Динамически (растёт при вызовах функций) | +| **heap** | rw- | Динамически выделяемая память (malloc) | Динамически (по запросу программы) | + +### Визуализация секций программы + +```mermaid +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 +``` + +### Пример: просмотр секций программы + +```bash +# Скомпилируйте 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 + +```asm +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 + +Процессор выполняет программу в бесконечном цикле из трёх фаз: + +```mermaid +graph LR + A[FETCH
Загрузить инструкцию
из памяти по адресу RIP] --> B[DECODE
Декодировать
опкод и операнды] + B --> C[EXECUTE
Выполнить операцию
обновить регистры/память] + C --> D[Увеличить RIP
на длину инструкции] + 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` изменяется только если условие истинно + +### Пример: пошаговое выполнение + +Рассмотрим последовательность инструкций: + +```asm +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. Компиляция программ на ассемблере + +### Двухэтапный процесс сборки + +```mermaid +graph LR + A[hello.asm
Исходный код] --> B[nasm] + B --> C[hello.o
Объектный файл
машинный код + метаданные] + C --> D[ld] + D --> E[hello
Исполняемый файл ELF] +``` + +### Этап 1: Ассемблирование (NASM) + +```bash +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) файл с адресами и машинными кодами + +Пример с отладочной информацией: + +```bash +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) + +```bash +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. Записывает исполняемый файл + +### Проверка результата + +```bash +# Проверить тип файла +file hello +# Вывод: hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV)... + +# Сделать исполняемым (если ld не установил права) +chmod +x hello + +# Запустить +./hello +``` + +### Makefile для автоматизации + +Создайте файл `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 +``` + +Использование: + +```bash +make # Собрать программу +make run # Собрать и запустить +make debug # Собрать и запустить в GDB +make clean # Удалить сгенерированные файлы +``` + +--- + +## 8. Разбор программы Hello World + +### Полный исходный код + +```asm +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 + +```asm +section .data +``` + +- Директива секции: все последующие объявления будут помещены в секцию `.data` (инициализированные данные) + +```asm + msg db "Hello, World!", 0xA +``` + +- `msg` — метка (адрес начала строки) +- `db` (define byte) — директива объявления байтов +- `"Hello, World!"` — ASCII-строка (13 байт) +- `0xA` — шестнадцатеричное значение 10 (код символа `\n` — перевод строки) +- **Итого:** 14 байт данных + +```asm + len equ $ - msg +``` + +- `len` — константа времени ассемблирования (не выделяет память) +- `equ` — директива определения константы +- `$` — текущий адрес (адрес сразу после строки) +- `$ - msg` — разность адресов = длина строки +- **Результат:** `len = 14` + +### Пошаговый разбор секции .text + +```asm +section .text +``` + +- Секция кода (исполняемая, read-only) + +```asm + global _start +``` + +- Объявляет символ `_start` глобальным (видимым компоновщику) +- Компоновщик `ld` использует `_start` как точку входа по умолчанию + +```asm +_start: +``` + +- Метка: адрес первой инструкции программы + +### Системный вызов write + +```asm + 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 + +```asm + 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:** + +```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 байта + +### Эксперимент: изменение строки + +Попробуйте изменить программу: + +```asm +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 +``` + +Соберите и запустите: + +```bash +nasm -f elf64 hello.asm && ld -o hello hello.o && ./hello +``` + +--- + +## 9. Системные вызовы Linux x86-64 + +### Что такое системный вызов? + +**Системный вызов (syscall)** — это интерфейс между программой в пользовательском пространстве (user space) и ядром операционной системы (kernel space). Это единственный легальный способ для программы: + +- Взаимодействовать с устройствами ввода-вывода +- Работать с файловой системой +- Управлять процессами и потоками +- Выделять память +- Взаимодействовать с сетью + +### Переход в режим ядра + +```mermaid +sequenceDiagram + participant U as User Space
(программа) + participant C as CPU + participant K as Kernel Space
(ядро Linux) + + U->>C: syscall (0x0F 0x05) + Note over C: Сохранить контекст + C->>K: Передать управление
Ring 3 → Ring 0 + Note over K: Проверить права
Выполнить операцию + 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) +Чтение данных из файлового дескриптора. + +```nasm +mov rax, 0 ; syscall номер: read +mov rdi, 0 ; fd = 0 (stdin) +mov rsi, buffer ; указатель на буфер +mov rdx, 128 ; максимальное количество байт для чтения +syscall +; rax содержит количество прочитанных байт (или -errno при ошибке) +``` + +#### 2. write (номер 1) +Запись данных в файловый дескриптор. + +```nasm +mov rax, 1 ; syscall номер: write +mov rdi, 1 ; fd = 1 (stdout) +mov rsi, message ; указатель на данные +mov rdx, 13 ; количество байт для записи +syscall +; rax содержит количество записанных байт +``` + +#### 3. exit (номер 60) +Завершение процесса. + +```nasm +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 | Неверный аргумент | + +**Проверка ошибок:** +```nasm +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 регистры имеют многоуровневую структуру для обратной совместимости: + +```mermaid +graph TD + A[RAX 64 бит
0x0123456789ABCDEF] --> B[EAX 32 бит младших
0x89ABCDEF] + B --> C[AX 16 бит младших
0xCDEF] + C --> D[AH 8 бит старших
0xCD] + C --> E[AL 8 бит младших
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 бита регистра! + +```nasm +mov rax, 0x123456789ABCDEF ; rax = 0x123456789ABCDEF +mov eax, 0x11111111 ; rax = 0x0000000011111111 (старшие биты обнулены!) +``` + +### Специальные регистры + +**rip** (Instruction Pointer): +- Указывает на адрес следующей инструкции +- Изменяется автоматически при выполнении +- Можно изменить через `jmp`, `call`, `ret` +- Используется для RIP-relative адресации + +**rflags** (Флаги процессора): +- 64-битный регистр состояния +- Содержит флаги результата операций (ZF, SF, CF, OF, ...) +- Изменяется автоматически арифметическими и логическими операциями + +### Примеры использования разных размеров + +```nasm +; Работа с байтами +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) + +```nasm +; Умножение: результат может не поместиться в 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 | + +**Популярные идиомы:** + +```nasm +; Обнуление регистра (быстрее 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:** + +```nasm +; Беззнаковый сдвиг (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, знак сохранён!) +``` + +**Быстрое умножение/деление на степени двойки:** + +```nasm +; Умножить на 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 | Переполнение при знаковых операциях | + +### Как устанавливаются флаги + +```nasm +; Арифметические операции устанавливают флаги +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 цифр):** + +```nasm +; Проверить, что символ в 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: +; Обработка цифры +``` + +**Знаковое сравнение:** + +```nasm +; Проверить, что 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 +``` + +**Цикл с счётчиком:** + +```nasm + mov rcx, 10 ; Счётчик = 10 +.loop: + ; Тело цикла + + dec rcx ; Уменьшить счётчик + jnz .loop ; Повторить, пока rcx != 0 + +; Выход из цикла +``` + +--- + +## 13. Типичные ошибки начинающих + +### Ошибка 1: Забыли указать размер секции .bss + +**Неправильно:** +```nasm +section .bss + buffer: ; ❌ Не указан размер! +``` + +**Правильно:** +```nasm +section .bss + buffer: resb 128 ; ✓ Резервировать 128 байт + counter: resq 1 ; ✓ Резервировать 1 qword (8 байт) +``` + +### Ошибка 2: Использовали неправильный размер операндов + +**Неправильно:** +```nasm +mov rax, [buffer] ; Загружает 8 байт +mov byte [buffer], rax ; ❌ Пытается записать 8 байт в 1 байт! +``` + +**Правильно:** +```nasm +mov al, [buffer] ; ✓ Загрузить 1 байт +mov byte [buffer], al ; ✓ Записать 1 байт + +; Или явно указать размер +movzx rax, byte [buffer] ; Загрузить байт с zero-extend +``` + +### Ошибка 3: Забыли про обнуление старших битов при 32-битной записи + +**Проблема:** +```nasm +mov rax, 0xFFFFFFFFFFFFFFFF +mov eax, 0 ; ❌ rax станет 0, а не 0xFFFFFFFF00000000! +``` + +**Объяснение:** запись в 32-битную часть (eax) обнуляет старшие 32 бита автоматически. + +### Ошибка 4: Неправильно вычислили длину строки + +**Неправильно:** +```nasm +section .data + msg: db "Hello", 10 ; Строка из 6 байт (5 букв + '\n') + +section .text + mov rdx, 5 ; ❌ Длина = 5, но должна быть 6! + ; В результате '\n' не выведется +``` + +**Правильно:** +```nasm +section .data + msg: db "Hello", 10 + msg_len equ $ - msg ; ✓ Автоматически вычислить длину + +section .text + mov rdx, msg_len ; ✓ Правильная длина = 6 +``` + +### Ошибка 5: Перепутали signed и unsigned переходы + +**Неправильно:** +```nasm +; Проверка ASCII кода (беззнаковое значение) +cmp al, '0' +jl .not_digit ; ❌ jl для signed сравнения! +``` + +**Правильно:** +```nasm +; ASCII коды — беззнаковые +cmp al, '0' +jb .not_digit ; ✓ jb (below) для unsigned +``` + +**Напоминание:** +- `jl/jg/jle/jge` — для **signed** (знаковых) сравнений +- `jb/ja/jbe/jae` — для **unsigned** (беззнаковых) сравнений + +### Ошибка 6: Не обработали ошибку системного вызова + +**Неправильно:** +```nasm +mov rax, 0 +mov rdi, 0 +mov rsi, buffer +mov rdx, 100 +syscall +; ❌ Не проверили, что rax >= 0 (может быть -errno) +mov [bytes_read], rax +``` + +**Правильно:** +```nasm +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`. + +**Неправильно:** +```nasm +section .data + msg: db "Hello" ; ❌ Нет '\0'! + +extern strlen + mov rdi, msg + call strlen ; strlen может читать за границу +``` + +**Правильно:** +```nasm +section .data + msg: db "Hello", 0 ; ✓ Завершающий ноль +``` + +--- + +## 14. Отладка с GDB + +### Основные команды + +**Запуск программы:** +```bash +gdb ./program +(gdb) break _start # Точка останова на метке +(gdb) run # Запустить программу +``` + +**Пошаговое выполнение:** +```bash +(gdb) stepi # Выполнить одну инструкцию (step into) +(gdb) nexti # Следующая инструкция (step over) +(gdb) continue # Продолжить до следующего breakpoint +(gdb) finish # Выполнить до выхода из функции +``` + +**Просмотр состояния:** +```bash +(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 # В десятичном (со знаком) +``` + +**Просмотр памяти:** +```bash +(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) + +**Точки останова:** +```bash +(gdb) break _start # На метке +(gdb) break *0x401000 # На конкретном адресе +(gdb) info breakpoints # Список всех breakpoints +(gdb) delete 1 # Удалить breakpoint №1 +(gdb) disable 2 # Временно отключить breakpoint №2 +``` + +**Дизассемблирование:** +```bash +(gdb) disassemble # Дизассемблировать текущую функцию +(gdb) disassemble _start # Конкретную метку +(gdb) set disassembly-flavor intel # Использовать Intel синтаксис +``` + +### Пример сессии отладки + +```bash +$ 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`:** +```gdb +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 | — | — | Завершить процесс | + +### Шаблон минимальной программы + +```nasm +; Минимальная программа 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 +``` + +### Полезные ссылки + +- **Intel Software Developer Manual**: https://software.intel.com/content/www/us/en/develop/articles/intel-sdm.html +- **Linux System Call Table**: https://filippo.io/linux-syscall-table/ +- **NASM Documentation**: https://www.nasm.us/xdoc/2.15.05/html/nasmdoc0.html +- **GDB Manual**: https://sourceware.org/gdb/current/onlinedocs/gdb/ +- **x86-64 ABI**: https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf + +--- + +**Конец методички для семинара 1** diff --git a/ЯПы/Семинары/Семинар-2.md b/ЯПы/Семинары/Семинар-2.md new file mode 100644 index 0000000..4828586 --- /dev/null +++ b/ЯПы/Семинары/Семинар-2.md @@ -0,0 +1,2106 @@ +# Методичка по языку ассемблера x86-64 +## Семинар 2: Функции, стек, конечные автоматы + +--- + +## Оглавление + +1. [Механизм работы стека](#1-механизм-работы-стека) +2. [Определение и вызов функций](#2-определение-и-вызов-функций) +3. [Функции с аргументами и возвращаемые значения](#3-функции-с-аргументами-и-возвращаемые-значения) +4. [Соглашение вызова (Calling Convention)](#4-соглашение-вызова-calling-convention) +5. [Использование стека для локальных переменных](#5-использование-стека-для-локальных-переменных) +6. [Фрейм стека и выравнивание](#6-фрейм-стека-и-выравнивание) +7. [Конечные автоматы (Finite State Machines)](#7-конечные-автоматы-finite-state-machines) +8. [Автоматы и регулярные выражения](#8-автоматы-и-регулярные-выражения) +9. [Кодирование автоматов на ассемблере](#9-кодирование-автоматов-на-ассемблере) +10. [Типичные ошибки](#10-типичные-ошибки) +11. [Практические задания](#11-практические-задания) +12. [Справочник: часто используемые конструкции](#справочник-часто-используемые-конструкции) + +--- + +## 1. Механизм работы стека + +### Что такое стек? + +**Стек (stack)** — это область памяти, организованная по принципу LIFO (Last In, First Out: последним пришёл — первым ушёл). Стек используется для: + +- Сохранения адресов возврата при вызовах функций +- Хранения локальных переменных функций +- Временного сохранения значений регистров +- Передачи аргументов функций (свыше 6 аргументов) + +### Регистр RSP (Stack Pointer) + +`RSP` (Stack Pointer) — регистр, указывающий на **вершину стека** (самый последний добавленный элемент). + +**Важно:** В x86-64 Linux стек растёт **вниз** (к меньшим адресам): + +``` +Высокие адреса (0x7FFFFFFFFFFF) + ↑ + | + [Пустое пространство] + | + ↓ ← RSP указывает сюда (вершина стека) + [Последний добавленный элемент] + [Предыдущий элемент] + [Ещё более ранний элемент] + | + ↓ +Низкие адреса +``` + +### Инструкции работы со стеком + +#### PUSH — положить на стек + +```asm +push rax +``` + +**Эквивалентно:** + +```asm +sub rsp, 8 ; Сдвинуть вершину стека на 8 байт вниз +mov [rsp], rax ; Записать значение rax по адресу вершины +``` + +#### POP — снять со стека + +```asm +pop rax +``` + +**Эквивалентно:** + +```asm +mov rax, [rsp] ; Прочитать значение с вершины стека +add rsp, 8 ; Сдвинуть вершину стека на 8 байт вверх +``` + +### Визуализация работы стека + +```mermaid +graph TD + A[Начальное состояние
RSP = 0x7FFFFFFFE000] + A --> B[push rax где rax=0x42] + B --> C[RSP = 0x7FFFFFFFDFF8
память 0x...DFF8: 0x42] + C --> D[push rbx где rbx=0x100] + D --> E[RSP = 0x7FFFFFFFDFF0
память 0x...DFF0: 0x100
память 0x...DFF8: 0x42] + E --> F[pop rcx] + F --> G[RCX = 0x100
RSP = 0x7FFFFFFFDFF8] + + style A fill:#334155 + style C fill:#475569 + style E fill:#64748b + style G fill:#475569 +``` + +### Пошаговый пример + +Предположим, `RSP = 0x7FFFFFFFE000` перед началом. + +```asm +push rax ; rax = 0x1234567890ABCDEF +push rbx ; rbx = 0xAABBCCDDEEFF0011 +pop rcx ; rcx = ? +pop rdx ; rdx = ? +``` + +| Шаг | Инструкция | RSP до | RSP после | Действие | rcx | rdx | +|-----|------------|--------|-----------|----------|-----|-----| +| 0 | — | `0x7FFFFFFFE000` | — | Начало | — | — | +| 1 | `push rax` | `0x7FFFFFFFE000` | `0x7FFFFFFFDFF8` | `[0x...DFF8] = 0x1234567890ABCDEF` | — | — | +| 2 | `push rbx` | `0x7FFFFFFFDFF8` | `0x7FFFFFFFDFF0` | `[0x...DFF0] = 0xAABBCCDDEEFF0011` | — | — | +| 3 | `pop rcx` | `0x7FFFFFFFDFF0` | `0x7FFFFFFFDFF8` | `rcx = [0x...DFF0]` | `0xAABBCCDDEEFF0011` | — | +| 4 | `pop rdx` | `0x7FFFFFFFDFF8` | `0x7FFFFFFFE000` | `rdx = [0x...DFF8]` | `0xAABBCCDDEEFF0011` | `0x1234567890ABCDEF` | + +**Результат:** `rcx = 0xAABBCCDDEEFF0011`, `rdx = 0x1234567890ABCDEF`, `RSP` вернулся к исходному значению. + +### Важное правило + +**Стек должен быть сбалансирован:** если вы добавили N элементов с помощью `push`, вы должны снять N элементов с помощью `pop` (или скорректировать `rsp` напрямую). + +Несбалансированный стек приводит к: +- Неправильным адресам возврата (`ret` вернётся не туда) +- Segmentation fault +- Непредсказуемому поведению программы + +--- + +## 2. Определение и вызов функций + +### Что такое функция в ассемблере? + +**Функция (подпрограмма, subroutine)** — это именованный блок инструкций, который можно вызвать из разных мест программы и который возвращает управление обратно вызывающему коду. + +В ассемблере функция = **метка** + **код** + **инструкция `ret`**. + +### Инструкция CALL + +```asm +call function_name +``` + +**Действия `call`:** + +1. **Сохранить адрес возврата на стек:** `push` адрес следующей инструкции после `call` +2. **Перейти к функции:** установить `RIP = адрес метки function_name` + +**Эквивалентно:** + +```asm +push rip_next ; (это псевдокод, rip нельзя использовать напрямую) +jmp function_name +rip_next: ; адрес следующей инструкции +``` + +### Инструкция RET + +```asm +ret +``` + +**Действия `ret`:** + +1. **Снять адрес возврата со стека:** `pop` значение в `RIP` +2. **Перейти по этому адресу:** продолжить выполнение + +**Эквивалентно:** + +```asm +pop rip ; (псевдокод) +``` + +### Визуализация call и ret + +```mermaid +sequenceDiagram + participant C as Вызывающий код + participant S as Стек + participant F as Функция + + C->>C: call my_func (адрес 0x401010) + C->>S: push 0x401015 (адрес возврата) + C->>F: jmp my_func (0x401080) + Note over F: Выполнение тела функции + F->>S: pop адрес (0x401015) + F->>C: jmp 0x401015 + C->>C: Продолжить выполнение +``` + +### Пример: простая функция + +```asm +section .data + msg db "Hello from function!", 0xA + msg_len equ $ - msg + +section .text + global _start + +; Функция печати сообщения +print_hello: + mov rax, 1 ; sys_write + mov rdi, 1 ; stdout + mov rsi, msg + mov rdx, msg_len + syscall + ret ; Вернуться к вызывающему коду + +_start: + call print_hello ; Вызов функции + call print_hello ; Вызов ещё раз + + ; Выход + mov rax, 60 + xor rdi, rdi + syscall +``` + +### Пошаговая трассировка выполнения + +Предположим: +- `_start` находится по адресу `0x401000` +- `print_hello` находится по адресу `0x401020` +- `RSP` изначально `0x7FFFFFFFE000` + +| RIP | Инструкция | RSP до | RSP после | Стек | Комментарий | +|-----|------------|--------|-----------|------|-------------| +| `0x401000` | `call print_hello` | `0x7FFFFFFFE000` | `0x7FFFFFFFDFF8` | `[...FF8]=0x401005` | Сохранить 0x401005 (адрес после call) | +| `0x401020` | `mov rax, 1` | `0x7FFFFFFFDFF8` | `0x7FFFFFFFDFF8` | `[...FF8]=0x401005` | Начало функции | +| ... | (syscall и др.) | `0x7FFFFFFFDFF8` | `0x7FFFFFFFDFF8` | `[...FF8]=0x401005` | Тело функции | +| `0x40102E` | `ret` | `0x7FFFFFFFDFF8` | `0x7FFFFFFFE000` | — | Снять 0x401005 и прыгнуть туда | +| `0x401005` | `call print_hello` | `0x7FFFFFFFE000` | `0x7FFFFFFFDFF8` | `[...FF8]=0x40100A` | Второй вызов | +| `0x401020` | `mov rax, 1` | `0x7FFFFFFFDFF8` | `0x7FFFFFFFDFF8` | `[...FF8]=0x40100A` | Снова в функции | +| `0x40102E` | `ret` | `0x7FFFFFFFDFF8` | `0x7FFFFFFFE000` | — | Вернуться к 0x40100A | +| `0x40100A` | `mov rax, 60` | `0x7FFFFFFFE000` | `0x7FFFFFFFE000` | — | Продолжить _start | + +### CALL vs JMP + +| Инструкция | Сохраняет адрес возврата? | Можно ли вернуться? | Использование | +|------------|---------------------------|---------------------|---------------| +| `call` | **Да** (на стек) | Да (`ret`) | Вызов функций, которые должны вернуться | +| `jmp` | **Нет** | Нет | Безусловные переходы, tail calls, `exit` | + +**Пример tail call:** + +```asm +my_function: + ; ... какая-то работа ... + jmp other_function ; Tail call: не создаём новый фрейм стека +``` + +Это эквивалентно: + +```asm +my_function: + ; ... какая-то работа ... + call other_function + ret +``` + +Но `jmp` эффективнее: не тратит место на стеке и не добавляет лишний `ret`. + +### Вложенные вызовы функций + +```asm +section .text + global _start + +func_c: + ; Самая внутренняя функция + ret + +func_b: + call func_c ; Вызвать func_c + ret ; Вернуться в func_a + +func_a: + call func_b ; Вызвать func_b + ret ; Вернуться в _start + +_start: + call func_a ; Вызвать func_a + ; Продолжить +``` + +**Стек при вложенных вызовах:** + +``` +1. _start вызывает func_a: + Стек: [адрес возврата в _start] + +2. func_a вызывает func_b: + Стек: [адрес возврата в func_a] + [адрес возврата в _start] + +3. func_b вызывает func_c: + Стек: [адрес возврата в func_b] + [адрес возврата в func_a] + [адрес возврата в _start] + +4. func_c делает ret: + Стек: [адрес возврата в func_a] + [адрес возврата в _start] + +5. func_b делает ret: + Стек: [адрес возврата в _start] + +6. func_a делает ret: + Стек: (пустой) +``` + +--- + +## 3. Функции с аргументами и возвращаемые значения + +### Передача аргументов через регистры + +В **System V ABI** (Linux x86-64) первые **6 целочисленных/указательных аргументов** передаются через регистры: + +| Порядок аргумента | Регистр | Размер | Примечание | +|-------------------|---------|--------|------------| +| 1-й | `rdi` | 64 бита | Может использоваться `edi`, `di`, `dil` для меньших размеров | +| 2-й | `rsi` | 64 бита | — | +| 3-й | `rdx` | 64 бита | — | +| 4-й | `rcx` | 64 бита | **Внимание:** разрушается при syscall! | +| 5-й | `r8` | 64 бита | — | +| 6-й | `r9` | 64 бита | — | + +**Аргументы 7+** передаются через стек (справа налево). + +### Возвращаемое значение + +| Тип результата | Регистр | +|----------------|---------| +| Целочисленное/указатель (до 64 бит) | `rax` | +| Дополнительное значение (128 бит) | `rdx:rax` (старшие 64 бита в `rdx`) | +| Числа с плавающей точкой | `xmm0`, `xmm1` | +| Структуры (маленькие) | `rax`, `rdx` | +| Структуры (большие) | Указатель передаётся как скрытый первый аргумент | + +### Пример: функция сложения + +```asm +; Функция: сложить два числа +; Аргументы: rdi = a, rsi = b +; Возвращает: rax = a + b +add_numbers: + mov rax, rdi ; rax = a + add rax, rsi ; rax = a + b + ret + +; Вызов: +_start: + mov rdi, 10 ; Первый аргумент + mov rsi, 32 ; Второй аргумент + call add_numbers ; Результат в rax + ; rax теперь содержит 42 +``` + +### Пример: функция с 3 аргументами + +```asm +; Функция: вычислить (a + b) * c +; Аргументы: rdi = a, rsi = b, rdx = c +; Возвращает: rax = (a + b) * c +calculate: + add rdi, rsi ; rdi = a + b + mov rax, rdi ; rax = a + b + imul rax, rdx ; rax = (a + b) * c + ret + +; Вызов: +_start: + mov rdi, 5 ; a = 5 + mov rsi, 3 ; b = 3 + mov rdx, 7 ; c = 7 + call calculate ; rax = (5+3)*7 = 56 +``` + +### Функция string_length + +Вычисляет длину null-terminated строки. + +```asm +; Аргумент: rdi = указатель на строку +; Возвращает: rax = длина строки (без null-terminator) +string_length: + xor rax, rax ; rax = 0 (счётчик) +.loop: + cmp byte [rdi + rax], 0 ; Проверить текущий байт + je .done ; Если 0, выйти из цикла + inc rax ; rax++ + jmp .loop +.done: + ret + +; Использование: +section .data + my_string db "Hello, World!", 0 + +section .text +_start: + mov rdi, my_string ; Передать адрес строки + call string_length ; rax = 13 +``` + +### Функция print_string + +Выводит null-terminated строку в stdout. + +```asm +; Аргумент: rdi = указатель на строку +; Возвращает: ничего (может вернуть количество байт в rax) +print_string: + push rdi ; Сохранить rdi (аргумент) + call string_length ; rax = длина строки + pop rsi ; rsi = адрес строки (восстановить из стека) + + mov rdx, rax ; rdx = длина + mov rax, 1 ; sys_write + mov rdi, 1 ; stdout + syscall ; Вывести строку + ret + +; Использование: +_start: + mov rdi, my_string + call print_string +``` + +### Функция с несколькими возвращаемыми значениями + +```asm +; Функция: разделить число на целую и дробную части (упрощённый пример) +; Аргумент: rdi = число вида XY, где X - десятки, Y - единицы +; Возвращает: rax = X (десятки), rdx = Y (единицы) +split_number: + mov rax, rdi ; rax = число + xor rdx, rdx ; rdx = 0 + mov rcx, 10 + div rcx ; rax = rax / 10, rdx = rax % 10 + ret + +; Вызов: +_start: + mov rdi, 47 ; Число 47 + call split_number + ; rax = 4 (десятки), rdx = 7 (единицы) +``` + +### Пример: print_hex + +Выводит 64-битное число в шестнадцатеричном формате. + +```asm +section .data + hex_digits db '0123456789ABCDEF' + +section .text + global _start + +; Аргумент: rdi = число для вывода +print_hex: + mov rcx, 64 ; Количество бит +.loop: + push rax ; Сохранить rax + sub rcx, 4 ; Обработать следующие 4 бита + mov rax, rdi ; rax = число + shr rax, cl ; Сдвинуть вправо на cl бит + and rax, 0xF ; Оставить младшие 4 бита + + lea rsi, [hex_digits + rax] ; rsi = адрес соответствующей цифры + mov rax, 1 ; sys_write + mov rdx, 1 ; Длина = 1 байт + push rdi + push rcx + mov rdi, 1 ; stdout + syscall + pop rcx + pop rdi + + pop rax + test rcx, rcx + jnz .loop + ret + +_start: + mov rdi, 0x1234ABCD ; Число для вывода + call print_hex + + ; Вывод переноса строки + mov rax, 1 + mov rdi, 1 + mov rsi, newline + mov rdx, 1 + syscall + + mov rax, 60 + xor rdi, rdi + syscall + +section .data + newline db 0xA +``` + +--- + +## 4. Соглашение вызова (Calling Convention) + +### Что такое Calling Convention? + +**Соглашение вызова (calling convention)** — это набор правил, определяющих: + +1. Как передаются аргументы (регистры, стек, порядок) +2. Какой регистр используется для возвращаемого значения +3. Какие регистры должны сохраняться функцией (callee-saved) +4. Какие регистры может свободно изменять функция (caller-saved) +5. Как выравнивается стек +6. Кто очищает стек после вызова + +### System V AMD64 ABI (Linux, BSD, macOS) + +Это стандартное соглашение для Unix-подобных систем на x86-64. + +### Классификация регистров + +```mermaid +graph TD + A[Регистры x86-64] + A --> B[Caller-saved
Вызывающий сохраняет] + A --> C[Callee-saved
Вызываемая функция сохраняет] + + B --> D[rax - возвращаемое значение] + B --> E[rdi, rsi, rdx, rcx, r8, r9 - аргументы] + B --> F[r10, r11 - временные] + + C --> G[rbx] + C --> H[rbp - frame pointer] + C --> I[r12, r13, r14, r15] + C --> J[rsp - stack pointer
всегда сохраняется!] + + style B fill:#ef4444 + style C fill:#22c55e + style D fill:#fca5a5 + style E fill:#fca5a5 + style F fill:#fca5a5 + style G fill:#86efac + style H fill:#86efac + style I fill:#86efac + style J fill:#86efac +``` + +### Таблица классификации регистров + +| Регистр | Тип | Назначение | Сохраняется? | +|---------|-----|------------|--------------| +| **rax** | Caller-saved | Возвращаемое значение, syscall номер | ❌ Нет | +| **rbx** | Callee-saved | Общего назначения | ✅ Да | +| **rcx** | Caller-saved | 4-й аргумент | ❌ Нет (разрушается при syscall!) | +| **rdx** | Caller-saved | 3-й аргумент, 2-е возвращаемое значение | ❌ Нет | +| **rsi** | Caller-saved | 2-й аргумент | ❌ Нет | +| **rdi** | Caller-saved | 1-й аргумент | ❌ Нет | +| **rbp** | Callee-saved | Frame pointer (база стека) | ✅ Да | +| **rsp** | Специальный | Stack pointer (вершина стека) | ✅ **Всегда** | +| **r8** | Caller-saved | 5-й аргумент | ❌ Нет | +| **r9** | Caller-saved | 6-й аргумент | ❌ Нет | +| **r10** | Caller-saved | Временный | ❌ Нет | +| **r11** | Caller-saved | Временный | ❌ Нет (разрушается при syscall!) | +| **r12-r15** | Callee-saved | Общего назначения | ✅ Да | + +### Что означает "Caller-saved" и "Callee-saved"? + +#### Caller-saved (сохраняет вызывающий) + +Если функция **A** вызывает функцию **B**, и функция **A** использует регистры `rax`, `rdi`, `rsi` и т.д., то: + +- Функция **A** должна **сохранить** эти регистры **перед** вызовом **B** (если значения нужны после вызова) +- Функция **B** **может свободно изменять** эти регистры без сохранения + +**Пример:** + +```asm +function_a: + mov rax, 42 ; Используем rax + + ; Хотим вызвать function_b, но нужно сохранить rax + push rax ; Сохранить rax (caller-saved) + call function_b ; function_b может изменить rax + pop rax ; Восстановить rax + + ; Теперь rax снова содержит 42 + ret + +function_b: + mov rax, 999 ; Можем свободно изменять rax + ret +``` + +#### Callee-saved (сохраняет вызываемая функция) + +Если функция **B** хочет использовать регистры `rbx`, `r12-r15`, `rbp`, то: + +- Функция **B** должна **сохранить** эти регистры в начале и **восстановить** перед `ret` +- Функция **A** может **не беспокоиться** об этих регистрах — они останутся неизменными после вызова **B** + +**Пример:** + +```asm +function_a: + mov rbx, 100 ; Используем rbx + call function_b ; function_b сохранит rbx внутри себя + ; rbx всё ещё содержит 100 + ret + +function_b: + push rbx ; Сохранить rbx (callee-saved) + mov rbx, 777 ; Используем rbx для своих нужд + ; ... работа с rbx ... + pop rbx ; Восстановить rbx + ret +``` + +### Почему такое разделение? + +**Производительность и оптимизация:** + +- **Caller-saved** регистров больше → функции могут использовать много регистров для аргументов и временных значений без накладных расходов +- **Callee-saved** регистров меньше, но они гарантируют стабильность → удобно для переменных, которые должны "пережить" вызовы функций +- Компилятор может оптимизировать, сохраняя только действительно используемые регистры + +### Сравнение: функция VS системный вызов + +| Аспект | Функция (System V ABI) | Системный вызов (syscall) | +|--------|------------------------|---------------------------| +| Номер операции | — | `rax` | +| Аргумент 1 | `rdi` | `rdi` | +| Аргумент 2 | `rsi` | `rsi` | +| Аргумент 3 | `rdx` | `rdx` | +| Аргумент 4 | `rcx` | **`r10`** (не `rcx`!) | +| Аргумент 5 | `r8` | `r8` | +| Аргумент 6 | `r9` | `r9` | +| Возвращаемое значение | `rax` | `rax` | +| Разрушаемые регистры | Caller-saved | **`rcx`, `r11`** (всегда!) | +| Callee-saved | `rbx`, `rbp`, `r12-r15` | `rbx`, `rbp`, `r12-r15` | + +**Важно:** Системный вызов **всегда разрушает `rcx` и `r11`**, даже если они не используются для аргументов! + +### Пример: сохранение регистров + +```asm +my_function: + ; Хотим использовать rbx, r12, r13 (callee-saved) + push rbx + push r12 + push r13 + + mov rbx, rdi ; Сохранить 1-й аргумент + mov r12, rsi ; Сохранить 2-й аргумент + mov r13, rdx ; Сохранить 3-й аргумент + + ; Вызов другой функции (может разрушить caller-saved) + call some_other_function + + ; rbx, r12, r13 всё ещё содержат исходные значения + mov rax, rbx + add rax, r12 + add rax, r13 + + ; Восстановить callee-saved регистры + pop r13 + pop r12 + pop rbx + ret +``` + +--- + +## 5. Локальные переменные на стеке + +### Зачем нужен фрейм стека? + +Когда функции требуются локальные переменные, массивы или буферы, их размещают на стеке. Для управления локальными данными используется **фрейм стека** (stack frame) — структура, ограниченная двумя регистрами: + +- **`rbp`** (base pointer) — указатель на базу текущего фрейма (фиксированная точка отсчёта) +- **`rsp`** (stack pointer) — указатель на вершину стека (динамически меняется при push/pop) + +### Структура фрейма стека + +```mermaid +graph TD + A["Высокие адреса"] --> B["[arg 8] ← rbp + 24"] + B --> C["[arg 7] ← rbp + 16"] + C --> D["[return address] ← rbp + 8"] + D --> E["[старый rbp] ← rbp (saved frame pointer)"] + E --> F["[локальная переменная 1] ← rbp - 8"] + F --> G["[локальная переменная 2] ← rbp - 16"] + G --> H["[временные данные] ← rsp (текущая вершина стека)"] + H --> I["Низкие адреса"] + + style E fill:#ffd700 + style D fill:#ff6b6b + style F fill:#4ecdc4 + style G fill:#4ecdc4 +``` + +**Ключевые особенности:** + +1. **Аргументы 1-6** передаются в регистрах, аргументы **7+** — на стеке (выше return address) +2. **Return address** сохраняется автоматически инструкцией `call` +3. **Старый rbp** сохраняется в прологе функции для восстановления фрейма вызывающей функции +4. **Локальные переменные** располагаются ниже rbp (отрицательные смещения) +5. **rbp** остаётся неизменным в течение выполнения функции, что упрощает адресацию локальных переменных + +### Пролог и эпилог функции + +**Стандартный пролог:** +```nasm +push rbp ; Сохранить старый базовый указатель +mov rbp, rsp ; Установить rbp на текущую позицию стека +sub rsp, N ; Выделить N байт для локальных переменных +``` + +**Стандартный эпилог:** +```nasm +mov rsp, rbp ; Освободить локальные переменные (восстановить rsp) +pop rbp ; Восстановить старый базовый указатель +ret ; Вернуться (pop return address в rip) +``` + +**Альтернативные инструкции:** +- `enter N, 0` — эквивалент пролога (медленнее, редко используется) +- `leave` — эквивалент `mov rsp, rbp; pop rbp` (часто используется) + +### Пример: функция с локальным массивом + +```nasm +; Функция, которая создаёт локальный массив из 5 qword +; и заполняет его квадратами чисел 0-4 +; Возвращает сумму элементов в rax + +square_sum: + ; === ПРОЛОГ === + push rbp ; Сохранить старый rbp + mov rbp, rsp ; rbp теперь указывает на сохранённый rbp + sub rsp, 40 ; Выделить 40 байт (5 qword по 8 байт) + ; После пролога: + ; rbp+8 → return address + ; rbp+0 → старый rbp + ; rbp-8 → array[0] + ; rbp-16 → array[1] + ; rbp-24 → array[2] + ; rbp-32 → array[3] + ; rbp-40 → array[4] ← rsp + + ; === ТЕЛО ФУНКЦИИ === + ; Заполнить массив квадратами + xor rcx, rcx ; Счётчик (i = 0) +.fill_loop: + cmp rcx, 5 + je .fill_done + + mov rax, rcx ; rax = i + imul rax, rax ; rax = i * i + + ; Вычислить адрес array[i] = rbp - 8*(i+1) + lea rdx, [rcx + 1] ; rdx = i + 1 + shl rdx, 3 ; rdx = (i + 1) * 8 + neg rdx ; rdx = -(i + 1) * 8 + mov [rbp + rdx], rax ; array[i] = i² + + inc rcx + jmp .fill_loop + +.fill_done: + ; Вычислить сумму элементов + xor rax, rax ; sum = 0 + xor rcx, rcx ; i = 0 +.sum_loop: + cmp rcx, 5 + je .sum_done + + lea rdx, [rcx + 1] + shl rdx, 3 + neg rdx + add rax, [rbp + rdx] ; sum += array[i] + + inc rcx + jmp .sum_loop + +.sum_done: + ; rax содержит сумму 0² + 1² + 2² + 3² + 4² = 30 + + ; === ЭПИЛОГ === + mov rsp, rbp ; Освободить локальные переменные + pop rbp ; Восстановить старый rbp + ret ; Вернуться + +; Альтернативный эпилог: +; leave ; Эквивалентно mov rsp, rbp; pop rbp +; ret +``` + +### Пошаговая трассировка создания фрейма + +Предположим, функция `square_sum` вызвана из адреса `0x401234`. Проследим изменения стека: + +| Шаг | Инструкция | rsp | rbp | Стек (содержимое по адресам) | +|-----|-----------|-----|-----|-----------------------------| +| 0 | `call square_sum` | `0x7fff0000` | `0x7fff0100` | `[0x7fff0000] = 0x401239` (return addr) | +| 1 | `push rbp` | `0x7ffefff8` | `0x7fff0100` | `[0x7ffefff8] = 0x7fff0100` (старый rbp) | +| 2 | `mov rbp, rsp` | `0x7ffefff8` | `0x7ffefff8` | rbp теперь указывает на новый фрейм | +| 3 | `sub rsp, 40` | `0x7ffeFFd0` | `0x7ffefff8` | Выделено 40 байт для массива | +| 4 | Заполнение array[0] | `0x7ffeffd0` | `0x7ffefff8` | `[rbp-8] = 0` (0²) | +| 5 | Заполнение array[1] | `0x7ffeffd0` | `0x7ffefff8` | `[rbp-16] = 1` (1²) | +| ... | ... | ... | ... | ... | +| N | `mov rsp, rbp` | `0x7ffefff8` | `0x7ffefff8` | Локальные переменные освобождены | +| N+1 | `pop rbp` | `0x7fff0000` | `0x7fff0100` | rbp восстановлен | +| N+2 | `ret` | `0x7fff0008` | `0x7fff0100` | Возврат по адресу 0x401239 | + +### Почему используется rbp? + +**Без rbp (использование только rsp):** +- Каждая операция push/pop в теле функции смещает rsp +- Смещения локальных переменных относительно rsp постоянно меняются +- Компилятор должен отслеживать текущее смещение rsp на каждой инструкции +- Усложняется отладка (стековые трейсы требуют сложного раскручивания) + +**С rbp:** +- rbp фиксирован в течение выполнения функции +- Локальные переменные имеют постоянные смещения: `[rbp-8]`, `[rbp-16]`, ... +- Отладчики могут легко найти границы фрейма и параметры +- Упрощается генерация debug-информации (DWARF) + +**Современная оптимизация:** компиляторы с флагом `-fomit-frame-pointer` могут не использовать rbp, освобождая ещё один регистр общего назначения. Это требует точного отслеживания глубины стека на этапе компиляции. + +--- + +## 6. Выравнивание стека (Stack Alignment) + +### Зачем нужно выравнивание? + +Системное соглашение **System V AMD64 ABI** требует, чтобы стек был выровнен на **16-байтную границу** в момент выполнения инструкции `call`. Это означает, что `rsp` должен быть кратен 16 перед `call`. + +**Причины требования:** + +1. **SSE/AVX инструкции**: многие SIMD-инструкции (например, `movaps`, `movdqa`) требуют выравнивания на 16 байт для работы с памятью. Невыровненный доступ вызывает исключение `SIGSEGV` или `SIGBUS`. +2. **Производительность**: даже инструкции, которые допускают невыровненный доступ (например, `movups`), работают медленнее при нарушении выравнивания. +3. **Совместимость с библиотеками**: все системные библиотеки (libc, libm) ожидают выровненный стек. + +### Механика выравнивания + +```mermaid +graph TD + A["До вызова функции: rsp выровнен на 16"] --> B["call: push return address (8 байт)"] + B --> C["Внутри функции: rsp = старое_значение - 8"] + C --> D{"rsp кратен 16?"} + D -->|Нет| E["rsp = (old_rsp - 8) mod 16 = 8"] + E --> F["Невыровненный стек!"] + D -->|Да| G["Функция должна сохранить выравнивание"] + + style F fill:#ff6b6b + style G fill:#4ecdc4 +``` + +**Важное следствие:** после `call` стек **смещён на 8 байт** (return address). Если до `call` было `rsp % 16 == 0`, то после `call` будет `rsp % 16 == 8`. + +### Кто отвечает за выравнивание? + +**Вызывающая функция (caller):** +- Гарантирует, что `rsp % 16 == 0` перед инструкцией `call` +- Если нужно передать аргументы через стек (7+), должна учесть их при выравнивании + +**Вызываемая функция (callee):** +- Может предполагать, что `rsp % 16 == 8` в начале (после `call`) +- Если нужно вызвать другую функцию, должна восстановить выравнивание на 16 + +### Визуализация: выравнивание при вложенных вызовах + +```mermaid +sequenceDiagram + participant Main as main (rsp=0x7fff0000) + participant FuncA as func_a + participant FuncB as func_b + + Note over Main: rsp = 0x7fff0000 (кратно 16) + Main->>FuncA: call func_a + Note over FuncA: rsp = 0x7ffefff8 (0x7fff0000 - 8)
НЕ кратно 16! + + Note over FuncA: push rbp (rsp -= 8)
rsp = 0x7ffefff0 (кратно 16) + Note over FuncA: sub rsp, 24
rsp = 0x7ffeffe8 (НЕ кратно 16) + Note over FuncA: Нужно выровнять перед вызовом! + Note over FuncA: sub rsp, 8
rsp = 0x7ffeffd8 (кратно 16) + + FuncA->>FuncB: call func_b + Note over FuncB: rsp = 0x7ffeffD0 (0x7ffeffd8 - 8)
Смещение 8, как ожидалось + FuncB-->>FuncA: ret + + Note over FuncA: add rsp, 8 (убрать padding) + Note over FuncA: add rsp, 24 (освободить локальные) + Note over FuncA: pop rbp + FuncA-->>Main: ret +``` + +### Пример: ручное выравнивание стека + +```nasm +; Функция, которая вызывает printf с выравниванием стека +section .data + format_str: db "Value: %d", 10, 0 + +section .text +extern printf + +print_value: + ; Аргумент в rdi = значение для печати + + ; === ПРОЛОГ === + push rbp ; rsp -= 8, теперь rsp % 16 == 0 + mov rbp, rsp + + ; Сохранить аргумент (rdi может быть испорчен) + push rdi ; rsp -= 8, теперь rsp % 16 == 8 (невыровнен!) + + ; === ВЫРАВНИВАНИЕ === + ; Перед вызовом printf нужно rsp % 16 == 0 + ; Сейчас rsp % 16 == 8, поэтому нужно вычесть ещё 8 + sub rsp, 8 ; Padding для выравнивания + ; Теперь rsp % 16 == 0 + + ; === ВЫЗОВ PRINTF === + mov rdi, format_str ; 1-й аргумент: формат + mov rsi, [rbp - 8] ; 2-й аргумент: сохранённое значение + xor rax, rax ; printf - varargs, требует rax=0 + call printf ; Стек выровнен! + + ; === ОЧИСТКА === + add rsp, 8 ; Убрать padding + pop rdi ; Восстановить (хотя уже не нужен) + + ; === ЭПИЛОГ === + pop rbp + ret +``` + +### Автоматическое выравнивание: формула + +Предположим, функции нужно: +- Сохранить N callee-saved регистров (N × 8 байт) +- Выделить M байт для локальных переменных + +**Алгоритм:** + +1. После `push rbp` стек выровнен на 16 (так как до `call` было кратно 16, `call` вычел 8, `push rbp` вычел ещё 8) +2. Вычислить общий размер: `total = N*8 + M` +3. Если `total % 16 != 0`, добавить padding: `padding = (16 - (total % 16)) % 16` +4. Выделить: `sub rsp, total + padding` + +**Пример:** +```nasm +; Нужно: 3 callee-saved регистра (rbx, r12, r13) + 20 байт локальных +; total = 3*8 + 20 = 44 +; 44 % 16 = 12, padding = 16 - 12 = 4 + +push rbp +mov rbp, rsp +push rbx +push r12 +push r13 ; Сохранено 3*8 = 24 байта +sub rsp, 20 + 4 ; 20 локальных + 4 padding = 24 +; Теперь rsp % 16 == 0 +``` + +### Битовая маска для выравнивания + +Часто используется трюк с битовой маской: + +```nasm +; Выровнять rsp на 16-байтную границу (округлить вниз) +and rsp, -16 ; Обнулить младшие 4 бита +; -16 в дополнительном коде = 0xFFFFFFFFFFFFFFF0 +; AND с этой маской убирает биты [3:0] +``` + +**Пример использования:** +```nasm +_start: + ; rsp может быть любым при старте программы + and rsp, -16 ; Принудительно выровнять + call main ; Теперь main получит выровненный стек +``` + +### Типичная ошибка: забыть про выравнивание + +**Неправильно:** +```nasm +my_function: + push rbp + mov rbp, rsp + sub rsp, 32 ; Выделить 32 байта + ; rsp % 16 == 8 (невыровнен!) + call some_function ; SEGFAULT, если some_function использует SSE! +``` + +**Правильно:** +```nasm +my_function: + push rbp ; rsp % 16 == 0 после этого + mov rbp, rsp + sub rsp, 32 ; Всё ещё rsp % 16 == 0 (32 кратно 16) + call some_function ; OK! +``` + +**Или с нечётным размером:** +```nasm +my_function: + push rbp ; rsp % 16 == 0 + mov rbp, rsp + sub rsp, 24 ; 24 % 16 = 8, теперь rsp % 16 == 8 + sub rsp, 8 ; Добавить padding, rsp % 16 == 0 + call some_function ; OK! + add rsp, 32 ; Освободить всё (24 + 8) + pop rbp + ret +``` + +### Проверка выравнивания в GDB + +```bash +(gdb) p/x $rsp +$1 = 0x7fffffffe4a8 + +# Проверить выравнивание: +(gdb) p $rsp % 16 +$2 = 8 # Не выровнен! Должно быть 0 перед call + +# Или: +(gdb) p/t $rsp # Вывести в двоичном виде +$3 = 111111111111111111111111111100101000 +# ^^^^ младшие 4 бита должны быть 0000 +``` + +--- + +## 7. Конечные автоматы: теория + +### Что такое конечный автомат? + +**Конечный автомат** (Finite State Machine, FSM) — математическая модель вычислений, состоящая из: + +1. **Конечного множества состояний** Q = {q₀, q₁, ..., qₙ} +2. **Начального состояния** q₀ ∈ Q +3. **Множества принимающих (конечных) состояний** F ⊆ Q +4. **Входного алфавита** Σ (например, ASCII-символы) +5. **Функции переходов** δ: Q × Σ → Q + +**Детерминированный автомат (DFA):** для каждого состояния и символа существует ровно один переход. + +**Недетерминированный автомат (NFA):** может иметь несколько переходов для одной пары (состояние, символ) или ε-переходы. + +### Пример 1: проверка чётности количества единиц + +**Задача:** принять строку из '0' и '1', если в ней чётное количество единиц. + +```mermaid +stateDiagram-v2 + [*] --> Even: начало + Even --> Odd: 1 + Even --> Even: 0 + Odd --> Even: 1 + Odd --> Odd: 0 + Even --> [*]: конец (принять) + + note right of Even + Чётное количество 1 + (включая 0) + end note + + note right of Odd + Нечётное количество 1 + end note +``` + +**Таблица переходов:** + +| Состояние | Символ '0' | Символ '1' | +|-----------|------------|------------| +| Even | Even | Odd | +| Odd | Odd | Even | + +**Код на ассемблере:** + +```nasm +; Проверка чётности единиц +; Вход: rdi = указатель на строку +; Выход: rax = 1 (чётно), 0 (нечётно) + +check_parity: + xor rax, rax ; Начальное состояние: Even (rax = 0) + +.loop: + movzx rcx, byte [rdi] ; Прочитать символ + test rcx, rcx ; Конец строки? + jz .done + + cmp cl, '1' ; Если символ == '1' + jne .next + xor rax, 1 ; Переключить состояние (Even ↔ Odd) + +.next: + inc rdi + jmp .loop + +.done: + ; rax = 0 (Even, принять) или 1 (Odd, отклонить) + xor rax, 1 ; Инвертировать: 1 = принято, 0 = отклонено + ret +``` + +### Пример 2: распознавание целых чисел + +**Задача:** проверить, является ли строка корректным целым числом (возможен знак +/−, затем цифры). + +**Грамматика (регулярное выражение):** `[+-]?[0-9]+` + +**Автомат:** + +```mermaid +stateDiagram-v2 + [*] --> Start + Start --> Sign: +, - + Start --> Digit: 0-9 + Sign --> Digit: 0-9 + Digit --> Digit: 0-9 + Digit --> [*]: конец строки (принять) + + Start --> Error: другой символ + Sign --> Error: другой символ + Digit --> Error: другой символ + Error --> [*]: отклонить + + note right of Start + Начальное состояние + end note + + note right of Sign + Прочитан знак + end note + + note right of Digit + Прочитана хотя бы + одна цифра + end note +``` + +**Состояния:** +- **Start (A)**: начало, ожидаем знак или цифру +- **Sign (B)**: прочитан знак, ожидаем цифру +- **Digit (C)**: прочитана хотя бы одна цифра (принимающее состояние) +- **Error (E)**: обнаружен недопустимый символ (отклонение) + +**Таблица переходов:** + +| Состояние | '+' или '−' | '0'–'9' | Конец строки | Другое | +|-----------|-------------|---------|--------------|--------| +| Start (A) | Sign (B) | Digit (C)| Error (E) | Error (E)| +| Sign (B) | Error (E) | Digit (C)| Error (E) | Error (E)| +| Digit (C) | Error (E) | Digit (C)| **Accept** | Error (E)| +| Error (E) | Error (E) | Error (E)| **Reject** | Error (E)| + +### Реализация автомата в ассемблере: паттерн + +**Типичная структура:** + +```nasm +; Шаблон для реализации DFA +check_pattern: + ; Инициализация: начальное состояние + ; (можно использовать регистр для хранения состояния) + +.state_A: + call read_symbol ; Прочитать следующий символ в al + test al, al ; Конец ввода? + jz .reject ; В состоянии A конец = ошибка + + ; Проверить переходы из состояния A + cmp al, '+' + je .state_B + cmp al, '-' + je .state_B + cmp al, '0' + jb .reject + cmp al, '9' + ja .reject + jmp .state_C ; Цифра: переход в C + +.state_B: + call read_symbol + test al, al + jz .reject ; Только знак без цифр + + cmp al, '0' + jb .reject + cmp al, '9' + ja .reject + jmp .state_C + +.state_C: + call read_symbol + test al, al + jz .accept ; Конец в состоянии C = успех + + cmp al, '0' + jb .reject + cmp al, '9' + ja .reject + jmp .state_C ; Петля: ещё цифры + +.accept: + mov rax, 1 + ret + +.reject: + xor rax, rax + ret +``` + +### Свойства конечных автоматов + +**Преимущества:** +- Эффективная проверка за O(n), где n — длина входа +- Детерминированность: нет backtracking, каждый символ обрабатывается один раз +- Просто реализовать на низком уровне (ассемблер, FPGA) +- Легко доказать корректность (формальная верификация) + +**Ограничения:** +- Не могут считать (например, проверить баланс скобок) +- Не могут распознать язык `{aⁿbⁿ | n ≥ 0}` (нужен стек → автомат с магазинной памятью) +- Не могут проверить, что число является палиндромом произвольной длины + +**Что могут DFA:** +- Все регулярные языки (эквивалентны регулярным выражениям) +- Лексический анализ (токенизация) +- Валидация формата (email, URL, числа) +- Протоколы с фиксированным числом состояний + +--- + +## 8. Реализация FSM: анализ check_int.asm + +### Автомат из check_int.asm + +Программа `check_int.asm` реализует автомат для распознавания целых чисел (с опциональным знаком). Разберём его подробно. + +**Диаграмма состояний:** + +```mermaid +stateDiagram-v2 + [*] --> A: начало + A --> B: + или - + A --> C: 0-9 + A --> E: другое + + B --> C: 0-9 + B --> E: другое + + C --> C: 0-9 + C --> D: конец ввода (\0 или \n) + C --> E: другое + + D --> [*]: принять (Okay) + E --> [*]: отклонить (Fail) + + note right of A + Начальное состояние: + ожидаем знак или цифру + end note + + note right of B + Прочитан знак: + теперь нужна цифра + end note + + note right of C + Прочитана хотя бы + одна цифра: + принимающее состояние + end note +``` + +### Код с комментариями + +```nasm +global _start +_start: + ; === СОСТОЯНИЕ A: начало === + _A: + call getsymbol ; Прочитать символ в al + + ; Проверить на знак '+' + cmp al, '+' + je _B ; Переход A → B + + ; Проверить на знак '-' + cmp al, '-' + je _B ; Переход A → B + + ; Проверить на цифру (ASCII '0' = 0x30, '9' = 0x39) + cmp al, '0' + jb _E ; Меньше '0' → ошибка + cmp al, '9' + ja _E ; Больше '9' → ошибка + jmp _C ; Цифра: переход A → C + + ; === СОСТОЯНИЕ B: прочитан знак === + _B: + call getsymbol ; Прочитать следующий символ + + ; После знака ОБЯЗАТЕЛЬНО должна быть цифра + cmp al, '0' + jb _E + cmp al, '9' + ja _E + jmp _C ; Переход B → C + + ; === СОСТОЯНИЕ C: читаем цифры === + _C: + call getsymbol + + ; Проверить на конец ввода + test al, al ; Проверить на NULL (\0) + jz _D ; Конец ввода → принять (C → D) + + cmp al, 10 ; Проверить на \n (newline) + jz _D ; Newline → принять (C → D) + + ; Проверить, ещё одна цифра? + cmp al, '0' + jb _E ; Не цифра → ошибка + cmp al, '9' + ja _E + jmp _C ; Ещё цифра → петля C → C + + ; === СОСТОЯНИЕ D: принятие === + _D: + mov rdi, ok_msg ; Указатель на "Okay\n" + mov rsi, 5 ; Длина строки + call print_string + call exit + + ; === СОСТОЯНИЕ E: ошибка === + _E: + mov rdi, fail_msg ; Указатель на "Fail\n" + mov rsi, 5 + call print_string + call exit +``` + +### Ключевые особенности реализации + +1. **Метки = состояния**: каждой метке (`_A`, `_B`, `_C`) соответствует состояние автомата +2. **Переходы = переходы**: `jmp`, `je`, `ja`, `jb` реализуют функцию переходов δ +3. **Чтение символов**: функция `getsymbol` читает по одному символу из stdin +4. **Обработка EOF**: `getsymbol` возвращает 0 при конце файла +5. **Принимающее состояние**: только состояние C может перейти в D (принятие) + +### Функция getsymbol: чтение одного символа + +```nasm +getsymbol: + xor rax, rax ; syscall 0 = read + xor rdi, rdi ; fd = 0 (stdin) + dec rsp ; Выделить 1 байт на стеке + mov rsi, rsp ; Буфер = вершина стека + mov rdx, 1 ; Читать 1 байт + syscall ; read(0, rsp, 1) + + test rax, rax ; Проверить результат + jz .EOF ; Если rax == 0 → конец файла + + mov al, [rsp] ; Загрузить прочитанный символ + jmp .END + +.EOF: + xor rax, rax ; Вернуть 0 (конец ввода) + +.END: + inc rsp ; Восстановить стек + ret +``` + +**Особенности:** +- Использует стек как временный буфер (1 байт) +- `dec rsp` / `inc rsp` выделяют и освобождают место на стеке +- Возвращает символ в `al` (младший байт rax) +- При EOF возвращает 0 + +--- + +## 9. Пошаговая трассировка работы FSM + +### Пример 1: ввод "123" + +**Входная строка:** `'1', '2', '3', '\n'` + +| Шаг | Состояние | Прочитанный символ (al) | Проверка | Переход | +|------|----------|-------------------|-----------|---------| +| 1 | **A** (начало) | '1' (0x31) | Не '+', не '−' | Проверка цифры | +| | | | '0' ≤ '1' ≤ '9' | **A → C** | +| 2 | **C** | '2' (0x32) | Не \0, не \n | Проверка цифры | +| | | | '0' ≤ '2' ≤ '9' | **C → C** (петля) | +| 3 | **C** | '3' (0x33) | Не \0, не \n | Проверка цифры | +| | | | '0' ≤ '3' ≤ '9' | **C → C** | +| 4 | **C** | '\n' (0x0A) | cmp al, 10 | **C → D** | +| 5 | **D** (принятие) | — | Вывод "Okay" | exit(0) | + +**Результат:** принято ✓ + +### Пример 2: ввод "-45" + +**Входная строка:** `'-', '4', '5', '\n'` + +| Шаг | Состояние | Прочитанный символ | Проверка | Переход | +|------|----------|-------------------|-----------|---------| +| 1 | **A** | '−' (0x2D) | cmp al, '-' | **A → B** (je _B) | +| 2 | **B** | '4' (0x34) | '0' ≤ '4' ≤ '9' | **B → C** | +| 3 | **C** | '5' (0x35) | Не \0, не \n | Проверка цифры | +| | | | '0' ≤ '5' ≤ '9' | **C → C** | +| 4 | **C** | '\n' (0x0A) | cmp al, 10 | **C → D** | +| 5 | **D** | — | Вывод "Okay" | exit(0) | + +**Результат:** принято ✓ + +### Пример 3: ввод "+" (ошибка) + +**Входная строка:** `'+', '\n'` + +| Шаг | Состояние | Прочитанный символ | Проверка | Переход | +|------|----------|-------------------|-----------|---------| +| 1 | **A** | '+' (0x2B) | cmp al, '+' | **A → B** | +| 2 | **B** | '\n' (0x0A) | cmp al, '0' | al < '0' (0x0A < 0x30) | +| | | | jb _E | **B → E** | +| 3 | **E** (ошибка) | — | Вывод "Fail" | exit(0) | + +**Результат:** отклонено ✗ (знак без цифр) + +### Пример 4: ввод "12a3" (ошибка) + +**Входная строка:** `'1', '2', 'a', '3'` + +| Шаг | Состояние | Прочитанный символ | Проверка | Переход | +|------|----------|-------------------|-----------|---------| +| 1 | **A** | '1' | Цифра | **A → C** | +| 2 | **C** | '2' | Цифра | **C → C** | +| 3 | **C** | 'a' (0x61) | Не \0, не \n | Проверка цифры | +| | | | cmp al, '9' | 'a' > '9' (0x61 > 0x39) | +| | | | ja _E | **C → E** | +| 4 | **E** | — | Вывод "Fail" | exit(0) | + +**Результат:** отклонено ✗ (недопустимый символ) + +### Детальная трассировка с регистрами: "-7" + +**Вход:** `echo "-7" | ./check_int` + +| Шаг | RIP (метка) | Инструкция | al | Флаги | Примечание | +|------|---------|------------|-----|--------|-------------| +| 1 | `_A` | `call getsymbol` | ? | — | Чтение символа | +| 2 | `_A` | (after call) | 0x2D ('-') | — | Символ прочитан | +| 3 | `_A` | `cmp al, '+'` | 0x2D | ZF=0 | 0x2D ≠ 0x2B | +| 4 | `_A` | `je _B` | 0x2D | ZF=0 | Не переходит | +| 5 | `_A` | `cmp al, '-'` | 0x2D | ZF=1 | 0x2D == 0x2D | +| 6 | `_A` | `je _B` | 0x2D | ZF=1 | **Переход A → B** | +| 7 | `_B` | `call getsymbol` | 0x2D | — | Читаем след. символ | +| 8 | `_B` | (after call) | 0x37 ('7') | — | Прочитано '7' | +| 9 | `_B` | `cmp al, '0'` | 0x37 | CF=0 | 0x37 ≥ 0x30 | +| 10 | `_B` | `jb _E` | 0x37 | CF=0 | Не переходит | +| 11 | `_B` | `cmp al, '9'` | 0x37 | CF=1 | 0x37 < 0x39 | +| 12 | `_B` | `ja _E` | 0x37 | CF=1 | Не переходит | +| 13 | `_B` | `jmp _C` | 0x37 | — | **Переход B → C** | +| 14 | `_C` | `call getsymbol` | 0x37 | — | Читаем след. символ | +| 15 | `_C` | (after call) | 0x0A ('\n') | — | Прочитан newline | +| 16 | `_C` | `test al, al` | 0x0A | ZF=0 | 0x0A ≠ 0 | +| 17 | `_C` | `jz _D` | 0x0A | ZF=0 | Не переходит | +| 18 | `_C` | `cmp al, 10` | 0x0A | ZF=1 | 0x0A == 10 | +| 19 | `_C` | `jz _D` | 0x0A | ZF=1 | **Переход C → D** | +| 20 | `_D` | `mov rdi, ok_msg` | — | — | Подготовка вывода | +| 21 | `_D` | `call print_string` | — | — | Вывод "Okay\n" | +| 22 | — | `call exit` | — | — | exit(0) | + +**Результат:** программа выводит "Okay" и завершается с кодом 0 ✓ + +### Визуализация переходов для "-7" + +```mermaid +graph LR + Start([Запуск]) --> A + A[Состояние A
читает '-'] --> B[Состояние B
читает '7'] + B --> C[Состояние C
читает newline] + C --> D[Состояние D
выводит Okay] + D --> End([exit 0]) + + style D fill:#51cf66 + style End fill:#51cf66 +``` + +--- + +## 10. Типичные ошибки при работе со стеком и функциями + +### Ошибка 1: Неправильное выравнивание стека + +**Проблема:** забыли выровнять стек перед `call`, что приводит к SEGFAULT в функциях, использующих SSE. + +**Неправильный код:** +```nasm +my_function: + push rbp ; rsp % 16 == 0 после этого + mov rbp, rsp + sub rsp, 24 ; 24 % 16 = 8 → rsp % 16 == 8 (НЕВЫРОВНЕН!) + + ; Передать параметры + mov rdi, format_str + mov rsi, 42 + call printf ; ❌ SEGFAULT! printf ожидает выровненный стек + + add rsp, 24 + pop rbp + ret +``` + +**Правильный код:** +```nasm +my_function: + push rbp ; rsp % 16 == 0 + mov rbp, rsp + sub rsp, 32 ; 32 % 16 == 0 → rsp % 16 == 0 (ВЫРОВНЕН!) + + mov rdi, format_str + mov rsi, 42 + call printf ; ✓ Работает корректно + + add rsp, 32 + pop rbp + ret +``` + +**Альтернатива с padding:** +```nasm +my_function: + push rbp ; rsp % 16 == 0 + mov rbp, rsp + sub rsp, 24 ; Выделить 24 байта локальных + sub rsp, 8 ; Добавить 8 байт padding → итого 32 + ; Теперь rsp % 16 == 0 + + call printf ; ✓ OK + + add rsp, 32 ; Освободить всё (24 + 8) + pop rbp + ret +``` + +### Ошибка 2: Не сохранили callee-saved регистры + +**Проблема:** использовали rbx/r12-r15 без сохранения, вызывающая функция получила испорченные данные. + +**Неправильный код:** +```nasm +calculate: + ; Вызывающая функция полагается на rbx + mov rbx, rdi ; ❌ Испортили rbx! + add rbx, 10 + mov rax, rbx + ret ; Вернулись, но rbx испорчен +``` + +**Правильный код:** +```nasm +calculate: + push rbx ; Сохранить rbx + + mov rbx, rdi + add rbx, 10 + mov rax, rbx + + pop rbx ; Восстановить rbx + ret ; ✓ Теперь вызывающая функция видит исходный rbx +``` + +### Ошибка 3: Дисбаланс стека (push/pop не совпадают) + +**Проблема:** количество `push` не равно количеству `pop`, стек смещён при `ret`. + +**Неправильный код:** +```nasm +broken_function: + push rbx + push r12 + push r13 ; Сохранили 3 регистра + + ; ... тело функции ... + + pop r13 + pop r12 ; ❌ Забыли pop rbx! + ret ; Возврат по неправильному адресу → CRASH +``` + +**Правильный код:** +```nasm +correct_function: + push rbx + push r12 + push r13 + + ; ... тело функции ... + + pop r13 + pop r12 + pop rbx ; ✓ Все 3 регистра восстановлены + ret +``` + +**Диагностика в GDB:** +```bash +(gdb) break broken_function +(gdb) run +(gdb) info registers rsp +# Запомнить значение rsp + +(gdb) stepi # Пошагово до ret +(gdb) info registers rsp +# Если значение отличается на неожиданную величину → дисбаланс! +``` + +### Ошибка 4: Неправильный порядок pop + +**Проблема:** `pop` в обратном порядке по отношению к `push`, регистры получают неправильные значения. + +**Неправильный код:** +```nasm +wrong_order: + push rbx ; Стек: [rbx] + push r12 ; Стек: [r12, rbx] + push r13 ; Стек: [r13, r12, rbx] + + ; ... + + pop rbx ; ❌ rbx получит значение r13! + pop r12 ; r12 получит значение r12 (случайно правильно) + pop r13 ; r13 получит значение rbx + ret +``` + +**Правильный код:** +```nasm +correct_order: + push rbx ; Стек: [rbx] + push r12 ; Стек: [r12, rbx] + push r13 ; Стек: [r13, r12, rbx] + + ; ... + + pop r13 ; ✓ r13 восстановлен + pop r12 ; ✓ r12 восстановлен + pop rbx ; ✓ rbx восстановлен + ret +``` + +**Мнемоника:** LIFO (Last In, First Out) — что последним положили, то первым достаём. + +### Ошибка 5: Использование caller-saved регистра без учёта его изменения + +**Проблема:** положились на сохранение значения в r10/r11 через вызов функции. + +**Неправильный код:** +```nasm +buggy_code: + mov r10, 100 ; Сохранили значение в r10 + mov rdi, 42 + call some_function ; some_function может испортить r10! + + add rax, r10 ; ❌ r10 может содержать мусор! + ret +``` + +**Правильный код (вариант 1: использовать callee-saved):** +```nasm +fixed_code_v1: + push rbx ; Сохранить rbx + mov rbx, 100 ; Использовать rbx вместо r10 + mov rdi, 42 + call some_function ; rbx НЕ изменится + + add rax, rbx ; ✓ rbx содержит 100 + pop rbx + ret +``` + +**Правильный код (вариант 2: сохранить на стеке):** +```nasm +fixed_code_v2: + mov r10, 100 + push r10 ; Сохранить r10 на стеке + + mov rdi, 42 + call some_function + + pop r10 ; Восстановить r10 + add rax, r10 ; ✓ r10 содержит 100 + ret +``` + +### Ошибка 6: Неправильное использование локальных переменных + +**Проблема:** забыли выделить место на стеке для локальных переменных или обращаемся по неправильному смещению. + +**Неправильный код:** +```nasm +bad_locals: + push rbp + mov rbp, rsp + ; ❌ Забыли sub rsp, N! + + mov qword [rbp - 8], 42 ; Перезаписываем старый rbp! + mov rax, [rbp - 8] + + pop rbp + ret +``` + +**Правильный код:** +```nasm +good_locals: + push rbp + mov rbp, rsp + sub rsp, 16 ; ✓ Выделили 16 байт для локальных + + mov qword [rbp - 8], 42 ; Локальная переменная 1 + mov qword [rbp - 16], 100 ; Локальная переменная 2 + + mov rax, [rbp - 8] + add rax, [rbp - 16] + + mov rsp, rbp ; Освободить локальные + pop rbp + ret +``` + +### Ошибка 7: Неполный FSM (отсутствующие переходы) + +**Проблема:** автомат не обрабатывает все возможные входные символы, программа падает или зависает. + +**Неправильный код:** +```nasm +incomplete_fsm: + _A: + call getsymbol + cmp al, '0' + jb _C ; Если < '0' + cmp al, '9' + ja _C ; Если > '9' + jmp _B ; Иначе цифра + + _B: + ; ❌ Что дальше? Нет обработки следующего символа! + ret ; Немедленный возврат? +``` + +**Правильный код:** +```nasm +complete_fsm: + _A: + call getsymbol + test al, al ; ✓ Проверить EOF + jz _ERROR + + cmp al, '0' + jb _ERROR + cmp al, '9' + ja _ERROR + jmp _B + + _B: + call getsymbol ; ✓ Продолжить чтение + test al, al + jz _ACCEPT ; EOF после цифры = OK + + cmp al, '0' + jb _ERROR + cmp al, '9' + ja _ERROR + jmp _B ; Петля для нескольких цифр + + _ACCEPT: + mov rax, 1 + ret + + _ERROR: + xor rax, rax + ret +``` + +### Ошибка 8: Неправильный выбор условного перехода (signed vs unsigned) + +**Проблема:** использовали `jl`/`jg` для беззнаковых чисел или `jb`/`ja` для знаковых. + +**Неправильный код:** +```nasm +; Проверить, что ASCII код в диапазоне [0x30, 0x39] +check_digit_wrong: + cmp al, 0x30 + jl .not_digit ; ❌ jl для знакового сравнения! + cmp al, 0x39 ; ASCII коды беззнаковые + jg .not_digit ; ❌ jg для знакового сравнения! + ; ... +``` + +**Правильный код:** +```nasm +; ASCII коды — беззнаковые значения +check_digit_correct: + cmp al, 0x30 + jb .not_digit ; ✓ jb (below) для unsigned + cmp al, 0x39 + ja .not_digit ; ✓ ja (above) для unsigned + ; ... +``` + +**Таблица выбора условных переходов:** + +| Тип данных | "Меньше" | "Меньше/равно" | "Больше" | "Больше/равно" | +|------------|----------|----------------|----------|----------------| +| Unsigned | `jb` | `jbe` | `ja` | `jae` | +| Signed | `jl` | `jle` | `jg` | `jge` | + +**Пример ошибки:** `-1` (0xFF) как unsigned = 255, как signed = -1. Сравнение с 10: +- `cmp al, 10; jb` → 255 > 10, не переходит (неправильно, если ожидали signed) +- `cmp al, 10; jl` → -1 < 10, переходит (правильно для signed) + +--- + +## 11. Практические задания + +### Задание 1: Функция с локальными переменными + +Напишите функцию `sum_array`, которая: +- Принимает указатель на массив (rdi) и длину (rsi) +- Создаёт локальную переменную-счётчик на стеке +- Возвращает сумму элементов в rax +- Корректно выравнивает стек + +**Требования:** +- Использовать фрейм стека (rbp) +- Сохранить callee-saved регистры при необходимости +- Правильное выравнивание для возможных вложенных вызовов + +### Задание 2: FSM для проверки email + +Реализуйте конечный автомат для упрощённой проверки email формата: `[a-z]+@[a-z]+\.[a-z]+` + +**Состояния:** +- A: начало, ожидаем букву (локальная часть) +- B: читаем локальную часть +- C: прочитан @, ожидаем букву (домен) +- D: читаем домен +- E: прочитана точка, ожидаем букву (TLD) +- F: читаем TLD (принимающее состояние) +- ERR: ошибка + +**Пример входов:** +- `"user@example.com"` → принять +- `"test@domain.org"` → принять +- `"invalid"` → отклонить +- `"@example.com"` → отклонить +- `"user@.com"` → отклонить + +### Задание 3: Рекурсивная функция факториала + +Реализуйте рекурсивную функцию `factorial(n)`: + +```nasm +; factorial(n) = n * factorial(n-1), factorial(0) = 1 +; Вход: rdi = n +; Выход: rax = n! + +factorial: + ; TODO: реализовать с правильным управлением стеком +``` + +**Требования:** +- Обработать базовый случай (n = 0) +- Сохранить rdi перед рекурсивным вызовом (caller-saved!) +- Правильно восстановить стек +- Проверить для n = 5 (должно быть 120) + +### Задание 4: FSM с подсчётом + +Напишите автомат, который: +- Читает строку из цифр +- Подсчитывает количество нулей +- Возвращает count в rax +- Использует локальную переменную-счётчик на стеке + +**Пример:** +- `"12003"` → возвращает 2 +- `"999"` → возвращает 0 +- `"00000"` → возвращает 5 + +### Задание 5: Отладка с GDB + +Дан код с ошибкой: + +```nasm +mystery_function: + push rbp + mov rbp, rsp + push rbx + push r12 + + mov rbx, rdi + mov r12, rsi + + ; Какие-то вычисления... + add rbx, r12 + mov rax, rbx + + pop rbx ; ❌ Ошибка здесь! + pop r12 + pop rbp + ret +``` + +**Задания:** +1. Найдите ошибку визуально +2. Проверьте с GDB, какие регистры испорчены +3. Исправьте код +4. Убедитесь, что все callee-saved регистры восстановлены правильно + +--- + +## 12. Справочная информация + +### Calling Convention System V AMD64 ABI (краткая памятка) + +**Передача аргументов:** +| Аргумент | Регистр | Тип | +|----------|---------|-----| +| 1 | rdi | integer/pointer | +| 2 | rsi | integer/pointer | +| 3 | rdx | integer/pointer | +| 4 | rcx | integer/pointer | +| 5 | r8 | integer/pointer | +| 6 | r9 | integer/pointer | +| 7+ | стек | push в обратном порядке | + +**Возвращаемое значение:** rax (или rax:rdx для 128-бит) + +**Caller-saved (может быть испорчен вызываемой функцией):** +- rax, rcx, rdx, rsi, rdi, r8, r9, r10, r11 + +**Callee-saved (должен быть сохранён вызываемой функцией):** +- rbx, rbp, r12, r13, r14, r15 +- rsp (указатель стека) + +**Выравнивание:** `rsp % 16 == 0` перед инструкцией `call` + +### Инструкции управления стеком + +| Инструкция | Описание | Эффект на rsp | +|------------|----------|---------------| +| `push reg` | Сохранить регистр на стеке | rsp -= 8 | +| `pop reg` | Восстановить регистр со стека | rsp += 8 | +| `call addr` | Вызвать функцию (push rip, jmp addr) | rsp -= 8 | +| `ret` | Вернуться (pop rip) | rsp += 8 | +| `leave` | Эквивалент `mov rsp, rbp; pop rbp` | rsp = rbp, затем rsp += 8 | +| `enter N, 0` | Создать фрейм стека (медленно, не рекомендуется) | | + +### Условные переходы для FSM + +**После `cmp op1, op2`:** + +| Условие | Unsigned | Signed | Флаги | +|---------|----------|--------|-------| +| Равно | `je` / `jz` | `je` / `jz` | ZF=1 | +| Не равно | `jne` / `jnz` | `jne` / `jnz` | ZF=0 | +| op1 < op2 | `jb` | `jl` | CF=1 (unsigned) / SF≠OF (signed) | +| op1 ≤ op2 | `jbe` | `jle` | CF=1 or ZF=1 / ZF=1 or SF≠OF | +| op1 > op2 | `ja` | `jg` | CF=0 and ZF=0 / ZF=0 and SF=OF | +| op1 ≥ op2 | `jae` | `jge` | CF=0 / SF=OF | + +**Мнемоника:** +- **B**elow / **A**bove — для unsigned +- **L**ess / **G**reater — для signed +- **E**qual — для любых + +### Системные вызовы (частичный список) + +| Номер | Имя | rdi | rsi | rdx | Описание | +|-------|-----|-----|-----|-----|----------| +| 0 | read | fd | buf* | count | Читать из файла | +| 1 | write | fd | buf* | count | Писать в файл | +| 60 | exit | code | — | — | Завершить процесс | + +**Использование:** +```nasm +mov rax, syscall_number +mov rdi, arg1 +mov rsi, arg2 +mov rdx, arg3 +syscall +; Результат в rax +``` + +### Отладка с GDB + +**Основные команды:** + +```bash +# Запуск +gdb ./program +(gdb) break _start # Точка останова +(gdb) run # Запустить + +# Пошаговое выполнение +(gdb) stepi # Выполнить одну инструкцию +(gdb) nexti # Следующая инструкция (не заходить в call) +(gdb) continue # Продолжить до следующей точки останова + +# Просмотр +(gdb) info registers # Все регистры +(gdb) info registers rax rbx rsp # Конкретные регистры +(gdb) x/10gx $rsp # 10 qword в памяти от rsp (стек) +(gdb) x/s $rdi # Строка по адресу в rdi +(gdb) disassemble # Дизассемблировать текущую функцию + +# Точки останова +(gdb) break _A # Остановиться на метке _A +(gdb) break *0x401234 # Остановиться на адресе +(gdb) delete 1 # Удалить точку останова №1 + +# Проверка выравнивания +(gdb) p $rsp % 16 # Должно быть 0 перед call +``` + +### Полезные макросы для NASM + +```nasm +; Сохранить все callee-saved регистры +%macro SAVE_CALLEE 0 + push rbx + push r12 + push r13 + push r14 + push r15 +%endmacro + +; Восстановить все callee-saved регистры +%macro RESTORE_CALLEE 0 + pop r15 + pop r14 + pop r13 + pop r12 + pop rbx +%endmacro + +; Стандартный пролог функции +%macro FUNC_PROLOGUE 1 + push rbp + mov rbp, rsp + sub rsp, %1 ; %1 = размер локальных переменных +%endmacro + +; Стандартный эпилог функции +%macro FUNC_EPILOGUE 0 + mov rsp, rbp + pop rbp + ret +%endmacro +``` + +**Использование:** +```nasm +my_function: + FUNC_PROLOGUE 32 ; Выделить 32 байта локальных + SAVE_CALLEE ; Сохранить регистры + + ; Тело функции + + RESTORE_CALLEE + FUNC_EPILOGUE +``` + +### Ссылки на документацию + +- [System V AMD64 ABI](https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf) — официальное соглашение о вызовах +- [Intel Software Developer Manual](https://software.intel.com/content/www/us/en/develop/articles/intel-sdm.html) — полное описание инструкций x86-64 +- [Linux System Call Table](https://filippo.io/linux-syscall-table/) — таблица системных вызовов +- [NASM Documentation](https://www.nasm.us/doc/) — документация по ассемблеру NASM + +--- + +**Конец методички для семинара 2** diff --git a/ЯПы/ЯПы.md b/ЯПы/ЯПы.md new file mode 100644 index 0000000..1df9e10 --- /dev/null +++ b/ЯПы/ЯПы.md @@ -0,0 +1,6 @@ + +## Что это? +Методички по семенарам (разжёванная теория для самых маленьких + доп материалы) + +[[Семинар-1]] - Окружение, инструменты, первая программа +[[Семинар-2]] - Функции, стек, конечные автоматы \ No newline at end of file -- 2.52.0