Compare commits
9 Commits
365256ed47
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 89d9b70b79 | |||
| 383d5c1525 | |||
| 10540b1f7d | |||
| eb374cf32a | |||
| 8d0960ee7f | |||
| 442c8d45a6 | |||
| 6e2835d301 | |||
| b87a5d51cf | |||
| 2c3a4cdbf5 |
+2
-1
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
- **[Архитектура компьютера](Архитектура%20компьютера/Архитектура%20компьютера.md)**
|
- **[Архитектура компьютера](Архитектура%20компьютера/Архитектура%20компьютера.md)**
|
||||||
- **[Веб-программирование](Веб-программирование/Веб-программирование.md)**
|
- **[Веб-программирование](Веб-программирование/Веб-программирование.md)**
|
||||||
- **[Языки программирования](Языки%20программирования/Языки%20программирования.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/1WvS1ad9J6jXNwSxTRnQs4yGgUBlwFzopUXUiYbGCZMw/edit?usp=sharing)
|
||||||
- [Веб-программирование](https://docs.google.com/spreadsheets/d/1A2XNYpN1Q1rr5-mpkAk5g0BP4NDY-cwRWtBudLz6Xis/edit?gid=696774553#gid=696774553)
|
- [Веб-программирование](https://docs.google.com/spreadsheets/d/1A2XNYpN1Q1rr5-mpkAk5g0BP4NDY-cwRWtBudLz6Xis/edit?gid=696774553#gid=696774553)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
- [[#HTML-формы]]
|
- [[#HTML-формы]]
|
||||||
- [[#HTML5]]
|
- [[#HTML5]]
|
||||||
- [[#Браузер]]
|
- [[#Браузер]]
|
||||||
- [[#DOM]]
|
- [[#Document Object Model]]
|
||||||
- [[#Стилизация страниц. Основы CSS]]
|
- [[#Стилизация страниц. Основы CSS]]
|
||||||
- [[#CSS]]
|
- [[#CSS]]
|
||||||
- [[#Источники CSS]]
|
- [[#Источники CSS]]
|
||||||
@@ -26,13 +26,39 @@
|
|||||||
- [[#LESS]]
|
- [[#LESS]]
|
||||||
- [[#Sass]]
|
- [[#Sass]]
|
||||||
- [[#SCSS]]
|
- [[#SCSS]]
|
||||||
|
- [[#Динамические сценарии на стороне клиента. Язык JavaScript]]
|
||||||
|
- [[#JavaScript]]
|
||||||
|
- [[#Структура языка]]
|
||||||
|
- [[#ECMAScript]]
|
||||||
|
- [[#BOM]]
|
||||||
|
- [[#DOM]]
|
||||||
|
- [[#Отличия ES5 от ES6]]
|
||||||
|
- [[#Видимость переменных]]
|
||||||
|
- [[#Вставка переменной в строку]]
|
||||||
|
- [[#Деструктуризация]]
|
||||||
|
- [[#Классы и объекты]]
|
||||||
|
- [[#Наследование]]
|
||||||
|
- [[#Промисы]]
|
||||||
|
- [[#Стрелочные функции]]
|
||||||
|
- [[#Цикл с итератором]]
|
||||||
|
- [[#Параметры по умолчанию]]
|
||||||
|
- [[#Rest-параметр]]
|
||||||
|
- [[#Операция spread]]
|
||||||
|
- [[#Асинхронное клиент-серверное взаимодействие. DHTML и AJAX]]
|
||||||
|
- [[#AJAX]]
|
||||||
|
- [[#DHTML]]
|
||||||
|
- [[#XMLHttpRequest]]
|
||||||
|
- [[#jQuery]]
|
||||||
|
- [[#SuperAgent]]
|
||||||
|
- [[#Fetch API]]
|
||||||
|
- [[#WebSocket]]
|
||||||
## Введение
|
## Введение
|
||||||
|
|
||||||
В основном все держится на 4 вещах:
|
В основном все держится на 4 вещах:
|
||||||
- [[#Клиент-серверное взаимодействие. Протокол HTTP|Hypertext Transfer Protocol (HTTP)]] - предназначен для передачи гипертекста между клиентом и сервером
|
- [[#Клиент-серверное взаимодействие. Протокол HTTP|Hypertext Transfer Protocol (HTTP)]] - предназначен для передачи гипертекста между клиентом и сервером
|
||||||
- [[#Разметка страниц. Язык HTML|Hypertext Markup Language (HTML)]] - язык разметки гипертекста
|
- [[#Разметка страниц. Язык HTML|Hypertext Markup Language (HTML)]] - язык разметки гипертекста
|
||||||
- [[#Стилизация страниц. Основы CSS|Cascade StyleSheets (CSS)]] - язык описания внешнего вида HTML-документов
|
- [[#Стилизация страниц. Основы CSS|Cascade StyleSheets (CSS)]] - язык описания внешнего вида HTML-документов
|
||||||
- JavaScript - язык для написания динамических сценариев, выполняемых на стороне клиента
|
- [[#Динамические сценарии на стороне клиента. Язык JavaScript|JavaScript (JS)]] - язык для написания динамических сценариев, выполняемых на стороне клиента
|
||||||
## Клиент-серверное взаимодействие. Протокол HTTP
|
## Клиент-серверное взаимодействие. Протокол HTTP
|
||||||
|
|
||||||
### HTTP
|
### HTTP
|
||||||
@@ -137,7 +163,7 @@ HTML5 - как следует из названия, пятая версия с
|
|||||||
На самом деле, о браузере можно говорить с разных точек зрения: можно определять его главным образом через отображение документов, а можно смотреть на него как на runtime для JavaScript.
|
На самом деле, о браузере можно говорить с разных точек зрения: можно определять его главным образом через отображение документов, а можно смотреть на него как на runtime для JavaScript.
|
||||||
|
|
||||||
Очень важно не путать браузер с поисковиком. Браузер занимается самой отрисовкой документов и чтении динамических сценариев и стилей. В то время как поисковик просто с помощью умных алгоритмов ищет информацию в интернете по заданному запросу. Как главное доказательство их различия, в настройках браузера можно поставить любой другой поисковик.
|
Очень важно не путать браузер с поисковиком. Браузер занимается самой отрисовкой документов и чтении динамических сценариев и стилей. В то время как поисковик просто с помощью умных алгоритмов ищет информацию в интернете по заданному запросу. Как главное доказательство их различия, в настройках браузера можно поставить любой другой поисковик.
|
||||||
### DOM
|
### Document Object Model
|
||||||
|
|
||||||
>**Объектная модель документа** (DOM) - это платформо-независимый интерфейс, позволяющий программам и скриптам получить доступ к содержимому HTML-документа.
|
>**Объектная модель документа** (DOM) - это платформо-независимый интерфейс, позволяющий программам и скриптам получить доступ к содержимому HTML-документа.
|
||||||
|
|
||||||
@@ -196,4 +222,155 @@ CSS представляет собой сложную таблицу стиле
|
|||||||
Синтаксис у Sass принципиально отличается от CSS и больше похож на таковой у языка программирования Ruby.
|
Синтаксис у Sass принципиально отличается от CSS и больше похож на таковой у языка программирования Ruby.
|
||||||
#### SCSS
|
#### SCSS
|
||||||
|
|
||||||
SCSS иногда называют диалектом Sass, только с синтаксисом, похожим на CSS. При этом этот язык наиболее популярен среди надстроек для CSS.
|
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, который делает запросы очень часто.
|
||||||
@@ -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}$.
|
||||||
@@ -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.
|
||||||
|
#### Примечания
|
||||||
|
Оба операнда не могут одновременно быть ячейками памяти. Часто данную команду используют для обнуления указанного дважды регистра.
|
||||||
+282
@@ -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`.
|
||||||
@@ -91,6 +91,8 @@
|
|||||||
Для компиляции на первом этапе используется команда `nasm` (Netwide Assembler). В [[#Простейшая программа|простейшей программе]] далее при компиляции используются флаги `-g`, `-felf64` и `-o`. Флаг `-g` выводит debug-информацию о процессе компиляции. Флаг `-f` устанавливает формат итогового файла, в данном случае `elf64`, что является стандартным вариантом для большинства Unix-подобных систем на архитектуре `x86-64`. Флаг `-o` указывает, в какой файл записать вывод программы.
|
Для компиляции на первом этапе используется команда `nasm` (Netwide Assembler). В [[#Простейшая программа|простейшей программе]] далее при компиляции используются флаги `-g`, `-felf64` и `-o`. Флаг `-g` выводит debug-информацию о процессе компиляции. Флаг `-f` устанавливает формат итогового файла, в данном случае `elf64`, что является стандартным вариантом для большинства Unix-подобных систем на архитектуре `x86-64`. Флаг `-o` указывает, в какой файл записать вывод программы.
|
||||||
|
|
||||||
Для компоновки на втором этапе используется команда ld (стандартный линкер для GNU). В [[#Простейшая программа|простейшем примере]] далее при компиляции используется флаг `-o`. Как и в описании флагов для компилятора, данный флаг записывает результат работы программы в указанный файл.
|
Для компоновки на втором этапе используется команда ld (стандартный линкер для GNU). В [[#Простейшая программа|простейшем примере]] далее при компиляции используется флаг `-o`. Как и в описании флагов для компилятора, данный флаг записывает результат работы программы в указанный файл.
|
||||||
|
|
||||||
|
Более подробно этот процесс и подробности выполняемых команд описаны в [семинаре 3](Цикл%20компиляции,%20препроцессор,%20утилита%20make.md#Цикл%20компиляции).
|
||||||
### Простейшая программа
|
### Простейшая программа
|
||||||
|
|
||||||
Далее подробно рассмотрим простейшую программу на языке ассемблера, которая выводит "hello, world!" в стандартный поток вывода.
|
Далее подробно рассмотрим простейшую программу на языке ассемблера, которая выводит "hello, world!" в стандартный поток вывода.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
- [[#История]]
|
- [[#История]]
|
||||||
- [[#Иерархия Хомского]]
|
- [[#Иерархия Хомского]]
|
||||||
- [[#Конечный автомат]]
|
- [[#Конечный автомат]]
|
||||||
- [#ДКА и НКА]]
|
- [[#ДКА и НКА]]
|
||||||
- [[#Регулярные выражения и автоматы]]
|
- [[#Регулярные выражения и автоматы]]
|
||||||
- [[#Построение Томпсона]]
|
- [[#Построение Томпсона]]
|
||||||
- [[#Минимизация]]
|
- [[#Минимизация]]
|
||||||
|
|||||||
@@ -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 и возвращает ее результат
|
||||||
+272
@@ -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,19 +1,27 @@
|
|||||||
## Лекции
|
## Лекции
|
||||||
- [Парадигмы и машинная модель вычислений (неполная)](Парадигмы%20и%20машинная%20модель%20вычислений.md)
|
- [Парадигмы и машинная модель вычислений (неполная)](Парадигмы%20и%20машинная%20модель%20вычислений.md)
|
||||||
- [Формальные грамматики и теория автоматов](Формальные%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ассемблера.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)
|
- [Гитлаб](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
|
- Книга - [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) - Хопкрофт, Мотвани, Ульман
|
- Книга - [Введение в теорию автоматов, языков и вычислений](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
|
- Книга - [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
|
- Учебник - [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
|
- Доки - [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
|
- Доки - [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
|
||||||
Reference in New Issue
Block a user