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

1512 lines
58 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Методичка по языку ассемблера 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<br/>Высокие адреса] --> B[Stack<br/>Стек растёт вниз]
B --> C[...]
C --> D[Memory Mapping<br/>mmap, разделяемые библиотеки]
D --> E[...]
E --> F[Heap<br/>Куча растёт вверх]
F --> G[BSS<br/>Неинициализированные данные]
G --> H[Data<br/>Инициализированные данные]
H --> I[Text<br/>Код программы read-only]
I --> J[0x0000000000400000<br/>Низкие адреса]
```
### Секции исполняемого файла ELF
| Секция | Права доступа | Содержимое | Размер определяется |
|--------|---------------|------------|---------------------|
| `.text` | r-x | Машинный код программы | На этапе компиляции |
| `.rodata` | r-- | Константы, строковые литералы | На этапе компиляции |
| `.data` | rw- | Инициализированные глобальные переменные | На этапе компиляции |
| `.bss` | rw- | Неинициализированные глобальные переменные | На этапе компиляции (обнуляется при загрузке) |
| **stack** | rw- | Локальные переменные, адреса возврата | Динамически (растёт при вызовах функций) |
| **heap** | rw- | Динамически выделяемая память (malloc) | Динамически (по запросу программы) |
### Визуализация секций программы
```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<br/>Загрузить инструкцию<br/>из памяти по адресу RIP] --> B[DECODE<br/>Декодировать<br/>опкод и операнды]
B --> C[EXECUTE<br/>Выполнить операцию<br/>обновить регистры/память]
C --> D[Увеличить RIP<br/>на длину инструкции]
D --> A
```
### Детальное объяснение фаз
1. **FETCH (Выборка)**
- Процессор читает байты инструкции из памяти по адресу, на который указывает регистр `RIP` (Instruction Pointer)
- В x86-64 инструкции имеют переменную длину: от 1 до 15 байт
- Процессор использует кэш инструкций (I-cache) для ускорения выборки
2. **DECODE (Декодирование)**
- Декодер разбирает байты инструкции:
- Префиксы (опционально)
- Опкод (код операции)
- ModR/M байт (режим адресации и операнды)
- SIB байт (scale-index-base, для сложной адресации)
- Смещение и immediate-значения
- Определяет, какие исполнительные блоки нужны (ALU, FPU, Load/Store Unit)
3. **EXECUTE (Выполнение)**
- Исполнительные блоки выполняют операцию:
- Арифметико-логическое устройство (ALU) для вычислений
- Блок загрузки/сохранения для операций с памятью
- Блок управления потоком для переходов
- Обновляются регистры назначения и флаги
4. **Обновление RIP**
- `RIP` увеличивается на длину выполненной инструкции
- Для инструкций перехода (jump, call, ret) `RIP` устанавливается в целевой адрес
- Для условных переходов `RIP` изменяется только если условие истинно
### Пример: пошаговое выполнение
Рассмотрим последовательность инструкций:
```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<br/>Исходный код] --> B[nasm]
B --> C[hello.o<br/>Объектный файл<br/>машинный код + метаданные]
C --> D[ld]
D --> E[hello<br/>Исполняемый файл 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<br/>(программа)
participant C as CPU
participant K as Kernel Space<br/>(ядро Linux)
U->>C: syscall (0x0F 0x05)
Note over C: Сохранить контекст
C->>K: Передать управление<br/>Ring 3 → Ring 0
Note over K: Проверить права<br/>Выполнить операцию
K->>C: Вернуть результат в RAX
Note over C: Восстановить контекст
C->>U: Продолжить выполнение
**Кольца защиты (Protection Rings):**
- **Ring 0** (kernel mode): полный доступ ко всему оборудованию, выполнение привилегированных инструкций
- **Ring 3** (user mode): ограниченный доступ, не может напрямую взаимодействовать с устройствами
### Механизм вызова
**Инструкция `syscall` (x86-64):**
```nasm
syscall ; 0x0F 0x05 в машинном коде
```
**Передача параметров:**
| Параметр | Регистр | Описание |
|----------|---------|----------|
| Номер syscall | rax | Идентификатор системного вызова (0-331+) |
| Аргумент 1 | rdi | Первый параметр |
| Аргумент 2 | rsi | Второй параметр |
| Аргумент 3 | rdx | Третий параметр |
| Аргумент 4 | r10 | Четвёртый параметр (не rcx!) |
| Аргумент 5 | r8 | Пятый параметр |
| Аргумент 6 | r9 | Шестой параметр |
| **Возвращаемое значение** | **rax** | **Результат выполнения** |
**Важно:** в отличие от calling convention функций, системные вызовы используют **r10** вместо rcx для 4-го аргумента (rcx используется ядром для сохранения return address).
### Основные системные вызовы
#### 1. read (номер 0)
Чтение данных из файлового дескриптора.
```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 бит<br/>0x0123456789ABCDEF] --> B[EAX 32 бит младших<br/>0x89ABCDEF]
B --> C[AX 16 бит младших<br/>0xCDEF]
C --> D[AH 8 бит старших<br/>0xCD]
C --> E[AL 8 бит младших<br/>0xEF]
style A fill:#4ecdc4
style B fill:#95e1d3
style C fill:#f38181
style D fill:#ffd93d
style E fill:#6bcf7f
```
### Полный список регистров общего назначения
| 64-бит | 32-бит | 16-бит | 8-бит (low) | 8-бит (high) | Назначение в calling convention |
|--------|--------|--------|-------------|--------------|--------------------------------|
| rax | eax | ax | al | ah | Возвращаемое значение, accumulator |
| rbx | ebx | bx | bl | bh | Callee-saved, base |
| rcx | ecx | cx | cl | ch | 4-й аргумент, counter |
| rdx | edx | dx | dl | dh | 3-й аргумент, data |
| rsi | esi | si | sil | — | 2-й аргумент, source index |
| rdi | edi | di | dil | — | 1-й аргумент, destination index |
| rsp | esp | sp | spl | — | Stack pointer (указатель стека) |
| rbp | ebp | bp | bpl | — | Base pointer (база фрейма) |
| r8 | r8d | r8w | r8b | — | 5-й аргумент |
| r9 | r9d | r9w | r9b | — | 6-й аргумент |
| r10 | r10d | r10w | r10b | — | Caller-saved |
| r11 | r11d | r11w | r11b | — | Caller-saved |
| r12 | r12d | r12w | r12b | — | Callee-saved |
| r13 | r13d | r13w | r13b | — | Callee-saved |
| r14 | r14d | r14w | r14b | — | Callee-saved |
| r15 | r15d | r15w | r15b | — | Callee-saved |
**Важно:** запись в младшую 32-битную часть (eax, ebx, ...) **обнуляет** старшие 32 бита регистра!
```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**