58 KiB
Методичка по языку ассемблера x86-64
Семинар 1: Окружение, инструменты, первая программа
Содержание
- Необходимые инструменты
- Командная оболочка и окружение выполнения
- Регулярные выражения и grep
- Переменные окружения
- Модель памяти процесса x86-64
- Цикл выполнения инструкций
- Компиляция программ на ассемблере
- Разбор программы Hello World
- Системные вызовы Linux x86-64
- Регистры процессора x86-64
- Отладчик GDB
- Типичные ошибки и их решение
- Практические задания
- Справочник: часто используемые конструкции
- Полезные ссылки и документация
1. Необходимые инструменты
Базовый набор инструментов
Для работы с ассемблером x86-64 в Linux потребуются:
- NASM (Netwide Assembler) — ассемблер, переводящий мнемоники в машинный код
- ld (GNU linker) — компоновщик, создающий исполняемые файлы из объектных файлов
- GDB (GNU Debugger) — отладчик для пошагового выполнения и анализа программ
- make — утилита для автоматизации сборки
- grep, sed, awk — инструменты обработки текста
Установка в Ubuntu/Debian
sudo apt update
sudo apt install nasm gdb build-essential
Установка в Fedora/RHEL
sudo dnf install nasm gdb make gcc
Проверка установки
nasm -version # Должна показать версию NASM
ld --version # Должна показать версию GNU ld
gdb --version # Должна показать версию GDB
Виртуальные машины
Если вы работаете на Windows или macOS, рекомендуется использовать виртуальную машину с Linux:
- VirtualBox + Ubuntu 22.04 LTS
- VMware Workstation Player + Fedora
- WSL2 (Windows Subsystem for Linux) — для Windows 10/11
Важно: Убедитесь, что виртуализация включена в BIOS/UEFI, и что VM имеет доступ к 64-битным инструкциям процессора.
2. Командная оболочка и окружение выполнения
Стандартные потоки ввода-вывода
Каждый процесс в Unix/Linux имеет три предопределённых потока:
| Поток | Дескриптор | Назначение | Обычное устройство |
|---|---|---|---|
| stdin | 0 | Стандартный ввод | Клавиатура |
| stdout | 1 | Стандартный вывод | Терминал |
| stderr | 2 | Стандартный поток ошибок | Терминал |
Перенаправление потоков
# Перенаправление stdout в файл (перезапись)
./program > output.txt
# Перенаправление stdout в файл (добавление)
./program >> output.txt
# Перенаправление stderr в файл
./program 2> errors.txt
# Перенаправление и stdout, и stderr в один файл
./program > all.txt 2>&1
# Перенаправление stdin из файла
./program < input.txt
# Конвейер (pipe): stdout первой программы → stdin второй
./program1 | ./program2
Пример: разделение потоков
Создайте тестовый скрипт test.sh:
#!/bin/bash
echo "Это сообщение в stdout"
echo "Это сообщение в stderr" >&2
Запустите с перенаправлением:
chmod +x test.sh
./test.sh > stdout.txt 2> stderr.txt
cat stdout.txt # Покажет только stdout
cat stderr.txt # Покажет только stderr
Диаграмма: процесс и стандартные потоки
graph LR
A[Клавиатура] -->|stdin fd=0| B[Процесс]
B -->|stdout fd=1| C[Терминал]
B -->|stderr fd=2| C
B -->|write fd=3| D[Файл data.txt]
3. Регулярные выражения и grep
Основные метасимволы
| Символ | Значение | Пример | Что найдёт |
|---|---|---|---|
. |
Любой символ | a.c |
abc, a5c, a c |
* |
0 или более повторений | ab*c |
ac, abc, abbc |
+ |
1 или более повторений | ab+c |
abc, abbc (но не ac) |
? |
0 или 1 повторение | ab?c |
ac, abc |
^ |
Начало строки | ^Hello |
строки, начинающиеся с Hello |
$ |
Конец строки | world$ |
строки, заканчивающиеся на world |
[] |
Класс символов | [abc] |
a, b или c |
[^] |
Отрицание класса | [^0-9] |
любой нецифровой символ |
| |
Альтернатива (или) | cat|dog |
cat или dog |
() |
Группировка | (ab)+ |
ab, abab, ababab |
Примеры использования grep
# Поиск строк, содержащих слово "error" (регистронезависимо)
grep -i "error" /var/log/syslog
# Поиск строк, начинающихся с цифры
grep "^[0-9]" file.txt
# Поиск email-адресов (упрощённый паттерн)
grep -E "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}" file.txt
# Показать номера строк
grep -n "function" program.c
# Инвертированный поиск (строки БЕЗ паттерна)
grep -v "^#" config.conf
# Рекурсивный поиск в директории
grep -r "TODO" ./src/
# Подсчёт совпадений
grep -c "error" logfile.txt
Расширенные регулярные выражения (ERE)
Для использования расширенного синтаксиса добавьте флаг -E:
# Найти числа от 100 до 199
grep -E "1[0-9]{2}" file.txt
# Найти IP-адреса (упрощённый паттерн)
grep -E "([0-9]{1,3}\.){3}[0-9]{1,3}" file.txt
# Найти строки с 3 или более повторяющимися буквами
grep -E "([a-z])\1{2,}" file.txt
4. Переменные окружения
Что такое переменные окружения?
Переменные окружения — это пары ключ-значение, которые хранятся в памяти процесса и наследуются дочерними процессами. Они используются для настройки поведения программ без изменения их кода.
Просмотр переменных окружения
# Показать все переменные окружения
env
# Показать конкретную переменную
echo $PATH
echo $HOME
echo $USER
# Показать все переменные, включая локальные переменные оболочки
set
Важные переменные окружения
| Переменная | Назначение | Пример значения |
|---|---|---|
PATH |
Список директорий для поиска исполняемых файлов | /usr/local/bin:/usr/bin:/bin |
HOME |
Домашняя директория пользователя | /home/username |
USER |
Имя текущего пользователя | username |
SHELL |
Путь к текущей оболочке | /bin/bash |
PWD |
Текущая рабочая директория | /home/username/projects |
LANG |
Локаль системы | en_US.UTF-8 |
LD_LIBRARY_PATH |
Пути поиска динамических библиотек | /usr/local/lib:/usr/lib |
Установка и экспорт переменных
# Локальная переменная (только в текущей оболочке)
MY_VAR="hello"
# Экспорт переменной (доступна дочерним процессам)
export MY_VAR="hello"
# Одной командой
export MY_VAR="hello"
# Временная переменная только для одной команды
MY_VAR="hello" ./program
# Добавление в PATH
export PATH="$PATH:/new/directory"
Постоянные переменные окружения
Для постоянного сохранения переменных добавьте их в конфигурационные файлы:
~/.bashrc— для bash (интерактивные non-login сессии)~/.bash_profileили~/.profile— для login сессий~/.zshrc— для zsh
Пример в ~/.bashrc:
export MY_PROJECT_PATH="$HOME/projects/asm"
export PATH="$PATH:$MY_PROJECT_PATH/bin"
После изменения выполните:
source ~/.bashrc
5. Модель памяти процесса x86-64
Адресное пространство процесса
В x86-64 Linux каждый процесс имеет виртуальное адресное пространство размером до 2^48 байт (256 ТБ в теории, практически меньше). Память организована в несколько сегментов:
graph TD
A[0x00007FFFFFFFFFFF<br/>Высокие адреса] --> B[Stack<br/>Стек растёт вниз]
B --> C[...]
C --> D[Memory Mapping<br/>mmap, разделяемые библиотеки]
D --> E[...]
E --> F[Heap<br/>Куча растёт вверх]
F --> G[BSS<br/>Неинициализированные данные]
G --> H[Data<br/>Инициализированные данные]
H --> I[Text<br/>Код программы read-only]
I --> J[0x0000000000400000<br/>Низкие адреса]
Секции исполняемого файла ELF
| Секция | Права доступа | Содержимое | Размер определяется |
|---|---|---|---|
.text |
r-x | Машинный код программы | На этапе компиляции |
.rodata |
r-- | Константы, строковые литералы | На этапе компиляции |
.data |
rw- | Инициализированные глобальные переменные | На этапе компиляции |
.bss |
rw- | Неинициализированные глобальные переменные | На этапе компиляции (обнуляется при загрузке) |
| stack | rw- | Локальные переменные, адреса возврата | Динамически (растёт при вызовах функций) |
| heap | rw- | Динамически выделяемая память (malloc) | Динамически (по запросу программы) |
Визуализация секций программы
graph LR
A[hello.asm исходник] --> B[NASM ассемблер]
B --> C[hello.o объектный файл]
C --> D[ld компоновщик]
D --> E[hello исполняемый ELF]
subgraph "Секции в памяти при запуске"
F[.text: инструкции]
G[.data: msg + len]
H[.bss: пусто]
I[stack: начальный фрейм]
end
Пример: просмотр секций программы
# Скомпилируйте hello.asm
nasm -f elf64 hello.asm
ld -o hello hello.o
# Просмотр секций
readelf -S hello
# Просмотр размеров секций
size hello
# Дизассемблирование секции .text
objdump -d -M intel hello
Типичный вывод size:
text data bss dec hex filename
181 13 0 194 c2 hello
text(181 байт) — код программыdata(13 байт) — строка "Hello, World!\n"bss(0 байт) — нет неинициализированных данных
Пример объявления секций в NASM
section .data ; Инициализированные данные
msg db "Hello", 0xA
len equ $ - msg
counter dq 42 ; 64-битное число
section .bss ; Неинициализированные данные
buffer resb 100 ; Резерв 100 байт
number resq 1 ; Резерв 8 байт (qword)
section .text ; Код программы
global _start
_start:
; Ваш код здесь
Важное замечание о Stack и Heap
-
Stack (стек) используется для:
- Локальных переменных функций
- Адресов возврата из функций
- Сохранённых значений регистров
- Аргументов функций (сверх 6 аргументов в System V ABI)
-
Heap (куча) используется для:
- Динамического выделения памяти (
malloc,mmap) - Больших структур данных, размер которых не известен на этапе компиляции
- Данных, которые должны жить дольше, чем функция, создавшая их
- Динамического выделения памяти (
В ассемблере вы обычно работаете только со стеком (через регистры rsp, rbp) и статическими секциями (.data, .bss). Для работы с кучей нужно вызывать системные вызовы brk/mmap или библиотечные функции C.
6. Цикл выполнения инструкций
Fetch-Decode-Execute Cycle
Процессор выполняет программу в бесконечном цикле из трёх фаз:
graph LR
A[FETCH<br/>Загрузить инструкцию<br/>из памяти по адресу RIP] --> B[DECODE<br/>Декодировать<br/>опкод и операнды]
B --> C[EXECUTE<br/>Выполнить операцию<br/>обновить регистры/память]
C --> D[Увеличить RIP<br/>на длину инструкции]
D --> A
Детальное объяснение фаз
-
FETCH (Выборка)
- Процессор читает байты инструкции из памяти по адресу, на который указывает регистр
RIP(Instruction Pointer) - В x86-64 инструкции имеют переменную длину: от 1 до 15 байт
- Процессор использует кэш инструкций (I-cache) для ускорения выборки
- Процессор читает байты инструкции из памяти по адресу, на который указывает регистр
-
DECODE (Декодирование)
- Декодер разбирает байты инструкции:
- Префиксы (опционально)
- Опкод (код операции)
- ModR/M байт (режим адресации и операнды)
- SIB байт (scale-index-base, для сложной адресации)
- Смещение и immediate-значения
- Определяет, какие исполнительные блоки нужны (ALU, FPU, Load/Store Unit)
- Декодер разбирает байты инструкции:
-
EXECUTE (Выполнение)
- Исполнительные блоки выполняют операцию:
- Арифметико-логическое устройство (ALU) для вычислений
- Блок загрузки/сохранения для операций с памятью
- Блок управления потоком для переходов
- Обновляются регистры назначения и флаги
- Исполнительные блоки выполняют операцию:
-
Обновление RIP
RIPувеличивается на длину выполненной инструкции- Для инструкций перехода (jump, call, ret)
RIPустанавливается в целевой адрес - Для условных переходов
RIPизменяется только если условие истинно
Пример: пошаговое выполнение
Рассмотрим последовательность инструкций:
mov rax, 5 ; Загрузить 5 в rax
add rax, 3 ; Добавить 3 к rax
mov rbx, rax ; Скопировать rax в rbx
Предположим, код загружен по адресу 0x401000.
| Цикл | RIP | Инструкция | FETCH | DECODE | EXECUTE | Результат |
|---|---|---|---|---|---|---|
| 1 | 0x401000 |
mov rax, 5 |
Читает 7 байт | Опкод 48 C7 C0 05 00 00 00 |
rax ← 5 |
rax=5, RIP=0x401007 |
| 2 | 0x401007 |
add rax, 3 |
Читает 4 байта | Опкод 48 83 C0 03 |
rax ← rax + 3 |
rax=8, RIP=0x40100B |
| 3 | 0x40100B |
mov rbx, rax |
Читает 3 байта | Опкод 48 89 C3 |
rbx ← rax |
rbx=8, RIP=0x40100E |
Современные оптимизации
Современные процессоры x86-64 (Intel Core, AMD Ryzen) используют:
- Конвейеризацию (Pipelining): несколько инструкций находятся на разных стадиях одновременно
- Суперскалярность: несколько исполнительных блоков выполняют инструкции параллельно
- Out-of-Order Execution: инструкции выполняются не по порядку, если это безопасно
- Предсказание переходов (Branch Prediction): процессор угадывает, будет ли выполнен условный переход
Однако логическая модель остаётся последовательной: программа ведёт себя так, как будто инструкции выполняются строго по порядку.
7. Компиляция программ на ассемблере
Двухэтапный процесс сборки
graph LR
A[hello.asm<br/>Исходный код] --> B[nasm]
B --> C[hello.o<br/>Объектный файл<br/>машинный код + метаданные]
C --> D[ld]
D --> E[hello<br/>Исполняемый файл ELF]
Этап 1: Ассемблирование (NASM)
nasm -f elf64 hello.asm -o hello.o
Опции:
-f elf64— формат вывода: ELF 64-bit (для Linux x86-64)-o hello.o— имя выходного объектного файла-g— добавить отладочную информацию (для GDB)-F dwarf— формат отладочной информации DWARF-l hello.lst— создать листинг (listing) файл с адресами и машинными кодами
Пример с отладочной информацией:
nasm -f elf64 -g -F dwarf hello.asm -o hello.o
Что делает NASM:
- Разбирает синтаксис исходного кода
- Преобразует мнемоники в опкоды (например,
mov rax, 1→48 C7 C0 01 00 00 00) - Разрешает метки (заменяет имена меток на адреса или смещения)
- Создаёт объектный файл в формате ELF с секциями
.text,.data,.bss - Добавляет таблицу символов и релокации (для последующей компоновки)
Этап 2: Компоновка (ld)
ld -o hello hello.o
Опции:
-o hello— имя выходного исполняемого файла-m elf_x86_64— целевая платформа (обычно определяется автоматически)-dynamic-linker /lib64/ld-linux-x86-64.so.2— для динамической линковки с библиотеками C-lc— линковка с libc (если используете функции C)-e _start— точка входа (по умолчанию_start)
Что делает ld:
- Объединяет секции из всех объектных файлов (если их несколько)
- Разрешает внешние символы (например, вызовы функций из других файлов)
- Выполняет релокацию (заменяет временные адреса на окончательные)
- Создаёт заголовок ELF и таблицы программных заголовков
- Записывает исполняемый файл
Проверка результата
# Проверить тип файла
file hello
# Вывод: hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV)...
# Сделать исполняемым (если ld не установил права)
chmod +x hello
# Запустить
./hello
Makefile для автоматизации
Создайте файл Makefile:
# Переменные
ASM = nasm
LD = ld
ASMFLAGS = -f elf64 -g -F dwarf
LDFLAGS =
# Цель по умолчанию
all: hello
# Правило для объектных файлов
%.o: %.asm
$(ASM) $(ASMFLAGS) $< -o $@
# Правило для исполняемого файла
hello: hello.o
$(LD) $(LDFLAGS) $< -o $@
# Очистка
clean:
rm -f *.o hello
# Запуск
run: hello
./hello
# Отладка
debug: hello
gdb ./hello
.PHONY: all clean run debug
Использование:
make # Собрать программу
make run # Собрать и запустить
make debug # Собрать и запустить в GDB
make clean # Удалить сгенерированные файлы
8. Разбор программы Hello World
Полный исходный код
section .data
msg db "Hello, World!", 0xA ; Строка с переводом строки (0xA = '\n')
len equ $ - msg ; Вычислить длину строки
section .text
global _start
_start:
; Системный вызов write(1, msg, len)
mov rax, 1 ; syscall номер: sys_write
mov rdi, 1 ; arg1: file descriptor (stdout)
mov rsi, msg ; arg2: указатель на буфер
mov rdx, len ; arg3: количество байт
syscall ; Выполнить системный вызов
; Системный вызов exit(0)
mov rax, 60 ; syscall номер: sys_exit
xor rdi, rdi ; arg1: exit code = 0
syscall ; Выполнить системный вызов
Пошаговый разбор секции .data
section .data
- Директива секции: все последующие объявления будут помещены в секцию
.data(инициализированные данные)
msg db "Hello, World!", 0xA
msg— метка (адрес начала строки)db(define byte) — директива объявления байтов"Hello, World!"— ASCII-строка (13 байт)0xA— шестнадцатеричное значение 10 (код символа\n— перевод строки)- Итого: 14 байт данных
len equ $ - msg
len— константа времени ассемблирования (не выделяет память)equ— директива определения константы$— текущий адрес (адрес сразу после строки)$ - msg— разность адресов = длина строки- Результат:
len = 14
Пошаговый разбор секции .text
section .text
- Секция кода (исполняемая, read-only)
global _start
- Объявляет символ
_startглобальным (видимым компоновщику) - Компоновщик
ldиспользует_startкак точку входа по умолчанию
_start:
- Метка: адрес первой инструкции программы
Системный вызов write
mov rax, 1 ; syscall номер: sys_write
mov rdi, 1 ; arg1: file descriptor (stdout)
mov rsi, msg ; arg2: указатель на буфер
mov rdx, len ; arg3: количество байт
syscall ; Выполнить системный вызов
Регистры для системных вызовов (System V ABI):
| Регистр | Назначение | В данном примере |
|---|---|---|
rax |
Номер системного вызова | 1 (sys_write) |
rdi |
1-й аргумент | 1 (stdout) |
rsi |
2-й аргумент | Адрес строки msg |
rdx |
3-й аргумент | Длина строки 14 |
r10 |
4-й аргумент | — |
r8 |
5-й аргумент | — |
r9 |
6-й аргумент | — |
После syscall:
- Управление передаётся ядру Linux
- Ядро выполняет
sys_write(1, msg, 14) - Строка "Hello, World!\n" выводится в терминал
- Управление возвращается в программу
raxсодержит результат (количество записанных байт или код ошибки)
Системный вызов exit
mov rax, 60 ; syscall номер: sys_exit
xor rdi, rdi ; arg1: exit code = 0
syscall ; Выполнить системный вызов
sys_exit(0)завершает процесс с кодом возврата 0 (успех)xor rdi, rdi— идиоматический способ обнулить регистр (быстрее и короче, чемmov rdi, 0)
Проверка кода возврата в bash:
./hello
echo $? # Выведет 0
Таблица инструкций с машинными кодами
| Адрес (пример) | Машинный код | Инструкция | Описание |
|---|---|---|---|
0x401000 |
48 C7 C0 01 00 00 00 |
mov rax, 1 |
Загрузить 1 в rax |
0x401007 |
48 C7 C7 01 00 00 00 |
mov rdi, 1 |
Загрузить 1 в rdi |
0x40100E |
48 BE xx xx xx xx xx xx xx xx |
mov rsi, msg |
Загрузить адрес msg в rsi |
0x401018 |
48 C7 C2 0E 00 00 00 |
mov rdx, 14 |
Загрузить 14 в rdx |
0x40101F |
0F 05 |
syscall |
Системный вызов |
0x401021 |
48 C7 C0 3C 00 00 00 |
mov rax, 60 |
Загрузить 60 в rax |
0x401028 |
48 31 FF |
xor rdi, rdi |
Обнулить rdi |
0x40102B |
0F 05 |
syscall |
Системный вызов |
Обратите внимание:
- Инструкция
syscallкодируется всего двумя байтами:0F 05 movс 64-битным immediate занимает 10 байт (префикс REX.W48+ опкод + 8 байт данных)xor rdi, rdiзанимает всего 3 байта
Эксперимент: изменение строки
Попробуйте изменить программу:
section .data
msg db "Hello, Assembly!", 0xA, 0 ; Добавлен null-terminator (для совместимости с C)
len equ $ - msg - 1 ; Не считаем null-terminator
section .text
global _start
_start:
mov rax, 1
mov rdi, 1
mov rsi, msg
mov rdx, len
syscall
mov rax, 60
xor rdi, rdi
syscall
Соберите и запустите:
nasm -f elf64 hello.asm && ld -o hello hello.o && ./hello
9. Системные вызовы Linux x86-64
Что такое системный вызов?
Системный вызов (syscall) — это интерфейс между программой в пользовательском пространстве (user space) и ядром операционной системы (kernel space). Это единственный легальный способ для программы:
- Взаимодействовать с устройствами ввода-вывода
- Работать с файловой системой
- Управлять процессами и потоками
- Выделять память
- Взаимодействовать с сетью
Переход в режим ядра
sequenceDiagram
participant U as User Space<br/>(программа)
participant C as CPU
participant K as Kernel Space<br/>(ядро Linux)
U->>C: syscall (0x0F 0x05)
Note over C: Сохранить контекст
C->>K: Передать управление<br/>Ring 3 → Ring 0
Note over K: Проверить права<br/>Выполнить операцию
K->>C: Вернуть результат в RAX
Note over C: Восстановить контекст
C->>U: Продолжить выполнение
**Кольца защиты (Protection Rings):**
- **Ring 0** (kernel mode): полный доступ ко всему оборудованию, выполнение привилегированных инструкций
- **Ring 3** (user mode): ограниченный доступ, не может напрямую взаимодействовать с устройствами
### Механизм вызова
**Инструкция `syscall` (x86-64):**
```nasm
syscall ; 0x0F 0x05 в машинном коде
Передача параметров:
| Параметр | Регистр | Описание |
|---|---|---|
| Номер syscall | rax | Идентификатор системного вызова (0-331+) |
| Аргумент 1 | rdi | Первый параметр |
| Аргумент 2 | rsi | Второй параметр |
| Аргумент 3 | rdx | Третий параметр |
| Аргумент 4 | r10 | Четвёртый параметр (не rcx!) |
| Аргумент 5 | r8 | Пятый параметр |
| Аргумент 6 | r9 | Шестой параметр |
| Возвращаемое значение | rax | Результат выполнения |
Важно: в отличие от calling convention функций, системные вызовы используют r10 вместо rcx для 4-го аргумента (rcx используется ядром для сохранения return address).
Основные системные вызовы
1. read (номер 0)
Чтение данных из файлового дескриптора.
mov rax, 0 ; syscall номер: read
mov rdi, 0 ; fd = 0 (stdin)
mov rsi, buffer ; указатель на буфер
mov rdx, 128 ; максимальное количество байт для чтения
syscall
; rax содержит количество прочитанных байт (или -errno при ошибке)
2. write (номер 1)
Запись данных в файловый дескриптор.
mov rax, 1 ; syscall номер: write
mov rdi, 1 ; fd = 1 (stdout)
mov rsi, message ; указатель на данные
mov rdx, 13 ; количество байт для записи
syscall
; rax содержит количество записанных байт
3. exit (номер 60)
Завершение процесса.
mov rax, 60 ; syscall номер: exit
mov rdi, 0 ; код возврата (0 = успех)
syscall
; не возвращается
Стандартные файловые дескрипторы
| Номер | Имя | Назначение |
|---|---|---|
| 0 | stdin | Стандартный ввод |
| 1 | stdout | Стандартный вывод |
| 2 | stderr | Стандартный вывод ошибок |
Коды ошибок
При ошибке системный вызов возвращает отрицательное значение -errno:
| Код | Макрос | Описание |
|---|---|---|
| -1 | EPERM | Операция не разрешена |
| -2 | ENOENT | Файл или каталог не существует |
| -9 | EBADF | Неверный файловый дескриптор |
| -14 | EFAULT | Неверный адрес памяти |
| -22 | EINVAL | Неверный аргумент |
Проверка ошибок:
syscall
test rax, rax ; Проверить знак результата
js .error ; Если отрицательное → ошибка
; Успех
jmp .continue
.error:
neg rax ; Преобразовать -errno в errno
; rax содержит код ошибки (положительный)
Полная таблица системных вызовов
Смотрите docs/system-calls.md или https://filippo.io/linux-syscall-table/
Популярные системные вызовы:
- 0: read
- 1: write
- 2: open
- 3: close
- 9: mmap (выделение памяти)
- 12: brk (изменение размера кучи)
- 39: getpid
- 57: fork
- 59: execve
- 60: exit
10. Регистры общего назначения
Иерархия регистров
В x86-64 регистры имеют многоуровневую структуру для обратной совместимости:
graph TD
A[RAX 64 бит<br/>0x0123456789ABCDEF] --> B[EAX 32 бит младших<br/>0x89ABCDEF]
B --> C[AX 16 бит младших<br/>0xCDEF]
C --> D[AH 8 бит старших<br/>0xCD]
C --> E[AL 8 бит младших<br/>0xEF]
style A fill:#4ecdc4
style B fill:#95e1d3
style C fill:#f38181
style D fill:#ffd93d
style E fill:#6bcf7f
Полный список регистров общего назначения
| 64-бит | 32-бит | 16-бит | 8-бит (low) | 8-бит (high) | Назначение в calling convention |
|---|---|---|---|---|---|
| rax | eax | ax | al | ah | Возвращаемое значение, accumulator |
| rbx | ebx | bx | bl | bh | Callee-saved, base |
| rcx | ecx | cx | cl | ch | 4-й аргумент, counter |
| rdx | edx | dx | dl | dh | 3-й аргумент, data |
| rsi | esi | si | sil | — | 2-й аргумент, source index |
| rdi | edi | di | dil | — | 1-й аргумент, destination index |
| rsp | esp | sp | spl | — | Stack pointer (указатель стека) |
| rbp | ebp | bp | bpl | — | Base pointer (база фрейма) |
| r8 | r8d | r8w | r8b | — | 5-й аргумент |
| r9 | r9d | r9w | r9b | — | 6-й аргумент |
| r10 | r10d | r10w | r10b | — | Caller-saved |
| r11 | r11d | r11w | r11b | — | Caller-saved |
| r12 | r12d | r12w | r12b | — | Callee-saved |
| r13 | r13d | r13w | r13b | — | Callee-saved |
| r14 | r14d | r14w | r14b | — | Callee-saved |
| r15 | r15d | r15w | r15b | — | Callee-saved |
Важно: запись в младшую 32-битную часть (eax, ebx, ...) обнуляет старшие 32 бита регистра!
mov rax, 0x123456789ABCDEF ; rax = 0x123456789ABCDEF
mov eax, 0x11111111 ; rax = 0x0000000011111111 (старшие биты обнулены!)
Специальные регистры
rip (Instruction Pointer):
- Указывает на адрес следующей инструкции
- Изменяется автоматически при выполнении
- Можно изменить через
jmp,call,ret - Используется для RIP-relative адресации
rflags (Флаги процессора):
- 64-битный регистр состояния
- Содержит флаги результата операций (ZF, SF, CF, OF, ...)
- Изменяется автоматически арифметическими и логическими операциями
Примеры использования разных размеров
; Работа с байтами
mov al, 0xFF ; al = 255, остальная часть rax не изменяется
mov ah, 0x12 ; ah = 18, al и остальное не изменяется
; Работа с 16-битными значениями
mov ax, 0x1234 ; ax = 0x1234, старшие 48 бит rax не изменяются
; 32-битная запись обнуляет старшие биты
mov rax, -1 ; rax = 0xFFFFFFFFFFFFFFFF (все биты = 1)
mov eax, 0 ; rax = 0x0000000000000000 (старшие 32 бита обнулились!)
; 8 и 16-битная запись НЕ обнуляет старшие биты
mov rax, -1 ; rax = 0xFFFFFFFFFFFFFFFF
mov al, 0 ; rax = 0xFFFFFFFFFFFFFF00 (только младший байт обнулён)
11. Арифметические и логические операции
Арифметические инструкции
| Инструкция | Описание | Пример | Эффект |
|---|---|---|---|
add dst, src |
Сложение | add rax, rbx |
rax = rax + rbx |
sub dst, src |
Вычитание | sub rax, 10 |
rax = rax - 10 |
inc dst |
Инкремент (+1) | inc rcx |
rcx = rcx + 1 |
dec dst |
Декремент (-1) | dec rdx |
rdx = rdx - 1 |
imul dst, src |
Знаковое умножение | imul rax, rbx |
rax = rax * rbx |
mul src |
Беззнаковое умножение | mul rbx |
rdx:rax = rax * rbx |
idiv src |
Знаковое деление | idiv rbx |
rax = rdx:rax / rbx, rdx = остаток |
div src |
Беззнаковое деление | div rcx |
rax = rdx:rax / rcx, rdx = остаток |
neg dst |
Смена знака | neg rax |
rax = -rax |
Примечание о умножении/делении:
mul/divработают с парой rdx:rax (128-битный результат/делимое)imulс двумя операндами работает как обычная операция- Перед делением нужно расширить делимое:
cqo(sign-extend rax → rdx:rax)
; Умножение: результат может не поместиться в 64 бита
mov rax, 0x8000000000000000
mov rbx, 2
mul rbx ; rdx:rax = rax * rbx
; rax = 0, rdx = 1 (результат = 2^64)
; Деление: делимое в rdx:rax
mov rax, 100 ; Делимое = 100
cqo ; Расширить rax в rdx:rax (rdx = 0 или -1)
mov rbx, 7
idiv rbx ; rax = 100 / 7 = 14, rdx = 100 % 7 = 2
Логические операции
| Инструкция | Описание | Пример | Эффект |
|---|---|---|---|
and dst, src |
Логическое И | and rax, 0xFF |
rax = rax & 0xFF |
or dst, src |
Логическое ИЛИ | or rax, rbx |
rax = rax | rbx |
xor dst, src |
Исключающее ИЛИ | xor rax, rax |
rax = 0 (обнуление) |
not dst |
Логическое НЕ | not rax |
rax = ~rax (инверсия) |
test op1, op2 |
Тест (AND без записи) | test rax, rax |
Установить флаги по rax & rax |
cmp op1, op2 |
Сравнение (SUB без записи) | cmp rax, 10 |
Установить флаги по rax - 10 |
Популярные идиомы:
; Обнуление регистра (быстрее mov rax, 0)
xor rax, rax ; rax = 0, обнуляет CF и OF
; Проверка на ноль
test rax, rax ; ZF = 1 если rax == 0
jz .is_zero
; Извлечение младшего байта
and rax, 0xFF ; Обнулить все биты кроме младших 8
; Установка бита
or rax, 0x80 ; Установить бит 7
; Сброс бита
and rax, ~0x80 ; Сбросить бит 7
; Инверсия бита
xor rax, 0x80 ; Инвертировать бит 7
Сдвиги и вращения
| Инструкция | Описание | Пример | Эффект |
|---|---|---|---|
shl dst, count |
Логический сдвиг влево | shl rax, 3 |
rax = rax << 3 (× 8) |
shr dst, count |
Логический сдвиг вправо | shr rax, 2 |
rax = rax >> 2 (÷ 4, беззнаковое) |
sal dst, count |
Арифметический сдвиг влево | sal rax, 1 |
То же что shl |
sar dst, count |
Арифметический сдвиг вправо | sar rax, 1 |
rax = rax >> 1 (÷ 2, знаковое) |
rol dst, count |
Циклический сдвиг влево | rol rax, 8 |
Биты циклически влево |
ror dst, count |
Циклический сдвиг вправо | ror rax, 4 |
Биты циклически вправо |
Различие между shr и sar:
; Беззнаковый сдвиг (shr)
mov rax, -8 ; rax = 0xFFFFFFFFFFFFFFF8
shr rax, 1 ; rax = 0x7FFFFFFFFFFFFFFC (= 9223372036854775804)
; Знаковый сдвиг (sar) сохраняет знаковый бит
mov rax, -8 ; rax = 0xFFFFFFFFFFFFFFF8 (= -8)
sar rax, 1 ; rax = 0xFFFFFFFFFFFFFFFC (= -4, знак сохранён!)
Быстрое умножение/деление на степени двойки:
; Умножить на 8 (2^3)
shl rax, 3 ; Быстрее чем imul rax, 8
; Разделить на 4 (2^2)
shr rax, 2 ; Для беззнакового
sar rax, 2 ; Для знакового (сохранит знак)
12. Флаги процессора и условные переходы
Регистр RFLAGS
Основные флаги (биты регистра rflags):
| Флаг | Бит | Имя | Устанавливается когда |
|---|---|---|---|
| CF | 0 | Carry Flag | Перенос при сложении / заём при вычитании (unsigned) |
| PF | 2 | Parity Flag | Чётное количество единиц в младшем байте |
| ZF | 6 | Zero Flag | Результат равен нулю |
| SF | 7 | Sign Flag | Результат отрицательный (старший бит = 1) |
| OF | 11 | Overflow Flag | Переполнение при знаковых операциях |
Как устанавливаются флаги
; Арифметические операции устанавливают флаги
mov rax, 5
sub rax, 5 ; ZF = 1 (результат 0)
mov rax, 10
sub rax, 20 ; SF = 1 (результат отрицательный)
; CF = 1 (заём при unsigned вычитании)
; test и cmp устанавливают флаги без изменения операндов
test rax, rax ; Выполнить rax & rax, установить флаги
; ZF = 1 если rax == 0
cmp rax, 10 ; Выполнить rax - 10, установить флаги
; ZF = 1 если rax == 10
; CF = 1 если rax < 10 (unsigned)
Условные переходы
После сравнения двух значений (cmp a, b):
| Условие | Unsigned | Signed | Проверяемые флаги |
|---|---|---|---|
| a == b | je (jump if equal) |
je |
ZF = 1 |
| a != b | jne (not equal) |
jne |
ZF = 0 |
| a < b | jb (below) |
jl (less) |
CF = 1 / SF ≠ OF |
| a <= b | jbe (below/equal) |
jle (less/equal) |
CF = 1 or ZF = 1 / ZF = 1 or SF ≠ OF |
| a > b | ja (above) |
jg (greater) |
CF = 0 and ZF = 0 / ZF = 0 and SF = OF |
| a >= b | jae (above/equal) |
jge (greater/equal) |
CF = 0 / SF = OF |
Проверка отдельных флагов:
| Инструкция | Условие | Флаги |
|---|---|---|
jz / je |
Ноль / равно | ZF = 1 |
jnz / jne |
Не ноль / не равно | ZF = 0 |
js |
Знак отрицательный | SF = 1 |
jns |
Знак положительный | SF = 0 |
jc |
Перенос | CF = 1 |
jnc |
Нет переноса | CF = 0 |
jo |
Переполнение | OF = 1 |
jno |
Нет переполнения | OF = 0 |
Примеры использования
Проверка диапазона (0-9 для ASCII цифр):
; Проверить, что символ в al — цифра '0'-'9'
cmp al, '0'
jb .not_digit ; Если al < '0' → не цифра (unsigned)
cmp al, '9'
ja .not_digit ; Если al > '9' → не цифра (unsigned)
; Здесь: это цифра
jmp .is_digit
.not_digit:
; Обработка не-цифры
.is_digit:
; Обработка цифры
Знаковое сравнение:
; Проверить, что rax находится в диапазоне [-100, 100]
cmp rax, -100
jl .out_of_range ; Если rax < -100 (signed)
cmp rax, 100
jg .out_of_range ; Если rax > 100 (signed)
; Здесь: -100 <= rax <= 100
Цикл с счётчиком:
mov rcx, 10 ; Счётчик = 10
.loop:
; Тело цикла
dec rcx ; Уменьшить счётчик
jnz .loop ; Повторить, пока rcx != 0
; Выход из цикла
13. Типичные ошибки начинающих
Ошибка 1: Забыли указать размер секции .bss
Неправильно:
section .bss
buffer: ; ❌ Не указан размер!
Правильно:
section .bss
buffer: resb 128 ; ✓ Резервировать 128 байт
counter: resq 1 ; ✓ Резервировать 1 qword (8 байт)
Ошибка 2: Использовали неправильный размер операндов
Неправильно:
mov rax, [buffer] ; Загружает 8 байт
mov byte [buffer], rax ; ❌ Пытается записать 8 байт в 1 байт!
Правильно:
mov al, [buffer] ; ✓ Загрузить 1 байт
mov byte [buffer], al ; ✓ Записать 1 байт
; Или явно указать размер
movzx rax, byte [buffer] ; Загрузить байт с zero-extend
Ошибка 3: Забыли про обнуление старших битов при 32-битной записи
Проблема:
mov rax, 0xFFFFFFFFFFFFFFFF
mov eax, 0 ; ❌ rax станет 0, а не 0xFFFFFFFF00000000!
Объяснение: запись в 32-битную часть (eax) обнуляет старшие 32 бита автоматически.
Ошибка 4: Неправильно вычислили длину строки
Неправильно:
section .data
msg: db "Hello", 10 ; Строка из 6 байт (5 букв + '\n')
section .text
mov rdx, 5 ; ❌ Длина = 5, но должна быть 6!
; В результате '\n' не выведется
Правильно:
section .data
msg: db "Hello", 10
msg_len equ $ - msg ; ✓ Автоматически вычислить длину
section .text
mov rdx, msg_len ; ✓ Правильная длина = 6
Ошибка 5: Перепутали signed и unsigned переходы
Неправильно:
; Проверка ASCII кода (беззнаковое значение)
cmp al, '0'
jl .not_digit ; ❌ jl для signed сравнения!
Правильно:
; ASCII коды — беззнаковые
cmp al, '0'
jb .not_digit ; ✓ jb (below) для unsigned
Напоминание:
jl/jg/jle/jge— для signed (знаковых) сравненийjb/ja/jbe/jae— для unsigned (беззнаковых) сравнений
Ошибка 6: Не обработали ошибку системного вызова
Неправильно:
mov rax, 0
mov rdi, 0
mov rsi, buffer
mov rdx, 100
syscall
; ❌ Не проверили, что rax >= 0 (может быть -errno)
mov [bytes_read], rax
Правильно:
syscall
test rax, rax
js .read_error ; ✓ Если отрицательное → ошибка
; Успех
mov [bytes_read], rax
jmp .continue
.read_error:
neg rax ; Преобразовать -errno в errno
; Обработать ошибку (rax содержит код ошибки)
Ошибка 7: Забыли про завершающий ноль в строках C
Проблема: при передаче строк в функции C (printf, strlen) они ожидают завершающий \0.
Неправильно:
section .data
msg: db "Hello" ; ❌ Нет '\0'!
extern strlen
mov rdi, msg
call strlen ; strlen может читать за границу
Правильно:
section .data
msg: db "Hello", 0 ; ✓ Завершающий ноль
14. Отладка с GDB
Основные команды
Запуск программы:
gdb ./program
(gdb) break _start # Точка останова на метке
(gdb) run # Запустить программу
Пошаговое выполнение:
(gdb) stepi # Выполнить одну инструкцию (step into)
(gdb) nexti # Следующая инструкция (step over)
(gdb) continue # Продолжить до следующего breakpoint
(gdb) finish # Выполнить до выхода из функции
Просмотр состояния:
(gdb) info registers # Показать все регистры
(gdb) info registers rax rbx rsp # Конкретные регистры
(gdb) print $rax # Вывести значение rax
(gdb) print/x $rax # В hex формате
(gdb) print/t $rax # В бинарном формате
(gdb) print/d $rax # В десятичном (со знаком)
Просмотр памяти:
(gdb) x/10gx $rsp # 10 qword в hex от адреса rsp
(gdb) x/20bx $rsi # 20 байт в hex от адреса rsi
(gdb) x/s $rdi # Строку (до '\0') по адресу rdi
(gdb) x/10i $rip # 10 инструкций от текущего rip
Формат команды x (examine):
- Формат:
x/[count][format][size] address - count: количество единиц
- format: o(octal), x(hex), d(decimal), u(unsigned), t(binary), i(instruction), c(char), s(string)
- size: b(byte), h(halfword-2), w(word-4), g(giant-8)
Точки останова:
(gdb) break _start # На метке
(gdb) break *0x401000 # На конкретном адресе
(gdb) info breakpoints # Список всех breakpoints
(gdb) delete 1 # Удалить breakpoint №1
(gdb) disable 2 # Временно отключить breakpoint №2
Дизассемблирование:
(gdb) disassemble # Дизассемблировать текущую функцию
(gdb) disassemble _start # Конкретную метку
(gdb) set disassembly-flavor intel # Использовать Intel синтаксис
Пример сессии отладки
$ gdb ./hello
(gdb) break _start
Breakpoint 1 at 0x401000
(gdb) run
Starting program: /path/to/hello
Breakpoint 1, 0x0000000000401000 in _start ()
(gdb) disassemble
0x0000000000401000 <_start+0>: mov eax,0x1
0x0000000000401005 <_start+5>: mov edi,0x1
=> 0x000000000040100a <_start+10>: movabs rsi,0x402000
...
(gdb) info registers rax rdi rsi rdx
rax 0x0 0
rdi 0x0 0
rsi 0x0 0
rdx 0x0 0
(gdb) stepi
(gdb) info registers rax
rax 0x1 1
(gdb) stepi
(gdb) info registers rdi
rdi 0x1 1
(gdb) stepi
(gdb) info registers rsi
rsi 0x402000 4202496
(gdb) x/s $rsi
0x402000: "hello, world!\n"
(gdb) continue
hello, world!
[Inferior 1 (process 12345) exited normally]
Полезные настройки
Файл ~/.gdbinit:
set disassembly-flavor intel
set pagination off
set print pretty on
# Показывать регистры после каждого шага
define hook-stop
info registers rip rax rbx rcx rdx rsi rdi rsp rbp
end
15. Справочник
Директивы NASM
| Директива | Описание | Пример |
|---|---|---|
section .data |
Секция инициализированных данных | |
section .bss |
Секция неинициализированных данных | |
section .text |
Секция кода | |
global symbol |
Экспортировать символ | global _start |
extern symbol |
Импортировать внешний символ | extern printf |
equ |
Определить константу | BUF_SIZE equ 128 |
%define |
Макроопределение | %define STDOUT 1 |
Объявление данных
| Директива | Размер | Описание | Пример |
|---|---|---|---|
db |
1 байт | Define byte | db 0x41, 'A', 10 |
dw |
2 байта | Define word | dw 0x1234 |
dd |
4 байта | Define double word | dd 0x12345678 |
dq |
8 байт | Define quad word | dq 0x123456789ABCDEF |
resb |
1 байт | Reserve bytes | resb 100 |
resw |
2 байта | Reserve words | resw 50 |
resd |
4 байта | Reserve double words | resd 25 |
resq |
8 байт | Reserve quad words | resq 10 |
Системные вызовы (краткая таблица)
| rax | Имя | rdi | rsi | rdx | Описание |
|---|---|---|---|---|---|
| 0 | read | fd | buf* | count | Чтение |
| 1 | write | fd | buf* | count | Запись |
| 2 | open | path* | flags | mode | Открыть файл |
| 3 | close | fd | — | — | Закрыть файл |
| 60 | exit | code | — | — | Завершить процесс |
Шаблон минимальной программы
; Минимальная программа hello world
section .data
msg: db 'Hello, World!', 10
msg_len equ $ - msg
section .text
global _start
_start:
; write(1, msg, msg_len)
mov rax, 1
mov rdi, 1
mov rsi, msg
mov rdx, msg_len
syscall
; exit(0)
mov rax, 60
xor rdi, rdi
syscall
Полезные ссылки
- 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