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