# Методичка по языку ассемблера x86-64
## Семинар 2: Функции, стек, конечные автоматы
---
## Оглавление
1. [Механизм работы стека](#1-механизм-работы-стека)
2. [Определение и вызов функций](#2-определение-и-вызов-функций)
3. [Функции с аргументами и возвращаемые значения](#3-функции-с-аргументами-и-возвращаемые-значения)
4. [Соглашение вызова (Calling Convention)](#4-соглашение-вызова-calling-convention)
5. [Использование стека для локальных переменных](#5-использование-стека-для-локальных-переменных)
6. [Фрейм стека и выравнивание](#6-фрейм-стека-и-выравнивание)
7. [Конечные автоматы (Finite State Machines)](#7-конечные-автоматы-finite-state-machines)
8. [Автоматы и регулярные выражения](#8-автоматы-и-регулярные-выражения)
9. [Кодирование автоматов на ассемблере](#9-кодирование-автоматов-на-ассемблере)
10. [Типичные ошибки](#10-типичные-ошибки)
11. [Практические задания](#11-практические-задания)
12. [Справочник: часто используемые конструкции](#справочник-часто-используемые-конструкции)
---
## 1. Механизм работы стека
### Что такое стек?
**Стек (stack)** — это область памяти, организованная по принципу LIFO (Last In, First Out: последним пришёл — первым ушёл). Стек используется для:
- Сохранения адресов возврата при вызовах функций
- Хранения локальных переменных функций
- Временного сохранения значений регистров
- Передачи аргументов функций (свыше 6 аргументов)
### Регистр RSP (Stack Pointer)
`RSP` (Stack Pointer) — регистр, указывающий на **вершину стека** (самый последний добавленный элемент).
**Важно:** В x86-64 Linux стек растёт **вниз** (к меньшим адресам):
```
Высокие адреса (0x7FFFFFFFFFFF)
↑
|
[Пустое пространство]
|
↓ ← RSP указывает сюда (вершина стека)
[Последний добавленный элемент]
[Предыдущий элемент]
[Ещё более ранний элемент]
|
↓
Низкие адреса
```
### Инструкции работы со стеком
#### PUSH — положить на стек
```asm
push rax
```
**Эквивалентно:**
```asm
sub rsp, 8 ; Сдвинуть вершину стека на 8 байт вниз
mov [rsp], rax ; Записать значение rax по адресу вершины
```
#### POP — снять со стека
```asm
pop rax
```
**Эквивалентно:**
```asm
mov rax, [rsp] ; Прочитать значение с вершины стека
add rsp, 8 ; Сдвинуть вершину стека на 8 байт вверх
```
### Визуализация работы стека
```mermaid
graph TD
A[Начальное состояние
RSP = 0x7FFFFFFFE000]
A --> B[push rax где rax=0x42]
B --> C[RSP = 0x7FFFFFFFDFF8
память 0x...DFF8: 0x42]
C --> D[push rbx где rbx=0x100]
D --> E[RSP = 0x7FFFFFFFDFF0
память 0x...DFF0: 0x100
память 0x...DFF8: 0x42]
E --> F[pop rcx]
F --> G[RCX = 0x100
RSP = 0x7FFFFFFFDFF8]
style A fill:#334155
style C fill:#475569
style E fill:#64748b
style G fill:#475569
```
### Пошаговый пример
Предположим, `RSP = 0x7FFFFFFFE000` перед началом.
```asm
push rax ; rax = 0x1234567890ABCDEF
push rbx ; rbx = 0xAABBCCDDEEFF0011
pop rcx ; rcx = ?
pop rdx ; rdx = ?
```
| Шаг | Инструкция | RSP до | RSP после | Действие | rcx | rdx |
|-----|------------|--------|-----------|----------|-----|-----|
| 0 | — | `0x7FFFFFFFE000` | — | Начало | — | — |
| 1 | `push rax` | `0x7FFFFFFFE000` | `0x7FFFFFFFDFF8` | `[0x...DFF8] = 0x1234567890ABCDEF` | — | — |
| 2 | `push rbx` | `0x7FFFFFFFDFF8` | `0x7FFFFFFFDFF0` | `[0x...DFF0] = 0xAABBCCDDEEFF0011` | — | — |
| 3 | `pop rcx` | `0x7FFFFFFFDFF0` | `0x7FFFFFFFDFF8` | `rcx = [0x...DFF0]` | `0xAABBCCDDEEFF0011` | — |
| 4 | `pop rdx` | `0x7FFFFFFFDFF8` | `0x7FFFFFFFE000` | `rdx = [0x...DFF8]` | `0xAABBCCDDEEFF0011` | `0x1234567890ABCDEF` |
**Результат:** `rcx = 0xAABBCCDDEEFF0011`, `rdx = 0x1234567890ABCDEF`, `RSP` вернулся к исходному значению.
### Важное правило
**Стек должен быть сбалансирован:** если вы добавили N элементов с помощью `push`, вы должны снять N элементов с помощью `pop` (или скорректировать `rsp` напрямую).
Несбалансированный стек приводит к:
- Неправильным адресам возврата (`ret` вернётся не туда)
- Segmentation fault
- Непредсказуемому поведению программы
---
## 2. Определение и вызов функций
### Что такое функция в ассемблере?
**Функция (подпрограмма, subroutine)** — это именованный блок инструкций, который можно вызвать из разных мест программы и который возвращает управление обратно вызывающему коду.
В ассемблере функция = **метка** + **код** + **инструкция `ret`**.
### Инструкция CALL
```asm
call function_name
```
**Действия `call`:**
1. **Сохранить адрес возврата на стек:** `push` адрес следующей инструкции после `call`
2. **Перейти к функции:** установить `RIP = адрес метки function_name`
**Эквивалентно:**
```asm
push rip_next ; (это псевдокод, rip нельзя использовать напрямую)
jmp function_name
rip_next: ; адрес следующей инструкции
```
### Инструкция RET
```asm
ret
```
**Действия `ret`:**
1. **Снять адрес возврата со стека:** `pop` значение в `RIP`
2. **Перейти по этому адресу:** продолжить выполнение
**Эквивалентно:**
```asm
pop rip ; (псевдокод)
```
### Визуализация call и ret
```mermaid
sequenceDiagram
participant C as Вызывающий код
participant S as Стек
participant F as Функция
C->>C: call my_func (адрес 0x401010)
C->>S: push 0x401015 (адрес возврата)
C->>F: jmp my_func (0x401080)
Note over F: Выполнение тела функции
F->>S: pop адрес (0x401015)
F->>C: jmp 0x401015
C->>C: Продолжить выполнение
```
### Пример: простая функция
```asm
section .data
msg db "Hello from function!", 0xA
msg_len equ $ - msg
section .text
global _start
; Функция печати сообщения
print_hello:
mov rax, 1 ; sys_write
mov rdi, 1 ; stdout
mov rsi, msg
mov rdx, msg_len
syscall
ret ; Вернуться к вызывающему коду
_start:
call print_hello ; Вызов функции
call print_hello ; Вызов ещё раз
; Выход
mov rax, 60
xor rdi, rdi
syscall
```
### Пошаговая трассировка выполнения
Предположим:
- `_start` находится по адресу `0x401000`
- `print_hello` находится по адресу `0x401020`
- `RSP` изначально `0x7FFFFFFFE000`
| RIP | Инструкция | RSP до | RSP после | Стек | Комментарий |
|-----|------------|--------|-----------|------|-------------|
| `0x401000` | `call print_hello` | `0x7FFFFFFFE000` | `0x7FFFFFFFDFF8` | `[...FF8]=0x401005` | Сохранить 0x401005 (адрес после call) |
| `0x401020` | `mov rax, 1` | `0x7FFFFFFFDFF8` | `0x7FFFFFFFDFF8` | `[...FF8]=0x401005` | Начало функции |
| ... | (syscall и др.) | `0x7FFFFFFFDFF8` | `0x7FFFFFFFDFF8` | `[...FF8]=0x401005` | Тело функции |
| `0x40102E` | `ret` | `0x7FFFFFFFDFF8` | `0x7FFFFFFFE000` | — | Снять 0x401005 и прыгнуть туда |
| `0x401005` | `call print_hello` | `0x7FFFFFFFE000` | `0x7FFFFFFFDFF8` | `[...FF8]=0x40100A` | Второй вызов |
| `0x401020` | `mov rax, 1` | `0x7FFFFFFFDFF8` | `0x7FFFFFFFDFF8` | `[...FF8]=0x40100A` | Снова в функции |
| `0x40102E` | `ret` | `0x7FFFFFFFDFF8` | `0x7FFFFFFFE000` | — | Вернуться к 0x40100A |
| `0x40100A` | `mov rax, 60` | `0x7FFFFFFFE000` | `0x7FFFFFFFE000` | — | Продолжить _start |
### CALL vs JMP
| Инструкция | Сохраняет адрес возврата? | Можно ли вернуться? | Использование |
|------------|---------------------------|---------------------|---------------|
| `call` | **Да** (на стек) | Да (`ret`) | Вызов функций, которые должны вернуться |
| `jmp` | **Нет** | Нет | Безусловные переходы, tail calls, `exit` |
**Пример tail call:**
```asm
my_function:
; ... какая-то работа ...
jmp other_function ; Tail call: не создаём новый фрейм стека
```
Это эквивалентно:
```asm
my_function:
; ... какая-то работа ...
call other_function
ret
```
Но `jmp` эффективнее: не тратит место на стеке и не добавляет лишний `ret`.
### Вложенные вызовы функций
```asm
section .text
global _start
func_c:
; Самая внутренняя функция
ret
func_b:
call func_c ; Вызвать func_c
ret ; Вернуться в func_a
func_a:
call func_b ; Вызвать func_b
ret ; Вернуться в _start
_start:
call func_a ; Вызвать func_a
; Продолжить
```
**Стек при вложенных вызовах:**
```
1. _start вызывает func_a:
Стек: [адрес возврата в _start]
2. func_a вызывает func_b:
Стек: [адрес возврата в func_a]
[адрес возврата в _start]
3. func_b вызывает func_c:
Стек: [адрес возврата в func_b]
[адрес возврата в func_a]
[адрес возврата в _start]
4. func_c делает ret:
Стек: [адрес возврата в func_a]
[адрес возврата в _start]
5. func_b делает ret:
Стек: [адрес возврата в _start]
6. func_a делает ret:
Стек: (пустой)
```
---
## 3. Функции с аргументами и возвращаемые значения
### Передача аргументов через регистры
В **System V ABI** (Linux x86-64) первые **6 целочисленных/указательных аргументов** передаются через регистры:
| Порядок аргумента | Регистр | Размер | Примечание |
|-------------------|---------|--------|------------|
| 1-й | `rdi` | 64 бита | Может использоваться `edi`, `di`, `dil` для меньших размеров |
| 2-й | `rsi` | 64 бита | — |
| 3-й | `rdx` | 64 бита | — |
| 4-й | `rcx` | 64 бита | **Внимание:** разрушается при syscall! |
| 5-й | `r8` | 64 бита | — |
| 6-й | `r9` | 64 бита | — |
**Аргументы 7+** передаются через стек (справа налево).
### Возвращаемое значение
| Тип результата | Регистр |
|----------------|---------|
| Целочисленное/указатель (до 64 бит) | `rax` |
| Дополнительное значение (128 бит) | `rdx:rax` (старшие 64 бита в `rdx`) |
| Числа с плавающей точкой | `xmm0`, `xmm1` |
| Структуры (маленькие) | `rax`, `rdx` |
| Структуры (большие) | Указатель передаётся как скрытый первый аргумент |
### Пример: функция сложения
```asm
; Функция: сложить два числа
; Аргументы: rdi = a, rsi = b
; Возвращает: rax = a + b
add_numbers:
mov rax, rdi ; rax = a
add rax, rsi ; rax = a + b
ret
; Вызов:
_start:
mov rdi, 10 ; Первый аргумент
mov rsi, 32 ; Второй аргумент
call add_numbers ; Результат в rax
; rax теперь содержит 42
```
### Пример: функция с 3 аргументами
```asm
; Функция: вычислить (a + b) * c
; Аргументы: rdi = a, rsi = b, rdx = c
; Возвращает: rax = (a + b) * c
calculate:
add rdi, rsi ; rdi = a + b
mov rax, rdi ; rax = a + b
imul rax, rdx ; rax = (a + b) * c
ret
; Вызов:
_start:
mov rdi, 5 ; a = 5
mov rsi, 3 ; b = 3
mov rdx, 7 ; c = 7
call calculate ; rax = (5+3)*7 = 56
```
### Функция string_length
Вычисляет длину null-terminated строки.
```asm
; Аргумент: rdi = указатель на строку
; Возвращает: rax = длина строки (без null-terminator)
string_length:
xor rax, rax ; rax = 0 (счётчик)
.loop:
cmp byte [rdi + rax], 0 ; Проверить текущий байт
je .done ; Если 0, выйти из цикла
inc rax ; rax++
jmp .loop
.done:
ret
; Использование:
section .data
my_string db "Hello, World!", 0
section .text
_start:
mov rdi, my_string ; Передать адрес строки
call string_length ; rax = 13
```
### Функция print_string
Выводит null-terminated строку в stdout.
```asm
; Аргумент: rdi = указатель на строку
; Возвращает: ничего (может вернуть количество байт в rax)
print_string:
push rdi ; Сохранить rdi (аргумент)
call string_length ; rax = длина строки
pop rsi ; rsi = адрес строки (восстановить из стека)
mov rdx, rax ; rdx = длина
mov rax, 1 ; sys_write
mov rdi, 1 ; stdout
syscall ; Вывести строку
ret
; Использование:
_start:
mov rdi, my_string
call print_string
```
### Функция с несколькими возвращаемыми значениями
```asm
; Функция: разделить число на целую и дробную части (упрощённый пример)
; Аргумент: rdi = число вида XY, где X - десятки, Y - единицы
; Возвращает: rax = X (десятки), rdx = Y (единицы)
split_number:
mov rax, rdi ; rax = число
xor rdx, rdx ; rdx = 0
mov rcx, 10
div rcx ; rax = rax / 10, rdx = rax % 10
ret
; Вызов:
_start:
mov rdi, 47 ; Число 47
call split_number
; rax = 4 (десятки), rdx = 7 (единицы)
```
### Пример: print_hex
Выводит 64-битное число в шестнадцатеричном формате.
```asm
section .data
hex_digits db '0123456789ABCDEF'
section .text
global _start
; Аргумент: rdi = число для вывода
print_hex:
mov rcx, 64 ; Количество бит
.loop:
push rax ; Сохранить rax
sub rcx, 4 ; Обработать следующие 4 бита
mov rax, rdi ; rax = число
shr rax, cl ; Сдвинуть вправо на cl бит
and rax, 0xF ; Оставить младшие 4 бита
lea rsi, [hex_digits + rax] ; rsi = адрес соответствующей цифры
mov rax, 1 ; sys_write
mov rdx, 1 ; Длина = 1 байт
push rdi
push rcx
mov rdi, 1 ; stdout
syscall
pop rcx
pop rdi
pop rax
test rcx, rcx
jnz .loop
ret
_start:
mov rdi, 0x1234ABCD ; Число для вывода
call print_hex
; Вывод переноса строки
mov rax, 1
mov rdi, 1
mov rsi, newline
mov rdx, 1
syscall
mov rax, 60
xor rdi, rdi
syscall
section .data
newline db 0xA
```
---
## 4. Соглашение вызова (Calling Convention)
### Что такое Calling Convention?
**Соглашение вызова (calling convention)** — это набор правил, определяющих:
1. Как передаются аргументы (регистры, стек, порядок)
2. Какой регистр используется для возвращаемого значения
3. Какие регистры должны сохраняться функцией (callee-saved)
4. Какие регистры может свободно изменять функция (caller-saved)
5. Как выравнивается стек
6. Кто очищает стек после вызова
### System V AMD64 ABI (Linux, BSD, macOS)
Это стандартное соглашение для Unix-подобных систем на x86-64.
### Классификация регистров
```mermaid
graph TD
A[Регистры x86-64]
A --> B[Caller-saved
Вызывающий сохраняет]
A --> C[Callee-saved
Вызываемая функция сохраняет]
B --> D[rax - возвращаемое значение]
B --> E[rdi, rsi, rdx, rcx, r8, r9 - аргументы]
B --> F[r10, r11 - временные]
C --> G[rbx]
C --> H[rbp - frame pointer]
C --> I[r12, r13, r14, r15]
C --> J[rsp - stack pointer
всегда сохраняется!]
style B fill:#ef4444
style C fill:#22c55e
style D fill:#fca5a5
style E fill:#fca5a5
style F fill:#fca5a5
style G fill:#86efac
style H fill:#86efac
style I fill:#86efac
style J fill:#86efac
```
### Таблица классификации регистров
| Регистр | Тип | Назначение | Сохраняется? |
|---------|-----|------------|--------------|
| **rax** | Caller-saved | Возвращаемое значение, syscall номер | ❌ Нет |
| **rbx** | Callee-saved | Общего назначения | ✅ Да |
| **rcx** | Caller-saved | 4-й аргумент | ❌ Нет (разрушается при syscall!) |
| **rdx** | Caller-saved | 3-й аргумент, 2-е возвращаемое значение | ❌ Нет |
| **rsi** | Caller-saved | 2-й аргумент | ❌ Нет |
| **rdi** | Caller-saved | 1-й аргумент | ❌ Нет |
| **rbp** | Callee-saved | Frame pointer (база стека) | ✅ Да |
| **rsp** | Специальный | Stack pointer (вершина стека) | ✅ **Всегда** |
| **r8** | Caller-saved | 5-й аргумент | ❌ Нет |
| **r9** | Caller-saved | 6-й аргумент | ❌ Нет |
| **r10** | Caller-saved | Временный | ❌ Нет |
| **r11** | Caller-saved | Временный | ❌ Нет (разрушается при syscall!) |
| **r12-r15** | Callee-saved | Общего назначения | ✅ Да |
### Что означает "Caller-saved" и "Callee-saved"?
#### Caller-saved (сохраняет вызывающий)
Если функция **A** вызывает функцию **B**, и функция **A** использует регистры `rax`, `rdi`, `rsi` и т.д., то:
- Функция **A** должна **сохранить** эти регистры **перед** вызовом **B** (если значения нужны после вызова)
- Функция **B** **может свободно изменять** эти регистры без сохранения
**Пример:**
```asm
function_a:
mov rax, 42 ; Используем rax
; Хотим вызвать function_b, но нужно сохранить rax
push rax ; Сохранить rax (caller-saved)
call function_b ; function_b может изменить rax
pop rax ; Восстановить rax
; Теперь rax снова содержит 42
ret
function_b:
mov rax, 999 ; Можем свободно изменять rax
ret
```
#### Callee-saved (сохраняет вызываемая функция)
Если функция **B** хочет использовать регистры `rbx`, `r12-r15`, `rbp`, то:
- Функция **B** должна **сохранить** эти регистры в начале и **восстановить** перед `ret`
- Функция **A** может **не беспокоиться** об этих регистрах — они останутся неизменными после вызова **B**
**Пример:**
```asm
function_a:
mov rbx, 100 ; Используем rbx
call function_b ; function_b сохранит rbx внутри себя
; rbx всё ещё содержит 100
ret
function_b:
push rbx ; Сохранить rbx (callee-saved)
mov rbx, 777 ; Используем rbx для своих нужд
; ... работа с rbx ...
pop rbx ; Восстановить rbx
ret
```
### Почему такое разделение?
**Производительность и оптимизация:**
- **Caller-saved** регистров больше → функции могут использовать много регистров для аргументов и временных значений без накладных расходов
- **Callee-saved** регистров меньше, но они гарантируют стабильность → удобно для переменных, которые должны "пережить" вызовы функций
- Компилятор может оптимизировать, сохраняя только действительно используемые регистры
### Сравнение: функция VS системный вызов
| Аспект | Функция (System V ABI) | Системный вызов (syscall) |
|--------|------------------------|---------------------------|
| Номер операции | — | `rax` |
| Аргумент 1 | `rdi` | `rdi` |
| Аргумент 2 | `rsi` | `rsi` |
| Аргумент 3 | `rdx` | `rdx` |
| Аргумент 4 | `rcx` | **`r10`** (не `rcx`!) |
| Аргумент 5 | `r8` | `r8` |
| Аргумент 6 | `r9` | `r9` |
| Возвращаемое значение | `rax` | `rax` |
| Разрушаемые регистры | Caller-saved | **`rcx`, `r11`** (всегда!) |
| Callee-saved | `rbx`, `rbp`, `r12-r15` | `rbx`, `rbp`, `r12-r15` |
**Важно:** Системный вызов **всегда разрушает `rcx` и `r11`**, даже если они не используются для аргументов!
### Пример: сохранение регистров
```asm
my_function:
; Хотим использовать rbx, r12, r13 (callee-saved)
push rbx
push r12
push r13
mov rbx, rdi ; Сохранить 1-й аргумент
mov r12, rsi ; Сохранить 2-й аргумент
mov r13, rdx ; Сохранить 3-й аргумент
; Вызов другой функции (может разрушить caller-saved)
call some_other_function
; rbx, r12, r13 всё ещё содержат исходные значения
mov rax, rbx
add rax, r12
add rax, r13
; Восстановить callee-saved регистры
pop r13
pop r12
pop rbx
ret
```
---
## 5. Локальные переменные на стеке
### Зачем нужен фрейм стека?
Когда функции требуются локальные переменные, массивы или буферы, их размещают на стеке. Для управления локальными данными используется **фрейм стека** (stack frame) — структура, ограниченная двумя регистрами:
- **`rbp`** (base pointer) — указатель на базу текущего фрейма (фиксированная точка отсчёта)
- **`rsp`** (stack pointer) — указатель на вершину стека (динамически меняется при push/pop)
### Структура фрейма стека
```mermaid
graph TD
A["Высокие адреса"] --> B["[arg 8] ← rbp + 24"]
B --> C["[arg 7] ← rbp + 16"]
C --> D["[return address] ← rbp + 8"]
D --> E["[старый rbp] ← rbp (saved frame pointer)"]
E --> F["[локальная переменная 1] ← rbp - 8"]
F --> G["[локальная переменная 2] ← rbp - 16"]
G --> H["[временные данные] ← rsp (текущая вершина стека)"]
H --> I["Низкие адреса"]
style E fill:#ffd700
style D fill:#ff6b6b
style F fill:#4ecdc4
style G fill:#4ecdc4
```
**Ключевые особенности:**
1. **Аргументы 1-6** передаются в регистрах, аргументы **7+** — на стеке (выше return address)
2. **Return address** сохраняется автоматически инструкцией `call`
3. **Старый rbp** сохраняется в прологе функции для восстановления фрейма вызывающей функции
4. **Локальные переменные** располагаются ниже rbp (отрицательные смещения)
5. **rbp** остаётся неизменным в течение выполнения функции, что упрощает адресацию локальных переменных
### Пролог и эпилог функции
**Стандартный пролог:**
```nasm
push rbp ; Сохранить старый базовый указатель
mov rbp, rsp ; Установить rbp на текущую позицию стека
sub rsp, N ; Выделить N байт для локальных переменных
```
**Стандартный эпилог:**
```nasm
mov rsp, rbp ; Освободить локальные переменные (восстановить rsp)
pop rbp ; Восстановить старый базовый указатель
ret ; Вернуться (pop return address в rip)
```
**Альтернативные инструкции:**
- `enter N, 0` — эквивалент пролога (медленнее, редко используется)
- `leave` — эквивалент `mov rsp, rbp; pop rbp` (часто используется)
### Пример: функция с локальным массивом
```nasm
; Функция, которая создаёт локальный массив из 5 qword
; и заполняет его квадратами чисел 0-4
; Возвращает сумму элементов в rax
square_sum:
; === ПРОЛОГ ===
push rbp ; Сохранить старый rbp
mov rbp, rsp ; rbp теперь указывает на сохранённый rbp
sub rsp, 40 ; Выделить 40 байт (5 qword по 8 байт)
; После пролога:
; rbp+8 → return address
; rbp+0 → старый rbp
; rbp-8 → array[0]
; rbp-16 → array[1]
; rbp-24 → array[2]
; rbp-32 → array[3]
; rbp-40 → array[4] ← rsp
; === ТЕЛО ФУНКЦИИ ===
; Заполнить массив квадратами
xor rcx, rcx ; Счётчик (i = 0)
.fill_loop:
cmp rcx, 5
je .fill_done
mov rax, rcx ; rax = i
imul rax, rax ; rax = i * i
; Вычислить адрес array[i] = rbp - 8*(i+1)
lea rdx, [rcx + 1] ; rdx = i + 1
shl rdx, 3 ; rdx = (i + 1) * 8
neg rdx ; rdx = -(i + 1) * 8
mov [rbp + rdx], rax ; array[i] = i²
inc rcx
jmp .fill_loop
.fill_done:
; Вычислить сумму элементов
xor rax, rax ; sum = 0
xor rcx, rcx ; i = 0
.sum_loop:
cmp rcx, 5
je .sum_done
lea rdx, [rcx + 1]
shl rdx, 3
neg rdx
add rax, [rbp + rdx] ; sum += array[i]
inc rcx
jmp .sum_loop
.sum_done:
; rax содержит сумму 0² + 1² + 2² + 3² + 4² = 30
; === ЭПИЛОГ ===
mov rsp, rbp ; Освободить локальные переменные
pop rbp ; Восстановить старый rbp
ret ; Вернуться
; Альтернативный эпилог:
; leave ; Эквивалентно mov rsp, rbp; pop rbp
; ret
```
### Пошаговая трассировка создания фрейма
Предположим, функция `square_sum` вызвана из адреса `0x401234`. Проследим изменения стека:
| Шаг | Инструкция | rsp | rbp | Стек (содержимое по адресам) |
|-----|-----------|-----|-----|-----------------------------|
| 0 | `call square_sum` | `0x7fff0000` | `0x7fff0100` | `[0x7fff0000] = 0x401239` (return addr) |
| 1 | `push rbp` | `0x7ffefff8` | `0x7fff0100` | `[0x7ffefff8] = 0x7fff0100` (старый rbp) |
| 2 | `mov rbp, rsp` | `0x7ffefff8` | `0x7ffefff8` | rbp теперь указывает на новый фрейм |
| 3 | `sub rsp, 40` | `0x7ffeFFd0` | `0x7ffefff8` | Выделено 40 байт для массива |
| 4 | Заполнение array[0] | `0x7ffeffd0` | `0x7ffefff8` | `[rbp-8] = 0` (0²) |
| 5 | Заполнение array[1] | `0x7ffeffd0` | `0x7ffefff8` | `[rbp-16] = 1` (1²) |
| ... | ... | ... | ... | ... |
| N | `mov rsp, rbp` | `0x7ffefff8` | `0x7ffefff8` | Локальные переменные освобождены |
| N+1 | `pop rbp` | `0x7fff0000` | `0x7fff0100` | rbp восстановлен |
| N+2 | `ret` | `0x7fff0008` | `0x7fff0100` | Возврат по адресу 0x401239 |
### Почему используется rbp?
**Без rbp (использование только rsp):**
- Каждая операция push/pop в теле функции смещает rsp
- Смещения локальных переменных относительно rsp постоянно меняются
- Компилятор должен отслеживать текущее смещение rsp на каждой инструкции
- Усложняется отладка (стековые трейсы требуют сложного раскручивания)
**С rbp:**
- rbp фиксирован в течение выполнения функции
- Локальные переменные имеют постоянные смещения: `[rbp-8]`, `[rbp-16]`, ...
- Отладчики могут легко найти границы фрейма и параметры
- Упрощается генерация debug-информации (DWARF)
**Современная оптимизация:** компиляторы с флагом `-fomit-frame-pointer` могут не использовать rbp, освобождая ещё один регистр общего назначения. Это требует точного отслеживания глубины стека на этапе компиляции.
---
## 6. Выравнивание стека (Stack Alignment)
### Зачем нужно выравнивание?
Системное соглашение **System V AMD64 ABI** требует, чтобы стек был выровнен на **16-байтную границу** в момент выполнения инструкции `call`. Это означает, что `rsp` должен быть кратен 16 перед `call`.
**Причины требования:**
1. **SSE/AVX инструкции**: многие SIMD-инструкции (например, `movaps`, `movdqa`) требуют выравнивания на 16 байт для работы с памятью. Невыровненный доступ вызывает исключение `SIGSEGV` или `SIGBUS`.
2. **Производительность**: даже инструкции, которые допускают невыровненный доступ (например, `movups`), работают медленнее при нарушении выравнивания.
3. **Совместимость с библиотеками**: все системные библиотеки (libc, libm) ожидают выровненный стек.
### Механика выравнивания
```mermaid
graph TD
A["До вызова функции: rsp выровнен на 16"] --> B["call: push return address (8 байт)"]
B --> C["Внутри функции: rsp = старое_значение - 8"]
C --> D{"rsp кратен 16?"}
D -->|Нет| E["rsp = (old_rsp - 8) mod 16 = 8"]
E --> F["Невыровненный стек!"]
D -->|Да| G["Функция должна сохранить выравнивание"]
style F fill:#ff6b6b
style G fill:#4ecdc4
```
**Важное следствие:** после `call` стек **смещён на 8 байт** (return address). Если до `call` было `rsp % 16 == 0`, то после `call` будет `rsp % 16 == 8`.
### Кто отвечает за выравнивание?
**Вызывающая функция (caller):**
- Гарантирует, что `rsp % 16 == 0` перед инструкцией `call`
- Если нужно передать аргументы через стек (7+), должна учесть их при выравнивании
**Вызываемая функция (callee):**
- Может предполагать, что `rsp % 16 == 8` в начале (после `call`)
- Если нужно вызвать другую функцию, должна восстановить выравнивание на 16
### Визуализация: выравнивание при вложенных вызовах
```mermaid
sequenceDiagram
participant Main as main (rsp=0x7fff0000)
participant FuncA as func_a
participant FuncB as func_b
Note over Main: rsp = 0x7fff0000 (кратно 16)
Main->>FuncA: call func_a
Note over FuncA: rsp = 0x7ffefff8 (0x7fff0000 - 8)
НЕ кратно 16!
Note over FuncA: push rbp (rsp -= 8)
rsp = 0x7ffefff0 (кратно 16)
Note over FuncA: sub rsp, 24
rsp = 0x7ffeffe8 (НЕ кратно 16)
Note over FuncA: Нужно выровнять перед вызовом!
Note over FuncA: sub rsp, 8
rsp = 0x7ffeffd8 (кратно 16)
FuncA->>FuncB: call func_b
Note over FuncB: rsp = 0x7ffeffD0 (0x7ffeffd8 - 8)
Смещение 8, как ожидалось
FuncB-->>FuncA: ret
Note over FuncA: add rsp, 8 (убрать padding)
Note over FuncA: add rsp, 24 (освободить локальные)
Note over FuncA: pop rbp
FuncA-->>Main: ret
```
### Пример: ручное выравнивание стека
```nasm
; Функция, которая вызывает printf с выравниванием стека
section .data
format_str: db "Value: %d", 10, 0
section .text
extern printf
print_value:
; Аргумент в rdi = значение для печати
; === ПРОЛОГ ===
push rbp ; rsp -= 8, теперь rsp % 16 == 0
mov rbp, rsp
; Сохранить аргумент (rdi может быть испорчен)
push rdi ; rsp -= 8, теперь rsp % 16 == 8 (невыровнен!)
; === ВЫРАВНИВАНИЕ ===
; Перед вызовом printf нужно rsp % 16 == 0
; Сейчас rsp % 16 == 8, поэтому нужно вычесть ещё 8
sub rsp, 8 ; Padding для выравнивания
; Теперь rsp % 16 == 0
; === ВЫЗОВ PRINTF ===
mov rdi, format_str ; 1-й аргумент: формат
mov rsi, [rbp - 8] ; 2-й аргумент: сохранённое значение
xor rax, rax ; printf - varargs, требует rax=0
call printf ; Стек выровнен!
; === ОЧИСТКА ===
add rsp, 8 ; Убрать padding
pop rdi ; Восстановить (хотя уже не нужен)
; === ЭПИЛОГ ===
pop rbp
ret
```
### Автоматическое выравнивание: формула
Предположим, функции нужно:
- Сохранить N callee-saved регистров (N × 8 байт)
- Выделить M байт для локальных переменных
**Алгоритм:**
1. После `push rbp` стек выровнен на 16 (так как до `call` было кратно 16, `call` вычел 8, `push rbp` вычел ещё 8)
2. Вычислить общий размер: `total = N*8 + M`
3. Если `total % 16 != 0`, добавить padding: `padding = (16 - (total % 16)) % 16`
4. Выделить: `sub rsp, total + padding`
**Пример:**
```nasm
; Нужно: 3 callee-saved регистра (rbx, r12, r13) + 20 байт локальных
; total = 3*8 + 20 = 44
; 44 % 16 = 12, padding = 16 - 12 = 4
push rbp
mov rbp, rsp
push rbx
push r12
push r13 ; Сохранено 3*8 = 24 байта
sub rsp, 20 + 4 ; 20 локальных + 4 padding = 24
; Теперь rsp % 16 == 0
```
### Битовая маска для выравнивания
Часто используется трюк с битовой маской:
```nasm
; Выровнять rsp на 16-байтную границу (округлить вниз)
and rsp, -16 ; Обнулить младшие 4 бита
; -16 в дополнительном коде = 0xFFFFFFFFFFFFFFF0
; AND с этой маской убирает биты [3:0]
```
**Пример использования:**
```nasm
_start:
; rsp может быть любым при старте программы
and rsp, -16 ; Принудительно выровнять
call main ; Теперь main получит выровненный стек
```
### Типичная ошибка: забыть про выравнивание
**Неправильно:**
```nasm
my_function:
push rbp
mov rbp, rsp
sub rsp, 32 ; Выделить 32 байта
; rsp % 16 == 8 (невыровнен!)
call some_function ; SEGFAULT, если some_function использует SSE!
```
**Правильно:**
```nasm
my_function:
push rbp ; rsp % 16 == 0 после этого
mov rbp, rsp
sub rsp, 32 ; Всё ещё rsp % 16 == 0 (32 кратно 16)
call some_function ; OK!
```
**Или с нечётным размером:**
```nasm
my_function:
push rbp ; rsp % 16 == 0
mov rbp, rsp
sub rsp, 24 ; 24 % 16 = 8, теперь rsp % 16 == 8
sub rsp, 8 ; Добавить padding, rsp % 16 == 0
call some_function ; OK!
add rsp, 32 ; Освободить всё (24 + 8)
pop rbp
ret
```
### Проверка выравнивания в GDB
```bash
(gdb) p/x $rsp
$1 = 0x7fffffffe4a8
# Проверить выравнивание:
(gdb) p $rsp % 16
$2 = 8 # Не выровнен! Должно быть 0 перед call
# Или:
(gdb) p/t $rsp # Вывести в двоичном виде
$3 = 111111111111111111111111111100101000
# ^^^^ младшие 4 бита должны быть 0000
```
---
## 7. Конечные автоматы: теория
### Что такое конечный автомат?
**Конечный автомат** (Finite State Machine, FSM) — математическая модель вычислений, состоящая из:
1. **Конечного множества состояний** Q = {q₀, q₁, ..., qₙ}
2. **Начального состояния** q₀ ∈ Q
3. **Множества принимающих (конечных) состояний** F ⊆ Q
4. **Входного алфавита** Σ (например, ASCII-символы)
5. **Функции переходов** δ: Q × Σ → Q
**Детерминированный автомат (DFA):** для каждого состояния и символа существует ровно один переход.
**Недетерминированный автомат (NFA):** может иметь несколько переходов для одной пары (состояние, символ) или ε-переходы.
### Пример 1: проверка чётности количества единиц
**Задача:** принять строку из '0' и '1', если в ней чётное количество единиц.
```mermaid
stateDiagram-v2
[*] --> Even: начало
Even --> Odd: 1
Even --> Even: 0
Odd --> Even: 1
Odd --> Odd: 0
Even --> [*]: конец (принять)
note right of Even
Чётное количество 1
(включая 0)
end note
note right of Odd
Нечётное количество 1
end note
```
**Таблица переходов:**
| Состояние | Символ '0' | Символ '1' |
|-----------|------------|------------|
| Even | Even | Odd |
| Odd | Odd | Even |
**Код на ассемблере:**
```nasm
; Проверка чётности единиц
; Вход: rdi = указатель на строку
; Выход: rax = 1 (чётно), 0 (нечётно)
check_parity:
xor rax, rax ; Начальное состояние: Even (rax = 0)
.loop:
movzx rcx, byte [rdi] ; Прочитать символ
test rcx, rcx ; Конец строки?
jz .done
cmp cl, '1' ; Если символ == '1'
jne .next
xor rax, 1 ; Переключить состояние (Even ↔ Odd)
.next:
inc rdi
jmp .loop
.done:
; rax = 0 (Even, принять) или 1 (Odd, отклонить)
xor rax, 1 ; Инвертировать: 1 = принято, 0 = отклонено
ret
```
### Пример 2: распознавание целых чисел
**Задача:** проверить, является ли строка корректным целым числом (возможен знак +/−, затем цифры).
**Грамматика (регулярное выражение):** `[+-]?[0-9]+`
**Автомат:**
```mermaid
stateDiagram-v2
[*] --> Start
Start --> Sign: +, -
Start --> Digit: 0-9
Sign --> Digit: 0-9
Digit --> Digit: 0-9
Digit --> [*]: конец строки (принять)
Start --> Error: другой символ
Sign --> Error: другой символ
Digit --> Error: другой символ
Error --> [*]: отклонить
note right of Start
Начальное состояние
end note
note right of Sign
Прочитан знак
end note
note right of Digit
Прочитана хотя бы
одна цифра
end note
```
**Состояния:**
- **Start (A)**: начало, ожидаем знак или цифру
- **Sign (B)**: прочитан знак, ожидаем цифру
- **Digit (C)**: прочитана хотя бы одна цифра (принимающее состояние)
- **Error (E)**: обнаружен недопустимый символ (отклонение)
**Таблица переходов:**
| Состояние | '+' или '−' | '0'–'9' | Конец строки | Другое |
|-----------|-------------|---------|--------------|--------|
| Start (A) | Sign (B) | Digit (C)| Error (E) | Error (E)|
| Sign (B) | Error (E) | Digit (C)| Error (E) | Error (E)|
| Digit (C) | Error (E) | Digit (C)| **Accept** | Error (E)|
| Error (E) | Error (E) | Error (E)| **Reject** | Error (E)|
### Реализация автомата в ассемблере: паттерн
**Типичная структура:**
```nasm
; Шаблон для реализации DFA
check_pattern:
; Инициализация: начальное состояние
; (можно использовать регистр для хранения состояния)
.state_A:
call read_symbol ; Прочитать следующий символ в al
test al, al ; Конец ввода?
jz .reject ; В состоянии A конец = ошибка
; Проверить переходы из состояния A
cmp al, '+'
je .state_B
cmp al, '-'
je .state_B
cmp al, '0'
jb .reject
cmp al, '9'
ja .reject
jmp .state_C ; Цифра: переход в C
.state_B:
call read_symbol
test al, al
jz .reject ; Только знак без цифр
cmp al, '0'
jb .reject
cmp al, '9'
ja .reject
jmp .state_C
.state_C:
call read_symbol
test al, al
jz .accept ; Конец в состоянии C = успех
cmp al, '0'
jb .reject
cmp al, '9'
ja .reject
jmp .state_C ; Петля: ещё цифры
.accept:
mov rax, 1
ret
.reject:
xor rax, rax
ret
```
### Свойства конечных автоматов
**Преимущества:**
- Эффективная проверка за O(n), где n — длина входа
- Детерминированность: нет backtracking, каждый символ обрабатывается один раз
- Просто реализовать на низком уровне (ассемблер, FPGA)
- Легко доказать корректность (формальная верификация)
**Ограничения:**
- Не могут считать (например, проверить баланс скобок)
- Не могут распознать язык `{aⁿbⁿ | n ≥ 0}` (нужен стек → автомат с магазинной памятью)
- Не могут проверить, что число является палиндромом произвольной длины
**Что могут DFA:**
- Все регулярные языки (эквивалентны регулярным выражениям)
- Лексический анализ (токенизация)
- Валидация формата (email, URL, числа)
- Протоколы с фиксированным числом состояний
---
## 8. Реализация FSM: анализ check_int.asm
### Автомат из check_int.asm
Программа `check_int.asm` реализует автомат для распознавания целых чисел (с опциональным знаком). Разберём его подробно.
**Диаграмма состояний:**
```mermaid
stateDiagram-v2
[*] --> A: начало
A --> B: + или -
A --> C: 0-9
A --> E: другое
B --> C: 0-9
B --> E: другое
C --> C: 0-9
C --> D: конец ввода (\0 или \n)
C --> E: другое
D --> [*]: принять (Okay)
E --> [*]: отклонить (Fail)
note right of A
Начальное состояние:
ожидаем знак или цифру
end note
note right of B
Прочитан знак:
теперь нужна цифра
end note
note right of C
Прочитана хотя бы
одна цифра:
принимающее состояние
end note
```
### Код с комментариями
```nasm
global _start
_start:
; === СОСТОЯНИЕ A: начало ===
_A:
call getsymbol ; Прочитать символ в al
; Проверить на знак '+'
cmp al, '+'
je _B ; Переход A → B
; Проверить на знак '-'
cmp al, '-'
je _B ; Переход A → B
; Проверить на цифру (ASCII '0' = 0x30, '9' = 0x39)
cmp al, '0'
jb _E ; Меньше '0' → ошибка
cmp al, '9'
ja _E ; Больше '9' → ошибка
jmp _C ; Цифра: переход A → C
; === СОСТОЯНИЕ B: прочитан знак ===
_B:
call getsymbol ; Прочитать следующий символ
; После знака ОБЯЗАТЕЛЬНО должна быть цифра
cmp al, '0'
jb _E
cmp al, '9'
ja _E
jmp _C ; Переход B → C
; === СОСТОЯНИЕ C: читаем цифры ===
_C:
call getsymbol
; Проверить на конец ввода
test al, al ; Проверить на NULL (\0)
jz _D ; Конец ввода → принять (C → D)
cmp al, 10 ; Проверить на \n (newline)
jz _D ; Newline → принять (C → D)
; Проверить, ещё одна цифра?
cmp al, '0'
jb _E ; Не цифра → ошибка
cmp al, '9'
ja _E
jmp _C ; Ещё цифра → петля C → C
; === СОСТОЯНИЕ D: принятие ===
_D:
mov rdi, ok_msg ; Указатель на "Okay\n"
mov rsi, 5 ; Длина строки
call print_string
call exit
; === СОСТОЯНИЕ E: ошибка ===
_E:
mov rdi, fail_msg ; Указатель на "Fail\n"
mov rsi, 5
call print_string
call exit
```
### Ключевые особенности реализации
1. **Метки = состояния**: каждой метке (`_A`, `_B`, `_C`) соответствует состояние автомата
2. **Переходы = переходы**: `jmp`, `je`, `ja`, `jb` реализуют функцию переходов δ
3. **Чтение символов**: функция `getsymbol` читает по одному символу из stdin
4. **Обработка EOF**: `getsymbol` возвращает 0 при конце файла
5. **Принимающее состояние**: только состояние C может перейти в D (принятие)
### Функция getsymbol: чтение одного символа
```nasm
getsymbol:
xor rax, rax ; syscall 0 = read
xor rdi, rdi ; fd = 0 (stdin)
dec rsp ; Выделить 1 байт на стеке
mov rsi, rsp ; Буфер = вершина стека
mov rdx, 1 ; Читать 1 байт
syscall ; read(0, rsp, 1)
test rax, rax ; Проверить результат
jz .EOF ; Если rax == 0 → конец файла
mov al, [rsp] ; Загрузить прочитанный символ
jmp .END
.EOF:
xor rax, rax ; Вернуть 0 (конец ввода)
.END:
inc rsp ; Восстановить стек
ret
```
**Особенности:**
- Использует стек как временный буфер (1 байт)
- `dec rsp` / `inc rsp` выделяют и освобождают место на стеке
- Возвращает символ в `al` (младший байт rax)
- При EOF возвращает 0
---
## 9. Пошаговая трассировка работы FSM
### Пример 1: ввод "123"
**Входная строка:** `'1', '2', '3', '\n'`
| Шаг | Состояние | Прочитанный символ (al) | Проверка | Переход |
|------|----------|-------------------|-----------|---------|
| 1 | **A** (начало) | '1' (0x31) | Не '+', не '−' | Проверка цифры |
| | | | '0' ≤ '1' ≤ '9' | **A → C** |
| 2 | **C** | '2' (0x32) | Не \0, не \n | Проверка цифры |
| | | | '0' ≤ '2' ≤ '9' | **C → C** (петля) |
| 3 | **C** | '3' (0x33) | Не \0, не \n | Проверка цифры |
| | | | '0' ≤ '3' ≤ '9' | **C → C** |
| 4 | **C** | '\n' (0x0A) | cmp al, 10 | **C → D** |
| 5 | **D** (принятие) | — | Вывод "Okay" | exit(0) |
**Результат:** принято ✓
### Пример 2: ввод "-45"
**Входная строка:** `'-', '4', '5', '\n'`
| Шаг | Состояние | Прочитанный символ | Проверка | Переход |
|------|----------|-------------------|-----------|---------|
| 1 | **A** | '−' (0x2D) | cmp al, '-' | **A → B** (je _B) |
| 2 | **B** | '4' (0x34) | '0' ≤ '4' ≤ '9' | **B → C** |
| 3 | **C** | '5' (0x35) | Не \0, не \n | Проверка цифры |
| | | | '0' ≤ '5' ≤ '9' | **C → C** |
| 4 | **C** | '\n' (0x0A) | cmp al, 10 | **C → D** |
| 5 | **D** | — | Вывод "Okay" | exit(0) |
**Результат:** принято ✓
### Пример 3: ввод "+" (ошибка)
**Входная строка:** `'+', '\n'`
| Шаг | Состояние | Прочитанный символ | Проверка | Переход |
|------|----------|-------------------|-----------|---------|
| 1 | **A** | '+' (0x2B) | cmp al, '+' | **A → B** |
| 2 | **B** | '\n' (0x0A) | cmp al, '0' | al < '0' (0x0A < 0x30) |
| | | | jb _E | **B → E** |
| 3 | **E** (ошибка) | — | Вывод "Fail" | exit(0) |
**Результат:** отклонено ✗ (знак без цифр)
### Пример 4: ввод "12a3" (ошибка)
**Входная строка:** `'1', '2', 'a', '3'`
| Шаг | Состояние | Прочитанный символ | Проверка | Переход |
|------|----------|-------------------|-----------|---------|
| 1 | **A** | '1' | Цифра | **A → C** |
| 2 | **C** | '2' | Цифра | **C → C** |
| 3 | **C** | 'a' (0x61) | Не \0, не \n | Проверка цифры |
| | | | cmp al, '9' | 'a' > '9' (0x61 > 0x39) |
| | | | ja _E | **C → E** |
| 4 | **E** | — | Вывод "Fail" | exit(0) |
**Результат:** отклонено ✗ (недопустимый символ)
### Детальная трассировка с регистрами: "-7"
**Вход:** `echo "-7" | ./check_int`
| Шаг | RIP (метка) | Инструкция | al | Флаги | Примечание |
|------|---------|------------|-----|--------|-------------|
| 1 | `_A` | `call getsymbol` | ? | — | Чтение символа |
| 2 | `_A` | (after call) | 0x2D ('-') | — | Символ прочитан |
| 3 | `_A` | `cmp al, '+'` | 0x2D | ZF=0 | 0x2D ≠ 0x2B |
| 4 | `_A` | `je _B` | 0x2D | ZF=0 | Не переходит |
| 5 | `_A` | `cmp al, '-'` | 0x2D | ZF=1 | 0x2D == 0x2D |
| 6 | `_A` | `je _B` | 0x2D | ZF=1 | **Переход A → B** |
| 7 | `_B` | `call getsymbol` | 0x2D | — | Читаем след. символ |
| 8 | `_B` | (after call) | 0x37 ('7') | — | Прочитано '7' |
| 9 | `_B` | `cmp al, '0'` | 0x37 | CF=0 | 0x37 ≥ 0x30 |
| 10 | `_B` | `jb _E` | 0x37 | CF=0 | Не переходит |
| 11 | `_B` | `cmp al, '9'` | 0x37 | CF=1 | 0x37 < 0x39 |
| 12 | `_B` | `ja _E` | 0x37 | CF=1 | Не переходит |
| 13 | `_B` | `jmp _C` | 0x37 | — | **Переход B → C** |
| 14 | `_C` | `call getsymbol` | 0x37 | — | Читаем след. символ |
| 15 | `_C` | (after call) | 0x0A ('\n') | — | Прочитан newline |
| 16 | `_C` | `test al, al` | 0x0A | ZF=0 | 0x0A ≠ 0 |
| 17 | `_C` | `jz _D` | 0x0A | ZF=0 | Не переходит |
| 18 | `_C` | `cmp al, 10` | 0x0A | ZF=1 | 0x0A == 10 |
| 19 | `_C` | `jz _D` | 0x0A | ZF=1 | **Переход C → D** |
| 20 | `_D` | `mov rdi, ok_msg` | — | — | Подготовка вывода |
| 21 | `_D` | `call print_string` | — | — | Вывод "Okay\n" |
| 22 | — | `call exit` | — | — | exit(0) |
**Результат:** программа выводит "Okay" и завершается с кодом 0 ✓
### Визуализация переходов для "-7"
```mermaid
graph LR
Start([Запуск]) --> A
A[Состояние A
читает '-'] --> B[Состояние B
читает '7']
B --> C[Состояние C
читает newline]
C --> D[Состояние D
выводит Okay]
D --> End([exit 0])
style D fill:#51cf66
style End fill:#51cf66
```
---
## 10. Типичные ошибки при работе со стеком и функциями
### Ошибка 1: Неправильное выравнивание стека
**Проблема:** забыли выровнять стек перед `call`, что приводит к SEGFAULT в функциях, использующих SSE.
**Неправильный код:**
```nasm
my_function:
push rbp ; rsp % 16 == 0 после этого
mov rbp, rsp
sub rsp, 24 ; 24 % 16 = 8 → rsp % 16 == 8 (НЕВЫРОВНЕН!)
; Передать параметры
mov rdi, format_str
mov rsi, 42
call printf ; ❌ SEGFAULT! printf ожидает выровненный стек
add rsp, 24
pop rbp
ret
```
**Правильный код:**
```nasm
my_function:
push rbp ; rsp % 16 == 0
mov rbp, rsp
sub rsp, 32 ; 32 % 16 == 0 → rsp % 16 == 0 (ВЫРОВНЕН!)
mov rdi, format_str
mov rsi, 42
call printf ; ✓ Работает корректно
add rsp, 32
pop rbp
ret
```
**Альтернатива с padding:**
```nasm
my_function:
push rbp ; rsp % 16 == 0
mov rbp, rsp
sub rsp, 24 ; Выделить 24 байта локальных
sub rsp, 8 ; Добавить 8 байт padding → итого 32
; Теперь rsp % 16 == 0
call printf ; ✓ OK
add rsp, 32 ; Освободить всё (24 + 8)
pop rbp
ret
```
### Ошибка 2: Не сохранили callee-saved регистры
**Проблема:** использовали rbx/r12-r15 без сохранения, вызывающая функция получила испорченные данные.
**Неправильный код:**
```nasm
calculate:
; Вызывающая функция полагается на rbx
mov rbx, rdi ; ❌ Испортили rbx!
add rbx, 10
mov rax, rbx
ret ; Вернулись, но rbx испорчен
```
**Правильный код:**
```nasm
calculate:
push rbx ; Сохранить rbx
mov rbx, rdi
add rbx, 10
mov rax, rbx
pop rbx ; Восстановить rbx
ret ; ✓ Теперь вызывающая функция видит исходный rbx
```
### Ошибка 3: Дисбаланс стека (push/pop не совпадают)
**Проблема:** количество `push` не равно количеству `pop`, стек смещён при `ret`.
**Неправильный код:**
```nasm
broken_function:
push rbx
push r12
push r13 ; Сохранили 3 регистра
; ... тело функции ...
pop r13
pop r12 ; ❌ Забыли pop rbx!
ret ; Возврат по неправильному адресу → CRASH
```
**Правильный код:**
```nasm
correct_function:
push rbx
push r12
push r13
; ... тело функции ...
pop r13
pop r12
pop rbx ; ✓ Все 3 регистра восстановлены
ret
```
**Диагностика в GDB:**
```bash
(gdb) break broken_function
(gdb) run
(gdb) info registers rsp
# Запомнить значение rsp
(gdb) stepi # Пошагово до ret
(gdb) info registers rsp
# Если значение отличается на неожиданную величину → дисбаланс!
```
### Ошибка 4: Неправильный порядок pop
**Проблема:** `pop` в обратном порядке по отношению к `push`, регистры получают неправильные значения.
**Неправильный код:**
```nasm
wrong_order:
push rbx ; Стек: [rbx]
push r12 ; Стек: [r12, rbx]
push r13 ; Стек: [r13, r12, rbx]
; ...
pop rbx ; ❌ rbx получит значение r13!
pop r12 ; r12 получит значение r12 (случайно правильно)
pop r13 ; r13 получит значение rbx
ret
```
**Правильный код:**
```nasm
correct_order:
push rbx ; Стек: [rbx]
push r12 ; Стек: [r12, rbx]
push r13 ; Стек: [r13, r12, rbx]
; ...
pop r13 ; ✓ r13 восстановлен
pop r12 ; ✓ r12 восстановлен
pop rbx ; ✓ rbx восстановлен
ret
```
**Мнемоника:** LIFO (Last In, First Out) — что последним положили, то первым достаём.
### Ошибка 5: Использование caller-saved регистра без учёта его изменения
**Проблема:** положились на сохранение значения в r10/r11 через вызов функции.
**Неправильный код:**
```nasm
buggy_code:
mov r10, 100 ; Сохранили значение в r10
mov rdi, 42
call some_function ; some_function может испортить r10!
add rax, r10 ; ❌ r10 может содержать мусор!
ret
```
**Правильный код (вариант 1: использовать callee-saved):**
```nasm
fixed_code_v1:
push rbx ; Сохранить rbx
mov rbx, 100 ; Использовать rbx вместо r10
mov rdi, 42
call some_function ; rbx НЕ изменится
add rax, rbx ; ✓ rbx содержит 100
pop rbx
ret
```
**Правильный код (вариант 2: сохранить на стеке):**
```nasm
fixed_code_v2:
mov r10, 100
push r10 ; Сохранить r10 на стеке
mov rdi, 42
call some_function
pop r10 ; Восстановить r10
add rax, r10 ; ✓ r10 содержит 100
ret
```
### Ошибка 6: Неправильное использование локальных переменных
**Проблема:** забыли выделить место на стеке для локальных переменных или обращаемся по неправильному смещению.
**Неправильный код:**
```nasm
bad_locals:
push rbp
mov rbp, rsp
; ❌ Забыли sub rsp, N!
mov qword [rbp - 8], 42 ; Перезаписываем старый rbp!
mov rax, [rbp - 8]
pop rbp
ret
```
**Правильный код:**
```nasm
good_locals:
push rbp
mov rbp, rsp
sub rsp, 16 ; ✓ Выделили 16 байт для локальных
mov qword [rbp - 8], 42 ; Локальная переменная 1
mov qword [rbp - 16], 100 ; Локальная переменная 2
mov rax, [rbp - 8]
add rax, [rbp - 16]
mov rsp, rbp ; Освободить локальные
pop rbp
ret
```
### Ошибка 7: Неполный FSM (отсутствующие переходы)
**Проблема:** автомат не обрабатывает все возможные входные символы, программа падает или зависает.
**Неправильный код:**
```nasm
incomplete_fsm:
_A:
call getsymbol
cmp al, '0'
jb _C ; Если < '0'
cmp al, '9'
ja _C ; Если > '9'
jmp _B ; Иначе цифра
_B:
; ❌ Что дальше? Нет обработки следующего символа!
ret ; Немедленный возврат?
```
**Правильный код:**
```nasm
complete_fsm:
_A:
call getsymbol
test al, al ; ✓ Проверить EOF
jz _ERROR
cmp al, '0'
jb _ERROR
cmp al, '9'
ja _ERROR
jmp _B
_B:
call getsymbol ; ✓ Продолжить чтение
test al, al
jz _ACCEPT ; EOF после цифры = OK
cmp al, '0'
jb _ERROR
cmp al, '9'
ja _ERROR
jmp _B ; Петля для нескольких цифр
_ACCEPT:
mov rax, 1
ret
_ERROR:
xor rax, rax
ret
```
### Ошибка 8: Неправильный выбор условного перехода (signed vs unsigned)
**Проблема:** использовали `jl`/`jg` для беззнаковых чисел или `jb`/`ja` для знаковых.
**Неправильный код:**
```nasm
; Проверить, что ASCII код в диапазоне [0x30, 0x39]
check_digit_wrong:
cmp al, 0x30
jl .not_digit ; ❌ jl для знакового сравнения!
cmp al, 0x39 ; ASCII коды беззнаковые
jg .not_digit ; ❌ jg для знакового сравнения!
; ...
```
**Правильный код:**
```nasm
; ASCII коды — беззнаковые значения
check_digit_correct:
cmp al, 0x30
jb .not_digit ; ✓ jb (below) для unsigned
cmp al, 0x39
ja .not_digit ; ✓ ja (above) для unsigned
; ...
```
**Таблица выбора условных переходов:**
| Тип данных | "Меньше" | "Меньше/равно" | "Больше" | "Больше/равно" |
|------------|----------|----------------|----------|----------------|
| Unsigned | `jb` | `jbe` | `ja` | `jae` |
| Signed | `jl` | `jle` | `jg` | `jge` |
**Пример ошибки:** `-1` (0xFF) как unsigned = 255, как signed = -1. Сравнение с 10:
- `cmp al, 10; jb` → 255 > 10, не переходит (неправильно, если ожидали signed)
- `cmp al, 10; jl` → -1 < 10, переходит (правильно для signed)
---
## 11. Практические задания
### Задание 1: Функция с локальными переменными
Напишите функцию `sum_array`, которая:
- Принимает указатель на массив (rdi) и длину (rsi)
- Создаёт локальную переменную-счётчик на стеке
- Возвращает сумму элементов в rax
- Корректно выравнивает стек
**Требования:**
- Использовать фрейм стека (rbp)
- Сохранить callee-saved регистры при необходимости
- Правильное выравнивание для возможных вложенных вызовов
### Задание 2: FSM для проверки email
Реализуйте конечный автомат для упрощённой проверки email формата: `[a-z]+@[a-z]+\.[a-z]+`
**Состояния:**
- A: начало, ожидаем букву (локальная часть)
- B: читаем локальную часть
- C: прочитан @, ожидаем букву (домен)
- D: читаем домен
- E: прочитана точка, ожидаем букву (TLD)
- F: читаем TLD (принимающее состояние)
- ERR: ошибка
**Пример входов:**
- `"user@example.com"` → принять
- `"test@domain.org"` → принять
- `"invalid"` → отклонить
- `"@example.com"` → отклонить
- `"user@.com"` → отклонить
### Задание 3: Рекурсивная функция факториала
Реализуйте рекурсивную функцию `factorial(n)`:
```nasm
; factorial(n) = n * factorial(n-1), factorial(0) = 1
; Вход: rdi = n
; Выход: rax = n!
factorial:
; TODO: реализовать с правильным управлением стеком
```
**Требования:**
- Обработать базовый случай (n = 0)
- Сохранить rdi перед рекурсивным вызовом (caller-saved!)
- Правильно восстановить стек
- Проверить для n = 5 (должно быть 120)
### Задание 4: FSM с подсчётом
Напишите автомат, который:
- Читает строку из цифр
- Подсчитывает количество нулей
- Возвращает count в rax
- Использует локальную переменную-счётчик на стеке
**Пример:**
- `"12003"` → возвращает 2
- `"999"` → возвращает 0
- `"00000"` → возвращает 5
### Задание 5: Отладка с GDB
Дан код с ошибкой:
```nasm
mystery_function:
push rbp
mov rbp, rsp
push rbx
push r12
mov rbx, rdi
mov r12, rsi
; Какие-то вычисления...
add rbx, r12
mov rax, rbx
pop rbx ; ❌ Ошибка здесь!
pop r12
pop rbp
ret
```
**Задания:**
1. Найдите ошибку визуально
2. Проверьте с GDB, какие регистры испорчены
3. Исправьте код
4. Убедитесь, что все callee-saved регистры восстановлены правильно
---
## 12. Справочная информация
### Calling Convention System V AMD64 ABI (краткая памятка)
**Передача аргументов:**
| Аргумент | Регистр | Тип |
|----------|---------|-----|
| 1 | rdi | integer/pointer |
| 2 | rsi | integer/pointer |
| 3 | rdx | integer/pointer |
| 4 | rcx | integer/pointer |
| 5 | r8 | integer/pointer |
| 6 | r9 | integer/pointer |
| 7+ | стек | push в обратном порядке |
**Возвращаемое значение:** rax (или rax:rdx для 128-бит)
**Caller-saved (может быть испорчен вызываемой функцией):**
- rax, rcx, rdx, rsi, rdi, r8, r9, r10, r11
**Callee-saved (должен быть сохранён вызываемой функцией):**
- rbx, rbp, r12, r13, r14, r15
- rsp (указатель стека)
**Выравнивание:** `rsp % 16 == 0` перед инструкцией `call`
### Инструкции управления стеком
| Инструкция | Описание | Эффект на rsp |
|------------|----------|---------------|
| `push reg` | Сохранить регистр на стеке | rsp -= 8 |
| `pop reg` | Восстановить регистр со стека | rsp += 8 |
| `call addr` | Вызвать функцию (push rip, jmp addr) | rsp -= 8 |
| `ret` | Вернуться (pop rip) | rsp += 8 |
| `leave` | Эквивалент `mov rsp, rbp; pop rbp` | rsp = rbp, затем rsp += 8 |
| `enter N, 0` | Создать фрейм стека (медленно, не рекомендуется) | |
### Условные переходы для FSM
**После `cmp op1, op2`:**
| Условие | Unsigned | Signed | Флаги |
|---------|----------|--------|-------|
| Равно | `je` / `jz` | `je` / `jz` | ZF=1 |
| Не равно | `jne` / `jnz` | `jne` / `jnz` | ZF=0 |
| op1 < op2 | `jb` | `jl` | CF=1 (unsigned) / SF≠OF (signed) |
| op1 ≤ op2 | `jbe` | `jle` | CF=1 or ZF=1 / ZF=1 or SF≠OF |
| op1 > op2 | `ja` | `jg` | CF=0 and ZF=0 / ZF=0 and SF=OF |
| op1 ≥ op2 | `jae` | `jge` | CF=0 / SF=OF |
**Мнемоника:**
- **B**elow / **A**bove — для unsigned
- **L**ess / **G**reater — для signed
- **E**qual — для любых
### Системные вызовы (частичный список)
| Номер | Имя | rdi | rsi | rdx | Описание |
|-------|-----|-----|-----|-----|----------|
| 0 | read | fd | buf* | count | Читать из файла |
| 1 | write | fd | buf* | count | Писать в файл |
| 60 | exit | code | — | — | Завершить процесс |
**Использование:**
```nasm
mov rax, syscall_number
mov rdi, arg1
mov rsi, arg2
mov rdx, arg3
syscall
; Результат в rax
```
### Отладка с GDB
**Основные команды:**
```bash
# Запуск
gdb ./program
(gdb) break _start # Точка останова
(gdb) run # Запустить
# Пошаговое выполнение
(gdb) stepi # Выполнить одну инструкцию
(gdb) nexti # Следующая инструкция (не заходить в call)
(gdb) continue # Продолжить до следующей точки останова
# Просмотр
(gdb) info registers # Все регистры
(gdb) info registers rax rbx rsp # Конкретные регистры
(gdb) x/10gx $rsp # 10 qword в памяти от rsp (стек)
(gdb) x/s $rdi # Строка по адресу в rdi
(gdb) disassemble # Дизассемблировать текущую функцию
# Точки останова
(gdb) break _A # Остановиться на метке _A
(gdb) break *0x401234 # Остановиться на адресе
(gdb) delete 1 # Удалить точку останова №1
# Проверка выравнивания
(gdb) p $rsp % 16 # Должно быть 0 перед call
```
### Полезные макросы для NASM
```nasm
; Сохранить все callee-saved регистры
%macro SAVE_CALLEE 0
push rbx
push r12
push r13
push r14
push r15
%endmacro
; Восстановить все callee-saved регистры
%macro RESTORE_CALLEE 0
pop r15
pop r14
pop r13
pop r12
pop rbx
%endmacro
; Стандартный пролог функции
%macro FUNC_PROLOGUE 1
push rbp
mov rbp, rsp
sub rsp, %1 ; %1 = размер локальных переменных
%endmacro
; Стандартный эпилог функции
%macro FUNC_EPILOGUE 0
mov rsp, rbp
pop rbp
ret
%endmacro
```
**Использование:**
```nasm
my_function:
FUNC_PROLOGUE 32 ; Выделить 32 байта локальных
SAVE_CALLEE ; Сохранить регистры
; Тело функции
RESTORE_CALLEE
FUNC_EPILOGUE
```
### Ссылки на документацию
- [System V AMD64 ABI](https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf) — официальное соглашение о вызовах
- [Intel Software Developer Manual](https://software.intel.com/content/www/us/en/develop/articles/intel-sdm.html) — полное описание инструкций x86-64
- [Linux System Call Table](https://filippo.io/linux-syscall-table/) — таблица системных вызовов
- [NASM Documentation](https://www.nasm.us/doc/) — документация по ассемблеру NASM
---
**Конец методички для семинара 2**