Mov add sub язык программирования

Вам может быть интересно, как работает ваш компьютер: что происходит. Когда вы пишете программу. А затем компилируете ее? Что такое ассемблер и каков основной принцип программирования в нем? Этот учебник должен прояснить это для вас, он не предназначен для того. Чтобы научить вас программированию на ассемблере. А скорее дать вам необходимые основы. Чтобы понять. Что на самом деле происходит под капотом. Это также намеренно упрощает некоторые вещи. Чтобы вы не были перегружены дополнительной информацией. Однако я предполагаю. Что у вас есть некоторые знания в области программирования высокого уровня (C/C++. Visual Basic. Python, Pascal. Java и многое другое…).

Кроме того, я надеюсь, что более опытные ребята простят меня за то. Что я здесь многое упрощаю. Мое намерение состояло в том. Чтобы сделать объяснение ясным и простым для тех. Кто понятия не имеет об этой теме.

Примечание: Я буду очень благодарен за любую обратную связь по этому поводу. Трудно писать объяснения для людей, которые мало знают об этой теме. Поэтому я, возможно. Пропустил некоторые важные вещи или недостаточно прояснил что-то, поэтому. Если что-то неясно. Не беспокойтесь спрашивать.

Как работает процессор (CPU)?

Возможно, вы знаете. Что Центральный процессор (Центральный процессор или просто процессор) является “мозгом” компьютера. Контролирующим все остальные части компьютера и выполняющим различные вычисления и операции с данными.

Но как он этого добивается?

Процессор-это схема. Которая предназначена для выполнения отдельных инструкций: фактически целой серии из них. Одна за другой. Инструкции. Которые должны быть выполнены. Хранятся в некоторой памяти, в ПК. Это операционная память. Представьте себе память в виде большой сетки ячеек. Каждая ячейка может хранить небольшое число. И каждая ячейка имеет свой собственный уникальный номер – адрес.

Процессор сообщает адрес ячейки памяти. И память отвечает значением (числом. Но оно может представлять что угодно – буквы, графику. Звук… все может быть преобразовано в числовые значения). Хранящиеся в ячейке. Конечно, процессор может сказать памяти. Чтобы сохранить новый номер в данной ячейке, а также.

Сами инструкции тоже в основном числовые: каждой простой операции присваивается свой уникальный цифровой код. Процессор извлекает это число и решает, что делать: например. Число 35 заставит процессор скопировать данные из одной ячейки памяти в другую. Число 48 может сказать ему сложить два числа вместе. А число 12 может сказать ему выполнить простую логическую операцию под названием ИЛИ.

Какие операции назначаются каким номерам, решают инженеры. Проектирующие данный процессор, или. Лучше сказать. Архитектура процессора: они решают. Какие числовые коды будут назначены различным операциям (и, конечно. Они решают другие аспекты процессора. Но сейчас это не имеет значения). Этот набор правил затем называется архитектурой. Таким образом, производители могут создавать различные процессоры. Поддерживающие заданную архитектуру: они могут отличаться по скорости. Энергопотреблению и цене. Но все они понимают одни и те же коды как одни и те же инструкции.

Как только процессор завершает действие, определенное кодом (инструкцией). Он просто запрашивает следующее и повторяет весь процесс. Иногда он также может решить перейти к различным местам в памяти. Например к какой – то подпрограмме (функции) или перейти на несколько ячеек назад к предыдущей инструкции и выполнить ту же последовательность снова- в основном создавая цикл. Последовательность числовых кодов, образующих программу. Называется машинным кодом.

Что такое инструкции и как они используются?

Как я уже упоминал, инструкции-это очень простые задачи. Которые может выполнять процессор. Каждый из которых имеет свой уникальный код. Схема, составляющая процессор, сконструирована таким образом. Чтобы выполнять заданные операции в соответствии с кодами. Которые он загружает из памяти. Числовой код часто называют кодом операции.

Операции. Которые выполняют инструкции, обычно очень просты. Только записав последовательность этих простых операций. Можно заставить процессор выполнить определенную задачу. Однако написание последовательности числовых кодов довольно утомительно (хотя именно так и делалось программирование давным-давно). Поэтому был создан язык программирования ассемблера. Он присваивает опкодам (числовому коду) символ – имя. Которое как бы описывает то. Что он делает.

Учитывая предыдущие примеры. Где число 35 заставляет процессор перемещать данные из одной ячейки памяти в другую. Мы можем присвоить этой инструкции имя MOV, которое является сокращением от MOVe. Число 48, которое является инструкцией , которая складывает два числа вместе, получает имяADD, а число 12, которое выполняет логическую операцию ИЛИ. Получает имя ORL.

Программист пишет последовательность инструкций – простых операций. Которые процессор может выполнять. Используя эти имена. Которые гораздо легче читать. Чем просто числовые коды. Затем он выполняет инструмент с именем ассемблер (но часто термин “ассемблер” используется также для языка программирования. Хотя технически это означает инструмент). Который преобразует эти символы в соответствующие числовые коды. Которые могут быть выполнены процессором.

Однако в большинстве случаев одной инструкции недостаточно. Например, если вы хотите сложить два числа вместе, вам, очевидно. Нужно указать их. То же самое касается логических операций или перемещения данных из ячейки памяти в другую: вам нужно указать адрес исходной и целевой ячейки. Это делается путем добавления в инструкцию так называемых операндов-просто одного или нескольких значений (чисел). Которые предоставят дополнительную информацию для инструкции. Необходимую для выполнения данной операции. Операнды также хранятся в памяти вместе с опкодами команд.

Например, если вы хотите переместить данные из местоположения с адресом 1000 в местоположение 1258, вы можете написать:

АСМ

Копировать Код

MOV 1258, 1000

Первое число является целевым адресом. А второе-источником (в сборке вы обычно пишете сначала цель. А источник-как второй. Это довольно распространено). Ассемблер (инструмент. Который преобразует исходный код в машинный) также хранит эти операнды, поэтому. Когда процессор впервые загружает опкоды команд. Он сообщает ему. Что он должен переместить данные из одного места в другое. Конечно, он должен знать. Из какого места двигаться в какое место назначения. Поэтому он также загрузит значения операндов из памяти (они могут быть по адресам сразу после опкодов команд). И как только у него будут все необходимые данные. Он выполнит операцию.

Небольшой пример

Давайте рассмотрим небольшой код и опишем, что он делает. Обратите внимание. Что это псевдокод. Он не создан для конкретной архитектуры или языка. И различные символы могут отличаться. Однако принцип остается тем же.

АСМ

Копировать Код

MOV A, 2000 ПЕТЛЯ: ДОБАВЬТЕ A, #5 JNL A, #200, LOOP MOV 2001, A

Первая команда переместит номер из ячейки памяти с адресом 2000 в регистр А – это временное место. Где процессор хранит номера. Он может иметь много таких регистров. Вторая строка содержит что-то, называемое меткой: это не инструкция. Это просто метка в исходном коде. Которую мы можем использовать позже (вы увидите, как).

На третьей строке есть ADDинструкция. Которая складывает два числа вместе. Операндами являются регистр А и номер 5 (знак # перед ним говорит ассемблеру. Что это номер пять. А не число в ячейке памяти с адресом 5). А помнишь? Мы сохранили значение из ячейки памяти 2000 в регистре А. Так что каким бы оно ни было. Эта инструкция добавит к нему число 5.

Следующая команда называется условным переходом: процессор будет тестировать какое-то условие и в зависимости от результата будет прыгать или нет. В этом случае условие состоит в том. Не больше ли данное число другого (JNL = Прыжок (если) Не Больше). Сравниваемое число-это число в регистре А с числом 200 (опять же. Пометка # означает. Что это прямое число. А не число из ячейки памяти с адресом 200). В этом случае число в А меньше 200 (следовательно. Не больше 200 – условие истинно). Процессор сделает скачок по инструкции. Указанной третьим операндом. И именно здесь появляется наша метка: инструмент ассемблера (транслятор) заменит “LOOP” адресом памяти инструкции сразу после этой метки.

Таким образом, если число меньше. Процессор вернется к инструкции ADDи снова добавит значение 5 к числу А (которое уже больше по сравнению с предыдущим вычислением). А затем вернется к JNL инструкция. Если число все еще меньше 200, оно снова прыгнет назад; однако. Если оно больше. То условие больше не будет истинным. Поэтому никакого прыжка не происходит. И следующая инструкция выполняется. Он перемещает значение из регистра А в ячейку памяти с адресом 2001, в основном сохраняя там результирующее число. Важно добавить. Что ячейка памяти с адресом 2000 все еще содержит исходное значение. Потому что мы создали его копию в регистре А. А не модифицировали оригинал.

Этот фрагмент кода на самом деле не имеет большой цели; он просто предназначен для демонстрации. А также для некоторой гипотетической архитектуры. Реальные программы состоят из сотен, тысяч и даже сотен тысяч инструкций.

Архитектуры и языки ассемблера

Я уже упоминал ранее термин Он описывает. Какие простые операции может выполнять процессор (некоторые могут выполнять только дюжину из них. Некоторые сотни различных операций). И какие опкоды имеет каждая инструкция. Он также указывает много других вещей: что и сколько регистров (небольших мест хранения непосредственно в самом процессоре. Где программист может временно хранить данные) у него есть. Как он может взаимодействовать с другими чипами и устройствами. Такими как память, чипсет. Видеокарта и другие особенности его функции.

Это означает. Что каждый процессор имеет свой собственный язык ассемблера. Потому что инструкции у него разные. Таким образом. Ассемблер (или просто ассемблер. Хотя это технически неверно) — это не просто один язык. Это целый набор языков. Все они довольно похожи, но отличаются тем, какие существуют инструкции. Каковы операнды и некоторые другие особенности. Характерные для процессора. Однако основной принцип один и тот же (если только это не один из моих экспериментальных процессоров WPU 🙂 ) среди них. Так что если вы понимаете принцип одного ассемблера для данной архитектуры. То изучение других будет подпругой.

Поэтому важно понимать: язык ассемблера всегда предназначен для использования с определенной архитектурой. Например, большинство персональных компьютеров используют архитектуру x86 или. В случае 64-разрядных систем и приложений. Ее расширение x64, поэтому. Если вы хотите программировать для этой архитектуры. Вы должны использовать язык ассемблера x86. Многие мобильные устройства используют архитектуру ARM, поэтому. Если вы запрограммировали эти процессоры на ассемблере. Вы будете использовать язык ассемблера ARM. Если бы вы хотели запрограммировать какую-нибудь старую консоль, например Sega Genesis, вы бы использовали язык ассемблера 68000, потому что он использует процессор Motorola 68000 и так далее. Существуют сотни различных архитектур для различных целей.

Кроме того, как я уже сказал. На рынке существует множество процессоров с различными скоростями. Ценой и энергопотреблением. Но многие из них поддерживают одну и ту же архитектуру – поэтому программы. Написанные на данном языке ассемблера. Будут работать на них. Они просто будут работать быстрее или медленнее.

Однако программы, созданные для одной архитектуры, как правило. Не будут работать на другой. Потому что процессор просто отличается: опкоды разные. Поддерживаемые инструкции и другие функции разные. Поэтому машинный код (программный набор числовых кодов) для архитектуры x86 был бы тарабарщиной для архитектуры ARM. Это также означает. Что когда вы пишете программу на ассемблере для одной архитектуры процессора. Она не будет работать на другой: вам нужно будет полностью переписать ее на ассемблер для другой архитектуры.

Потребность в высокоуровневых языках программирования и компиляторах

Были две большие насущные проблемы, которые привели к высокоуровневым языкам программирования, которые вы. Скорее всего. Уже знаете. Во-первых, создание сложных программ требует разбиения их на множество простых инструкций. Поэтому для достижения более сложных действий нужно написать много инструкций: это и утомительно. И отнимает много времени. Не говоря уже о том. Что это сложнее понять. Вторая проблема уже упоминалась: программа. Написанная для одной архитектуры. Не будет работать на другой без полной перезаписи. Языки высокого уровня решают обе эти проблемы.

Сложность программ

Давайте представим. Что вы хотите выполнить более сложный расчет, например. Вы хотите вычислить результат = 2 + ( 7 – 3 ) * 2. Однако процессор не поддерживает ничего подобного. Он может выполнять только очень простые операции. Поэтому. Если вы хотите написать код в сборке. Вам нужно разделить это вычисление на простые операции. Которые поддерживает процессор. Для математического выражения это делается так же. Как если бы вы вычисляли выражение вручную в математическом классе. Например: сначала вам нужно вычислить значение в скобках (вычесть 3 из 7). Затем умножить результат на 2 и, наконец. Добавить его к числу 2. Результат будет сохранен в регистре A. Таким образом. Ассемблерный код будет выглядеть следующим образом ( “ ; ” запускает комментарий — не часть кода. А просто замечание о том. Что он делает):

АСМ

Копировать Код

Суб #3, #7 ООО в, #2 добавить #2, а 

Конечно. Вы редко будете использовать фиксированные числа в расчетах. Вы скорее будете вычислять значения из памяти. Поэтому давайте усложним весь процесс следующим изменением уравнения: @250 = @200 + ( @201 — @202 ) * @203. Здесь @(number) означает “по адресу” – номер. Хранящийся в ячейке памяти по данному адресу. Чтобы завершить вычисление, нам нужно загрузить значения из памяти. Потому что процессор не позволяет вычислять числа непосредственно по заданным адресам памяти. Однако он предоставляет другой регистр B.

АСМ

Копировать Код

Мову Б, 202 мову а, 201 подгруппы А, Б формате MOV Б, 203 мул - А, Б мову Б, 200 добавить : А, Б формате MOV 250, а 

Как видите. Даже простые выражения могут усложниться и потребуют нескольких строк кода. Что довольно утомительно. Не говоря уже о том. Что из кода довольно трудно понять. Что он на самом деле делает. Если только вы не объясните это в комментариях. Но, к сожалению, другого пути нет, просто так работает процессор. Вы можете себе представить. Что при более сложном коде количество инструкций и утомляемость быстро возрастут.

Другая проблема заключается в том. Что значения. С которыми вы работаете (в основном вы можете считать их переменными). Являются просто числами (адресами). С которыми не совсем легко иметь дело. Вы можете присвоить адресам некоторые имена. Но это немного задерживает проблему: вам все равно нужно сказать. Что адрес 204 будет известен под именем MYVARIABLE а если количество переменных будет увеличиваться. Это быстро станет проблематичным. Хотя назначение точных адресов обычно автоматизируется. Не говоря уже о том. Что вы можете использовать адрес как определенную переменную только на короткое время. А затем повторно использовать его как другую. Но вы также должны убедиться. Что оба (или даже несколько) использования не столкнутся.

Где появляется компилятор

Итак, вот вопрос: если вы можете разбить выражения и задачи на ряд простых инструкций и если вы можете назначить места памяти для переменных. Почему это не может быть сделано программой? И это именно то, что делает компилятор. Язык программирования определяет, какие операторы можно писать и как. И компилятор должен их поддерживать. Таким образом. Вы можете просто написать следующий код (это C-подобный код):

C++

Копировать Код

int a, b, c, d, e; a = 2; b = 7; с = 3; d = 2; e = a + (b - c) * d;

Когда вы скомпилируете этот код. Компилятор проанализирует (проанализирует) этот код и обнаружит. Что вам нужно пять переменных. Вам не нужно решать. Какие ячейки памяти будут назначены этим переменным: все это обрабатывается за вас. Например, компилятор может решить, что содержимое переменной с именем “a” будет храниться в ячейке памяти с адресом 200, “b” в 201-м и так далее. Компилятор отслеживает эти назначения. Поэтому. Где бы вы ни использовали данную переменную. Он будет следить за тем. Чтобы использовался правильный адрес памяти. На самом деле процесс часто немного сложнее, но принцип остается тем же.

В приведенном примере кода есть несколько назначений значений. Начинающихся с “a = 2;”. Компилятор прочтет это и согласно правилам языка программирования. Это означает. Что переменной присваивается значение 2. Компилятор знает. Какой адрес памяти соответствует переменной с именем“a”, поэтому он сгенерирует для вас соответствующие инструкции: помните. Процессор не понимает выражения типа “a = 2;”. Он может работать только с простыми инструкциями. Но это работа компилятора. Чтобы преобразовать эти высокоуровневые операторы в инструкции. Которые понимает процессор:

АСМ

Копировать Код

MOV 200, #2 MOV 201, #7 MOV 202, #3 MOV 203, #2 

Надеюсь. Это достаточно просто; каждое задание соответствует инструкции процессора (я написал их на ассемблере. Компилятор, конечно. Сгенерирует соответствующие числовые коды для каждой команды – машинный код). Однако, когда дело доходит до последнего оператора. Который присваивает результат выражения “a + (b — c) * d” переменной “e” это не может быть сделано с помощью одной инструкции. Как вы видели раньше. Однако все. Что вам нужно сделать, это написать это выражение. И компилятор прочитает его и сделает разбиение на серию простых инструкций сам. Даже не зная об этом (по крайней мере. До сих пор: -)). Например, он может генерировать следующие инструкции:

АСМ

Копировать Код

Мову Б, 202 мову а, 201 подгруппы А, Б формате MOV Б, 203 мул - А, Б мову Б, 200 добавить : А, Б формате MOV 250, а 

Я думаю, не нужно объяснять. Что гораздо проще просто написать “e = a + (b — c) * d” вместо серии инструкций. И тот же принцип применим ко всему. Высокоуровневый язык программирования позволяет более четко. Легко и понятно выражать выполняемые действия. А компилятор позаботится о преобразовании этого в серию простых инструкций. Понятных процессору. И обработает для вас все остальные детали. Это называется абстракцией и решает проблему сложности программы: вы можете писать и управлять гораздо более сложными программами. Потому что вам не нужно беспокоиться обо всех деталях: они заботятся о вас автоматически.

Возможно, было бы важно упомянуть. Как обрабатываются некоторые базовые программные конструкции. Например, оператор“if”. Давайте рассмотрим следующий код на языке Си:

C++

Копировать Код

если2) b = 3; еще { b = 5; с = 8а = 8;

Процессор не понимает, чтоifтакое оператор“”. Однако у него есть условная команда перехода: он перейдет к другой команде. Если условие истинно. Таким образом, этот фрагмент будет переведен в следующий ассемблерный код:

АСМ

Копировать Код

MOV A, 200 ЛГР #2 ЮЖД еще мову 201, #3 СПМ окончание ЕЩЁ: MOV 201, #5 MOV 202, #8 КОНЕЦ: MOV 201, #8 

То же самое касается циклов, за исключением того. Что инструкция jump заставляет прыгать назад в последовательности. Поэтому часть кода повторяется. При программировании на языке высокого уровня вам не нужно беспокоиться о том. Как именно строится цикл с помощью команды перехода. Процессор на самом деле может иметь их несколько. Каждая из которых имеет свое условие: компилятор обработает эту схему за вас. А также выберет соответствующую команду перехода и выполнит дополнительную работу. Чтобы можно было выполнить условный переход: в этом примере мы предположили. Что у процессора нет условной команды. Которая сравнивала бы. Какое число больше: вместо этого у него есть отдельная команда для проверки этого числа.. Таким образом. Компилятор сначала использует инструкцию для сравнения двух чисел. А затем использует результат с соответствующей инструкцией условного перехода.

Потребность в ассемблере в наши дни

Вы можете задаться вопросом: если компилятор может справиться со всеми этими вещами за вас. Какой смысл знать ассемблер сегодня? Во-первых, компилятор не всегда может генерировать оптимальные инструкции: некоторые действия могут быть выполнены с использованием меньшего количества инструкций менее стандартным способом. О котором компилятор не знает. Это проблема, когда вам нужно выжать каждый бит производительности; в таких случаях вы можете сами написать инструкции для критических частей производительности, гарантируя. Что она работает именно так. Как вы хотите.

Это может быть еще более серьезной проблемой при программировании для небольших встроенных устройств с ограниченными ресурсами. Где вы просто не можете позволить себе никаких накладных расходов (больше инструкций. Которые действительно необходимы. И неоптимальные способы решения проблем). Другая причина-ограничения компилятора: вы ограничены только тем. Что он поддерживает. Если вы хотите использовать некоторые функции процессора. Для которых компилятор не может генерировать инструкции, например. Новые специальные инструкции. Вам нужно будет написать их самостоятельно.

Знание ассемблера абсолютно необходимо. Если вы хотите проанализировать существующее программное обеспечение. Взломать (изменить его поведение) или взломать его. Как вы уже знаете. Программа состоит из серии простых инструкций – числовых кодов. Представляющих различные действия. Легко разобрать существующую программу. Когда у вас нет ее исходного кода: числовые коды просто заменяются соответствующими именами. В результате чего получается код языка ассемблера, поэтому. Если вы хотите анализировать и изменять их. Вы должны знать язык ассемблера.

Гораздо сложнее декомпилировать программу – преобразовать ее обратно в исходный код высокого уровня: для этого требуется обширный анализ инструкций и их структуры. А полученный исходный код будет все еще очень далек от оригинала: важные вещи. Такие как имена переменных. Функций и комментариев. Теряются во время компиляции (не обязательно для всех языков). Потому что они просто не нужны процессору: все. Что ему нужно. — это адрес памяти. Который является просто числом.

Портативность

Вторая серьезная проблема ассемблерного программирования-переносимость: чтобы перенести вашу программу на другую платформу с другой архитектурой процессора. Вам нужно полностью переписать ее на ассемблере для целевой платформы. Использование языка высокого уровня решает эту проблему довольно легко. Код на языке высокого уровня обычно не зависит от платформы: это компилятор. Который генерирует соответствующие инструкции, а не вы.

Поэтому. Если вы хотите, чтобы ваш код работал на ПК с архитектурой x86, вы предоставляете исходные коды компилятору. Который генерирует инструкции для x86. Если вы хотите сделать двоичный код (машинный код) для мобильного устройства с архитектурой ARM. Вы даете тот же исходный код компилятору ARM. И он будет генерировать инструкции для этой архитектуры. Без необходимости что-либо делать.

Это, конечно, требует. Чтобы компилятор существовал для данной архитектуры; если нет компилятора для конкретной архитектуры. То вы останетесь с ассемблерным программированием. Если только вы не напишете компилятор самостоятельно.

Интерпретируемые языки

До сих пор мы имели дело только с тем. Что называется машинным кодом: языки. Которые создают машинный код. Приводят к инструкциям. Которые непосредственно выполняются данным процессором. Если вы создадите двоичный файл (содержащий эти необработанные инструкции для данного процессора). Он будет работать только на данной архитектуре; если вы хотите использовать его на другой. Вам нужно будет скомпилировать его для этой архитектуры: сгенерировать соответствующий машинный код. Который понимает процессор данной архитектуры.

Однако существует нечто, называемое интерпретируемым языком. Что значительно облегчает переносимость. Я упомяну об этом лишь вкратце. Поскольку сама по себе эта тема может быть развернута в длинную статью. С интерпретируемым языком программирования исходный код остается таким. Как он есть. Или компилируется в “универсальный” ассемблерный код (именно это происходит с Java – полученный универсальный ассемблерный код называется байт-кодом). Если вы хотите запустить такую программу. Вам нужен интерпретатор: это программа в машинном коде – то. Что процессор понимает непосредственно. Что может прочитать этот универсальный код и перевести его в инструкции для целевой архитектуры на лету – по мере запуска программы.

Преимуществом такого подхода является простота переносимости. Безопасность и гибкость: вы можете написать свою программу один раз. А затем она будет работать на любой архитектуре. Где доступен интерпретатор для данного языка. Без необходимости что-либо менять в вашей программе. Поскольку интерпретатор контролирует то. Что может сделать программа. Безопасность также повышается. Поскольку он может блокировать определенные действия. Что гораздо сложнее с машинным кодом. Если вообще возможно. Вы также можете быстро протестировать и изменить свою программу. Не компилируя ее каждый раз.

Одним из основных недостатков является снижение скорости: например. В случае назначения типа “a = 5” интерпретируемому языку может потребоваться процессор для выполнения даже нескольких десятков инструкций. Которые прочитают это утверждение, решат. Что оно означает. А затем, наконец. Сделают это. В то время как с компилируемым языком (в результате чего получается машинный код) одна команда часто может справиться с этой задачей.

Вывод

Если вы зашли так далеко: поздравляю! Я надеюсь, что помог раскрыть некоторые секреты работы процессора и то. Как он связан как с ассемблером (низкоуровневым). Так и с высокоуровневым программированием. Хотя это не научит вас программировать на ассемблере и взламывать/взламывать/анализировать существующие программы. Мы надеемся. Что это даст вам необходимые знания. Чтобы начать изучать эти вещи и знать. Чего ожидать.

Обратите внимание. Что многое из описанного было упрощено ради удобства понимания. Многих тем, затронутых здесь, было бы достаточно. Чтобы заполнить несколько книг. И в настоящее время я не планирую писать ни одной. По крайней мере. Не об этих темах 🙂

Я буду благодарен, если вы проявите хоть какую-то признательность за эту статью и предоставите какие-либо отзывы, будь то о восприимчивости статьи или о некоторых ошибках с моей стороны (в основном грамматических и орфографических, пожалуйста. Игнорируйте преднамеренные упрощения фактов).

Спасибо за чтение.