1512 lines
58 KiB
Markdown
1512 lines
58 KiB
Markdown
# Методичка по языку ассемблера 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**
|