diff --git a/2 курс/Языки программирования/Вызов функций. Использование стека. Конечные автоматы.md b/2 курс/Языки программирования/Вызов функций. Использование стека. Конечные автоматы.md new file mode 100644 index 0000000..b25bbad --- /dev/null +++ b/2 курс/Языки программирования/Вызов функций. Использование стека. Конечные автоматы.md @@ -0,0 +1,282 @@ +## Содержание +- [[#Введение]] + - [[#Переходы]] + - [[#Локальные метки]] + - [[#Регистры]] + - [[#Хранение данных в памяти]] + - [[#Указатели]] +- [[#Функции]] + - [[#Определение функции]] + - [[#Возврат из функций]] + - [[#Функции с аргументами]] + - [[#Возврат значений из функции]] + - [[#Соглашение вызова]] +- [[#Стек]] + - [[#Использование стека для локальных переменных]] + - [[#Выравнивание стека]] +- [[#Конечный автомат]] + - [[#Введение в конечные автоматы]] + - [[#Разбор числа со знаком]] + - [[#Пять состояний процесса]] + - [[#Другие ситуации использования автоматов]] + - [[#Чем автоматы непривычны]] + - [[#Автоматы и регулярные выражения]] + - [[#Недетерминизм]] + - [[#Неопределенность]] + - [[#Полнота]] + - [[#Реализация]] + +## Введение + +### Переходы + +Как известно, все инструкции в языке ассемблера выполняются последовательно. Но порядок выполнения инструкций можно явно изменить. + +>**Переходы** - инструкции, которые явно меняют порядок выполнения программы. + +Безусловный переход к необходимой инструкции по адресу выполняется с помощбю инструкции `jmp`. Инструкцию `jmp addr` можно интерпретировать как команду `mov rip, addr` (сама такая команда, к слову, исполнена быть [[#Регистры|не может]]. + +Но переходы также бывают и условными, то есть исполняются только при определенном условии. Чаще всего такие условия устанавливаются по [[#Регистры|регистру]] `eflags/rflags` во время исполнения программы. Почти для каждого флага (коих всего шесть - CF (Carry Flag), PF (Parity Flag), AF (Auxiliary Carry Flag), ZF (Zero Flag), SF (Sign Flag) и OF (Overflow Flag), но AF при условных переходах не используется) существует как минимум по две инструкции перехода: если флаг установлен в ноль или в единицу. Помимо этого, существуют другие полезные инструкции, такие как `ja`, `jb` (для беззнаковых числе), `jg`, `jl` (для знаковых чисел), `jae`, `jle` и подобные. + +Для того, чтобы поставить необходимые для перехода флаги, в основном используются инструкция `test` или инструкция `cmp`, которые эти флаги ставят. Для проверки какого-либо регистра `reg` на ноль чаще всего используют команду `test reg, reg`. +### Локальные метки + +Для того, чтобы выделять различные участки подпрограммы, используются локальные метки. +Локальные метки начинаются с точки. Такие метки должны находится внутри глобальных меток. Для доступа внутри последней глобальной метки можно использовать название в точности соответствующее названию переменной. Если же необходимо обратиться к этой метке извне последней глобальной, то для этого можно соединить названия этой метки и локальной метки, получив одну метку с точкой посередине. +### Регистры + +У каждого регистра существует собственное назначение. Так, регистры `rax` и `rdx` используются для получения значений из функций, регистры `rdi`, `rsi`, `rdx`, `rcx`, `r8` и `r9` - для передачи аргументов в функцию, `rbp` и `rsp` - для обращения к стеку, `r10` и `r11` - для временных значений, а регистры `rbx`, `r12`, `r13`, `r14` и `r15` - регистры общего назначения. Также существуют специальные регистры, такие как `rip` или `rflags`, которые контролируют процесс исполнения и их нельзя изменить напрямую. + +Перед началом выполнения программы значения большинства регистров являются мусором и не заданы явно. Поэтому при написании программы нужно либо явно задавать значения регистров, либо считать, что в них записано рандомное число. +### Хранение данных в памяти + +Можно реализовать косвенную адресацию. Для этого регистр, с которого берется адрес, можно заключить в квадратные скобки. Тогда программа перейдет по адресу, указанному в регистре, и возьмет столько байт, сколько ей нужно, в зависимости от операции. + +Существует несколько способов хранения данных в памяти. Big endian сохраняет несколько значений начиная с самого старшего значения. Little endian сохраняет несколько значений начиная с самого младшего значения. Intel в основном использует little endian из-за того, что в некоторых моментах он удобнее: если есть значение длиной 8 байт, которое занимает не все из них (то есть один или несколько самых старших байт равны нулю), то начиная чтение с адреса читаться будут сразу же значимые значения, а не просто нули. + +Помимо этого существует еще и middle endian. Если есть несколько значений, то сначала записывается половина из них (самые младшие) в обратном порядке (от большего к меньшему), а после - оставшаяся половина, также в обратном порядке. +### Указатели + +Указатели - это адреса ячеек памяти. Они могут быть сохранены в регистры или ячейки памяти. Длина указателя - 8 байт. + +Данные в основном занимают несколько ячеек. В указателе нет никакой информации о длине хранимых данных. + +Для сохранения по указателю данных, длина хранения которых не указана, можно использовать специальные указатели размера: byte, word, dword, qword. По факту, можно думать об этом как о спецификации типов данных. Так, перед загрузкой значения в стек можно указать word, dword или qword для обозначения размера выделенной памяти под значение. + +Существует несколько способов закодировать операнды в инструкциях: напрямую (инструкция хранится в памяти, операнды - часть этой памяти и имеют собственные адреса; по факту - прямая загрузка операнда в память), через регистр (необходимое значение сначала передается в регистр, а потом уже используется), прямым доступом к памяти (по адресу напрямую обратится к операнду или взять значение адреса напрямую из регистра) и по индексу с размерностью и расположением (имеем базу, индекс, размер и расположение; точный адрес высчитывается как база + индекс\*размер + расположение; база и индекс - значения, заданные напрямую, или регистры, размер - количество байт (1, 2, 4 или 8), а расположение - всегда заданно напрямую). +## Функции + +Функции позволяют отделить логически различные части кода друг от друга. Такое построение кода помогает использовать абстракции и создавать более сложные программы. + +Для того, чтобы в языке ассемблера определить функцию, необходимо: +1. создать для нее [метку](Среда%20выполнения.%20Основы%20языка%20ассемблера.md#Секции,%20метки%20и%20комментарии); +2. выделить код функции отдельно по [метке](Среда%20выполнения.%20Основы%20языка%20ассемблера.md#Секции,%20метки%20и%20комментарии). + +По факту, функции работают как [[#Переходы|переход]] с возвратом в место вызова. При вызове функции адрес возврата сохраняется в [[#Стек|стек]]. Для вызова функции используется инструкция `call` (буквально `call addr` можно разложить как `push rip` и `jmp addr`, хотя вручную такие команды выполнить [[#Регистры|не получится]]). + +Можно расписать вызов функции по шагам: +1. Сохранить все важные значения [[#Соглашение%20вызова|caller-saved]] регистров (их можно сохранить в стеке). +2. Выставить необходимые [[#Функции%20с%20аргументами|аргументы]] в нужные регистры. +3. Вызвать функцию инструкцией `call`. +4. Сохранить [[#Возврат значений из функции|результат работы]] функции из `rax`. +5. Вернуть [[#Соглашение%20вызова|caller-saved]] регистры в исходное положение. + +Иногда функцию также называют подпрограммой, чтобы отделить функцию от таковых в языках высокого уровня. +### Определение функции + +В качестве примера выделения функции можно рассмотреть [простейший пример](Среда%20выполнения.%20Основы%20языка%20ассемблера.md#Простейшая%20программа) программы на ассемблере. Есть смысл выделить в этой программе функцию выхода: тогда можно будет обращаться к ней из любого места программы. + +Для определения функции необходимо использовать [метку](Среда%20выполнения.%20Основы%20языка%20ассемблера.md#Секции,%20метки%20и%20комментарии), которая на эту функцию будет указывать: в данном случае, очевидно, метка будет называться `exit`. После [метки](Среда%20выполнения.%20Основы%20языка%20ассемблера.md#Секции,%20метки%20и%20комментарии) идет выделенный для этой функции код. Для вызова функции в программе будет использоваться инструкция `call`. + +Таким образом получим новую программу. +``` +section .data +message: db 'hello, world!', 10 + +section .text +global _start + +exit: ; Это метка начала функции exit + mov rax, 60 ; Это функция exit + xor rdi, rdi ; Это функция exit + syscall ; Это функция exit + +_start: + mov rax, 1 + mov rdi, 1 + mov rsi, message + mov rdx, 14 + syscall + call exit ; Это вызов функции exit +``` +### Возврат из функций + +Вообще, подпрограмма обязана иметь возможность быть вызвана отовсюду и вернутся в место вызова. Для того, чтобы завершить функцию и вернутся к месту ее вызова, используется инструкция `ret` (буквально команду `ret` можно интерпретировать как `pop rip`). + +Не стоит завершать функцию, если [[#Стек|стек]] не находится в том же положении, в котором был в начале инструкции. Иначе программа в качестве адреса возврата из [[#Стек|стека]] возьмет первую попавшуюся переменную, которая в этот момент будет на вершине стека, и скорее всего переход не удастся. + +При выполнении подпрограммы для завершения программы с системным выходом `exit` можно не писать возврат из подпрограммы, так как программа все равно уже не будет работать. + +При этом инструкцию `call` можно заменить на инструкцию `jmp` в случае, если возврат из подпрограммы не подразумевается (к примеру, в подпрограмме выполняется системный вызов `exit`). +### Функции с аргументами + +>**Аргумент** - кусок данных, который нужен функции для работы. + +Существует три способа передать аргументы в функцию: загрузить их в регистры, в [[#Стек|стек]] или напрямую в память. + +Функции могут иметь сколько угодно аргументов. Как упоминалось [[#Регистры|ранее]], для получения первых шести из них используются регистры `rdi`, `rsi`, `rdx`, `rcx`, `r8` и `r9` по порядку. Остальные аргументы ложатся в [[#Стек|стек]] в обратном порядке. +### Возврат значений из функции + +Очень часто основная цель функций - это получение каких-либо значений на основе полученных [[#Функции с аргументами|аргументов]] и возврат этого значения обратно в программу, которая эту функцию вызвала. Поэтому необходим механизм для возврата значений из функции. Сделать это можно теми же способами, что и передать аргументы в функцию. + +Тем не менее, для возврата результата функции принято использовать регистр `rax`. Если нужно вернуть два аргумента, то для второго можно использовать `rdx`. + +Некоторые [системные вызовы](Среда%20выполнения.%20Основы%20языка%20ассемблера.md#Команды) тоже возвращают значения. +### Соглашение вызова + +Существуют правила, в соответствии с которыми стоит писать функции. + +Такие правила гарантируют возможность свободно переписывать функции и подменять их реализации. Иначе же вызов функций станет ненадежным: что-то сломается или сразу при использовании, или позже при замене реализации функции. + +Очевидно, регистры могут изменятся внутри функции. Поэтому регистры делят на **callee-saved** (регистры, которые по завершению функции должны оказаться теми же, что и в начале) и **caller-saved** (регистры, которые по завершению функции могут быть изменены). Всего callee-saved регистров семь: `rbx`, `rbp`, `rsp`, `r12`, `r13`, `r14`, `r15`. Все остальные - caller-saved. +## Стек + +Стек располагается в нижних ячейках памяти и читается снизу вверх (то, что снизу, было сложено туда раньше). В ассемблере на вершину стека (то есть последний сложенный туда элемент) указывает регистр `rsp` (`stack pointer` - указатель стека). При увеличении количества элементов в стеке значение регистра `rsp` уменьшается, и наоборот - при уменьшении количества элементов в стеке значение регистра `rsp` увеличивается. +### Использование стека для локальных переменных + +>**Выделить память в стеке** - значит уменьшить указатель стека так, чтобы между его новым значением и последним значимым элементом образовалось незанятое пространство для данных. + +Всего существует три способа выделить память в стеке (для, к примеру, трех значений - каждое из них 8 байт, итого 24). +``` +Первый способ: +sub rsp, 24 -> add rsp, 24 + +Второй способ: +push 0 pop +push 0 -> pop +push 0 pop + +Третий способ: +push rbp mov rsp, rbp +mov rbp, rsp -> pop rbp +sub rsp, 24 +``` + +Первыми двумя способами указатель стека просто передвигается вверх. Разница между ними заключается в том, что в первом случае на месте освобожденных значений может быть любой мусор. Во втором же случае все используемые значения будут гарантированно нулями. + +Третий способ выделения памяти в стеке работает немного иначе. + +>**Фрейм стека** - диапазон памяти в стеке, используемый в отдельной [[#Функции|функции]]. + +Как известно, в `rsp` хранится адрес вершины стека. Для обращения к переменным внутри стека используется относительная адресация от вершины стека. Но это не всегда удобно: некоторые переменные могут иметь разное смещение в разных частях программы, ведь значение вершины стека может изменяться. Поэтому есть смысл сохранять состояние вершины в отдельный регистр и обращаться к переменным относительно него, тогда все смещения будут постоянными. Для этого принято использовать регистр `rbp` (`base pointer`). + +Из-за того, что регистр `rbp` является callee-saved, его необходимо отдельно сохранять в начале вызываемой подпрограммы. Это показано в третьем способе выделения памяти в стеке. + +Выделение памяти под стек также иногда называется прологом, а в ассемблере существует инструкция `enter`, которая реализует третий способ пролога. В то же время освобождение памяти в стеке также иногда называется эпилогом, и в ассемблере для него существует функция `leave`. +### Выравнивание стека + +При работе со стеком принято выравнивать значение стека по 16 байтам. Это значит, что при выравнивании стека под каждое значение внутри отводится по 16 байт. В самом начале стек изначально выравнен по 16, так как в начале программы у функции `_start` нет адреса возврата. + +В основном такое требование существует, потому что помимо 64-битных регистров существуют также и 128-битные, и как раз для значений, которые они могут хранить, иногда было бы важно, чтобы стек был выровнен. +## Конечный автомат + +*Данный раздел является логическим продолжением [лекции](Формальные%20грамматики%20и%20теория%20автоматов.md).* + +>**Модель вычислений** - язык, на котором описывается решение проблемы. + +Проблема, нахождение решения которой в одной модели может быть очень сложной, в другой может решаться тривиально. Именно по этому важно знать несколько моделей вычислений и использовать их в тех местах, в которых они дадут наибольшую пользу по сравнению с другими моделями. + +Одной из таких моделей вычислений являются [конечные автоматы](Формальные%20грамматики%20и%20теория%20автоматов.md#Конечный%20автомат). + +>**Детерминированные конечные автоматы** - абстрактная машина для чтения строки, следующая некоторым правилам. +### Введение в конечные автоматы + +Чаще всего конечные автоматы используются для контроля внутренних устройств и поиска подстрок, которые удовлетворяют определенному шаблону. + +Некоторые автоматы при переходе между состояниями могут демонстрировать определенную реакцию (то есть выполнять какое-то действие). +#### Разбор числа со знаком + +Для того, чтобы разобрать число со знаком с помощью конечного автомата, потребуется три состояния. + +Алгоритм работы: +- A - начальное состояние + - A -> B, если "+" или "-" + - A -> C, если "0", "1", "2", "3", "4", "5", "6", "7", "8" или "9" +- B - первый символ числа - знак + - B -> C, если "0", "1", "2", "3", "4", "5", "6", "7", "8" или "9" +- C - принимающее состояние + - C -> C, если "0", "1", "2", "3", "4", "5", "6", "7", "8" или "9" +#### Пять состояний процесса + +Как известно, у процесса есть пять основных состояний: new, ready, running, waiting и terminated. Жизненный цикл процесса с такими состояниями несложно представить в виде конечного автомата. + +Алгоритм работы: +- new - начальное состояние + - admit: new -> ready +- ready - процесс готов к работе + - dispatch: ready -> running +- running - процесс выполняется + - interrupt: running -> ready + - I/O or event wait: running -> waiting + - release: running -> terminated +- waiting - процесс ждет + - I/O or event completion: waiting -> ready +- terminated - принимающее состояние +#### Другие ситуации использования автоматов + +Чаще всего конечные автоматы используют для управления роботами или машинами (в таком случае, событием будут показания сенсоров, а реакцией - какое-либо действие), или для контроля сетевых протоколов (в таком случае, событием будет прием пакетов разного типа, а действием - ответы на эти пакеты). + +Существует много ситуаций, для которых можно построить конечный автомат, но не во всех из них это будет уместно. + +Очевидно, не все программы можно рассматривать как конечные автоматы. Эта модель вычислений не [Тьюринг-полная](https://ru.wikipedia.org/wiki/Полнота_по_Тьюрингу), и не может анализировать сложные рекурсивно структурированные тексты. +### Чем автоматы непривычны + +Главное различие между автоматами и стандартной моделью компьютерных вычислений заключается в том, что у автомата нет памяти, совсем никакой. Вместо этого есть только конечный набор фиксированных состояний. + +В конечных автоматах нет условных переходов. Все, что существует - это состояния и переходы между ними. + +Из-за этого автоматы удобны для описания таких алгоритмов, в которых действия и условия глобальны. + +В отличие от стандартных языков программирования, автоматы можно легко проанализировать, что помимо прочего позволяет легко писать программы на их основе. +### Автоматы и регулярные выражения + +[Регулярные выражения](Формальные%20грамматики%20и%20теория%20автоматов.md#Регулярные%20выражения%20и%20автоматы) - один из способов кодирования конечных автоматов. + +В основном используется два подхода для проверки строки на соответствие регулярному выражению. + +Первый подход - подход "в лоб". То есть, проверка всех строк, подходящих для регулярного выражение, на соответствие данной строке. Этот подход далеко не самый оптимальный: бывают случаи, когда необходимо перебрать миллиарды различных комбинаций, чтобы найти необходимое или понять, что подходящей комбинации не существует. + +Второй подход - построить конечный автомат на основе данного регулярного выражения. В основном, такие автоматы являются [[#Недетерминизм|недетерминированными]]. +### Недетерминизм + +Недетерминизм противоположен [детерминизму](Формальные%20грамматики%20и%20теория%20автоматов.md#ДКА%20и%20НКА). + +В отличие от детерминированных автоматов, недетерминированные могут иметь несколько функций перехода для одного и того же состояния и входного символа. + +В случаях с несколькими переходами автомат выполняет все эти переходы, и дальше уже вместо одного состояния рассматривает все полученные (то есть, вместо одного состояния - множество состояний). Таким образом, каждое из таких состояний является равноправным. +### Неопределенность + +>**Неопределенное поведение** - состояние, при котором для текущего входящего символа нет функции перехода. + +Так как неопределенное поведение рассматривается чаще в математике, чем в инженерии, здесь рассматриваются только определенные состояния. Все неопределенные состояния классифицируются как ошибки. +### Полнота + +>**Полный автомат** - автомат, у которого нет неопределенного поведения. + +Это означает, что из каждого состояния для каждого возможного события есть переход. + +Неполный автомат можно легко достроить до полного, добавив к нему новые состояния. + +Кодировать имеет смысл только полные автоматы. +### Реализация + +Есть несколько шагов, для того чтобы реализовать конечный автомат на ассемблере или C. +1. Дополнить автомат до [[#Полнота|полноты]]. +2. Внедрить программу для получения каждого следующего символа. +3. Для каждого состояния необходимо... + 1. создать [метку](Среда%20выполнения.%20Основы%20языка%20ассемблера.md#Секции,%20метки%20и%20комментарии); + 2. вызвать программу чтения символов; + 3. прочитать полученный символ и вызвать необходимую [[#Переходы|функцию перехода]] (перейти к другой [метке](Среда%20выполнения.%20Основы%20языка%20ассемблера.md#Секции,%20метки%20и%20комментарии) состояния); + 4. обработать все остальные символы как [[#Неопределенность|неопределенное поведение]]. + + diff --git a/2 курс/Языки программирования/Языки программирования.md b/2 курс/Языки программирования/Языки программирования.md index cd754d5..942bb3b 100644 --- a/2 курс/Языки программирования/Языки программирования.md +++ b/2 курс/Языки программирования/Языки программирования.md @@ -5,6 +5,7 @@ ## Семинары - [Среда выполнения. Основы языка ассемблера](Среда%20выполнения.%20Основы%20языка%20ассемблера.md) +- [Вызов функций. Использование стека. Конечные автоматы](Вызов%20функций.%20Использование%20стека.%20Конечные%20автоматы.md) - (и тд) ## Ссылки