Compare commits

...

29 Commits

Author SHA1 Message Date
LeterZP 89d9b70b79 Добавлена лекция 1 по физике 2026-11-01 00:48:08 +03:00
LeterZP 383d5c1525 Добавлена лекция 4 по языкам программирования 2026-10-31 19:29:43 +03:00
LeterZP 10540b1f7d Добавлен семинар 3 по языкам программирования 2026-10-15 01:21:29 +03:00
LeterZP eb374cf32a Слегка изменено оформление лекции 3 по языкам программирования 2026-10-14 21:35:24 +03:00
LeterZP 8d0960ee7f Добавлена лекция 3 по языкам программирования 2026-10-14 00:04:33 +03:00
LeterZP 442c8d45a6 Добавлена лекция 3 по веб-программированию 2026-10-11 21:46:22 +03:00
LeterZP 6e2835d301 Добавлена библиотека команд ассемблера для языков программирования 2026-10-10 01:12:19 +03:00
LeterZP b87a5d51cf Добавлен семинар 2 по языкам программирования 2026-10-06 23:21:50 +03:00
LeterZP 2c3a4cdbf5 Изменена ошибка ссылки в содержании 2026-10-06 20:18:11 +03:00
LeterZP 365256ed47 Изменены ссылки на заголовки внутри файлов 2026-10-01 23:44:56 +03:00
LeterZP 02f469090a Добавлен семинар 1 по языкам программирования 2026-09-30 00:09:23 +03:00
LeterZP b4288cd2b1 Добавлены ссылки на дополнительные материалы 2026-09-27 22:11:35 +03:00
LeterZP c67c952069 Добавлена лекция 2 по языкам программирования 2026-09-27 21:56:45 +03:00
LeterZP 67d743f810 Добавлена лекция 1 по языкам программирования 2026-09-26 16:55:40 +03:00
LeterZP 8a40990ea5 Добавлена лекция 3 по архитектуре компьютера 2026-09-26 00:36:51 +03:00
LeterZP 368c7de9de Исправлены опечатки в лекции 2 теории вероятностей 2026-09-22 12:31:59 +03:00
LeterZP 5159d80daf Слегка дополнена лекция 1 по веб-программированию 2026-09-20 00:01:53 +03:00
LeterZP 1a346c2aee Добавлена лекция 2 по веб-программированию 2026-09-19 23:57:21 +03:00
LeterZP 8a5812c71b Исправлены некоторые опечатки 2026-09-19 21:47:26 +03:00
LeterZP 31cd3d4985 Исправлены опечатки в лекции 1 по теории вероятностей 2026-09-19 01:54:42 +03:00
LeterZP 6569622ef8 Слегка дополнена лекция 1 по теории вероятностей 2026-09-19 01:53:02 +03:00
LeterZP 158d62042c Добавлена лекция 2 по теории вероятностей 2026-09-19 01:42:56 +03:00
LeterZP 03840f5e8e Добавлена лекция 2 по архитектуре компьютера 2026-09-19 00:02:12 +03:00
LeterZP 04248a3517 Ссылки изменены в соответствии со стандартом, используемым Obsidian 2026-09-18 21:01:54 +03:00
LeterZP 30ac973762 Снова попытка исправить ссылки 2026-09-18 16:37:16 +03:00
LeterZP 7842603919 Попытка исправить ошибки относительными ссылками 2026-09-18 15:22:32 +03:00
LeterZP 1231c4e4a3 Снова немного исправлены ссылки 2026-09-18 15:18:03 +03:00
LeterZP 187dbf537b Изменено название основного md-файла 2026-09-18 15:10:05 +03:00
LeterZP 9bb3732236 Исправлены небольшие проблемы с ссылками 2026-09-18 15:08:58 +03:00
21 changed files with 3073 additions and 39 deletions
@@ -2,9 +2,9 @@
- **[Архитектура компьютера](Архитектура%20компьютера/Архитектура%20компьютера.md)**
- **[Веб-программирование](Веб-программирование/Веб-программирование.md)**
- **[Языки программирования](Языки%20программирования/Языки%20программирования.md)**
- **[Теория вероятностей](Теория%20вероятностей/Теория%20вероятностей.md)**
- **[Физика](Физика/Физика.md)**
- **[Языки программирования](Языки%20программирования/Языки%20программирования.md)**
### Журналы
- [Архитектура компьютера](https://docs.google.com/spreadsheets/d/1WvS1ad9J6jXNwSxTRnQs4yGgUBlwFzopUXUiYbGCZMw/edit?usp=sharing)
- [Веб-программирование](https://docs.google.com/spreadsheets/d/1A2XNYpN1Q1rr5-mpkAk5g0BP4NDY-cwRWtBudLz6Xis/edit?usp=sharing)
- [Веб-программирование](https://docs.google.com/spreadsheets/d/1A2XNYpN1Q1rr5-mpkAk5g0BP4NDY-cwRWtBudLz6Xis/edit?gid=696774553#gid=696774553)
@@ -1,5 +1,7 @@
### Лекции
- [Вычислительные платформы](Вычислительные%20платформы.md)
- [Информационные и управляющие системы. Понятия системы и архитектуры](Информационные%20и%20управляющие%20системы.%20Понятия%20системы%20и%20архитектуры.md)
- [Еще не компьютеры. Реле](Еще%20не%20компьютеры.%20Реле.md)
- (и тд)
### Ссылки
@@ -9,5 +11,5 @@
- [Эмулятор ассемблера](https://wrench.edu.swampbuds.me/submit-form)
### Доп. материалы
- Книга - Цифровая схемотехника и архитектура компьютера - Дэвид Харрис и Сара Харрис
- Книга - Архитектура компьютера (англ. *Structured Computer Organisation*) - Эндрю Таненбаум (и другие его книги)
- Книга - [Цифровая схемотехника и архитектура компьютера](https://picloud.pw/media/resources/posts/2018/03/08/Харрис_Цифровая_схемотехника_и_архитектура.pdf) - Дэвид Харрис и Сара Харрис
- Книга - [Архитектура компьютера (англ. *Structured Computer Organisation*)](https://turan-edu.uz/media/books/2024/05/27/Andrew_S._Tanenbaum_-_Structured_Computer_Organization.pdf) - Эндрю Таненбаум (и другие его книги)
@@ -0,0 +1,276 @@
## Содержание
- [[#Решение задач компьютеров без компьютеров]]
- [[#Отдельные операции]]
- [[#Таблицы значений]]
- [[#Устройства]]
- [[#Механизмы]]
- [[#Сравнение устройств и механизмов]]
- [[#Современность]]
- [[#Информационные системы]]
- [[#Эффективное решение]]
- [[#Достоинства]]
- [[#Практика]]
- [[#Системы управления]]
- [[#Жаккард]]
- [[#Проблема создания станка]]
- [[#Жаккардовый ткацкий станок]]
- [[#Программное обеспечение]]
- [[#Другие примеры]]
- [[#Поиск кратчайшего пути на взвешенном графе]]
- [[#Задача коммивояжера]]
- [[#Приставка Dendy и пистолет]]
- [[#Реле и релейные диаграммы]]
- [[#Электрическое реле]]
- [[#Релейная диаграмма]]
- [[#Управление двигателем]]
- [[#Практика применения реле]]
- [[#Реле и состояние]]
- [[#RS-триггер]]
- [[#От реле к транзистору]]
## Решение задач компьютеров без компьютеров
Несмотря на то, что сейчас компьютеры являются неотъемлемой частью нашей жизни, основные задачи, которые они решают, схожи с задачами, которые ставились еще несколько столетий назад. И все же, люди в прошлом такие задачи решали даже без компьютеров.
Еще до создания компьютеров существовали примитивные вычислительные алгоритмы. Такие с одной стороны простые, но в тоже время сложные, алгоритмы могли выполнять те же задачи без электрических схем и компьютеров, используя при этом различные способы.
### Отдельные операции
Самым примитивным методом для вычисления являются простейшие устройства, способные вычислять заранее заданные функции для заранее заданных параметрах (обычно такое задание включает либо точное количество используемых значений, либо какой-то промежуток значений). Но даже они не оставались неизменными со временем - из изначального вычисления с помощью дискретных и цифровых величин ([[#Таблицы%20значений|таблиц]])) осуществлялся постепенный переход к аналоговым [[#Устройства|устройствам]]), а позже возвращение обратно к цифровым [[#Механизмы|механизмам]].
#### Таблицы значений
Таблицы значений отражают в себе какую-то зависимость, которую можно представить в виде математической функции.
>**Математическая функция** - отображение множества A в множество B.
Обратно, если входные и выходные значения математической функции счетные, то ее можно представить в виде таблицы значений.
Примерами таких таблиц могут служить [таблицы истинности](https://ru.wikipedia.org/wiki/Таблица_истинности), [таблицы Брадиса](https://ru.wikipedia.org/wiki/Таблицы_Брадиса) (таблицы, по которым можно найти значения тригонометрических функций и логарифмов по входным значениям) и тому подобное.
Но у таких таблиц есть существенная проблема - они занимают слишком много места. К примеру, таблица для сложения 32-битных чисел потребовала бы $2^{32} \cdot 2^{32} = 2^{64}$ строк, что определенно не поместится ни в какую память.
#### Устройства
В данном случае, устройства воплощают в себе таблицу значений в виде какой-либо шкалы зависимости, а значит поиск решения из вычисления превращается в простое сопоставление необходимых значений.
Такие на первый взгляд примитивные устройства могут выполнять различные операции, к примеру умножение, деление, возведение в степень, взятие корня, вычисление логарифма, поиск значений тригонометрических или даже гиперболических функций.
В качестве примера таких устройств могут служить [логарифмическая линейка](https://ru.wikipedia.org/wiki/Логарифмическая_линейка?ysclid=muh4ib08v3154892960) (простая конструкция из нескольких линеек со специальной шкалой, находит нужное значение при правильном сопоставлении полученных данных и взятии другого значения из места для нужного ответа; с помощью такой линейки можно проводить даже операцию деления), [номограмма](https://ru.wikipedia.org/wiki/Номограмма?ysclid=muh36utp8i349343873) для умножения (три шкалы, две из них расположены симметрично, образуя параболу, а третья с другим масштабом идет из точки пересечения шкал ровно вверх; при проведении прямой между двумя умножаемыми значениями такая линия будет пересекать третью линию ровно в нужном значении) и тому подобное.
#### Механизмы
Механизмы в данном случае представляют из себя специальные механические устройства, с помощью которых производятся вычисления. В основном, вычисление на таких устройствах происходит алгоритмически: для каждого возможного шага предусматривается специальный алгоритм, выполняемый автоматически на основе начальных значений.
Такие механизмы способны выполнять простейшие операции, такие как сложение, вычитание, умножение и деление. При этом при каждом таком действии механизм автоматически переносит необходимые значения между разрядами. Но количество разрядов в таком механизме фиксировано.
Основным примером такого механизма является [арифмометр](https://ru.wikipedia.org/wiki/Арифмометр?ysclid=muh4mtud3e581075263), но могут быть и другие примеры.
#### Сравнение устройств и механизмов
В качестве объектов для сравнения возьмем ранее введенные арифмометр и логарифмическую линейку. Сравнивать будем по нескольким критериям, таким как тип входящих и выходящих значений, рост области определения и размера, а также время расчета.
Для линейки как входные, так и выходные величины являются аналоговыми. Это дает ряд преимуществ: при правильном вычислении границы между точными значениями почти стираются, и появляется возможность находить значения с очень высокой точностью. Но у этого есть и обратная сторона: никто не отменял человеческий фактор, а значит при вычислениях с помощью таких инструментов необходимо учитывать как погрешность ввода и вывода, так и погрешность определенного измерительного экземпляра (ведь линейка может погнуться и показывать значения неправильно) и точность измеряющего. У арифмометра таких недостатков нет: его значения всегда точны до степени, установленной используемыми алгоритмами, и результат не зависит не от человека, ни от конкретного экземпляра. Но отсутствуют и плюсы, присущие линейке: точность вводимых и выводимых значений строго ограниченна.
У линейки рост размера происходит очень быстро - чем больше увеличивается область определения, тем больше значений необходимо обработать, и тем точнее должно быть деление. В то же время размер арифмометра изменяется довольно медленно - он замедляется с каждым увеличением роста области определения на новый разряд.
Время вычисления результата с помощью линейки почти не изменяется - всего-то надо сопоставить несколько значений, время в данном случае больше зависит от самого измеряющего, нежели от самой линейки. Но вот у арифмометра все иначе - рост времени расчета зависит от вычисляемой функции, и если функция будет достаточно сложна, то время такого вычисления составит довольно внушительное число.
#### Современность
Несмотря на то, что данным технологиям уже много лет, они все еще существуют в качестве концепций, но в слегка измененном виде. К примеру, в современных компьютерах таблицы истинности применяются в цифровых схемах, таблицы предрассчитанных значений в [cache](https://ru.wikipedia.org/wiki/Кэш), warm cache и [memoization](https://en.wikipedia.org/wiki/Memoization), а также любые алгоритмические расчеты.
### Информационные системы
В [информационных системах](Информационные%20и%20управляющие%20системы.%20Понятия%20системы%20и%20архитектуры.md#Информационные%20системы) задачи представляют из себя в основном большое количество вычислений с таким же большим количеством промежуточных значений.
Рассмотрим пример решения данной задачи, взяв за основу задачу о расчете артиллерийских таблиц. Представим, что у нас есть артиллерийские орудия, но для того чтобы их правильно использовать, необходимо правильно рассчитать все параметры и данные окружения для точного выстрела. При этом нам доступны только простейшие счетные устройства в ограниченном количестве; математики, физики и другие ученые; а также офицерский корпус и грамотные солдаты.
Данная задача, как вычислительная, характеризуется многократным расчетом сложных формул с большим количеством операций для различных входных параметров. Поэтому использование наивного решения (просто заставить людей производить расчеты по заданным формулам) здесь не подойдет. При таком решении очень высока вероятность возникновения различных проблем, таких как ошибка операции (человек из-за постоянного изменения контекста перепутал операции, к примеру, сложения и умножения), ошибка промежуточных значений (опечатки, изменения в примитивного вида регистровом файле), арифметические ошибки, а также склонность людей к сокрытию ошибок (легче просто подогнать ответ под результат, чем пересчитывать снова).
#### Эффективное решение
В данной задаче, архитектурой самого эффективного решения будет являться реконфигурируемый потоковый вычислитель. Иными словами, представим формулу как орграф, в котором вершины - необходимые вычислительные операции, а стрелки - перемещение промежуточных значений от одной операции к другой. На каждую вершину поставим человека, который будет производить определенную операцию, получая на вход несколько значений с других операции (или, если он первый в цепи, то просто извне системы) и выдавая результат операции в качестве выходного значения.
У такого решения есть довольно много положительных моментов. Основными среди них являются параллельное исполнение независимых операций в графе (несколько вершин, у которых известны все входные значения, могут не дожидаясь других вершин начать вычисления), конвейеризация расчетов (как только вершина выдала результат, можем на вход подать другие значения, что устраняет бесполезное простаивание) и возможность двоирования (вместо одного графа построить несколько одинаковых и соединить их в общей точке для сравнения результатов; если результаты не совпадают, можно отправить данные на перерасчет; это обеспечивает системе надежность и правильность значений).
#### Достоинства
То есть, можем отдельно выделить ряд достоинств эффективного решения в более общем виде. Во-первых, рост производительности, обеспечиваемый параллельным исполнением независимых операций и конвейеризацией расчетов. Во-вторых, рост надежности, обеспечиваемый высоким уровнем специализации узлов (и, соответственно, их простотой) и минимальной вариативностью процесса. В-третьих, возможность реконфигурации вычислителя под новую задачу.
#### Практика
Все вышеперечисленные механизмы так или иначе используются в современных информационных системах. Примерами такого использования могут быть процессоры общего назначения (использующие параллелизм инструкций и конвейеризацию), специализированные процессоры (использующие специализацию узлов и реконфигурируемость (в таких технологиях, как [FPGA](https://ru.wikipedia.org/wiki/Программируемая_пользователем_вентильная_матрица?ysclid=muh6vra9o9775540320) и [CGRA](https://en.wikipedia.org/wiki/Coarse-grained_reconfigurable_array))) или распределенные системы (использующие дублирование и сверку результатов (репликация, консенсус)).
### Системы управления
В [системах управления](Информационные%20и%20управляющие%20системы.%20Понятия%20системы%20и%20архитектуры.md#Управляющие%20системы) задачи в основном представляют из себя создание алгоритма для управления физической установкой. При этом системы управления глубоко специализированы, поэтому под каждую отдельную систему управления разрабатывается свой алгоритм работы.
В качестве простейшего примера рассмотрим жаккардовый ткацкий станок. Жаккардовый ткацкий станок считается первой программно-управляемой машиной. Но как его создали?
#### Жаккард
>[Жаккард](https://ru.wikipedia.org/wiki/Жаккардовая_ткань?ysclid=muh7v5kyci692504189) - это такой вид ткани с узором, на обратной стороне которого можно увидеть "инвертированный рисунок". Полотно такой такни состоит из вертикальных нитей, которые все равномерно натянуты и ничем друг от друга не отличаются, и горизонтальных нитей разных цветов, которые могут идти как сверху, так и снизу каждой отдельной вертикальной нити, что и формирует сам узор (то есть если нить продета сверху - виден цвет нити, иначе - цвет самого полотна).
#### Проблема создания станка
Для того, чтобы изготовить данное полотно вручную, существует специальный алгоритм действий. Для начала, все вертикальные нити сортируются на две категории: те, которые должны быть сверху следующей протягиваемой горизонтальной нити, и те, которые должны быть снизу. Далее между ними пропускается челнок с натягиваемой горизонтальной нитью ("гребенка") и проходит через все вертикальные нити. Этот процесс повторяется до тех пор, пока полотно не будет готово. Узор при этом может быть как уникальным (что не подлежит автоматизации), так и цикличным.
Очевидно, продевать каждую нить таким образом вручную очень трудозатратно. Поэтому необходимо найти такое решение, которое поможет эти затраты снизить и автоматизировать процесс.
В данном случае проблема, которую необходимо решить, довольно очевидна. Необходимо разработать такой способ автоматизации, который мог бы самостоятельно и нерегулярно разделять вертикальные нити на верхние и нижние. "Гребёнка" в данном случае не работает.
#### Жаккардовый ткацкий станок
Тогда, для того чтобы решить проблему, необходимо заменить "гребенку" другим механизмом, с помощью которого можно было бы динамически менять поднятые вертикальные нити и управлять этим процессом.
В жаккардовом ткацком станке гребенка заменена на специальную систему крючков на пружинах с поворотным механизмом. При простом нажатии на крючок он цепляет нить и поднимает ее вверх. Но если нажать сильнее и провернуть, то крючок отпускает нить и поднимается без нее. Но это все еще не полная автоматизация, ведь на крючки все равно приходится нажимать вручную.
#### Программное обеспечение
Автоматизация в данном станке построена на ленте из пластин. Каждая пластина в ленте имеет отверстия в нужных местах, что позволяет "нажимать" на все необходимые для узора крючки разом (такая пластина называется [перфокартой](https://ru.wikipedia.org/wiki/Перфокарта)). Объединив такие пластины в ленту, можно получить уникальный узор, заданный программой из соединения пластин.
Таким образом, можно составить алгоритм работы станка. Для начала происходит нажатие пластиной на крючки (что делит крючки на верхние и нижние). Далее пропускается челнок с нитью, создающий узор. После продевания нити все крючки нажимаются и проворачиваются, что гарантирует отпускание нитей с крючков. А дальше этот процесс повторяется для каждой следующей пластины, которые продвигаются при сдвиге ленты.
Полученную программу вполне можно назвать программным обеспечением станка. Такой станок в работе можно посмотреть на [видео](https://www.youtube.com/watch?v=K6NgMNvK52A).
### Другие примеры
#### Поиск кратчайшего пути на взвешенном графе
Самым распространенным методом решения данной задачи является [метод Дейкстры](https://ru.wikipedia.org/wiki/Алгоритм_Дейкстры). Данный алгоритм имеет сложность $O(n^2)$ и является вполне универсальным для разных начальных условий задачи. Помимо этого, для него также имеется огромное количество оптимизаций и эвристических приемов для сокращения вычислительной сложности.
Но существует также альтернативное решение. Для него изготавливается натуральная модель взвешенного графа, в котором вершины заменяются на контактные площадки, а ребра - на проводники с сопротивлением, обратно пропорциональным весу ребра. После завершения конструкции на интересующие вершины графа подается питание. В итоге наблюдается цепочка либо сгоревших, либо сильно нагревшихся проводников, и эта цепочка и будет ответом на задачу. Площадь такого решения - $O(n+e)$; время - $O(n)$, при скорости около скорости света.
#### Задача коммивояжера
В этой задаче необходимо найти самый выгодный маршрут через указанные города с посещением их хотя бы по одному разу и возвратом в исходный город. Данная задача NP-полная, то есть точный перебор всех вариантов растет факториально от числа городов $O(n!)$. Также данную можно решить, используя методы динамического программирования, но это тоже довольно затратно - $O(n^22^n)$.
Тем не менее, существуют и другие, иногда немного абсурдные способы. К примеру, в [данной статье](https://royalsocietypublishing.org/rsos/article/5/12/180396/87638/Remarkable-problem-solving-ability-of-unicellular) приведен пример с использованием амебы в качестве коммивояжера. Из-за особенностей амебы, а именно того, что она любит питательную среду, не любит свет и стремится занять максимальную площадь, ей можно манипулировать и направлять так, чтобы она выбирала наилучший путь. По данным статьи, при таком методе заявлены линейный рост скорости поиска от количества городов $O(n)$. Но, кончено, у данного способа есть ограничения, ведь амеба может искать кратчайший маршрут только при количестве городов не более 8 штук.
#### Приставка Dendy и пистолет
В 1990-ых годах, когда появилась приставка Dendy, для нее отдельно продавался пистолет, с помощью которого можно было стрелять в уток на экране телевизора. При этом пистолет безошибочно определял, куда направлен пистолет, и регистрировал попадания почти без потерь. Однако в то время это было настоящее чудо техники, ведь сами пистолеты стоили недорого, а телевизоры были старыми.
Но в чем был секрет такой технологии? На самом деле, все достаточно просто. Экран CRT рисовал кадр построчно лучом, а не выводил его сразу целиком. При выстреле игра на мгновение гасила экран до чёрного, затем вырисовывала все цели по очереди белыми точками. Фотодиод в стволе пистолета засекал момент вспышки; по времени этого момента относительно начала кадра консоль вычисляла, в какую именно цель (и примерно куда на экране) целился игрок. Координата определялась не пространственно, а через время — ещё один пример вычисления "не совсем компьютером", как и остальные примеры этого раздела.
## Реле и релейные диаграммы
### Электрическое реле
Само по себе реле состоит из нескольких элементов: катушка (на которую подается напряжение), ферромагнитный стержень (на который намотана катушка), основание (на котором закреплена катушка), неподвижные контакты (которые реле по своей сути должно замыкать), подвижный якорь (который находится сверху от катушки; на одном конце скреплен с основанием, а на другом имеет контакты; при подаче сигнала на катушку притягивается и замыкает неподвижные контакты реле) и пружина (которая не дает подвижному якорю упасть на катушку без напряжения).
Принцип использования реле достаточно прост. С двух сторон от реле находятся вход и выход, а само реле между ними является ключом для сети. При подаче напряжения на катушку реле замыкает ключ, и сеть соединяется. Если же ток перестает течь, реле возвращается в нормальное состояние. У реле есть всего два состояния: это 0 и 1 (то есть, "нет питания" и "есть питание"). При этом важно помнить, что в схемах не существует сигналов, есть только уровни, которые эти сигналы "создают", поэтому говорить о сигналах в электрических сетях неправильно.
Существует множество других видов реле, таких как механические, пневматические, тепловые, оптические, акустические, магнитные и так далее. Но мы будем рассматривать только электрические реле, потому что они наиболее простые среди всех и ближе всего к тому, что будет изучаться в дальнейшем.
На схемах обозначать реле будем в квадратных скобках \[ ]. При этом простое реле \[ ] называется нормально разомкнутым реле (или повторителем), а реле с палочкой \[\\] - нормально замкнутым реле (и обозначает логическое "НЕ" на схемах).
#### Релейная диаграмма
Чаще всего релейные диаграммы описывают схемы включения оборудования в релейные стойки.
В релейных диаграммах будем также использовать другие специфические обозначения. Обозначим нормально неактивный актуатор через ( ), а нормально активные актуатор - через (\\) (актуаторы - это буквально те же ключи, только их цель заключается не в изменении прохождения через них сигнала, а отправки этого сигнала куда-нибудь в другое место). Также на диаграммах отдельно будем подписывать линии, по которым передается ток и подписывать их буквой L.
Далее опишем стандартные логические операторы через релейные схемы. В качестве примера будем использовать дверной мотор, который может открывать или закрывать дверь.
Первая схема - логическое "И".
```
L1 L2
o o
| Logical AND |
| |
+--[ ]--------[ ]-----------( )---------+
| Key 1 Key 2 Door motor |
| |
```
На схеме видно, что дверь открывается (то есть течет ток) только тогда, когда оба релейных переключателя замкнуты.
Второй опишем схему для логического "НЕ".
```
L1 L2
o o
| Logical NOT |
| |
+--[ ]----------------------(\)---------+
| Key 1 Door motor |
| |
```
По схеме видно, что дверь всегда открыта, кроме того момента, когда переключатель замкнут.
Третьей опишем схему для логического "ИЛИ".
```
L1 L2
o o
| Logical OR |
| |
+----+--[ ]--------------+--( )---------+
| | Exterior unlock | Unlock |
| | | |
| +--[ ]--------------+ |
| Interior unlock |
| |
```
На схеме видно, дверь будет открыта как в случае, когда нажат внутренний переключатель, так и в случае, когда нажат внешний переключатель (и когда оба тоже).
#### Управление двигателем
Теперь, когда у нас есть все необходимые логические элементы, можно составить схему чего-нибудь посложнее. Возьмем, к примеру, схему включения электродвигателя с аварийной остановкой. Обозначим сигнал обычной остановки через Stop, сигнал аварийной остановки через ES, и сигнал запуска двигателя через Start (напомним, что говорить сигнал в контексте электрических схем несовсем корректно).
Сама схема приведена ниже.
```
L1 L2
o o
| |
+--[\]---[\]----+--[ ]---+---------( )----+
| ES Stop | Start | Run |
| | | : |
| +--[ ]---+ : |
| Run : |
| ^ : |
| +.............../ |
| v |
+------------------[ ]-------------( )----+
| Run Motor |
| |
```
На схеме видно, что при подаче сигнала Start цепь замыкается, создавая бесконечный цикл работы с Run. При размыкания любого из реле ES и Stop ток в верхней половине схемы перестает течь и цепь размыкается. То есть на данной схеме ES и Stop ничем не отличаются.
При этом важно отметить, что на данной схеме Run представлена и как реле, и как актуатор. В данном случае оно имеет один и тот же смысл: актуатор и реле Run замыкаются и размыкаются в одно и то же время (на что указывает провод, протянутый между ними).
Для более наглядной демонстрации работы схемы можем нарисовать диаграмму сигналов для одного из запусков цепи, представленной на схеме.
```
________ (1)
ES _______________...___/ \___ (0)
(1)
Stop _______________...________________ (0)
________ (1)
Start __/ \___...________________ (0)
__________..._____ (1)
Run ____/ \__________ (0)
________..._______ (1)
Motor ______/ \________ (0)
------------------ time ------------------->
```
В данном примере с течением времени сначала на короткое время замыкается Start (ровно на то время, чтобы двигатель завелся), после чего Run и Motor работают до тех пор, пока не подается сигнал ES, который с некоторой задержкой отключает сначала реле Run, а после - актуатор Motor.
При этом поговаривают, что если залезть в шкаф управления, то можно найти схему, подобную схеме для управления двигателем.
#### Практика применения реле
Когда-то раньше реле использовались для того, чтобы делать компьютеры. На сегодняшний же день реле можно найти либо в силовых шкафах управления (где высокий ток и неблагоприятная среда эксплуатации), либо в местах, где электроника не работает (но в таких случаях используют неэлектрические реле), либо в программируемых логических контроллерах (ПЛК) (но о них будет сказано в лекциях позднее).
### Реле и состояние
Но все же, для того чтобы собрать полноценный компьютер, все еще не хватает памяти (места, где можно будет хранить информацию). Кончено, для этого тоже существуют релейные схемы.
#### RS-триггер
>**RS-триггер** - асинхронный триггер, который сохраняет своё предыдущее состояние при неактивном состоянии обоих входов и изменяет свое состояние при подаче на один из входов активного уровня.
Для того, чтобы представить RS-триггер в виде небольшой релейной схемы, не требуется ничего особенного.
```
L1 L2
o o
| |
+---+---[ ]---+---(\)---+
| | R | Q |
| | | |
| +---[ ]---+ |
| NQ |
| |
| +---[ ]---+ |
| | Q | |
| | | |
+---+---[ ]---+---(\)---+
| S NQ |
| |
```
На данной схеме, несмотря на то, что уровни Q и NQ графически не соединены, формально они являются одним и тем же. Для того, чтобы удостоверится в правильности данной схемы, можно просто подставить все возможные значения для Q, S и R.
Важно помнить, что в RS-триггере комбинация сигналов R=1 и S=1 является запрещенной комбинацией, и ее никогда не стоит использовать.
### От реле к транзистору
Несмотря на прекрасную конструкцию реле, у него все же имеется некоторые недостатки, ведь он ограничен механикой, а значит способен на износ, дребезг, а также имеет не самую быструю скорость реакции.
Транзистор же является отличной заменой реле, ведь это тот же ключ, но электронный, без движущихся частей, и при этом на порядок быстрее. Так как это такой же ключ, все логические схемы переносятся на него без изменения. А значит, с помощью него уже можно рисовать цифровую схемотехнику.
@@ -0,0 +1,184 @@
## Содержание
- [[#Компьютерные системы]]
- [[#Системы с преобладающей программной составляющей]]
- [[#Классификация компьютерных систем]]
- [[#Информационные системы]]
- [[#Управляющие системы]]
- [[#Система. Системная инженерия]]
- [[#Системная инженерия]]
- [[#Понятие системы]]
- [[#Система как совокупность частей]]
- [[#Система как функциональное место]]
- [[#Операционное окружение]]
- [[#Представление и точка зрения]]
- [[#Система как жизненный цикл]]
- [[#Обеспечивающие системы]]
- [[#Требования для разработки успешной системы]]
- [[#Архитектура]]
- [[#Классическое определение архитектуры]]
- [[#Определение архитектуры в системной инженерии]]
- [[#Сущностные определения системы]]
- [[#Проблема коммуникации при разработке компьютерных систем]]
- [[#Причины важности архитектуры]]
- [[#Риск откладывания управления рисками]]
- [[#V-диаграмма]]
- [[#Эффект архитектурного проектирования]]
## Компьютерные системы
>**Компьютерные системы** (в данном курсе) - любые технические системы, оснащенные алгоритмами или заданным поведением.
Примерами таких компьютерных систем могут являться ПК, сервер, языковая модель, цифровая электрическая схема, кабели зарядки мобильных телефонов, светодиодная лампа, дверной замок, автомобиль, станок и тому подобное. У всех из них есть какие-либо алгоритмы, которые реагируют на определенные действия.
На самом деле, это довольно общее определение, и в современном понятие компьютерных систем определяется намного строже. Иногда бывают случаи, когда очень сложно определить, является ли что-либо компьютерной системой или нет.
В стандартных компьютерных системах в основном [[#Системы с преобладающей программной составляющей|преобладает программная составляющая]].
### Системы с преобладающей программной составляющей
>**Software-intensive systems** are systems in which software development and/or integration are dominant considerations. This includes computer-based systems ranging from individual software applications, information systems, embedded systems, software product lines and product families and systems-of-systems --- [ISO/IEC/IEEE 42010](https://en.wikipedia.org/wiki/ISO/IEC_42010). (*Системы с преобладающей программной составляющей - это такие системы, в которых основной упор делается на разработку и/или внедрение программного обеспечения. Это включает в себя такие компьютерные системы, как отдельное программное обеспечение, информационные системы, встроенные системы, линейки и семейства программного обеспечения и системы систем.*)
В настоящее время такие системы преобладают. В современном мире около половины стоимости всей разработки какого-либо оборудования уходит на разработку программного обеспечения.
### Классификация компьютерных систем
В основе своей, вся [вычислительная техника](Вычислительные%20платформы.md) создана для того, чтобы либо быстро обрабатывать данные - [[#Информационные системы|информационные системы]], либо точно управлять данными - [[#Управляющие системы|управляющие системы]]. Оба этих класса обладают разными требованиями к своим составляющим, и это в конечном итоге выливается в различие между внутренней составляющей таких систем.
#### Информационные системы
Основная цель информационных систем - получить данные, преобразовать или накопить их каким-либо образом, и выдать их в измененном или обработанном виде. Приоритетом таких систем всегда является экономия времени и скорость обработки данных, и даже не важно насколько - даже если можно сэкономить одну лишнюю миллисекунду, это нужно сделать (если можно быстрее, то нужно быстрее).
Из-за этого у такой системы развился ряд особенностей. Основными из них являются упор в производительность, появление параллелизма (параллельных вычислений) и спекулятивных вычислений ("угадывание" ответа), а также развитие кластерных и облачных вычислений.
У этих систем история довольно долгая. Начиналось все еще с огромных установок-мейнфреймов, на которых считали сложные формулы, используя перфокарты. Очевидно, при таком подходе сам по себе компьютер был общим для всех. По мере же уменьшения таких конструкций у каждого человека постепенно появился свой пользователь на машине, что позже переросло в персональные компьютеры. Следующим шагом для информационных систем стало развитие интернета и переход к клиент-серверной архитектуре, что позволило обрабатывать данные не на локальных устройствах, а с использованием серверов в интернете. В последствии же решили отказаться даже от компьютеров, и центр сместился на вычисления с использованием мобильных устройств и облачных серверов.
#### Управляющие системы
Основным приоритетом управляющих систем является взаимодействие с реальным физическим миром с целью контроля или управления посредством получения данных, их преобразования или накопления, и выдачи в измененном или обработанном виде. Приоритетом таких систем является своевременность получения и выдачи данных, а не скорость их обработки.
Основными особенностями таких систем являются встроенное исполнение (то есть интеграция в реальный мир, обладание ограниченными ресурсами или энергией и специализация функций, платформы и аппаратуры), автономная эксплуатация и требование реального времени.
В данном случае, реальное время представляет собой вполне определенный срок. Реальное время не значит быструю работу, абсолютно точную работу или работу без отказов. На самом деле, реальное время - это предсказуемо и в срок. В управляющих системах мы всегда знаем объем вычислительных задач, которые необходимо решить, поэтому основной упор делается на реагирование именно в тот момент времени, в который мы ожидаем, не раньше и не позже. Реальное время также обозначает спецификацию между внешним воздействием и реакцией на него.
Конечно же, нас также интересует история управляющих систем. Началось все еще с 60-ых годов прошлого столетия, с появлением информационно-управляющих систем (ИУС). На их основе были построены встроенные системы (ВсС), а позже они развились и до распределенных встроенных систем (РВсС). В настоящее время самым современным решением считаются киберфизические системы, которые разрабатываются посередине между системами управления и системами вычислительной техники. С каждым десятилетием степень интеграции встроенных систем с объектом мониторинга и/или управления возрастает, хотя изначально ИУС были интегрированы в объект управления достаточно мало.
Параллельно с управляющими системами также развивались малые интегральные схемы, тесно связанные с управляющими системами. С появлением микропроцессоров было принято решение устанавливать их во встроенные системы. Во времена распределенных встроенных систем появление систем на кристалле повысило актуальность таких схем. С появлением же сетей на кристалле, а также графических ускорителей и нейропроцессоров, эти схемы стали неотъемлемой частью киберфизических систем. Таким образом, интегральные схемы выросли параллельно с управляющими системами, увеличив количество элементов в микросхеме с десяти до миллиарда.
## Система. Системная инженерия
### Системная инженерия
>**Системная инженерия** (SE) - это междисциплинарные подход и средство, позволяющее реализовать успешные системы. Он фокусируется на целостном и одновременном понимании потребностей заинтересованных сторон (стейкхолдеров); изучении возможностей; документировании требований; и синтезе, проверке, приемке и разработке решений при рассмотрении всей проблемы, от исследования концепции системы до вывода системы из эксплуатации. (--- [The Guide to the Systems Engineering Body of Knowledge (SEBoK), V.1.3.2014](https://www.academia.edu/8522529/Guide_to_the_Systems_Engineering_Body_of_Knowledge_SEBoK_version_1_3_Copyright_and_Licensing)).
Из определения следует, что системная инженерия - процесс рассмотрения системы не только с какой-то одной точки зрения, а исследование всех углов системы и ее полное понимание. Такой подход помогает принимать решения, понимая все процессы внутри системы и результаты какого-либо вмешательства в нее.
Предметом системной инженерии может быть что угодно, будь то даже машина, самолет или нефтяная вышка (такие системы иногда называют сверхбольшими). В таких системах очень важно иметь представление о всех ее составляющих, ведь одно неверное изменение в конструкции может спровоцировать ряд проблем в других частях системы.
Отсюда вытекает и роль системной инженерии. Системный инженер способен взаимодействовать с покупателем (заказчиком), понимая и анализируя его требования, передавая эти требования в нужном виде команде разработчиков для внедрения, а также поддерживая всю систему на протяжении всего ее жизненного цикла. Системная инженерия фокусируется на том, чтобы удостоверится в достижении целей и задач системы связыванием всех ее частей воедино.
### Понятие системы
Понятие системы можно рассматривать с трех разных точек зрения. С одной стороны, система - это [[#Система как совокупность частей|совокупность частей]] (различные небольшие элементы образуют полноценную систему). С другой, система - это [[#Система как функциональное место|функциональное место]] (система - это место, откуда мы получаем результат, и не важно, что происходит внутри этой системы). С третьей, система - это [[#Система как жизненный цикл|жизненный цикл]] (то, как система рождается и умирает).
#### Система как совокупность частей
Система - это комбинация взаимодействующих между собой элементов, организованных для достижения одной или нескольких установленных целей. Такую систему можно рассматривать как готовый продукт или услугу, которую она предоставляет.
Для такого определения системы можно использовать аналогию. Пусть система - это дом. У этого дома есть свои конструкция, электрическое обеспечение, водопровод, канализация, вентиляция и так далее. В разных частях дома эти конструкции иногда соприкасаются друг с другом, а иногда сосуществуют независимо друг от друга. Также и со структурой системы. Она состоит из различных неотделимых друг от друга частей. С разных ракурсов такую систему можно увидеть по-разному. В зависимости от того, что с этой системой происходит, ее структура может меняться.
#### Система как функциональное место
В данном случае, систему определяет только название, назначение и границы. При этом нам не важно, что именно находится внутри этой системы и как оно работает (что как раз и означает, что мы не пересекаем границы системы).
##### Операционное окружение
При таком подходе у системы можно выделить операционное окружение.
>**Операционное окружение** - окружение, в котором развертываются системы. Проблема или возможность, в ответ на которую была разработана система, тоже является частью этого окружения.
Это важный фактор, который учитывается при определении возможностей системы, желаемых результатов и выгод для заинтересованных сторон, а также ограничений.
##### Представление и точка зрения
Определив систему как функциональное место, мы тогда можем рассматривать систему с разных сторон, с разных точек представления, где каждая рассматривает собственную задачу, выполняемую системой.
>**Представление** (View) - представление системы с заданной точки зрения.
Таким образом, система разбивается на несколько частей, каждая из которых решает собственную функцию.
>**Stakeholder** - individual or organization having a right, share, claim, or interest in a system or in its possession of characteristics that meet their needs and expectations --- [ISO/IEC/IEEE 2015](https://en.wikipedia.org/wiki/ISO/IEC_15288) (*Заинтересованная сторона - это человек или организация, у которой есть права, доля, притязания или интерес к системе или к части ее характеристик, удовлетворяющих соответствующим требованиям и ожиданиям*).
Иными словами, система не собирается в одиночку. За каждую часть системы, за каждое представление, отвечает определенная заинтересованная сторона.
>**Точка зрения** (Viewpoint) - это спецификация соглашений, правил построения и использования представления с целью решения проблем заинтересованных сторон.
То есть, каждая функция системы определяется через собственную точку зрения на систему. Тем не менее, мы не можем представить систему сразу со всех точек зрения. Именно поэтому нам это разбиение так необходимо.
#### Система как жизненный цикл
>**Жизненный цикл системы** - это эволюция интересующей системы во времени от концепции до вывода из эксплуатации.
У жизненного цикла системы можно выделить несколько типовых стадий ее развития.
1. Концептуальный этап
2. Этап разработки
3. Этап производства
4. Этап утилизации
5. Этап поддержки
6. Этап вывода из эксплуатации
В данном случае слово "утилизация" используется как образованное от английского слова "utilization" и означает основной этап использования системы и ее эксплуатации.
Тем не менее, это лишь типовые стадии. Для каждой системы необходимо подгонять стадии под основные нужды с учетом специфики системы, ее целей и возможностей.
##### Обеспечивающие системы
Конечно, система не может выполнить все такие стадии самостоятельно. Именно для этого существуют обеспечивающие системы.
>**Обеспечивающая система** - система, которая дополняет интересующую систему на этапах ее жизненного цикла, но не обязательно вносит непосредственный вклад в ее функционирование во время эксплуатации. (--- [ISO 15288](https://en.wikipedia.org/wiki/ISO/IEC_15288))
Иными словами, для каждого этапа системы существует собственная система, находящаяся на этапе утилизации (эксплуатации) и выполняющая основные функции обеспечения и помощи.
### Требования для разработки успешной системы
Разработка успешной системы требует
1. рассмотрения ее [[#Система как совокупность частей|структуры]]
2. рассмотрения ее [[#Система как функциональное место|функционального места]]
3. рассмотрения ее [[#Операционное окружение|операционного окружения]]
4. рассмотрения ее [[#Система как жизненный цикл|жизненного цикла]]
5. рассмотрения ее [[#Обеспечивающие системы|обеспечивающих систем]]
Аналогичные результаты были получены в ряде других исследований, и такой же вывод можно найти, к примеру, в [OMG Essence](https://www.omg.org/spec/Essence/1.2) (стандарт задающий общее ядро понятий для описания, сравнения и разработки ПО) или в [СМД-методологии](https://web.archive.org/web/20220409190315/https://ru.wikipedia.org/wiki/СМД-методология) (системомыследеятельностный подход по Г. П. Щедровицкому).
### Архитектура
При разработке компьютерных систем часто возникают [[#Проблема коммуникации при разработке компьютерных систем|проблемы коммуникации]]. Одной из основных причин возникновения таких проблем является взгляд на систему только с какой-то одной [[#Представление и точка зрения|точки зрения]]. Из-за этого становится важным навык видеть все вопросы и проблемы, возникающие при разработке. Как говорил [Барри Боэм](https://ru.wikipedia.org/wiki/Боэм,_Барри) (Barry W. Boehm): "Poor management can increase software costs more rapidly than any other factor. (*Плохой менеджмент может увеличить стоимость программного обеспечения намного стремительнее, чем любой другой фактор.*)". Именно поэтому при разработке очень важна архитектура.
>**Архитектура** - формальный способ зафиксировать множественность точек зрения на систему и передать ее другим.
#### Классическое определение архитектуры
>**Архитектура** - логическая и физическая структура компонентов системы и их взаимосвязи, сформированные всеми стратегическими и тактическими проектными решениями, применяемыми во время разработки. Логический взгляд на систему учитывает концепции, созданные в концептуальной модели, и устанавливает существование и роль ключевых абстракций и механизмов, которые будут определять архитектуру и общий дизайн системы. Физическая модель системы описывает конкретный программный и аппаратный состав реализации системы. Очевидно, что физическая модель зависит от конкретной технологии. --- [Гради Буч](https://ru.wikipedia.org/wiki/Буч,_Гради?ysclid=mu79qh6nbn865221362)
#### Определение архитектуры в системной инженерии
>**Architecture** - fundamental concepts of properties of a system in its environment embodied in its elements, relationships, and in the principles of its design and evolution. Architecture description - work product used to express an architecture. There is no single characterization of what is essencial or fundamental to a system; characterization could pertain to any or all of: system constituents or elements; how system elements are arranged or interrelated; principles of the system's organization or disign; and principles governing the evolution of the system over its life cycle. --- [ISO 42010](https://en.wikipedia.org/wiki/ISO/IEC_42010) (*Архитектура - это фундаментальные концепты свойств системы в ее окружении, заключенные в ее элементах, отношениях, а также принципах ее дизайна и эволюции. Описание архитектуры - это рабочий продукт, используемый для выражения архитектуры. Не существует единственного обозначения того, что важно и фундаментально для системы; но оно может иметь все или несколько из таких характеристик, как: элементы или части системы; способы взаимодействия элементов системы; принципы организации или дизайна системы; и принципы направления эволюцией системы на протяжении всего ее жизненного цикла.*)
#### Сущностные определения системы
>**Архитектура** - все важное. --- [Интернет](https://ru.wikipedia.org/wiki/Интернет)
>**Software architecture** is the set of design decisions which, if made incorrectly, may cause your project to be cancelled. --- [Eoin Woods](https://www.eoinwoods.info/) ([SEI 2010](https://book.northwind.ir/bookfiles/documenting-software-architectures-views-and-beyond/Documenting%20Software%20Architectures%20--Views_and_Beyond%202nd%20Ed.%20[2010].pdf)) (*Архитектура программного обеспечения - это набор проектных решений, которые, если они будут приняты неправильно, могут привести к отмене вашего проекта.*)
#### Проблема коммуникации при разработке компьютерных систем
При передаче информации или делегировании задач важные для успеха вопросы могут быть упущены. Существует несколько основных причин таких упущений, которые стоит учитывать.
1. Вопрос выходит за пределы компетенции разработчика.
2. Преобладает шаблонное проектирование.
3. Искусственное сужение проектных требований.
4. Расстановка приоритетов.
5. Подмена одной задачи другой.
Предположим, что у нас есть технический специалист, который поручил работу своему младшему коллеге с целью проработки и усвоения данной темы. Старший специалист отлично знает эту область, и он хочет, чтобы его коллега смог сам изучить все важные вопросы и нюансы, поэтому постарался подобрать наиболее оптимальную задачу, при этом подробно объяснив основы и способ ее выполнения. Несмотря на все трудности, младший коллега все таки выполнил задачу, но не учел некоторых важных вопросов, которые хоть и не были важны в этой конкретной задаче, но имеют не последнее значение в этой теме. Таким образом, коммуникация между двумя коллегами не удалась, ведь младший коллега не смог изучить все необходимые вопросы самостоятельно, несмотря на все указания старшего.
Но что, если задачу ставит человек, который не разбирается в этой теме, но компетентен в других? Предположим, что менеджер ставит задачу разработчику. При поручении задачи менеджер, опираясь на свои знания своей темы, постарался максимально подробно объяснить разработчику основные важные моменты. Но разработчик, обладая знаниями совершенно другой темы, решил задачу, опираясь на свои знания. В итоге, хоть он и учел все возможные проблемы, менеджер все равно не принял его работу, ведь она не соответствовала одному из самых важных вопросов, о котором разработчик даже не догадывался.
Эти два примера, особенно последний, показывают, насколько важно минимизировать проблемы коммуникации между людьми, и соответственно, насколько важна архитектура.
#### Причины важности архитектуры
##### Риск откладывания управления рисками
Очень важно не откладывать управление рисками. Как доказано на практике, чем позже дефект продукта будет исправлен, тем больше ресурсов на это будет необходимо потратить. При этом стоимость такого исправления будет расти экспоненциально.
##### V-диаграмма
Важным понятием при проектировании системы является V-модель, или V-диаграмма. Она показывает несколько этапов развития и внедрения проекта. Внизу данной V-диаграммы находится интеграция в систему. По левой ветке сверху вниз идет процесс определения и уточнения проекта: все начинается с концепции, продолжается требованиями и архитектурой, а позже уточняются детали. По правой ветке снизу вверх идет процесс создания проекта: начиная с внедрения, тестирования и проверки отдельных частей, продолжая тестированием с интегрированием в систему, заканчивая полным внедрением и использованием. При этом каждая правая часть проходит этап верификации: каждый этап проверяется на соответствие тому, что было заложено в соответствующей левой части. В конце, после того как проект стал полностью готов к внедрению, происходит валидация: проверка проекта на соответствие тому, что нам изначально было нужно и какую проблему мы стремились решить (то есть соответствие самому верхнему уровню правой части диаграммы самому верху левой части).
##### Эффект архитектурного проектирования
Проектирование системы с архитектурой может затруднить ее разработку на раннем этапе (ведь собрать все точки зрения и составить из них полноценную архитектуру, учитывающую все факторы происходящего, довольно дорого), но намного упростит ее на позднем этапе и решит большую часть проблем, которые могут возникнуть.
Наглядно это можно изобразить на графике зависимости затрат на проектирование от отставания проекта от плана. В данном случае, затраты - это функция отношения стоимости и качества архитектуры к стоимости всего проекта, а отставание - это функция отношения реального времени разработки проекта от запланированного. На таком графике видно, что, хоть в начале разброс и получается огромным, но ближе к концу он уменьшается, чаще всего достигая или перегоняя план, но самое главное устанавливая минимальный разброс от плана, что гарантирует стабильность развития проекта на позднем этапе.
@@ -1,36 +1,79 @@
## Содержание
- [Введение](#Введение)
- [Клиент-серверное взаимодействие. Протокол HTTP.](#Клиент-серверное%20взаимодействие.%20Протокол%20HTTP.)
- [HTTP](#HTTP)
- [Структура запроса HTTP](#Структура%20запроса%20HTTP)
- [Структура ответа HTTP](#Структура%20ответа%20HTTP)
- [Методы HTTP](#Методы%20HTTP)
- [Коды состояния](#Коды%20состояния)
- [Заголовки HTTP](#Заголовки%20HTTP)
- [URI, URN и URL](#URI,%20URN%20и%20URL)
- [REST](#REST)
- [[#Введение]]
- [[#Клиент-серверное взаимодействие. Протокол HTTP]]
- [[#HTTP]]
- [[#Структура запроса HTTP]]
- [[#Структура ответа HTTP]]
- [[#Методы HTTP]]
- [[#Коды состояния]]
- [[#Заголовки HTTP]]
- [[#Кеширование]]
- [[#URI, URN и URL]]
- [[#REST]]
- [[#Разметка страниц. Язык HTML]]
- [[#HTML]]
- [[#Структура HTML-документа]]
- [[#HTML-формы]]
- [[#HTML5]]
- [[#Браузер]]
- [[#Document Object Model]]
- [[#Стилизация страниц. Основы CSS]]
- [[#CSS]]
- [[#Источники CSS]]
- [[#Структура CSS]]
- [[#Приоритеты стилей]]
- [[#LESS & Sass / SCSS]]
- [[#LESS]]
- [[#Sass]]
- [[#SCSS]]
- [[#Динамические сценарии на стороне клиента. Язык JavaScript]]
- [[#JavaScript]]
- [[#Структура языка]]
- [[#ECMAScript]]
- [[#BOM]]
- [[#DOM]]
- [[#Отличия ES5 от ES6]]
- [[#Видимость переменных]]
- [[#Вставка переменной в строку]]
- [[#Деструктуризация]]
- [[#Классы и объекты]]
- [[#Наследование]]
- [[#Промисы]]
- [[#Стрелочные функции]]
- [[#Цикл с итератором]]
- [[#Параметры по умолчанию]]
- [[#Rest-параметр]]
- [[#Операция spread]]
- [[#Асинхронное клиент-серверное взаимодействие. DHTML и AJAX]]
- [[#AJAX]]
- [[#DHTML]]
- [[#XMLHttpRequest]]
- [[#jQuery]]
- [[#SuperAgent]]
- [[#Fetch API]]
- [[#WebSocket]]
## Введение
В основном все держится на 4 вещах:
- [Hypertext Transfer Protocol (HTTP)](#HTTP) - предназначен для передачи гипертекста между клиентом и сервером
- Hypertext Markup Language (HTML) - язык разметки гипертекста
- Cascade StyleSheets (CSS) - язык описания внешнего вида HTML-документов
- JavaScript - язык для написания динамических сценариев, выполняемых на стороне клиента
## Клиент-серверное взаимодействие. Протокол HTTP.
- [[#Клиент-серверное взаимодействие. Протокол HTTP|Hypertext Transfer Protocol (HTTP)]] - предназначен для передачи гипертекста между клиентом и сервером
- [[#Разметка страниц. Язык HTML|Hypertext Markup Language (HTML)]] - язык разметки гипертекста
- [[#Стилизация страниц. Основы CSS|Cascade StyleSheets (CSS)]] - язык описания внешнего вида HTML-документов
- [[#Динамические сценарии на стороне клиента. Язык JavaScript|JavaScript (JS)]] - язык для написания динамических сценариев, выполняемых на стороне клиента
## Клиент-серверное взаимодействие. Протокол HTTP
### HTTP
Сам по себе HTTP - протокол прикладного уровня. В его основе лежит технология "клиент-сервер". Он может использоваться в качестве "транспорта" для других протоколов прикладного уровня. Основной объект манипуляций этого протокола - ресурс, на который указывает [URI](#URI,%20URN%20и%20URL). Обмен сообщениями в этом протоколе идет по схеме "запрос-ответ". Сам по себе протокол не сохраняет состояние (stateless-протокол), поэтому для реализации сессий используется cookies.
Сам по себе HTTP - протокол прикладного уровня. В его основе лежит технология "клиент-сервер". Он может использоваться в качестве "транспорта" для других протоколов прикладного уровня. Основной объект манипуляций этого протокола - ресурс, на который указывает [[#URI, URN и URL|URI]]. Обмен сообщениями в этом протоколе идет по схеме "запрос-ответ". Сам по себе протокол не сохраняет состояние (stateless-протокол), поэтому для реализации сессий используется cookies.
#### Структура запроса HTTP
Первая строка запроса HTTP является стартовой, в ней содержится основное описание необходимого действия (команда). При ее составлении через пробел указывают три основные для протокола вещи: [метод](#Методы%20HTTP), [URI](#URI,%20URN%20и%20URL) и версию протокола HTTP (в формате HTTP/Версия).
Первая строка запроса HTTP является стартовой, в ней содержится основное описание необходимого действия (команда). При ее составлении через пробел указывают три основные для протокола вещи: [[#Методы HTTP|метод]], [[#URI, URN и URL|URI]] и версию протокола HTTP (в формате HTTP/Версия).
За первой строкой следует следующий раздел - заголовки. Этот раздел содержит, как ни странно, [заголовки](#Заголовки%20HTTP).
За первой строкой следует следующий раздел - заголовки. Этот раздел содержит, как ни странно, [[#Заголовки HTTP|заголовки]].
Последним разделом запроса является тело сообщения. Чаще всего в запросе тело остается пустым. В теле сообщения может находится что угодно, что необходимо передать с помощью протокола на другую сторону. Вся информация о теле, необходимая другой стороне, должна быть указана в заголовках.
Последним разделом запроса является тело (или сущность) сообщения. Чаще всего в запросе тело остается пустым. В теле сообщения может находится что угодно, что необходимо передать с помощью протокола на другую сторону. Вся информация о теле, необходимая другой стороне, должна быть указана в заголовках.
#### Структура ответа HTTP
Ответ мало чем отличается от [запроса](#Структура%20запроса%20HTTP). Единственным отличием является стартовая строка. Эта строка составляется тем же образом, что и стартовая строка запроса, перечисляя через пробел основную информацию запроса: версию протокола HTTP (в формате HTTP/Версия), [код состояния](#Код%20состояния) и пояснение кода состояния. По сути, в пояснении можно указать что угодно, но лучше придерживаться стандартных шаблонов сообщений.
Ответ мало чем отличается от [[#Структура запроса HTTP|запроса]]. Единственным отличием является стартовая строка. Эта строка составляется тем же образом, что и стартовая строка запроса, перечисляя через пробел основную информацию запроса: версию протокола HTTP (в формате HTTP/Версия), [[#Код состояния|код состояния]] и пояснение кода состояния. По сути, в пояснении можно указать что угодно, но лучше придерживаться стандартных шаблонов сообщений.
#### Методы HTTP
Методов HTTP довольно много, поэтому перечислим только используемые чаще всего.
@@ -38,7 +81,7 @@
- GET - получение данных с ресурса (запрос содержимого ресурса)
- HEAD - аналог GET, но в ответе не передается (отсутствует) тело
- POST - передача данных ресурсу
- PUT - загрузка содержимого запроса на указанный [URI](#URI,%20URN%20и%20URL)
- PUT - загрузка содержимого запроса на указанный [[#URI, URN и URL|URI]]
- DELETE - удаление данных с сервера
#### Коды состояния
@@ -57,11 +100,14 @@
- Request Headers - используется только в запросах клиента
- Response Headers - используется только в запросах сервера
- Entity Headers - сопровождают любую сущность сообщения
#### Кеширование
*Информация будет добавлена позже*.
### URI, URN и URL
> **URI (Uniform Resource Identifier)** - уникальный идентификатор ресурса - символьная строка, позволяющая идентифицировать ресурс. Строится по форме <схема>:<идентификатор>. Идентификатор определяется в зависимости от схемы.
> **URL (Uniform Resource Locator)** - URI, позволяющий определить местоположение ресурса. Может представлять из себя абсолютную ссылку (https://), относительную ссылку (../task.html) и тп.
> **URL (Uniform Resource Locator)** - URI, позволяющий определить местоположение ресурса. Может представлять из себя абсолютную ссылку (https://), относительную ссылку (../task.html) и тому подобное.
> **URN (Uniform Resource Name)** - URI, содержащий единообразное имя ресурса (не указывает на его местонахождение). Строится по способу, подобному URI, но начинается с urn, и только после него идет уже сама схема с идентификатором.
### REST
@@ -69,7 +115,262 @@
> **REST (Representation State Transformer)** - передача состояния представления - подход к архитектуре сетевых протоколов, обеспечивающих доступ к информационным ресурсам. Он описывает взаимодействие между клиентом и сервером, а также между front-end и back-end частями.
Основные концепции REST:
- данные должны передаваться в виде небольшого числа стандартных форматов (к примеру, HTML, XML, JSON)
- данные должны передаваться в виде небольшого числа стандартных форматов (к примеру, [[#HTML]], [XML](https://ru.wikipedia.org/wiki/XML?ysclid=mu8vgeju1m2168967), [JSON](https://ru.wikipedia.org/wiki/JSON?ysclid=mu8vgmwdcz833033530))
- сетевой протокол должен поддерживать кеширование, не должен зависеть от сетевого слоя, не должен сохранять информацию о состоянии между парами "запрос-ответ"
Существует подход, противоположный REST, который основан на вызове удаленных процедур (RPC - Remote Process Call).
## Разметка страниц. Язык HTML
### HTML
>**HyperText Markup Language** (HTML) - стандартный декларативный язык разметки документов в интернете.
В основном, он интерпретируется [[#Браузер|браузером]] и отображается в виде документа. Изначально, он являлся частным случаем SGML (стандартного обобщенного языка разметки).
Этот язык был разработан [Тимом Бернерсом-Ли](https://ru.wikipedia.org/wiki/Бернерс-Ли,_Тим?ysclid=mu8uowzcj46005688) в 1989-1991 годах.
#### Структура HTML-документа
В начале документа всегда находится строка с объявлением версии HTML. Хотя чаще всего для версии [[#HTML5]] это правило игнорируется, так как такой стандарт используется неявно.
По синтаксису он очень напоминает [XML](https://ru.wikipedia.org/wiki/XML?ysclid=mu8urdj3jm497572817), и даже существует отдельная нотация [XHTML](https://ru.wikipedia.org/wiki/XHTML?ysclid=mu8urty7p0976864513), которая является частным случаем этого языка. В частности, документ также состоит в основном из элементов, начало и конец которых обозначаются тегами.
Теги однозначно определяют закрытие или открытие того или иного элемента. Это позволяет вкладывать теги друг в друга, при этом сохраняя строгую структуру. В то же время теги могут быть одинарными (или пустыми) - то есть только открываться, не закрываясь. Также теги могут содержать атрибуты - специальные переменные, указанные в открывающем теге.
Начало и конец документа обозначаются специальными тегами. Внутри этих тегов должны находится два других тега, которые определяют заголовок и тело документа соответственно.
В заголовке чаще всего указывают, помимо самого заголовка, мета-информацию и различные необходимые для работы сайта файлы, к примеру файлы [[#Стилизация страниц. Основы CSS|css]] или js.
#### HTML-формы
Основное предназначение форм - обмен данными между пользователем и сервером. Приятной особенностью таких форм является то, что они могут работать без JavaScript.
Сам HTML-документ может содержать любое число форм, но на сервер одновременно может быть отправлена только одна. При этом вложенные формы запрещены.
Границы формы в документе, как и границы любого другого элемента, задаются тегами. В атрибутах формы можно отдельно указать [[#Методы HTTP|метод HTTP]] и ссылку, куда отправляется форма. При этом в конце формы обязательно должна находится отдельная кнопка для ее отправки.
#### HTML5
HTML5 - как следует из названия, пятая версия спецификации языка HTML. Стандарт такого языка был принят в 2014 году (в то время как HTML4 - в 1999 году).
Конечно, за это время в язык добавили много новых синтаксических особенностей. В первую очередь, изменения были внесены для более удобного управления мультимедийным содержимым страницы. Это включает в себя добавление таких тегов, как аудио и видео. Несмотря на то, что они сами по себе достаточно просты (в них присутствует всего три кнопки - старт, стоп и перемотка), они универсальны и работают на любом [[#Браузер|браузере]].
Помимо этого были добавлены новые семантические теги - замена для стандартных и универсальных. В основном они редко где используются, но иногда могут помочь в решении проблем доступности (к примеру, для слепых) или для более точного пояснения к содержимому тега.
Также была переработана структура тела HTML. С этой версии он больше не базируется на SGML, но все еще имеет обратную совместимость с HTML4. И формат вводной строки документа тоже изменился.
В дополнение к этому, были также переработаны формы. С этой версии в ней стали доступны новые поля ввода, новые атрибуты и еще куча других изменений.
### Браузер
>**Браузер** - программа, отображающая HTML-документ в его отформатированном виде.
На самом деле, о браузере можно говорить с разных точек зрения: можно определять его главным образом через отображение документов, а можно смотреть на него как на runtime для JavaScript.
Очень важно не путать браузер с поисковиком. Браузер занимается самой отрисовкой документов и чтении динамических сценариев и стилей. В то время как поисковик просто с помощью умных алгоритмов ищет информацию в интернете по заданному запросу. Как главное доказательство их различия, в настройках браузера можно поставить любой другой поисковик.
### Document Object Model
>**Объектная модель документа** (DOM) - это платформо-независимый интерфейс, позволяющий программам и скриптам получить доступ к содержимому HTML-документа.
В DOM документ представляется в виде дерева узлов. Узлы связаны между собой отношением "родитель-потомок".
Основным стандартом в данном случае является [W3C](https://www.w3.org/).
Чаще всего DOM используется для динамического изменения HTML-страницы и реализации на ней сложных сценариев поведения. Тем не менее, HTML без JavaScript статичен, а вся динамика возможна только при работе вместе с ним.
## Стилизация страниц. Основы CSS
### CSS
>**Cascade StyleSheets** (CSS) - декларативная технология описания внешнего вида документа, написанного языком разметки.
В основном CSS используется для задания цветов, шрифтов и других аспектов представления документа. Его основная цель - разделение содержимого документа и его представления. Это, к примеру, позволяет представлять один и тот же документ в различных методах вывода.
#### Источники CSS
Файлы со стилями CSS берутся браузером из разных источников.
Самым главным образом берутся авторские стили - информация стилей, предоставленная автором страницы. Она состоит из inline-стилей (при котором стиль элемента указывается напрямую в его атрибуте), встроенных стилей (блоки CSS прямо внутри самого документа), а также внешних таблиц стилей (отдельный файл .css).
Вторыми рассматриваются пользовательские стили. Они представляют собой локальный файл CSS, указанный пользователем в настройках браузера, который заменяет или переопределяет авторские стили.
Последним является стандартный стиль браузера. Если браузер не смог найти никаких стилей для описания страницы, он использует стиль по умолчанию, который чаще всего выглядит довольно плохо.
Очень неприятная подводная мина для разработчиков кроется в том, что, хоть пользовательские стили и рассматриваются менее приоритетно, чем авторские, существует специальное правило "important", которое позволяет пользователям игнорировать это правило для определенных элементов и обрабатывать элементы пользовательских стилей приоритетнее авторских.
#### Структура CSS
CSS представляет собой сложную таблицу стилей. Вся эта таблица состоит из различных наборов правил.
Каждое правило состоит из набора селекторов и блока определения. В данном случае селектор - это те теги, для которых будет активно данное правило. Блок определения состоят из разделенных между собой точкой с запятой пар "ключ: значение". В ключе задается свойство, которое нужно изменить, а в значении - значение, на которое меняется свойство.
#### Приоритеты стилей
Очень сложной темой является приоритеты использования стилей в случае, если к одному элементу подходит сразу несколько стилей. Для CSS существует сложная формула, учитывающая все особенности сравниваемых стилей, по которой он дает итоговый результат того, что показывать. Поэтому, не зная точных характеристик сравниваемых стилей, сложно определить точный итоговый результат.
Но можно выделить основные критерии приоритетов, которые чаще всего совпадают с результатом формулы.
1. Свойство задано при помощи !important.
2. Стиль прописан напрямую в теге.
3. Наличие идентификаторов (#id) в селекторе.
4. Количество классов (:classes) и псевдоклассов (:pseudoclass) в селекторе.
5. Количество имен тегов в селекторе.
Помимо вышеперечисленных приоритетов, также важен относительный порядок расположения свойства в файле: будет использоваться то свойство, что указанно ближе к концу файла.
### LESS & Sass / SCSS
Помимо самого CSS, существуют также надстройки над ним - некоторые дополняющие языки стилей. Они позволяют повысить уровень абстракции CSS-кода и упростить структуру таблиц стилей.
По сравнению с "обычным" CSS, они позволяют использовать переменные (константы и примеси), вложенные правила, а также математические операторы, благодаря которым нет необходимости выносить вычисления за пределы файла CSS. Также у них есть более мощные возможности по импорту и наследованию стилей.
Несмотря на то, что браузеры могут не поддерживать такие надстройки, существуют специальный транслятор, который преобразует эти правила в "обычный" и, соответственно, читаемый для браузера CSS.
#### LESS
По своему синтаксису, LESS почти ничем не отличается от CSS, а просто добавляет некоторые новые функции.
#### Sass
Синтаксис у Sass принципиально отличается от CSS и больше похож на таковой у языка программирования Ruby.
#### SCSS
SCSS иногда называют диалектом Sass, только с синтаксисом, похожим на CSS. При этом этот язык наиболее популярен среди надстроек для CSS.
## Динамические сценарии на стороне клиента. Язык JavaScript
### JavaScript
>**JavaScript** (JS) - объектно-ориентированный скриптовый императивный язык программирования.
Скриптовый в определении означает, что у этого языка нет точки входа.
В основном, JS используется для придания интерактивности веб-страницам. Именно он реализовывает динамические сценарии на стороне клиента. Добавить его на [[#HTML]]-страницу можно, используя три способа: в отдельном теге на странице, внутри используемого скриптом тега и в отдельном скриптовом файле. Предпочтительнее использовать последний способ, так как он наиболее удобен.
В данном языке инициализация скриптов происходит по порядку их объявления в файле. При этом объявить скрипт можно в любом месте [[#HTML]]-кода страницы. Важно учитывать, что если для исполнения скрипта необходим полностью построенный [[#Document Object Model|DOM]], то его следует подключать в конце тега body.
JS однопоточный, и по умолчанию он не поддерживает ни асинхронность, ни многопоточность, а работает только по принципу конкуренции процессов. По факту, все JS-скрипты выполняются в одном event-loop'е, который только один. Тем не менее, в некоторых местах кода можно использовать await, который позволяет другим более срочным процессам выполнится именно в этом месте кода, что позволяет реализовать структуру, похожую на асинхронность.
Сам по себе JS в [[#Браузер|браузере]] не имеет прямой работы с вводом-выводом, все данные передаются ему через API [[#Браузер|браузера]], на котором он работает.
Этому языку присущ ряд характерных архитектурных черт: динамическая или слабая типизация, автоматическое управление памятью, а также прототипное программирование (то есть одна функция наследует другую функцию-шаблон). Помимо этого, функции в этом языке являются объектами первого класса.
#### Структура языка
Полный язык можно поделить на три части. Первая из них - ядро, [[#ECMAScript]], язык в его чистом виде. Вторая часть языка состоит из [[#BOM|объектной модели браузера (Brawser Object Model, BOM)]]. Третий уровень языка представляет из себя [[#DOM|объектную модель документа (Document Object Model, DOM)]].
[[#BOM]] и [[#DOM]] существуют только внутри [[#Браузер|браузера]], в котором исполняется JS, поэтому для работы с языком вне [[#Браузер|браузера]] требуются специальные инструменты, которые могут их заменить (к примеру, Node.js).
##### ECMAScript
Сам по себе ECMAScript является отдельным языком программирования. Он встраиваемый, расширяемый, и при этом не имеющий средств ввода-вывода.
В данном языке, функция - это тоже объект. При этом, функцию можно задавать анонимно, можно объявить функцию в переменной, и можно объявить функцию с использованием объектной формы записи (хотя этот вариант уже не актуален и не используется).
С новой версией ES6, выпущенной в 2015 году, стали доступны [[#Отличия ES5 от ES6|новые возможности]]. В основном эти изменения коснулись синтаксиса языка.
Этот язык поддерживается практически всеми современными [[#Браузер|браузерами]], хотя для некоторых может потребоваться отдельная программа, называемая транспилером. Транспилеры могут понизить версию кода JS до версий более популярных браузеров. Самым распространенным транспилером на данный момент является Babel.
##### BOM
По факту, BOM - это прослойка между [[#ECMAScript|ядром]] и [[#DOM]]. Основное ее предназначение - управление окнами [[#Браузер|браузера]] и обеспечение их взаимодействия. Она представляет каждое окно [[#Браузер|браузера]] как отдельный объект. При этом она специфична для каждого браузера.
Данная модель предоставляет множество возможностей. Среди них управление фреймами, поддержка задержки в исполнении кода и зацикливания с задержкой, системные диалоги, управление адресом открытой страницы, управление информацией о [[#Браузер|браузере]], управление информацией о параметрах монитора, ограниченное управление историей просмотра страниц, поддержка работы с [[#HTTP]] cookie.
При использовании JS можно в любой части скрипта получить доступ к BOM, с использованием объекта window. С помощью этого объекта можно изменять окно пользователя и записывать функции в окно из любого места в коде.
##### DOM
Используя JS, с [[#Document Object Model|DOM]] можно производить такие манипуляции, как получение узлов, изменение узлов, изменение связей между узлами, удаление узлов. Для этого используется объект document.
DOM одинаков для каждого браузера.
### Отличия ES5 от ES6
Уже очень долгое время ES6 является стандартом написания [[#JavaScript|JS]] кода. И он актуален и по сей день, так как радикальных изменений после ES6 не производилось. Тем не менее, существует много старых систем, которые все еще работают с ES5. Именно поэтому важно знать их отличия и уметь работать с обеими версиями [[#ECMAScript|ES]].
#### Видимость переменных
В стандарте ES5 все переменные обозначались ключевым словом var, видимость которой распространялась на весь код программы - то есть в любом месте можно было эту переменную задать или перезаписать, и также в любом месте можно было эту переменную использовать.
Главная проблема этого подхода состоит в том, что переменные, объявленные внутри блока, автоматически перезаписывают таковые извне блока. То есть, при запуске блока, внутри которого переопределяется определенная переменная, старое значение данной переменной автоматически стирается, даже если сам блок эту переменную не меняет, а только инициализирует. При этом переопределенные переменные остаются таковыми и извне блока.
В то же время в большинстве других языков программирования область видимости переменной ограничивается блоком, в котором она была создана. Это позволяет удобно отслеживать переменные и дает много преимуществ и удобств по сравнению с неограниченной областью видимости.
Тем не менее, в ES5 существовали "костыли" для ограничения видимости переменной внутри блока - использование IIFE (Immediately Invoked Function Expression - выражение немедленно вызываемой функции). С таким подходом приходилось писать весь блок с необходимой переменной внутри функции, а после сразу вызывать эту функцию. В таком случае все действия внутри функции выполнялись, в то время как объявленная внутри нее переменная не выходила из блока во вешнюю область видимости. Но это все еще было неудобно.
Поэтому в версии ES6 были добавлены два новых ключевых слова - let и const - видимость которых стала ограничена как раз блоком. По своей сути, let работает почти так же, как и var, в то время как const стала честной константой let - константой с ограниченной блоком областью видимости, которая также в некоторой степени ограничивает манипуляции с содержащимся в ней объектом, в том числе и изменение свойств объекта.
Тем не менее, сам var все также существует в ES6, и его можно использовать теми же способами, как и в ES5, хотя это и не рекомендуется.
#### Вставка переменной в строку
С версии ES6 стало возможно вставлять переменные с помощью специальной конструкции ${}. Тем не менее, сделать это можно только в специальных строках, заключенных в обратные кавычки \`\`.
До этого же приходилось постоянно конкатенировать строки при их использовании.
#### Деструктуризация
>**Деструктуризация** - особый синтаксис присвоения, при котором можно присвоить массив или объект сразу нескольким переменным, разбив его на части.
В ES6 было добавлено много моделей деструктуризации с широким спектром применения.
#### Классы и объекты
Также в версии ES6 появился новый синтаксис описания и инициализации объектов. Модель описания объектов стала более схожей с таковой у других популярных языков программирования, к примеру добавилось ключевое слово class, а также функция конструктора constructor.
В то же время, чтобы реализовать такое на ES5, приходилось определять класс как переменную, отдельную функцию конструктора, а методы реализовывать через прототипное программирование.
#### Наследование
Помимо классов, также были добавлены и ключевые слова для наследования - extends и super. Это, при взаимодействии с классами, намного уменьшило количество необходимого кода для описания наследования, и в то же время увеличило его читаемость.
В то же время, для реализации того же самого в ES5 требовалось использовать прототипное программирование, много вызовов функции, а также бесполезных на первый взгляд усложнений.
Самое интересное, что на самом деле весь этот ужас из ES5 никуда не делся, просто его скрыли за красивыми ключевыми словами. "Под капотом" же ES6 работает точно так же.
#### Промисы
Вместо коллбэков стали использоваться промисы. Тем не менее, коллбэки все также используются и по сей день. Дословно промисы обещают выполнить функцию.
#### Стрелочные функции
Стрелочная функция - это, по факту, та же анонимная функция, только опуская ключевое слово function, а в некоторых ситуациях и даже само тело функции.
#### Цикл с итератором
В ES5 существовало только два вида for: обычный for, аналогичный таковому в java, а также forEach для элементов массива.
В новой же версии ES6 стал доступен цикл for с итератором массива, который последовательно перебирает все элементы массива. Но по факту, это все тот же forEach, но более мощный.
#### Параметры по умолчанию
В ES6 также добавились возможность устанавливать параметры для функции по умолчанию, которой в ES5 не было.
Раньше в ES5 приходилось использовать различные конструкции ветвления, чтобы понять, была ли передана нужная переменная в функцию, и если нет, установить ей нужное значение.
#### Rest-параметр
В ES6 появилась возможность устанавливать Rest-параметр. По факту, это массив параметров, количество которых по умолчанию не известно. В Java есть аналог такой конструкции - vararg. Названия переменных при таком подходе наследуются.
#### Операция spread
В ES6 появилась новая операция spread, обозначающаяся как [[#Rest-параметр]], которая может передать в функцию не сам массив данных, а разложить его на различные переменные и передать их отдельно друг от друга, как разные переменные.
## Асинхронное клиент-серверное взаимодействие. DHTML и AJAX
### AJAX
>**AJAX** (Asynchronous Javascript And XML) - это подход к построению интерактивных пользовательских интерфейсов веб-приложений.
Несмотря на то, что в абревиатуре используется XML, на сегодняшний день он уже не так важен, вместо него используется любой структурированный текст. Поэтому на сегодняшний день AJAX - это просто асинхронный способ выполнения [[#JavaScript|JS]].
Сам по себе, AJAX основан на фоновом обмене данными между [[#Браузер|браузером]] и веб-сервером. При таком подходе во время обмена данными страница не перезагружается полностью, а лишь меняется в соответствии с поступающими данными.
Основными принципами AJAX являются совместное использование технологии динамического обращения к серверу без перезагрузки полностью всей веб-страницы (с использованием, к примеру, [[#XMLHttpRequest]], динамического создания дочерних фреймов, или динамического создания тега, содержащего [[#JavaScript|JS]]) и [[#DHTML]] для динамического изменения содержания страницы.
Преимуществами такого подхода являются экономия трафика (ведь нет необходимости при изменении страницы отправлять копию страницы с изменениями, можно обойтись только самими изменениями), уменьшение нагрузки на сервер (по той же причине) и ускорение реакции интерфейса (и снова по той же причине).
Недостатками такого подхода являются отсутствие возможности поисковикам найти динамически загружаемое содержимое страницы, сложность собирания метрик по использованию сайта, усложнение проекта и требование браузеру включить [[#JavaScript]] (во всех современных браузерах эта функция включена по умолчанию).
#### DHTML
>**Dynamic HTML** - способ создания интерактивного веб-сайта, использующий сочетание статического языка разметки [[#HTML]], выполняемого на стороне клиента скриптового языка [[#JavaScript]], [[#CSS]] (каскадных таблиц стилей) и [[#Document Object Model|DOM]] (объектной модели документа).
#### XMLHttpRequest
>**XMLHTTP** (или XMLHttpRequest, или XHR) - это набор API, позволяющий осуществлять [[#Структура запроса HTTP|HTTP-запросы]] к серверу без необходимости перезагружать страницу.
При данном подходе данные можно пересылать как в виде XML, так и в виде JSON, [[#HTML]] или даже просто неструктурированным текстом. При пересылке чаще всего используется текстовый протокол [[#HTTP]], и поэтому данные должны передаваться в виде текста, не важно какого вида.
#### jQuery
>**jQuery** - это [[#JavaScript|JS]]-библиотека, предназначенная для разработки [[#DHTML]] и AJAX-приложений.
Использование библиотеки jQuery упрощает доступ к элементам [[#Document Object Model|DOM]] с помощью множества различных способов. Помимо этого, данная библиотека упрощает и унифицирует (для различных браузеров) реализацию AJAX, а также упрощает добавление визуальных эффектов.
#### SuperAgent
>**SuperAgent** - это API для реализации AJAX.
#### Fetch API
>**Fetch API** - это современный стандарт, призванный заменить [[#XMLHttpRequest]] для реализации AJAX.
На данный момент, поддерживается всеми современными [[#Браузер|браузерами]]. Тем не менее, на старых [[#Браузер|браузерах]] он не поддерживается, а значит и работать не будет.
В отличие от [[#XMLHttpRequest]], Fetch API работает на [[#Промисы|промисах]], при этом поддерживает кросс-браузерность, а также сам по себе он уже встроен в [[#браузер]] (что означает, что нет необходимости подключать что-либо отдельно, в том числе не нужны [[#jQuery]] и [[#SuperAgent]]).
### WebSocket
>**WebSocket** - это протокол полнодуплексной связи поверх TCP-соединения, предназначенный для обмена сообщениями между [[#Браузер|браузером]] и веб-сервером в режиме реального времени.
Данный протокол позволяет отправлять данные [[#Браузер|браузеру]] без дополнительного запроса со стороны клиента. При этом обмен такими данными ведется через отдельное TCP-соединение. На данный момент, этот протокол поддерживается всеми современными браузерами.
Основное преимущество WebSocket заключается в том, что с ним данные могут отправляться как с клиента на сервер, так и наоборот. Большинство других реализаций того же самого могут отправлять данные только от клиента только на сервер.
Данный протокол является альтернативой для совместного подхода [[#AJAX]] и Long Polling'а. В данном случае Polling - это постоянные запросы от клиента для проверки состояния сервера. Long Polling означает, что опрос клиентом производится не слишком часто, а с определенным интервалом времени. В то же время существует Short Polling, который делает запросы очень часто.
@@ -1,9 +1,9 @@
## Содержание
- [Случайные события. Статическая вероятность](#Случайные%20события.%20Статическая%20вероятность)
- [События](#События)
- [Вероятность](#Вероятность)
- [Дискретное вероятностное пространство](#Дискретное%20вероятностное%20пространство)
- [Геометрические вероятности](#Геометрические%20вероятности)
- [[#Случайные события. Статическая вероятность]]
- [[#События]]
- [[#Вероятность]]
- [[#Дискретное вероятностное пространство]]
- [[#Геометрические вероятности]]
## Случайные события. Статическая вероятность
@@ -58,11 +58,13 @@
>**Измеримое пространство** - пара ($\Omega, \mathcal{F}$), состоящая из множества $\Omega$ и $\sigma$-алгебры $\mathcal{F}$ подмножеств $\Omega$.
## Вероятность
Далее будем использовать такие понятия, как [[#События|алгебра]], [[#События|событие]], [[#События|измеримое пространство]].
>**Мера** - числовая функция $\mu : \mathcal{F} \to \mathbb{R}$, заданная на $\sigma$-алгебре событий $\mathcal{F}$, удовлетворяющая условиям
> 1) $\mu(\varnothing) = 0$
> 2) $\mu(\bigcup\limits_{i=1}^{\infty} A_i) = \sum\limits_{i=1}^{\infty} \mu(A_i)$, где $\{A_i\} \subset \mathcal{F} : \forall i,j \ (i \neq j) \ A_i \bigcap A_j = \varnothing$
>**Вероятность** (на измеримом пространстве ($\Omega, \mathcal{F}$)) - мера $\text{P} : \mathcal{F} \to [0;1]$, удовлетворяющих условиям
>**Вероятность** (на измеримом пространстве ($\Omega, \mathcal{F}$)) - мера $\text{P} : \mathcal{F} \to [0,1]$, удовлетворяющих условиям
> P1) $\text{P}(A) \geq 0 \quad \forall A \in \mathcal{F}$
> P2) $\text{P}(\Omega) = 1$
> P3) $\text{P}(A+B) = \text{P}(A) + \text{P}(B) \quad \forall A, B \in \mathcal{F}, \ AB = \varnothing$
@@ -99,6 +101,8 @@
>***Теорема***. Система аксиом P1), P2), P3), P4) равносильна системе аксиом P1), P2), P3*).
## Дискретное вероятностное пространство
Далее будем использовать такие понятия, как [[#Вероятность|вероятностное пространство]].
>**Конечное вероятностное пространство** - вероятностное пространство, построенное следующим образом. Пусть $\Omega$ = $\{ \omega_1, \dots, \omega_n \}$ - конечное множество, $\mathcal{A}$ - алгебра всех подмножеств множества $\Omega$ (так как $\Omega$ конечно, то $\mathcal{A}$ - $\sigma$-алгебра), $p_i, i = 1, \dots, n$ - положительные числа, $\sum\limits_{i=1}^{n} p_i = 1$. Для события A положим $\text{P}(A) = \sum\limits_{\omega_i \in A} p_i = 1$. Для невозможного события положим $\text{P}(\varnothing) = 0$. Нетрудно доказать, что данное вероятностное пространство удовлетворяет всем аксиомам.
>**Классическое определение вероятности** - частный случай определения вероятности, при котором все $p_i$ равны между собой. Тогда в данном случае считается, что $p_i = \frac{1}{| \Omega |}$ и $\text{P}(A) = \frac{| A |}{| \Omega |}$.
@@ -108,6 +112,8 @@
>**Дискретное вероятностное пространство** - обобщение конечного вероятностного пространства, построенное следующим образом. Пусть $\Omega = \{ \omega_1, \omega_2, \dots \}$ - счетное множество, $\mathcal{F}$ - набор всех подмножеств $\Omega$, $p_n, n = 1, 2, \dots,$ - неотрицательные числа, такие что выполнено условие $\sum\limits_{n=1}^{\infty} p_n = 1$. $\forall A \in \mathcal{F}$ положим $\text{P}(A) = \sum\limits_{n \in \{ n: \omega_n \in A \}}^{\infty} p_n$. Нетрудно доказать, что вероятностное пространство $(\Omega, \mathcal{F}, \text{P})$ удовлетворяет всем аксиомам.
## Геометрические вероятности
Далее будем использовать такие понятия, как [[#Вероятность|вероятностное пространство]].
>Определим вероятностное пространство, служащее моделью задач, в которых частица случайном образом бросается в какую-либо область евклидова n-мерного пространства с конечным n-мерным объемом (предполагается, что вероятность попасть частице в какую-либо область этого пространства пропорциональна n-мерному объему этой области). Пусть $\Omega$ - область евклидова n-мерного пространства с конечным n-мерным объемом, а событие - подмножество $\Omega$, для которых можно определить n-мерный объем, множество событий - $\sigma$-алгебра $\mathcal{B}$ борелевских подмножеств $\Omega$. Положим вероятность события A $\text{P}(A) = \frac{|A|}{|\Omega|}$, где $|A|, |\Omega|$ - n-мерный объем соответствующего множества. Нетрудно понять, что вероятностное пространство ($\Omega, \mathcal{B}, \text{P}$) удовлетворяет всем аксиомам.
>**Абсолютно непрерывное вероятностное пространство** - обобщение вероятностного пространства, построенного выше. Пусть $\Omega = \{ (x_1, x_2, \dots, x_n) \}$ - n-мерное вещественное евклидово пространство, $\pi(x_1, x_2, \dots, x_n)$ - неотрицательная функция, интегрируемая по Риману по любой квадрируемой области из $\Omega$. Будем предполагать, что существует несобственный интеграл по $\Omega$ от функции $\pi(x_1, x_2, \dots, x_n)$ и $\int\limits_{\Omega} \pi(x_1, x_2, \dots, x_n) dx_1 \dots dx_n = 1$. Обозначим через $\mathcal{F}$ алгебру, порожденную квадрируемыми областями в $\Omega$. Положим $\forall A \in \mathcal{F} \quad \text{P}(A) = \int\limits_{A} \pi(x_1, x_2, \dots, x_n) dx_1 \dots dx_n$. Нетрудно понять, что вероятностное пространство ($\Omega, \mathcal{F}, \text{P}$) удовлетворяет всем аксиомам.
@@ -1,4 +1,5 @@
### Лекции
- [Вероятностное пространство](Вероятностное%20пространство.md)
- [Условные вероятности. Независимость](Условные%20вероятности.%20Независимость.md)
- (и тд)
@@ -0,0 +1,53 @@
## Содержание
- [[#Условные вероятности]]
- [[#Формула полной вероятности]]
- [[#Формула Байеса]]
- [[#Независимость событий]]
## Условные вероятности
Далее будем использовать такие понятия, как [событие](Вероятностное%20пространство.md#События), [вероятность](Вероятностное%20пространство.md#Вероятность) ($P$).
>**Условной вероятностью** $P(A|B)$ события $A$ при условии, что произошло событие $B$, называется величина $P(A|B) = \frac{P(AB)}{P(B)}$ (где $A, B$ - некоторые события, причем $P(B) > 0$). Для условной вероятности также применяется обозначение $P_B(A)$.
>**Теоремой умножения** называется равенство $P(AB) = P(B)P(A|B)$, которое следует из определения условной вероятности.
>***Теорема***. (Теорема умножения). Пусть события $A_1, \dots, A_n$ таковы, что $P(A_1, \dots, A_{n-1}) > 0$. Тогда $P(A_1 \dots A_n) = P(A_1)P(A_2|A_1)P(A_3|A_1A_2) \dots P(A_n|A_1 \dots A_{n-1})$.
>
>*Доказательство*. Из условия теоремы следует существование всех условных вероятностей в требуемом уравнении. Индукция по $n$. При $n = 2$ требуемое равенство вытекает из определения теоремы умножения. Пусть требуемое равенство справедливо для $n = k$. Докажем его при $n = k + 1$. Подставляя в определение теоремы умножения $B = A_1 \dots A_k$ и $A = A_{k+1}$, получим $P(A_1 \dots A_k A_{k+1}) = P(A_1 \dots A_k) P(A_{k+1}|A_1 \dots A_k)$. Из этого равенства и индукционного предположения следует требуемое равенство при $n = k+1$. $\blacksquare$
## Формула полной вероятности
Далее будем использовать такие понятия, как [пространство элементарных событий](Вероятностное%20пространство.md#События) ($\Omega$), [событие](Вероятностное%20пространство.md#События), [вероятность](Вероятностное%20пространство.md#Вероятность) ($P$), [[#Условные вероятности|условная вероятность]], [[#Условные вероятности|теорема умножения]].
>**Конечное разбиение** (пространства $\Omega$) - система событий $B_1, B_2, \dots, B_n$ таких, что они попарно несовместимы и $B_1 + B_2 + \dots + B_n = \Omega$.
>***Теорема***. (Формула полной вероятности). Пусть события $B_1, \dots, B_n$ образуют конечное разбиение $\Omega$ и все $P(B_k) > 0$. Тогда для любого события $A$ справедлива формула $P(A) = \sum\limits_{k=1}^{n} P(B_k) P(A|B_k)$, называемая формулой полной вероятности.
>
>*Доказательство*. Из определения конечного разбиения следует разложения $A$ на сумму $A = A \Omega = AB_1 + AB_2 + \dots + AB_n$. События $AB_1, AB_2, \dots, AB_n$ попарно независимы. Из конечной аддитивности $P$ и теоремы умножения получаем $P(A) = \sum\limits_{k=1}^{n} P(AB_k) = \sum\limits_{k=1}^{n} P(B_k) P(A|B_k)$. $\blacksquare$
## Формула Байеса
Далее будем использовать такие понятия, как [событие](Вероятностное%20пространство.md#События), [[#Формула полной вероятности|конечное разбиение]], [вероятность](Вероятностное%20пространство.md#Вероятность) ($P$), [[#Условные вероятности|условная вероятность]], [[#Условные вероятности|теорема умножения]], [[#Формула полной вероятности|формула полной вероятности]].
>***Теорема***. Пусть события $B_1, \dots, B_n$ образуют конечное разбиение $\Omega$ и все $P(B_k) > 0$. Тогда для любого события $A, P(A) > 0$, справедливы формулы $P(B_k|A) = \frac{P(B_k) P(A|B_k)}{\sum\limits_{i=1}^{n} P(B_i) P(A|B_i)}, k = 1, \dots , n$, называемые формулами Байеса.
>
>*Доказательство*. По теореме умножения $P(AB_k) = P(B_k) P(A|B_k) = P(A) P(B_k|A)$. Следовательно $P(B_k|A) = \frac{P(B_k)P(A|B_k)}{P(A)}$. Применяя к знаменателю $P(A)$ формулу полной вероятности, получаем требуемое равенство. $\blacksquare$
## Независимость событий
Далее будем использовать такие понятия, как [событие](Вероятностное%20пространство.md#События), [вероятность](Вероятностное%20пространство.md#Вероятность) ($P$), [[#Условные вероятности|условная вероятность]].
>**Независимые события** ($A, B$) - события, для которых верно $P(AB) = P(A)P(B)$.
>**Зависимые события** ($A, B$) - события, которые не независимые.
Пусть $A, B$ - независимые события.
1. $P(B) > 0, \ P(AB) = P(A)P(B), \ P(A|B) = \frac{P(AB)}{P(B)} \implies P(A|B) = P(A)$.
2. $P(\overline{A}B) = P(B - AB) = P(B)(1-P(A)) = P(\overline{A}) P(B) \implies \overline{A}, B$ - независимые события.
3. $B_1B_2 = \varnothing, \ P(A(B_1 + B_2)) = P(AB_1 + AB_2) = P(AB_1) + P(AB_2) = P(A)(P(B_1) + P(B_2)) = P(A)P(B_1 + B_2) \implies A, B_1 + B_2$ - независимые события.
Таким образом, получаем 3 свойства независимых событий.
1. $P(A|B) = P(A)$
2. $\overline{A}, B$ - независимы
3. $A, B_1 + B_2$ - независимы
Можно обобщить определение независимости для нескольких событий.
>**Независимые события** ($A_1, A_2, \dots, A_n$) - события, для которых верно $\forall \ 1 \leq i_1 < i_2 < \dots < i_m \leq n, \ 2 \leq m \leq n, \ P(A_{i_1} A_{i_2} \dots A_{i_m}) = P(A_{i_1}) P(A_{i_2}) \dots P(A_{i_m})$. В противном случае события зависимы. Независимость нескольких событий называется иногда независимостью событий в совокупности.
@@ -0,0 +1,232 @@
## Содержание
- [[#Механическое движение]]
- [[#Тело]]
- [[#Материальная точка]]
- [[#Абсолютно твердое тело]]
- [[#Траектория]]
- [[#Относительность формы траектории]]
- [[#Радиус кривизны траектории]]
- [[#Скорость]]
- [[#Линейная путевая скорость]]
- [[#Скорость как векторная величина]]
- [[#Вращательное движение. Угловая скорость]]
- [[#Ускорение]]
- [[#Система отсчета]]
- [[#Система координат]]
- [[#Декартова система координат]]
- [[#Полярная СК]]
- [[#Цилиндрическая СК]]
- [[#Сферическая СК]]
- [[#Вектор]]
- [[#Представление вектора в проекциях на оси декартовой системы координат]]
- [[#Скалярное произведение векторов]]
- [[#Модуль вектора]]
- [[#Векторное произведение векторов]]
- [[#Двойное векторное произведение]]
- [[#Радиус-вектор]]
## Механическое движение
>**Движение** (в общем) - всякое изменение.
>**Механическое движение** - изменение положения тел или их частей относительно друг друга в пространстве с течением времени.
Механическое движение является простейшей формой движения.
### Тело
>**Тело** - материальный объект, имеющий массу, форму и объем и отделенный от других тел внешней границей.
#### Материальная точка
>**Материальная точка** - тело, размерами которого можно пренебречь по сравнению с расстояниями до других тел в данной задаче.
К примеру, в задаче рассмотрения земной орбиты, радиус которой составляет около 150 миллионов километров, Землю можно считать материальной точкой, ведь ее размер всего 12 742 километра, что более чем в 10 тысяч раз меньше.
Другим примером может служить задача рассмотрения клетки живого организма, средний размер которой от $10^{-5}$ до $10^{-6}$ метров. В такой задаче материальной точкой может служить вирус, средний размер которого $10^{-8}$ метра, что примерно в 100 раз меньше.
#### Абсолютно твердое тело
> **Абсолютно твердое тело** (АТТ) - тело, деформациями которого можно пренебречь в условиях данной задачи.
Очевидно, пренебречь деформацией можно не во всех задачах. Взять, например, теннисный мяч. Если его с большой силой бросить в землю, то во время приземления он достаточно сильно деформируется, и его нельзя будет считать абсолютно твердым телом.
### Траектория
>**Траектория** - линия, по которой механически движется тело.
#### Относительность формы траектории
Очевидно, что при изменении системы отсчета меняется и траектория, с которой движется тело.
Самым простым и легко проверяемым примером такой относительности является взаимное движение Земли и Луны относительно друг друга. Если смотреть на Луну с Земли (то есть следить за траекторией Луны в системы отсчета, связанной с центром Земли), то ее траектория будет примерно напоминать окружность (естественно, ведь для земного наблюдателя она движется вокруг Земли). Но если же смотреть наоборот, с Луны на Землю (то есть следить за траекторией Земли в системе отсчета, связанной с центром Луны), то Земля будет оставаться практически неподвижной (на самом деле, она будет слегка смещаться со своего места, но незначительно). Как следствие данного явления, Луну можно разделить на две стороны: светлая (постоянно направленная на Землю) и темная (постоянно направленная от Земли).
#### Радиус кривизны траектории
Любую кривую траекторию можно представить как чередование прямолинейного движения и движения по сегменту окружности, только в каждой точке эта окружность будет обладать разными радиусами.
Конечно, в любой точке такой траектории этот радиус можно посчитать. Основная формула радиуса кривизны траектории $R = \frac{ds}{d \varphi}$. Но учитывая, что обе эти величины сильно малы, для расчетов такая формула не подойдет. Поэтому существуют другие способы.
Предположим, известна зависимости координаты $y$ от координаты $x$. Тогда можно сказать, что $\tan \varphi = \frac{dy}{dx} = y'$. Далее
$$
ds = \sqrt{(dx)^2 + (dy)^2} = dx \sqrt{1 + \left( \frac{dy}{dx} \right)^2} = dx \sqrt{1 + (y')^2}
$$
Изменение угла можно выразить через вторую производную, а значит получаем
$$
y'' = \frac{d}{dx}(\tan \varphi) = \frac{1}{\cos^2 \varphi} \frac{d \varphi}{dx} = (1 + \tan^2 \varphi) \frac{d \varphi}{dx}
$$
откуда можно выразить $d \varphi = \frac{y'' dx}{1 + (y')^2}$. Иными словами, получаем
$$
R = \frac{\left(1 + (y')^2 \right)^{\frac{3}{2}}}{y''}
$$
Но а если такая зависимость неизвестна? Тогда можно найти радиус кривизны траектории, зная зависимость обеих координат от времени. Для этого нужно рассмотреть
$$
k = \frac{1}{R} = \frac{d \varphi}{ds} = \frac{d \varphi}{ds} \cdot \frac{dt}{dt} = \frac{\frac{d \varphi}{dt}}{\frac{ds}{dt}}
$$
Для знаменателя легко получить
$$
\frac{ds}{dt} = v = \sqrt{v_x^2 + v_y^2} = \sqrt{\dot{x}^2 + \dot{y}^2}
$$
Зная, что $\tan \varphi = \frac{v_y}{v_x} = \frac{\dot{y}}{\dot{x}}$, а значит и $\varphi = \arctan \left( \frac{\dot{y}}{\dot{x}} \right)$, можно также легко получить и числитель
$$
\frac{d \varphi}{dt} =
\frac{d}{dt} \left( \arctan \left( \frac{\dot{y}}{\dot{x}} \right) \right) =
\frac{1}{1 + \left( \frac{\dot{y}}{\dot{x}} \right)^2} \cdot \left( \frac{\ddot{y} \dot{x} - \dot{y} \ddot{x}}{\dot{x}^2} \right) =
\frac{\ddot{y} \dot{x} - \dot{y} \ddot{x}}{\dot{x}^2 + \dot{y}^2}
$$
Тогда легко можно получит как $k = \frac{\ddot{y} \dot{x} - \dot{y} \ddot{x}}{\left( \dot{x}^2 + \dot{y}^2 \right)^{\frac{3}{2}}}$, так и финальный ответ
$$
R = \frac{\left( \dot{x}^2 + \dot{y}^2 \right)^{\frac{3}{2}}}{\ddot{y} \dot{x} - \dot{y} \ddot{x}}
$$
### Скорость
#### Линейная путевая скорость
>**Линейная путевая скорость** - скалярная величина, показывающая, какой путь тело проходит за единицу времени.
Средняя скорость тела на определенном участке пути, пройденном за определенное время, считается по формуле $\langle v \rangle = \frac{dS}{dt}$. Мгновенную скорость (то есть скорость в определенный момент времени) можно найти по формуле $v = \lim\limits_{\delta t \to 0} \frac{\delta S}{\delta t}$. Если тело движется равномерно, то его средняя и мгновенная скорости равны.
#### Скорость как векторная величина
Конечно, скорость можно изобразить и как векторную величину. Но только тогда это будет вектор скорости, а не сама скорость. Этот вектор можно найти по формуле $\vec{v} = \lim\limits_{\Delta t \to 0} \frac{\Delta \vec{r}}{\Delta t} = \frac{d \vec{r}}{dt}$, где $\vec{r}$ - вектор движения тела с началом в точке начала движения и с концом в точке конца движения. То есть, формула вектора скорости очень сложа с формулой мгновенной скорости, с изменением лишь пройденного расстояния на вектор перемещения тела.
Вектор скорости в трехмерном пространстве можно также, как и любой другой вектор, разложить на компоненты. Тогда получится $v_x = \frac{dx}{dt}$, $v_y = \frac{dy}{dt}$ и $v_z = \frac{dz}{dt}$.
Помимо этого, скорость можно также рассматривать как $\vec{v} = | v | \cdot \vec{\tau}$ (то есть модуль скорости на ее орт, что есть $\hat{e}_v = \frac{\vec{v}}{|v|} = \vec{\tau}$).
#### Вращательное движение. Угловая скорость
При движении по окружности, помимо обычной скорости, у тела появляется угловая скорость.
>**Угловая скорость** - векторная величина, направленная вдоль оси вращения тела, которая показывает изменение угла тела относительно оси вращения за единицу времени.
Из определения следует, что для нахождения мгновенной угловой скорости можно использовать формулу $\omega = \frac{d \varphi}{dt}$. Направление угловой скорости можно определить, использовав правило правой руки: если загнуть пальцы вдоль траектории движения тела, то большой палец покажет направление угловой скорости.
Помимо этого, с помощью угловой скорости можно найти и мгновенную линейную скорость по формуле $\vec{v} = \vec{\omega} \times \vec{r}$, где $\vec{r}$ - радиус-вектор. При этом эта формула будет верна при любом расположении центра системы координат на оси вращения тела.
### Ускорение
>**Ускорение** - скалярная величина, характеризующая изменение скорости тела относительно времени.
Как и у скорости, у ускорения может быть вектор. $\vec{a} = \lim\limits_{\Delta t \to 0} \frac{\Delta \vec{v}}{\Delta t} = \frac{d \vec{v}}{dt} = \frac{d^2 \vec{r}}{dt^2}$.
Также можно рассмотреть скорость как $\vec{v} = | v | \cdot \vec{\tau}$, тогда ускорение получается $\vec{a} = \frac{d \vec{v}}{dt} = \frac{d(v \vec{\tau})}{dt} = \frac{dv}{dt} \vec{\tau} + v \frac{d \vec{\tau}}{dt}$. Первое слагаемое в полученной сумме называют **тангенциальным ускорением** (то есть $\vec{a}_\tau = \frac{d |\vec{v}|}{dt} \cdot \vec{\tau}$ при $\vec{\tau} = const$).
При равномерном движении по окружности у тела возникает вектор $\vec{n}$, направленный постоянно от тела к оси вращения и меняющий свое направление со временем и изменением угла, по модулю при этом оставаясь неизменным.
Тогда рассмотрим второе слагаемое предыдущей суммы как $v \frac{d \vec{\tau}}{dt} = v \lim\limits_{\Delta t \to 0} \frac{\Delta \vec{\tau}}{\Delta t} = v \lim\limits_{\Delta t \to 0} \frac{\vec{n} \Delta \varphi}{\Delta t} = v \cdot \omega \vec{n} = \frac{v^2}{R} \vec{n}$. Такое ускорение $\vec{a}_n = \frac{v^2}{R} \cdot \vec{n}$ называется **нормальным** (или **центростремительным**). Иначе (без вектора) его можно записать как $a_n = \frac{v^2}{R} = v\omega = \omega^2 R$.
## Система отсчета
>**Система отсчета** - совокупность тела отсчета (к которому привязана система координат), единой меры расстояния и времени.
### Система координат
> **Система координат** - способ определения положения и перемещения тела в пространстве с помощью чисел.
#### Декартова система координат
Самой распространенной системой отсчета является декартова система координат. Иногда ее также называют прямоугольной. Она может иметь любую размерность, но дальше будет рассматриваться только трехмерный случай.
Состоит она из трех прямых - оси абсцисс (OX), оси ординат (OY) и оси аппликат (OZ). Прямоугольной она называется потому, что эти три прямые ортогональны (то есть имеют между собой угол 90$\degree$).
Направление этих прямых определяется двумя способами: правилом правой руки или правило левой руки. В обоих случаях большой палец определяет направление абсциссы, а указательный - ординаты. Разница существует в направлении аппликаты - оно определяется средним пальцем. Приоритетнее строить систему координат по правилу правой руки, но в зависимости от задачи направление осей можно менять.
В такой системе координат точка определяется тремя числами - значениями отклонения данной точки от центра оси координат в зависимости от осей и их направления. Иными словами. любую точку в пространстве можно представить в виде трех чисел - значений оси абсцисс, оси ординат и оси аппликат в этой точке.
#### Полярная СК
Другой распространенной системой координат является двумерная полярная система координат.
Она состоит из двух компонентов: радиуса до точки (расстояния, на котором находится точка от начала координат) и угла между прямой, на которой лежат точка и начало координат, и лучом, обозначающим нулевой угол. Чаще всего, угол в такой системе координат считается в радианах.
>**Радиан** (рад/rad, от лат. radius - луч, радиус) - угол, соответствующий дуге, длина которой равна ее радиусу.
На самом деле, такую систему координат очень легко превратить в двумерную декартову. Для этого достаточно представить представить ось абсцисс как $x = r \cos \varphi$ и ось ординат как $y = r \sin \varphi$. Таким образом, каждой паре радиуса и угла можно однозначно сопоставить пару абсциссы и ординаты. При таком представлении лучом с нулевым углом станет положительное направление оси абсцисс.
#### Цилиндрическая СК
Одним из способов расширить полярную систему координат до трехмерного случая является цилиндрическая система координат.
В случае такой системы координат два основных направления совпадают с полярной системой координат (то есть, угол и радиус). Третья ось добавляется перпендикулярно плоскости третьей плоскости и в точности соответствует оси аппликат из декартовой системы координат.
Соответственно, при представлении этой системы координат в виде декартовой, две оси из полярной системы координат (угол и радиус) переводятся точно также, как и в этой системе. Последняя ось остается без изменения.
#### Сферическая СК
Другим случаем расширения полярной системы координат до трехмерного случая является сферическая система координат.
Принцип построения данной системы координат почти полностью совпадает с таковым у полярной системы координат. Соответственно, радиус и угол из полярной системы координат остается без изменения. В то же время строится луч, ортогональный двумерной полярной системе координат, и он становится нулевым лучом нового угла. Таким образом, в систему добавляется новый угол с другим начальным лучом, что позволяет любую точку пространства представить в виде трех компонентов: радиуса и двух углов.
Эту систему тоже можно превратить в декартову систему координат. Только для этого придется использовать два угла, а не один. В таком случае получаем, что $x = r \sin \theta \cos \varphi$, $y = r \sin \theta \cos \varphi$, а $z = r \cos \theta$ (где $\theta$ - новый угол).
### Вектор
>**Вектор** - величина, характеризующая перемещение из одной точки пространства в другую.
#### Представление вектора в проекциях на оси декартовой системы координат
Любой вектор в декартовой системе координат представляет собой перемещение из одной точки, имеющей координаты, в другую. Но для удобной работы с вектором такого представления недостаточно.
Поэтому вектор в декартовой системе координат можно представить как перемещение из начала координат в точку плоскости, аналогичное изначальному перемещению из точки в точку. Такое представление позволяет записывать вектор в виде трех значений осей (как раз по точке, в которую он приходит из начала координат), а также удобно складывать и умножать вектора. А зная изначальную точку, из которой выходит вектор, можно с помощью поэлементного сложения значений координат найти точку, в которую этот вектор придет, а значит такое представление не сильно отличается от более простого (описанного выше), но гораздо удобнее в плане вычислений.
Иными словами, вектор можно представить как $\vec{a} = \{a_x; a_y; a_z\}$. Тогда можно представить вектор и как $\vec{a} = a_x \hat{e}_x + a_y \hat{e}_y + a_z \hat{e}_z$, где $\hat{e}_x, \hat{e}_y, \hat{e}_z$ - орты системы координат, то есть единичные вектора каждой координаты.
#### Скалярное произведение векторов
>**Скалярное произведение** - покомпонентное произведение векторов.
То есть, при таком произведении сначала перемножаются все компоненты используемых векторов, а после находится их сумма. Формула: $\vec{a} \cdot \vec{b} = a_x \cdot b_x + a_y \cdot b_y + a_z \cdot b_z$.
Если такое произведение равно нулю, то это значит, что рассматриваемые вектора ортогональны (или, если проще, перпендикулярны). Отсюда следует альтернативная формула: $\vec{a} \cdot \vec{b} = | \vec{a} | \cdot | \vec{b} | \cdot \cos(\angle (\vec{a}, \vec{b}))$.
Можно расписать скалярное произведение поподробнее, с использованием орт. Тогда получим
$$
\vec{a} \cdot \vec{b} =
(a_x \hat{e}_x + a_y \hat{e}_y + a_z \hat{e}_z) \cdot (b_x \hat{e}_x + b_y \hat{e}_y + b_z \hat{e}_z) =
a_x b_x \hat{e}_x^2 + a_y b_y \hat{e}_y^2 + a_z b_z \hat{e}_z^2 + a_x b_y \hat{e}_x \hat{e}_y + a_x b_z \hat{e}_x \hat{e}_z + a_y b_x \hat{e}_x \hat{e}_y + a_y b_z \hat{e}_y \hat{e}_z + a_z b_x \hat{e}_x \hat{e}_z + a_z b_y \hat{e}_y \hat{e}_z =
a_x b_x \hat{e}_x^2 + a_y b_y \hat{e}_y^2 + a_z b_z \hat{e}_z^2 + (a_x b_y + a_y b_x) \hat{e}_x \hat{e}_y + (a_x b_z + a_z b_x) \hat{e}_x \hat{e}_z + (a_y b_z + a_z b_y) \hat{e}_y \hat{e}_z
$$
Так как произведение различных орт равно нулю (они ортогональны), то получается необходимое равенство $\vec{a} \cdot \vec{b} = a_x b_x \hat{e}_x^2 + a_y b_y \hat{e}_y^2 + a_z b_z \hat{e}_z^2 = a_x \cdot b_x + a_y \cdot b_y + a_z \cdot b_z$.
#### Модуль вектора
>**Модуль вектора** - скалярная величина, характеризующая длину вектора.
Модуль вектора можно найти по формуле $a = | \vec{a} | = \sqrt{a_x^2 + a_y^2 + a_z^2}$. Подробнее это можно расписать как $a = \sqrt{(\vec{a}, \vec{a})}$, а дальше под корнем найти скалярное произведение векторов (можно даже с ортами, тогда получится $a = \sqrt{a_x^2 \hat{e}_x^2 + a_y^2 \hat{e}_y^2 + a_z^2 \hat{e}_z^2}$, что тоже самое).
#### Векторное произведение векторов
>**Векторное произведение** - вектор, полученный произведением двух векторов и который по направлению им ортогонален, а по модулю равен площади параллелограмма, полученного из них.
Векторное произведение векторов обозначается как $\vec{a} \times \vec{b}$ или $[\vec{a}, \vec{b}]$. Направление векторного произведения можно легко найти по правилу правой руки: если указательный палец направить по направлению первого вектора, а средний - по направлению второго, то большой палец покажет направление векторного произведения.
Из определения следует, что векторное произведение можно найти по формуле $\vec{a} \times \vec{b} = | \vec{a} | | \vec{b} | \sin \alpha \cdot \hat{n}$, где $\alpha$ - угол между векторами, а $\hat{n}$ - вектор нормали к плоскости, в которой эти два вектора лежат.
Также векторное произведение можно найти и другим способом, используя матрицу и ее определитель, по формуле $\vec{a} \times \vec{b} = \mathop{\left| \begin{matrix} \hat{i} & \hat{j} & \hat{k} \\ a_x & a_y & a_z \\ b_x & b_y & b_z \end{matrix} \right|} = \hat{i} \mathop{\left| \begin{matrix} a_y & a_z \\ b_y & b_z \end{matrix} \right|} - \hat{j} \mathop{\left| \begin{matrix} a_x & a_z \\ b_x & b_z \end{matrix} \right|} + \hat{k} \mathop{\left| \begin{matrix} a_x & a_y \\ b_x & b_y \end{matrix} \right|} = \hat{i} (a_y b_z - a_z b_y) - \hat{j} (a_x b_z - a_z b_x) + \hat{k} (a_x b_y - a_y b_x) = \hat{i} (a_y b_z - a_z b_y) + \hat{j} (a_z b_x - a_x b_z) + \hat{k} (a_x b_y - a_y b_x)$.
#### Двойное векторное произведение
Также существует довольно простая формула для нахождения двойного векторного произведения трех векторов: $[\vec{a}, [\vec{b}, \vec{c}]] = \vec{b} (\vec{a}, \vec{c}) - \vec{c} (\vec{a}, \vec{b})$. Его легко запомнить как "бац минус цаб"
Данную формулу можно достаточно легко доказать. Для начала, провернем систему координат так, чтобы вектор $\vec{c}$ в этой системе имел координаты $\{0, 0, c_z\}$, а после еще раз провернем так, чтобы вектор $\vec{b}$ в этой системе имел координаты $\{0, b_y, b_z\}$. Далее посчитаем произведение
$$
[\vec{a}, [\vec{b}, \vec{c}]] =
\left[\vec{a}, \mathop{\left| \begin{matrix} \hat{i} & \hat{j} & \hat{k} \\ 0 & b_y & b_z \\ 0 & 0 & c_z \end{matrix} \right|}\right] =
\left[ \vec{a}, \hat{i} (b_y c_z) \right] =
\mathop{\left| \begin{matrix} \hat{i} & \hat{j} & \hat{k} \\ a_x & a_y & a_z \\ b_y c_z & 0 & 0 \end{matrix} \right|} =
\hat{j} (a_z b_y c_z) - \hat{k} (a_y b_y c_z) =
\hat{j} (a_z b_y c_z) - \hat{k} (a_y b_y c_z) + \hat{k} (a_z b_z c_z) - \hat{k} (a_z b_z c_z) =
\hat{j} (a_z b_y c_z) + \hat{k} (a_z b_z c_z) - \hat{k} (a_y b_y c_z + a_z b_z c_z) =
\vec{b} (a_z c_z) - \vec{c} (a_y b_y + a_z b_z) =
\vec{b} (\vec{a}, \vec{c}) - \vec{c} (\vec{a}, \vec{b})
$$
Таким образом, получаем нужное равенство.
#### Радиус-вектор
>**Радиус-вектор** - вектор от центра координат до точки в пространстве.
Иными словами, для точки с координатами $\{x, y, z\}$ радиус-вектор будет равен $\vec{r} = x \cdot \hat{e}_x + y \cdot \hat{e}_y + z \cdot \hat{e}_z$. Очевидно, тогда длина радиус-вектора будет равна $r = | \vec{r} | = \sqrt{x^2 + y^2 + z^2}$.
+3
View File
@@ -0,0 +1,3 @@
## Лекции
- [Кинематика материальной точки](Кинематика%20материальной%20точки.md)
- (и тд)
@@ -0,0 +1,326 @@
### Содержание
- [[#ADD]]
- [[#CALL]]
- [[#CMP]]
- [[#DEC]]
- [[#DIV]]
- [[#ENTER]]
- [[#IDIV]]
- [[#IMUL]]
- [[#INC]]
- [[#JMP]]
- [[#LEA]]
- [[#LEAVE]]
- [[#MOV]]
- [[#MUL]]
- [[#NEG]]
- [[#POP]]
- [[#PUSH]]
- [[#RET]]
- [[#SAL/SAR/SHL/SHR|SHIFT]]
- [[#SUB]]
- [[#SYSCALL]]
- [[#TEST]]
- [[#XOR]]
### ADD
`add <op1>, <op2>` добавляет значение операнда `<op1>` к значению операнда `<op2>` и записывает результат в операнд `<op1>`.
#### Комбинации операндов
`<op1>` - `<op2>`
- регистр - регистр или ячейка памяти
- регистр или ячейка памяти - регистр
- регистр - явно заданный операнд
- `al/ax/eax/rax` - явно заданный операнд
#### Флаги
Ставит OF, SF, ZF, AF, CF и PF.
#### Примечания
Как видно, нельзя сложить две ячейки памяти. Когда вторым операндом является явно заданное число, его знак расширяется до размера первого операнда.
### CALL
`call <address>` сохраняет информацию (регистр `rip`) о текущем процессе и передает управление процессу, расположенному по адресу `<address>` (заменят значение регистра `rip` на `address`).
#### Комбинации операндов
В зависимости от типа адресации, `<address>` может быть как регистром или ячейкой памяти (абсолютная адресация), так и меткой (относительная адресация).
#### Флаги
Не меняются.
#### Примечания
При использовании в качестве абсолютного адреса значения вершины стека `[rsp]` (с использованием косвенной адресации), команда сначала сохранит необходимый адрес перехода, и только потом сохранит адрес возврата в стек.
### CMP
`cmp <op1>, <op2>` выполняет операцию вычитания (`sub <op1>, <op2>`) без сохранения результата.
#### Комбинации операндов
Те же, что и у [[#SUB|инструкции]] `sub`.
#### Флаги
Ставит CF, OF, SF, ZF, AF и PF.
#### Примечания
В основном используется перед командами, исполнение которых зависит от флагов состояния. При необходимости, знак второго операнда расширяется до размера первого.
### DEC
`dec <op>` уменьшает значение операнда на единицу.
#### Комбинации операндов
Регистр или ячейка памяти.
#### Флаги
Не меняет CF.
Ставит OF, SF, ZF, AF и PF.
#### Примечания
Может использоваться для изменения счетчика цикла без изменения флага CF. Для того, чтобы также изменить значение флага CF, можно использовать [[#SUB|инструкцию]] `sub`.
### DIV
`div <op>` производит беззнаковое деление значения регистра `ax` (или `dx:ax`, `edx:eax`, `rdx:rax`, в зависимости от размера) на операнд `<op>`, сохраняя целую часть деления в регистр `al` (или `ax`, `eax`, `rax`), а остаток - в регистр `ah` (или `dx`, `edx`, `rdx`).
#### Комбинации операндов
Регистр или ячейка памяти.
В зависимости от размера операнда, меняются регистры для изначального значения и результата.
- `<op> = byte` (1 байт) - `ax -> al:ah`
- `<op> = word` (2 байта) - `dx:ax -> dx:ax`
- `<op> = dword` (4 байта) - `edx:eax -> edx:eax`
- `<op> = qword` (8 байт) - `rdx:rax -> rdx:rax`
#### Флаги
Не определены CF, OF, SF, ZF, AF и PF.
#### Примечания
Все значения округляются в сторону нуля. Чтобы не запутаться, в какой регистр какая часть результата записывается, можно регистры `rdx:rax` читать как дробь, где сверху будет `rdx` - остаток, а снизу - `rax` - целая часть.
### ENTER
`enter <op1>, <op2>` создает фрейм стека для подпрограммы с указанным размером `<op1>` и уровнем вложенности `<op2>`. При нулевом уровне вложенности сохраняет `rbp` в стеке, новое значение `rbp` ставит равным текущему `rsp`, а после сдвигает `rsp` на указанное количество байт `<op1>`. Если же вложенность больше 0, то помимо `rbp`, на стек будут также записаны значения, указывающие на фреймы всех предыдущих уровней вложенности, а также указатель текущего фрейма, и только после этого выделится нужное количество памяти.
#### Комбинации операндов
`<op1>` - явно заданное 16-битное число
`<op2>` - явно заданное 8-битное число
#### Флаги
Не меняет.
#### Примечания
Уровни вложенности применяются только в блочных структурах, где нужен доступ к памяти функции предыдущих уровней вложенности. На практике же можно применять только нулевой уровень вложенности. В таком случае команду `enter <op1>, 0` можно расписать как `push rbp; mov rbp, rsp; sub rsp, <op1>`. Чаще всего используется в паре с [[#LEAVE|инструкцией]] `leave`.
### IDIV
`idiv <op>` производит знаковое деление значения регистра `ax` (или `dx:ax`, `edx:eax`, `rdx:rax`, в зависимости от размера) на операнд `<op>`, сохраняя целую часть деления в регистр `al` (или `ax`, `eax`, `rax`), а остаток - в регистр `ah` (или `dx`, `edx`, `rdx`).
#### Примечания
В основном, данная команда схожа с [[#DIV|инструкцией]] `div`, только используется для знаковых чисел, а не для беззнаковых.
### IMUL
`imul <op1>, <op2>, <op3>` производит знаковое умножение двух операндов. При этом операнды `<op2>` и `<op3>` необязательны, но от их наличия изменяется поведение команды.
#### Комбинации операндов
- При наличии только одного операнда `<op1>` команда аналогична [[#MUL|инструкции]] `mul`. В данном случае `<op1>` это регистр или ячейка памяти.
- Если операнда два - `<op1>` и `<op2>`, тогда производится умножение значения операнда `<op1>` на значение операнда `<op2>`, а результат обрезается и записывается как значение операнда `<op1>`. В данном случае `<op1>` - регистр, а `<op2>` - регистр или ячейка памяти.
- Если операнда три - `<op1>`, `<op2>` и `<op3>`, тогда производится умножение значения операнда `<op2>` на значение операнда `<op3>`, а результат обрезается и записывается как значение операнда `<op1>`. В данном случае `<op1>` - регистр, `<op2>` - регистр или ячейка памяти, а `<op3>` - явно заданное 8/16/32/64-битное число.
#### Флаги
Ставит CF и OF, если
- для одного операнда: верхняя половина не ноль;
- для двух или трех операндов: результат должен быть обрезан, чтобы поместится в ячейку;
иначе CF и OF устанавливаются в ноль.
Не определены SF, ZF, AF и PF.
### INC
`inc <op>` увеличивает значение операнда `<op>` на единицу.
#### Комбинации операндов
Регистр или ячейка памяти.
#### Флаги
Ставит OF, SF, ZF, AF и PF.
Не меняет CF.
#### Примечания
Может использоваться для изменения счетчика цикла без изменения флага CF. Для того, чтобы также изменить значение флага CF, можно использовать [[#ADD|инструкцию]] `add`.
### JMP
`jmp <address>` производит безусловный переход на другую часть программы, указанную адресом `<address>`, и передает ей управление программой.
Помимо этого также существуют условные переходы, работающие по похожему принципу. Они производят переход только в определенном случае, в зависимости от значений установленных флагов.
#### Условные переходы
- `jc` - `jump if carry set (CF = 1)`
- `jnc` - `jump if carry clear (CF = 0)`
- `jo` - `jump if overflow set (OF = 1)`
- `jno` - `jump if overflow clear (OF = 0)`
- `jp` - `jump if parity set (PF = 1)`
- `jnp` - `jump if parity clear (PF = 0)`
- `js` - `jump if sign set (SF = 1)`
- `jns` - `jump if sign clear (SF = 0)`
- `jz` - `jump if zero set (ZF = 1)`
- `jnz` - `jump if zero clear (ZF = 0)`
- `je` - `jump if equal (ZF = 1)`
- `jne` - `jump if not equal (ZF = 0)`
- `ja` - `jump if above (CF = 0 & ZF = 0)`
- `jna` - `jump if not above (CF = 1 & ZF = 1)`
- `jae` - `jump if above or equal (CF = 0)`
- `jnae` - `jump if not above or equal (CF = 1)`
- `jb` - `jump if below (CF = 1)`
- `jnb` - `jump if not below (CF = 0)`
- `jbe` - `jump if below or equal (CF = 1 & ZF = 1)`
- `jnbe` - `jump if not below or equal (CF = 0 & ZF = 0)`
- `jg` - `jump if greater (ZF = 0 & SF = OF)`
- `jng` - `jump if not greater (ZF = 1 & SF != OF)`
- `jge` - `jump if greater or equal (SF = OF)`
- `jnge` - `jump if not greater or equal (SF != OF)`
- `jl` - `jump if less (SF != OF)`
- `jnl` - `jump if not less (SF = OF)`
- `jle` - `jump if less or equal (ZF = 1 & SF != OF)`
- `jnle` - `jump if not less or equal (ZF = 0 & SF = OF)`
#### Комбинации операндов
Регистр, ячейка памяти или явно заданное число.
#### Флаги
Не меняет.
#### Примечания
Не сохраняет информацию о возврате.
### LEA
`lea <op>, <address>` сохраняет адрес до значения по адресу `<address>` в качестве значения операнда `<op>`.
В зависимости от размера операндов, действие команды слегка меняется. Если они одного размера, ничего необычного не происходит. Если размер регистра превышает размер адреса, то адрес расширяется нулями до нужного значения и записывается в регистр. Если размер адреса больше размера регистра, то в регистр записывается только нижняя часть адреса.
#### Комбинации операндов
`<op>` - регистр.
`<address>` - ячейка памяти.
#### Флаги
Не меняет.
### LEAVE
`leave` очищает стек от фрейма, который ранее был выделен с помощью [[#ENTER|инструкции]] `enter`.
#### Флаги
Не меняет.
#### Примечания
Данную команду можно заменить последовательностью команд `mov rsp, rbp; pop rbp`. Чаще всего эта команда сопровождается [[#RET|инструкцией]] `ret`, что возвращает управление предыдущей программе.
### MOV
`mov <op1>, <op2>` копирует значение операнда `<op2>` и записывает его как значение операнда `<op1>`.
#### Комбинации операндов
`<op1>` - `<op2>`
- регистр или ячейка памяти - регистр
- регистр - регистр или ячейка памяти
- регистр или ячейка памяти - явно заданное значение
#### Флаги
Не меняет.
#### Примечания
Оба операнда должны быть одинакового размера. Как видно, оба операнда не могут быть ячейкой памяти.
### MUL
`mul <op>` производит беззнаковое умножение значения операнда `<op>` со значением регистра `al` (или `ax`, `eax`, `rax`, в зависимости от размера операнда `<op>`) и записывает результат в регистр `ax` (или `dx:ax`, `edx:eax`, `rdx:rax`).
#### Комбинации операндов
Регистр или ячейка памяти.
В зависимости от размера операнда, меняются регистры для изначального значения и результата.
- `<op> = byte` (1 байт) - `al -> ax`
- `<op> = word` (2 байта) - `ax -> dx:ax`
- `<op> = dword` (4 байта) - `eax -> edx:eax`
- `<op> = qword` (8 байт) - `rax -> rdx:rax`
#### Флаги
Ставит CF и OF в ноль, если верхняя половина результата пуста и равна нулю, иначе CF и OF ставятся в 1.
Не определены SF, ZF, AF и PF.
#### Примечания
При записи результата два регистра можно рассматривать как соединенные вместе: меньшая часть записывается в регистр `rax`, а большая, если не вмещается, в регистр `rdx`.
### NEG
`neg <op>` заменяет значение операнда на значение, полученное вычитанием изначального значения из нуля.
#### Комбинации операндов
Регистр или ячейка памяти.
#### Флаги
Ставит CF в 0, если значение операнда `<op> = 0`, иначе ставит CF в 1.
Ставит OF, SF, ZF, AF и PF.
### POP
`pop <op>` загружает значение, находящееся на вершине стека, в указанный операнд `<op>` и увеличивает указатель вершины стека. В зависимости от размера читаемого операнда `<op>` изменится и увеличение стека (сколько загружено, на столько стек и увеличится).
#### Комбинации операндов
Регистр или ячейка памяти.
#### Флаги
Не меняет.
#### Примечания
При исполнении команды `pop rsp` увеличивает указатель стека до того, как значение старой вершины стека будет записано в регистр.
### PUSH
`push <op>` уменьшает указатель вершины стека и загружает значение операнда `<op>` на вершину стека. В зависимости от размера операнда `<op>` изменится и уменьшение стека (сколько будет загружено, на столько стек и уменьшится).
#### Комбинации операндов
Регистр, ячейка памяти или явно заданное значение.
#### Флаги
Не меняет.
#### Примечания
Если операнд - явно заданное число, занимающее меньше места, чем полное значение операнда, то недостающие биты будут заполнены в соответствии со знаком числа. Команда `push rsp` запишет значение регистра `rsp` такое, какое было до его уменьшения. То же самое происходит, если адрес операнда `<op>` вычисляется на ходу и зависит от значения регистра `rsp`.
### RET
`ret <op>` берет с вершины стека значение адреса и передает ему управление программой, уменьшая указатель стека на размер взятого адреса. Опционально может быть указан операнд `<op>`, по значению которого будет освобождено соответствующее количество байт из стека (то есть указатель стека увеличится на соответствующее количество байт).
#### Флаги
Не меняет.
#### Примечания
Чаще всего используется после [[#CALL|инструкции]] `call` для возврата из подпрограммы.
### SAL/SAR/SHL/SHR
`shift <op1>, <op2>` (где `shift` - семейство команд сдвигов) сдвигает биты значения операнда `<op1>` на соответствующее значение операнда `<op2>`. Существует несколько разновидностей сдвигов:
- `sal` - арифметический сдвиг влево;
- `sar` - арифметический сдвиг вправо;
- `shl` - логический сдвиг влево;
- `shr` - логический сдвиг вправо.
Арифметический и логический сдвиг влево производят одну и ту же операцию: сдвигают биты значения влево (в сторону наиболее значимых битов). Логический сдвиг вправо заменяет все освобожденные сдвигом биты на ноль. Арифметический сдвиг вправо устанавливает все освобожденные сдвигом биты в зависимости от знака изначального числа.
#### Комбинации операндов
`<op1>` - регистр или ячейка памяти.
`<op2>` - явно заданное 8-битное значение или значение регистра `cl`. При этом данное значение урезается до 6 бит.
#### Флаги
Если значение операнда `<op2> = 0`, не меняет.
Ставит CF по последнему сдвинутому за пределы числа биту; CF не определен для логических сдвигов, если значение операнда `<op2>` больше или равно битовой длине значения операнда `<op1>`.
Ставит OF в 0 только для сдвигов на один бит только в том случае, если
- при сдвиге влево два крайних левых бита равны;
- при арифметическом сдвиге вправо;
иначе ставит в 1. В случае логических сдвигов вправо ставит OF по значению старшего бита изначального числа. Иначе, OF не определен.
Ставит SF, ZF и PF.
Не определен AF.
#### Примечания
Сдвиги также можно представить как умножение на два `<op2>` количество раз (в случае сдвига влево) или как деление на два `<op2>` количество раз (знаковое (с округлением в сторону отрицательной бесконечности) или беззнаковое, в зависимости от соответственно арифметического или логического сдвига вправо).
### SUB
`sub <op1>, <op2>` вычитает значение операнда `<op2>` из операнда `<op1>`, и записывает результат в операнд `<op1>`.
#### Комбинации операндов
`<op1>` - `<op2>`
- регистр - регистр или ячейка памяти
- регистр или ячейка памяти - регистр
- регистр - явно заданный операнд
- `al/ax/eax/rax` - явно заданный операнд
#### Флаги
Ставит OF, SF, ZF, AF, PF и CF.
#### Примечания
Оба операнда не могут быть одновременно ячейками памяти. Когда используется явно заданное значение, его знак расширяется до размера операнда `<op1>`.
### SYSCALL
`syscall` вызывает системный вызов на основе переданных через аргументы функции значений.
#### Флаги
Ставит все флаги.
#### Примечания
Подробнее об этой команде можно почитать в конспекте [семинара 1](Среда%20выполнения.%20Основы%20языка%20ассемблера.md#Команды).
### TEST
`test <op1>, <op2>` выполняет операцию логического "или" между значениями операндов `<op1>` и `<op2>` без сохранения результата.
#### Комбинации операндов
`<op1>` - `<op2>`
- регистр или ячейка памяти - регистр
- регистр или ячейка памяти - явно заданное число
- `al/ax/eax/rax` - явно заданное число
#### Флаги
Ставит OF и CF в 0.
Ставит SF, ZF и PF.
Не определен AF.
#### Примечания
Часто эту команду используют для сравнения операнда с нулем, дублируя этот операнд.
### XOR
`xor <op1>, <op2>` выполняет операцию исключающего "или" между значениями операндов `<op1>` и `<op2>` и записывает значение результата как значение операнда `<op1>`.
#### Комбинации операндов
`<op1>` - `<op2>`
- регистр - регистр или ячейка памяти
- регистр или ячейка памяти - регистр
- регистр - явно заданный операнд
- `al/ax/eax/rax` - явно заданный операнд
#### Флаги
Ставит AF и CF в 0.
Ставит SF, ZF и PF.
Не определен AF.
#### Примечания
Оба операнда не могут одновременно быть ячейками памяти. Часто данную команду используют для обнуления указанного дважды регистра.
@@ -0,0 +1,282 @@
## Содержание
- [[#Введение]]
- [[#Переходы]]
- [[#Локальные метки]]
- [[#Регистры]]
- [[#Хранение данных в памяти]]
- [[#Указатели]]
- [[#Функции]]
- [[#Определение функции]]
- [[#Возврат из функций]]
- [[#Функции с аргументами]]
- [[#Возврат значений из функции]]
- [[#Соглашение вызова]]
- [[#Стек]]
- [[#Использование стека для локальных переменных]]
- [[#Выравнивание стека]]
- [[#Конечный автомат]]
- [[#Введение в конечные автоматы]]
- [[#Разбор числа со знаком]]
- [[#Пять состояний процесса]]
- [[#Другие ситуации использования автоматов]]
- [[#Чем автоматы непривычны]]
- [[#Автоматы и регулярные выражения]]
- [[#Недетерминизм]]
- [[#Неопределенность]]
- [[#Полнота]]
- [[#Реализация]]
## Введение
### Переходы
Как известно, все инструкции в языке ассемблера выполняются последовательно. Но порядок выполнения инструкций можно явно изменить.
>**Переходы** - инструкции, которые явно меняют порядок выполнения программы.
Безусловный переход к необходимой инструкции по адресу выполняется с помощбю [инструкции](Библиотека%20команд%20ассемблера.md#JMP) `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 при условных переходах не используется) существует как минимум по две инструкции перехода: если флаг установлен в ноль или в единицу. Помимо этого, существуют другие полезные [инструкции](Библиотека%20команд%20ассемблера.md#JMP), такие как `ja`, `jb` (для беззнаковых числе), `jg`, `jl` (для знаковых чисел), `jae`, `jle` и подобные.
Для того, чтобы поставить необходимые для перехода флаги, в основном используются [инструкция](Библиотека%20команд%20ассемблера.md#TEST) `test` или [инструкция](Библиотека%20команд%20ассемблера.md#CMP) `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комментарии).
По факту, функции работают как [[#Переходы|переход]] с возвратом в место вызова. При вызове функции адрес возврата сохраняется в [[#Стек|стек]]. Для вызова функции используется [инструкция](Библиотека%20команд%20ассемблера.md#CALL) `call` (буквально `call addr` можно разложить как `push rip` и `jmp addr`, хотя вручную такие команды выполнить [[#Регистры|не получится]]).
Можно расписать вызов функции по шагам:
1. Сохранить все важные значения [[#Соглашение%20вызова|caller-saved]] регистров (их можно сохранить в стеке).
2. Выставить необходимые [[#Функции%20с%20аргументами|аргументы]] в нужные регистры.
3. Вызвать функцию [инструкцией](Библиотека%20команд%20ассемблера.md#CALL) `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комментарии) идет выделенный для этой функции код. Для вызова функции в программе будет использоваться [инструкция](Библиотека%20команд%20ассемблера.md#CALL) `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
```
### Возврат из функций
Вообще, подпрограмма обязана иметь возможность быть вызвана отовсюду и вернутся в место вызова. Для того, чтобы завершить функцию и вернутся к месту ее вызова, используется [инструкция](Библиотека%20команд%20ассемблера.md#RET) `ret` (буквально команду `ret` можно интерпретировать как `pop rip`).
Не стоит завершать функцию, если [[#Стек|стек]] не находится в том же положении, в котором был в начале инструкции. Иначе программа в качестве адреса возврата из [[#Стек|стека]] возьмет первую попавшуюся переменную, которая в этот момент будет на вершине стека, и скорее всего переход не удастся.
При выполнении подпрограммы для завершения программы с системным выходом `exit` можно не писать возврат из подпрограммы, так как программа все равно уже не будет работать.
При этом [инструкцию](Библиотека%20команд%20ассемблера.md#CALL) `call` можно заменить на [инструкцию](Библиотека%20команд%20ассемблера.md#JMP) `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, его необходимо отдельно сохранять в начале вызываемой подпрограммы. Это показано в третьем способе выделения памяти в стеке.
Выделение памяти под стек также иногда называется прологом, а в ассемблере существует [инструкция](Библиотека%20команд%20ассемблера.md#ENTER) `enter`, которая реализует третий способ пролога. В то же время освобождение памяти в стеке также иногда называется эпилогом, и в ассемблере для него существует [инструкция](Библиотека%20команд%20ассемблера.md#LEAVE) `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. обработать все остальные символы как [[#Неопределенность|неопределенное поведение]].
@@ -0,0 +1,343 @@
## Содержание
- [[#Введение]]
- [[#Область видимости]]
- [[#Виды]]
- [[#Время жизни]]
- [[#Модель памяти процессора]]
- [[#Стек вызовов]]
- [[#Соглашение о вызовах]]
- [[#Вызов функции]]
- [[#Кадр стека]]
- [[#Виртуальная память]]
- [[#Страницы]]
- [[#Страничное нарушение]]
- [[#mmap]]
- [[#Формат ELF]]
- [[#Из файла в процесс]]
- [[#История]]
- [[#Практика]]
- [[#Расположение переменных]]
- [[#Карта памяти процесса]]
- [[#Вызов функции глазами компилятора]]
- [[#Отображение памяти mmap]]
- [[#Заголовки ELF]]
- [[#Отложенное связывание]]
- [[#Путь от исходника к процессу]]
## Введение
В [лекции 3](Цикл%20трансляции,%20лексический%20и%20синтаксический%20анализ.md#Цикл%20трансляции) был подробно описан процесс превращения исходного текста программы в полноценный исполняемый файл. Несмотря на весь этот процесс, часть [символов](Цикл%20трансляции,%20лексический%20и%20синтаксический%20анализ.md#Таблицы) этого файла остается неразрешенными до запуска программы.
Очевидно, исполняемый файл и процесс отличаются друг от друга: первый - всего лишь байты на диске, в то время как второй - полноценный динамический набор из различных регистров, стека, кучи и открытых файлов. Но что именно представляется из себя этот запуск программы?
## Область видимости
>**Область видимости** (scope) - часть текста программы, где имя связано с сущностью.
Иными словами, область видимости отвечает за то, чтобы каждому имени [символа](Цикл%20трансляции,%20лексический%20и%20синтаксический%20анализ.md#Таблицы) в программе была сопоставлена определенная переменная программы с ее адресом.
### Виды
Очевидно, у область видимости бывают различные виды. Всего их два: лексическая (или статическая) и динамическая.
**Лексическая область видимости** ищет имя [символа](Цикл%20трансляции,%20лексический%20и%20синтаксический%20анализ.md#Таблицы) в тексте программы на основе вложенности блоков. То есть, от места обращения к [символу](Цикл%20трансляции,%20лексический%20и%20синтаксический%20анализ.md#Таблицы) программа будет проходить от уровня вложенности, где был вызван [символ](Цикл%20трансляции,%20лексический%20и%20синтаксический%20анализ.md#Таблицы), все выше и выше, до тех пор, пока необходимый [символ](Цикл%20трансляции,%20лексический%20и%20синтаксический%20анализ.md#Таблицы) не будет найден. Таким образом, адрес вызываемого [символа](Цикл%20трансляции,%20лексический%20и%20синтаксический%20анализ.md#Таблицы) будет известен уже сразу при [компиляции](Цикл%20трансляции,%20лексический%20и%20синтаксический%20анализ.md#Компиляция) программы.
**Динамическая область видимости** ищет имя [символа](Цикл%20трансляции,%20лексический%20и%20синтаксический%20анализ.md#Таблицы) на основе цепочки активных вызовов. То есть, такая область видимости возьмет первое попавшееся значение (точнее последнее установленное значение) переменной по имени и передаст именно ее. Таким образом, результат работы программы с такой областью видимости будет зависеть от окружения, в котором данный [символ](Цикл%20трансляции,%20лексический%20и%20синтаксический%20анализ.md#Таблицы) был вызван.
Большинство языков программирования (к примеру, C, Java, Python, Haskell) имеют лексическую область видимости, ведь она более удобна и предсказуема. Тем не менее, динамическая область видимости все также существует (к примеру, в bash, Emacs Lisp, ранние Lisp), и несмотря на то, что читать и понимать код с такой областью видимости труднее, она является удобным стандартным кейсом.
### Время жизни
Время жизни и область видимости - очень похожие понятия, но у них есть существенные различия, поэтому их лучше не путать между собой. Область видимости показывает, где именно можно использовать имя переменной и к какой переменной это будет относится. Время жизни показывает, в какое время исполнения программы данная переменная будет доступна для обращения к ней по имени.
К примеру, для локальной переменной программы, находящейся в стеке, область видимости и время жизни совпадают: оба ограничены блоком (хотя для времени жизни было бы правильнее сказать, что переменная "живет", пока выполняется блок). Но вот для локальной `static` переменной, находящейся в [[#Модель памяти процессора|области]] `.data / .bss`, все немного иначе: несмотря на то, что ее область видимости ограничена блоком, "жить" такая переменная будет все время исполнения программы.
Другим примером совпадения области видимости и времени жизни является глобальная переменная, хранящаяся в [[#Модель памяти процессора|области]] `.data / .bss`, ведь "жить" такая переменная будет все время исполнения программы, и будет "видна" в любом месте файла программы (очевидно, включая `extern` из файла).
Интересным примером является объект, полученный из [[#mmap|памяти]] `malloc`. У самого такого объекта нет имени, имя есть только у указателя на него. "Жить" такой объект будет в [[#Модель памяти процессора|куче]], с момента его объявления (функцией `malloc`), до его освобождения (функцией `free`).
Из-за такого несовпадения области видимости и времени жизни иногда возникают так называемые висячие указатели - когда сам объект уже недоступен, но указатель на него все еще существует и "виден". Такое можно наблюдать, если попытаться вернуть из функции адрес локальной переменной.
## Модель памяти процессора
>**Адресное пространство** - линейный массив байт, который заполнен не сплошь, а кусками.
Адресное пространство разделено на несколько различных по назначению и правам доступа областей. Каждая такая область имеет адрес начала и максимальный размер.
Основными областями памяти являются (от наименьшего адреса начала к наибольшему):
- `.text` - машинный код программы (права `r-x`)
- `.rodata` - строковые литералы или константы (права `r--`)
- `.data / .bss` - сегмент статических данных (то есть глобальные и `static` переменные) (права `rw-`)
- `Heap` (Куча) - `malloc` / `free` (права `rw-`)
- Отображения файлов - `mmap` (права `r-x/rw`)
- Стек - локальные переменные, адреса возврата (права `rw-`)
Куча растет вниз, к наибольшим адресам. Стек растет в обратную сторону - вверх, к наименьшим адресам. То есть, они растут навстречу друг другу.
Если в программе идет обращение к неотображенному адресу, то подается сигнал SIGSEGV ([[#Страничное нарушение|ошибка сегментации]]).
## Стек вызовов
### Соглашение о вызовах
В первую очередь, соглашение о вызовах - это именно соглашение, а не какое-то обязательство. Но оно иногда сильно помогает при анализе написанного кода, а также исключает многие ошибки при изменении функциональности подпрограммы и предотвращает непредсказуемое поведение.
Одно из правил, которые фиксирует данное соглашение, являются места передаваемых аргументов. Первые шесть аргументов передаются в регистрах `rdi`, `rsi`, `rdx`, `rcx`, `r8`, `r9`, строго по порядку. Все остальные аргументы передаются через стек, и складываются таким образом, чтобы сверху были аргументы с наименьшим номером. Если же аргументы являются числами с плавающей точкой (к примеру, типа float или doubel), то для них предусмотрены отдельные регистры от `xmm0` до `xmm7`, опять же по порядку.
Место возвращаемого аргумента также описывается соглашением. В основном, все аргументы возвращаются в регистре `rax`, но также можно передавать результат и в регистре `rdx` (то есть, `rdx:rax`). Для числе с плавающей точкой местом возврата является регистр `xmm0`.
Помимо этого, соглашение устанавливает, какие регистры во время исполнения функции могут изменяться, а какие должны остаться неизменными после завершения ее работы. Регистры, которые не должны изменяться, называются `callee-saved` (то есть, вызванная функция обязана восстановить данные регистры после завершения своей работы), таковых всего 7: `rbx`, `rbp`, `r12`, `r13`, `r14`, `r15` и `rsp`. Остальные регистры (к примеру, `rax`, `rcx`, `rdx`, `rsi`, `rdi`, `r8`, `r9`, `r10`, `r11`) являются `caller-saved` (то есть, данные регистры могут быть изменены вызовом функции, поэтому должны быть сохранены тем, кто эту функцию вызывает).
Также в соглашении рекомендуется, чтобы при вызове функции указатель стека был кратен 16. В основном это сделано для функций, которые работают с 16-байтовыми регистрами или значениями, для которых это может быть важно (к примеру, некоторые SIMD-инструкции внутри libc).
### Вызов функции
Очевидно, вызов функции внутри программы можно логически разделить на несколько этапов.
В самом начале функции передаются необходимые для ее работы аргументы: некоторые в регистры, некоторые на стек (желательно, соблюдая соглашение). Следующим шагом производится сама команда call, то есть сохранение адреса возврата в стек и переход на по адресу функции.
После этого внутри функции выполняется пролог. Старое значение регистра `rbp` сохраняется на вершину стека, текущее значение регистра `rsp` записывается как новое значение регистра `rbp`, а после уменьшением регистра `rsp` на необходимое число байт освобождается фрейм стека под необходимые функции локальные переменные.
Далее начинается тело функции, то есть основная ее часть. В ней производится работа с различными аргументами и локальными переменными (если таковые есть), и на выходе получается результат, который записывается в регистр `rax` (а при необходимости, в `rdx:rax`).
Перед завершением функции происходит эпилог. Эпилог состоит из двух этапов. На первом освобождается фрейм стека, который использовался для функции (в основном, с помощью прибавления к значению регистра `rsp` необходимого значения). На втором этапе предыдущее значение регистра `rbp` снимается с верхушки стека и возвращается на регистр.
В самом конце происходит выход из функции с помощью команды `ret`. Эта команда берет значение с верхушки стека в качестве адреса возврата и переходит по нему, передавая обратно управление программе, которая эту функцию вызвала. Важно помнить, что команда `ret` слепо доверяет стеку, и без проверки переходит по адресу, который там лежит. Это важно учитывать, потому что этим можно воспользоваться и перенаправить программу куда угодно, что может привести к плохим последствиям.
### Кадр стека
>**Кадр стека** - область стека одного активного вызова функции.
Иными словами, у каждой функции есть специальная отведенная область в памяти, где хранятся все необходимые данные. Кадр стека вызываемой функции состоит из пяти частей.
На самой верхушке стека располагаются локальные переменные, специфичные для этой функции. Чаще всего, к ним можно обратится при помощи регистра `rsp`, который обычно указывает на начало области данных с этими переменными.
После локальных переменных идут сохраненные в начале функции callee-saved регистры, такие как `rbx`, `r12`, `r13`, `r14` и `r15` (конечно, к callee-saved также относятся `rsp` и `rbp`, но `rsp` вернется на то же место при завершении функции, а `rbp` сохраняется чуть позже).
В последней ячейке сохраненных регистров идет `rbp`. При прологе функции регистр `rbp` сохраняется в стек, а новое значение этого регистра ставится на ячейку со значением старого.
Далее в памяти располагается адрес возврата функции. По сути, данный адрес является границей между двумя кадрами: вызываемой функцией и вызывающей функцией, ведь именно на этом этапе происходит сам вызов функции, то есть исполнение команды `call`.
В конце памяти располагаются аргументы, которые не поместились в регистры и были переданы в через стек.
При таком расположении `rbp` очень легко использовать для доступа как к локальным переменным функции (начиная с `[rbp - 8]`, при условии что никакие другие регистры не сохраняются), так и к аргументам, переданным в функцию (начиная с `[rbp + 16]`).
Именно с помощью сохранения различных значений `rbp` работает `backtrace` в отладчике `gdb`, который строит цепочку из таких значений и после использует ее для отладки.
## Виртуальная память
>**Виртуальная память** - набор адресов, которые видит программа; все адреса там - виртуальные, в перевод в нестоящие осуществляется с помощью MMU оп таблицам, подготовленным ОС.
Иными словами, виртуальная память - это ненастоящая память (неким образом связанная с настоящими адресами), которая дается процессу для работы. У существования виртуальной памяти есть три основных назначения.
Первым таким назначением является изоляция. Благодаря виртуальной памяти, у каждого процесса есть свое собственное адресное пространство, а один и тот же адрес в двух разных процессах будет ссылаться на две абсолютно разные физические ячейки памяти компьютера.
Вторым является простота адресации. Вся память внутри процесса принадлежит только ему, и ему нет необходимости знать реальные физические размеры памяти или то, какие другие процессы также могут обращаться к этим ячейкам памяти.
Третьим является экономия. Благодаря виртуальной памяти, процессу нет необходимости знать точные ее размеры. Поэтому чаще всего страницы в памяти выделяются лениво, по первому запросу, а редко используемая память вытесняется на диск. При этом, libc в памяти одна на сотни процессов.
### Страницы
Вся виртуальная память на машине хранится по страницам. Каждая страница виртуальной памяти занимает 4 КБ (именно поэтому все границы в карте памяти процесса кратны 0x1000). Права доступа к памяти выдаются постранично, различны между страницами и одинаковы для всей страницы.
Перевод из виртуального адреса в физический занимает всего четыре шага. На первом этапе виртуальный адрес разбивается на адрес страницы и на смещение внутри этой страницы (то есть, по факту, просто отбрасывая последние 12 бит до лучших времен), а после передает дальше для поиска адреса страницы. На втором шаге ядро ищет , по которому процессор синтезирует из виртуального адреса настоящий; но процессор кэширует все запросы к страницам с помощью специального кэша страниц - TLB (Translation Lookaside Buffer), поэтому если какая-либо страница доставалась из памяти до этого, она уже будет в кэше и ее не придется искать снова. На третьем этапе определяется физический кадр (то есть физическое пространство в памяти, соответствующее виртуальной странице), а также его права доступа, и производится проверка на, собственно, доступ. Напоследок, если доступ есть, к адресу физического кадра возвращается отброшенное ранее смещение (не измененное), и происходит чтение необходимой ячейки физической памяти.
#### Страничное нарушение
Очевидно, иногда у ядра случаются промахи в памяти: к примеру, нет такой записи в памяти или банально не хватает прав. На такой случай у ядра есть специальный штатный механизм (не ошибка! - он может отрабатывать до тысяч раз в секунду, и на программу это никак не повлияет) page fault, при возникновении которого в зависимости от причины определяются пути решения.
Самая частая причина возникновения page fault - указанного места в памяти не существует. Тогда с помощью карты областей ядро решает, должно ли данное место являться доступным для программы, и если оно действительно доступно, до место в памяти выделяется. Если изначальное обращение было выполнено к стеку или куче, то ядро выделяет ранее обнуленное место в кадре стека. Если же это было обращение к обычной памяти, то ядро сначала загружает эту область памяти в основную память, а после дает к ней доступ программе.
Также иногда случается, что ранее загруженная программой страница внезапно при следующем обращении к ней оказывается вытеснена в своп. Такая проблема тоже вызывает механизм page fault. В таком случае ядро загружает память из свопа обратно, и программа продолжает работать, будто ничего и не произошло.
Конечно, page fault иногда также возвращает ошибки, если доступ к памяти действительно заблокирован. К таким случаям относятся, к примеру, попытка обращения к адресу вне области процесса, или работа с памятью вне прав доступа (к примеру, запись в область с правами r--). В таких случаях ядро генерирует сигнал SIGSEGV, отправляет его обратно в процесс, и там он становится печально известной ошибкой Segmentation fault.
### mmap
Назначение mmap довольно простое - он создает новую область в адресном пространстве процесса.
Чаще всего используется отображение файла в память. То есть память с диска напрямую переносится в процесс. При этом можно либо перенести в память процесса копию памяти с диска (`MAP_PRIVATE`), либо сразу работать с памятью на диске (`MAP_SHARED`). В основном, такая память выделяется лениво - то есть загружается в память только при первом обращении к ней с помощью page fault.
Помимо отображения файла в память также иногда применяется и анонимная память (`MAP_ANONYMOUS`). В отличие от отображения в памяти, оно, как очевидно из названия, не имеет память в качестве источника, а создается напрямую в памяти процесса (то есть не связана с какими-либо данными вне процесса). При создании такой области все значения в ней заполняются нулями, а фактическое расположение указывает на стек или кучу. Такая память все еще выделяется лениво, но вместо копирования памяти в память процесса просто выделяет кадр в стеке (или куче). Тем не менее, хоть анонимная память и существует только внутри процесса, она также передается при форке - у потомка тоже будет доступ к анонимной памяти родителя.
Отображение памяти чаще всего применяется библиотеками или для чтения файлов. Анонимная память больше подходит для больших `malloc`, стеков потоков, а также аллокаторов. В `glibc` большие блоки `malloc` (по умолчанию, $\geq$ 128 КБ) выделяются сразу анонимным образом, а маленькие блоки используют кучу.
### Формат ELF
Формат ELF (Executable and Linkable Format) используется для исполняемых и компонуемых файлов (к примеру, объектных файлов типа .o), как понятно из названия. Он состоит из различных секций. в каждой из которых хранятся разные по назначению данные.
В начале файла идет заголовок, занимающий первые 64 байта. В нем в основном хранятся магические числа (массив байт для идентификации файла, первые четыре байта `7f 45 4c 46`, что характеризует файл ELF), тип файла, архитектуру процессора для запуска, версия ELF, точку входа программы, дополнительные флаги, а также вся необходимая информация про заголовки таблиц внутри файла.
Сразу после заголовка идет таблица программных заголовков. Она используется загрузчиком, и дает ему всю необходимую информацию для того, чтобы понять, что и куда необходимо загрузить.
После программных заголовков идут обычные секции файла. Среди них секция `.text` (сам исполняемый код программы), секция `.rodata` (константы для программы), секция `.data` (инициализированные данные программы), а также несколько секций поменьше для символов и отладочной информации (`.symtab`, `.strtab`, `.debug_*`).
В конце файла расположена таблица заголовков секций. В ней перечислены все именованные секции программы, а также вся необходимая информация про них (имена, типы, смещения секций). Данная таблица используется компоновщиком или отладчиком для их корректной и быстрой работы.
На самом деле, исполняемому файлу не нужны ни таблица программных заголовков, ни разделение на секции. Даже без всего этого, исполняемый файл все еще может быть запущен. Поэтому в объектном файле уже не хранятся программные заголовки, а секции удаляются.
#### Из файла в процесс
Очевидно, нельзя просто взять и запустить программу из памяти - даже если это уже полноценно сформированный ELF файл.
В самом начале текущий исполняемый процесс (чаще всего, оболочка) делает форк, создавая потомка, и уже в этом форке исполняет необходимый файл (с помощью `execev()`). При исполнении такой программы ядро читает заголовок ELF файла, а также все программные заголовки, и получает всю необходимую информацию о том, как этот файл нужно запускать.
Следующим шагом идет подготовка к запуску программы. Каждый необходимый `LOAD` загружается в процесс с помощью `mmap`, выполняется обнуление `.bss`, а также стека вместе с `argv` и `envp`. С помощью `INTERP` происходит загрузка `libc.so.6` в процесс и все необходимые релокации.
После всех таких подготовок наконец-то происходит исполнение файла. В случае скомпилированного файла из языка C по метке `_start` происходит запуск кода из `main`, а после завершение процесса с помощью `exit`. Таким образом можно заметить, что ядро никогда не копирует программу в память целиком, оно лишь создает необходимые отображения, а страницы подгружает по первому запросу.
### История
Первый пример виртуальной памяти появился еще в 1962 году, когда в Манчестерском университете создали компьютер [Atlas](https://ru.wikipedia.org/wiki/Atlas_(компьютер)). Данная машина имела несколько страниц памяти с разным временем доступа до каждого из них, и при этом видела каждый из этих листов. Поэтому она могла перемещать данные и таким образом динамически менять время доступа до разных разделов памяти, в зависимости от того, что приоритетнее.
Через пару лет, в 1965 году, впервые появился прообраз mmap - операционная система [Multics](https://ru.wikipedia.org/wiki/Multics) считала все файлы как часть адресного пространства.
Ближе к концу 1970-ых годов страничная виртуальная память уже стала нормой в [Unix](https://ru.wikipedia.org/wiki/Unix).
В конце 1980-ых годов происходит выход [System V](https://ru.wikipedia.org/wiki/System_V) Release 4, в котором впервые используется формат ELF. Раньше для тех же целей использовались [a.out](https://ru.wikipedia.org/wiki/A.out) и [COFF](https://ru.wikipedia.org/wiki/COFF).
В 2000-ых впервые в явном виде описывается соглашение о вызовах, заданное в System V AMD64 с помощью ABI (Application Binary Interface).
Иными словами, все необходимые технологии были придуманы достаточно давно. Все, что используется на данный момент в языке C, просто получило удобный способ использования.
## Практика
### Расположение переменных
Рассмотрим код на языке C. Для каждой переменной в коде ниже распишем, в каком сегменте данных она находится.
```c
int counter = 42;
char buffer[4096];
const char *msg = hello;
int main(void) {
int local = 1;
static int calls = 0;
int *p = malloc(100);
calls++;
printf("%p %p %p\n", (void*)&local, (void*)p, (void*)&counter);
free(p);
return 0;
}
```
Первая переменная `counter` является числом, поэтому она сразу записывается в сегмент `.data`.
Переменная `buffer` в файле не хранится, обнулена и находится в `.bss`.
Во время создания третьей переменной сам литерал `"hello"` записывается в `.rodata`, а `.msg` получает указатель на него и хранит его в секции `.data`.
Машинный код функции main хранится в секции `.text`.
Локальная переменная `local` попадает в кадр стека функции `main` и хранится там.
Следующая переменная `calls` является `static`, поэтому существует на протяжении выполнения всей программы в секции `.data`.
Для следующей переменной из кучи выделяется 100 байт, а для указателя `p` выделяется место в кадре стека функции `main` для хранения адреса этого пространства.
### Карта памяти процесса
Все занимаемые программами секции памяти можно легко посмотреть через файл `/proc/PID/maps` (где `PID` - номер процесса). Ниже представлен пример такого вывода.
```sh
$ cat /proc/$(pgrep -n demo)/maps
55d0c8a00000-55d0c8a01000 r--p 00000000 08:02 1572903 /home/st/demo
55d0c8a01000-55d0c8a02000 r-xp 00001000 08:02 1572903 /home/st/demo
55d0c8a02000-55d0c8a03000 r--p 00002000 08:02 1572903 /home/st/demo
55d0c8a03000-55d0c8a04000 rw-p 00002000 08:02 1572903 /home/st/demo
55d0c9b6e000-55d0c9b8f000 rw-p 00000000 00:00 0 [heap]
7f3a1c400000-7f3a1c428000 r--p 00000000 08:02 2622221 /usr/lib/x86_64-linux-gnu/libc.so.6
7f3a1c428000-7f3a1c5bd000 r-xp 00028000 08:02 2622221 /usr/lib/x86_64-linux-gnu/libc.so.6
7f3a1c7e4000-7f3a1c7e6000 r--p 00000000 08:02 2622218 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffd3e9a1000-7ffd3e9c2000 rw-p 00000000 00:00 0 [stack]
```
Вывод этого файла будет состоять из шести колонок. В первой будет указан диапазон адресов, которые занимает тот или иной процесс. Во втором столбце будут указаны права доступа к секции, а также настройки ее приватности (`p = private`, `s = shared`). В третьей колонке будет указано смещение для файла, в котором хранится память этого процесса. В четвертом столбце будет указано устройство, на котором эта память хранится. В пятой колонке будет указан inode файла хранения памяти. В шестом столбце будет хранится путь до процесса.
Важно учитывать, что для такой анонимной памяти, как к примеру стек или куча (которая не сохраняется в файле), смещение, номер устройства и inode всегда будут равны нулю.
### Вызов функции глазами компилятора
Предположим, что у нас есть функция на языке C.
```c
long sum3(long a, long b, long c) {
long s = a + b;
return s + c;
}
```
Компилятор `gcc` позволяет скомпилировать файл в объектный, а после из него получить инструкции ассемблера. Это можно сделать следующим образом.
```sh
gcc -O0 -c sum3.c
objdump -d -M intel --no-show-raw-insn sum3.o
```
В результате, получится следующий набор инструкций.
```
<sum3>:
push rbp
mov rbp,rsp
mov QWORD PTR [rbp-0x18],rdi ; a
mov QWORD PTR [rbp-0x20],rsi ; b
mov QWORD PTR [rbp-0x28],rdx ; c
mov rdx,QWORD PTR [rbp-0x18]
mov rax,QWORD PTR [rbp-0x20]
add rax,rdx
mov QWORD PTR [rbp-0x8],rax ; s
mov rdx,QWORD PTR [rbp-0x8]
mov rax,QWORD PTR [rbp-0x28]
add rax,rdx ; результат в rax
pop rbp
ret
```
### Отображение памяти mmap
Ниже рассмотрим код на языке C для отображения файла в память и простейшей работы с ним.
```c
int fd = open("hello.txt", O_RDONLY);
struct stat st;
fstat(fd, &st);
char* data = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (data == MAP_FAILED) { perror("mmap"); }
/* файл теперь - обычный массив */
fwrite(data, 1, st.st_size, stdout);
mummap(data, st.st_size);
close(fd);
```
Данная простая программа отображает (копирует) файл (`fd`) в область памяти (`data`), а после выводит содержимое этого файла (`fwrite`) в стандартный поток вывода (`stdout`).
### Заголовки ELF
Как было сказано ранее, в заголовках файла ELF много полезной информации. И конечно, ее можно посмотреть прямо из файла с помощью специальной команды.
```sh
$ readelf -h demo
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Entry point address: 0x1060
$ readelf -lW demo # столбец PhysAddr опущен
Type Offset VirtAddr FileSiz MemSiz Flg Align
INTERP 0x000318 0x0000000000000318 0x00001c 0x00001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x000000 0x0000000000000000 0x000628 0x000628 R 0x1000
LOAD 0x001000 0x0000000000001000 0x000175 0x000175 R E 0x1000
LOAD 0x002000 0x0000000000002000 0x0000f4 0x0000f4 R 0x1000
LOAD 0x002db8 0x0000000000003db8 0x000258 0x000260 RW 0x1000
```
По названию столбцов и строк вполне понятно, какой столбец за что отвечает.
Также можно отметить, что последний `LOAD` имеет размер памяти больше размера файла. Эта разница заполняется нулями загрузчиком. Этот фрагмент и есть секция `.bss` (сегмент статических данных).
### Отложенное связывание
Из-за особенностей исполнения файла программе не всегда заранее известно, по какому адресу будет находится метка. Для этого существует механизм отложенного связывания. При копировании программы в память для GOT (Global Offset Table) отделяется специальное место, куда по нахождению будут записываться адресы меток. В то же время, в самом коде все метки заменяются заглушками из PLT (Procedure Linkage Table).
Таким образом, при первом вызове функции загрузчик найдет адрес необходимой метки и запишет его на нужное место в GOT, а после совершит переход. Все же последующие вызовы функции будут происходить сразу, ведь адрес метки уже известен и записан. При этом важно помнить, что GOT открывается только для чтения.
Пример программы с GOT и PLT представлен ниже.
```sh
$ objdump -d -M intel demo
0000000000001139 <main>:
1139: push rbp
113a: mov rbp,rsp
113d: lea rax,[rip+0xec4]
1144: mov rdi,rax
1147: call 1030 <puts@plt>
0000000000001030 <puts@plt>:
1030: jmp QWORD PTR [rip+0x2fe2]
# ячейка GOT для puts
```
### Путь от исходника к процессу
Таким образом, у каждой сущности в C есть различные отображения как в объектном файле, так и в сегменте ELF, а также в уже запущенном процессе. Для каждой такой сущности важно уметь назвать данные отображения. Ниже представлены пара примеров.
Для глобальной переменной `int g = 5` можно сказать, что в объектном файле она будет записана в секции `.data`, в файле ELF она будет записана в виде `LOAD RW`, а в процессе она будет представлена как `rw-p` и путь к файлу.
Для переменной `char buf[4096]` можно сказать, что в объектном файле она будет записана в секции `.bss`, в файле ELF она будет записана в виде `LOAD RW (MemSiz > FileSiz)`, а в процессе она будет представлена обнуленные страницы.
Для функции `main` можно сказать, что в объектном файле она будет записана в секции `.text`, в файле ELF она будет записана в виде `LOAD R E`, а в процессе она будет представлена как `r-xp` и путь к файлу.
Для локальной переменной `int x` можно сказать, что в объектном файле она записана не будет, в файле ELF ее тоже не будет, а в процессе она появится как кадр стека `[stack]`.
Для аллокации памяти `malloc(100)` можно сказать, что в объектном файле и в файле ELF ее не будет, а в процессе она появится как часть `[heap]` или анонимный `mmap`.
Для `puts` из `libc` можно сказать, что в объектном файле она будет представлена в виде символа с его релокацией, в файле ELF она будет представлена в виде `PLT / GOT, INTERP`, а в процессе она будет представлена `libc.so.6` с правами `r-xp`.
@@ -0,0 +1,70 @@
>[!WARNING]
>К сожалению, данная лекция на данный момент не доведена до завершения, и в ней отсутствуют некоторые моменты.
>Недостающие материалы будут дополнены позже.
## Содержание
- [[#Парадигмы]]
- [[#Императивная парадигма]]
- [[#Объектно-ориентированная парадигма]]
- [[#Функциональная парадигма]]
- [[#Логическая парадигма]]
- [[#Машинная модель вычислений]]
- [[#Регистры]]
- [[#Память]]
- [[#Стек]]
- [[#Система команд]]
- [[#Способы исполнения программы]]
- [[#Компиляция]]
- [[#Интерпретация]]
- [[#JIT]]
## Парадигмы
> **Парадигма** - набор концепций и абстракций, определяющих стиль структурирования программы.
Парадигма задает "основную единицу" программы, а также выражение потока управления, параллельно с этим работая с состоянием программы определенным образом. Каждый язык программирования использует одну или несколько парадигм.
До 1957 года код писался напрямую в машинных кодах. Созданный в это время [Fortran](https://ru.wikipedia.org/wiki/Фортран) стал первым массовым языком высокого уровня, закрепив [[#Императивная парадигма|императивный стиль]]. В 1958 году [Lisp](https://ru.wikipedia.org/wiki/Лисп) ввел функции как данные, а также рекурсию, что закрепило [[#Функциональная парадигма|функциональную парадигму]].
(История создания)
При этом не стоит путать парадигмы с такими вещами, как процедурное программирование (в рамках [[#Императивная парадигма|императивной парадигмы]], с акцентом на процедурах как единицах декомпозиции), структурное программирование (в рамках [[#Императивная парадигма|императивной парадигмы]]), паттерны проектирования (типовые решения в [[#Объектно-ориентированная парадигма|ООП]]), архитектурные стили (REST, микросервисы), мультипоточность и асинхронность.
Есть пара советов, в связи с которыми можно выбрать основную парадигму для задачи. Но редко бывает, что задача решается исключительно одной парадигмой, поэтому чаще всего в проекте есть основная парадигма, а остальные используются "точечно", там где нужно.
Но парадигма - лишь абстракция, в конце концов все становится машинным кодом, а процессор знает лишь инструкции - ему плевать на парадигмы. Парадигма - инструмент мышления и реализации кода.
### Императивная парадигма
Суть императивной парадигмы - напрямую приказывать программе делать что-то. Основная абстракция - состояние программы и ее изменение. Ничего не делается само по себе: каждое действие программы руководствуется прописанными инструкции.
Ассемблер - предельный случай императивности. В нем нет скрытых механизмов: все делается ровно по инструкции. Каждое действие физически отражается в компьютере: изменение регистров, смена выполняемой инструкции и так далее.
### Объектно-ориентированная парадигма
Основная абстракция - объект: данные и поведение над ними объединены вместе. Основные принципы ООП: инкапсуляция, наследование и полиморфизм.
Есть два способа переиспользоать код в программе: наследование и композиция. Наследование - отношение "является", композиция - отношение "содержит". В индустрии закрепилось правило: "предпочитайте композицию наследованию". Это пошло из-за хрупкости наследования: одно изменение в классе-предке может сломать всех потомков, о существовании которых можно даже не догадываться.
### Функциональная парадигма
Основная абстракция -функция как знаечние первого класса. Нет изменяемого состояния - результат зависит только от входных аргументов.
### Логическая парадигма
Программа - это набор фактов и правил логического вывода, а вычисление - поиск ответа на вопрос, при этом программа сама ищет путь выполнить задачу, а если не получается - возвращается назад. Программист описывает лишь то, что является истинным, а не как это вычислить.
## Машинная модель вычислений
### Регистры
Регистры общего назначения - самая быстрая память процессора для текущих операндов. На x86-64 - 16 64-битных регистров общего назначения, а также 2 регистра служебного назначения. Регистр - ограниченный ресурс, их нельзя путать с бесконечным количеством перемнных.
### Память
### Стек
### Система команд
У процессора есть система команд, это самые базовые команды работы с процессором и регитсрами. Выполнение программы на самом базовом уровне - небольшой цикл: взять адрес инструкции, выполнить ее, передвинуться на одну инструкцию ниже и повторить все по новой.
## Способы исполнения программы
Способ исполнения - не парадигма, один язык может иметь разные реализации.
### Компиляция
Компиляция - перевод всего текста программы заранее в машинный код (традиционно в него).
### Интерпретация
Интерпретация - исполнение построчно специальной программой.
### JIT
JIT - гибрид двух способов, сначала интерпретирует код, а после "горячие" участки компилирует.
@@ -0,0 +1,216 @@
## Содержание
- [[#Среда окружения]]
- [[#Использование интерактивной оболочки]]
- [[#Виды интерактивных оболочек]]
- [[#Стандартные потоки ввода-вывода]]
- [[#Дерево процессов]]
- [[#Регулярные выражения]]
- [[#Переменные окружения]]
- [[#Основы языка ассемблера]]
- [[#Компиляция ассемблерных программ]]
- [[#Простейшая программа]]
- [[#Секции, метки и комментарии]]
- [[#Директивы]]
- [[#Команды]]
- [[#Пошаговый анализ]]
- [[#Отладчик ассемблера gdb]]
- [[#Запуск программы]]
- [[#Основные команды]]
- [[#Форматы данных]]
## Среда окружения
### Использование интерактивной оболочки
Любая операционная система предполагает наличие интерфейса взаимодействия между пользователем и различными компонентами компьютерной системы. Чаще всего, таким интерфейсом является оболочка.
>**Оболочка** (англ. *shell*) - программный уровень, который поддерживает ввод команд и параметров для получения необходимых результатов.
Все оболочки имеют некоторые общие функции и свойства. Среди них интерпретация команд; доступ к командам и результатам их выполнения; обработка файлов, операция стандартного ввода и вывода; реализация специального языка программирования оболочки.
#### Виды интерактивных оболочек
Существует множество различных оболочек. Самой распространенной из всех является `bash` ([Bourne-Again Shell](https://ru.wikipedia.org/wiki/Bash)), которая используется по умолчанию для большинства дистрибутивов Linux. В MacOS по умолчанию стоит [zsh](https://ru.wikipedia.org/wiki/Zsh), а в Windows используется только [PowerShell](https://ru.wikipedia.org/wiki/PowerShell). Для того, чтобы узнать используемую оболочку, чаще всего достаточно посмотреть значение переменных `$SHELL` (в которой содержится путь до используемой по умолчанию в системе оболочки) или `$0` (в которой содержится имя текущего исполняемого процесса, а в контексте оболочки это как раз она).
Для того, чтобы изменить используемую оболочку, чаще всего достаточно команды `chsh`, которая изменяет оболочку пользователя. Использовать ее удобнее всего с флагом `-s/--shell` и путем к необходимой оболочке, которые сразу поставят необходимую оболочку. Важно помнить, что `chsh` перед установкой оболочки сверяется с содержимым файла `/etc/shells`, поэтому путь к необходимой оболочке должен быть в этом файле указан.
Основная суть оболочки `sh` ([Bourne shell](https://ru.wikipedia.org/wiki/Командная_оболочка_Unix)) в том, что она является стандартом POSIX-синтаксиса и имеет в себе только необходимые команды и функции для использования. Для повседневной работы `sh` не удобна, потому что в ней отсутствует множество в основном привычных и почти необходимых функций, и чаще всего она уступает более гибким вариантам оболочек. Тем не менее, `sh` удобно использовать для скриптов, которые не зависят от оболочки, ведь так как `sh` является стандартом, большинство оболочек имеют те же команды и могут исполнять скрипты `sh` по умолчанию. И именно из-за этого в настоящее время `sh` используется чаще всего не как интерактивная оболочка, а как просто интерпретатор для скриптов и автоматизации.
#### Стандартные потоки ввода-вывода
Для того, чтобы работать с пользовательским вводом и выводом, в оболочках предусмотрены потоки ввода-вывода. По умолчанию, стандартный поток ввода читается напрямую с клавиатуры, а стандартные потоки вывода пишутся в терминал. Для обозначения стандартного потока ввода используют `stdin` (с дескриптором 0), для обозначения стандартного потока вывода - `stdout` (с дескриптором 1), а для обозначения стандартного потока вывода ошибок - `stderr` (с дескриптором 2).
Для работы с потоками ввода-вывода в оболочке предусмотрены специальные символы перенаправления. Для перенаправления стандартного потока вывода в файл используются операторы `>` или `>>` (в зависимости от того, нужно ли нам перезаписывать файл, как это делает `>`, или дописать в конец файла, как это делает `>>`). Таким же образом можно перенаправить стандартный поток вывода в файл, только при этом явно указывая номер потока перед оператором (то есть, используя команды `2>` и `2>>`). Помимо файла, поток можно перенаправить и в другой поток теми же символами, указав после оператора `&` с номером потока (то есть, к примеру, `2>&1` направит вывод стандартного потока вывода ошибок в обычный стандартный поток вывода). Для стандартного потока ввода тоже есть похожие команды `<` и `<<`, но они работают немного по-другому. Оператор `<` читает содержимое указанного после него файла и отправляет в стандартный поток ввода. Оператор `<<` читает следующий вводимый текст до определенного символа, указанного после данного оператора, и также отправляет в стандартный поток ввода.
Помимо этого, с помощью стандартных потоков ввода-вывода можно перенаправлять результат одной команды в другую. Для этого используется конвейер `|`, который берет стандартный поток вывода первой команды и сразу перенаправляет его в стандартный поток ввода следующей в цепи команды.
#### Дерево процессов
>**Дерево процессов** - логическое представление всех процессов в операционной системе, связанных отношением "родитель - потомок".
В Linux все процессы, кроме самого первого, имеют своего родителя - процесс, который их создал. Это обеспечивает иерархию между процессами и помогает определить, какие процессы связаны и зависят друг от друга.
При этом у каждого процесса есть свой уникальный номер - `PID`, а также уникальный номер родителя - `PPID`. Это помогает следить за иерархией прямо внутри процессов. У корня дерева - основного процесса, у которого нет родителя - чаще всего устанавливается `PID 1`.
Очевидно, дерево процессов динамическое, потому что процессы в системе постоянно появляются и исчезают. Потоки внутри определенного процесса, по факту, тоже являются своего рода процессами, но их обычно к таковым не относят.
### Регулярные выражения
>**Регулярные выражения** - набор символов и/или метасимволов (символов, которые имеют особое значение), которые наделены особыми свойствами.
В данном случае мы будем рассматривать только регулярные выражения, используемые командой `grep`. Данная команда используется для поиска данных по заданному шаблону. Она может найти подходящие под шаблон строки, слова, количество таких строк или слов и много чего еще.
При задании шаблона в `grep` чаще всего используются специальные символы. Ниже представлены основные из них.
- `*` - символ слева может повторяться ноль или более раз
- `\+` - символ слева может повторяться один или более раз
- `?` - символ слева может повторятся ноль или один раз
- `\{` и `\}` - число точного количества повторений символа слева
- `.` - любой символ
- `^` - начало строки
- `$` - конец строки
- `[` и `]` - подмножество допустимых символов (символ `^` внутри квадратных скобок используется для отрицания)
- `\<` и `\>` - начало и конец слова
- `\` - экранирование специальных символов
Больше информации про `grep` и регулярные выражения можно найти в [статье](https://www.opennet.ru/docs/RUS/bash_scripting_guide/c11895.html).
### Переменные окружения
>**Переменные окружения** - именованные переменные, содержащие текстовую информацию, которую могут использовать запускаемые программы.
В основном, такие переменные содержат общие настройки системы, параметры графической или командной оболочки, данные о предпочтениях пользователя и многое другое.
Некоторые переменные окружения хранят значения, которые используются для особых целей.
- `LD_LIBRARY_PATH` - директория, где по умолчанию ищутся динамические библиотеки
- `PATH` - место размещения исполняемых файлов в системе
- `EDITOR` - имя предпочитаемого текстового редактора
При создании дочернего процесса от родительского по умолчанию наследуются все переменные окружения. Но данные переменные могут быть явно изменены во время или после запуска дочернего процесса. Переменные могут изменится во время создания процесса, либо если явно указать передаваемые переменные или заменить их, либо если система сочтет небезопасным передавать эти переменные окружения новому процессу. Также переменные могут быть изменены уже внутри дочернего процесса. При этом изменение переменных в дочернем процессе никак не могут повлиять на переменные в родительском процессе.
Помимо переменных окружения существуют также и переменные оболочки. В отличие от переменных окружения, они живут только внутри одного процесса и не передаются ни вверх по [[#Дерево процессов|дереву процессов]], ни вниз. Для того, чтобы из переменной оболочки сделать переменную окружения, которая могла бы передаваться в другие процессы, используется ключевое слово `export`.
## Основы языка ассемблера
### Компиляция ассемблерных программ
Сам файл с языком ассемблера имеет расширение `.asm`. Компиляция такого файла в исполняемый машинный код происходит в два этапа. На первом этапе используется компилятор, который создает из файла с расширением `.asm` файл с расширением `.o` - файл, содержащий все машинные инструкции вместо кода, но вместо настоящих операндов-адресов памяти имеющий заглушки. На втором этапе линкер (или компоновщик) создает из одного или более объектных файлов с расширением `.o` один исполняемый файл без расширения.
Для компиляции на первом этапе используется команда `nasm` (Netwide Assembler). В [[#Простейшая программа|простейшей программе]] далее при компиляции используются флаги `-g`, `-felf64` и `-o`. Флаг `-g` выводит debug-информацию о процессе компиляции. Флаг `-f` устанавливает формат итогового файла, в данном случае `elf64`, что является стандартным вариантом для большинства Unix-подобных систем на архитектуре `x86-64`. Флаг `-o` указывает, в какой файл записать вывод программы.
Для компоновки на втором этапе используется команда ld (стандартный линкер для GNU). В [[#Простейшая программа|простейшем примере]] далее при компиляции используется флаг `-o`. Как и в описании флагов для компилятора, данный флаг записывает результат работы программы в указанный файл.
Более подробно этот процесс и подробности выполняемых команд описаны в [семинаре 3](Цикл%20компиляции,%20препроцессор,%20утилита%20make.md#Цикл%20компиляции).
### Простейшая программа
Далее подробно рассмотрим простейшую программу на языке ассемблера, которая выводит "hello, world!" в стандартный поток вывода.
```
; hello.asm
section .data
message: db 'hello, world!', 10
section .text
global _start
_start:
mov rax, 1 ; 'write' syscall number
mov rdi, 1 ; stdout descriptor
mov rsi, message ; string address
mov rdx, 14 ; string length in bytes
syscall
mov rax, 60 ; 'exit' syscall number
xor rdi, rdi
syscall
```
#### Секции, метки и комментарии
Как известно из [архитектуры фон-Неймана](https://ru.wikipedia.org/wiki/Архитектура_фон_Неймана), программа хранит как данные, так и коды инструкции в одном месте - нераздельной памяти. Но для удобства использования программы их все же необходимо разделить. Поэтому программы на языке ассемблера чаще всего делят на секции.
>**Секция** - специальный метод деления кода на части, призванный разделить различные логически раздельные части друг от друга.
Для каждой отдельной секции существует свой способ и место применения. Так, в простейшей программе используются секции `.text` для объявления инструкций, которые должна выполнять программа, и `.data` для объявления глобальных переменных (переменных, доступных во время всего исполнения программы). При этом даже если в коде программы постоянно чередовать одни и те же секции, при компиляции одинаковые секции все равно объединяются вместе и в память соответственно будут записываться вместе.
Для того, чтобы не использовать прямые адреса к значениям (что достаточно трудно и доставляет много проблем), используются `labels` (метки). По сути, это просто читаемые названия адресов. В примере программы используется только одна метка - `_start`.
Также стоит заметить, что в отличие от всего остального кода названия секций и меток чувствительны к регистру. Иными словами, инструкции в программе могут быть записаны в любом регистре, и компилятор поймет это, но метки или секции с разным регистром уже будут расцениваться как две разные метки или секции.
Одна программа может делится на несколько файлов и запускаться вся сразу. Для того, чтобы не запутаться в порядке исполнения программы и запустить именно то, что нужно, существует точка входа в программу. В случае языка ассемблера точкой входа обычно служит метка `_start` - это часть программы, которая при запуске отрабатывает первее всех. При этом важно, что метка с таким названием обязательно должна быть указана [[#Директивы|директивой]] `global`.
Также в языке ассемблера можно писать комментарии к коду. Для этого используется специальный символ `;`, после которого до конца строки текст не будет расцениваться как код программы.
#### Директивы
В основном, язык ассемблера состоит из команд, которые при компиляции превращаются напрямую в машинные команды. Но не все инструкции в языке являются командами.
>**Директивы** - конструкции в языке, которые являются не командами для исполнения, а командами, контролирующими процесс перевода программы в машинный код. Иногда некоторые директивы также называют псевдо инструкциями.
В простейшем примере присутствуют три директивы: команды `global`, `section` и `db`. Секции были рассмотрены ранее, а `global` будет рассмотрен позже. Директива `db` используется для создания байтовых данных (`db = data bytes`).
Также существуют другие директивы, использующиеся для определения данных. Чаще всего из них используются `dw` (`dw = data word`, двухбайтовое слово), `dd` (`dd = data double`, четырехбайтовое удвоенное слово) и `dq` (`dq = data quad`, восьмибайтовое учетверенное слово). Все эти директивы кодируют входящие символы в коды ASCII. В каждой из указанных ранее директив для хранения данных можно указывать несколько значений подряд через запятую. Так, в примере после самой строки на вывод указывается через запятую символ с кодом 10, который завершает строку.
Помимо этого, существует полезная директива `times`, которая позволяет повторять указанную после нее команду заданное количество раз.
#### Команды
Команда `xor` выполняет операцию логического "или" между двумя указанными регистрами и записывает результат в первый из указанных. Очень часто данная команда используется для обнуления указанного дважды регистра.
Команда `mov` используется для того, чтобы записать значение из второго операнда в первый. Операнды могут быть регистром или памятью, а второй - еще и явным значением. Тем не менее, важно учитывать два важных недостатка: эта команда не может копировать данные из памяти в память, а также источник и место назначения должны быть одного и того же размера.
Команда `syscall` позволяет исполнять системные вызовы внутри программы. Из-за того, что операции ввода-вывода зависят от устройства, а также могут использоваться несколькими программами одновременно, в коде нельзя контролировать эти операции напрямую, в обход операционной системы. Каждый системный вызов имеет собственный номер. При исполнении вызова программа берет его номер из регистра `rax`, а необходимые аргументы (в зависимости от их количества последовательно) из регистров `rdi`, `rsi`, `rdx`, `r10`, `r8` и `r9`. Соответственно, системный вызов не может принимать больше шести аргументов. При исполнении команда меняет значения в регистрах `rcx` и `r11`.
Важно помнить, что программа не завершится, пока данная инструкция не будет указана явно. Иными словами, если убрать из примера последние три строки с вызовом завершения программы, то она продолжит работать, читая из памяти последующие значения. При этом эти значения, если они не были явно обработаны памятью, никак не могут быть нулями или правильными инструкциями, поэтому программа завершится с ошибкой с кодом 6 (инвалидные инструкции).
#### Пошаговый анализ
Таким образом, можно рассмотреть программу по шагам.
1. Запись переменной `message` с данными `hello world!` и окончанием строки с помощью команды `db` в секции `.data`
2. Начало секции `.text` и объявление метки `_start` в область `global`
3. Начало программы с адресом в `_start`
4. Установка номера системного вызова записи `write` 1 в регистр `rax`
5. Установка первого аргумента системного вызова, в данном случае номер дескриптора системного потока вывода 1, в регистр `rdi`
6. Установка второго аргумента системного вызова, в данном случае адрес начала читаемой строки `message`, в регистр `rsi`
7. Установка третьего аргумента системного вызова, в данном случае количество байтов для чтения 14, в регистр `rdx`
8. Системный вызов
9. Установка номера системного вызова завершения программы `exit` 60 в регистр `rax`
10. Установка первого аргумента системного вызова в ноль
11. Системный вызов
Во время исполнения программы можно увидеть постоянные изменения регистра `rip`. Он указывает на адрес следующей исполняемой инструкции программы.
После выполнения системного вызова в регистр `rax` записывается результат выполнения системного вызова. Для системного вызова `write` в `rax` записывается число реально записанных байт или отрицательный код ошибки.
### Отладчик ассемблера gdb
>**Отладчик** - инструмент, который позволяет запускать программу под своим контролем, останавливать ее в нужных местах и изучать ее состояние: значения переменных, память, стек вызовов, потоки, файлы, регистры процессора.
gdb - популярный и мощный отладчик для языков ассемблера и C. В его основные функции входит исполнение программы по шагам, изучение состояния программы и слепка регистров и памяти на каждом шаге, а также управление отладчиком с помощью специальной системы команд. Помимо этого он также может перемещаться и изучать стек вызовов, вычислять арифметические выражения, запускать функции, автоматизировать отладку с помощью скриптов на Python и многое другое.
В коде можно установить специальные точки останова (англ. *breakpoint*), которые будут останавливать программу в нужном месте.
У gdb есть специальная [[#Основные команды|система команд]], с помощью которой можно управлять процессом отладки. Поэтому для этого в него встроена командная строка. Помимо этого, с gdb можно работать и через интерактивный интерфейс. Но с помощью команд можно создавать скрипты и автоматизировать отладку программы, что иногда бывает удобнее.
#### Запуск программы
Для запуска программы в отладчике gdb есть два способа: используя встроенную в gdb команду `file`, или запустив отладчик сразу с именем программы в качестве аргумента.
С помощью сочетания клавиш `Ctrl-X` и `o` можно менять фокус на окнах и взаимодействовать с выбранным окном. С помощью сочетания клавиш `Ctrl-L` можно перерисовать интерфейс отладчика.
#### Основные команды
Две самые важные команды в отладчике - это команды `quit` и `help`.
В число основных команд также входят.
- `run` или `r` - запуск выполнения программы
- `break <метка>` или `b <метка>` - установка точки останова в программе по имени метки
- `break *<адрес>` или `b *<адрес>` - установка точки останова в программе на указанный адрес
- `continue` - продолжение выполнения программы
- `stepi` или `si` - выполнение ровно одной инструкции
- `nexti` или `ni` - выполнение ровно одной инструкции, при этом не заходя в функции, вызванные при помощь команды `call`, а выполняя всю функцию за шаг
- `layout` - переключение режима интерфейса
- `print /<format> <val>` - просмотр содержимого регистра или памяти; в `<format>` указывается формат данных (необязательно); в `<val>` указывается метка адреса памяти или знак `$` с именем регистра
- `x /<format> <address>` - аналог `print`, но принимает адрес напрямую (или регистр); можно использовать символ `&` для взятия адреса с метки, что позволяет этой команде также работать с метками
- `record` - запуск записи
- `record stop` - завершение записи
- `reverse-stepi` или `rsi` - шаг назад ровно на одну инструкцию (обратная к `si`)
- `reverse-nexti` или `rni` - шаг назад ровно на одну инструкцию, но без захода в функции (обратная к `ni`)
Чаще всего с командой `layout` используется аргумент `regs`, но иногда можно и `asm`.
#### Форматы данных
В некоторых командах можно указывать форматы выходных данных, что иногда удобно при отладке.
- `x` - hexadecimal - шестнадцатеричная система счисления
- `a` - address - адрес
- `i` - instruction - инструкция (gdb попытается машинный код превратить в команду ассемблера)
- `c` - char - символ
- `s` - null-terminated string - строка с символами ASCII и с окончанием символом `0x00`
- `b` - byte - байт
- `g` - giant - 8 байт
- любое число - количество выбираемых объектов
@@ -0,0 +1,281 @@
## Содержание
- [[#Формальные грамматики]]
- [[#Основы]]
- [[#Форма Бэкуса-Наура]]
- [[#Пример вывода строки]]
- [[#Спецификация языка]]
- [[#Теория автоматов]]
- [[#История]]
- [[#Иерархия Хомского]]
- [[#Конечный автомат]]
- [[#ДКА и НКА]]
- [[#Регулярные выражения и автоматы]]
- [[#Построение Томпсона]]
- [[#Минимизация]]
- [[#Границы конечных автоматов]]
- [[#Формы представления конечного автомата]]
- [[#Примеры конечных автоматов]]
- [[#Автомат для идентификаторов]]
- [[#Автомат для комментариев]]
- [[#Автомат для регулярного выражения]]
- [[#Автомат для четного числа символов "a"]]
## Формальные грамматики
>**Формальная грамматика** - точный, механический способ задать, какие строки принадлежат языку.
Формальные грамматики состоят из четырех компонентов: терминалы, нетерминалы, правила вывода и стартовый символ.
>**Терминалы** - конечные символы, которые реально появляются в тексте (к примеру, числа, арифметические символы, скобки).
>**Нетерминалы** - промежуточные категории, которые раскрываются по правилам (к примеру, "выражение", "слагаемое").
>**Вывод строки** - это последовательное применение правил от стартового символа до терминалов.
### Основы
Формальные грамматики состоят из нескольких уровней: синтаксиса, семантики и прагматики.
**Синтаксис** отвечает за правильность написания самого текста программы. Примером синтаксиса может являться правильность расставленных скобок, постановка точки с запятой в конце инструкции и так далее.
**Семантика** отвечает за значение записанной информации. Примером семантики может быть значение записи "a+b" (что именно должна вычислить данная запись) и тому подобное.
**Прагматика** отвечает за то, как данные инструкции могут быть использованы на практике. Примерами прагматики являются стиль, идиомы, соглашения сообщества и так далее.
Тем не менее, очень важно не путать уровни между собой (хотя это и не просто). К примеру, выражение $\frac{1}{0}$ синтаксически безупречно (форм записи оно не нарушает), но семантически возникает вопрос: а что данное выражение должно вычислить? В разных языках ответ на этот вопрос представлен по-разному: исключение, бесконечность, неопределенное поведение.
### Форма Бэкуса-Наура
>**Форма Бэкуса-Наура** (БНФ) - стандартная нотация записи формальных грамматик.
Данная форма была названа в честь [Джона Бэкуса](https://ru.wikipedia.org/wiki/Бэкус,_Джон) и [Питера Наура](https://ru.wikipedia.org/wiki/Наур,_Петер). Впервые данная форма была использована ими для описания [Algol 60](https://en.wikipedia.org/wiki/ALGOL_60?ysclid=mujsiofdar620274942).
В основе синтаксиса грамматики лежат пара простых символов: `::=` (раскрывается в) и `|` (альтернатива, "или"). Нетерминалы в этой грамматике чаще всего берутся в угловые скобки или пишутся курсивом. Терминалы же берутся в кавычки.
Вывод строки в этой грамматике происходит пошагово. На каждом шаге раскрывается по одному нетерминалу. Порядок раскрытия может быть любым - это не влияет на итоговый результат.
При этом, если для строки не существует последовательности шагов, за которую ее можно было бы вывести, то она не принадлежит языку. Это и есть формальный, механический критерий "правильности" строки.
#### Пример вывода строки
Рассмотрим грамматику, заданную следующими условиями.
```
<expr> ::= <expr> "+" <term>
| <term>
<term> ::= <term> "*" <factor>
| <factor>
<factor> ::= "(" <expr> ")"
| <number>
<number> ::= "0" | "1" | "2" | ... | "9"
```
С такой грамматикой выведем строку "2+3" и таким образом докажем, что она принадлежит языку.
```
<expr>
-> <expr> + <term>
-> <term> + <term>
-> <factor> + <term>
-> 2 + <term>
-> 2 + <factor>
-> 2 + 3
```
#### Спецификация языка
По своей сути, спецификация языка - это та же формальная грамматика.
Рассмотрим фрагмент реальной спецификации языка (упрощенно, в духе C).
```
<if-statement> ::=
"if" "(" <expr> ")" <statement>
| "if" "(" <expr ")" <statement>
"else" <statement>
<statement> ::=
<if-statement>
| <block>
| <expr> ";"
<block> ::= "{" <statement>* "}"
```
Про звезду * в последней строке после `<statement>` поговорим [[#Построение Томпсона|чуть позже]].
В примере видно, что `<statement>` ссылается сам на себя, используя для этого `<if-statement>` и `<block>`. Такая рекурсивная вложенность в грамматике и делает синтаксис язык [[#Иерархия Хомского|контекстно-свободным, а не регулярным]].
## Теория автоматов
### История
Изначально, теория автоматов родилась не из программирования, а из вопроса "что вообще значит вычислять?". Программирование появилось позже, как массовая практика, и унаследовало уже готовый аппарат.
В самом начале, в 1943 году, [Уоррен Маккаллок](https://ru.wikipedia.org/wiki/Мак-Каллок,_Уоррен) и [Уолтер Питтс](https://ru.wikipedia.org/wiki/Питтс,_Уолтер) создали [формальную модель нейрона](https://ru.wikipedia.org/wiki/Искусственный_нейрон), которая по своей сути является переключательным автоматом. В 1950-ых годах [Стивен Клини](https://ru.wikipedia.org/wiki/Клини,_Стивен_Коул) вывел [[#Регулярные выражения и автоматы|связь регулярных выражений и конечных автоматов]]. В 1956 году [Ноам Хомский](https://ru.wikipedia.org/wiki/Хомский,_Ноам) предложил [[#Иерархия Хомского|иерархию формальных грамматик]]. В 1959 году [Михаэль Рабин](https://ru.wikipedia.org/wiki/Рабин,_Михаэль) и [Дана Скотт](https://ru.wikipedia.org/wiki/Скотт,_Дана) дали [[#ДКА и НКА|строгое определение НКА]], а также доказали его эквивалентность с ДКА.
### Иерархия Хомского
В своей теории Хомский делит грамматику языка на четыре типа - от 3 до 0. Чем ниже номер типа, тем мощнее грамматика и тем сложнее устройство, нужное для распознавания.
Тип 3 - **регулярные** грамматики. Примерами языка с такой грамматикой будут являться различные идентификаторы или числа. Для распознавания такой грамматики в основном используется [[#Конечный автомат|конечный автомат]].
Тип 2 - **контекстно-свободные** грамматики. Примером языка с такой грамматикой может быть синтаксис любого языка программирования. Для распознавания такой грамматики используется [автомат с магазинной памятью](https://ru.wikipedia.org/wiki/Автомат_с_магазинной_памятью).
Тип 1 - **контекстно-зависимые** грамматики. Примером языка с такой грамматикой может быть какой-либо согласование типов или размерностей. Для распознавания такой грамматики используется [линейно ограниченный автомат](https://en.wikipedia.org/wiki/Linear_bounded_automaton).
Тип 0 - **рекурсивно перечислимые** грамматики. Примером языка с такой грамматикой может быть любой алгоритмически заданный язык. Для распознавания такой грамматики используется [машина Тьюринга](https://ru.wikipedia.org/wiki/Машина_Тьюринга).
### Конечный автомат
>**Конечный автомат** - пятерка ($Q, \Sigma, \delta, q_0, F$), где $Q$ - конечное множество состояний, $\Sigma$ - алфавит (то есть множество допустимых входных символов), $\delta$ - функция переходов из состояния и символа в новое состояние, $q_0$ - начальное состояние, $F$ - множество принимающих (или финальных) состояний.
Строка принимается автоматом, если, начав в $q_0$ и применяя $\delta$ символ за символом, автомат оказывается в одном из состояний $F$.
Такие автоматы можно встретить в лексическом анализе (разбивает текст программы на токены), сетевых протоколах (разбирает TCP-состояние, парсит [HTTP-заголовки](../Веб-программирование/Общие%20стандарты%20и%20протоколы%20сети%20Интернет.md#Заголовки%20HTTP)), играх (поведение NPC), [[#Регулярные выражения и автоматы|регулярных выражениях]] и схемотехнике (сам по себе конечный автомат является стандартной моделью цифровых схем управления).
#### ДКА и НКА
>**Детерминированный конечный автомат** (ДКА) - конечный автомат, в котором существует ровно один возможный переход из определенного состояния по данному символу.
>**Недетерминированный конечный автомат** (НКА) - конечный автомат, в котором существует ноль, один или несколько возможных переходов из определенного состояния по данному символу, а также возможный $\varepsilon$-переходы (то есть переходы без чтения символа).
Выразительная сила как у ДКА, так и у НКА одинакова. То есть, любой НКА можно преобразовать в эквивалентный ДКА.
Тем не менее, НКА иногда естественнее описывает поставленную задачу, но ДКА проще моделировать словами. Чаще всего при переходе из НКА в ДКА теряется удобство описания.
#### Регулярные выражения и автоматы
>**Теорема Клини** - регулярные выражения и конечные автоматы имеют одинаковую выразительную силу.
Иными словами, любому регулярному выражению можно сопоставить эквивалентный конечный автомат, и наоборот - любому конечному автомату можно сопоставить регулярное выражение. Регулярное выражение - это просто более компактная и понятная для человека форма записи того же языка.
##### Построение Томпсона
Пусть у нас есть регулярное выражение. По теореме Клини, конечный автомат, эквивалентный данному выражению, существует. Но как его получить?
>**Построение Томпсона** - алгоритм для получения конечного автомата из регулярного выражения, согласно которому необходимо сначала разобрать регулярное выражение на элементарные части, а после собрать снизу вверх.
Всего таких элементарных частей три.
- Конкатенация (ab) - соединить автомат для a с автоматом для b по $\varepsilon$-переходу
- Альтернатива (a|b) - новое начальное состояние с двумя $\varepsilon$-переходами к автоматам a и b
- Звезда Клини (a*) - $\varepsilon$-переход, позволяющий пройти через автомат для a ноль, один или более раз.
Именно по такому алгоритму работает большинство движков для регулярных выражений в стандартных библиотеках языков программирования.
#### Минимизация
[[#Построение Томпсона]] очень хорошо работает для построения автомата, но обычно дает автомат с избыточным числом состояний. Именно поэтому существуют методы минимизации полученного конечного автомата.
>**Теорема Майхилла-Нероуда** - для каждого регулярного языка существует единственный минимальный ДКА.
Данную теорему придумали и доказали [Джон Майхилл](https://en.wikipedia.org/wiki/John_Myhill) и [Энил Нероуд](https://en.wikipedia.org/wiki/Anil_Nerode) в 1958 году.
По сути это означает, что для минимизации конечного автомата можно объединить два состояния, если они ведут себя одинаково для любого продолжения строки. На практике это означает, что будет меньше состояний, а значит на регистр или переменную будет уходить меньше памяти, а значит переход будет быстрее.
Современные компиляторные генераторы лексеров (flex/lex) минимизируют автомат автоматически, прежде чем сгенерировать код.
#### Границы конечных автоматов
Конечно, не все языки можно выразить через конечные автоматы.
Классическим примером этого является язык сбалансированных скобок. Для того, чтобы конечный автомат смог его разобрать, ему необходима неограниченная память (ведь он должен помнить количество скобок, которые еще не закрылись).
Формально такую ограниченность конечных автоматов можно доказать через [лемму о накачке](https://en.wikipedia.org/wiki/Pumping_lemma_for_regular_languages).
Почти в любом языке программирования синтаксис представляет собой [[#Иерархия Хомского|контекстно-свободный, а не регулярный]] язык. Поэтому лексер (конечный автомат) сам по себе не может разобрать структуру программы целиком, для этого ему нужен парсер с дополнительной памятью (стеком).
#### Формы представления конечного автомата
Конечный автомат можно описать в виде пять разных форм.
- Словесное описание
- Регулярные выражения
- Диаграмма конечного автомата
- Формальная функция переходов $\delta$
- Код
#### Примеры конечных автоматов
##### Автомат для идентификаторов
В данном случае, идентификатор - буква, а затем произвольное количество букв или цифр. Необходимо проверить, является ли данный идентификатор частью языка. Тогда у нас есть два состояния: q0 и q1.
q0 - это начальное состояние, когда ни одного символа еще не прочитано. Если следующим символом будет прочитана буква, то состояние переходит в q1, иначе строка не идентификатор.
q1 - это принимающее состояние, когда прочитана хотя бы одна буква, и дальше можно продолжать как буквами, так и цифрами. Если следующим будет прочитана либо буква, либо цифра, то состояние переходит снова в q1, иначе строка не идентификатор.
##### Автомат для комментариев
Пусть у лексера есть задача распознать комментарий вида /\* текст \*/. В идеале, этот автомат мог бы состоять из двух состояний, но тут есть ловушка: внутри может быть символ "\*", который не заканчивает комментарий. Поэтому в данном случае необходимо четыре состояния.
Эти состояния можно задать следующим образом.
```
q0: обычный текст
'/' -> q1
иначе -> q0
q1: увидели '/', ждем '*'
'*' -> q2
иначе -> q0
q2: внутри комментария
'*' -> q3
иначе -> q2
q3: увидели '*' внутри, ждем '/'
'/' -> q0 (комментарий закрыт)
'*' -> q3 (еще одна звездочка)
иначе -> q2 (это была не концовка)
```
##### Автомат для регулярного выражения
Пусть нам необходимо прочитать только те строки, которые начинаются с TODO. Для этого используется регулярное выражение ^TODO.
Тогда для реализации автомата нам понадобится четыре состояния, по одному на каждую букву слова TODO. При любом отклонении от шаблона строка сразу выбрасывается.
Тогда можем представить такой автомат.
```
$ grep '^TODO' notes.txt
; автомат для ^TODO (упрощенно)
; q0 --T--> q1 --O--> q2 --D--> q3 --O--> q4 (accept)
; любой другой символ из q0...q3 -> reject
q0: читаем 'T' -> q1, иначе reject
q1: читаем 'O' -> q2, иначе reject
q2: читаем 'D' -> q3, иначе reject
q3: читаем 'O' -> q4 (accept)
```
##### Автомат для четного числа символов "a"
В данном случае, язык - строки над алфавитом {a, b} (то есть строки, состоящие только из символов a и b), где количество символов a - четное.
Тогда для того, чтобы описать этот автомат, можем использовать два состояния: q_even (начальное и принимающее) и q_odd.
Символ a переключает четность, символ b четность не меняет. А значит, будет существовать всего четыре правила переходов.
```
δ(q_even, a) = q_odd
δ(q_even, b) = q_even
δ(q_odd, a) = q_even
δ(q_odd, b) = q_odd
```
Тогда можем закодировать этот автомат на языке C.
```
enum { Q_EVEN, Q_ODD } state = Q_EVEN;
for (const char *p = input; *p; p++) {
if (*p == 'a') {
state = (state == Q_EVEN) ? Q_ODD : Q_EVEN;
}
// символ 'b' состояние не меняет
}
int accepted = (state == Q_EVEN);
```
При таком кодировании автомата, состояние - это переменная или регистр. В главном цикле читается каждый очередной символ и определяется текущее состояние. Далее выполняется switch или if по состоянию и реализация правил переходов напрямую. В конце проверяется нахождение итогового значения во множестве принимающих.
Аналогичным образом можем закодировать этот автомат на ассемблере.
```
; rdi - указатель на строку, rsi - длина
; al = 0 (Q_EVEN) / 1 (Q_ODD)
count_parity:
xor al, al ; state = Q_EVEN
xor rcx, rcx ; i = 0
.loop:
cmp rcx, rsi
jge .done
mov dl, [rdi + rcx]
cmp dl, 'a'
jne .next ; символ b - state не меняем
xor al, 1 ; переключить четность
.next:
inc rcx
jmp .loop
.done:
ret ; al = итоговое состояние
```
В данном случае регистр играет роль переменной state. Условный переход jne/je реализует переход по состоянию. Цикл по строке - это метка + условный переход назад.
@@ -0,0 +1,160 @@
## Содержание
- [[#Введение]]
- [[#Цикл компиляции]]
- [[#Препроцессинг]]
- [[#Однострочные команды препроцессора с аргументами]]
- [[#Команды ветвления и циклы]]
- [[#Многострочные команды препроцессора]]
- [[#Компилятор]]
- [[#Описание интерфейсов]]
- [[#Заголовочные файлы]]
- [[#Компоновщик]]
- [[#Таблица символов]]
- [[#Таблица релокации]]
- [[#Задание зависимостей процесса сборки]]
- [[#Оптимизация процесса сборки]]
- [[#makefile]]
- [[#Phony targets]]
- [[#Декларативность make]]
- [[#Переменные]]
- [[#Автоматические переменные]]
- [[#Шаблоны]]
- [[#Функции]]
## Введение
Как было сказано на [лекциях](Языки%20программирования.md#Лекции), для того, чтобы создать и развивать хоть сколько-нибудь хорошую большую систему, необходимо следовать двум важным принципам: делить систему на модули и абстрагировать эти модули (то есть максимально скрывать их внутреннее устройство).
Исходный код программы после трансляции в машинные инструкции и запуска приложения "встраивается" в вычислительную систему. Когда программа написана в одном большом файле (по факту, монолит), при ее запуске в программу добавляется один большой модуль, который сложно изучать и отслеживать. Если же разделить исходный код на файлы, то запуск программы можно мысленно воспринимать как внесение в систему нескольких небольших модулей. Это существенно облегчает труд разработчиков системы.
## Цикл компиляции
Как было сказано на [лекции](Цикл%20трансляции,%20лексический%20и%20синтаксический%20анализ.md#Этапы), трансляция программы в машинный код происходит в несколько больших этапов (которые, в свою очередь, состоят из множества маленьких).
Первым этапом компиляции программы является преобразование исходного текста программы на ассемблере с директивами макропроцессора в текстовый файл уже без этих директив. Этот этап называется [[#Препроцессинг|препроцессингом]] и выполняется препроцессором - отдельной программой или частью компилятора.
Вторым этапом компиляции программы является преобразование текстового файла на ассемблере в объектный файл (то есть в файл с машинными инструкциями, но без проставленных адресов). Этот этап называется [[#Компилятор|трансляцией]] и выполняется самим компилятором.
Третьим этапом компиляции программы является сборка всех объектных файлов в один исполняемый файл. Этот этап называется [[#Компоновщик|компоновкой]] (или линковкой) и выполняется компоновщиком (или линкером).
В [первом семинаре](Среда%20выполнения.%20Основы%20языка%20ассемблера.md#Компиляция%20ассемблерных%20программ) были вкратце описаны необходимые команды для компиляции одного файла на ассемблере. Тем не менее, подробности команд там не раскрывались. Команда `nasm` выполняет последовательно препроцессинг и компиляцию для одного исходного файла, получая объектный файл. Команда `ld` выполняет компоновку, делая из объектного файла исполняемый.
Если программа состоит из нескольких файлов, то для каждого отдельного файла отдельно выполняется препроцессинг и компиляция (команда `nasm`), а потом все необходимые файлы соединяются в один исполняемый файл с помощью компоновщика (команда `ld`).
### Препроцессинг
Препроцессинг производит в файле текстовые замены, описанные специальными командами - макросами. Это подготавливает программу к компиляции и позволяет полуавтоматически генерировать рутинный код, который иначе пришлось бы иначе писать вручную. В исходном ассемблерном коде макросы обозначаются через знак `%`.
Фактически, после выполнения препроцессинга программа становится окончательно готовой к компиляции. Команда `nasm` позволяет просмотреть результат работы препроцессора, если при компиляции указать ключ `-E` (что на выходе даст код программы после препроцессинга, но еще не скомпилированный).
Одним из самых часто используемых макросов является `%define`, который определяет замену одной строчки на другую. Но у него есть одна особенность: раскрытие значения происходит именно в момент использования макроса, а не в момент создания замены. Это означает, что по ходу выполнения программы замену директивы `%define` можно заменить на другую. Чтобы избежать этого, существует директива `%xdefine`, раскрытие которой происходит в момент объявления замены. Чтобы отменить замену на определенном этапе кода, можно использовать директиву `%undef`.
Для сохранения значения математических выражений препроцессор умеет выполнять простые вычисления и сохранять полученное значение для последующей замены с помощью директивы `%assign`.
#### Однострочные команды препроцессора с аргументами
Команды препроцессора могут использовать аргументы, имена которых записываются в круглых скобках после имени команды.
При этом необходимо не забывать, что препроцессор - это прежде всего текстовая подстановка, а значит ответственность за итоговый результат лежит именно на программисте и на том, правильно ли он реализует подстановку текста из макроса.
#### Команды ветвления и циклы
Помимо вышеперечисленных команд, препроцессор также поддерживает такие директивы, как директива `%if`, директива `%elif`, директива `%else` и завершающую директиву `%endif`. Помимо этого, существуют также различные специализированные версии ветвления, к примеру директива `%ifdef` (проверить, определено ли имя в препроцессоре), директива `%ifmacro` (проверить, что имя определено в препроцесссоре как [[#Многострочные команды препроцессора|многострочный макрос]]), и так далее.
Помимо этого, существует также директива `%rep`, которая позволяет повторить код, написанный после нее, указанное в аргументе количество раз. Чтобы завершить блок повторяемого кода, используется директива `%endrep`.
#### Многострочные команды препроцессора
Помимо однострочных макросов, рассмотренных ранее, существуют также и многострочные макросы. Для их задания используется директива `%macro`, а завершается блок многострочного макроса с использованием директивы `%endmacro`.
Конечно, таким макросам тоже можно передавать аргументы. Количество передаваемых в макрос аргументов указывается после названия макроса. Для доступа к аргументам внутри многострочного макроса используются директивы с соответствующим номером необходимого аргумента (к примеру, `%1` для первого аргумента, `%2` для второго и так далее).
Внутри многострочного макроса можно использовать локальные имена задаваемых значений, дописав им в начало символы `%%`. Такое задание переменных помогает гарантировать, что можно задавать одну и ту же переменную в разных макросах, и пересечений имен не будет. Чаще всего такую конструкцию используют для задания имен локальных меток, но можно использовать и по-другому.
Количество аргументов также можно указать в виде диапазона через символ '-'. Это значит, что макрос может принимать любое количество аргументов, указанных в этом диапазоне, включая крайние значения.
Помимо этого, у многострочного макроса может быть неопределенное значение аргументов. Одним из способов задания неопределенного количества аргументов является "жадный" аргумент '+', который все аргументы, превышающие заданное количество, присоединяет к последнему аргументу через запятую.
Другим способом задать неопределенное количество значений является аргумент '\*'. В отличие от предыдущего "жадного" аргумента, этот просто сохранит все заданные значения как разные аргументы, и указывается через диапазон. Для того, чтобы узнать количество переданных аргументов, можно использовать `%0`. Также можно вращать все аргументы, меняя их местами по кругу. Для этого используется директива `%rotate`.
### Компилятор
Задача компилятора - перевести исходный код в набор исполняемых инструкций. В случае с языком ассемблера, этот процесс достаточно прозрачен: каждой мнемонике ассемблера соответствует одна машинная инструкция.
Для программ из нескольких файлов-модулей, компилируемых по-отдельности, возникает необходимость явно описывать, какие именно внешние функции или данные нужно добавить в каждый отдельный файл. Для этого используются директива `global`, которая устанавливает возможность экспортировать функцию или данные в другие файлы, и директива `extern`, которая устанавливает, какие именно функции или данные необходимо импортировать в файл.
## Описание интерфейсов
Иногда возникают ситуации, когда необходимо из одного файла перенести определенное число функций в другой. Это легко сделать, когда функций всего единицы. А что если десятки? Если сотни? А если набор функций изменится, то неужели все это придется пересматривать и переписывать? На все эти вопросы существуют относительно простые решения, описанные ниже.
### Заголовочные файлы
Наиболее продуктивным подходом к разработке библиотек с большим количеством экспортируемых функций заключается в том, чтобы создавать для них заголовочные файлы, в которых будет указываться, какие именно функции необходимо импортировать из этой библиотеки. В ассемблере такой специальный файл использует расширение `.inc`.
Тогда с использованием препроцессорной директивы `%include` (которая копирует содержимое указанного после нее файла и заменяет себя этим содержимым) можно будет просто включить в программу этот заголовочный файл, и все функции из данной библиотеки будут успешно импортированы.
Фактически, один единственный заголовочный файл почти полностью описывает интерфейс взаимодействия с модулем библиотеки. Для того, чтобы получить полный таковой интерфейс, достаточно лишь учесть все метки, помеченные директивой `global` из самого исходного файла библиотеки.
Как говорилось [ранее](Среда%20выполнения.%20Основы%20языка%20ассемблера#Секции,%20метки%20и%20комментарии), метку `_start` тоже необходимо помечать с помощью директивы `global`. Это важно, так как при составлении исполняемого файла компоновщик должен найти эту метку и поставить на нее адрес входа в программу. Если же метка окажется локально (не `global`), то компоновщик просто не сможет экспортировать ее, а значит не будет знать, что она существует.
### Компоновщик
Компоновщик работает с объектными файлами (по факту, полуфабрикатами) и собирает их в исполняемый файл, параллельно заполняя все необходимые адреса.
В данном курсе рассматриваются объектные файлы формата ELF. Помимо простого исполняемого кода, они также содержат и большое количество мета-информации. К примеру, какие строчки исходной программы на языке высокого уровня соответствуют каким ассемблерным инструкциям, какие функции начинаются с каких адресов, где находятся глобальные переменные, какие данные изменяемые, какие данные должны быть защищены от перезаписи, по какому адресу программа загружается в память, и так далее.
#### Таблица символов
Информация про ассемблерные метки после компиляции аккумулируется в таблице символов (до этого про нее упоминалось в [лекции](Цикл%20трансляции,%20лексический%20и%20синтаксический%20анализ.md#Таблицы)) и становится частью ELF-файла. Компоновщик использует таблицу символов для того, чтобы связать обращение к адресам и определения меток. С помощью утилиты `nm` можно посмотреть содержание таблицы символов объектного файла (помимо этого, там также указывается тип метки для каждой метки таблицы символов).
Из объектного файла можно также достать и инструкции ассемблерного кода. Для этого можно использовать, к примеру, утилиту `objdump`, тогда команда будет выглядеть примерно как `objdump -M intel-mnemonic -d file.o`. Важно помнить, что такой код еще не компонован, а значит адреса меток там не проставлены, то есть установлены в ноль.
В таблицу символов попадает и другая служебная информация. Исполняемые файлы тоже имеют формат ELF, только в них больше мета-информации.
#### Таблица релокации
Также в мета-информации файла ELF существуют и другие данные. Одной из частей этих данных также является и таблица релокации (она также упоминалась ранее в [лекции](Цикл%20трансляции,%20лексический%20и%20синтаксический%20анализ.md#Таблицы)). Она служит для того, чтобы компоновщик знал, куда и какие необходимо подставить адреса, чтобы получить исполняемый файл.
## Задание зависимостей процесса сборки
Как было описано [[#Описание интерфейсов|ранее]] (и [[#Цикл компиляции|ранее]]), процесс получения исполняемых файлов и исходников является очень трудоемким. Но что происходит в по-настоящему больших системах, где различные файлы могут быть написаны на разных языках, а весь процесс сборки может занимать по несколько часов?
Тогда хочется, чтобы пересобирались только необходимые и измененные файлы. Иными словами, необходимо средство, которое смогло бы контролировать процесс пересборки и пересобирать именно то, что нужно, а не все подряд.
Самым распространенным, хотя и не особо хорошо спроектированным и не самым современным, является инструмент для организации процесса сборки make.
### Оптимизация процесса сборки
Для оптимизации процесса сборки ассемблерных файлов можно придерживаться стандартных правил и здравого смысла. Нужно просто пересобирать только те файлы, которые были изменены, а также все зависящие от них файлы, вплоть до интересующего главного файла. Остальные же файлы можно не перекомпилировать, ведь в них все равно ничего не изменилось, а значит и в скомпилированных файлах тоже ничего не изменится (кроме, конечно, времени компиляции).
То есть, если процесс пересборки необходимо оптимизировать, то надо стороннюю программу заставить просчитывать оптимальную последовательность команд для пересборки проекта именно в соответствии с этими принципами.
### makefile
Программа make отслеживает взаимосвязи между файлами. Если изменяется какой-либо один файл, то необходимо изменить и другой, от него зависящий. Она может просчитывать транзитивные зависимости, а значит таким образом в конечном итоге пересоберет все файлы, связанные с измененным каким-либо образом.
Зависимость одного файла от другого программа make берет из графа зависимостей. Сам этот граф зависимостей строится по специальному конфигу, указанному в проекте - файлу, который называется makefile. Про синтаксис такого конфига упоминалось в [лекции](Цикл%20трансляции,%20лексический%20и%20синтаксический%20анализ.md#Утилита%20make).
В контексте make существуют специальные понятия. Правило (rule) - это то, что нужно делать для того, чтобы пересобрать файл, и когда его нужно пересобирать. Цель (target) - это тот файл, который нужно пересобрать. Зависимости (prerequisites) - это те файлы, которые необходимы для сборки цели.
#### Phony targets
Помимо целей, которые создаются в виде файла, также существуют цели, которые итоговый файл не создают. Такие цели называются phony targets. Они могут служить для различных дополнительных задач в процессе сборки, к примеру удаления временных файлов, которые больше не нужны.
Тем не менее, если такую цель указать как обычную цель в makefile, то она будет правильно работать лишь до того момента, пока в проекте не существует файла с таким же названием, как и у такой цели. Поэтому чтобы не путать phony targets с обычными целями, они указывается после специального ключевого слова `.PHONY`.
#### Декларативность make
С точки зрения программирования, make является декларативным языком. То есть в нем описывается лишь набор ограничений (необходимые зависимости), а он уже сам по измененным файлам и графу зависимостей выбирает, какие и в каком порядке пересобирать файлы.
Также при сборке можно использовать ключ `-j`, чтобы запустить сборку в несколько потоков. Тогда вся сборка (все правила) будет выполнятся в рандомном порядке, но зависимости будут все также идти одна за другой (будут синхронизироваться между собой).
По умолчанию (при сборке с помощью команды `make`) собираться будет первая указанная в файле makefile цель, а также все необходимые для нее зависимости. Также можно явно указать, какую именно цель необходимо собрать, тогда make соберет все зависимости для нее, а также пересоберет все цели, которые зависят от нее.
#### Переменные
В makefile можно задавать переменные. Переменные определяются через знак `=` а используются через конструкцию `$()`, внутри которой пишется нужная переменная.
#### Автоматические переменные
Помимо этого, make также умеет подставлять переменные автоматически там, где это нужно. Для этого используется ряд специальных символов, результат которых зависит от собираемой цели.
- `$@` - цель
- `$<` - первая зависимость
- `$?` - все зависимости, которые новее цели, разделенные запятой и пробелом
- `$^` - все различные зависимости, разделенные запятой и пробелом
- `$+` - все указанные (с повторениями, если так указано) зависимости, разделенные запятой и пробелом
Конечно, это не все автоматические переменные, но самые основные из них, и их хватит для большинства сборок.
#### Шаблоны
Шаблон является еще одним полезным инструментом makefile, который позволяет задать правило для группы файлов, которые собираются одинаковым образом, а не расписывать каждый файл отдельно. Для указания шаблона используется символ `%`.
#### Функции
Также утилита make предоставляет широкий набор функций для работы со строками и файлами. Вызов функции записывается в круглых скобках, а после имени функции указываются передаваемые аргументы, указанные через запятую.
- $(wildcard T) - возвращает список имен файлов, соответствующих шаблону T
- $(subst, X,Y,T) - возвращает строку T, где все вхождения X изменены на Y
- $(patsubst, X,Y,T) - возвращает строку T, где все вхождения шаблона X изменены на шаблон Y
- $(strip T) - возвращает строку T без лишних пробельных символов
- $(shell C) - выполняет команду C и возвращает ее результат
@@ -0,0 +1,272 @@
## Содержание
- [[#Введение]]
- [[#История]]
- [[#Цикл трансляции]]
- [[#Этапы]]
- [[#Таблицы]]
- [[#Компиляция]]
- [[#Типы ошибок компилятора]]
- [[#Проходы]]
- [[#Представление программы]]
- [[#Лексический анализ]]
- [[#Токенизация]]
- [[#Принцип максимального совпадения]]
- [[#Синтаксический анализ]]
- [[#Дерево разбора]]
- [[#Приоритет]]
- [[#Ассоциативность]]
- [[#AST]]
- [[#Табличный метод разбора]]
- [[#Практика]]
- [[#Рекурсивный спуск]]
- [[#Трансляция препроцессора]]
- [[#Утилита make]]
## Введение
Как было описано в [лекции 2](Формальные%20грамматики%20и%20теория%20автоматов.md), [конечные автоматы](Формальные%20грамматики%20и%20теория%20автоматов.md#Конечный%20автомат) не могут разложить код языка программирования. Из-за бесконечной вложенности большинство языков относится к типу 2 по [Иерархии Хомского](Формальные%20грамматики%20и%20теория%20автоматов.md#Иерархия%20Хомского) (контекстно-свободные грамматики).
Тем не менее, именно вложенность является основой любого языка программирования. Поэтому в данном случае необходимы автоматы с магазинной памятью. Но вводить их формально - дело скучное и по большей части, опять же, формальное, потому можно сразу рассмотреть данные автоматы с точки зрения инженерной реализации.
## История
Разбор языка программирования никогда не был простым приложением, созданным на основе теории вычислений. Напротив, разбор языка программирования является одной из первых по-настоящему прикладных задач теории вычислений.
В 1952 году [Грейс Хоппер](https://ru.wikipedia.org/wiki/Хоппер,_Грейс) создал [A-0](https://ru.wikipedia.org/wiki/A-0_System) - первую программу, которая была способна компоновать небольшие заранее подготовленные подпрограммы в одну программу, при этом сопоставляя их с помощью числовых кодов. По факту, данная система положила начало созданию [[#Компиляция|компиляторов]], и является их самым первым прототипом.
В 1957 году в языке [Fortran](https://ru.wikipedia.org/wiki/Фортран), созданном [IBM](https://ru.wikipedia.org/wiki/IBM) под руководством [Джона Бэкуса](https://ru.wikipedia.org/wiki/Бэкус,_Джон), появился первый по-настоящему массовый оптимизирующий компилятор. На данной примере компания IBM смогла показать, насколько большим может быть ускорение программы, прошедшей обработку через компилятор, при этом без изменения смысла кода.
В 1960-ых годах, как упоминалось [ранее](Формальные%20грамматики%20и%20теория%20автоматов.md#Форма%20Бэкуса-Наура), ALGOL 60 стал первым языком программирования, синтаксис которого был формально описан в БНФ. Это важно, ведь до него все спецификации писались словесно, а значит имели неоднозначность.
В 1965 году [Дональд Кнут](https://ru.wikipedia.org/wiki/Кнут,_Дональд_Эрвин) смог полностью формализовать [[#Табличный метод разбора|LR-парсинг]] (также известный как табличный метод разбора программы). По факту, таким образом была организована теоретическая база, на основе которой были построены все последующие быстрые [[#Дерево разбора|парсеры]].
В 1975 году [Bell Labs](https://ru.wikipedia.org/wiki/Лаборатории_Белла) смогла создать первые массовые генераторы [[#Токенизация|лексеров]] и [[#Дерево разбора|парсеров]], которые назывались lex (первый создатель [[#Токенизация|лексеров]]) и yacc (создатель табличных [[#Табличный метод разбора|LR-парсеров]]). По факту, это была первая по-настоящему формализованная инженерная реализация полноценной системы, которую называют компилятором.
## Цикл трансляции
Цикл трансляции является необходимым процессом, через который проходит любая программа на компилируемом языке программирования перед тем, как будет запущена. Он позволяет программу из простого текста с кодом превратить в по-настоящему исполняемый набор машинных команд, собранных в один исполняемый файл.
### Этапы
Существует всего четыре общих этапа транслирования программы в машинный код. В современных [[#Компиляция|компиляторах]] каждый такой этап можно выполнять по отдельности и анализировать, какие изменения с кодом происходят на каждом из этапов.
Первый этап называется **препроцессингом**. Он применяется напрямую к исходному коду программы и служит для того, чтобы выполнить все макроподстановки (да и вообще любые подстановки) в коде. Подробнее об этих подстановках описано в [[#Трансляция препроцессора|примере]]. В конце данного этапа исходный текст программы становится текстом после макроподстановки.
Второй этап называется **компиляцией**. Он применяется к полученном на первом этапе тексту программы после макроподстановки. Подробнее об этом этапе написано [[#Компиляция|ниже]]. В конце данного этапа текст программы превращается в полноценный ассемблерный код (который, в свою очередь, зависит от аппаратуры, на которой будет запускаться программа).
Третий и четвертый этапы называются **ассемблированием** и **компоновкой** (или линковкой) соответственно. Данные этапы были рассмотрены [ранее](Среда%20выполнения.%20Основы%20языка%20ассемблера.md#Компиляция%20ассемблерных%20программ) в одном из предыдущих семинаров.
### Таблицы
Во время цикла трансляции при [[#Компиляция|компиляции]] создаются вспомогательные таблицы с данными, используемые компилятором для сопоставления различных имен переменных с настоящими адресами памяти. Одними из таких таблиц являются таблицы символов и таблицы релокации, без которых современная компиляция практически невозможна.
>**Таблица символов** - это таблица, сопоставляющая различные имена в коде (функций, переменных) с их настоящими адресами или атрибутами внутри объектного файла. В данном случае имена, сопоставляющиеся таблицей, называют символами.
Тем не менее, даже с использованием таблицы символов, остается важная проблема. Как сопоставить символ в одном файле, который во время компиляции еще не известен и будет объявлен только в другом файле, с его настоящим адресом в памяти? Чтобы решить эту конкретную проблему, существует таблица релокации.
>**Таблица релокации** - это список мест в [объектном файле](Среда%20выполнения.%20Основы%20языка%20ассемблера.md#Компиляция%20ассемблерных%20программ), куда компоновщик должен поставить настоящий адрес вместо заглушки после объединения всех компилируемых файлов.
Но тут возникает еще одна проблема: откуда знать, что этот символ действительно будет объявлен в другом файле? Для того, чтобы предотвратить такое поведение, существуют заголовочные файлы. По факту, заголовочный файл является контрактом между единицами трансляции, который гарантирует существование доступных символов, но пока не дает их фактических адресов или реализации.
Именно благодаря таблицам символов и таблицам релокации возможна независимая компиляция несколько отдельных файлов кода, ведь компиляция каждого отдельного файла не ждет компиляции всех остальных. Окончательное соединение всех адресов и символов происходит только на этапе компановки.
### Компиляция
>**Компилятор** - это специальная программа, которая выполняет процесс преобразования программы из кода напрямую в машинные инструкции.
Полноценный процесс компиляции состоит в первую очередь из [[#Лексический анализ|лексического]] и [[#Синтаксический анализ|синтаксического]] анализа.
#### Типы ошибок компилятора
Все ошибки компилятора можно разделить на три вида: лексические, синтаксические и сематические, что очень напоминает [уровни формальных грамматик](Формальные%20грамматики%20и%20теория%20автоматов.md#Основы).
Лексические ошибки находятся [[#Токенизация|лексером]] при выполнении лексического анализа программы. Синтаксические ошибки находятся [[#Дерево разбора|парсером]] при выполнении синтаксического анализа программы. Семантические ошибки находятся уже после парсинга программы с помощью проверки типов, когда компилятор сверяет типы по обе стороны присваивания (подробнее в лекции 5).
#### Проходы
Также компиляторы делятся по количеству используемых проходов для компилирования программы. Существуют однопроходные и многопроходные компиляторы.
>**Однопроходный компилятор** - это компилятор, который читает исходный текст программы ровно один раз, и сразу же после этого генерирует итоговый исполняемый код.
>**Многопроходный компилятор** - это компилятор, который по исходному тексту программы строит промежуточное представление (к примеру, [[#AST]]), и проходит по нему несколько раз для разных задач.
Изначально, однопроходные компиляторы были эффективнее на старых компьютерах, ведь там приходилось гнаться за каждой возможной оптимизацией, даже если она доставляла некоторые неудобства. Поэтому однопроходные компиляторы являются именно исторической реализацией, и все еще используются.
Очевидно, у однопроходных компиляторов есть несколько проблем. Основной среди них является то, что для функции, объявленной ниже по файлу программы, требуется знать ее структуру именно сейчас, а не когда она уже будет объявлена.
В C, который стандартно использует однопроходный компилятор, данная проблема решена лишь частично. Поэтому существует требование объявлять функцию заранее, до ее первого использования (те же [[#Таблицы|заголовочные файлы]]).
Современные компиляторы, такие как Clang или GCC, в основном многопроходные. Они имеют отдельные проходы для [[#Синтаксический анализ|разбора]], проверки типов, оптимизации и генерации кода.
### Представление программы
Во время цикла трансляции программы из исходного кода в исполняемый файл программа проходит полноценный путь, приобретая несколько видов представлений самой себя. Всего таких представлений существует пять.
В самом начале программа является исходным текстом, написанным программистом. После этого программа проходит [[#Лексический анализ|лексический анализ]], превращаясь в [[#Токенизация|поток токенов]]. Далее происходит [[#Синтаксический анализ|синтаксический анализ]] программы, превращая поток токенов в полное [[#Дерево разбора|дерево разбора]], оставляя все использованные правила грамматики. После этого она превращается в [[#AST]], убирая служебные узлы и оставляя только смысловую структуру. Наконец, происходит генерация кода на основе AST, что окончательно и превращает программу в машинный код.
## Лексический анализ
Одной из функций [[#Компиляция|компилятора]] является проведение лексического анализа: проверка текста программы на соблюдение всех лексических правил языка.
### Токенизация
Одним из способов проверки соблюдения лексических правил языка является токенизация всего текста программы.
>**Токенизация** - разбиение потока символов на поток токенов.
Иными словами, каждое ключевое слово языка, каждый идентификатор, каждый литерал, каждый оператор, каждый разделитель (по факту все, что подсвечивается в редакторе кода) превращается в набор токенов, и вся остальная работа с программой происходит уже по токенам, а не с самим текстом.
>**Лексер** - специальная программа, которая превращает текст программы в поток токенов, чаще всего при этом отбрасывая все пробелы и комментарии. Фактически является [конечным автоматом](Формальные%20грамматики%20и%20теория%20автоматов.md#Конечный%20автомат).
### Правило максимального совпадения
>**Максимальное совпадение** (maximal munch) - правило лексера, при котором он выбирает самый длинный [[#Токенизация|токен]], который может распознать в данной позиции.
Если проще, то лексер просто жадно поглощает символ за символом, пока идентификатор остается валидным. И только если следующий символ сделает идентификатор окончательно инвалидным, лексер останавливается и начинает процесс поглощения идентификатора заново.
Это правило чаще всего не слишком очевидно, поэтому из-за него иногда возникают небольшие проблемы. И это несмотря на то, что такое поведение является следствием фактической цели и принципа работы лексера: он не знает о существовании [[#Синтаксический анализ|синтаксиса языка]] и о смыслах синтаксических конструкций, он просто локально, символ за символом, решает, что является валидным, а что нет.
## Синтаксический анализ
Одной из функций [[#Компиляция|компилятора]] является проведение синтаксического анализа: проверка текста программы на соблюдение всех синтаксических правил языка.
### Дерево разбора
Главным методом проверки кода на соблюдение синтаксических правил является построение полно дерева разбора.
>**Парсер** - это специальная программа, которая на вход получает [[#Токенизация|поток токенов]], созданный лексером, и [грамматику языка](Формальные%20грамматики%20и%20теория%20автоматов.md#Формальные%20грамматики), а на выходе получает полное дерево разбора.
>**Дерево разбора** - это структура, которая отражает, как какая-либо строка (по которой это дерево было построено) может быть выведена по заданной грамматике.
Самым интуитивным методом разбора кода программы является рекурсивный спуск. Он же является самым распространенным.
Основная суть такого метода заключается в том, что по каждому нетерминалу грамматики строится одна функция, внутри которой для каждого полученного в ней нетерминала также строится по одной функции, и так далее, пока не останутся только терминалы. Отсюда данный метод и получил свое название - рекурсия по всем нетерминалам граматики.
Данный метод рекурсивного спуска хорошо отражает вложенность языка. В то же время фактически данный метод можно рассматривать как [автомат с магазинной памятью](Формальные%20грамматики%20и%20теория%20автоматов.md#Иерархия%20Хомского), а стек вызовов - именно как ту память, которой не хватает конечному автомату для реализации такого метода.
#### Приоритет
Приоритет в синтаксическом анализе программы закладывается через уровни вложенности грамматики, а не через специальные правила [[#Дерево разбора|парсера]].
К примеру, в [[#Рекурсивный спуск|примере]] разбор выражения с операторами '+' и '-' (`parse_expr`) вызывает разбор выражения с операторами '\*' и '/' (`parse_term`). Отсюда получается, что связь '\*' и '/' теснее, чем между '+' и '-'. Как итог, получаем, что в данном случае парсер ведет себя "жаднее" с символами '\*' и '/' и по возможности "съедает" их полностью, прежде чем вернуться на предыдущий уровень.
#### Ассоциативность
Иногда при разборе выражения можно обойтись и без рекурсии. К примеру, если все возможные символы не будут менять уровень вложенности [[#Дерево разбора|парсера]], то тогда можно будет обойтись только один циклом внутри одного уровня, без рекурсии. Такая ситуация называется **левой ассоциативностью**.
Тем не менее, в некоторых случаях требуется именно рекурсия, и цикла будет недостаточно. Тогда такая ситуация называется **правой ассоциативностью**.
#### AST
>**Абстрактное синтаксическое дерево** (AST - Abstract Sintax Tree) - это вид синтаксического дерева, который при разборе отбрасывает всю ненужную синтаксическую упаковку и оставляет только смысловую структуру разбираемой программы.
AST и [[#Дерево разбора|дерево разбора]], хоть и выполняют примерно похожую функцию, имеют ряд принципиальных различий. Само их предназначение различается: дерево разбора создается для того, чтобы формально вывести строку по правилам [грамматики](Формальные%20грамматики%20и%20теория%20автоматов.md#Формальные%20грамматики), в то время как AST составляет дерево для дальнейшей работы (интерпретация, генерация кода), а значит больше ориентировано на практическую ценность.
Дерево разбора изначально призвано при разборе программы отразить каждое установленное правило грамматики, с учетом служебных символов, поэтому при разборе оно сохраняет все скобки (в виде отдельных узлов-токенов), ключевые слова-разделители (к примеру, точки с запятой) и подобные символы. AST же служит более практическим целям, поэтому все, что не имеет смысловой нагрузки, либо безжалостно отбрасывается за ненадобностью (как в случае с ключевыми словами-разделителями), либо учитывается при построении самого дерева (к примеру, скобки).
### Табличный метод разбора
Метод [[#Дерево разбора|рекурсивного спуска]], который был описан при определении дерева разбора и [[#Рекурсивный спуск|пример]] которого приведен ниже, не является единственным методом разбора программы в дерево. Помимо него также применятся и табличный метод.
В отличие от рекурсивного спуска, табличный метод разбирает программу снизу вверх, от листьев к корню, что позволяет ему обрабатывать большее количество [грамматик](Формальные%20грамматики%20и%20теория%20автоматов.md#Формальные%20грамматики) (все таки, рекурсивный спуск может разобрать не любую рекурсию). Помимо этого, табличный метод реализуется проще - он создается автоматически на основе грамматики, а не пишется вручную в виде обычного кода (как в случае с рекурсивным спуском). Для этого он заранее строит таблицы переходов, а после использует их при разборе.
Из-за таких положительных факторов табличного метода его чаще используют в промышленных компиляторах (к примеру, [yacc](https://ru.wikipedia.org/wiki/Yacc) или [bison](https://ru.wikipedia.org/wiki/GNU_Bison)), в то время как рекурсивный спуск можно использовать на каких-нибудь учебных компиляторах или простых языках программирования.
## Практика
### Рекурсивный спуск
Предположим, что у нас есть выражение "1 + 2 * 3", которое необходимо разобрать, используя метод рекурсивного спуска.
Для начала зададим грамматику.
```
expr ::= term (('+' | '-') term)*
term ::= factor (('*' | '/') factor)*
factor ::= NUMBER | '(' expr ')'
```
Далее образно распишем работу лексера для выражения "1 + 2 * 3".
```
parse_expr()
parse_term()
parse_factor() -> 1
// читает 1, далее нет '*' или '/', а значит term готов
left = 1 -> полученный узел "1"
// далее '+', значит expr продолжается
parse_term()
parse_factor() -> 2
// читает 2, далее идет '*', значит term прододжается
parse_factor() -> 3
// конец выражения, значит term готов
left = 2 * 3 -> полученный узел "2*3"
// expr закончился, значит получаем готовый узел
left = 1 + (2 * 3)
// токенов больше нет, а значит полученное выражение и есть итоговое дерево
```
Опишем все более подробно. `parse_expr()` последовательно разбивает полученную строку на узлы, и только в конце соединяет их в итоговое выражение. Иными словами, он не воспринимает те же символы "2, \*, 3" отдельно друг от друга, он их даже не читает. Вместо этого он получает от `parse_term` сразу готовый узел (2 \* 3) и подсоединяет его к строящемуся дереву.
Тот же разбор математического выражения можно расписать на языке C.
```c
Node *parse_expr(Lexer *lx) {
Node *left = parse_term(lx);
while (peek(lx) == '+' || peek(lx) == '-') {
char op = next(lx);
Node *right = parse_term(lx);
left = make_binary(op, left, right)
}
return left;
}
Node *parse_term(Lexer *lx) {
Node *left = parse_factor(lx);
while (peek(lx) == '*' || peek(lx) == '/') {
char op = next(lx);
Node *right = parse_factor(lx);
left = make_binary(op, left, right)
}
return left;
}
Node *parse_factor(Lexer *lx) {
if (peek(lx) == '(') {
next(lx); // съесть '('
Node *inner = parse_expr(lx); // рекурсия!
expect(lx, ')');
return inner;
}
return make_number(next_number(lx));
}
```
### Трансляция препроцессора
[[#Этапы|Препроцессор]] в языке C работает до [[#Лексический анализ|лексического]] или [[#Синтаксический анализ|синтаксического]] анализа программы. По факту, это просто подстановка текста в некоторые заранее определенные места программы, не более.
Поэтому макросы - не часть [грамматики](Формальные%20грамматики%20и%20теория%20автоматов.md#Формальные%20грамматики) языка C, а просто слой поверх нее. Именно поэтому макрос может породить [[#Типы ошибок компилятора|синтаксически некорректный]] C-код, а препроцессор об этом даже не узнает.
Для реализации таких макроподстановок используются специальные директивы, включая `#include`, `#define`, `#ifdef`, `#ifndef`.
Примером такой ошибки препроцессора может служить следующий код.
```c
#define SQUARE(x) x * x
int a = SQUARE(2 + 3)
... // производится подстановка
int a = 2 + 3 * 2 + 3 // = 11, а не 25
```
Поэтому при использовании макросов рекомендуется использовать защиту.
```c
#define SQUARE(x) ((x) * (x))
int a = SQUARE(2 + 3)
... // производится подстановка
int a = ((2 + 3) * (2 + 3)) // а вот это уже 25
```
### Утилита make
Утилита make является одной из главных систем сборки файлов. Чаще всего она используется для получения какого-либо исполняемого файла или промежуточного объектного файла.
Makefile описывает граф зависимостей, от которых зависит собираемый файл. Таким образом, ему легко видеть, от каких файлов зависит собираемая цель.
При пересборке цели make не выполняет всю пересборку целиком, а пересобирает только те файлы, хотя бы одна зависимость которых новее самих файлов. Это критически важно для больших систем, где одна цель может полностью пересобираться по несколько часов, а такой образ работы make сильно ускоряет этот процесс.
У make существует специальный синтаксис, который позволяет точно описать, какие зависимости нужны файлу и как их собирать. Для этого используются различные специальные символы (к примеру, $@ - имя цели, $< - первая зависимость, $^ - все зависимости), а также шаблонные правила (%), которые избавляют от повторения одного и того же правила для каждого файла.
Ниже представлен пример сборки некоторого исполняемого файла с помощью компилятора gcc, использующий все вышеперечисленные правила.
```
CC = gcc
CFLAGS = -Wall -std=c17
hello: hello.o string.o
$(CC) -o $@ $^
%.o: %.c
$(CC) -c $(CFLAGS) -o $@ $<
clean:
rm -f hello *.o
.PHONY: clean
```
@@ -1 +1,27 @@
*К сожалению, тут пока ничего нет...*
## Лекции
- [Парадигмы и машинная модель вычислений (неполная)](Парадигмы%20и%20машинная%20модель%20вычислений.md)
- [Формальные грамматики и теория автоматов](Формальные%20грамматики%20и%20теория%20автоматов.md)
- [Цикл трансляции, лексический и синтаксический анализ](Цикл%20трансляции,%20лексический%20и%20синтаксический%20анализ.md)
- [Память и модели вычислений](Память%20и%20модели%20вычислений.md)
- (и тд)
## Семинары
- [Среда выполнения. Основы языка ассемблера](Среда%20выполнения.%20Основы%20языка%20ассемблера.md)
- [Вызов функций. Использование стека. Конечные автоматы](Вызов%20функций.%20Использование%20стека.%20Конечные%20автоматы.md)
- [Цикл компиляции, препроцессор, утилита make](Цикл%20компиляции,%20препроцессор,%20утилита%20make.md)
- (и тд)
## Ссылки
- [Гитлаб](https://gitlab.se.ifmo.ru/programming-languages/cse-programming-languages-fall-2026/main)
## Доп. материалы
- Заметки - [Библиотека команд ассемблера](Библиотека%20команд%20ассемблера.md)
- Книга - [Engineering a Compiler](http://www.r-5.org/files/books/computers/compilers/writing/Keith_Cooper_Linda_Torczon-Engineering_a_Compiler-EN.pdf) - Keith D. Cooper
- Книга - [Введение в теорию автоматов, языков и вычислений](https://djvu.online/file/iPQzGw3Q4wPsJ) - Хопкрофт, Мотвани, Ульман
- Книга - [Introduction to the Theory of Computation](https://broman.dev/download/Introduction%20to%20the%20Theory%20of%20Computation%203rd%20Edition.pdf) - Sipser
- Книга - [Компиляторы: принципы, технологии и инструментарий ("Книга дракона")](https://djvu.online/file/1rx8rq5S0mBNx) - Aho, Lam, Sethi, Ullman
- Книга - [Computer Systems: A Programmer's Perspective](https://csharpcooking.github.io/theory/Computer-Systems-A-Programmer-Perspective-En.pdf) - Bryant, O'Hallaron
- Учебник - [Low-level programming](https://picloud.pw/media/resources/posts/2018/02/20/Low-Level_Programming.pdf) - Igor Zhirkov
- Доки - [Intel® 64 and IA-32 Architectures Software Developer’s Manuals](https://software.intel.com/content/www/us/en/develop/download/intel-64-and-ia-32-architectures-sdm-combined-volumes-1-2a-2b-2c-2d-3a-3b-3c-3d-and-4.html) - Intel
- Доки - [Debugging with GDB](https://sourceware.org/gdb/current/onlinedocs/gdb.html/index.html) - GNU
- Доки - [System V Application Binary Interface](https://gitlab.com/x86-psABIs/x86-64-ABI) - x86_64
+3
View File
@@ -0,0 +1,3 @@
# Основная информация
- [2 курс](2%20курс/2%20курс.md)
-3
View File
@@ -1,3 +0,0 @@
# Основная информация
- [2 курс](2%20курс/2%20курс%20ИТМО.md)