2107 lines
78 KiB
Markdown
2107 lines
78 KiB
Markdown
# Методичка по языку ассемблера 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[Начальное состояние<br/>RSP = 0x7FFFFFFFE000]
|
||
A --> B[push rax где rax=0x42]
|
||
B --> C[RSP = 0x7FFFFFFFDFF8<br/>память 0x...DFF8: 0x42]
|
||
C --> D[push rbx где rbx=0x100]
|
||
D --> E[RSP = 0x7FFFFFFFDFF0<br/>память 0x...DFF0: 0x100<br/>память 0x...DFF8: 0x42]
|
||
E --> F[pop rcx]
|
||
F --> G[RCX = 0x100<br/>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<br/>Вызывающий сохраняет]
|
||
A --> C[Callee-saved<br/>Вызываемая функция сохраняет]
|
||
|
||
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<br/>всегда сохраняется!]
|
||
|
||
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)<br/>НЕ кратно 16!
|
||
|
||
Note over FuncA: push rbp (rsp -= 8)<br/>rsp = 0x7ffefff0 (кратно 16)
|
||
Note over FuncA: sub rsp, 24<br/>rsp = 0x7ffeffe8 (НЕ кратно 16)
|
||
Note over FuncA: Нужно выровнять перед вызовом!
|
||
Note over FuncA: sub rsp, 8<br/>rsp = 0x7ffeffd8 (кратно 16)
|
||
|
||
FuncA->>FuncB: call func_b
|
||
Note over FuncB: rsp = 0x7ffeffD0 (0x7ffeffd8 - 8)<br/>Смещение 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<br/>читает '-'] --> B[Состояние B<br/>читает '7']
|
||
B --> C[Состояние C<br/>читает newline]
|
||
C --> D[Состояние D<br/>выводит 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**
|