Короткий познавательный пост, основанный на моей больной фантазии. Все совпадения случайны.
Обратился как-то один человек на профильном форуме с вопросом, как запустить (не сказать чтобы старую) игру на Windows 10. Игра (хотя я бы назвал это технодемо) была разработана на легендарном XNA 4.0 (земля пухом) и планировалась к выпуску через Steam Greenlight (и ему земля пухом) еще в далеком 2012, после чего заброшена и убрана со всех магазинов.
Игра была куплена тем человеком еще в тот момент, когда она легально продавалась. А спустя годы она просто перестала подавать признаки жизни. $5 за полную версию уплочены, а поиграть нельзя. Обидно.
Так как типовые решения по типу установки XNA Redistributable не помогли, то был предложен единственно возможный выход из ситуации - отладка через dnSpy.
Внимание: обратная разработка хоть прямо и не запрещена законодательно, но разрешенные случаи весьма ограниченны. В других странах разрешен явный запрет любых манипуляций через лицензию, превращающий это занятие во вполне наказуемое. Не повторяйте дома.
Так как ПО было приобретено легально и оно не работает, то этот теоретический случай допустим в соответствии со статьей 1280 гражданского кодекса.
Демонстрировать буду на бесплатной демоверсии, скачать можно здесь.
dnSpy
Театр начинается с вешалки, а программа на C# с атрибутов сборки. Они нам сейчас не сильно интересны, поэтому пытаемся сразу запускать отладку. С неизвестными программами лучше использовать виртуальную машину, так как может быть встроена какая-нибудь пакость для кулхацкеров, но мне можно и так.
Ошибка. Открываем стек вызовов и ищем место, где произошел провальный запрос в сеть.
А вот и виновник торжества. По сообщению из исключения можно сделать вполне однозначный вывод, что проблемой является скупердяйство и непредусмотрительность автора, который решил перестать платить за домен и хостинг, а вместе с этим случайно (а может и намеренно) сделал запуск игры невозможным без интернета. Так как на сайте заявлено отсутствие DRM, то это явно недосмотр.
Что тут происходит?
Если почитать документацию на XNA, то этот метод предназначен для загрузки контента и вызывается автоматически при запуске игры. Первым делом выполняется инициализация некоторых вещей, после чего открывается файл настроек (options.xml) и начинает последовательно считываться. Как только считываются параметры аккаунта для таблицы рекордов, то происходит отправка логина+пароля на официальный сайт и попытка получить ID. Если сервер возвращает Failure, то открывается диалог входа, который можно пропустить. Если сервер возвращает число, то игра запускается. А вот если сервер недоступен, то вылетает исключение, которое никак не обрабатывается и приводит к молчаливому вылету.
Диалог входа официально можно пропустить.
На этом этапе есть 2 варианта действий:
Вручную отредактировать настройки и убрать пункт об аккаунте. Тогда игра будет всегда запускаться без входа.
Физически убрать запрос на несуществующий сайт.
Нормальные герои всегда идут в обход, так что переходим к написанию небольшого патча. (На самом деле, о первом варианте я додумался не сразу. Более того, полная версия зависает при убирании этого пункта из настроек).
Выделяем строчку с запросом и нажимает "Изменить инструкции IL". Открывается окно с заголовком "Изменение тела метода".
Это - язык MSIL. Он очень прост и сильно напоминает Forth, на самом деле, но новичка может отпугнуть.
Ключевым элементом является стек. Что такое стек я уже когда-то рассказывал. Это некоторое место, на вершину которого можно последовательно добавлять значения (числа, строки и прочие объекты), а так же снимать их в обратном порядке.
Не эти стеки, но суть та же. Можно добавлять и снимать сверху.
ldloc.2: Значение локальной переменной под номером 2 (с типом строки)
ldstr: Строку "&p=".
ldloc.3: Значение локальной переменной под номером 3 (тоже строка).
Потом мы вызываем String::Concat(), принимающий в себя 4 строки и соединяющий их вместе. При вызове метода будут сняты 4 верхние строки со стека и добавлена новая строка.
Вызов следующего метода (WebClient::DownloadString()) снимает строку, полученную от String::Concat(), и веб-клиент. В результате должна остаться только 1 строка, возвращенная WebClient::DownloadString().
Следующим этапом мы должны снять эту строку со стека и сохранить её в локальную переменную 1, но этого не произойдет, так как WebClient::DownloadString() всегда будет проваливаться и выбрасывать исключение. Исключение, если его не обработать сразу, будет раз за разом проваливаться глубже по стеку вызовов, пока не достигнет обработчика или дна. Так как никаких обработчиков по пути нет, то исключение достигает дна и приводит к вылету.
Чтобы избежать получения исключения, мы можем убрать всё, что предназначено для запроса в сеть. Примерно вот так:
8 инструкций превратились в 2: загрузка строки с числом (которое может быть любым) и сохранение в локальную переменную под номером 1.
Сохраняем и проверяем результат.
Было.
Стало.
Из декомпилированного кода полностью исчез кусок с запросом. Осталась лишь загрузка константной строки и сравнение с "Failure", которое всегда будет успешным.
Сохраняем модуль и пытаемся запускать опять.
Жопиздан!
С этой задачей успешно справился человек, который не смыслит в программировании вообще ничего. По его словам, он джва года ждал эту игру!
Но не всё так просто. Если попытаться пройти любой уровень, то вылетит то же самое исключение.
Для исправления точно так же выделяем строку и опять редактируем инструкции в теле метода. Но так как отправка статистики нонче ни на что не влияет, то просто удаляем выделенные инструкции без остатка.
А для надежности выделяем WebClient, нажимаем "Анализировать" и смотрим, где еще может ждать нас подлянка.
В Finish.Update() мы ошибку только что исправили, в Game1.LoadContent() исправили в самом начале, а Login.button1_Click(), Program.Main() и Title.Update() нас не интересуют, так как достаточно просто не делать того, что приведет к обращению в сеть (а конкретно: не открывать диалог входа, не указывать никакие аргументы при запуске и не пытаться авторизоваться через главное меню (вроде бы не реализовано)).
Создаю на верстаке меч волшебный. Когда крафт заканчивается, стол взрывается вместе со мной.
Начинаю разбираться.
Когда крафт заканчивается, предметы-ингредиенты для него исчезают(до этого хранятся чтобы можно было сделать отмену крафта и обратно получить компоненты)
Но! Я вызывал метод дестрой у компонентов, который я переопределил ранее под пользовательские нюансы.
И камни с энергией ( взял за основу powerstone из gurps4e) взрываются при уничтожении обычно. А должны просто исчезнуть в этом конкретном случае.
Числа Фибоначчи - это последовательность, где каждое очередное число равно сумме двух предыдущих. 1 = 0+1, 2 = 1+1, 3 = 2+1, 5 = 3+2, 8 = 5+3, ...
В программировании для вычисления этой и подобных последовательностей (факториалы, степени, если нет прямой возможности...) используются циклы.
Цикл представляет собой некоторые условия: начальные условия, условия завершения, изменение данных для достижения условий завершения. (Заметка для тех кто в теме, это касается любых циклов, а не только цикла for)
А вот то, что делается внутри цикла называется ТЕЛОМ цикла.
У каждого сайта есть этот самый body. Да, у любого. Даже если рукожопый разраб забудет указать этот тег, то за него это сделает браузер.
Тег head (голова) тоже будет добавлен, если что, но сайт без головы может работать спокойно. А вот тело будет по умолчанию. Забыли голову - пофиг, тело всё равно будет.
И вы, пользователи, видите именно тело сайта.
На изображении кусочек кода Вомбата. У меня включена тёмная тема, поэтому у вас код может быть другим.
Теория эта весьма занимательна и необычна. На неё обрушился шквал критики, один из примеров которой мысленный эксперимент "Парадокс близнецов": "С точки зрения домоседа часы движущегося путешественника имеют замедленный ход времени, поэтому при возвращении они должны отстать от часов домоседа. С другой стороны, в системе отсчета путешественника двигалась и ускорялась Земля, поэтому отстать должны часы домоседа. На самом деле братья равноправны, следовательно, после возвращения их часы должны показывать одно время. "
Время безжалостно.
Есть простое объяснение этого парадокса (на самом деле всё сложнее): " Братья не являются равноправными, так как один из них (путешественник) испытывал этапы ускоренного движения, необходимые для его возвращения на Землю."
Но рассуждения рассуждениями, а как их проверить?
И вот в октябре 1971 года
Дж. Хафеле и Ричард Китинг придумали экперимент. Взяли четыре комплекта атомных часов на сверхтонком переходе атома цезия-133. Часики синхронизировали. Два комплекта остались на земле, а два полетели обычными пассажирскими авиалиниями, но в разных направлениях: с запада на восток и с востока на запад (для того, что бы учесть вращение Земли вокруг своей оси).
Фото с вики - один из комплектов, учавствовавших в эксперименте.
Результаты полностью подтвердили предсказания СТО и ОТО:
Соскриншотил с википедии.
Все циферки в ентой табличке представлены в наносекундах. Почти за 65 с половиной часов полётов экспериментаторы потратили почти 7 600 баксов. Но этот облёт стоил того - предсказания обеих теорий относительности по поводу замедления/ускорения времени полностью подтвердились с очень высокой точностью.
Магнетар - это нейтронная звезда с диким магнитным полем.
Магнетар в фантазиях художника (C) ESO/L. Calçada
Нейтронные звёзды образуются в процессе эволюции обычных звёзд с массой более 8 солнечных. После взрыва звезды остаётся остаток с массой примерно как у Солнца, но диаметром 10-20 км. При этом, если остаток будет весить более трёх солнечных, то ядро схлопнется в чёрную дыру. Такие космические тела(ну вот и ивент приплёл) не редкость в космосе и очень ярко излучают в рентгеновском и гамма диапазонах.
Астрономы, изучали сверхновую SN 2024afav, которая вспыхнула в декабре 2024 года и заметили что, пик яркости, достигнутый примерно через 50 дней после взрыва начал уменьшаться необычным образом. Вместо плавного затухания ученые заметили серию из четырех «всплесков» – колебаний яркости, частота которых постепенно увеличивалась.
Аккреционный диск вокруг магнетара, опять фантазии художников (C) Joseph Farah and Curtis McCully, Las Cumbres Observatory
«Согласно расчетам, образовавшаяся нейтронная звезда вращается с периодом около 4,2 миллисекунды (238 оборотов в секунду или 14 285 об/мин) и обладает магнитным полем примерно в 300 триллионов раз сильнее земного – типичными характеристиками магнетара. Наши наблюдения стали решающим доказательством того, что магнетары действительно могут возникать в центре сверхярких сверхновых и питать их энергией», – заключили авторы открытия.
Не рекомендуется принимать данную статью близко к сердцу или как истину в последней инстанции. Скорее как научпоп-грелку для мозгов. Тут довольно много упрощений, упущений или просто странно написанных моментов. Если что-то объяснено совсем криво, то добро пожаловать в комментарии.
Все сказанное далее применимо везде, но детали описаны для защищенного режима x86 (только я опустил префиксEдля регистров) и языков семейства C (в основном C и C++, местами C#). Для понимания рекомендуется знать про указатели и базово представлять, что происходит в процессоре.
Приятного чтения.
Думаю все помнят, что такое функция в математике
Начнем со сложного
Глоссарий
Чувствую, что большинство не имеет ни малейшего представления об указателях
В части про x86 я упоминал о том, что процессор представляет из себя бешенный самоуправляемый калькулятор, которому указом может быть разве что хранилище кода, аппаратный сброс и немаскируемое прерывание. И что у него есть системная шина, состоящая из шины адреса, шины данных и шины управления, к которой подключена память, хранящая данные и код.
А память это огромная одномерная полоска, в которой каждая ячейка имеет свой порядковый номер от 0 до 2^(разрядность шины адреса)-1. И указатель представляет из себя самую обычную численную переменную, хранящую номер (адрес) любой ячейки.
Я не понимаю, почему для многих это настолько сложная тема, а существенное количество новичков забрасывают изучение C после встречи с ними. Они в своей сути максимально элементарны (до тех пор, пока не нужно исправлять уязвимости и ошибки, которые появились в результате злоупотребления или неполного понимания нюансов).
Также существуют ссылки, так или иначе представляющие из себя подвид указателей, но обычно с запретом на арифметику с ними (основная сложность и опасность указателей) и/или подсчетом количества активных ссылок для сборки мусора.
Все языки или имеют указатели (Pascal, C, C++, блоки unsafe в C# и Rust), или являются ссылочными (Java, C#, Python, JS, т.д. (почти все современные языки)). С++ тоже имеет ссылки, но как синтаксический сахар над указателями, призванный упростить их передачу в функции и избавиться от проблемы нулевых указателей.
Ассемблер - если вы даже примерно не знаете, что это такое, то первую часть статьи вероятно можно пролистать и перейти на обсуждение парадигм.
Регистр - именованная численная переменная внутри процессора. Может иметь особое предназначение, а может просто использоваться для хранения любых чисел и арифметики. Их немного.
Инструкция - одиночная команда, представляет из себя последовательность из нескольких байт со специальным значением. Любая программа представляет из себя последовательность инструкций внутри памяти. Указатель на инструкцию, которая будет выполнена следующей, содержится в регистре IP, который сам увеличивается после выполнения каждой инструкции. По-хорошему стоило нарисовать пошаговую наглядную анимацию с демонстрацией всех регистров и куском ассемблерного кода, но мне лень, а в гугле я ничего толкового не нашел.
Метка - константа, содержащая адрес чего-либо. К примеру процедуры или глобальной переменной. Примерно как метка для goto в высокоуровневых языках, но универсальнее. Применяются при написании на ассемблере, в процессоре как таковые не существуют и разрушаются до обычных чисел при ассемблировании и линковке.
Разыменование - операция над указателем, когда тот превращается в переменную с адресом, который был записан в указателе (своеобразный пульт Д/У для переменной).
Парадигма - "стиль" написания и постройки архитектуры программы, частично определяется языком. Технически на том же C можно писать в практически любой парадигме (через костыли можно писать в стиле ООП, через макросы реализовать метапрограммирование, а через нестандартные расширения вообще ядерный бред), но родная для него - процедурная. А Go, к примеру, хоть и имеет недоклассы и методы, но лишен практически всех благ ООП и не сильно далеко ушел от C.
Синтаксический сахар - необязательная возможность, которая сокращает количество кода, повышает его читаемость или удобство поддержки
Стек
Большинство процессоров Фон-Неймановской архитектуры в своей конструкции предлагают механизмы стека. Кто играл в покер должны вспомнить стеки фишек. То есть некие значения, сложенные друг на друга (обычно это переменные, в частности адреса в памяти). При этом основными операциями являются добавление фишки на вершину (инструкция PUSH) и ее снятие (инструкция POP). Еще можно косвенно читать и заменять (перезаписывать) фишки относительно вершины (на вершину указывает регистр SP, неявно обновляется через POP и PUSH) или основания (указывает регистр BP) вглубь.
Стеки можно переключать (но не в MOS6502), это нужно для многозадачности
Помимо хранения локальных переменных стек позволяет делать довольно интересную вещь: мы можем положить на стек все необходимые аргументы (x в математике, но их может быть несколько), адрес следующей инструкции (взяв из указателя инструкции IP, это будет адрес возврата), после чего совершить прыжок на какой-нибудь другой адрес (сохранение адреса и прыжок делаются инструкцией CALL).
А на этом адресе может быть функция. Сначала она кладет текущий BP на стек, после чего приравнивает основание к вершине (BP к SP), тем самым создав для себя "новый стек" сразу после предыдущего (это еще называется стековым кадром), в котором якобы лежит только значение старого основания стека (BP), чтобы можно было восстановить его перед возвратом.
Серое это стековый кадр от предыдущей функции
После чего функция может прочитать переданные ей аргументы относительно основания своего стека вниз (технически это будет выход за границы текущего стека), выполнить с этим какие-либо действия (записать в файл, вывести в консоль, просто перемножить) и сохранить результат (обычно результат сохраняется не на стек, а в регистр AX).
После чего функция восстанавливает старое значение BP, сняв его со стека, и выполняет команду RET, которая снимает со стека адрес возврата и совершает переход на него, тем самым переключившись на инструкцию сразу после CALL.
И эта система так или иначе перекочевала в большинство высокоуровневых языков начиная с FORTRAN. К примеру в C CALL превратился в круглые скобки, RET в return, а адреса и метки в имена (при этом без скобок они все еще являются указателями, то есть адресами).
Пример кода (к сожалению, штатного форматирования не предусмотрено):
#include <stdio.h> int pow2(int x) { return x * x; } int main() { int y = pow2(16); // Вызов функции. В y будет сохранено число 256 printf("%p", pow2); // Без скобок вместо вызова просто выведет адрес функции return 0; // Возврат нуля из главной функции означает отсутствие ошибок. }
Такой стиль программирования называется процедурным (выделение кода в блоки называется структурным). А вот называть процедурный язык функциональным совершенно неправильно, ибо функциональное программирование ≠ процедурное, они даже в разных категориях (императивное и декларативное).
А теперь скомпилируем этот код и разберем ассемблерный листинг
Важно понимать, что стек в большинстве архитектур традиционно растет от больших адресов к меньшим (и в x86). То есть для того, чтобы отодвинуть его вершину вверх, от регистра SP нужно отнимать значения, а вот для ужимания и съедания ненужных значений к указателю на вершину значения прибавляют. И для доступа к значениям относительно основания или вершины это тоже важно учитывать. Это может звучать запутанно, но через время привыкаешь и всё становится очевидным.
Надеюсь это возможно будет разобрать. Код скомпилирован MSVC v19.28 для x86 со стандартными настройками в Godbolt
Post scriptum
Это всё довольно упрощенно. Как минимум, в защищенном режиме используется больше 5 разных соглашений о вызове, которые отличаются деталями реализации. Это было описание для cdecl, обычно используемого в C. Еще часто используются соглашения pascal, fastcall, thiscall, winapi и другие. Fastcall, к примеру, избегает хранения аргументов на стеке, если их возможно передать через регистры, что улучшает производительность. А winapi отличается от cdecl тем, что функция сама очищает стек от аргументов для себя при возврате. А еще я упустил, к примеру, сохранение регистров, которые функция может перезаписать и испортить, а потому обязана предварительно сохранить и перед возвратом восстановить, передачу переменного количества аргументов (как в printf) и возврат значений шире 32 бит (которые не влезут в EAX).
Плюс сейчас мало кто компилирует ПО под защищенный 32-битный режим, а в длинном режиме (AMD64) используется пара других соглашений, основанных на fastcall и имеющих несколько отличий друг от друга.
Так процедура или функция? Или подпрограмма?
Процедурное программирование предлагает делить код на подпрограммы, которые принято называть функциями и процедурами (функция обычно является наиболее понятным, частым и обобщенным названием, поэтому я его использую).
Процедура от функции отличается только тем, что функция возвращает какое-то значение (как в математике), а вот процедура этого не делает. Не во всех языках явно есть процедуры (в Pascal есть, но не в C). В таком случае их заменяют функции, возвращающие ничего (void, Unit, undefined, None).
Хотя и тут есть свои особенности. К примеру функция, возвращающая void в C и Java является прямым аналогом процедур, как-либо использовать возвращенное значение из такой функции невозможно, ибо его нет физически. А вот Unit в Kotlin это синглтон (а-ля единственная и уникальная константа уникального типа), ссылку на который можно присвоить в переменную, но в этом особого смысла нет. Undefined в JS и None в Python тоже уникальные константы специальных типов.
Но не тут-то было
Вроде бы процедура никогда не может ничего вернуть...
Только она этого никогда не делает напрямую. При она этом может записать результат в глобальную переменную, а еще часто принимает в себя указатели или ссылки, по которым может записать результат. Это еще удобно тем, что можно "вернуть" несколько значений. Пример:
void procedure(int x1, int *x2, int *x3) { // Функция ничего не возвращает, то есть это процедура *x2 = x1 * x1; // Разыменовываем указатель и записываем по его адресу результат. *x3 = x1 * x1 * x1; // Разыменовываем другой указатель и записываем по его адресу результат. } int y1, y2; procedure(16, &y1, &y2); // В y1 оказался результат, аналогичный прошлому примеру. А в y2 куб числа.
PS: оператор звездочка при указании типа превращает его в тип-указатель, а при применении на переменную-указатель разыменовывает ее до изначальной переменной. Амперсанд превращает переменную в указатель на нее (иногда еще называется оператором получения адреса).
То есть мы вернули сразу 2 разных значения из процедуры, которая якобы ничего не возвращает. Чудеса. Подобные чудеса есть в том числе в Pascal с явным делением на процедуры и функции (плюс там это сделано немного удобнее). Хотя механизм тут отличается от того, который используется в возврате значения из функции и совпадает с механизмом передачи обычных аргументов, поэтому никакой магии.
Еще про связь с математикой
Главное отличие функций в программировании от функций в математике в том, что они могут делать что-то на стороне и не обязаны возвращать одинаковый результат при одинаковых аргументах.
К примеру функция получения случайного числа по определению не может существовать в математике, если она не принимает в себя предыдущее случайное число или зерно для его видоизменения. Или функция записи в файл, возвращающая 0 в случае успеха и другое число при провале. Ко всему прочему, такая функция имеет побочный нематематический эффект, то есть запись в файл, что тоже недопустимо традиционной математикой без высоких абстракций.
Поэтому придумали чистые функции. По сути это ограничитель, которые делают функцию полным отражением таковой в математике. Им запрещено возвращать разные значения при одинаковых аргументах (точнее запрещено всё, что может такое позволить сделать), запрещено обращаться к нечистым функциям, запрещено обращаться к тому, что не является аргументом или локальной переменной, запрещены вообще любые действия, которые могут сделать что-то на стороне (даже функция sin() в C не всегда является чистой, ибо может зависеть от состояния FPU).
Чистые функции через ключевое слово pure явно есть в D и FORTRAN (проверка на чистоту во время компиляции), а также являются основой функционального программирования.
Чистая процедура тоже имеет право на жизнь, используя механизм со ссылками (на счет указателей не уверен из-за возможности арифметики над ними).
Функциональное программирование
Это очень сложная категория, которую постоянно путают с процедурным программированием. А еще это де-факто противоположный стиль: декларативный. Традиционное императивное программирование детально описывает процесс получения результата, а декларативное сам результат, без деталей реализации (хотя разделение обычно довольно нечеткое). При этом второй типичен для языков разметки типа HTML и CSS. То есть, условно, как одна и та же операция могла бы выглядеть в императивном и декларативном стиле:
document.tags.A.color = "blue" /* Императивный (JSSS). Сделать ссылки синими */
a { color: blue } /* Декларативный (CSS). Ссылки должны быть синими */
Почувствуйте разницу.
И функциональное программирование я никогда не изучал и слишком мало о нем знаю. Так что готовьтесь к ошибкам и не воспринимайте всё за чистую монету.
Внутри чистого функционального программирования
Основано полностью на математике, все функции обязаны быть чистыми. Операция присваивания запрещена (разрешены константы), переменных в привычном виде нет. Прикольно? Очень!
Во многих процедурных языках функции и процедуры являются объектами второго класса (не путать с классами из ООП), что не позволяет их свободно присваивать в переменные, передавать как аргументы в другие функции или возвращать из них (только через указатели). Функциональные языки расценивают функцию как объект первого класса, то есть их можно, а часто нужно передавать в другие функции напрямую.
Это дает некоторые преимущества, особенно в плане безопасности и при работе с многопоточностью (по причине неизменяемости данных и отсутствия глобального состояния), но вся концепция имеет один фатальный недостаток: вы мало чего полезного можете сделать, ибо что ввод, что вывод являются математически нечистыми, а потому запрещены. Вот такое вот гениальное изобретение безумных математиков.
Функциональное и процедурное программирование. Холст, масло
Каждый чисто функциональный язык выкручивается из этого по-своему, к примеру через монады. Это позволяет им существовать вне шуток и даже использоваться на практике.
Это не все особенности функциональных языков, но одни из самых важных. Самый известный такой язык: Haskell. Функциональные F#, Lisp, ML и многие другие не являются 100% чистыми.
Смешанное функциональное программирование
Последнее время часто используются смешанные языки, к примеру вместе с процедурным или объектно-ориентированным программированием, что избавляет от ограничений математики, но дает гибкость в том, что функциями можно оперировать как с любыми другими типами данных, а еще дает много очень удобного сахара вроде замыканий и лямбд. Это C#, Python, JS, частично Java и C++ (в них нужны костыли в виде интерфейсов из одного метода или оберток над указателями).
Особенности
Функции как объект первого класса. К примеру в C# это реализовано через систему делегатов, которые представляют из себя тип-обертку для функций:
Action<string> printer = Console.WriteLine; // Action<string> - тип-делегат. Неявно создаем его объект и присваиваем туда функцию printer("Hello, World!"); // Вызываем функцию через делегат
Локальные функции: как обычные, только вложенные в другую функцию (объявленные внутри нее)
void Func1() { // Глобальная функция void Func2() { // Локальная функция Console.WriteLine("В локальной функции"); } Func2(); }
Лямбды: возможность объявить безымянную функцию посреди кода (часто удобнее локальных)
var pow2AsLambda = x => x * x; // => - оператор лямбда-выражения pow2AsLambda(5); // Вернет 25
Замыкания (можно использовать вместе с лямбдами и локальными функциями):
int someValue = 42; var pow2AsLambda = x => x * x + someValue; // someValue будет захвачено замыканием, хотя напрямую не передано pow2AsLambda(5); // Вернет 67
Особенность замыканий в том, что они могут захватить локальную переменную родительской функции внутрь себя, продлевая ей время жизни за пределы блока с кодом. После чего такую лямбду можно передать в другую функцию, которая просто так не имеет доступа к someValue (в обычных условиях someValue вообще уже будет уничтожен), а вот переданная лямбда сможет ее прочитать или записать всегда и откуда угодно. И самое интересное то, что значение этой переменной будет сохранятся между вызовами к замыканию. То есть она становится глобальной, но видимой только из функции-замыкания.
ООП
Посмотрим на индекс TIOBE по популярности языков. Фиолетовым я пометил чистые функциональные языки. Языки, имеющие возможности, присущие функциональным языкам (смешанные) - синим, процедурным - красным, а объектно-ориентированным (ООП) - зеленым. Языки, в которых нельзя объявить функцию, принадлежащую самой себе или модулю, процедурными считать не совсем корректно (Java).
Не претендую на 100% точность
Видите фиолетовые точки? И я не вижу. А вот синих 12. В то же время красных и зеленых по 16. При этом реально применяемый чисто процедурный язык всего 1: С. Остальные имеют какую-либо встроенную поддержку других парадигм.
И сейчас практически всё большое ПО пишут в ООП. Ибо позволяет хоть как-то сдерживать структурированность кода после перехода с десятков тысяч строк к сотням (и миллионам).
Внутри
ООП является развитием идеи процедурного программирования. Проблема была в том, что помимо кода в программе существуют еще и данные, которые было бы неплохо связать с соответствующим им кодом. И через некоторое время после появления процедур появились структуры (они же записи в Pascal, не путать со структурным программированием).
Структуры были удобным способом объединить несколько переменных в единое целое. Пример:
typedef struct user_struct { char *username; int userid; int reputation; } user_t; user_t someuser = { "IvanKr08", 17002, 253 }; // Инициализируем значениями someuser.reputation += 10; // Обращаемся к полю
(Вместе с этим объявляем структуру новым типом через typedef, что не делается по умолчанию в C, в отличии от C++. Один из ярких примеров несовместимости C и C++):
После чего user_t превращается в новый тип, как int или char. Можно создать полноценную переменную типа user_t (или массив такого типа), а потом обратиться к какой-нибудь ее части (полю) через оператор точки (.). Можно сделать функцию, которая будет принимать или возвращать переменную типа user_t (к примеру someuser). Можно сделать указатель на user_t, тогда для обращение к полю используется оператор стрелки (->). Или можно встроить переменную типа одной структуры в другую, это тоже не запрещено. Внутри структурная переменная представляет из себя последовательно слепленные поля в одно целое.
Если поля разного размера, то часто добавляют пустоты между полей в целях выравнивания по размеру наибольшего поля и улучшения производительности, но это сложная тема
Возвращаемся к ООП
Суть ООП в том, что помимо полей структуры могут содержать процедуры и функции. Такая структура называется классом, переменная класса - объект, а процедура класса - метод.
Метод принципиально ничем не отличается от любой процедуры или функции, кроме того, что неявно принимает в себя специальный аргумент this (в Python это делается явно, в некоторых языках вместо this может быть self). Он содержит в себе ссылку/указатель на объект класса, от которого был вызван. Пример:
class User { public: char *username; int userid; int reputation;
void print() { printf("Пользователь \"%s\" (%i). Репутация: %i\n", this->username, this->userid, this->reputation); // Постоянно писать this-> не обязательно. Если локальной переменной с таким именем нет, то будет произведен доступ к полю } }; User someuser = { "IvanKr08", 17002, 253 }; // Инициализируем значениями someuser.print(); // Вызвали метод, print() неявно получил в себя указатель this, который указывает на someuser someuser.reputation += 10;// Обращаемся к полю someuser.print(); // Теперь вывод поменяется
Да, никто не мешает объявить обычную функцию, которая будет явно принимать в себя указатель на User и ничего не поменяется, и так повсеместно делают в C (тот же WinAPI на этом построен целиком и полностью), но ООП на самом деле крайне сложная, большая и холиварная тема, которая развивалась на протяжении 60 лет в разных направлениях и которую поднимать тут глупо. Только опишу еще несколько разновидностей методов:
Статичный метод - как обычный метод, только не получает в себя this и вызывается от имени класса, а не объекта (не someuser.method(), а User::method()). Зачем это нужно и в чем отличие от просто функции? Статичный метод можно спрятать за инкапсуляцией, плюс он привязан к классу, а не болтается в глобальном пространстве имен.
Конструктор - автоматически вызывается при создании нового объекта. Может иметь аргументы, тогда обязан явно вызываться как функция с именем класса, т.е. User("IvanKr08", 17002, 253)
Деструктор - есть в C++. В C# называется финализатором и имеет несколько важных отличий. Автоматически вызывается перед уничтожением объекта (к примеру выход из области видимости, явное удаление оператором delete или удаление сборщиком мусора в C#). Не может иметь аргументов.
Свойство - есть в Delphi и C#, куда перешел от первого. С точки зрения синтаксиса это поле, которое можно читать и записывать, только вместо прямого обращения в память вызываются соответствующие методы (set и get). Пример: ... int SomeProperty{ get =>42; set => Console.WriteLine("Set"); } ... test.SomeProperty = 10; // Значение никуда не сохранится, будет выведено "Set" в консоль Console.WriteLine(test.SomeProperty); // Всегда будет 42
Перегрузка оператора - специальный метод, который позволяет переопределить стандартные операции для типа. К примеру нельзя сложить два объекта User или User и число, но если перегрузить оператор +, то можно будет определить свою логику для этого (не обязательно, чтобы оно что-то реально складывало. Это может быть любое действие).
Индексатор - синоним перегрузки оператора квадратных скобок. Объект начинает вести себя как массив.
Функтор - перегрузка оператора круглых скобок (иначе называется перегрузкой оператора вызова функции). Позволяет "вызывать" объект так же, как и функцию. Звучит запутанно, но можно погуглить. Есть в C++. Также бывает функтор в функциональном программировании, но он не имеет ничего общего с функтором в C++.
Виртуальная функция (метод) - самый интересный и сложный тип, составляет основу полиморфизма. Вкратце объяснить его невозможно, но одним из ключевых принципов ООП является наследование, то есть один класс (наследник) копирует в себя все поля и методы другого класса (родитель), и может добавить новые. И суть в том, что указатель (ссылка) на объект класса-наследника может быть присвоен в переменную-указатель родительского типа. А самое интересное то, что любые обращения (к примеру вызовы методов), сделанные через родительский указатель, будут вести себя аналогично обращениям, сделанным напрямую через указатель с типом наследника. Это называется полиформизмом. Но это было бы не слишком полезным, если бы не возможность объявить метод в родительском классе виртуальным, а в наследнике полностью переопределить его код.
И то, что я забыл
Послесловие
Если вы осилили эти 3000 слов и даже смогли что-то понять, то найдите и распечатайте себе утешительную грамоту. А я устал и пойду искать смысл жизни...
UPD: исправлены косяки форматирования, очепятки, добавлено еще пару пунктов про разновидности методов и функциональное программирование.
Хотел написать про работу системы управления памятью, но понял, что для полноценной статьи у меня не хватает практических знаний. Поэтому кратко
Куча
Баян
Помимо стека, о котором было рассказано в прошлой статье и который содержит локальные переменные, в "потребительской" программе в среднестатистической ОС еще бывают глобальные переменные и динамическая память. Глобальные переменные описываются внутри сегментов .bss и .data, резервируясь в памяти сразу после загрузки образа программы (по-правильному это называется процессом).
Динамическая память иначе называется кучей, так как представляет из себя кучу динамических данных, который могут появляться и исчезать во время работы.
Для работы с динамической памятью в C существует malloc(size_t), запрашивающий (выделяющий, аллоцирующий) у ОС кусок памяти указанного размера и возвращающий указатель на первый байт, и free(void *), принимающий этот указатель и возвращающий (освобождающий) кусок назад в ОС.
В C++ есть операторы new и delete, они устроены гораздо сложнее и я не буду их рассматривать, ибо не пишу на нем. Помимо запроса памяти у ОС они занимаются ее инициализацией.
Если по какой-то причине стандартной библиотеки языка в наличии не имеется, то в Windows за память отвечает HeapAlloc и устаревшие GlobalAlloc и LocalAlloc из времен Windows 3.11 (malloc к ним и обращается внутри. Еще есть парные HeapFree, GlobalFree и LocalFree). Они предоставляют больше контроля над результатом, но проприетарны для Windows. А полный список функций для работы с памятью в WinAPI есть здесь, их сотни на любой случай жизни. Можно даже внутри контекста чужого процесса выделить или освободить кусок памяти (обычно такое используется вирусами и прочей нечистью).
Утечки
А что будет, если постоянно выделять, но не освобождать? А еще лучше: выделять, но терять адреса выделенной памяти. ОС достаточно быстро подскажет правильный ответ.
В теории есть несколько возможных выходов, можно даже обработать подобный случай и, к примеру, экстренно освободить всё выделенное (только если не терять указатели) или попытаться спасти нужные данные из разваливающейся программы, но если никаких действий не предпринять, то ОС заставит процесс экстренно прекратить свою деятельность и существование.
Случай с потерей указателей на выделенную память называется утечкой памяти и ее последствия необратимы. Но как только ОС завершает выполнение процесса, то вся его память, даже утекшая, становится снова свободной.
Ленивые программисты и мусоросборка
Отслеживание правильности выделения и освобождения памяти является достаточно сложной и опасной задачей, особенно если требуется обеспечивать стабильность и безопасность (лучше даже не думать, что будет, если рукожопый программист забьет на MISRA и устроит утечку памяти в подсистеме тормозов в Тесле), и практически невыполнимой в некоторых парадигмах (особенно в функциональном программировании). Поэтому еще с древнейших времен (с 1959 года) существуют языки со сборкой мусора:
Особенности
Первое: в таких языках указателей или нет вообще, или они очень ограничены в пользовании (C#).
Второе: для работы с динамической памятью вместо указателей применяются ссылки. Ссылка по факту представляет из себя тот же указатель, только очень сильно ограниченный. Над ссылками запрещены все операции, кроме присваивания значения другой ссылки и разыменования, в то время как указатели имеют права и возможности обычных целых чисел.
Третье: Каждый выделенный кусок памяти всегда занят каким-то объектом.
Четвертое: такие языки всегда тянут за собой подсистему сборки мусора, которая самостоятельно учитывает каждый объект и выделенный под него кусок памяти, а еще подсчитывает количество активных ссылок на каждый подчиненный объект. Это называется подсчетом ссылок.
Пятое: как только процент использованной памяти переходит через порог, то запускается сборщик мусора, который сканирует все объекты и количество существующих на них ссылок. Если ссылок на объект нет (то есть он "утек"), то его память освобождается.
Минусы и проблемы
Дорого. При злоупотреблении сборщик мусора может запускаться слишком часто, а каждый его запуск вносит заметный лаг. Особенно критично это в играх.
Он не устраняет все возможные способы вызвать утечку памяти, к примеру просто выделяя бессмысленные данные, но сохраняя на них ссылки. Или если объект, который управляется сборщиком мусора будет содержать в себе то, что сборщиком мусора не управляется (открытые файлы или нативная память вне его подчинения).
А еще он изредка может удалять то, что удалять не нужно, поэтому появляются костыли уровня GC.KeepAlive() (а-ля самый маленький метод в C#).
Но несмотря не это, практически все современные языки используют сборку мусора, кроме C++ и Rust. C++ по большей части перешел на умные указатели, которые тоже считают количество активных ссылок, но делают это без сборщика мусора. А Rust использует свой уникальный Borrow Checker, который понимают только растофилы.
С гоблинами на посту Олега разобрались, продолжаем сокращать список дел.
Перед отъездом с поста Хамако ещё успел перекинуться с Олегом парой слов по поводу найденного у агрессивного дикаря портрета девушки. Ийанна и Зироу были удивлены, что кинетик ни разу об этом не упомянул и спросили, как именно он собирается рассказывать Бокену (Девушка на портрете в кулоне похожа на Бокена, алхимика с первых игр) о том, что произошло с друидом, если окажется, что тот его родственник или друг. Хамако ненадолго задумался и сказал, что расскажет правду. В ней нет ничего плохого.
Нок-Нок очень хотел отправиться к Наргулуму, чтобы избавиться от соперника поскорее, но Хамако завуалированными аллегориями про колбасу и прочую еду (по-другому Нок-Нок отказывался понимать) убедил его отложить этот поход и отправиться к алхимику, как изначально и собирались.
К удивлению Ийанны, правда, что решил рассказать Хамако алхимику оказалась лучшим вариантом развития событий. Бокен поведал, что этот агрессивный и ненормальный человек его младший брат. В последнюю их встречу этот недоносок отрубил ему мизинец, а их мать избил, так что он точно не будет горевать по ублюдку. За такую, как казалось изначально, плохую новость, Бокен выдал в подарок хорошее зелье лечения. Помимо новостей, Хамако пригласил зельевара переехать в Ревланд, и, после недолгих уговоров, он согласился перебраться поближе.
Можно было возвращаться домой. Уже в Ревланде Хамако попросил у Каэсси дополнительные тренировки, ему казалось, что нынешних сил ему недостаточно. Во время этих занятий кинетик был отмечен стихией огня и обзавелся клеймом. Его фамильяр в виде маленькой струйки дыма также стал сильнее, обрел тело и трансформировался в антропоморфный дух огня (Огненный атронах Обливина, привет).
Набравшись смелости, Зироу в разговоре с Тристианом пригласила того провести как-нибудь свободный вечер вместе, если у него будет время. Юноша был приятно удивлен, даже пошутил, что обычно она только спрашивала у него совета и приходила излить душу, а тут что-то новенькое. Сразу после этой немного неловкой и милой беседы, едва Тристиан удалился, к плуту подошел Ландер Лебеда, демонстративно шутя по поводу их отношений и слухов. Девушка отреагировала весьма прохладно, но тут же, опомнившись, попросила его рассказать о темах, которые обычно поднимают на свидании. Ландер обозначил, что всё зависит от объекта интереса, так что, если речь о Тристиане, то его интересы это поэзия и чтение. Также эмиссар упомянул, что Октавия посоветовала жрецу одну книгу, и в данный момент он ей буквально зачитывается. Зироу, осознав, что поступила глупо, не обратившись к Октавии сразу, хлопнула себя по лбу и отправилась к девушке за советом.
Пока Зироу узнавала, как сблизиться с Тристианом, Ийанна наконец гаданием ответила ему на вопрос, принесет ли сделка с демоном зло или добро. Карты были довольно красноречивы, и, конечно, сделка принесет зло. Что ещё могла принести сделка со злым существом? В лучшем случае он просто не даст Ревланду ресурсов. Девушку результат расстроил не меньше Тристиана, и единственное ободряющее, что она смогла сказать, касалось баланса добра и зла в мире. Перед тем как уйти, заигрывающе извинилась за то, что заночевала у него в комнате после праздника. Тристиан в очередной раз повел себя очень мило, сказав, что в этой ситуации нет ничего страшного.
Всё ещё много дел не терпело отлагательств, поэтому было решено разобраться с непонятной кричащей феей, как её обозвали строители, мешавшей возводить лесопилку.
Добравшись до места вместе с Октавией, герои пообщались с работниками. Двое из них были под гипнозом и стояли около воды, остальные были очень насторожены. Во время разговора команда заметила, как над гладью небольшой заводи неподалеку появилась часть головы. Синяя кожа, длинные темные волосы, черные глаза - подводная дева показалась, чтобы повторно сообщить о своем недовольстве проводимыми работами. Она не раз просила убраться, но её не послушали. Никому не позволено рубить деревья в её доме! Эти работники заслужили их околдованное состояние! Ийанна смогла её убедить, что все они не желают зла, и ссориться с ней тоже не хотят. Чародейка пообещала восстановить деревья со временем, что водную фею Мелиансе не устроило, ведь это слишком долго. Так что она направила группу к дриаде по имени Эрисия, у которой есть особые жёлуди быстрорастущих деревьев.
Мен Ра своим обезьяньим зрением в два счета нашел место, описанное водной феей. Домом дриады был большой раскидистый дуб, с зеркально чистым прудом у корней. Магия поддерживала солнечную погоду под кроной дерева, что было весьма кстати, учитывая, что в пути герои попали под холодный моросящий дождь.
Дриада выслушала причину появления в её окрестностях. Немного подумав, она согласилась предоставить желуди, если команда разберется с деревом-косой, которое уже вкусило плоти дриад, а так же сильно ранило её мужа. Пара фей уже пыталась с ним сразиться и погибла. Дерево-коса - подобие разумного растения, способного к передвижению, питается кровью и плотью. Его можно найти к югу от её дуба, в темной, загрязнённой лощине. Оно боится топоров, как и все деревья, и особо падкое на сентиментальных, заботящихся о зелени существ.
Место обитания найти было несложно, гиблая атмосфера распространялась далеко за пределы самой лощины. Вокруг темнота, запах гниения и кучи различных костей. Множество очень сухих деревьев осложняло поиск врага, поэтому Хамако решил просто их все поджечь. План отвергли, потому что огонь слишком непредсказуемый, он мог перекинуться на лес неподалеку. Так что Ийанна воспользовалась стандартным методом ловли на живца. Девушка бродила между деревьями, причитала, как ей их жалко, как она хочет их выходить, и в один момент чудовище клюнуло. Только чародейка не ожидала, что удары будут столь сильны! Ийанна едва успела призвать дерево-защитника под удивленный крик Хамако. Он был в недоумении, зачем им было отправляться сюда, если теперь Ийанна умеет выращивать деревья за секунды самостоятельно. Несмотря на древесный щит и помощь от Зироу, Ийанна пала от двух мощных ударов. Октавия, испугавшись за подругу, начала неистово атаковать. Под натиском дерево быстро было побеждено, а Ийанну вернули в сознание. Разумеется, она выслушала нравоучение от Хамако, на которое с улыбкой ответила, что он явно за неё беспокоится, хоть и скрывает это. Хамако лишь хмыкнул, проигнорировав ехидный взгляд Октавии.
Вернувшись к дриаде, команда забрала пять желудей, а Ийанна взяла несколько растений для посадки в будущем своем саду. Также Ийанна получила в подарок веточку остролистой омелы, которая никогда не гниёт и не увядает. Приятный и неожиданный подарок.
Глупый был план, соваться к дереву без топоров. Хорошо, что Ийанна не умерла, хотя она была довольно близка к этому. Опять.
Комментарии