Русский
Русский
English
Статистика
Реклама

Ненормальное программирование

Перевод Чиптюн-музыка на ATtiny4 и трехцентовом Padauk

10.05.2021 16:07:24 | Автор: admin

Когда я услышал Bitshift Variations in С Minor Роба Майлза 16-минутный фрагмент 4-голосого полифонического аудио произведения мне очень захотелось воплотить такое аппаратно. Реализовать это на любом микроконтроллере слишком уж просто, поэтому я решил взять самый мелкий, какой смог найти ATtiny4. Чуть позже я портировал эту программу на небезызвестный трехцентовый микроконтроллер Padauk PMS150С.

Ах да при этом он полностью уместился в RCA-штекер и автоматически обнаруживает подключение.


Плата ATtiny4, внутренняя сборка и готовое устройство.


Как он работает


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

Генерация музыки


ATtiny имеет довольно мощный таймер/счетчик и выполняет двойную работу, генерируя как ШИМ-сигнал для аудио выхода, так и прерывания для генерации очередного PCM-сэмпла.
Говоря точнее, мы устанавливаем его в 8-битный не инвертированный ШИМ-режим без предварительного делителя и включаем прерывание переполнения. Это означает, что таймер отсчитывает от нуля до 255, используя ту же тактовую частоту 4МГц, что и ядро ЦПУ, ШИМ-выход поднимается от нуля до заданного коэффициента заполнения, и при достижении значения TOP сбрасывается к нулю, вызывая прерывание по переполнению.

Немного быстрых расчетов: самая высокая частота, на которой МК может работать, будучи запитанным от 3В таблетки, равна 4МГц. Эти 4МГц, поделенные на 256 шагов, дают нам базовую частоту ШИМ в 15.625КГц. Слегка откалибровав внутренний генератор, мы можем добиться ее округления до 16КГц. Поскольку частота дискретизации исходного тона равна 8КГц, то новый сэмпл нужно генерировать только раз в два переполнения/прерывания. Это оказывается весьма удобно, так как генерация сэмплов в итоге занимает немногим более 400 циклов.



На этом графике я отразил счетчик, его значение для сопоставления, итоговый выход ШИМ, а также прерывание по переполнению и коэффициент заполнения/выполнение подпрограммы sample, запускаемой этим прерыванием.

Ниже вы видите коэффициент заполнения и выход ШИМ на реальной микросхеме. Здесь отчетливо отражено, что в течение одного прерывания генерируется два сэмпла. (И этот вывод коэффициента заполнения/отладки позднее тоже пригодился, так как позволил откалибровать генератор через отслеживание частоты на осциллографе Rigol).


Канал 1 (нижний) показывает выход отладки, канал 2 (верхний) показывает сигнал ШИМ. Обратите также внимание на отображение частоты в верхнем правом углу.

Обнаружение подключения


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

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

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

Согласно спецификации, потребление энергии в таком состоянии ниже 0.15мкВ. Примерно с той же скоростью происходит саморазряд таблетки, так что хватить ее должно на годы.

Фильтрация выхода


При использовании базовой частоты ШИМ 16кГц есть один недостаток: она слышима. Изначально я собирался просто это игнорировать, но один друг справедливо убедил меня добавить RC-фильтр нижних частот.

Быстрый и грубый спектральный анализ, сделанный с телефона, подтвердил, что эта частота действительно шумит. Границу среза и кривую отклика я выбрал довольно произвольно (глядя на коробку под SMD-компоненты) около 8кГц, тем не менее с задачей она справляется отлично, так что при заказе итогового списка материалов я придерживался именно ее.


Соотношение сигнал-шум после добавления фильтра существенно улучшилось.

Программное обеспечение


Разобравшись с теорией, осталось только прописать код. Я решил вручную воспроизвести Си программу Роба на ассемблере AVR, отчасти ради забавы, отчасти в качестве (поспешной?) стратегии по оптимизации. У ATtiny нет аппаратного mul/div/mod, и мне потребовалось лишь немного правосторонних множителей/делителей, для чего я написал несколько специальных оптимизированных вариантов.

Начал я с нетронутой программы Си и сначала просто ее упростил, после чего заменил каждую операцию на макрос Си, реализующий соответствующую инструкцию машинного кода. После каждого малейшего изменения я генерировал PCM-поток и сравнивал его с заведомо корректным образцом, чтобы избежать ошибок. Каждое изменение автоматически отправлялось в репозиторий, в результате чего получилось 136 коммитов под именем new version. Только затем я добавил код инициализации и произвел запуск на реальном микроконтроллере.

На этом этапе, сам того не ведая, я допустил ошибку при написании одного из псевдо-ASM макросов: я инвертировал условие ветвления в mod3, в результате чего оно переключалось, когда не должно было, и наоборот. Это привело к невозможности распознавания голосов 3 и 4 на микроконтроллере. Причину ошибки мне удалось обнаружить только год спустя, когда я вновь вернулся к проекту после того, как в simavr, наконец-таки, появилась элементарная поддержка семейства ATtiny 10. Когда я запустил gdb(1), проблема тут же стала очевидной, и для патча потребовалась всего одна инструкция машинного кода.

Гибкие печатные платы



Гибкие печатные платы, которые можно обернуть вокруг батарейки

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

Все эти проблемы на раз решила гибкая печатная плата. Я спроектировал ее так, чтобы она оборачивала таблетку, при этом контакты батарейки находились внутри, а компоненты снаружи. Зажим, сделанный из скобы для бумаги, сдавливает этот сэндвич, обеспечивая тем самым его питание. Сама плата также изолирует батарею, для чего достаточно одного оборота скотча по кругу, и делает этот мини комплект достаточно жестким.

И все же нужно было еще проложить два провода, идущих от платы к точкам пайки на внутренней стороне RCA-штекера. Задача оказалась непростой, и я не сразу додумался, как скрутить две части корпуса штекера, не прокручивая провода. Решением стало максимально глубоко задвинуть плату в корпус и слегка провернуть провода против часовой стрелки, чтобы при закрытии корпуса они, наоборот, развернулись.


Стек слоев в KiCad. PDF-схема.

Портирование на Padauk


При использовании ATtiny меня не покидало ощущение, что я мухлюю: он снабжен сравнительно богатой периферией и содержит много очень гибких регистров (16), которыми можно управлять напрямую. К тому же у меня завалялся самодельный программатор для микроконтроллеров Padauk, который подогнал мне один из участников форумов EEVBlog, а также около 500 штук PMS150C.

Эти микроконтроллеры прославились своей невысокой стоимостью около 3 американских центов за экземпляр при приобретении в сравнительно небольших количествах. За свою цену они неплохо оснащены: ПЗУ на 1024 слова (программируемых один раз), 64 байта статического ОЗУ, 8-битный таймер с ШИМ, (несколько странный) 16-битный таймер, внутренний компаратор и источник опорного напряжения. По мнению некоторых, набор инструкций Padauk во многом следует более старой модели PIC, и большинство его операций происходят в одном накапливающем регистре.

Недостаток же в том, что производитель поскупился на документацию: спецификация не описывает, как мнемоника ассемблера сопоставляется с реальными единицами и нулями, то есть вы должны использовать их проприетарные (и доступные только для Windows) IDE, программатор и внутрисхемный эмулятор. Эта IDE даже не имеет компилятора Си, а использует странный язык, который они зовут мини Си: по сути, это ассемблер с позаимствованным из Си синтаксическим сахаром.

Талантливая группа любителей во главе с js_12345678_55AA, tim_ (cpldcpu) и spth (pkk), невзирая на отсутствие вышестоящей поддержки, создала впечатляющую и полностью открытую цепочку инструментов Си, включая компилятор, ассемблер, компоновщик, дизассемблер, симулятор, программное и аппаратное обеспечение программатора, а также низкоуровневую документацию.


Внутренняя сборка Padauk-версии

Для портирования чиптюнов на PMS150С потребовалось полностью перевести исходный Си-код в ассемблер, чтобы наилучшим образом использовать сжатые требования к циклам (в рамках которых я оставался с трудом: в худшем случае использовалось 507 из 512 доступных циклов). После того, как в процессе поиска правильного способа инициализации периферии я сжег тестовыми программами 5 схем, потребовалось еще всего 2, чтобы добиться полноценной отладки программы.

Итого семь микросхем, но при этом гораздо больше попыток: на деле можно программировать одноразовую память несколько раз при условии изменения только единиц на нули. Так что я оставил немного пространства под векторами сброса и прерывания, подставил новую версию кода и исправил инструкции GOTO, заменив их на NOP и добавив новый переход сразу же после. Скажу честно, я не столь скуп, но на неоднократную возню с ZIF-разъемом ушло бы больше времени, чем на этот обходной вариант.


Выделение некоторых отличий в версиях ассемблера: загрузка нот из памяти

Версия Padauk имеет небольшие отличия в сравнении с версией ATtiny: во-первых, здесь я задействую оба таймера, что позволяет использовать более высокую базовую частоту ШИМ (64кГц) и обойтись без ФНЧ. Во-вторых, внутреннее подтягивание Padauk достаточно высокое, и внешнее уже не требуется. Это означает, что мне удалось добиться полного отсутствия внешних компонентов.

И все же без сложностей не обошлось: t1sn M.n (тест старшего бита в статической ОЗУ и пропуск следующей инструкции) и set1 M.n (установка бита в области статической ОЗУ) работают только для первых 16 адресов; по данному поводу в спецификации толком ничего не сказано (заметил я это лишь потому, что в документации по реконструированным наборам инструкций присутствовало 4-битное адресное поле). В симуляторе микроконтроллера ucism было несколько ошибок, связанных с этими (и аналогичными) инструкциями, что слегка сбило меня с пути (патчи я отправил в список рассылки).

При этом также наблюдалось странное поведение режимов прерывания и ожидания timer16, разобраться с которым мне помог JS. В остальном же весь процесс оказался достаточно плавным, и все благодаря качественной работе упомянутых выше ребят.


Обратите внимание на повышенную частоту ШИМ и более интенсивное использование ЦПУ в сравнении с версией ATtiny.

Я не стал озадачиваться созданием новых печатных плат; у меня оставались варианты без ФНЧ, и я просто оставил внешний подтягивающий резистор неподключенным.

Живое демо



Видео одного полного проигрывания музыки. Начало несколько затяжное, но с 1:35 становится интереснее.

Примечания


Считаю необходимым упомянуть проект Noiseplug от DoJoe. На него я наткнулся при поиске информации по ATtiny и могу сказать, что по своей сути он похож на мой.

Я собрал себе макетную плату из платы-переходника, поскольку шаг ее контактных площадок в точности соответствует одной стороне форм фактора SOT23-6. Вторую сторону я после подключил проводами. В другом, более раннем, варианте макетной платы использовалась миниатюрная адаптерная ATtiny, которую я приклеил на общую панель плат-переходников, чтобы ее расширить. Последнюю из них я представил на 35C3.




Макетные платы

Гибкие платы я заказал с OSHPark.com, и обошлись они примерно по доллару за штуку. Заказ был обработан довольно быстро, правда некоторые из них пришли с дефектами травления.

Подробнее..

Перевод Интеграция собственных шрифтов в ПЗУ VGA-карты

27.05.2021 22:07:59 | Автор: admin

История об изысканиях возможных путей использования в видеокарте собственных шрифтов, которая увенчалась их полной интеграцией в ПЗУ VGA. На этом пути рассматривались различные варианты, возникали определенные сложности, но в конечном итоге старое доброе отображение шрифта было получено!

Предыстория


Когда-то давно у меня была видеокарта ISA VGA Chips and Technologies, в которой использовался приятный шрифт. Позднее мне довелось владеть еще одной интересной картой ATI EGA с красивым шрифтом. И сейчас у меня установлена ATI VGA Wonder 16 тоже с весьма привлекательным шрифтом. Суть в том, что все названные ISA-видеокарты очень медленные.

Первые две я продал, а последнюю все же решил оставить. Время от времени я ностальгирую по экземпляру от Chips and Technologies. Я так и не понял, почему ПК всякий раз выдавал звуковой сигнал ошибки VGA, хотя работала карта при этом отлично.

Ну да ладно, вернемся к современности. Для моих изысканий в области разработки самодельных систем необходима быстрая ISA-видеокарта. Исходя из этих соображений, я купил новую (!) залежавшуюся (и при том дорогую) Tseng Labs ET4000/W32i с 2Мб ОЗУ у парня по имени Тед Аллен, владельца Micro-Labs Inc. в США. Работает эта карточка идеально. Я без проблем могу запускать игры под MS-DOS, включая Quake в разрешении 320х200.

И все же, поскольку я до сих пор много программирую в MS-DOS, мне зачастую недостает шрифтов тех видеокарт, на которых я учился писать код. В купленной же мной карте Tseng Labs используется монотонный (считай квази-скучный) современный типовой шрифт из ПЗУ. Он не столь плох, как шрифты PCI-видеокарт, но все же.

Вступление


Шрифты в ПЗУ VGA не так уж легко заменить. Однако есть быстрый способ, который позволяет загружать собственный шрифт в ОЗУ VGA. Это делается с помощью небольшой резидентной программы, которая обеспечивает использование видеокартой заданного шрифта. Но по какой-то причине после запуска нескольких программ MS-DOS, устанавливающих собственные шрифты (например, HWiNFO, NSSI, NU), или программ графического режима резидентная утилита дает сбой, и карта возвращается к исходному шрифту из ПЗУ. Кроме того, она занимает 4096 байтов ОЗУ под данные шрифта и еще 368 байтов под программный код.

Чтобы решить эту условную проблему с памятью, я поместил резидентную программу в AUTOEXEC.BAT. Теперь эти 4.3Кб загружаются в свободный блок верхней области памяти (UMA). Для этого также должен быть загружен EMM386.EXE или аналогичный драйвер расширенной памяти.

Программное обеспечение


Но как же я создал свой собственный шрифт? Что ж, сначала я использовал FONTEDIT.COM. К сожалению, эта программа может генерировать лишь .COM файлы без возможностей резидентной программы. Может, я просто не знаю, как это делается? Еще одна проблема в том, что размер исполняемой программы получается около 8Кб.

Тогда-то я и нашел в интернете новую интересную утилиту Fontraption, написанную VileR. Он создал настолько грамотный продукт, что я оценил его уже сразу после загрузки.

Плохо лишь то, что Fontraption при отрисовке моей Tseng Labs ET4000/w32i выглядит как-то странновато. Окно редактирования шрифта вроде в порядке, но было бы лучше, если бы весь интерфейс прорисовывался также четко.



Очевидно, что VileR создавал эту программу с удовольствием. Он даже внедрил в нее техники управления палитрой, добавив таким образом комбинации цветов, которые по умолчанию на VGA-картах недоступны. Лично у меня с этой темой ассоциируются далекие воспоминания. Это был где-то 1997 год, когда я работал на 80286 ПК, подключенном к янтарно-монохромному ЭЛТ-монитору Tandon. Он был очень похож на газоразрядный дисплей, который, в свою очередь, походил на созданную VileR палитру Plasmatics.



Дополнено позднее: VileR нашел способ исправить баг и отправил мне обновленную версию Fontraption. Теперь она работает прекрасно, верно загружая шрифты из ПЗУ VGA. Исправление оказалось простым, но сама суть проблемы весьма интересна. Как сказал VileR:

[...] при обращении к VideoBIOS для загрузки встроенных шрифтов BL устанавливает целевой блок в карте 2 ОЗУ VGA (от 0 до 7), а для шрифта 8 х 16 у меня по случайности было установлено значение BL=8. Невалидно, но работает с большинством BIOS, поскольку они выполняют для этого значения операцию AND с 7. Тем не менее Tseng этого не делает, и шрифт не применяется, в результате чего программа при считывании из ожидаемого блока (0) получает мусор []

И вот теперь я думаю, что именно хотели преодолеть те программисты BIOS с помощью этой странной операции AND в нижней части регистра BX. Должна быть какая-то причина, но вскроется она наверняка нескоро.



Ну да хватит лишних слов, продолжим.



Таким образом, я приступил к разработке шрифта моей мечты. Кому-то он покажется красивым, кому-то, наоборот, страшным. Но мне он очень нравится. Это первый вариант шрифта, и я доволен. Еще многое предстоит проделать. Я проделал над этими шрифтами много работы, и теперь они вполне готовы к использованию. В основу моего дизайна лег оригинальный шрифт, который я извлек из ПЗУ ET4000/W32i. После я усердно старался вспомнить, как же именно выглядели старые шрифты, и опирался при этом на образец из ПЗУ ATI VGA Wonder 16.

Я экспортировал этот шрифт в виде исполняемой резидентной программы и радостно использовал его около года. Несколько же дней назад, работая над собственной реализацией ПЗУ BIOS для интерфейса ввода/вывода ISA, я подумал о включении этого шрифта в реальный код VGA BIOS. Это, конечно, может быть опасной и безумной затеей, но вполне выполнимой.

Для ее реализации я экспортировал сырой шрифт в двоичный файл с помощью команды Fontraption [^S]ave... F2. Отлично, наличие Fontraption фактически избавило меня от необходимости писать собственный инструмент для извлечения и управления шрифтом из ПЗУ.

Итак, я начал анализировать VGA BIOS своей Tseng Labs, для чего мне понадобился дамп ПЗУ-памяти. Не так давно я написал небольшую утилиту, способную считывать содержимое ПЗУ, код которой вместе с двоичным файлом загрузил на GitHub.

Изначально я создавал эту программу под использование с EEPROM 28C64B на моем интерфейсе ISA, но единственное существенное отличие здесь в наличии кода защиты от записи (SDP). Используется же он только для задач, связанных с записью EEPROM. В реальности эта программа может считывать любую ПЗУ, отображенную в ПК, при условии, что известен адрес отображения, и ее размер не превышает 32768 байтов.

GitHub Репозиторий: https://github.com/agroza/eepromrw
Основная программа: eepromrw.pas

Для получения ПЗУ VGA BIOS я использовал эту команду:

eepromrw.exe -read -addr=C000:0000 -size=8000 -file=vgarom.bin

Она считывает 32768 байтов (8000 в hex-системе) из адреса, обозначенного сегментом C000: 0000, в двоичный файл VGAROM.BIN.

После я написал еще один быстрый и грубоватый инструмент, сканирующий файл VGAROM.BIN в поиске стартовой точки размещения шрифта 8 x 16 в ПЗУ. В памяти Tseng Labs этот шрифт начинается в смещении 17984 (461C в hex-системе). На данный момент я не знаю, является ли это смещение стандартным для шрифта 8 х 16, хотя, меня это не особо волнует, так как моя цель заменить исходный шрифт из ПЗУ на собственный.

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

GitHub Репозиторий: https://github.com/agroza/romfontr
Основная программа: romfontr.pas

Новый шрифт в ПЗУ VGA BIOS я вшил с помощью этой команды:

romfontr.exe -offset=461C -romfile=vgarom.bin -fontfile=ag868x16.bin

Она записывает 4096 байтов (1000 в hex-системе) данных шрифта из AG868X16.BIN в заданное смещение (461С в hex-системе) файла VGAROM.BIN.

Аналогичные операции я проделал для всех размеров шрифтов и всех смещений, где эти шрифты могут находиться:

  • [12015 (2EEF)] данные шрифта 8 x 14 размером 3584 байтов;
  • [15900 (3E1C)] данные шрифта 8 x 8 размером 2048 байтов;
  • [17948 (461C)] данные шрифта 8 x 16 размером 4096 байтов.

Пока все хорошо, но есть еще один нюанс. ПЗУ VGA BIOS это дополнительная ПЗУ, и у нее есть контрольные суммы. Если System BIOS не находит действительную контрольную сумму в конце ПЗУ, то отказывается ее инициализировать. В результате никакого вывода видео, несмотря на нетронутость кода VGA BIOS и наличие шрифтов 8 x 16.

Теперь пора писать еще одну программу. Так, стоп У меня уже есть такая, которая генерирует 8-битные контрольные суммы ПЗУ. Писал я ее для Windows, но она вполне подойдет, так как для программирования микросхем я все равно использую именно эту ОС. Данная программа также загружена на GitHub.

GitHub Репозиторий: https://github.com/agroza/romcksum
Основная программа: romcksum.dpr

Вычисление и обновление ПЗУ VGA BIOS я делал с помощью этой команды:

romcksum.exe -o -vgarom.bin

Она вычисляет 8-битную контрольную сумму всех данных ПЗУ и соответствующим образом обновляет последний байт. Более того, дополнительный параметр -O сообщает программе, что файл нужно проверять как дополнительную ПЗУ.

Дополнено позднее: я добавил функции вычисления контрольной суммы и обновления непосредственно в программу ROM Font Replacer.

Это означает, что теперь команда, которую я мог использовать для вложения нового шрифта в ПЗУ VGA BIOS и вычисления -> обновления 8-битной контрольной суммы, выглядит так:

romfontr.exe -u -romfile=vgarom.bin -fontfile=ag868x16.bin

Такая возможность все ускоряет и позволяет пропускать промежуточный шаг использования программы ROM Checksum Calculator.

Теперь файл ПЗУ BIOS видеокарты Tseng Labs ET4000/W32i содержит мой собственный излюбленный шрифт 8 х 16 в качестве основного для всех текстовых режимов. На этом программная часть заканчивается и можно переходить к аппаратной.

Реверс инжиниринг ПЗУ VGA BIOS


В первую очередь мы займемся реверс-инжинирингом. Но почему? Потому что, если карта установлена в режим 03h, то она автоматически заменяет несколько выбранных глифов на кастомные, которые расположены после 4096 байтов данных шрифта 8 х 16. Не уверен, какая логика стоит за этим, но решение это очень интересно. Странность в том, что если переключить карту в режим 11h или 12h, то шрифт 8 х 16 используется корректно, и никакие глифы не заменяются.

Эту тайну быстро раскрыл мистер VileR. Я начал с ним беседу по поводу проблемы с отрисовкой Fontraption и попутно рассказал о глифах. Он сказал, что это альтернативные глифы шрифта для режимов 9 х 16. Я знал о них еще в 90-х, но мне было неизвестно, где и как они хранятся в ПЗУ.

Очевидно, что есть таблицы шрифтов 9 х 14 и 9 х 16, расположенные сразу после стандартных таблиц 8 х 14 и 8 х 16. В частности, каждому типу глифа предшествует байт, обозначающий код символа ASCII. В процессе обратного инжиниринга вскрылся еще один интересный факт, а именно то, что эти альтернативные таблицы шрифтов завершаются нулевым байтом.

Поэтому я дополнительно создал расширенные варианты шрифтов 9 х 14 и 9 х 16, после чего написал еще одну программу, которая выделяет эти расширенные глифы из массива данных шрифтов и записывает их в отдельные файлы в формате, ожидаемом процедурами отрисовки ПЗУ VGA ROM. Затем я совместил эти файлы с измененной ПЗУ VGA ROM и повторно вычислил байт контрольной суммы.

Структура всей области данных шрифтов ПЗУ VGA видеокарты Tseng Labs ET4000/W32i такова:

  • [12015 (2EEF)] данные шрифта 8 x 14 размером 3584 байтов;
  • [15599 (3CEF)] альтернативные данные шрифта 9 x 14 (19 глифов) размером 300 байтов;
  • [15899 (3E1B)] нулевой байт (0);
  • [15900 (3E1C)] данные шрифта 8 x 8 размером 2048 байтов;
  • [17948 (461C)] данные шрифта 8 x 16 размером 4096 байтов;
  • [22044 (561C)] альтернативные данные шрифта 8 x 16 (18 глифов) размером 323 байта;
  • [22367 (575F)] нулевой байт (0).

Общий размер данных шрифтов ПЗУ VGA составил 10353 байта.

Я закончил изменение обоих пакетных файлов MS-DOS, которые использовал для тестирования, и теперь они записывают все данные шрифтов последовательно в файл VGAROM.BIN. Ниже приведено исполняемое содержимое этого файла, который также вычисляет и обновляет 8-битную контрольную сумму.

romfontr.exe -u -offset=2EEF -romfile=vga.bin -fontfile=ag868x14.binromfontr.exe -u -offset=3CEF -romfile=vga.bin -fontfile=ag869x14.binromfontr.exe -u -offset=3E1C -romfile=vga.bin -fontfile=ag868x8.binromfontr.exe -u -offset=461C -romfile=vga.bin -fontfile=ag868x16.binromfontr.exe -u -offset=561C -romfile=vga.bin -fontfile=ag869x16.bin

Теперь у меня есть полноценный файл ПЗУ VGA BIOS со всеми новыми шрифтами и их альтернативными представлениями.

Работа с платой


Ниже вы видите мою Tseng Labs ET4000/W32i от Micro Labs. Мне очень нравится ее компоновка. Помню, что спрашивал Теда, какую программу CAD он использовал, но его ответ уже позабыл. Было бы здорово задокументировать и это тоже для истории.

В данной видеокарте использован неизвестный тип EEPROM. Неизвестный, потому что на ней наклеен красивый фиолетовый лейбл с надписью Made in America, который я не хочу ни сдирать, ни портить. В итоге узнать тип установленной EEPROM не получается. Однако шелкография на печатной плате гласит, что это 27C256. Вообще-то, я бы итак мог поспорить, что установлена именно 27C256 (или совместимая), потому что большинство, если не все, VGA-карты оснащались ПЗУ на 32Кбит.



Время знакомиться с видеокартой подробнее. Кроватка ПЗУ здесь одна из самых дешевых. Так как произведена карта в США, я бы ожидал, что Micro Labs используют Mill-Max или нечто подобное. Но они все же пошли по пути снижения стоимости, как это было свойственно производственным компаниям в то время. Тем не менее под кроваткой мы видим надпись: PCB MADE IN HONG KONG.

Ага! У США были (и еще есть) отличные предприятия по производству печатных плат. Но их услуги выливаются в приличную копеечку, которая в итоге отражается на стоимости конечного продукта. Как ни крути, а в конце-концов все производственные расходы оплачивает именно покупатель. Плата сделана добротно не лучше и не хуже большинства компьютерных плат начала 90-х. Однако она явно не сравнится по качеству с платой моей ATI VGA Wonder 16.



За генерацию тактовых частот карты отвечает интегральная схема CHRONTEL CH9294. Такие до сих пор можно приобрести на различных сайтах. В качестве RAMDAC* использована AT&T ATT20C490-80. Эта деталь тоже вполне доступна онлайн, как и микросхема ET4000/W32i. Тут я даже призадумался: А не собрать ли мне собственную VGA-карту? У меня все еще имеется каталог спецификаций Tseng Labs, и этот VGA-контроллер вместе с образцом схемы подробно в нем описан. Я бы даже мог улучшить дизайн, использовав современную более дешевую и доступную память. Но это уже отдельная тема, и пока она остается лишь пищей для размышлений.

*RAMDAC (сокр. от Random Access Memory Digital-to-Analog Converter) это часть компьютерной видеокарты, которая преобразует цифровые данные в аналоговый сигнал, отображаемый на мониторе компьютера.



Память представлена 16 микросхемами NN51425P производства NPNX, Япония. Честно, я ранее не слышал ни об этой компании, ни о подобных микросхемах памяти. В моем экземпляре карты один из модулей, а именно U12, не соответствует остальным и рассчитан не на 45 нс, а на 50.

Далее я демонтирую кроваткумикросхемы ПЗУ VGA BIOS. Эта операция не для трепетных сердец, но мне уже доводилось проделывать такую на других компьютерных платах. Я планирую использовать качественную кроватку Mill-Max класса AUGAT. Это упростит дальнейшее экспериментирование с различными микросхемами ПЗУ VGA BIOS.







После вмешательства сторона пайки смотрится вполне себе неплохо, и сама видеокарта будто только сошла с заводского конвейера.



Далее я запрограммирую микросхему EPROM 27C256-15 от Texas Instruments, допускающую стирание с помощью УФ. Для этих целей я использую универсальный программатор TL866II Plus, который мне очень полюбился. По началу после его покупки меня терзали некоторые сомнения, но после они полностью развеялись. Единственная сложность теперь это стирание EPROM. УФ-лампы у меня под рукой нет, но можно сымпровизировать, разбив стекло ртутной газоразрядной лампы на 250Вт.


Старая ПЗУ и новая EPROM

Эта ртутная лампа является мощным источником УФ-излучения. И поскольку у меня нет электронного балласта 250Вт, то в качестве его альтернативы я использую три вольфрамовых лампы накаливания (две по 75Вт и одну на 100Вт), соединив их последовательно с ртутной.



Очевидные рекомендации:

  • Никогда не смотреть на источник УФ-излучения, так как это может вызвать необратимую слепоту.
  • Разбивание стеклянной колбы также очень опасно, поскольку пары ртути чрезвычайно вредны для дыхательной системы.
  • Работа с оголенными внутренностями ртутной лампы представляет высокий риск поражения током и тем самым угрозу для жизни.

В моем случае риски минимизированы, так как опыт обращения с подобным инструментом у меня есть. В дальнейшие подробности я вдаваться не буду, но самое важное отмечу.

С этим импровизированным устройством я оперирую вне дома, так как УФ-излучение ионизирует воздух, производя озон. На улице я размещаю схему EPROM под ртутной лампой при отключенном питании. После этого удаленно подаю напряжение, находясь в нескольких метрах от места. Никогда нельзя исключать возможный риск взрыва ртутной лампы. С момента подачи питания я отсчитываю пять минут и выключаю устройство. Затем тестирую EPROM, определяя, все ли байты перешли в состояние FF. Если стирание произошло не полностью, я облучаю ее еще минуту и повторяю тест. Подобные одноминутные циклы я повторяю до тех пор, пока память не будет полностью стерта. Обычно мне удается добиться этого за 7-8 минут.

Дополнение: ночное сжигание битов с байтами. На меня нахлынула волна энтузиазма, и я увлекся стиранием еще нескольких EPROM, подготовив их к новому микрокоду. Все фото, где наблюдается УФ-излучение, сделаны удаленно при помощи автосъемки.





Отлично, EPROM очищена. Все биты установлены на 1 (иначе говоря, все байты в hex-системе сейчас выражены как FF). Микросхему я запрограммировал обновленным файлом ПЗУ. Кроватка на месте, и EPROM встает в него впритирку.



Время проверять карту. Ниже вы видите полученный результат. Большую часть времени я провожу в DOS-навигаторе и IDE Pascal, поэтому вполне естественно, что мне нравится, когда экран выглядит именно так.





Я уже и забыл, какая это морока фотографировать ЭЛТ-дисплеи Очень уж сложно не поймать линию развертки электронного луча где-нибудь в середине экрана.

Теперь все работает отлично. Никаких больше сбросов шрифтов после запуска HWiNFO. Да и вообще изображение мне очень нравится, так что на этом данный опыт можно завершать.

Надеюсь, что для вас он оказался интересен.


Подробнее..

Перевод SB181 логический вычислитель на базе АЛУ 74LS181

10.06.2021 16:07:13 | Автор: admin


Уже какое-то время в моей мастерской дожидаются своего проекта пара микросхем АЛУ 74LS181. Но так как мысль о создании на их базе целого процессора была несколько пугающей, я решил задействовать эти чипы в роли логического вычислителя: своеобразного 8-битного калькулятора, который получает шестнадцатеричные входные данные и отображает результаты различных логических операций в двоичном и hex-форматах. Будучи собранным исключительно на базе логики микросхем 74-й серии без какого-либо микроконтроллера или ЦПУ, такое устройство оказывается удобным помощником в 8-битном программировании.

Описание схемы


Микросхема шифратора 74C923 на 20 клавиш (хотя подключена она только к 16) получает пользовательский ввод. В зависимости от положения движкового переключателя вводимое число сохраняется в регистре А, регистре В или функциональном регистре. Переключатель просто перенаправляет стробирующий импульс Data Available из 74С923 на вход синхронизации соответствующего регистра. Резистор подтягивает входы синхронизации каждого регистра на высокий уровень, исключая их произвольное срабатывание в пассивном состоянии. Этот импульс срабатывает до появления на выходе фактических данных, поэтому я просто инвертировал данный сигнал через элемент И-НЕ, чтобы регистры защелкивались на заднем фронте. В противном случае защелки всегда сохраняли бы предыдущую введенную цифру.


Небрежно проложенные провода

Регистры А и В являются 8-битными защелками, использующими 74HC273. Каждое нажатие кнопки сдвигает 4 нижних бита регистра в 4 верхних, и в то же время значение кнопки загружается в нижние 4. Это позволяет вводить 2 шестнадцатеричные цифры одну за другой, как это делается на калькуляторе. Затем содержимое этих двух регистров передается на входы операндов А и В АЛУ 74LS181. Эти сохраненные значения регистров также отправляются на дисплейную плату, где hex-значение отображается двумя TIL311, а двоичное при помощи светодиодной гистограммы.

Функциональный регистр это 4-битный 74HC175. Несмотря на то, что АЛУ 74LS181 поддерживает множество функций, практическое применение имеет лишь их ограниченное число. Поэтому в данном случае я выбрал те же 16 функций, что и agp.cooper. Отображение вывода кнопочной панели 0-15 в разные значения, требуемые на входах S-функции микросхемы 74LS181, реализуется с помощью EEPROM. Это также означает, что можно выбирать различные функции или их порядок на кнопочной панели просто перепрограммируя ее. То есть я могу приблизительно сгруппировать функции каждой кнопки, расположив простые внизу, а более сложные сверху. В таком случае EEPROM просто обработает этот перенос.

4-битный вывод функционального регистра отправляется обратно на плату клавишного блока. Затем дешифратор 74HC4514, подключенный к светодиодам в каждом переключателе блока клавиш, подсвечивает текущую выбранную функцию.


Логическая плата с тремя регистрами-защелками, EEPROM и двойной 74LS181

Другая кнопка подключена к схеме защелки, основанной на логическом элементе И-НЕ. Это позволяет устанавливать входной сигнал переноса для нижнего 74LS181. Ширина каждого из этих двух чипов АЛУ составляет всего 4 бита, но подключение выхода переноса нижнего к входу верхнего дает уже 8-битное АЛУ. Здесь мне не нужно озадачиваться схемой ускоренного переноса, поскольку это полностью статичная настройка, где не требуется отслеживать синхронизацию или другие тайминги.

Затем сигналы выхода F АЛУ отправляются на плату дисплея, где снова отображаются в hex- и двоичном форматах. Дополнительный светодиод показывает, был ли сгенерирован верхним 74LS181 выход переноса.


Дисплейная плата

Сборка


SB181 состоит из 3 отдельных печатных плат. Это позволило добиться относительно компактного размера корпуса, а также вписаться в стоимость по $2 за JLCPCB.

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

Используя 3-позиционный движковый переключатель, те же 16 кнопок можно задействовать для ввода различных операндов или применяемой к этим операндам функции.

Основная логическая плата содержит всего 3 регистра-защелки, функциональную EEPROM и пару АЛУ 74LS181. Вывод этих компонентов передается на плату дисплея через шлейф.
Дисплей постоянно показывает значения двух операндов в регистрах А и В, а также вывод текущей функции АЛУ.

Микросхемы TIL311 представляют красивый шестнадцатеричный дисплей. Расстроила меня лишь их стоимость, а также характеристики энергопотребления, которые не позволили запитать устройство по USB. В результате я использовал стандартную схему импульсного источника питания, которая получает 12В при 750мА и понижает их до 5В. Гистограммные дисплеи тоже отлично справляются с отображением двоичных данных, для чего задействуют 8 из 10 доступных светодиодов. Один из оставшихся при этом используется для индикации выхода переноса.

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

В своей схеме я смешал оси X и Y шифратора клавиш, поэтому для обеспечения правильного расположения чисел пришлось проложить несколько проводов. Я также забыл, что выход переноса 74LS181 активируется низким уровнем сигнала, в связи с чем пришлось сделать перенаправление, пробросив на плате отдельный провод.

Детали корпуса сделаны из алюминия, а в качестве крепежей использованы самозажимные гайки. Боковые стенки я изготовил из ламинированного под дуб МДФ, что придало устройству ретро-подобный облик.


Внутренняя компоновка плат. Импульсный источник питания скрыт за угловым алюминиевым сегментом, расположенным справа

Схемы


Плата клавишного блока
Логическая плата
Дисплейная плата


Подробнее..

Перевод Портируем Quake 3 на Rust

14.06.2021 20:18:14 | Автор: admin


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

Тестирование на небольших программах командной строки постепенно устаревает, поэтому для переноса мы выбрали ничто иное, как Quake 3. Стоит ли говорить, что буквально через пару дней мы уже были первыми, кто играл в Rust-версию этого популярного шутера.

Подготовка: исходники Quake 3


Посмотрев оригинальный код Quake 3 и различные форки, мы остановились на ioquake3. Это форк коммьюнити, который до сих пор обслуживается и строится на современных платформах.

Для начала мы убедились, что сможем воссоздать проект как есть:

$ make release

Сборка ioquake3 создает несколько библиотек и исполняемых файлов:

$ tree --prune -I missionpack -P "*.so|*x86_64". build     debug-linux-x86_64         baseq3            cgamex86_64.so          # клиент             qagamex86_64.so         # игровой сервер            uix86_64.so             # ui         ioq3ded.x86_64              # исполняемый файл выделенного сервера         ioquake3.x86_64             # основной исполняемый файл         renderer_opengl1_x86_64.so  # модуль рендеринга opengl1         renderer_opengl2_x86_64.so  # модуль рендеринга opengl2

Клиентскую, серверную и UI библиотеки можно собрать в виде Quake VM либо как нативные общие библиотеки x86. Мы предпочли второй вариант. Переносить VM на Rust и использовать версии QVM было бы существенно проще, но задачей было протестировать C2Rust максимально тщательно.

Сосредоточились мы на UI, игровом сервере, клиенте, модуле рендеринга OpenGL1 и основном исполняемом файле. Можно было также перевести модуль OpenGL2, но мы решили его не трогать, так как он активно использует файлы шейдеров .glsl, которые система сборки включает в исходники Си в виде строковых литералов. Конечно, можно было добавить поддержку скрипта кастомной сборки для встраивания кода GLSL в строки Rust после транспиляции, но нас остановило отсутствие надежного автоматического способа транспилировать эти автогенерируемые временные файлы.

Вместо этого мы перевели библиотеку рендеринга OpenGL1 и определили ее использование игрой в качестве предустановленного отрисовщика. Мы также решили пропустить файлы выделенного сервера и missionpack. Перевести их было несложно, но для нашей демонстрации они не требовались.

Транспиляция


Чтобы сохранить используемую в Quake 3 структуру каталогов и не прибегать к изменению ее исходного кода, нужно было создать в точности такие же двоичные файлы, что и в нативной сборке, то есть четыре общие библиотеки и один двоичный файл. Поскольку C2Rust использует для сборки файлов Cargo, каждому исполняемому файлу требуется собственный контейнер Rust с соответствующим файлом Cargo.toml. Чтобы C2Rust на выходе создал для каждого исполняемого файла контейнер, ему нужно предоставить список двоичных файлов вместе с соответствующими им объектными или исходными файлами, а также вызов компоновщика, определяющего прочие детали, такие как зависимости.

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

В большинстве инструментов, создающих такую базу данных, подобное ограничение присутствует преднамеренно, например cmake с CMAKE_EXPORT_COMPILE_COMMANDS, bear и compiledb. Насколько нам известно, единственным инструментом, включающим команды компоновки, является build-logger из CodeChecker, который мы не задействовали только потому, что узнали о нем после написания собственных оберток (приводятся ниже). Это означало невозможность использовать файл compile_commands.json, создаваемый любым типовым инструментом для транспиляции мультибинарной программы Си.

Тогда мы решили написать собственные скрипты оберток, которые внесут в базу данных все необходимые вызовы компилятора и компоновщика, а затем преобразуют их в расширенный compile_commands.json. Вместо обычной сборки, запускаемой командой:

$ make release

Мы добавили обертки для перехвата процесса сборки:

$ make release CC=/path/to/C2Rust/scripts/cc-wrappers/cc

Обертки создают каталог с файлами JSON, по одному файлу за вызов. Второй скрипт агрегирует все эти файлы в новый compile_commands.json, который содержит уже и команды компиляции, и команды сборки. Мы расширили C2Rust для считывания компонующих команд из базы данных и создания отдельного контейнера для каждого связанного двоичного файла. Кроме того, теперь C2Rust считывает зависимости каждого исполняемого файла и автоматически добавляет их в файл build.rs его контейнера.

В качестве облегчения процесса все исполняемые файлы можно собрать за раз, если они будут находиться в одном рабочем пространстве. C2Rust производит высокоуровневый файл рабочего пространства Cargo.toml, позволяя собирать проект одной командой cargo build в каталоге quake3-rs:

$ tree -L 1. Cargo.lock Cargo.toml cgamex86_64 ioquake3 qagamex86_64 renderer_opengl1_x86_64 rust-toolchain uix86_64$ cargo build --release

Исправление недочетов


При первой попытке собрать переносимый код возникла пара проблем с исходниками Quake 3, которые C2Rust не мог корректно обработать или не обрабатывал совсем.

Указатели на массивы


В нескольких местах оригинальный код содержит выражения, указывающие на элемент, следующий за последним элементом массива. Вот упрощенный пример кода Си:

int array[1024];int *p;// ...if (p >= &array[1024]) {   // error...}

Стандарт Си (загляните, например, в C11, Section 6.5.6) допускает указатели на элемент, выходящий за границу массива. Проблема же в том, что Rust это запрещает даже при получении только адреса элемента. Примеры этого шаблона мы нашли в функции AAS_TraceClientBBox.

Компилятор Rust также отметил похожий, но уже действительно ошибочный пример в G_TryPushingEntity, где условие представлено как >, а не >=. После этого условия выходящий за границы указатель разыменовывался, что являлось реальной ошибкой безопасности памяти.

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

Члены динамических массивов


На первый проверочный запуск игры Rust отреагировал паникой:

thread 'main' panicked at 'index out of bounds: the len is 4 but the index is 4', quake3-client/src/cm_polylib.rs:973:17

Заглянув в cm_polylib.c, мы заметили разыменовывание поля p в следующей структуре:

typedef struct{intnumpoints;vec3_tp[4];// переменный размер} winding_t;

Поле p здесь это более ранняя несовместимая с C99 версия члена массива переменной длины, которая до сих пор принимается gcc. C2Rust распознает членов динамических массивов с синтаксисом С99 (vec3_t p[]) и реализует простую эвристику для попутного обнаружения более ранних версий этого шаблона (массивов с размером 0 и 1 в конце структур; в исходниках ioquake3 мы нашли несколько таких).

Панику удалось устранить, изменив вышеприведенную структуру на синтаксис C99:

typedef struct{intnumpoints;vec3_tp[];// переменный размер} winding_t;

Реализация автоматического исправления этого шаблона для общих случаев (массивы с размером не 0 или 1) оказалась бы слишком сложной, поскольку пришлось бы различать членов стандартных массивов от членов массивов с произвольными размерами. Вместо этого мы рекомендуем корректировать исходный код Си вручную как сделали сами в случае с ioquake3.

Связанные операнды во встроенном ассемблере


Еще одним источником сбоев был встроенный в Си код ассемблера из системного заголовка /usr/include/bits/select.h:

# define __FD_ZERO(fdsp)                                            \  do {                                                              \    int __d0, __d1;                                                 \    __asm__ __volatile__ ("cld; rep; " __FD_ZERO_STOS               \                          : "=c" (__d0), "=D" (__d1)                \                          : "a" (0), "0" (sizeof (fd_set)           \                                          / sizeof (__fd_mask)),    \                            "1" (&__FDS_BITS (fdsp)[0])             \                          : "memory");                              \  } while (0)

Он определяет внутреннюю версию макроса __FD_ZERO. Это определение вызывает редкий граничный случай встроенного ассемблерного кода gcc: связанные входные/выходные операнды разного размера.

Выходной операнд =D (_d1) привязывает регистр edi к переменной _d1 в качестве 32-битного значения, в то время как 1 (&__FDS_BITS (fdsp)[0]) привязывает тот же регистр к адресу fdsp->fds_bits в качестве 64-битного указателя. gcc и clang исправляют это несоответствие, взамен используя 64-битный регистр rdi и затем усекая его значение перед присваиванием к _d1. Rust же по умолчанию использует семантику LLVM, которая оставляет этот случай неопределенным. В отладочных сборках (не релизных, которые работали корректно) оба операнда присваивались регистру edi, обуславливая преждевременное усечение указателя до 32 бит еще до достижения встроенного кода ассемблера, что и вызывало сбои.

Поскольку rustc передает встроенный ассемблерный код Rust в LLVM с минимальными изменениями, мы решили исправить этот частный случай в C2Rust. Для этого мы реализовали новый контейнер c2rust-asm-casts, корректирующий проблему через систему типов Rust с помощью типажа (trait) и вспомогательных функций, которые автоматически расширяют и усекают значения связанных операндов до внутреннего размера, достаточного для хранения обоих операндов.

Вышеприведенный код корректно транспилируется в следующее:

let mut __d0: c_int = 0;let mut __d1: c_int = 0;// Ссылка на выходное значение первого операндаlet fresh5 = &mut __d0;// Внутреннее хранилище для первого связанного операндаlet fresh6;// Ссылка на выходное значение второго операндаlet fresh7 = &mut __d1;// Внутреннее хранилище для второго операндаlet fresh8;// Входное значение первого операндаlet fresh9 = (::std::mem::size_of::<fd_set>() as c_ulong).wrapping_div(::std::mem::size_of::<__fd_mask>() as c_ulong);// Входное значение второго операндаlet fresh10 = &mut *fdset.__fds_bits.as_mut_ptr().offset(0) as *mut __fd_mask;asm!("cld; rep; stosq"     : "={cx}" (fresh6), "={di}" (fresh8)     : "{ax}" (0),       // Приведение входных операндов к внутреннему типу хранилища       // с дополнительным нулевым или знаковым расширением       "0" (AsmCast::cast_in(fresh5, fresh9)),       "1" (AsmCast::cast_in(fresh7, fresh10))     : "memory"     : "volatile");// Приведение операндов к внешнему типу (с выведением типов) и усечениеAsmCast::cast_out(fresh5, fresh9, fresh6);AsmCast::cast_out(fresh7, fresh10, fresh8);

Обратите внимание, что код выше не требует типов для любых входных или выходных значений инструкций ассемблера, полагаясь на вывод типов Rust (главным образом типов fresh6 и fresh8).

Выравнивание глобальных переменных


Последним источником сбоев была следующая глобальная переменная, где хранится константа SSE:
static unsigned char ssemask[16] __attribute__((aligned(16))) ={"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\x00\x00"};

На данный момент Rust поддерживает атрибут выравнивания для типов структур, но не глобальных переменных, то есть элементов static. Мы продолжаем искать универсальный способ решения этой проблемы в Rust или C2Rust, но для ioquake3 пока что решили ее вручную с помощью небольшого патча. Этот патч заменяет Rust-эквивалент ssemask на:

#[repr(C, align(16))]struct SseMask([u8; 16]);static mut ssemask: SseMask = SseMask([    255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0,]);

Запуск quake3-rs


Выполнение cargo build --release генерирует двоичные файлы, но все они генерируются в target/release с использованием структуры каталогов, не распознаваемой бинарником ioquake3. Мы написали скрипт, который создает символические ссылки на текущую директорию, чтобы дублировать верную структуру каталогов (включая ссылки на файлы .pk3, содержащие ресурсы игры):

$ /path/to/make_quake3_rs_links.sh /path/to/quake3-rs/target/release /path/to/paks

Путь /path/to/paks должен указывать на каталог, содержащий файлы .pk3.

Ну а теперь пора запускать игру! При запуске нужно передать команду +set vm_game 0 и пр., чтобы загрузить эти модули как общие библиотеки Rust, а не ассемблерный код QVM, а также команду cl_renderer, чтобы использовать OpenGL1.

$ ./ioquake3 +set sv_pure 0 +set vm_game 0 +set vm_cgame 0 +set vm_ui 0 +set cl_renderer "opengl1"

Иии



Перед нами рабочая Rust-версия Quake3!



Вот видео, в котором мы транспилируем игру, загружаем ее и недолго тестируем игровой процесс:


Транспилированный исходный код лежит в ветке transpiled нашего репозитория. Там также есть ветка refactored, содержащая те же файлы, но уже с примененными командами рефакторинга.

Инструкции по транспиляции


Если вы захотите повторить аналогичный процесс переноса и запуска Quake 3, то имейте в виду, что вам понадобятся либо оригинальные ресурсы игры, либо скачанные из интернета демоверсии таких ресурсов. Помимо этого, потребуется установить C2Rust (минимальная необходимая ночная версия Rust на момент написания это nightly-2019-12-05, но мы рекомендуем заглянуть в репозиторий C2Rust или на сайт crates.io на предмет наличия последней):

$ cargo +nightly-2019-12-05 install c2rust

а также копии репозиториев C2Rust и ioquake3:

$ git clone git@github.com:immunant/c2rust.git$ git clone git@github.com:immunant/ioq3.git

В качестве альтернативы установке c2rust вышеприведенной командой вы можете собрать C2Rust вручную с помощью cargo build --release. В обоих случаях вам все равно потребуется репозиторий C2Rust, так как там находятся скрипты оберток компилятора, необходимые для транспиляции ioquake3.

Мы предоставляем скрипт, который автоматически транспилирует код Си и применяет патч ssemask. Чтобы его использовать, выполните из верхнего уровня репозитория ioq3следующую команду:

$ ./transpile.sh </path/to/C2Rust repository> </path/to/c2rust binary>

Эта команда должна создать подкаталог quake3-rs, содержащий код Rust, где можно будет последовательно выполнять cargo build --release и остальные ранее описанные шаги.


Подробнее..

Перевод Юмористичный обзор Rust с перспективы JavaScript

16.06.2021 20:20:54 | Автор: admin

В этой статье я в несколько забавном ключе документирую кое-какие размышления о своем знакомстве с Rust с позиции прожженного энтузиаста JavaScript. Здесь вас ждет импровизированная прогулка по феодам Вестероса, встреча с Ланнистерами и даже замаскированный под остров корабль занятные аналогии, которые можно провести с работой в этом языке.

Я по достоинству оценил Rust в качестве средства для написания небольших инструментов. Мой повседневный рабочий процесс под завязку заполнен JavaScript, а так как Rust во многом его напоминает, то и знакомство с ним стало само собой разумеющимся.

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

Хорошие новости


Современный Rust оказывается весьма схож с JavaScript. Переменные объявляются через let, функции выглядят очень похоже, типы уже не чужды, так как мы привыкли к TypeScript, присутствуют async/await, да и в общем формируется весьма знакомое ощущение.

Плохие новости


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

У каждого из этих подходов есть и достоинства, и недостатки, поэтому они и используются, как правило, каждый для своей области задач. Rust же расположился ровно посередине. Он дает вам доступ ко всем внутренним процессам, попутно предоставляя прозрачность и простоту использования высокоуровневых абстракций. Но всегда есть это но вам, как разработчику, необходимо за это платить. В данном случае мы платим необходимостью научиться по-новому рассматривать построение программы.

Управлению памятью быть!


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

Высокоуровневые языки подобны заботливым родителям. Они все за вас терпеливо уберут, чтобы вы могли творить свое искусство, не пачкая руки. Они благоразумно предоставляют вам в помощь личного Санчо Пансу любезно именуемого Сборщиком мусора поддерживающего вас в те трудные моменты, когда вы отчаянно противостоите агрессивным средам тестирования.

Когда речь заходит об управлении памятью, то Rust как бы говорит: Ничего не знаю реальные шеф-повара сами за собой убирают. И на то есть хорошая причина, потому что сборщик мусора несет в себе собственный набор неочевидных проблем, которые могут навредить в самый неожиданный момент. Хотя в то же время, обогащенный опытом других языков, Rust признает, что заставлять программиста управлять памятью столь же разумно, сколь поручить Дугласу Адамсу написать Звездолет Титаник.

И для того, чтобы найти компромисс между удобством для человека и слишком сложным кодом, Rust предложил новую схему, которую можно образно обобщить как Теперь мы все Ланнистеры.



Как розу ты ни назови, а запах ее столь же сладок


верно сказал Шекспир. Хотя, как быть с не столь ароматными запашками? Сохраняется ли верность его слов в обратном случае? Учитывая многовековую традицию использования людьми эвфемизмов, можно с уверенностью заключить, что нет.


Корабль длиной 55 метров, замаскированный в Тихом океане под вид маленького острова для избежания обнаружения во время Второй мировой войны. Сработало

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

На землях Вестероса (смею я сказать ВестеRust?) есть крохотные, небольшие, а также крупные феоды. Загвоздка в том, что все они оккупированы Ланнистерами. Внутренне феоды занимаются своими собственными делами, а когда им требуются товары извне, то они берут на себя долг, чтобы эти товары получить. В последствии долг необходимо возвращать Богам Вестероса. Rust подобен королеве драконов этого мира он узрит свысока все нюансы и проследит, чтобы все долги перед Богами были уплачены.

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

Может ли возникнуть кризис?


Посмотрим, как это работает.



Здесь у нас две области: внешняя main и внутренняя, будем звать ее inner scope, для демонстрации. В этом случае владение работает так:

  1. main владеет a и b
  2. a хочет поработать в inner scope, поэтому main передает a во владение inner scope
  3. inner scope делает свои дела с a и завершается
  4. Скрытый код Rust отбрасывает a
  5. main делает свои дела с b и тоже завершается
  6. Rust отбрасывает b

Обратите внимание, что долг, обусловленный владением, возвращается системе, а не области, из которой возник. Владение a не возвращается в область main. Так, подождите звучит очень опасно.

Что, если у нас будет такой код?



Здесь область main хочет снова использовать a, но мы сказали, что Rust уже ее отбросил по завершении inner scope.

Не даст ли программа сбой и не сгорит ли, когда достигнет этой точки выполнения?



Да, так и будет. Но, как спартанцы ответили отцу Александра, королю Филиппу II Македонскому: если она этой точки достигнет.

Абсолютный бюрократ


Компилятор Rust является гордым послушником традиции Легизма, настолько верным, что Хан Фей-цзы с восторгом бы объявил вне закона все остальные языки. Ничто не происходит в землях Rust, пока компилятор не скрепит это действие печатью Утверждаю. Он будет тщательно проверять каждую мелочь, оценивая, насколько безопасен запуск программы, и только при удовлетворении всех требований выдаст-таки исполняемый файл.



В этом случае он понял, что наши навыки работы с долгами не соответствуют требованиям, и запрос на создание бинарника отклонил.

Программист сам должен изучить законы Rust и гарантировать их соблюдение, включая все подоплеки, хитрости и допущения. В противном случае компилятор будет сильно ругаться.

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

Rust RPG


В землях Rust переменные это игроки. Игроки обязаны принадлежать некоторому классу маги, священники, структуры. Более того, каждый игрок может иметь индивидуальное снаряжение. И в этом есть смысл. Ведь у вас может быть два священника один с посохом, а второй с жезлом не так ли?

Помните пример с dbg!()? Это макрос, представляющий грубый эквивалент console.log из JS. Давайте создадим собственную типизированную переменную и выведем ее в консоль.



Мы создали struct, которая, по сути, является типом. Затем мы создали объект этого типа. В завершении мы запросили вывод созданного объекта.



Ну дела. Наш игрок до такой степени нуб, что даже не может предоставить отладочную информацию. Правда! Просто удивительно

Ключевой момент здесь в том, что ваши рукотворные переменные все начинают в пешках без всякого снаряжения. А вот где вступает в игру и само снаряжение (на языке Rust называемое типажи (Trait)).

Нажмем F.



На этот раз работает. Единственное отличие в появившейся сверху строке. Здесь мы снаряжаем Noob типажом Debug. Теперь наш игрок готов к выходу в консоль какое достижение!

В Rust представлена уйма всяческого снаряжения разной степени распространенности. При этом вполне ожидаемо, что на основе собственного дизайна с его помощью можно создавать новые вариации.

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

Типажи являются глубинным принципом работы фабрики Rust. Вернемся еще раз к примеру с владением. Если внимательно прочесть сообщение об ошибке, то мы заметим в нем компилятор объясняет, что владение переменной пришлось переместить, потому что String не реализует типаж Copy. В противном случае компилятор не стал бы ее перемещать, а сделал копию.

Типаж Copy означает, что вы берете раздел памяти и memcpy его в другое место, работая непосредственно с байтами. Хорошо, значит String не имеет типажа Copy, нужно ли нам просто сообщить компилятору, чтобы он его присвоил? К сожалению, нет. В целях безопасного использования Copy является слишком низкоуровневой для применения к String.

Конечно же, Rust бы не был полезен, если бы история на этом завершалась. Есть более явный типаж, который проделывает практически то же самое Clone. String обладает типажом Clone, значит нам просто нужно использовать его вместо Copy.

В качестве примера немного подправим код:



Здесь компилятор видит, что нам нужно использовать a внутри inner scope, но теперь он также видит, что мы научились все делать правильно, задействовав вместо фактической a ее клона. Итак, получается следующее:

  1. a принадлежит main
  2. Создается a.clone и одалживается в inner scope
  3. inner scope делает свои дела и завершается
  4. Rust отбрасывает a.clone
  5. main без проблем использует a, потому что a всегда оставалась в ее владении

Красота. Естественно, это не единственный способ решить конкретно данную проблему, но он четко вписывается в наше небольшое исследование принципа владения и типажей.

Прежде чем завершать пост, считаю необходимым вкратце проговорить еще кое-что. Мы рассмотрели владение и то, как оно реализуется в области, но правда в том, что это было всего лишь упрощение. Для отслеживания владения Rust задействует принцип времени жизни (Lifetimes). Просто, получается, что чаще всего времена жизни и области совпадают. Хотя иногда компилятору все же требуется помощь. Следовательно, мы можем работать напрямую с временами жизни, а в некоторых случаях даже обязаны.

Конец?


Однозначно нет! Если сравнить Rust с айсбергом, то это даже не его вершина. Если сравнить его с тортом, то это даже не его глазировка. Если же представить Rust как криптовалюту, то это даже не сатоши. Но сегодня я явно не фонтанирую вдохновенными метафорами, так что давайте на этом закончим.

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


Подробнее..

Передаём файл между изолированными виртуальными машинами без регистрации и СМС

01.06.2021 18:16:36 | Автор: admin

В интернете кто-то неправ

Не далее пяти дней назад на хабре появилась новость под заголовком "В Apple M1 нашли уязвимость M1RACLES возможна быстрая скрытая передача данных между приложениями". В одном предложении суть формулируется так: в Apple M1 нашли регистр, который кто угодно может читать и писать из непривилегированного режима. Значит, это можно использовать для обмена данными в обход механизмов межпроцессного взаимодействия, предоставленных ОС. Однако, сам автор эту особенность значимой уязвимостью не считает, о чём пишет в разъяснительном комментарии:

So what's the point of this website?
Poking fun at how ridiculous infosec clickbait vulnerability reporting has become lately. Just because it has a flashy website or it makes the news doesn't mean you need to care.

If you've read all the way to here, congratulations! You're one of the rare people who doesn't just retweet based on the page title :-)

But how are journalists supposed to know which bugs are bad and which bugs aren't?
Talk to people. In particular, talk to people other than the people who discovered the bug. The latter may or may not be honest about the real impact.

If you hear the words covert channel it's probably overhyped. Most of these come from paper mills who are endlessly recycling the same concept with approximately zero practical security impact. The titles are usually clickbait, and sometimes downright deceptive.

В комментариях к этой публикации развязалась умеренно оживлённая дискуссия о статусе находки: всё-таки серьёзная уязвимость или пустяк? Наряду с @SergeyMaxи @wataru я обратил внимание, что каналов скрытого обмена и так существует предостаточно просто потому, что исполняющая клиентский софт или виртуальные машины аппаратура является разделяемой средой, грамотная модуляция состояний которой делает возможным произвольный обмен данными вне зависимости от инвариантов нижележащей ОС или гипервизора. Противоположной точки зрения придерживаются почтенные господа @creker и @adjachenko, утверждая, что доселе известные побочные каналы качественно уступают возможностям M1RACLES.

Эта заметка является наглядной иллюстрацией к моему утверждению о легкодоступности скрытых каналов обмена. Я собрал простой PoC из ~500 строк на C++, при помощи которого был успешно отправлен файл из одной виртуальной машины в другую на том же физическом хосте. Далее я кратко описываю его устройство, пределы применимости, и привожу выводы и мнение самого автора M1RACLES в конце.

Я не являюсь специалистом по информационной безопасности, поэтому чрезмерно полагаться на мои собственные выводы в конце статьи не следует.

Передача

Повторю ключевой тезис для прозрачности: в общем случае, полная информационная изоляция исполняемых на едином аппаратном узле сред невозможна, потому что общая среда исполнения является разделяемым ресурсом, модуляция состояний которого может использоваться для построения каналов обмена данными. Гипервизоры, MMU, ОС, и прочие информационные барьеры могут лишь затруднить скрытый обмен данными, но не предотвратить его.

В случае M1RACLES, Apple случайно положила подходящий аппаратный ресурс на самое видное место. В других платформах общих регистров может не быть, но остаются другие средства. Сегодня мы сфокусируемся на вычислительных ресурсах процессора; любознательный читатель может в качестве факультатива слегка модифицировать код PoC, чтобы вместо процессорного времени модуляции подвергались, скажем, температура видеокарты, задержка доступа к файловой системе или RAM (забиванием кэша), и т.п.

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

Автор: Ivajkin Timofej (translated from the english wiki) - en.wikipedia.org, CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=15444663Автор: Ivajkin Timofej (translated from the english wiki) - en.wikipedia.org, CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=15444663

Мы будем передавать высокие уровни кодирующего сигнала повышая нагрузку на процессор, и наоборот:

void drivePHY(const bool level, const std::chrono::nanoseconds duration){    static auto deadline = std::chrono::steady_clock::now();    deadline += duration;  // Используем абсолютное время для предотвращения дрейфа фазы сигнала    if (level)  // Передаём высокий уровень высокой нагрузкой    {        std::atomic<bool> finish = false;        const auto loop = [&finish]() {            while (!finish) { }        };        static const auto thread_count = std::max<unsigned>(1, std::thread::hardware_concurrency());        std::vector<std::thread> pool;        for (auto i = 0U; i < (thread_count - 1); i++)        {            pool.emplace_back(loop);        }        while (std::chrono::steady_clock::now() < deadline) { }        finish = true;        for (auto& t : pool)        {            t.join();        }    }    else  // Низкий уровень -- низкой нагрузкой    {        std::this_thread::sleep_for(deadline - std::chrono::steady_clock::now());    }}

В примере мы нагружаем все ядра, что весьма расточительно. Можно нагружать лишь одно ядро, если ОС предоставляет API для задания CPU core affinity (macOS, насколько мне известно, не предоставляет), но это создаст трудности в преодолении барьеров виртуализации, потому что одно ядро внутри гипервизора может отображаться на другое физическое ядро хоста. Однако, если API для affinity есть, и нет нужды пробрасывать канал передачи между изолированными виртуальными средами, можно сделать так:

#include <pthread.h>  // -lpthreadcpu_set_t cpuset{};CPU_ZERO(&cpuset);CPU_SET(0, &cpuset);pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);

Как уже сказано, один бит передаётся прямым или инверсным кодом:

constexpr std::chrono::nanoseconds ChipPeriod{16'000'000};  // ок. 1...100 мсstd::bitset<CDMACodeLength> CDMACode("...");                // код пропущенvoid emitBit(const bool value){    for (auto i = 0U; i < CDMACode.size(); i++)    {        const bool bit = value ^ !CDMACode[i];        drivePHY(bit, ChipPeriod);    }}

Пакеты будем разделять девятью (или больше) нулевыми битами, а перед байтом данных будем отправлять один высокий бит, чтобы отличать его от разделителя пакетов (здесь есть простор для оптимизации):

void emitByte(const std::uint8_t data){    emitBit(1);   // Стартовый бит    auto i = 8U;  // Старший бит идёт первым    while (i --> 0)    {        emitBit((static_cast<std::uintmax_t>(data) & (1ULL << i)) != 0U);    }}void emitFrameDelimiter(){    for (auto i = 0U; i < 20; i++)  // не менее 9 нулевых битов    {        emitBit(0);    }}

Для обнаружения ошибок в конце каждого пакета добавим два байта обыкновенной CRC-16-CCITT:

void emitPacket(const std::vector<std::uint8_t>& data){    emitFrameDelimiter();    std::uint16_t crc = CRCInitial;    for (auto v : data)    {        emitByte(v);        crc = crcAdd(crc, v);    }    emitByte(static_cast<std::uint8_t>(crc >> 8U));    emitByte(static_cast<std::uint8_t>(crc >> 0U));    emitFrameDelimiter();}

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

Приём

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

bool readPHY(){    static auto deadline = std::chrono::steady_clock::now();  // См. заметку о дрейфе фазы    deadline += SampleDuration;    const auto started_at = std::chrono::steady_clock::now();    std::vector<std::int64_t> counters;    const auto loop = [&counters](std::uint32_t index) {        auto& cnt = counters.at(index);        while (std::chrono::steady_clock::now() < deadline)        {            cnt++;        }    };    static const auto thread_count = std::max<unsigned>(1, std::thread::hardware_concurrency());    if (thread_count > 1)    {        counters.resize(thread_count, 0);        std::vector<std::thread> pool;        for (auto i = 0U; i < thread_count; i++)        {            pool.emplace_back(loop, i);        }        for (auto& t : pool)        {            t.join();        }    }    else    {        counters.push_back(0);        loop(0);    }    const double elapsed_ns =        std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - started_at).count();    const double rate = double(std::accumulate(std::begin(counters), std::end(counters), 0)) / elapsed_ns;    static double rate_average = rate;    rate_average += (rate - rate_average) / PHYAveragingFactor;    return rate < rate_average;  // Просадка производительности если передатчик нагрузил ЦП}

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

Снова хочу отметить, что вместо модуляции загрузки ЦП можно управлять чем угодно ещё. Например, можно загружать диск или забивать кэш процессора холодными данными из памяти. Единственное требование заключается в получении на выходе (зашумлённого) двоичного сигнала, который можно семплировать с достаточно высокой фиксированной частотой.

Полученный зашумлённый сигнал с удалённым постоянным смещением мы направляем в CDMA коррелятор, как я изобразил на схеме:

Принципиальная схема приёмного трактаПринципиальная схема приёмного тракта

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

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

Практические системы здесь обычно включают фазовую автоподстройку частоты для синхронизации длительности периода кодирующей последовательности между передатчиком и приёмником. Мы этого делать не станем в угоду простоте реализации. Вместо этого, состояние среды передачи будет семплироваться с утроенной частотой; соответственно, количество каналов коррелятора будет равно длине кодирующей последовательности, умноженной на три.

Знакомый с обработкой сигналов в телекомах читатель сейчас понимающе позёвывает, ведь похожие принципы используются во многих распространённых приложениях, от GPS до сотовых сетей. Для сравнения, типичная архитектура приёмного тракта GPS приёмника выглядит примерно так (каналы Qx на схеме можно проигнорировать, поскольку квадратурное разложение в нашей среде смысла не имеет):

Схема из "Principles of GNSS, inertial, and multisensor integrated navigation systems", P. D. Groves, 2-е изд.Схема из "Principles of GNSS, inertial, and multisensor integrated navigation systems", P. D. Groves, 2-е изд.

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

Прямое применение кодов Хэмминга здесь вряд ли оправданно, потому что условия, при которых могут возникать битовые инверсии сигнала, неизбежно приведут к искажениям тактового сигнала, из чего последуют пропуски либо спонтанное защёлкивание бит, что сделает коды Хэмминга неэффективными. Более совершенные методы коррекции ошибок вроде кодов Рида-Соломона или LDPC представляют интерес, но их реализация несколько выходит за рамки этого эксперимента ввиду сравнительно высокой сложности.

Полученные на выходе коррелятора биты декодируются в пакеты тривиальной логикой, зеркалирующей логику передатчика, что я привёл выше. Здесь её код я показывать не буду, вместо этого рекомендую сразу пройти на гитхаб и увидеть всё сразу:

https://github.com/pavel-kirienko/cpu-load-side-channel

Результат

Процесс передачи файла с одной виртуалки на другую я записал на видео:

Условия здесь были далеки от идеальных, потому что в фоне на хосте виртуализации работал ffmpeg (записывающий это видео в 4K в реальном времени на процессоре), браузер с ~30 вкладками и музыкой, вечно голодная до ресурсов плазма с виджетами и обычный набор каждодневного софта на моей рабочей станции. Я привожу это в качестве дополнительной иллюстрации (очевидного) факта, что устойчивость канала к шуму является лишь вопросом минимальной скорости передачи.

Последняя может вызвать у читателя здравое недоумение: на видео, тактовый период кодирующей последовательности был равен 16 мс, и её длина 1023 бит, т.е., передача одного бита занимала примерно 16 секунд, давая скорость около 0.06 бит в секунду. Передача семибайтового файла с CRC и разделителями заняла почти полчаса. Много ли можно дел наворотить на такой-то скорости?

Если сравнивать с M1RACLES (скорость передачи более 8 Mb/s), то нет. Если сравнивать с популярным предположением по-умолчанию об информационной изоляции виртуализированных сред (скорость передачи 0 b/s), то да.

Преодоление барьеров виртуализации сильно зашумляет канал, что требует использования более длинной кодирующей последовательности. Также увеличивается временной джиттер, что требует и увеличения продолжительности тактового периода. Процессы же в пределах одной ОС могут коммуницировать на скорости более одного бита в секунду, в чем легко убедиться на практике, поменяв параметры ChipPeriod и CDMACodeLength в PoC (я экспериментировал на Manjaro с ядром 5.4 на Intel Core i7 990X @ 4 GHz).

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

Также было бы интересно поэкспериментировать с передачей за пределы изолированных систем (airgapped networks), о чём писал Mordechai Guri и многие до него. Для этого можно подвергать модуляции шум системы охлаждения, температуру её выхлопа, ЭМИ, светодиоды на клавиатуре, и вообще всё, что имеет видимые внешние проявления.

Выводы

Я опубликовал заметку в /r/netsec об этом эксперименте, где в комментариях отметился Hector Martin, автор M1RACLES. Моя изначальная позиция была следующая: побочные каналы присутствуют всегда и в любой системе; M1RACLES лишь добавляет ещё один, а значит, это ничего кардинально не меняет. Если считать его уязвимостью, это делает любую систему в принципе уязвимой по-умолчанию, ведь побочные каналы устранить невозможно; значит, сам термин "уязвимость" применимо к компьютерным системам теряет информационную нагрузку.

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

We already know all systems are vulnerable to certain side-channel attacks. In particular, all systems with shared resources have side channels. Those side channels are easy to turn into covert channels, as you did. The bandwidth, and the severity, is proportional to how closely coupled the security domains are (as you found out, where the VM boundary reduces your performance). As the severity gets higher, you go from 1 bit per second covert channels, to megabytes per second covert channels, to actually being able to steal data from noncooperative entities (actual dangerous side channels) under increasingly less demanding circumstances.

[...]

So M1RACLES is interesting because it is not a side channel - so it poses no danger of leaking data from unwitting processes - but it is a highly efficient covert channel, which does matter under a very small but not nonexistent set of attack scenarios. Does it add covert channel capability where none existed prior? No. But that doesn't mean it's not a vulnerability; as I said, we don't qualify systems on some kind of absolute "vulnerable/not vulnerable" scale. We look at individual issues and then figure out how they affect the overall security of the system under certain attack scenarios.

Заодно он жёстко раскритиковал работу Mordechai Guri, указав на околонулевую дельту его публикаций. Однако, это я не берусь комментировать, поскольку в инфобезе не ориентируюсь. Для меня это всё представляет интерес лишь в контексте технической реализации обработки сигналов, нежели значения для отрасли.

Финальные выводы я сделал следующие:

  • ОС и гипервизоры не являются средствами информационной изоляции и не в состоянии предотвратить межпроцессный обмен данными.

  • Однако, скрытый межпроцессный обмен серьёзной угрозой не является.

  • M1RACLES количественно улучшает доселе существующие возможности скрытого обмена, но итоговый эффект остаётся незначительным.

Подробнее..

Да хватит уже писать эти регулярки

08.06.2021 16:18:29 | Автор: admin

Здравствуйте, меня зовут Дмитрий Карловский и раньше я тоже использовал Perl для разработки фронтенда. Только гляньте, каким лаконичным кодом можно распарсить, например, имейл:


/^(?:((?:[\w!#\$%&'\*\+\/=\?\^`\{\|\}~-]){1,}(?:\.(?:[\w!#\$%&'\*\+\/=\?\^`\{\|\}~-]){1,}){0,})|("(?:((?:(?:([\u{1}-\u{8}\u{b}\u{c}\u{e}-\u{1f}\u{21}\u{23}-\u{5b}\u{5d}-\u{7f}])|(\\[\u{1}-\u{9}\u{b}\u{c}\u{e}-\u{7f}]))){0,}))"))@(?:((?:[\w!#\$%&'\*\+\/=\?\^`\{\|\}~-]){1,}(?:\.(?:[\w!#\$%&'\*\+\/=\?\^`\{\|\}~-]){1,}){0,}))$/gsu

Тут, правда, закралось несколько ошибок. Ну ничего, пофиксим в следующем релизе!


Шутки в сторону



По мере роста, регулярки очень быстро теряют свою понятность. Не зря в интернете есть десятки сервисов для отладки регулярок. Вот лишь некоторые из них:



А с внедрением новых фичей, они теряют и лаконичность:


/(?<слово>(?<буквица>\p{Script=Cyrillic})\p{Script=Cyrillic}+)/gimsu

У регулярок довольно развесистых синтаксис, который то и дело выветривается из памяти, что требует постоянное подсматривание в шпаргалку. Чего только стоят 5 разных способов экранирования:


/\t//\ci//\x09//\u0009//\u{9}/u

В JS у нас есть интерполяция строк, но как быть с регулярками?


const text = 'lol;)'// SyntaxError: Invalid regular expression: /^(lol;)){2}$/: Unmatched ')'const regexp = new RegExp( `^(${ text }){2}$` )

Ну, или у нас есть несколько простых регулярок, и мы хотим собрать из них одну сложную:


const VISA = /(?<type>4)\d{12}(?:\d{3})?/const MasterCard = /(?<type>5)[12345]\d{14}/// Invalid regular expression: /(?<type>4)\d{12}(?:\d{3})?|(?<type>5)[12345]\d{14}/: Duplicate capture group nameconst CardNumber = new RegExp( VISA.source + '|' + MasterCard.source )

Короче, писать их сложно, читать невозможно, а рефакторить вообще адски! Какие есть альтернативы?


Свои регулярки с распутным синтаксисом


Полностью своя реализация регулярок на JS. Для примера возьмём XRegExp:


  • API совместимо с нативным.
  • Можно форматировать пробелами.
  • Можно оставлять комментарии.
  • Можно расширять своими плагинами.
  • Нет статической типизации.
  • Отсутствует поддержка IDE.

В общем, всё те же проблемы, что и у нативных регулярок, но втридорога.


Генераторы парсеров


Вы скармливаете им грамматику на специальном DSL, а они выдают вам JS код функции парсинга. Для примера возьмём PEG.js:


  • Наглядный синтаксис.
  • Каждая грамматика вещь в себе и не компонуется с другими.
  • Нет статической типизации генерируемого парсера.
  • Отсутствует поддержка IDE.
  • Минимум 2 кб в ужатопережатом виде на каждую грамматику.

Пример в песочнице.


Это решение более мощное, но со своими косяками. И по воробьям из этой пушки стрелять не будешь.


Билдеры нативных регулярок


Для примера возьмём TypeScript библиотеку $mol_regexp:


  • Строгая статическая типизация.
  • Хорошая интеграция с IDE.
  • Композиция регулярок с именованными группами захвата.
  • Поддержка генерации строки, которая матчится на регулярку.

Это куда более легковесное решение. Давайте попробуем сделать что-то не бесполезное..


Номера банковских карт


Импортируем компоненты билдера


Это либо функции-фабрики регулярок, либо сами регулярки.


const {    char_only, latin_only, decimal_only,    begin, tab, line_end, end,    repeat, repeat_greedy, from,} = $mol_regexp

Ну или так, если вы ещё используете NPM


import { $mol_regexp: {    char_only, decimal_only,    begin, tab, line_end,    repeat, from,} } from 'mol_regexp'

Пишем регулярки для разных типов карт


// /4(?:\d){12,}?(?:(?:\d){3,}?){0,1}/gsuconst VISA = from([    '4',    repeat( decimal_only, 12 ),    [ repeat( decimal_only, 3 ) ],])// /5[12345](?:\d){14,}?/gsuconst MasterCard = from([    '5',    char_only( '12345' ),    repeat( decimal_only, 14 ),])

В фабрику можно передавать:


  • Строку и тогда она будет заэкранирована.
  • Число и оно будет интерпретировано как юникод кодепоинт.
  • Другую регулярку и она будет вставлена как есть.
  • Массив и он будет трактован как последовательность выражений. Вложенный массив уже используется для указания на опциональность вложенной последовательности.
  • Объект означающий захват одного из вариантов с именем соответствующим полю объекта (далее будет пример).

Компонуем в одну регулярку


// /(?:(4(?:\d){12,}?(?:(?:\d){3,}?){0,1})|(5[12345](?:\d){14,}?))/gsuconst CardNumber = from({ VISA, MasterCard })

Строка списка карт


// /^(?:\t){0,}?(?:((?:(4(?:\d){12,}?(?:(?:\d){3,}?){0,1})|(5[12345](?:\d){14,}?))))(?:((?:\r){0,1}\n)|(\r))/gmsuconst CardRow = from(    [ begin, repeat( tab ), {CardNumber}, line_end ],    { multiline: true },)

Сам список карточек


const cards = `    3123456789012    4123456789012    551234567890123    5512345678901234`

Парсим текст регуляркой


for( const token of cards.matchAll( CardRow ) ) {    if( !token.groups ) {        if( !token[0].trim() ) continue        console.log( 'Ошибка номера', token[0].trim() )        continue    }    const type = ''        || token.groups.VISA && 'Карта VISA'        || token.groups.MasterCard && 'MasterCard'    console.log( type, token.groups.CardNumber )}

Тут, правда, есть небольшое отличие от нативного поведения. matchAll с нативными регулярками выдаёт токен лишь для совпавших подстрок, игнорируя весь текст между ними. $mol_regexp же для текста между совпавшими подстроками выдаёт специальный токен. Отличить его можно по отсутствию поля groups. Эта вольность позволяет не просто искать подстроки, а полноценно разбивать весь текст на токены, как во взрослых парсерах.


Результат парсинга


Ошибка номера 3123456789012Карта VISA 4123456789012Ошибка номера 551234567890123MasterCard 5512345678901234

Заценить в песочнице.


E-Mail


Регулярку из начала статьи можно собрать так:


const {    begin, end,    char_only, char_range,    latin_only, slash_back,    repeat_greedy, from,} = $mol_regexp// Логин в виде пути разделённом точкамиconst atom_char = char_only( latin_only, "!#$%&'*+/=?^`{|}~-" )const atom = repeat_greedy( atom_char, 1 )const dot_atom = from([ atom, repeat_greedy([ '.', atom ]) ])// Допустимые символы в закавыченном имени сендбоксаconst name_letter = char_only(    char_range( 0x01, 0x08 ),    0x0b, 0x0c,    char_range( 0x0e, 0x1f ),    0x21,    char_range( 0x23, 0x5b ),    char_range( 0x5d, 0x7f ),)// Экранированные последовательности в имени сендбоксаconst quoted_pair = from([    slash_back,    char_only(        char_range( 0x01, 0x09 ),        0x0b, 0x0c,        char_range( 0x0e, 0x7f ),    )])// Закавыченное имя сендборксаconst name = repeat_greedy({ name_letter, quoted_pair })const quoted_name = from([ '"', {name}, '"' ])// Основные части имейла: доменная и локальнаяconst local_part = from({ dot_atom, quoted_name })const domain = dot_atom// Матчится, если вся строка является имейломconst mail = from([ begin, local_part, '@', {domain}, end ])

Но просто распарсить имейл эка невидаль. Давайте сгенерируем имейл!


//  SyntaxError: Wrong param: dot_atom=foo..barmail.generate({    dot_atom: 'foo..bar',    domain: 'example.org',})

Упс, ерунду сморозил Поправить можно так:


// foo.bar@example.orgmail.generate({    dot_atom: 'foo.bar',    domain: 'example.org',})

Или так:


// "foo..bar"@example.orgmail.generate({    name: 'foo..bar',    domain: 'example.org',})

Погонять в песочнице.


Роуты


Представим, что сеошник поймал вас в тёмном переулке и заставил сделать ему "человекопонятные" урлы вида /snjat-dvushku/s-remontom/v-vihino. Не делайте резких движений, а медленно соберите ему регулярку:


const translit = char_only( latin_only, '-' )const place = repeat_greedy( translit )const action = from({ rent: 'snjat', buy: 'kupit' })const repaired = from( 's-remontom' )const rooms = from({    one_room: 'odnushku',    two_room: 'dvushku',    any_room: 'kvartiru',})const route = from([    begin,    '/', {action}, '-', {rooms},    [ '/', {repaired} ],    [ '/v-', {place} ],    end,])

Теперь подсуньте в неё урл и получите структурированную информацию:


// `/snjat-dvushku/v-vihino`.matchAll(route).next().value.groups{    action: "snjat",    rent: "snjat",    buy: "",    rooms: "dvushku",    one_room: "",    two_room: "dvushku",    any_room: "",    repaired: "",    place: "vihino",}

А когда потребуется сгенерировать новый урл, то просто задайте группам нужные значения:


// /kupit-kvartiru/v-moskveroute.generate({    buy: true,    any_room: true,    repaired: false,    place: 'moskve',})

Если задать true, то значение будет взято из самой регулярки. А если false, то будет скипнуто вместе со всем опциональным блоком.


И пока сеошник радостно потирает руки предвкушая первое место в выдаче, незаметно достаньте телефон, вызовите полицию, а сами скройтесь в песочнице.


Как это работает?


Нативные именованные группы, как мы выяснили ранее, не компонуются. Попадётся вам 2 регулярки с одинаковыми именами групп и всё, поехали за костылями. Поэтому при генерации регулярки используются анонимные группы. Но в каждую регулярку просовывается массив groups со списком имён:


// time.source == "((\d{2}):(\d{2}))"// time.groups == [ 'time', 'hours', 'minutes' ]const time = from({    time: [        { hours: repeat( decimal_only, 2 ) },        ':',        { minutes: repeat( decimal_only, 2 ) },    ],)

Наследуемся, переопределям exec и добавляем пост-процессинг результата с формированием в нём объекта groups вида:


{    time: '12:34',    hours: '12,    minutes: '34',}

И всё бы хорошо, да только если скомпоновать с нативной регуляркой, содержащей анонимные группы, но не содержащей имён групп, то всё поедет:


// time.source == "((\d{2}):(\d{2}))"// time.groups == [ 'time', 'minutes' ]const time = wrong_from({    time: [        /(\d{2})/,        ':',        { minutes: repeat( decimal_only, 2 ) },    ],)

{    time: '12:34',    hours: '34,    minutes: undefined,}

Чтобы такого не происходило, при композиции с обычной нативной регуляркой, нужно "замерить" сколько в ней объявлено групп и дать им искусственные имена "0", "1" и тд. Сделать это не сложно достаточно поправить регулярку, чтобы она точно совпала с пустой строкой, и посчитать число возвращённых групп:


new RegExp( '|' + regexp.source ).exec('').length - 1

И всё бы хорошо, да только String..match и String..matchAll клали шуруп на наш чудесный exec. Однако, их можно научить уму разуму, переопределив для регулярки методы Symbol.match и Symbol.matchAll. Например:


*[Symbol.matchAll] (str:string) {    const index = this.lastIndex    this.lastIndex = 0    while ( this.lastIndex < str.length ) {        const found = this.exec(str)        if( !found ) break        yield found    }    this.lastIndex = index}

И всё бы хорошо, да только тайпскрипт всё равно не поймёт, какие в регулярке есть именованные группы:


interface RegExpMatchArray {    groups?: {        [key: string]: string    }}

Что ж, активируем режим обезьянки и поправим это недоразумение:


interface String {    match< RE extends RegExp >( regexp: RE ): ReturnType<        RE[ typeof Symbol.match ]    >    matchAll< RE extends RegExp >( regexp: RE ): ReturnType<        RE[ typeof Symbol.matchAll ]    >}

Теперь TypeScript будет брать типы для groups из переданной регулярки, а не использовать какие-то свои захардкоженные.


Ещё из интересного там есть рекурсивное слияние типов групп, но это уже совсем другая история.


Напутствие



Но что бы вы ни выбрали знайте, что каждый раз, когда вы пишете регулярку вручную, где-то в интернете плачет (от счастья) один верблюд.


Подробнее..

Мультивселенная и задачи о переправе

16.06.2021 04:11:19 | Автор: admin

Как-то прочел на Хабре статью Перевозим волка, козу и капусту через реку с эффектами на Haskell, которая так понравилась, что решил написать фреймворк для всего класса задач о переправах, используя мультипарадигменное проектирование. Наконец удалось найти время, и вот, спустя почти год, фреймворк готов. Теперь персонажи, их взаимодействия и описание искомого результата задаются через domain-specific language, который позволяет решать любые головоломки подобного рода с пошаговым выводом. Ниже приводится поэтапный разбор реализации DSL. Статья подойдет тем кто изучает язык Kotlin или просто интересуется примерами его использования. Некоторые малозначимые детали (вроде импортов и вывода) для кратости опущены.

Персонажа легко можно описать открытым для наследования классом:

open class Person(private val name: String)

Также просто определим понятие берега, как набора персонажей задачи:

typealias Riverside = Set<Person>

Дальше построим лодку. Лодка будет знать о населенности обоих берегов, но находится в состоянии квантовой неопределенности между ними, с возможностью инвертировать свое положение:

abstract class QuantumBoat(  val left: Riverside, val right: Riverside) {    abstract fun invert(): List<QuantumBoat>    fun where(    condition: Riverside.() -> Boolean,     selector: QuantumBoat.() -> Boolean  ) = Multiverse(this, condition).search(selector)}

Лодка также снабжена высокоуровневым методом where, для поиска необходимого состояния через N шагов по реке. Условие (condition) определяет валидность берегов в процессе, а селектор (selector) задает искомое конечное состояние. Обратите внимание, что при использовании этого метода лодка на самом деле не двигается с места, а перебирает альтернативные вселенные, пока не обнаружит подоходящую :)
Но об этом мы поговорим позже, а пока что перейдем к простой имплементации лодки для перемещения слева направо:

class LeftBoat(left: Riverside, right: Riverside) : QuantumBoat(left, right) {  override fun invert() =    left.map {      RightBoat(left - it - Farmer, right + it + Farmer)    } + RightBoat(left - Farmer, right + Farmer)}

Инверсия состояния возвращает сразу все возможные варианты перемещения на другую сторону. Это пригодится для реализации нашего мультиверсума. Поскольку по условиям таких задач, фермер выступает необходимым условием передвижения лодки, то перемещаем его во всех случаях вместе с ней. Аналогичным образом имплементируем и перемещение справа налево. Заметьте, насколько лаконичен наш код за счет предопределенных высокоуровневых функций Kotlin и перегрузки операторов для работы с множествами.

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

typealias History = LinkedList<QuantumBoat>  fun Sequence<History>.fork() = sequence {  for (history in this@fork) {    for (forked in history.last.invert()) {      yield((history.clone() as History).apply {        add(forked)      })    }  }}

Заодно описали функцию форка мультиверсума (историй перемещений) в следующий набор состояний (шаг). Чтобы все это добро не забивало лишний раз память, используем ленивые последовательности и yield.

Теперь нам осталось всего лишь описать мультиверсум (а код для поиска состояний у нас уже есть):

/** * Мультиверсум для лодки * @param boat исходное состояние лодки * @param condition валидатор промежуточных состояний */class Multiverse(boat: QuantumBoat, val condition: Riverside.() -> Boolean) {  /**   * Все смоделированные истории передвижений лодки   */  private var multiverse = sequenceOf(historyOf(boat))  /**   * Найти историю подходящей нам лодки   * @param selector нужное состояние берегов и лодки   * @return все найденные варианты достижения состояния   */  tailrec fun search(selector: QuantumBoat.() -> Boolean): List<History> {    multiverse = multiverse.fork().distinct().filter {      it.last.left.condition()        && it.last.right.condition()    }    val results = multiverse.filter { it.last.selector() }.toList()    return when {      results.isNotEmpty() -> results      else -> search(selector)    }  }}

Здесь мы заиспользовали оптимизацию хвостовой рекурсии, благодаря чему kotlinc сгенерирует импертивный цикл для повышения производительности. Что здесь происходит: на каждом шаге мы делаем форк всех состояний мультиверсума перемещая все возможные объекты на другой берег в параллельных вселенных. Затем отбрасываем дубликаты и невалидные состояния (коза и капуста например), а оставшиеся последовательности и будут ответами к задаче. Вуаля!

Наконец, пример использования DSL на всем известной задачке про волка, козу и капусту:

object Wolf : Person("")object Goat : Person("")object Cabbage : Person("")fun Riverside.rule() =  contains(Farmer) ||    (!contains(Wolf) || !contains(Goat)) &&    (!contains(Goat) || !contains(Cabbage))fun main() {  val property = setOf(Wolf, Goat, Cabbage)  // стартовали с левого берега  LeftBoat(property)     // отбросили все невалидные состояния    .where(Riverside::rule)    // выбрали из оставшихся те варианты,    // где все имущество оказалось на правом берегу    { right.containsAll(property) }     // выводим на экран пошаговое решение    .forEach(History::prettyPrint)}

Вот что получилось, вставляю скриншотом, потому что смайлики хабр не переваривает:

Всем удачного дня и побольше времени на написание собственных DSL :)

Исходный код здесь: demidko/Wolf-Goat-Cabbage
Приветствуется критика и предложения как сделать лучше.

Подробнее..

Лаконичный итератор для декларативного синтаксиса

31.05.2021 14:10:41 | Автор: admin


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

В продолжение темы разработки языка программирования с необычной концепцией, публикую описание итератора для доступа к данным в декларативном стиле с очень лаконичным синтаксисом.

Ну и судя по моим наблюдениям Хабр ума палата, буду рад любым комментариям и предложениям, которые помогут протестировать или улучшить предлагаемое решение.

Стандартный итератор


В обычных языках программирования итератор реализуется довольно просто. Выбирается одно или несколько ключевых слов для операции перебора элементов итератора и описывается необходимый для его реализации интерфейс.

Например в С++ это делается с помощью конструкций вида:
for (std::list<int>::iterator it = C.begin(),end = C.end(); it != end; ++it)

Или так:
std::for_each( C.begin(), C.end(), ProcessItem );

В Python так же любой пользовательский класс тоже может поддерживать итерацию. Для этого нужно определить метод __iter__(), который и создает итератор и метод next(), который должен возвращать следующий элемент.

for value in sequence:     print(value)it = iter(sequence)while True:    try:        value = it.next()    except StopIteration:        break    print(value)

В Java использование итераторов выглядит примерно также.

Iterator iter = list.iterator();//Iterator<MyType> iter = list.iterator(); в J2SE 5.0while (iter.hasNext())    System.out.println(iter.next());

или даже так

for (MyType obj : list)    System.out.print(obj);


Общепринятая логика у итератора следующая:

  1. После создания итератора, указатель сразу содержит актуальные данные
  2. Операции получения данных и перемещения указателя могут быть раздельными
  3. Большинство языков программирования поддерживают несколько управляющих конструкций с помощью которых может быть реализован перебор элементов коллекции с использованием итератора
  4. После завершения обхода всех элементов, итератор встает на позицию за последним элементом, тем самым делая невалидной операцию чтения данных


Концепция итератора для нового языка


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

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

В большей степени из-за необходимости работать с объектами из базы данных, так же как с локальными объектами, итоговая концепция итератора немного (или наоборот много) отличается от общепринятой классической, вынесенной в заголовок статьи.

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

Для чего выбрана такая логика работы с итератором?

Создание итератора


В случае реализации доступа к локальным данным внутри приложения, нет никаких причин разделять создание итератора и получение указателя на его первый элемент. Особенно в компилируемых языках (например С++), в которых итератор может быть реализован обычной ссылкой на элемент данных. В таких случаях, как правило, проблем при создании итератора не предвидится.

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

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

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

Автоматическое перемещение итератора


Как было указано выше, перемещение внутреннего указателя на следующий элемент данных выполняется всегда и автоматически, а если необходимо оставлять курсор на текущей позиции (т.е. считывать одни и те же данные постоянно), то после каждого чтения необходимо возвращать позицию курсора назад, на количество реально считанных данных. Фактически это повторяет логику работы итератора в Python (т.е. выполнение метода next).

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

Завершение работы итератора


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

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

Другими словами, если использовать такое поведение итератора, то при получении текущего значения становится неважно, к каким данным происходит обращение и использовать интерфейс итератора можно даже при обращении к обычным переменным (не коллекциям)!

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

Синтаксис лаконичного итератора


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

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

Довольно часто в различных языках программирования встречается управляющая конструкция вида name? или ?name. При этом интуитивно понятно, что требуется вывести значение переменой в консоль или отладочный лог. Причем, данная конструкция выглядит очень естественно и согласуется с привычным использованием данного знака препинания (знак вопроса).

И приняв это за отправную точку, операция чтения данных из итератора в декларативном стиле получается следующая:

? итератор возвращает весь набор элементов из коллекции в виде массива. Коллекция возвращается полностью, независимо от текущего положения курсора. Фактически, это означает, что перед каждым выполнением данного оператора, у итератора выполняется операция сброса в начальное состояние. Так же, с помощью конструкции вида ?(...) можно считывать произвольное количество возвращаемых элементов, например для пагинации.

! итератор возвращает один текущий объект из коллекции и переводит указатель на следующий элемент (сброс/инициализация итератора выполняется неявно и автоматически при обращении к первого элементу). В данном случае, операция чтения итератора возвращает именно сам элемент, а не массив с одним элементом. Если необходимо считать элемент данных как элемент массива, то необходимо использовать конструкцию ?(1).

При использовании только этих двух операторов нельзя вручную проверить завершение элементов в коллекции, но это делать и не требуется, так как в декларативном синтаксисе языка отсутствуют операторы для проверки условий, а перебор элементов и проверка итератора на завершение данных выполняется автоматически.

Хотя при необходимости, количество операторов для работы с итераторами может быть расширено. Например можно добавить оператор "!-" или "!!", который будет считывать один элемент данных и возвращать указатель на одну позицию назад, а оператор "??" считывать только остаток коллекции без принудительного сброса указателя в начало перед чтением данных.

Примеры использования


Мне кажется, что синтаксис получения данных с помощью итератора получился понятным, несмотря на его лаконичность, за счет использования близких по смыслу или по написанию знаков препинания.

Таким образом, конструкция name? будет означать создание итератора и вывод всех элементов коллекции, причем слева от итератора указываются параметры его создания. В общем случае, это будет класс терминов для чтения или имя функции/переменной. Дополнительно, в качестве аргументов можно задать и другие параметры, например для фильтрации терминов по заданным критериям: human(sex=man)?, но это уже зависит от реализации конкретного итератора.

Вообще все термины можно вывести с помощью конструкции "@term?", т.к. любой термин является производным от базового класса term, поэтому строки human(sex=man)? и "@term(class=human, sex=man)?" будут эквивалентны.

Надеюсь, сейчас стало более понятно, что делают итераторы в примере из предыдущей статьи:
Необычная концепция синтаксиса языка программирования

human::brother(test=human!) &&= $0!=$test, $0.sex==male, @intersec($0.parent, $test.parent);human.brother?

Подробнее..

АнтиBIMing

22.05.2021 20:11:20 | Автор: admin
image
Сама по себе автоматизация лишь инструмент и как каждый инструмент у нее есть своя область применения, своя техника безопасности внедрения и применения, а так же свои преимущества и негатив. Традиционно бизнес стремится внедряться IT-разработки там, где существуют достаточно высокая маржа, а значит проще получить прибыль и уменьшать издержки, однако существуют области в которых давно назрела необходимость что-нибудь внедрить с тем что бы упростить и тогда все сформируется. Речь о личном опыте решения таких задач при составлении исполнительной документации в строительстве.

Программа, в которой описываются основные понятия и определения встречающиеся в тексте
Состав ПСД. Приемо-сдаточная документация#1 делится на:
1. Разрешительная документация, включая ППР;
2. Исполнительная документация.

Вся структура приемо-сдаточной документации субподрядной организации по спецмонтажным работам будет выглядеть так:
Разрешительная документация#2 термин, используемый для обозначения документации, оформляемой в соответствии со статьями 45 51 Градостроительного кодекса РФ вплоть до получения разрешения на строительство (ст. 51 ГрКРФ), а также получение разрешения на ввод объекта в эксплуатацию (ст. 55 ГрКРФ).
Исполнительная документация (ИД)#2 представляет собой текстовые и графические материалы, отражающие фактическое исполнение проектных решений и фактическое положение объектов капитального строительства и их элементов в процессе строительства, реконструкции, капитального ремонта объектов капитального строительства по мере завершения определенных в проектной документации работ.

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

ОЖР Общий Журнал Работ. является основным документом подтверждающим выполнение работ, отражает последовательность выполнения отдельных видов работ, в том числе сроки и условия выполнения работ при строительстве, реконструкции, капитальном ремонте, а также сведения о строительном контроле и государственном строительном надзоре.

АОСР Акт освидетельствования скрытых работ. Некоторые работы после окончания строительства невозможно проверить. Это касается работ, проведенных внутри строительных конструкций и коммуникаций, например:
установки арматуры в бетонных стенах,
грунтования поверхности перед окрашиванием,
толщины песочной подложки под брусчаткой,
укладки труб перед засыпанием грунтом,
гидро, звуко-, теплоизоляции и т.п.

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

Действие Первое. В котором рассказывается про предпосылки для автоматизации и проблемные рутины в Строительстве

Ты что делаешь?
Анекдоты читаю.
А отчет?
Час назад уже у тебя на столе лежит.
Погоди, тогда почему твой предшественник на его подготовку тратил три часа?
Послушай, я тоже могу тратить три часа на его подготовку. Если хочешь, я могу читать анекдоты в столовой. Но результат будет тот же.
Структура договорных отношений между участниками строительства #1
Обычно Инвестор нанимает организацию, занимающуюся управлением строительства, она может называться заказчиком. Этот заказчик нанимает проектный институт (лицо, осуществляющее подготовку проектной документации), чтобы тот ему нарисовал проект, бывает, так же нанимает генпроектировщика, а тот нанимает субчиков. Потом играют в тендер (кстати, то же самое может быть и с институтом) и выбирают генподрядчика это ответственный за строительную площадку (лицо, осуществляющее строительство) и заключает с ним договор. Для заказчика существует только генподрядчик (подрядчик) так как им так легче и удобней работать. Генподрядчик уже без тендера выбирает себе субподрядчиков (лицо, выполняющее работы), обычно по видам работ и заключает с ними договора. Субподрядчик или даже сам генподрядчик так же часто себе набирает субчиков, но уже не официально как бы под своим флагом. Заказчик нанимает технический надзор или сам может выполнять данную функцию (представитель заказчика или технический надзор заказчика). Если объект подпадает под государственный строительный надзор (ГСН), то и следит за всем этим он в виде инспекторов, их уведомляет заказчик о начале строительства, те приезжают со своей инспекцией, пишут замечания и уезжают. Все отношения регулируются договорами и действующим законодательством.

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

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

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

При производстве работ по строительству объекта ведется всевозможная документация, основной и самой сложной частью из которой является исполнительная.


Согласно Википедии
BIM (англ. Building Information Model или Modeling) информационная модель (или моделирование) зданий и сооружений, под которыми в широком смысле понимают любые объекты инфраструктуры, например инженерные сети (водные, газовые, электрические, канализационные, коммуникационные), дороги, железные дороги, мосты, порты и тоннели и т. д.

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

Трёхмерная модель здания, либо другого строительного объекта, связанная с базой данных, в которой каждому элементу модели можно присвоить все необходимые атрибуты. Особенность такого подхода заключается в том, что строительный объект проектируется фактически как единое целое: изменение какого-либо из его параметров влечёт за собой автоматическое изменение связанных с ним параметров и объектов, вплоть до чертежей, визуализаций, спецификаций и календарного графика.

Что касается документации и информационной модели на стройке и откуда она там берется. Как правило Заказчик передает Подрядчику проект со штампом В производство работ на бумажном носителе (очень редко в формате pdf и почти никогда в dwg) для того что бы последний в соответствии с контрактом за оговоренную сумму произвел некоторые работы. Прораб/мастер/нач.участка заказывает через снабженцев (привет бухгалтерия и 1С) материалы согласно потребностям, проекту и графику производства работ, нанимается техника, она же ремонтируется, под нее покупается ГСМ и прочие расходы связанные с объектом, часть из которых связана с машинами и механизмами, часть с материалами которые будут монтироваться. Затем на объекте ведутся на бумажном носителе: журнал входного контроля, общий журнал работ и прочие специализированные журналы которые зависят от видов выполняемых работ. К концу каждого операционного цикла подготавливается исполнительная документация, которая представляет из себя акты и схемы выполненных работ (схемы по сути копируют проект, ибо отступление от проекта, без согласования с Заказчиком и контролирующими органами, недопустимо и будет означать лишь проблемы для Подрядчика). Такие акты и схемы на бумажном носителе подписываются представителями Подрядчика, Заказчика, контролирующих органов и организаций и только после того как пройдет успешная защита составляются финансовые акты (обычно по форме КС-2 и КС-3, но это не обязательно, достаточно к договору приложить свой шаблон), на основании которых в особо упоротых случаях бухгалтерия Заказчика может позволить списать материалы бухгалтерии Подрядчика (помимо актов выполненных работ составляются так же акты входного контроля и все вместе это передается Заказчику) в соответствии со сметными расценками.

Сегодня, в отличие от СССР, прораб/мастер/нач.участка не составляют исполнительную документацию. Это не означает что они не заполняют и не составляют бумаг, просто они другие, больше связаны с непосредственной организации управленческих процессов (открытие и закрытие нарядов, журналы инструктажа, выдачи заданий, заявки, письма и т.п.) объем бумаг достаточно большой и это нормальная (в том плане, что распространенная практика) брать в штат сотрудников с высшим (!) инженерным образованием инженеров ПТО, которые будут заниматься всей остальной документацией, а проще говоря исполнять работу технического секретаря. (На самом деле порог вхождения в процессию очень низок, т.к. базового школьного курса Черчения достаточно, что бы читать строительные чертежи и даже перерисовывать схемы, конечно потребуется навык работы с Word/Excel/Paint/AutoCAD/Компас, но это не так сложно как может показаться и потому такая специальность утилизирует людей как с профильным образованием, так и с гуманитарным менеджеров/юристов/учителей и т.д. и т.п.)

Как правило рабочее место, которое может быть и удалено (вагончик в поле), оборудовано МФУ, Wi-Fi точкой, раздающей 3G интернет, ноутбуком. В отсутствие сис.админа все это работает в меру сил и понимания инженера ПТО, который за все это отвечает, который выполняет не только прямые обязанности, но и те от которых не удалось отбрыкаться по разным причинам. Надо ли говорить, что общая техническая грамотность страдает. Обычно на ноутбуки установлен, хорошо если заботливо, Windows, MS Office, редактор для векторной графики, GIMP, программа оптического распознавания текстов. Такой скудный выбор связан с тем, что з/п и оснащение такого инженера находится в составе Накладных Расходов, а не в статьях Общей Заработной Платы, как в случае, например, рабочих, т.е. разные статьи расценок сметы.

Действие Второе. В котором рассказывается про предпосылки для автоматизации и проблемные рутины в Строительстве

Исаак Ньютон:
От флюенции возьму флюксию и обратно.
Лейбниц:
Могу делать то же самое!

Идея создания Программы родилась спонтанно, после 3-х закрытых объектов в 2016году ПАО Транснефть. Помимо сбора и компоновки информации большой блок времени отнимали задачи, связанные с банальным заполнением документов по шаблону, среди которых преобладали Акты входного контроля и Акты освидетельствования скрытых работ. Особенно много времени уходило на проверки в случае описок или различного рода неточностей. Т.к. если они выявлялись, то приходилось заново открывать и проверять такие акты. Иногда, как в ситуации в 2018году, когда Ростехнадзор поменял форму актов скрытых работ, их счет шел на десятки. Но так родилась идея: А что, если я соберу все данные, необходимые для заполнения актов, в таблицу, а уже Программа будет прописывать их в шаблоны за меня?.

Самой простой и пригодной из доступных для этого является MS Office с макросами VBA. Учитывая тот факт, что в 90-е годы я в школе ударно изучал QBasic 4.5 и Borland Pascal 7.0, то выбор платформы оказался более чем очевиден. Пробелы в синтаксисе помог закрыть Гугл. Сама Идея не нова, но в 2016-м году в открытом доступе, так сказать в open source, я нашел только один вариант через Слияние, который тогда, в далеком 2016-м году меня не устроил. И вот я начал разрабатывать свой велосипед:

1. Самое главное и без чего не имело все дальнейшее смысл это без наглядности и удобства в работе. Поэтому для варианта с экспортом в Excel был выбран путь перебора текста в ячейках с целью поиска комбинаций текстовых маркеров, которые априори не встречаются в русскоязычных регламентных формах актов, с последующей авто подстановкой значений из таблицы. (Например, f1, d3, b8 и т.д. и т.п.) Для того что бы не пришлось перебирать всю матрицу я ввел упрощение, при создании шаблона если в первой ячейке за областью печати располагается символ арабской единицы, то только в этом случае макрос ищет текст во всех ячейках этой строки. Позднее я решил вопрос как получить в макрос диапазон ячеек, отправляемых на печать.

В случае с экспортом в World тут все гораздо проще Закладки и ссылки на содержимое закладок, при повторном упоминании содержимого в тексте.

2. В отличие от многих конкурентов с более проработанными приложениями я пошел дорогой структурирования информации (привет BIM) и наглядного представления данных, а потому, не смотря на то что тот же Access, Visual Studio, 1С и т.п. предоставляет большие возможности и функционал все эти программы грешат тем, что в них нельзя протянуть строку или столбец с одинаковыми данными, а переключение между полями ввода требует большей точности при позиционировании (выбор поля через TAB или позиционирование курсора с кликом проигрывает в удобстве перемещению стрелками по ячейкам таблицы, не говоря про то, что копировать ячейками проще, чем из через выделения текста из поля ввода)

Следующий шаг это формирование логики заполнения данных, т.е.:

А) Данные организаций и участников строительства, а также общие характеристики объекта;

Б) Данные для формирования Актов входного контроля, т.е. в первую очередь определяемся не с работами, а с материалами

В) Затем определяемся с месячно-суточным графиком, в котором делаем привязку не только к сметам (т.е. сперва вводим сметные расценки в Заголовки), а уже затем формируем перечень работ из ВР (Ведомости объемов работ по РД (Рабочей документации)), соблюдая очередность работ. Т.е. алгоритм после проставления в графике объемов работ сам определит, в рамках каждого отчетного периода, какие Акты скрытых работ и в какой последовательности под каким номером, от каких дат необходимо будет оформить. Пользователю останется лишь внести сопутствующую информацию, которая является больше рутиной. Кроме того, благодаря связям Пользователь может видеть сколько материалов было списано по предыдущим Актам скрытых работ, что

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

И вот таким образом ПК/ноутбук превращаются из печатной машинки в помощника, который берет часть обязанностей на себя. Т.е. то, что часто многими упускается из виду при создании программ.

3. Но ведь регламентных форм актов сильно больше чем указанных мною выше 2-х шт., пусть они и представляют собой свыше 50% всех актов Исполнительной документации. Значит нужно создать, и это было сделано, аналогичную программу, которая могла бы заполнять таким же образом любой Пользовательский шаблон, а затем такие шаблоны можно было бы импортировать в основную программу, с тем что бы можно было увязывать текущие данные с новыми формами. А раз новые формы, то значит нужна и в меру удобная навигация. Все эти задачи были решены.

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

1. Зачастую архитектурные ограничения методов ограничивают юзабилити пользователей. Потому что разработчик не является исполнителем, который в дальнейшем будет пользоваться этой программой, и не изучают технологические операции конкретной специальности, взамен формируя свои технологические-схемы работ с таким софтом, которые не всегда являются удобнее аналогов.

2. Пользователю, т.е. инженерам-ПТО, нужен такой продукт и помощь, который будет прост, удобен и понятен, а еще по совместительству предложит такую схему работы, ошибок при которой будет меньше, чем без нее и трудоемкость уменьшится.

3. Зачастую люди, которым нужна автоматизация, не могут за нее заплатить, т.к. их оклад не такой уж и большой, а в их опыте даже нет рабочих примеров, когда софт облегчал им рабочую рутины, да еще и уменьшал ИХ КОЛИЧЕСТВО ОШИБОК. В то время как цены на такие программы сравнимы со стоимостью Сметных-комплексов. Но без сметных комплексов очень трудно обойтись, а вот без автоматизации Исполнительной документации элементарно.

4. Информационную модель BIM в ходе формирования исполнительной документации приходится формировать заново, работать дешифратором Рабочей документации на бумажном носителе, переводить ее в язык цифровых сигналов с тем, чтобы выводить результат в виде оформленных актов на бумажном носителе, но с подписью. Прочувствуйте всю иронию.

Действие третье. В котором рассказывается о том как кристаллизовалась программа


На стройке самое важное что? График производства работ и ключевые даты на нем (врезка, подключение, начало работ и окончание работ и некоторые другие). На участке ведется ОЖР, в котором записывается вручную что было выполнено за каждый конкретный рабочий день. Но если взять график (Месячно-суточный график) и заполнять его, то мы получим графическое представление, который и легче воспринимается и, затем, легче автоматизируется, служа исходными данными для актов и аналитики.

Рис.1 Пример Месячно-суточного графика

Теперь думаем дальше, если ввести только один раз и дальше расставить ссылки, на ответственных, описание объекта, контракта и организаций, участников строительства, то это зело упростит Работу. Ввел один раз и далее только развешиваешь ссылки.

Следующий Важный момент в строительстве это Материалы. Они должны соответствовать проекту и при их приемке осуществляется Входной контроль комиссией с оформлением бумаг (Акт входного контроля и Журнал Входного контроля). Это держим в уме. А дальше большая часть ИД в строительстве составляют Акты скрытых работ или полностью, Акт освидетельствования скрытых работ. Работы, их очередность и данные подтягиваем с Месячно-суточного графика, а материалы из перечня актов Входного контроля теперь еще и наглядно мониторить можно какие материалы в каком количестве списываются по актам АОСР.

А что если нам нужно иметь гибкость, что бы Пользователь добавлял свои формы актов, делал их вручную или использовал текущие? И это возможно, но обсудим мы это в

Действие четвертое. В котором речь пойдет о макросах VBA

Далее пойдут спойлеры с кодом, призванные решить те или иные вопросы/проблемы.
Немного ускоряем MS Excel при работе с макросами
'Ускоряем Excel путём отключения всего "тормозящего" Public Sub AccelerateExcel()   'Больше не обновляем страницы после каждого действия  Application.ScreenUpdating = False   'Расчёты переводим в ручной режим  Application.Calculation = xlCalculationManual   'Отключаем события  Application.EnableEvents = False   'Не отображаем границы ячеек  If Workbooks.Count Then      ActiveWorkbook.ActiveSheet.DisplayPageBreaks = False  End If   'Отключаем статусную строку  Application.DisplayStatusBar = False   'Отключаем сообщения Excel  Application.DisplayAlerts = False  End Sub

а теперь возвращаем настройки обратно
'Включаем всё то что выключили процедурой AccelerateExcelPublic Sub disAccelerateExcel()   'Включаем обновление экрана после каждого события  Application.ScreenUpdating = True   'Расчёты формул - снова в автоматическом режиме  Application.Calculation = xlCalculationAutomatic   'Включаем события  Application.EnableEvents = True   'Показываем границы ячеек  If Workbooks.Count Then      ActiveWorkbook.ActiveSheet.DisplayPageBreaks = True  End If   'Возвращаем статусную строку  Application.DisplayStatusBar = True   'Разрешаем сообшения Excel  Application.DisplayAlerts = True End Sub

Заполнение шаблонного файла в формате MS Excel
Здесь очень важна наглядность! Очень много проектов встали на абсолютных ссылках, по-этому я изначально решил сделать все универсально. После долгих мытарств я открыл для себя тот факт, что формулой можно получить диапазон ячеек, который будет выводится на печать.


Рис.2 Пример файла шаблона в формате MS Excel

Здесь в ячейке А1 содержится формула:
=СЦЕПИТЬ(АДРЕС(СТРОКА(Область_печати);СТОЛБЕЦ(Область_печати);1;1);":";АДРЕС(СТРОКА(Область_печати)+ЧСТРОК(Область_печати)-1;СТОЛБЕЦ(Область_печати)+ЧИСЛСТОЛБ(Область_печати)-1;1;1))
Т.е. мы можем получить область печати, обратившись к переменной, фигурирующей в диспетчере имен. Полученные абсолютные границы печати, которые будут автоматически меняться, если нам придется увеличить или уменьшить область печати. Зачем? Здесь следует сделать отступление.


Рис.3 Пример листа с хранящимися данными для автоматического заполнения актов.

Дело в том, что мною был выбран способ-маркеров в тексте, т.е. при составлении шаблона маркеры (a1, b0, c7, d8 и т.д. и т.п.) однозначно характеризуют с одной стороны строку, из которой будут браться данные (порядковый номер элемента массива, который автоматически завязан на номер строки), с другой стороны в русскоязычных шаблонах в строительстве абсурдное сочетание букв латиницы с цифрой не используется. А значит это наглядно. После чего обычный перебор текста решает, НО (!) чем больше ячеек в области печати, тем медленнее будет работать алгоритм. Значит ему надо помочь и подсветить только те строки, в которых априори что-то есть.
Код макроса VBA осуществляющий экспорт в шаблон в формате MS Excel
          With Workbooks.Open(ThisWorkbook.Path + "\Шаблоны аддонов\" + NameShablonPrimer, ReadOnly:=True)               .Sheets(NameShablonPrimerList).Visible = -1               .Sheets(NameShablonPrimerList).Copy after:=wb.Worksheets(Worksheets.Count)                              Let НачальныйНомерСтрокиВФайле = .Sheets(NameShablonPrimerList).Cells(1, 2)       ' Начальный номер строки в файле шаблона               Let НачальныйНомерСтолбцаВФайле = .Sheets(NameShablonPrimerList).Cells(1, 3)      ' Начальный номер столбца в файле шаблона               Let КонечныйНомерСтрокиВФайле = .Sheets(NameShablonPrimerList).Cells(1, 4)        ' Конечный номер строки в файле шаблона               Let КонечныйНомерСтолбцаВФайле = .Sheets(NameShablonPrimerList).Cells(1, 5)       ' Конечный номер столбца в файле шаблона                              .Close True          End With       End If    End If    Do       ИмяФайла = BDList + " "                                                                  ' Префикс имени файла       wb.Worksheets(NameShablonPrimerList).Copy after:=Worksheets(Worksheets.Count)       Set новыйЛист = wb.Worksheets(NameShablonPrimerList + " (2)")              For X = НачальныйНомерСтолбцаВФайле To КонечныйНомерСтолбцаВФайле Step 1                  ' Перебираем столбцы в листе Примера формы-шаблона           For Y = НачальныйНомерСтрокиВФайле To КонечныйНомерСтрокиВФайле Step 1                ' Перебираем строк в листе Примера формы-шаблона               If Sheets(новыйЛист.Name).Cells(Y, КонечныйНомерСтолбцаВФайле + 1) = 1 Then       ' При наличии спец символа проверяем на совпадении строку                  Let k = CStr(Sheets(новыйЛист.Name).Cells(Y, X))                               ' Ищем только если в ячейке что-то есть                  If k <> "" Then                     For i = 1 To Кол_воЭл_овМассиваДанных Step 1                         ContentString = CStr(Worksheets(BDList + " (2)").Cells(i + 1, НомерСтолбца))                         If Len(arrСсылкиДанных(i)) > 1 Then                            If ContentString = "-" Or ContentString = "0" Then ContentString = ""                            Let k = Replace(k, arrСсылкиДанных(i), ContentString)                         End If                     Next i                     новыйЛист.Cells(Y, X) = k                  End If               End If           Next Y       Next X                     For Y = НачальныйНомерСтрокиВФайле To КонечныйНомерСтрокиВФайле Step 1           Sheets(новыйЛист.Name).Cells(Y, КонечныйНомерСтолбцаВФайле + 1) = ""       Next Y            

Заполнение шаблонного файла в формате MS Word
вывода в шаблон формата Word, и здесь на самом деле есть 2 способа вывода текста:

1. Это через функционал закладок,
            Rem -= Открываем файл скопированного шаблона по новому пути и заполняем его=-            Set Wapp = CreateObject("word.Application"): Wapp.Visible = False            Set Wd = Wapp.Documents.Open(ИмяФайла)                        NameOfBookmark = arrСсылкиДанных(1)            ContentOfBookmark = Worksheets("Данные для проекта").Cells(3, 3)            On Error Resume Next            UpdateBookmarks Wd, NameOfBookmark, ContentOfBookmark            Dim ContentString As String            For i = 4 To Кол_воЭл_овМассиваДанных Step 1                If Len(arrСсылкиДанных(i)) > 1 Then                   NameOfBookmark = arrСсылкиДанных(i)                   ContentString = CStr(Worksheets("БД для АОСР (2)").Cells(i, НомерСтолбца))                   If ContentString = "-" Or ContentString = "0" Then ContentString = ""                   ContentOfBookmark = ContentString                   On Error Resume Next                   UpdateBookmarks Wd, NameOfBookmark, ContentOfBookmark                End If            Next i                         Rem -= Обновляем поля, что бы ссылки в документе Word так же обновились и приняли значение закладок, на которые ссылаются =-            Wd.Fields.Update                         Rem -= Сохраняем и закрываем файл =-            Wd.SaveAs Filename:=ИмяФайла, FileFormat:=wdFormatXMLDocument            Wd.Close False: Set Wd = Nothing

Sub UpdateBookmarks(ByRef Wd, ByVal NameOfBookmark As String, ByVal ContentOfBookmark As Variant)    On Error Resume Next    Dim oRng As Variant    Dim oBm    Set oBm = Wd.Bookmarks    Set oRng = oBm(NameOfBookmark).Range    oRng.Text = ContentOfBookmark    oBm.Add NameOfBookmark, oRngEnd Sub


2. Если рисовать таблицы средствами Word, то к ним можно обращаться с адресацией в ячейку
 Rem -= Заполняем данными таблицы ЖВК =-       Dim y, k As Integer       Let k = 1       For y = Worksheets("Титул").Cells(4, 4) To Worksheets("Титул").Cells(4, 5)           Wd.Tables(3).cell(k, 1).Range.Text = Worksheets("БД для входного контроля (2)").Cells(6, 4 + y)           Let k = k + 1       Next y       End With       


Между выводами в файлы форматов Word и Excel есть огромная пропасть, которая заключается в следующем:

Шаблон Excel требует перед использованием настроить отображение под конкретный принтер, т.к. фактическая область печати разнится от модели к модели. Так же перенос строки текста возможен, но только в пределах ячейки/объединенных ячеек. В последнем случае не будте автораздвигания строки, в случае переноса текста. Т.е. Вам вручную придется заранее определит границы области, которые будут содержать текст, который в свою очередь в них еще должен убраться. Зато Вы точно задали границы печати и выводимого текста и уверены, что не съедет информация (но не содержание) с одного листа на другой.

Шаблон Word при настройке автоматически переносит текст на последующую строку, если он не убрался по ширине ячейки/строки, однако этим самым он вызывает непрогнозируемый сдвиг текста по вертикали. Учитывая тот факт, что по требованиям к Исполнительной документации в строительстве ЗАПРЕЩЕНО один акт печатать на 2х и более листах, то это в свою очередь так же рождает проблемы.


С проектом можно ознакомиться тут:
vk.com/softpto
Все программы распространяются абсолютно бесплатно. Всем Добра! ;)
Подробнее..

Recovery mode Часы и волны

12.05.2021 22:22:20 | Автор: admin

Я показывал хабру свою графику, хотя меня и заминусовали, я хочу показать снова то, над чем я работал, может кому то из большой аудитории хабра понравится анимация, графика или идея, которая уже давно лежит на поверхности. Это прототип стрелочных часов, измеряющих время в 7300000-7320000 больших размерах, может столько и не нужно, но обычным 12-часовым часам явно не хватает делений для измерения хотя бы сегодняшней даты.

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

Картинка красивая, но лучше посмотреть работу часов, и их анимацию тут; исходники прилагаются.

Прототип часов

Вернемся к прототипу часов. Тут нет цифр, но к этому быстро привыкаешь. Хочу обратить внимание на другие качественные особенности: вывод месяца и даты (зеленая дуга), визуализаия четырехзначного номера года (4 серых дуги для обозначения каждой цифры текущего года, расположенные под углом 90 градусов по отношению к друг-другу, их может быть меньше, если в номере года встречаются нули). А также первая и вторая половина (pm/am) суток явно указываются часовой срелкой (на темное или светлое поле) - это обеспечивается вращающимся циферблатом.

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

Мне нравится такая реализация, я не понимаю почему таких штук нет в продаже. Мне кажется, такие календарные часы могли бы вытеснить устаревшие версии, которые годятся к использованию только вместе с календарем.

Подробнее..

MMORPG прошлого века как мы создали первый Киевский игровой сервер

09.05.2021 10:20:54 | Автор: admin

Давным давно на далеком сервере...

Вторая половина девяностых. В СНГ интернет как таковой еще только начинает развиваться. Коммерческих сайтов практически нет (а если и есть, то исключительно айтишной тематики). Web еще не стал тем местом, где пользователи проводят основную часть времени. Доминирующая технология последней мили - dial-up, на котором в основном читают почту и сидят в ICQ. Но энтузиасты айтишники уже пользуются и ньюсами и IRC и несколькими другими сервисами. И конечно играют в сетевые игры!

Doom, в который можно было поиграть вдвоем по модему или вчетвером в локальной сети уже смещен с позиции самой популярной сетевой игры. Сейчас поднимают пиратские батлнет сервера на PVPGN - там можно сразиться в Starcraft и Diablo уже в компании до 4-8 игроков (чем больше игроков, тем больше вероятность лагов). В шутерах доминирует Quake и Quake2 - в игре одновременно может быть уже десяток игроков, и уже не зависеть от чужих лагов (ибо UDP).

Кто-бы мог подумать, что примерно всего через десять лет появятся игры, в которые может одновременно играть сотня или тысяча игроков, и даже больше!

Но что это только что было?
Неужели такие игры уже есть?
Да, уже есть! А если взять англоязычный мир, так уже давным-давно!

Рождение FD

В 1998 году официально запускается первый Киевский MUD (Multi User Dungeon/Dimension) сервер "Forgotten Dungeon".
Для него по шапочному знакомству выделили немного места на одном из FreeBSD-серверов в общежитии Киевского Политехнического института.

Это не первый русскоязычный MUD сервер, но один из них.
Московский "Мир Мерлина" был запущен примерно на год раньше.
А вот с другими серверами в СНГ старшинство уже можно и оспорить (привет, Zmey и ко.!).

Благодаря большой локальной сети общежитий КПИ, раскинутой на витой паре, коаксиале и честном слове, сервер быстро заполняется игроками.
В активные дни количество игроков находящихся в мире превышает 100, то и 150, что по тем меркам весьма немало!
Такой онлайн позволяет не просто побегать по миру в компании практически в любое время суток, но и наладить дипломатические отношения между кланами и даже альянсами.

Внешний канал для института был проблемой - интернет в то время был медленным и дорогим, поэтому были периоды, когда игровой сервер не был доступен снаружи.
Но по договоренности с ректоратом, для "Forgotten Dungeon" в конечном счете все-таки сделали исключение, как для общественно-полезного проекта.
Снаружи он был известен по адресу mud.ntu-kpi.kiev.ua:4000 до конца своей активной жизни (2005-2007).
Хотя, честно говоря, внешние игроки составляли не более 10% основного онлайна.

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

Такая тесная и регулярная связь и общий энтузиазм вдохновляли на множество безумных идей, и "Forgotten Dungeon" очень быстро обрастал сложной боевой/магической механикой и уникальными фичами.

Конечно, по современным меркам, все разработчики в основном не дотягивали даже до джуниоров - обычные студенты, из которых не все отличали компилятор от линкера.
Мы просто писали код с горящими глазами.
Сервер падал - его поднимали и писали скрипты с автоматическим рестартом.
Сервер ломался - его чинили и писали новые фичи с новыми ошибками, которые снова чинили.
Иногда появлялись игроки и "мимокрокодилы" с факультетов информатики, которые могли написать немного больше, чем "hello world". Они делали свой вклад и пропадали.

Однако, несмотря на полное отсутствие коммерческого опыта разработки, мы были открыты к любой полезной информации.
В какой-то момент, узнав про существование систем контроля версий, я перевел все на CVS с коммитами и странной системой версионирования.
Еще не было Jenkins/Teamcity, не было gitlab/gerrit и других привычных CI инструментов, но у нас при коммите новых изменений уже триггерилась сборка новой версии с отправкой логов. А админ, не выходя из игры мог командой перезагрузить игровой сервер с обновлением на новую версию, что позволяло разграничить права доступа, давая возможность вносить изменения в код, но не давая доступ к полной админке.
На каждое падение сервера, автоматически генерировался отчет с отладочной информацией (dbg стектрейс) и уходил на почту.
Для тех времен, это было очень круто!
За почти 10 лет существования сервера, вклад в разработку внесло несколько десятков человек. Без контроля версий, и этих на коленке сделанных скриптов, наверное не получилось бы и половины вещей довести до ума.

Конечно, несмотря на все наши значимые (как нам тогда казалось) попытки причесать код, он очень далек от того, на что можно равняться.
Но мне кажется, это довольно неплохой проект "на поиграться" для начинающих разработчиков, которые хотят освоиться в Си, и научиться работать с базовыми вещами - условия, циклы, структуры данных, неважно тебе 10 или 30+ лет.
По слухам, на кодовой базе нашего сервера, было даже несколько человек, защитивших свои дипломы.
Но были и отчисленные за неуспеваемость слишком увлеченные игроки... Каждый берет что может (;

Технические данные

За базу был взят один из отделившихся от DikuMud движок, а именно ROM 2.4b4a - довольно популярная кодовая база на то время.
К сожалению, сам репозиторий со всей историей коммитов не удалось сохранить (это современные системы типа git хранят всю историю локально, а более старые CVS только на сервере).
Но сохранилась последняя версия исходного кода.
При помощи Google и stackoverflow я смог довести код до состояния, когда он собирается и запускается на современном Линуксе.
В коде есть текстовый файл, где вручную писали комментарии на жуткой смеси русского(нецензурного)/английского, но посторонним его читать не рекомендуется.

Еще до запуска сервера в институте, он стоял у меня на рабочем сервере, я игрался с ним в одиночку (зарегистрировались несколько знакомых, которые заходили в основном потестить какие-то фичи). Но раскручивать я его не собирался, ибо не был готов хостить полноценный сервер на работе, так что я и КПИ нашли друг друга (:
Добавил поддержку кодировок (koi-8, cp-866, win1251 и транслитерацию), добавил поддержку контрольных символов для ANSI colors, поправил какие-то ошибки.
И даже придумал и реализовал систему простых автоматических квестов.
Но эти вещи можно найти практически в каждом MUD, а я хочу поделиться некоторыми уникальными инсайдами или просто забавными моментами.

Делаем простые вещи

Если говорить о базовых элементах программирования - например на практике понять разницу между знанием синтаксиса if/then/else и умением им пользоваться хорошо, то игровые механики в таких простеньких консольных или браузерных играх это просто превосходный вариант для начинающих! Ведь для того, чтобы увидеть результат нового супер-пупер заклинания, тебе не нужен гейм-дизайнер, художник, музыкант и кто-то еще. Многие вещи делаются простыми командами.

Например: научился проверять значение переменной, и создал новую переменную, статус "friendly fire". Добавляем немного условий - и код готов. А с точки зрения игрока - полностью меняется боевая составляющая, потому что теперь можно сражаться команда на команду или клан на клан или стая однотипных монстров против игроков, пользуясь массовыми заклинаниями и не вредя своим "друзьям".

Можно сделать переменную "Статус зрителя", и создать арену для дуэлей, на которой могут находиться зрители в полной безопасности. Еще немного переменных - и можно уже делать ставки на победителя, и вот уже "просто посмотреть за чужой дуэлью" превращается в азартное зрелище вроде "арены гладиаторов" с букмекером! А учитывая что игра текстовая - все зрители могут смотреть, так сказать, с лучшего места в первом ряду ;)

Пробуем что-нибудь посложнее

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

Техническая реализация заклинания "полиморф" уже была посложнее. Высокоуровневый маг может превратиться в любой предмет. Полноценный. Его можно было взять. Если это было оружие, им можно было вооружиться и сражаться. Если это была еда, ее можно было съесть (съеденный игрок умирал :).
Сложность заключалась в том, что все триггеры на оповещение персонажа (кто-то зашел, кто-то сел, кто-то встал и ушел) - были завязаны на объект "персонаж". Следовало добавить многочисленные проверки и триггеры в код, чтобы игрок, превратившийся в объект "предмет", получал полную информацию из мира.
С заклинанием было связано довольно много проблем, чем с радостью пользовались "злоумышленники", но все они в конечном счете были исправлены, а само заклинание внесло значительные изменения в командную игру и особенно клановые войны.

Уникальные локации

Игровой мир в Forgotten Dungeon был довольно большим. И становился еще больше. Несколько карт было скачано с разных ресурсов, адаптировались, переводились, подключались.
Но еще больше карт рисовали сами игроки. Так была нарисована собственно карта и самого Киевского Политехнического института с кучей локальных мемов и отсылок. Среди NPC фигурируют фамилии преподавателей и ректоров. И наверное хорошо, что на тот момент эти люди были далеки от игр и компьютеров, ибо цензура на сервере была минимальна ;)
Парочка фамилий, которые можно встретить в игре: Федорова, Куцик, Сенько, Шпортюк, Кравцов, Савицкий, Скоробагатько, Линчевский и др.

Воспоминания про армию

Однажды мы придумали армию.
Натуральную.
По достижению 18 уровня, все вещи у игрока забираются, и его переносит в закрытую локацию.
В этой локации он даже пообщаться с другими игроками не может, за исключением тех, кто уже в "армии". Из внешнего мира можно было прийти в специальную "комнату переговоров", в котором можно было пообщаться и даже передать предметы (только с типом "вода" или "еда").
Без поддержки друзей, клана и хороших предметов, нужно было выжить три уровня с высокоуровневыми агрессивными монстрами (сержанты, солдаты, прапорщики..), пока на 21 уровне товарищ Майор не отправлял тебя назад в общий мир. Иногда даже игроки из враждующих кланов объединялись в армии =)
Но за смерть в армии не штрафовали, поэтому рано или поздно армию проходили все.
Казалось бы - ну придумали и придумали.
Но Армия оказала заметное социальное воздействие.

Небольшое отступление: в середине 90х во многих MUD у игроков вообще могла отсутствовать возможность сражаться друг с другом, так как многие MUD создавались как социальные игровые "чаты" с ролевой составляющей в стиле DnD (или другой тематикой). На ресурсах со списком серверов, даже отдельно указывалась поддержка PK (Player Killing) в описании сервера.

Смерть персонажа в игре воспринималась неприятно - потеря опыта, потеря предметов, и в отличие от уже одиночных RPG, нельзя было сохраниться.
Но Армия в нашем MUD "сделала из тварей дрожащих львов и тигров".
После армии, страх перед смертью заметно падал, отношение к игровому персонажу менялось.
Я слышал пару историй, когда игроки из нашего мира уходили в другие миры и наводили там шороху.
Даже верю, что это могло оказать какое-то влияние и в реале. Ну собственно для кого-то это так и было.
Конечно с армией тоже было связано много багов, которыми пользовались любители халявы, но мы это дело находили и прекращали:

Сотни мелочей

Введено много уникальных рас, с действительно заметными отличиями. Сейчас в MMORPG это уже норма, а тогда и рас было мало, и в базовом движке их отличие было в основном в небольшом разбросе изначальных параметров - силы, ловкости и так далее.
У нас вводились заметные изменения.
Раса Вампиров, которые заметно прибавляли в силе по ночам и вынуждали игроков следить за сменой дня и ночи в игре.
Банальные вещи - Ящеры умеют плавать. Пикси - летать. Этериалы - проходить сквозь двери и сопротивляться режущему оружию. Зомби могут не есть и не дышать. У Кентавров четыре ноги, поэтому удар двумя копытами вполне заменял серьезное оружие.
Гномы могут перековывать предметы, улучшая их при помощи специальных артефактных компонентов (да, у нас гномы-игроки уже были прародителями современных крафтеров).
В общем сотни мелочей, которые в сумме значительно влияли на то, как следует одевать/вооружать/прокачивать и отыгрывать своего персонажа в зависимости от расы.

Введены перерождения с частичным сохранением навыков предыдущего класса, позволяя создавать сложные комбинации (например вор/маг и маг/вор - далеко не одно и тоже, а это ведь необходимо дважды прокачаться до 101 уровня.)

Админы и игроки творят вместе

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

У нас были "отношений", включая секс, свадьбы, разводы и даже возможность получить статус "родственника". Да, следует ставить рейтинг 18+.

Были игроки-одиночки, которые не хотели в клан. Но клановые фичи были довольно "вкусные". И мы сделали для них специальный клан для одиночек "LONER". Он ни к чему не обязывал, но указывал, что человек сам по себе и давал возможность пользоваться защищенным тепленьким местом с кроватью для отдыха.

Внутренняя кухня

Некоторые решения были довольно странные по архитектуре. Например, никто из нас не умел работать с базами данных и не особо разбирались в том, насколько это удобно. Честно говоря, в то время из известных баз были Foxpro и MS Access. MySql еще не был известен. А Oracle какой-то версии 5-6 был жутко тяжелым и решения на его базе только начинали продаваться далеко в Москве.
В большинстве MUD движков данные игроков хранились в файлах. Проблемным моментом в этом было то, что сохранение файла игрока обычно происходило во время его выхода из игры, и практически все сервера были подвержены ошибке с дублированием предметов.
Рецепт примерно такой: Ты узнаешь любой баг в игре, который приводит к падению сервера (это встречалось частенько и в разных мадах).

Инструкция: в игру заходят два игрока, у одного есть "вкусный" предмет.
Он отдает его второму. Второй выходит из игры (его профайл сохраняется уже с предметом).
Первый игрок крашит сервер (или дожидается такого краша), и его инвентарь не успевает сохраниться в файл.
После рестарта сервера - оба заходят - и каждый с предметом.

Мое ужасное но рабочее решение было в лоб - если игрок производил любые манипуляции с предметами, на него вешался флаг "сохранить".
При сохранении какого-либо игрока, сохранялись все игроки с таким флагом одновременно.
При онлайне 100-200 игроков вопрос производительности не стоял, файлы игроков занимали в среднем 3-4 килобайта.
Решение сработало отлично, и duplicate bug в нашем мире исчез (главное чтобы не крашилась сама процедура сохранения).

Можно было просто почитать документацию о fopen/fwrite, и вот - добавлены административные команды для работы с оффлайн игроками.
Администраторам игры было полезно уметь забанить, разбанить, проверить информацию по игрокам, которые сейчас оффлайн, без доступа к консоли сервера.

Работаем с текстом

Переводя фразы изначального движка и создавая новые, текст для действий старались строить таким образом, чтобы он звучал корректно вне зависимости от пола игрока.
Например: "Ты уже благословил Aelita" - звучит неблагозвучно, если заклинание колдует девушка.
Нейтральная фраза выглядит как: "Aelita уже под божественным благословлением".

Но затем была придумана и реализована система работы с полом.
В коде это выглядело как: "Ты уже благословил$a Aelita", (ch->sex)
Нужное подбиралось из массива ["","а","о"] для он, она, оно.
Или с использованием готовой функции: act_new("Задание перв$b выполнил$r $n!",ch,NULL,NULL,TO_ALL,POS_DEAD);

Чуть позже, по той же схеме добавили обработку падежей.
Для этого в описании персонажей (в основном для NPC) нужно было руками прописать склонения: "волшебн|ый|ого|ому|ого|ым|ом дракош|а|и|е|у|ей|е".

В тексте фразы указывалась нужная форма:
act("$c1 выдыхает облако кислоты в $C4.",ch,NULL,victim,TO_NOTVICT);
Функция проверяла есть ли для данного персонажа набор падежей, если нет - просто брала инфинитив.

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

Каждый мир существовал в своем небольшом сообществе и действительно был гораздо более уникален, чем пиратские шарды современных MMORPG, которые в основном отличаются только "рейтами".

Зарубежом

История зарубежных англоязычных МАДов гораздо более долгая. Она начинается чуть ли сразу с появления первых сетей - второй половины семидесятых. Общее количество игроков и онлайн на отдельно взятом крупном сервере могло посоревноваться с общим онлайном всех серверов в СНГ вместе взятых.

Например Arctic MUD, который появился еще в 1992 году, к концу девяностых мог похвастать онлайном свыше 1000 человек, доходя в пиковое время почти до двух тысяч. Многие англоязычные MUD могли позволить себе монетизирование, которое в то время в СНГ было бы непонятым и попросту невозможным. Также для игры в MUD, точнее комфортной игры, желательный уровень английского языка заметно выше чем elementary. Это также было довольно серьезным барьером для желающих поиграть.
Вполне возможно что за рубежом было придумано и воплощено много разных интересных вещей, и многие из наших нововведений могли оказаться вторичными.

Пик MUD серверов в рунете пришелся на тот небольшой промежуток времени, когда сама идея многопользовательских текстовых игр была уже неплохо проработана. Существовало несколько готовых движков с разным подходом к основной механике: level-based (dikumud, ROM, Merc and many other), когда персонаж получает уровни и от уровня уже получает навыки, заклинания и улучшения параметров (elf 80-го левелу), или skill-based (Avalon, Clok, DartMUD), когда уровня нет вообще, или он ни на что не влияет, а значение имеет только уровень владения различными навыками и заклинаниями (владение двуручным мечом 12 уровня).

Но уже всего через несколько лет на хвост начали наступать первые "графические МАДы", как их называли вначале - Ultima Online, Everquest. А затем подтянулись уже и более современные MMORPG - Lineage/Aion/WOW и другие. Поэтому отечественные мады, ярко вспыхнув, горели не слишком долго - около десяти лет (примерно с 1997 по 2005-2007), вытесненные более современными играми.

Я потратил примерно одинаковое количество времени на администрирование "Forgotten Dungeon" и шарда Lineage2, и могу сказать, что с точки зрения игровой механики, МАД конечно гораздо глубже, шире, и предоставляет больше возможностей и свободы для игроков.

Легкость введения новых возможностей и требования к уровню разработчиков просто не сопоставима. Вдобавок для игры не нужен современный компьютер или мощная видеокарта. Даже требования к скорости подключения весьма скромные.

Но удобство и простота управления, привлекательность и конечно визуальная составляющая - конечно на стороне графических игр.

Я подсчитал, что в "Forgotten Dungeon" есть примерно 350 команд, которые можно ввести (правда часть из них административные, часть эмоции), около 120 заклинания и примерно 50 навыков (активных и пассивных). Гайдов и форумов практически не было в силу неразвитости веб, есть только довольно обширная но запутанная внутренняя справка. Примерно месяц занимало просто чтобы игрок стал чувствовать себя в игре уверенно и ориентироваться в мире.
А для большинства графических игр - месяц это зачастую если и не ветеран, то уже довольно опытный игрок.

На текущий момент (2021 год) еще можно найти несколько живых серверов в СНГ (http://www.mudconnector.su/MudList), но это лишь тень минулого и зачастую они пустуют.
А вот среди англоязычных серверов можно найти десяток серверов, с онлайном в 500-800 игроков.

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

Конец истории

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

Исходники "Forgotten Dungeon" доступны на https://github.com/sfkulyk/fdungeon
Кстати, выложить код в github (правда без некоторых последних правок), я успел еще до https://archiveprogram.github.com/, поэтому можно надеятся, что исходники FD переживут всех нас ;)

Сам игровой мир (пусть и пустой) живет по адресу mud.saboteur.com.ua:4000, чисто из чувства ностальгии. Много он не просит, а вдруг кто зайдет пустить слезу по временам, когда трава была зеленее.

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

Буду благодарен, если при этом вы сохраните в текстах наши юные смешные имена..

Спасибо всем, кто дочитал до конца.

Подробнее..

Перевод Перепрограммирование GameBoy за счёт бага в Pokemon Yellow

18.05.2021 10:20:20 | Автор: admin

Pokemon Yellow - это карманная вселенная со своими правилами. В ней можно покупать и продавать предметы, тренировать покемонов, побеждать других тренеров но нельзя менять правила самой игры. Нельзя построить себе дом, поменять музыку или даже переодеться. Точнее, так было задумано. На самом деле есть последовательность валидных команд (типа перемещения из одного места в другое и манипуляций с предметами), которая позволяет превратить игру в Pacman, тетрис, Pong, MIDI-проигрыватель и что угодно ещё.

Существует спидран Felipe Lopes de Freitas (p4wn3r), в котором Pokemon Yellow проходится за 1 минуту 36 секунд. Этот спидран основан на следующем хаке: в норме инвентарь игрока ограничен 20 предметами. Но есть баг, который позволяет игнорировать это ограничение и обращаться с памятью, расположенной сразу после инвентаря, как будто бы это был список предметов. Соответственно, стандартные манипуляции с предметами позволяют эту память переписывать. Спидраннер использует эту возможность, чтобы заставить дверь из стартовой комнаты переносить его на финальную локацию, в которой ему остаётся только выслушать поздравления.

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

Gameboy это восьмибитный компьютер. Соответственно, всё происходящее в игре результат того, что исполняется некая последовательность однобайтовых команд. Например, последовательность[62 16 37 224 47 240 37 230 15 55] в местных машинных кодах означает, что надо выяснить, какие кнопки нажаты в данный момент, и записать результат в регистр A. Можно создать программу, которая будет читать ввод с кнопок и записывать куда-нибудь в память исполняемый код, а потом передавать ему управление. Если удастся каким-то образом засунуть такую программу (назовём её программой ввода) в память и заставить Gameboy её исполнить то я выиграл, потому что теперь я могу исполнять произвольный код (скажем, тетрис) вместо Pokemon Yellow.

Во-первых, как написать такую программу? Восьмибитными числами представлены не только правила, но и состояние игры (инвентарь, список покемонов, имя главного героя и т.п.) Инвентарь хранится вот так:

item-one-id         (0-255)item-one-quantity   (0-255)item-two-id         (0-255)item-two-quantity   (0-255)...

Упомянутая выше подпрограмма для чтения кнопок хранится в той же последовательности битов, что и следующий набор предметов:

lemonade     x16guard spec.  x224leaf stone   x240guard spec.  x230parlyz heal  x55

Так что если нам удастся собрать нужные предметы в нужном количестве и порядке то получится код программы ввода. Правда, его с самого начала нужно писать так, чтобы он состоял только из валидных ID и количеств предметов. Это не так-то просто, потому что предметов в игре немного, а многие машинные инструкции состоят из 2-3 битов. Та версия программы, на которой я остановился, состоит из 92 бит; примерно половина из них не делает ничего полезного и вставлена только для того, чтоб код был ещё и корректным списком предметов.

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

Поехали!

Я начинаю с того, что называю своего противника Lp/k. Эти символы в своё время превратятся в предметы и будут перемещены на указатель функции, чтобы я мог исполнить программу ввода. Но сперва её надо записать, так что я начинаю забег так же, как и p4wn3r: перезапускаю игру при сохранении, чтобы сломать список покемонов. После этого я меняю местами 8 и 10 покемона, что ломает список предметов и позволяет мне манипулировать предметами после 20-го (то есть соответствующей областью памяти). С помощью этих манипуляций я выставляю скорость текста на максимум и переключаю свою дверь на магазин Celadon Dept. store. p4wn3r в этот момент переключил её на Зал славы и выиграл, но мне неинтересен спидран как таковой.

Поэтому я не останавливаюсь, а выкладываю из своего поломанного инвентаря в сундук множество глитчевых предметов 0x00, которые мне ещё пригодятся. Потом я забираю из сундука зелье, что вызывает переполнение счётчика предметов с 0xFF на 0x00 и восстанавливает мой инвентарь. Зелье при этом, правда, исчезает. После этого я забираю 255 0x00-предметов и отправляюсь в магазин. Там я продаю 2 0x00 за 414925 монет каждый, что позволяет мне купить следующие предметы:

+-------------------+----------+|##| Item           | Quantity |+--+----------------+----------+|1 | TM02           |  98      ||2 | TM37           |  71      ||3 | TM05           |   1      ||4 | TM09           |   1      ||5 | burn-heal      |  12      ||6 | ice-heal       |  55      ||7 | parlyz-heal    |  99      ||8 | parlyz-heal    |  55      ||9 | TM18           |   1      ||10| fire-stone     |  23      ||11| water-stone    |  29      ||12| x-accuracy     |  58      ||13| guard-spec     |  99      ||14| guard-spec     |  24      ||15| lemonade       |  16      ||16| TM13           |   1      |+--+----------------+----------+

Эти предметы я раскладываю в инвентаре в таком порядке, чтобы получилась моя первая программа ввода. Первая потому что написать окончательную версию программы ввода внутри игры у меня не получилось. Та, которую я собираю из предметов, умеет читать только кнопки A, B, start и select. Каждый фрейм игры она пишет 4 бита в определённую область памяти; собрав 200 с лишним байт, она исполняет то, что написала. С помощью этой программы я пишу следующую, которая читает уже все 8 кнопок и пишет произвольное число байтов в произвольную область памяти, а потом исполняет код по произвольному адресу. Эта программа, в свою очередь, используется для загрузки третьей, которая делает всё то же самое, но ещё и выводит записываемый код на экран.

Закончив с кодом первой программы, я отправляюсь в Celadon mansion (это не критично, мне просто нравится локация). Там я снова меняю местами сломанных покемонов и ломаю инвентарь. Проскроллив до имени своего противника, я выкидываю предметы до тех пор, пока имя не окажется на указателе функции. Как только я закрываю меню, программа ввода исполняется, и я могу заливать следующую программу.

Инфраструктура

Всё видео было записано ботами, а сам я к Gameboy (точнее, к эмулятору) даже не притрагивался. Ниже краткое описание инфраструктуры, которую я создал для этого проекта. Исходники доступны в http://hg.bortreb.com/vba-clojure

Прежде всего, мне нужен был программный доступ к эмулятору, так что я скачал vba-rerecording. Поверх него я написал низкоуровневый интерфейс на C, к которому я смогу обращаться из Java через JNI. Этот интерфейс позволяет обращаться к базовым функциям эмулятора: прогнать один фрейм игры или один тик часов Gameboy, обратиться к любому адресу памяти или регистру. Кроме того, он позволяет выгрузить состояние эмулятора в объект Java или загрузить его обратно.

Поверх JNI я написал обёртку на clojure, так что в итоге у меня получился функциональный интерфейс к vba-rerecording. Этот интерфейс работает с состоянием эмулятора как с иммутабельным объектом и позволяет делать в функциональном стиле всё то, что интерфейс на C позволял делать в императивном. С помощью этого интерфейса я написал функции, которые принимают на вход состояние эмулятора, находят и исполняют последовательность команд, которая приведёт к желаемому эффекту. Из этих функций я собрал высокоуровневые функции, которые выполняют в игре задачи типа перемещения и покупки предметов. Вот, например, код для перемещения кратчайшим путём из магазина покемонов в Viridian City в лабораторию Оака:

(defn-memo viridian-store->oaks-lab  ([] (viridian-store->oaks-lab       (get-oaks-parcel)))  ([script]     (->> script          (walk [                                                                                                                                                                                                                                       ])          (walk-thru-grass           [      ])          (walk [                                                ])                          (do-nothing 1))))

А вот функция, которая находит кратчайшую последовательность команд для переноса нужных предметов в сундук:

(defn-memo hacking-10  ([] (hacking-10 (hacking-9)))  ([script]     (->> script          begin-deposit          (deposit-held-item 17 230)          (deposit-held-item-named :parlyz-heal 55)          (deposit-held-item 14 178)          (deposit-held-item-named :water-stone 29)          (deposit-held-item 14 32)          (deposit-held-item-named :TM18 1)          (deposit-held-item 13 1)          (deposit-held-item 13 191)          (deposit-held-item-named :TM02 98)          (deposit-held-item-named :TM09 1)          close-menu)))

Вооружившись этими инструментами, я составил программу ввода, которая может быть представлена в инвентаре. Это было непросто, потому что многие полезные коды не соответствуют никакому предмету, а количество предметов не может быть больше 99. Итак, вот что у меня получилось:

Программа ввода

(defn pc-item-writer-program

[]

(let [;;limit 75

limit 201 ;; (item-hack 201 is the smallest I could make this.)

[target-high target-low] (disect-bytes-2 pokemon-list-start)]

(flatten

[[0x00 ;; (item-hack) no-op (can't buy repel (1E) at celadon)

0x1E ;; load limit into E

limit

0x3F ;; (item-hack) set carry flag no-op

;; load 2 into C.

0x0E ;; C == 1 means input-first nybble

0x04 ;; C == 0 means input-second nybble

0x21 ;; load target into HL

target-low

target-high

0x37 ;; (item-hack) set carry flag no-op

0x00 ;; (item-hack) no-op

0x37 ;; (item-hack) set carry flag no-op

0x00 ;; (item-hack) no-op

0xF3 ;; disable interrupts

;; Input Section

0x3E ;; load 0x20 into A, to measure buttons

0x10

0x00 ;; (item-hack) no-op

0xE0 ;; load A into [FF00]

0x00

0xF0 ;; load 0xFF00 into A to get

0x00 ;; button presses

0xE6

0x0F ;; select bottom four bits of A

0x37 ;; (item-hack) set carry flag no-op

0x00 ;; (item-hack) no-op

0xB8 ;; see if input is different (CP A B)

0x00 ;; (item-hack) (INC SP)

0x28 ;; repeat above steps if input is not different

;; (jump relative backwards if B != A)

0xED ;; (literal -19) (item-hack) -19 == egg bomb (TM37)

0x47 ;; load A into B

0x0D ;; dec C

0x37 ;; (item-hack) set-carry flag

;; branch based on C:

0x20 ;; JR NZ

23 ;; skip "input second nybble" and "jump to target" below

;; input second nybble

0x0C ;; inc C

0x0C ;; inc C

0x00 ;; (item-hack) no-op

0xE6 ;; select bottom bits

0x0F

0x37 ;; (item-hack) set-carry flag no-op

0x00 ;; (item-hack) no-op

0xB2 ;; (OR A D) -> A

0x22 ;; (do (A -> (HL)) (INC HL))

0x1D ;; (DEC E)

0x00 ;; (item-hack)

0x20 ;; jump back to input section if not done

0xDA ;; literal -36 == TM 18 (counter)

0x01 ;; (item-hack) set BC to literal (no-op)

;; jump to target

0x00 ;; (item-hack) these two bytes can be anything.

0x01

0x00 ;; (item-hack) no-op

0xBF ;; (CP A A) ensures Z

0xCA ;; (item-hack) jump if Z

target-low

target-high

0x01 ;; (item-hack) will never be reached.

;; input first nybble

0x00

0xCB

0x37 ;; swap nybbles on A

0x57 ;; A -> D

0x37 ;; (item-hack) set carry flag no-op

0x18 ;; relative jump backwards

0xCD ;; literal -51 == TM05; go back to input section

0x01 ;; (item-hack) will never reach this instruction

]

(repeat 8 [0x00 0x01]);; these can be anything

[;; jump to actual program

0x00

0x37 ;; (item-hack) set carry flag no-op

0x2E ;; 0x3A -> L

0x3A

0x00 ;; (item-hack) no-op

0x26 ;; 0xD5 -> L

0xD5

0x01 ;; (item-hack) set-carry BC

0x00 ;; (item-hack) these can be anything

0x01

0x00

0xE9 ;; jump to (HL)

Мне особенно пригодились глитч-предметы 0x00 и 0xFF. 0x00 стоит почти половину максимально возможного числа денег, и всего двух хватило на то, чтоб купить все необходимые предметы. К тому же 0x00 это NO-OP, который я могу вставлять куда угодно, чтобы подогнать программу под требования инвентаря. 0xFF имеет другое полезное свойство. В норме игра объединяет стеки предметов: если купить, например, покеболл, а потом ещё один покеболл, то инвентарь будет выглядеть вот так:

pokeball x2

Но если где-то перед тем в списке предметов есть 0xFF, то эта функция отключается, что позволяет мне получить вот такой инвентарь:

pokeball x1pokeball x1pokeball x1

Так что я вставляю в самое начало инвентаря 0xFF и больше об этом не беспокоюсь.

Полезная нагрузка, которую я залил в Gameboy, это тоже последовательность программ. Я создал упрощённый формат MIDI, имплементировал его в машинных кодах GameBoy, и перевёл в свой формат мелодию с http://www.everyponysings.com/. В полезной нагрузке последней программы ввода содержится как сам MIDI-файл, так и интерпретатор. Картинка устроена так же я перевёл PNG-файл в Gameboy-совместимый формат и добавил код, который её показывает. И картинку, и мелодию загружает последняя программа ввода, так что исходники можно увидеть на экране эмулятора (или скачать с http://hg.bortreb.com/vba-clojure).

Подробнее..

MoVfuscator2, безумный компилятор

21.05.2021 10:23:30 | Автор: admin


Однажды один умный чувак (Кристофер Домас) читал статью другого умного чувака (Стивена Долана) про удивительную особенность архитектуры x86. Стивен ругал её за избыточность и утверждал, что набор инструкций можно сократить до одной лишь mov, потому что она Тюринг-полная. Если бы Стивен не был таким умным, в его словах можно было бы усомниться, но у Кристофера загорелись глаза: проработав двадцать лет с x86, он не слышал ни о чём подобном, и ему страшно захотелось написать компилятор, который бы переводил весь код в наборы одних лишь mov-инструкций. Так родились M/o/Vfuscator и M/o/Vfuscator2, наглядно иллюстрирующие ненормальное программирование.

Идея


Небольшое отступление про инструкцию mov: это самая простая инструкция в ассемблере, перемещающая значение из памяти в регистры или из регистров в память. Как может штука, перекладывающая байты из одного места в другое, оказаться Тьюринг-полной? Ну, если вам правда интересно, почитайте оригинальную статью Стивена с доказательством. Если не очень, перейдём сразу к выводу:

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


Собственно, на последних словах Кристофер и загорается этой идеей и попутно соображает, что через подобную компиляцию можно нехило так обфусцировать код сам чёрт ногу сломит в этих бесконечных mov'ax! Для сравнения, обычный, читаемый ассемблер:



и та же самая программа на мувах:



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

mov может сравнивать значения
Допустим, вы хотите сравнить x и y, для этого вам понадобится следующий код:

  mov [x], 0  mov [y], 1  mov R, [x]


Если x == y, то в третьей строчке, где считывается значение по адресу x, окажется не ноль, а перезаписавшая его единица.
Если x != y, то считается ноль, так как единица лежит по другому адресу.

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



Ограничения
Для выполнения требуется одна инструкция jmp start (в конце списка mov'ов) для перевода программы в начало; для остановки нужен заведомо нерабочий адрес памяти.

Дальше Крис добавляет свои требования:

  • Использовать примитивные операции машины Тьюринга как основу для высокоуровневой логики
  • Работать надо с реальными данными, не с абстрактными символами (эксперимент Стивена всё-таки академичен, далёк от реального мира)
  • Должны быть реализованы основные операции: условные ветвления, арифметика, логика, циклы и так далее


Подробно о реализации некоторых вещей можно послушать в его докладе по ссылке (таймкод 9:06), а мы сразу перепрыгнем к состоянию оно реализовано и работает, чтобы не пересказывать оригинал.

Реализация


Первая версия компилятора была написана для брейнфака, для ощущения максимального абсурда и тщетности жизни реверсера, но, конечно, она осталась ужасно далека от реальных примеров и задач. Поэтому Крис спустя пару лет ВНЕЗАПНО выпустил M/o/Vfuscator2, рабочий mov-компилятор для С. Впечатляющий апгрейд, не правда ли?





Заявлена относительно легкая адаптация компилятора под другие платформы и языки, но всё же создавался он именно для x86, и с ним связан ворох особенностей и ограничений:

  • Для дробных чисел используется самописный эмулятор плавающей точки, из-за размера поставляется в трёх версиях: softfloat32.o для float, softfloat64.o для float и double, и softfloatfull для полной поддержки стандарта IEEE
  • Так как арифметика строится на таблицах поиска, таблицы символов могут занимать огромное количество места, и их, возможно, придётся обрезать флагом -s
  • Компилятор работает строго на C89 из-за использования LCC в качестве фронтенда. Нельзя использовать bool, for (int ...), и другие фишки C99
  • Код с нестрогой типизацией или небезопасными конвертациями, скорее всего, не скомпилируется тоже из-за LCC
  • Функция, использующая внешние библиотеки, без прототипа лишает компилятор информации о необходимости и моменте подключения этих библиотек, что почти гарантированно повесит приложение
  • Вызовы внешних функций (printf и т.д.) через указатели функций еще не реализованы
  • Для подключения библиотек, скомпилированных не на mov, могут потребоваться другие инструкции. Полностью избавиться от них можно, перекомпилировав в mov все ресурсы


Заключение


Несмотря на всю крутизну проекта и Кристофера, нужно понимать, что такая обфускация скорее игрушка, чем реальный рабочий инструмент. И всё же, учитывая возможность прикрутить другие фронтенды и архитектуры открывает для M/o/Vfuscator больше возможностей, чем мог бы получить другой безумный ассемблерный проект.

Информацию по установке и использованию можно найти на гитхабе.



На правах рекламы


Эпично! Недорогие серверы на базе новейших процессоров AMD EPYC для размещения проектов любой сложности, от корпоративных сетей и игровых проектов до лендингов и VPN.

Присоединяйтесь к нашему чату в Telegram.

Подробнее..

Перевод Как игре Pitfall для Atari удалось поместить 255 комнат в картридж на 4КБ

24.05.2021 10:16:39 | Автор: admin
Игры для Atari 2600 разрабатывались в условиях сильных ограничений. Когда Уоррен Робинетт продвигал идею, которая в дальнейшем станет игрой Adventure (в ней нужно исследовать мир из множества комнат и подбирать предметы, которые помогают игроку в пути), ему отказали, потому что посчитали, что её невозможно реализовать. И это было логично. Консоль появилась в конце 70-х; до Робинетта никто ещё не создавал игру с несколькими экранами. Это была эпоха Space Invaders и Pac Man, когда весь игровой мир постоянно находился у игрока перед глазами, поэтому то, что выпущенная в 1980 году Adventure состояла из 30 комнат, было весьма впечатляюще.


Первый экран игры Adventure. Игрок управляет точкой (которую Робинетт называл человеком).

Разработчикам даже пришлось объяснять эту концепцию в руководстве к игре:

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

Наличие нескольких комнат было довольно большой инновацией, а то, что в Adventure удалось реализовать целых 30 комнат, стало настоящей революцией. Однако в созданной Дэвидом Крэйном и выпущенной в 1983 году Pitfall! таких комнат было 255, и каждая из них была более сложной (с точки зрения графики), чем любая комната Adventure. В статье я расскажу, как этого удалось добиться.

Примечание: в игре Superman было несколько комнат и её выпустили до Adventure, но она создавалась на основе кода Adventure.


Типичный экран Pitfall!

Но чтобы в полной мере осознать сложность реализации этого достижения, стоит рассказать о сложностях, с которыми сталкивались программисты игр для Atari. Сама консоль имела всего 128 байт ОЗУ. Это 1024 бит. Для сравнения: это предложение при кодировании в ASCII занимает больше места, не говоря уже о формате UTF, в котором оно на самом деле закодировано. Этого достаточно, чтобы показать, что в Atari было не так много памяти

Но это ведь не важно, в самом картридже ведь будет достаточно места? Ну, в какой-то мере да. В то время картриджи Atari 2600 обычно содержали 4 КБ ПЗУ, подавляющее большинство которого приходилось занимать кодом игры. Даже если опустить необходимость хранения кода, на каждую комнату можно выделить всего 16 байт, а ведь код всё равно нужно где-то хранить.

Примечание: адресуемое пространство в Atari 2600 составляло всего 2 КБ. Использование 4 КБ было возможно благодаря технике под названием переключение банков (bank switching).

Так как же Крэйн справился с такими ограничениями пространства при создании игры?

Процедурная генерация


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

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

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

Описание комнаты


Для описания схемы всей комнаты Крэйн использовал всего один байт. Это может показаться невероятным, учитывая те действия, которые происходят в каждой комнате, но на самом деле всё довольно просто.

Байт, в котором хранится схема текущей комнаты, разделён на четыре части:

Биты 0-2: объекты


Первые три бита определяют типы создаваемых объектов. Эта система усложняется двумя аспектами, которые контролируют биты 3-5.

Во-первых, комната может содержать сокровище (если значения битов 3-5 равны 101). Если в ней есть сокровище, то обычный предмет, определяемый битами 0-2, не будет создан, и его место займёт соответствующее сокровище.

Во-вторых, существуют крокодилы (если значения битов 3-5 равны 100), при которых не создаются никакие другие объекты. Кроме того, если значения битов 0-2 равны 010, 011, 110 или 111, то создаётся лоза, позволяющая игроку раскачаться и перепрыгнуть через крокодилов. При всех других значениях лозы не будет и игроку придётся прыгать по головам крокодилов.

Примечание: я всегда записываю первым старший бит, поэтому 100 точнее было бы назвать битами с 5-й по 3-й.

Правила создания предметов и сокровищ:

Биты Предмет Сокровище
000 одно катящееся бревно деньги
001 два катящихся бревна серебро
010 два катящихся бревна золото
011 три катящихся бревна кольцо
100 одно неподвижное бревно деньги
101 три неподвижных бревна серебро
110 огонь золото
111 змея кольцо

(С этим было довольно сложно разобраться.)

Биты 3-5: тип ямы


Биты 3-5 контролируют тип ямы или ям, с которыми столкнётся игрок.

Биты Тип ямы
000 одна дыра в земле
001 три дыры в земле
010 ноль дыр в земле
011 ноль дыр в земле
100 крокодилы в воде
101 подвижная битумная яма с сокровищем
110 подвижная битумная яма
111 подвижные зыбучие пески

Подвижные битумные ямы без сокровища (биты 110) всегда имеют лозу, а если сокровище есть (биты 101), то над битумной ямой не будет лозы (благодарю Майка Ширальди за то, что сообщил мне это).

Биты 6-7: деревья


Биты 6 и 7 определяют паттерн деревьев. Это никак не влияет на геймплей, но даёт игроку ощущение смены локаций. Паттерны деревьев похожи друг на друга, поэтому я не буду вдаваться в подробности, но если вы хотите посмотреть, то они используются в комнатах 1, 2, 3 и 5, и имеют битовые паттерны 11, 10, 00 и 01.

Бит 7: подземная стена


Бит 7 также используется для того, чтобы управлять расположением подземной стены справа или слева. Он не управляет наличием стены, оно определяется в другой части кода, но если стена есть, то значение бита, равное 0, помещает её слева, а значение 1 справа.

Вот так единственный байт определяет структуру текущей комнаты. Но как я говорил, в памяти хранится только текущая комната. Это возможно благодаря способу, которым генерируются комнаты.

Регистры сдвига с линейной обратной связью


Описывающие комнату байты генерируются системой, которую Крэйн назвал полиномным счётчиком (polynomial counter); сегодня мы называем её регистром сдвига с линейной обратной связью (linear feedback shift register, LFSR).

LFSR это способ генерации псевдослучайных чисел по порождающему значению (seed): берётся двоичное число, выполняется логический сдвиг влево или вправо, а затем вычисляется входящий бит через линейную функцию исходных битов. Обычно этой функцией является последовательность из нескольких XOR.

В качестве примера давайте используем LFSR в Pitfall!

Когда игрок начинает игру, байт комнаты имеет шестнадцатеричное значение C4 (11000100 в двоичном виде, 196 в десятичном). Это порождающее значение (seed). Когда игрок переходит на одну комнату вправо, байт сдвигается влево, и младший бит (бит 0) становится XOR битов 3, 4, 5 и 7. Формула такова:

b0 b3' + b4' + b5' + b7'

Где + обозначает XOR, а апостроф бит в предыдущем состоянии. Этот паттерн имеет желательное свойство является LFSR максимальной длины, то есть создаёт каждое сочетание из 8 бит, за исключением одним нулей. Это позволяет миру Pitfall! и содержать максимальное количество комнат, и иметь равную вероятность любой битовой строки (повторюсь, за исключением нулей).

Примечание: + обозначает XOR, потому что сложение mod 2 эквивалентно операции XOR над битами.

То есть когда мы перемещаемся после первой комнаты вправо, байт меняет значение с 11000100 на 10001001. Все биты сдвигаются влево, а затем биту 0 присваивается значение 1, так как 1 = 0 + 0 + 0 + 1.

На ассемблере 6502 это было реализовано так:

; room' = room << 1 | (bit3 + bit4 + bit5 + bit7)LOOP_ROOM:  LDA ROOM  ASL  EOR ROOM  ASL  EOR ROOM  ASL  ASL  EOR ROOM  ASL  ROL ROOM  DEX  BPL LOOP_ROOM

Код целиком можно посмотреть здесь. Данный фрагмент начинается со строки 3012.

ROOM это байт, описывающий текущую комнату. Прежде чем переходить к тому, как он работает, важно обратить внимание на последние две строки и понять, почему всё это находится в цикле. Крэйн хотел, чтобы если Pitfall Harry (главный герой Pitfall!) находится в подземелье, то прохождение через комнату перемещало его через три комнаты. DEX выполняет декремент регистра X, а BPL выполняет ветвление, если результаты предыдущего вычисления не были отрицательными, поэтому Крэйн реализовал это поведение, задавая регистру X значение 2 перед вызовом этой подпроцедуры, если Гарри находится под землёй. В противном случае регистр X имеет значение 0 и зацикленность отсутствует.

Если точнее, ROOM это место в памяти, где находится байт, описывающий комнату.

Вот поэтому это цикл. Остальная часть кода, как часто бывает с ассемблером для Atari, довольно запутанна. Я пишу статью не про ассемблер 6502, поэтому не буду вдаваться в подробности, но, по сути, команды ASL (arithmetic shift left, арифметический сдвиг влево) перемещают биты в правильные позиции, а команды EOR (exclusive or, исключающее ИЛИ) выполняют XOR битов. В конце команда ROL (rotate left, вращение влево) сдвигает байт ROOM влево, записывая в бит 0 бит переноса. Этот бит переноса является результатом предыдущих EOR и ASL. Всё вышеописанное создаёт нужное поведение.

Если мы хотим увидеть каждую комнату, которую генерирует этот код, то можем воспользоваться приведённым ниже кодом ассемблера 6502, который обходит в цикле приведённый выше код, пока байт не вернётся к начальному значению, и сохраняет каждй сгенерированный байт по порядку в адреса с $00 по $FF (с 0 по 255).

  LDA #0          ; initialize address offset to 0  TAXdefine ROOM $00define SEED $C4  LDA #SEED  STA ROOMLOOP_ROOM:        ; do all the LFSR stuff  ASL  EOR ROOM  ASL  EOR ROOM  ASL  ASL  EOR ROOM  ASL  ROL ROOM  LDA ROOM  INX             ; increment address offset  STA $00,X       ; store generated byte  CMP #SEED       ; stop if we complete a cycle  BEQ STOP  JMP LOOP_ROOM   ; get next room byteSTOP:  BRK

Примечание: хороший эмулятор 6502 можно найти здесь.

Но всё это не даёт понимания того, почему дизайн Крэйна был настолько гениальным. Выше описывается происходящее, когда мы идём вправо, но что если мы пойдём влево, чтобы вернуться в предыдущую комнату? Восемь битов, описывающих эту комнату, никогда не сохраняются в памяти; в памяти хранится только текущая комната. Как Pitfall! реализует движение влево? При помощи этого LFSR:

b7 b4' + b5' + b6' + b0'

Примечание: я достаточно вольно обращаюсь с терминологией. Строго говоря, LFSR называется регистр, с которым производят действия, но здесь я буду называть этим термином формулу, вычисляющую входящий бит.

Этот LFSR примечателен тем, что является инверсией предыдущего. Каждый раз, когда игрок идёт влево, этот LFSR отменяет последнее действие, сделанное LFSR, когда игрок шёл вправо. Здесь и далее я буду называть этот LFSR левым LFSR, а предыдущий правым LFSR.

На ассемблере 6502 левый LFSR был реализован следующим образом:

; room' = room >> 1 | ((bit4 + bit5 + bit6 + bit0) * 128)LOOP_ROOM:  LDA ROOM  ASL  EOR ROOM  ASL  EOR ROOM  ASL  ASL  ROL  EOR ROOM  LSR  ROR ROOM  DEX  BPL LOOP_ROOM

Можно заметить, что этот LFSR тоже имеет метку LOOP_ROOM. Её мы взяли из дизассемблированного кода, потому что не знаем, как сам Крэйн назвал этот фрагмент кода, но то, что они имеют одинаковую метку это вполне нормально. Так получилось потому, что команды ветвления (например, BPL) могут выполнять смещение счётчика программ максимум на 255, а эти две метки разделены более чем тысячей команд. Чтобы перемещаться на более дальние расстояния, потребуется или JMP или JSR, то есть команды безусловных переходов.

Давайте минуту поразмыслим над тем, чего добился Крэйн. Он нашёл LFSR, который является инвертируемым и имеет максимальную длину. Это очень впечатляющий программный трюк. Но не верьте мне на слово, что эти два LFSR являются обратными друг другу, я докажу это вам.

Операции LFSR игры Pitfall инвертируемы. Доказательство:


Рассмотрим последовательность из восьми бит B = b7b6b5b4b3b2b1b0. Используем Br для обозначения B, к которому применён правый LFSR и Bl для обозначения B, к которому применён левый LFSR. Мы хотим показать, что Brl = Blr = B. То есть мы хотим показать, что результат применения сначала правого, а потом левого LFSR, или сначала левого, а потом правого, аналогичны отсутствию действий.

Примечание: формально мы покажем, что композиция двух функций в любом порядке равна функции тождественности, то есть две функции по определению являются обратными друг другу.

Чтобы показать, что Brl = B, вспомним, что такое правый LFSR:

b0 b3' + b4' + b5' + b7'

Применив это уравнение к B = b7b6b5b4b3b2b1b0, мы получим следующее:

Бит 7 Бит 6 Бит 5 Бит 4 Бит 3 Бит 2 Бит 1 Бит 0
B b7 b6 b5 b4 b3 b2 b1 b0
Br__ b6 b5 b4 b3 b2 b1 b0 b3 + b4 + b5 + b7

Тогда применив левый LFSR, который, как мы помним, имеет вид:

b7 b4' + b5' + b6' + b0'

к Br, мы получим:

Бит 7 Бит 6 Бит 5 Бит 4 Бит 3 Бит 2 Бит 1 Бит 0
B b7 b6 b5 b4 b3 b2 b1 b0
Br b6 b5 b4 b3 b2 b1 b0 b3 + b4 + b5 + b7
Brl___ 2(b3 + b4 + b5) + b7 = b7 b6 b5 b4 b3 b2 b1 b0

И это подтверждает факт, что Brl = B. Доказательство того, что Blr = B, почти такое же, поэтому я оставлю его в качестве задания для читателя*.

* Всегда ненавидел, когда так писали в учебниках.

Сказанное выше можно подтвердить и простым кодом, поэтому если вы хотите попробовать, то вот маленькая программа на JavaScript. Я добавил в неё и функцию, перечисляющую все комнаты.

Вот так Pitfall! создаёт свой мир. Компактная запись в сочетании с инвертируемым регистром сдвига с линейной обратной связью.



Постскриптум: как я во всём этом разобрался


Можно решить, что информация о столь важной для истории и популярной игры, как Pitfall!, широко распространена и доступна в Интернете. Но на самом деле это не так.

Многие знали, что в игре используется LFSR, но способ его реализации, насколько я знаю, не описан подробно нигде, за исключением самого ассемблера. Но когда я нашёл проанализированную и откомментированную версию ассемблера, то оказалось, что представленное там описание LFSR на самом деле неверно! По крайней мере, неверно описание левого LFSR. Но ошибка была довольно очевидна, поэтому было несложно понять, как он реализуется на самом деле.

Примечание: изначально в комментарии говорилось, что за декрементом LFSR следовал XOR с битом 1 вместо бита 0. Теперь эта ошибка исправлена.

Гораздо сложнее было разобраться, как байт преобразуется в рендеринг мира. Нигде, ни в одном обсуждении, ни на одном сайте, ни в одной книге и даже в откомментированном исходном коде не было описания этой последовательности битов, соответствующей паттернам в комнате. Сначала я попробовал разобраться в ассемблере, но в написанном для Atari ассемблере столько оптимизаций и хаков, и в нём используется так много трюков, что мне стало очевидно: пришлось бы приложить слишком много усилий.

Как же я это сделал? Я написал небольшую программу для генерации последовательности LFSR (программу на JavaScript, ссылка на которую дана выше) и сравнил её с комнатами. Проведение этого анализа для бита 7, управляющего стороной экрана, на которой отрисовывалась подземная стена, было простой задачей, как и биты 6 и 7, управляющие деревьями. Но всё остальное оказалось довольно монотонным. Бесценным ресурсом для меня стала эта карта.

Меня удивило, что, насколько я могу судить, мне довелось первым подробно описать способ рендеринга мира в игре Pitfall!, но в то же время я разочарован. Если вы не смотрели этот доклад на GDC о сохранении истории игр, то вам точно стоит это сделать. В отличие от истории многих других дисциплин, история ПО сохраняется не очень хорошо, несмотря на то, что сохранить её должно быть проще всего. У нас не сохранился оригинальный исходный код почти ни одной игры для Atari, NES, SNES, ColecoVision, и так далее. Дизассемблированный код бесценен, но всё-таки это не оригинал. И он не позволяет прочитать комментарии разработчиков.

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

Подробнее..

Перевод Внутренности Linux как procselfmem пишет в недоступную для записи память

26.05.2021 12:10:08 | Автор: admin

Странная причудливость псевдофайла /proc/*/mem заключается в его пробивной семантике. Операции записи через этот файл будут успешными даже если целевая виртуальная память помечена как недоступная для записи. Это сделано намеренно, и такое поведение активно используется проектами вроде компилятора Julia JIT или отладчика rr.

Но возникают вопросы: подчиняется ли привилегированный код разрешениям виртуальной памяти? До какой степени оборудование может влиять на доступ к памяти ядра?

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

Патчим libc с помощью /proc/self/mem


Как выглядит эта пробивная семантика? Рассмотрим код:

#include <fstream>#include <iostream>#include <sys/mman.h>/* Write @len bytes at @ptr to @addr in this address space using * /proc/self/mem. */void memwrite(void *addr, char *ptr, size_t len) {  std::ofstream ff("/proc/self/mem");  ff.seekp(reinterpret_cast<size_t>(addr));  ff.write(ptr, len);  ff.flush();}int main(int argc, char **argv) {  // Map an unwritable page. (read-only)  auto mymap =      (int *)mmap(NULL, 0x9000,                  PROT_READ, // <<<<<<<<<<<<<<<<<<<<< READ ONLY <<<<<<<<                  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);  if (mymap == MAP_FAILED) {    std::cout << "FAILED\n";    return 1;  }  std::cout << "Allocated PROT_READ only memory: " << mymap << "\n";  getchar();  // Try to write to the unwritable page.  memwrite(mymap, "\x40\x41\x41\x41", 4);  std::cout << "did mymap[0] = 0x41414140 via proc self mem..";  getchar();  std::cout << "mymap[0] = 0x" << std::hex << mymap[0] << "\n";  getchar();  // Try to writ to the text segment (executable code) of libc.  auto getchar_ptr = (char *)getchar;  memwrite(getchar_ptr, "\xcc", 1);  // Run the libc function whose code we modified. If the write worked,  // we will get a SIGTRAP when the 0xcc executes.  getchar();}

Здесь /proc/self/mem используется для записи в две недоступные для записи страницы памяти. Первая содержит сам код, а вторая принадлежит libc (функции getchar). Последняя часть вызывает больший интерес: код записывает байт 0xcc (точка прерывания в приложениях под x86-64), который в случае своего исполнения заставит ядро предоставить нашему процессу SIGTRAP. Это буквально меняет исполняемый код libc. И если при следующем вызове getchar мы получим SIGTRAP, то будем знать, что запись была успешной.

Вот как это выглядит при запуске программы:


Работает! Посередине выводятся выражения, которые доказывают, что значение 0x41414140 было успешно записано и считано из памяти. Последний вывод показывает, что после патчинга наш процесс получил SIGTRAP в результате нашего вызова getchar.

На видео:


Мы увидели, как эта возможность работает с точки зрения пользовательского пространства. Давайте копнём глубже. Чтобы полностью разобраться в том, как это работает, нужно посмотреть, как оборудование накладывает ограничения на память.

Оборудование


На платформе x86-64 есть две настройки процессора, которые управляют возможностью ядра обращаться к памяти. Они применяются модулем управления памятью (MMU).

Первая настройка бит защиты от записи, Write Protect bit (CR0.WP). Из руководства Intel (том 3, раздел 2.5) мы знаем:

Защита от записи (16-й бит CR0). Если задана, он не даёт процедурам уровня супервизора записывать в защищённые от записи страницы. Если бит пуст, то процедуры уровня супервизора могут записывать в защищённые от записи страницы (вне зависимости от настроек бита U/S; см. раздел 4.1.3 и 4.6).

Это не позволяет ядру записывать в защищённые от записи страницы, что, естественно, по умолчанию разрешено.

Вторая настройка предотвращение доступа в режиме супервизора, Supervisor Mode Access Prevention (SMAP) (CR4.SMAP). Полное описание, приведённое в томе 3, в разделе 4.6, многословное. Если вкратце, то SMAP полностью лишает ядро способности записывать или читать из памяти пользовательского пространства. Это предотвращает эксплойты, которые наполняют пользовательское пространство зловредными данными, которые ядро должно прочитать в ходе исполнения.

Если код ядра использует для доступа к пользовательскому пространству только одобренные каналы (copy_to_user и т.д.), то SMAP можно смело игнорировать, эти функции автоматически задействуют его до и после обращения к памяти. А что насчёт защиты от записи?

Если CR0.WP не задан, то реализация /proc/*/mem в ядре действительно сможет бесцеремонно записывать в защищённую от записи память пользовательского пространства.

Однако CR0.WP задаётся при загрузке и обычно живёт в течение всего времени работы систем. В этом случае при попытке записи будет выдаваться сбой страницы. Это, скорее, инструмент для копирования при записи (Copy-on-Write), чем средство защиты, поэтому не накладывает на ядро никаких реальных ограничений. Иными словами, требуется неудобная обработка сбоев, которая не обязательна при заданном бите.

Давайте теперь разберёмся с реализацией.

Как работает /proc/*/mem


/proc/*/mem реализован в fs/proc/base.c.

Структура file_operations содержит функции обработчика, а функция mem_rw() полностью поддерживает обработчик записи. mem_rw() использует для операций записи access_remote_vm(). А access_remote_vm() делает вот что:

  • Вызывает get_user_pages_remote(), чтобы найти физический фрейм, соответствующий целевому виртуальному адресу.
  • Вызывает kmap(), чтобы пометить этот фрейм доступный для записи в виртуальном адресном пространстве ядра.
  • Вызывает copy_to_user_page() для финального выполнения операций записи.

Эта реализация полностью обходит вопрос о способности ядра записывать в незаписываемую память пользовательского пространства! Контроль ядра над подсистемой виртуальной памяти позволяет полностью обойти MMU, позволяя ядру просто записывать в свое собственное адресное пространство, доступное для записи. Так что обсуждения CR0.WP становится неактуальным.

Рассмотрим каждый из этапов:

get_user_pages_remote()

Для обхода MMU ядру нужно, чтобы в приложении было вручную выполнено то, что MMU делает аппаратно. Сначала необходимо преобразовать целевой виртуальный адрес в физический. Этим занимается семейство функций get_user_pages(). Они проходят по таблицам страниц и ищут фреймы физической памяти, которые соответствуют заданному диапазону виртуальных адресов.

Вызывающий предоставляет контекст и с помощью флагов меняет поведение get_user_pages(). Особенно интересен флаг FOLL_FORCE, который передаётся mem_rw(). Флаг запускает check_vma_flags (логику проверки доступа в get_user_pages()), чтобы игнорировать запись в незаписываемые страницы и продолжить поиск. Пробивная семантика полностью относится к FOLL_FORCE (комментарии мои):

static int check_vma_flags(struct vm_area_struct *vma, unsigned long gup_flags){        [...]        if (write) { // If performing a write..                if (!(vm_flags & VM_WRITE)) { // And the page is unwritable..                        if (!(gup_flags & FOLL_FORCE)) // *Unless* FOLL_FORCE..                                return -EFAULT; // Return an error        [...]        return 0; // Otherwise, proceed with lookup}

get_user_pages() стоже соблюдает семантику копирования при записи (CoW). Если определяется запись в таблицу незаписываемой страницы, то эмулируется сбой страницы с помощью вызова handle_mm_fault, основного обработчика страничных ошибок. Это запускает соответствующую подпрограмму обработки копирования при записи посредством do_wp_page, которая при необходимости копирует страницу. Так что если записи через /proc/*/mem выполняются приватным разделяемым маппингом, например, libc, то они видимы только в рамках процесса.

kmap()

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

На 64-битной платформе x86 вся физическая память отображается через область линейного отображения виртуального адресного пространства ядра. В этом случае kmap() работает очень просто: ему лишь нужно добавить начальный адрес линейного отображения в физический адрес фрейма, чтобы вычислить виртуальный адрес, в который отображён этот фрейм.

На 32-битной платформе x86 линейное отображение содержит подмножество физической памяти, так что функции kmap() может потребоваться отобразить фрейм с помощью выделения highmem-памяти и манипулирования таблицами страницы.

В обоих случая линейное отображение и highmem-отображение выполняются с защитой PAGE_KERNEL, которая позволяет запись.

copy_to_user_page()

Последний этап выполнение записи. Это делается с помощью copy_to_user_page(), по сути memcpy. Это работает, поскольку целью является записываемое отображение из kmap().

Обсуждение


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

Поразительно, что здесь не используется CR0.WP. Реализация элегантно обходит этот момент, пользуясь тем, что она не обязана обращаться к памяти через указатель, полученный из пользовательского пространства. Поскольку ядро целиком контролирует виртуальную память, оно может просто переотобразить физический фрейм в собственное виртуальное адресное пространство с произвольными разрешениями и делать с ним что угодно.

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

Заключение


Изучив подробности пробивной семантики в реализации /proc/*/mem мы можем отразить взаимосвязи между ядром и процессором. На первый взгляд, способность ядра писать в недоступную для записи память вызывает вопрос: до какой степени процессор может влиять на доступ ядра к памяти? В руководстве описаны механизмы управления, которые могут ограничивать действия ядра. Но при внимательном рассмотрении оказывается, что ограничения в лучшем случае поверхностны. Это простые препятствия, которые можно обойти.
Подробнее..

C20 удивить линкер четыремя строчками кода

09.06.2021 16:12:52 | Автор: admin

Представьте себе, что вы студент, изучающий современные фичи C++. И вам дали задачу по теме concepts/constraints. У преподавателя, конечно, есть референсное решение "как правильно", но для вас оно неочевидно, и вы навертели гору довольно запутанного кода, который всё равно не работает. (И вы дописываете и дописываете всё новые перегрузки и специализации шаблонов, покрывая всё новые и новые претензии компилятора).

А теперь представьте себе, что вы преподаватель, который увидел эту гору, и захотел помочь студенту. Вы стали упрощать и упрощать его код, и даже тупо комментировать куски юнит-тестов, чтобы оно хоть как-то заработало... А оно всё равно не работает. Причём, в зависимости от порядка юнит-тестов, выдаёт разные результаты или вообще не собирается. Где-то спряталось неопределённое поведение. Но какое?

Сперва преподаватель (то есть, я) минимизировал код вот до такого: https://gcc.godbolt.org/z/TaMTWqc1T

// пусть у нас есть концепты указателя и вектораtemplate<class T> concept Ptr = requires(T t) { *t; };template<class T> concept Vec = requires(T t) { t.begin(); t[0]; };// и три перегрузки функций, рекурсивно определённые друг через другаtemplate<class T> void f(T t) {  // (1)  std::cout << "general case " << __PRETTY_FUNCTION__ << std::endl;}template<Ptr T> void f(T t) {  // (2)  std::cout << "pointer to ";  f(*t);  // допустим, указатель не нулевой}template<Vec T> void f(T t) {  // (3)  std::cout << "vector of ";  f(t[0]);  // допустим, вектор не пустой}// и набор тестов (в разных файлах)int main() {  std::vector<int> v = {1};    // тест А  f(v);  // или тест Б  f(&v);  // или тест В  f(&v);  f(v);  // или тест Г  f(v);  f(&v);}

Мы ожидаем, что

  • f(v) выведет "vector of general case void f(T) [T=int]"

  • f(&v) выведет "pointer to vector of general case void f(T) [T=int]"

А вместо это получаем

  • А: "vector of general case void f(T) [T=int]"

  • Б: "pointer of general case void f(T) [T=std::vector<int>]" ?

  • В: clang выводит Б и А ?!, gcc ошибку линкера

  • Г: clang и gcc выводят ошибку линкера

Что здесь не так?!

А не так здесь две вещи. Первая это то, что из функции (2) видны объявления только (1) и (2), поэтому результат разыменования указателя вызывается как (1).

Без концептов и шаблонов это тоже прекрасно воспроизводится: https://gcc.godbolt.org/z/47qhYv6q4

void f(int x)    { std::cout << "int" << std::endl; }void g(char* p)  { std::cout << "char* -> "; f(*p); }  // f(int)void f(char x)   { std::cout << "char" << std::endl; }void g(char** p) { std::cout << "char** -> "; f(**p); }  // f(char)int main() {  char x;  char* p = &x;  f(x);  // char  g(p);  // char* -> int  g(&p); // char** -> char}

В отличие от инлайн-определений функций-членов в классе, где все объявления видны всем, определение свободной функции видит только то, что находится выше по файлу.

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

Ладно, с этим разобрались. Вернёмся к шаблонам. Почему в тестах В и Г мы получили нечто, похожее на нарушение ODR?

Если мы перепишем код вот так:

template<class T> void f(T t) {.....}template<class T> void f(T t) requires Ptr<T> {.....}template<class T> void f(T t) requires Vec<T> {.....}

то ничего не изменится. Это просто другая форма записи. Требование соответствия концепту можно записать и так, и этак.

Но вот если прибегнем к старому доброму трюку SFINAE, https://gcc.godbolt.org/z/4sar6W6Kq

// добавим второй аргумент char или int - для разрешения неоднозначностиtemplate<class T, class = void> void f(T t, char) {.....}template<class T> auto f(T t, int) -> std::enable_if_t<Ptr<T>, void> {.....}template<class T> auto f(T t, int) -> std::enable_if_t<Vec<T>, void> {.....}..... f(v, 0) .......... f(&v, 0) .....

или ещё более старому доброму сопоставлению типов аргументов, https://gcc.godbolt.org/z/PsdhsG6Wr

template<class T> void f(T t) {.....}template<class T> void f(T* t) {.....}template<class T> void f(std::vector<T> t) {.....}

то всё станет работать. Не так, как нам хотелось бы (рекурсия по-прежнему сломана из-за правил видимости), но ожидаемо (вектор из f(T*) видится как "general case", из main - как "vector").

Что же ещё с концептами/ограничениями?

Коллективный разум, спасибо RSDN, подсказал ещё более минималистичный код!

Всего 4 строки: https://gcc.godbolt.org/z/qM8xYKfqe

template<class T> void f() {}void g() { f<int>(); }template<class T> void f() requires true {}void h() { f<int>(); }

Функция с ограничениями считается более предпочтительной, чем функция без них. Поэтому g() по правилам видимости выбирает из единственного варианта, а h() - из двух выбирает второй.

И вот этот код порождает некорректный объектный файл! В нём две функции с одинаковыми декорированными именами.

Оказывается, современные компиляторы (clang 12.0, gcc 12.0) не умеют учитывать requires в декорировании имён. Как когда-то старый глупый MSVC6 не учитывал параметры шаблона, если те не влияли на тип функции...

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

Проблема известна с 2017 года, но прогресса пока нет.

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

Подробнее..

Свой ремейк ZX игры Reskue в Steam

12.06.2021 16:15:59 | Автор: admin

Да, понимаю, что это игра про учёных, но это не игра про немого учёного с монтировкой с цифрой 3, которую все ждут. Что вышло за 4 года разработки.

Официальное описание: Научная станция в глубоком космосе перестала отвечать на запросы. Что там произошло? Вы агент, прибывший выяснить, что случилось и Ваша задача спасти важное открытие, сделанное на станции.

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

Заправьте свой корабль радиоактивным топливом и спасите ученых. Берегите себя, науку и удачи!

Это ретро экшен, требующий достаточной быстроты, либо ловкости, либо хитрости от игрока. Есть несколько тактик прохождения игры. Кроме арсенала в 10 видов оружия, каждое из которых накладывает особый эффект на врага и имеет несколько режимов стрельбы, есть ещё и хитрость в виде ловушек или предметов сильно отвлекающих противника. некоторых настолько что игрок будет почти невидим для врагов. Даже на нормальной сложности игра не будет лёгкой прогулкой по которой игрока будут водить за ручку и обьяснять Press F to Win. Однако если вам игра покажется слишком лёгкой уровень сложности можно повысить.

В самой игре заложена и пара "пасхалок" напоминающих о спектруме. Я не скажу где они однако одна из них содержит пару обьектов из Boulder dash, а другая ещё что-то.

Ключевые обновления:

  • Добавлен режим хотсита - кооп (. затем kp9) и версус (. затем kp7). (Игра на 2 клавиатурах и/или джойстиках). Cетевая игра увы возможна только через Steam remote play.

  • Все почти враги теперь анимированы (новое видео пока не готово)

  • Внешний интерфейс (USER GUI) доработан. (Инвентарь, выбранное оружие, датчик жизни, кулдаун, селектор оружия.)

  • Встроенная игровая справка по F2 с описанием тактики поведения и арсенала.

Ключевые особенности

  • Редактор карт поддерживает импорт карт M2K (mission 2000), Rescue+ с реального ZX-Spectrum. Карты будут работать если их копировать программой HOBETA.

  • Мультиязычность и кроссплатформенность.

  • Высокая скорость работы даже на устаревших компьютерах.

Подсказка: Максимально просто игру можно пройти вдвоем - Один из игроков должен выбрать ученого при начале совместной игры.

Игра нетребовательная идёт почти на всём. Требования: 64бит и OpenGL 3.3 и хотя бы 30 мб места.

Steam Ранний доступ.

Все списки обновлений переводятся также на 3 языка как и сама игра. Прочесть можно на страничке игры

Я ранее писал статью на DTF и если кто хочет просто попробовать игру Reskue или M2K или Colony (очень ранняя версия) может скачать их ниже.

Видео новее пока нет. если только про кооператив и версус ссылка под видео. С тех пор игра была сильно переработана но я пока не осилил трейлер.

Хотсит на русском.

Новый трейлер на польском.

Я на вопросы иногда делаю выпуски видеоответов. Реже чем надо было бы, но делаю. https://www.youtube.com/watch?v=JJAe2b-kTgs - Руководство о портировании готовой игры на Love в Android APK (с подписью) для Linux. В блоге разработчика (devblog) бывают и арты и рисунки и другая всячина.

Игра написана с нуля и сохранившая в основном идею оригинала (Rescue od Mastertronic) и схожесть с Spectrum играми (тайлы, цветовая гамма, некоторая хардкорность геймплея и некоторые команды в Lua движке.). От оригинальной игры не используется ничего, перерисовано все что возможно. Изначально я делал ремейк собственной игры M2K (mission2000), затем понял что на основе движка могу сделать новую игру что и начал в 2019, вдохновили меня в коллективе confa-gd на конкурсе Джем победы на это. Канал по игре.

C чего всё начиналось:

Это оказался прекрасный фундамент для применения множества идей как Sci-fi так и просто современных удобств геймплея каждую из которых пришлось делать самому, т.к. движок мой авторский на Love framework. Я постарался бережно развить идею так как ранее с ограничениями платформы ее развить было бы значительно сложнее. Я с трудом нашел нескольких художников и аниматоров и за ещё 2 года и 16К получилась эта игра. Отдельная история и целая лекция получилась по работе со Steamworks чтобы игра попала в магазин, это заняло целых полгода в основном доработки тексту. Я даже не думаю что она вообще когда то отобьется и будет кормить меня и принесет мне хоть что то. мне просто хотелось возродить частичку классики так как я её вижу и сделать ее доступной всем.

Также я веду небольшой ютуб и телеграм каналы без мемасиков и приколов посвященный Linux для домашнего использования. Сборка в основном для тестирования Windows игр предназначена. Сам я выбрал Mint c Mate DE немного новостей немного взаимопомощи.

N.B. Людям, которые ждут, что один человек почти в одно лицо накодит им Ведьмака 3 в 3D за денек, прошу выйти из чата. Этим методом получатся только игры для Gry z kosza (польская передача об очень плохих играх).

Мне просто хочется, чтобы в эту игру можно было сыграть и через много лет спустя, я думаю Steam с нами всеми надолго и игра там точно проживёт много лет. Да название я написал в духе названия mortal kombat. А почему нет?

Также я время от времени обновляю код движка на github. У меня там несколько проектов.

Подробнее..

Разместить FORTH в 512 байтах

17.06.2021 12:20:28 | Автор: admin
Связь СЛОВ через словарикСвязь СЛОВ через словарик

Оригинал текста Июнь 10, 2021 - 38 минут чтения

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

11 лет назад vanjos72 описал на Reddit то, что он называет мысленным экспериментом: что если бы вас заперли в комнате с IBM PC, на котором нет операционной системы? Какое минимальное количество программного обеспечения вам понадобилось бы для начала, чтобы вернуться к комфортной работе?

Так получилось, что в последнее время у меня появилось много свободного времени, поэтому я решил сделать это больше, чем просто мысленный эксперимент. Увы, мой компьютер не был оснащен переключателями на передней панели, поэтому некоторые программы должны уже присутствовать на компьютере ...

Самым минимальным вариантом может быть простая программа, которая принимает ввод с клавиатуры, а затем переходит на нее. Поскольку подпрограммы ввода с клавиатуры в BIOS реализуют escape-коды alt+numpad, вам даже не нужно писать код преобразования базы.2Более того, циклу даже не нужно условие завершения а просто пишите в буфер обратно, пока не столкнетесь с существующим кодом и не перезапишете точку перехода. Такой подход занимает всего 14 байт:

6a00    push word 007      pop esfd      stdbf1e7c  mov di, buffer+16 ; Adjust to taste. Beware of fenceposting.        input_loop:b400    mov ah, 0cd16    int 0x16aa      stosbebf9    jmp short input_loop        buffer:

Однако перспектива вводить код таким образом не кажется мне привлекательной. Я решил, что, поскольку BIOS в любом случае загружает целый сектор, любая загрузочная программа, которая помещается в бутсектор, является честной игрой.3Очевидно, что хочется максимизировать полезность выбранной программы. Какую самую мощную вещь мы можем уместить в 510 байт?

Оскар Толедо написал много интересных программ размером с сектор. Среди них много игр, таких как DooM-подобная игра или шахматный ИИ, а также базовый интерпретатор BASIC, но самой, пожалуй, актуальной для нашего случая является bootOS:

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

Она открывает свои подпрограммы файловой системы с помощью интерфейса прерываний и включает встроенную команду, которая позволяет создать файл, набрав его шестнадцатиричное представление. Очень аккуратно, но, очевидно, в основном предназначена для переключения между другими программами, имеющими размер сектора.

Я бы искал решение, которое минимизирует набор текста в машинном коде ручной сборки. В идеале это должен быть язык программирования, но такой, который, в отличие от BASIC, может быть расширен во время выполнения. Если вы прочитали заголовок этого поста, то уже знаете, на чем я остановился. Оказалось, что в бутсекторе можно разместить примитивный FORTH. Код можно посмотреть врепозитории Miniforth на GitHub, но я приведу большую его часть здесь.

Весь FORTH занимает на данный момент 504 байта. Как и следовало ожидать, процесс разработки включал в себя постоянный поиск возможностей экономии байтов. Однако, когда я опубликовал, как мне казалось, достаточно плотно оптимизированный код, появилсяИлья Курдюков и нашел 24 байта, которые можно сэкономить! Я быстро реинвестировал это сэкономленное место в новые возможности.

Вводная экскурсия в FORTH

Если вы когда-либо писали что-либо на FORTH, вы можете смело пропустить этот раздел.

FORTH - это язык, основанный на стеках. Например, число вталкивает свое значение в стек, а слово + выталкивает два числа и их сумму. Обычная утилита отладки, но не включенная в Miniforth, - это слово .s, которое печатает содержимое стека.

1 2 3 + .s  <enter><2> 1 5 ok

Примечание: ok - готовность системы к приёму слов языка

Пользователь может определять свои собственные слова с помощью : и ;. Например:

 : double dup + ; <enter>ok  3 double  .  <enter>6 ок

Это определяет слово double, которое делает то же самое, что и dup +. dup, кстати, является одним из слов FORTH для работы со стеком. Оно дублирует верхний элемент в стеке:

42 dup .s <enter><2> 42 42 ok

Это практически весь язык. Есть некоторые стандартные средства для условий и циклов, но нам пока не нужно ими заниматься, так как они могут быть построены поверх Miniforth позже.

Чтобы описать влияние слова на состояние стека, мы используем следующую нотацию:

dup ( a -- a a ) swap ( a b -- b a )

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

Шитый код

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

DOUBLE:    call DUP    call PLUS    ret

Однако это связывает аппаратный стек x86 для стека возврата, заставляя нас вручную обрабатывать отдельный стек для фактического стека пользовательского уровня (известного как стек параметров). Поскольку доступ к стеку параметров встречается гораздо чаще, мы хотели бы использовать для этого инструкции push и pop, а вместо них можно применить механизм, аналогичный call. Во-первых, давайте просто сохраним список указателей на слова:

DOUBLE:    dw DUP    dw PLUS

Это реализуется следующим образом: каждое примитивное слово извлекает из памяти адрес следующего слова и переходит к нему. Указатель на эту последовательность указателей хранится в SI, так что инструкция lodsw позволяет легко обрабатывать этот список:

DUP:    pop ax    push ax    push ax    lodsw    jmp axPLUS:    pop ax    pop bx     add ax, bx    push ax    lodsw    jmp ax

Этот общий код можно абстрагировать в макрос, который традиционно называется NEXT:

%macro NEXT 0    lodsw    jmp ax%endmacro

Этот механизм, кстати, известен как потоковый код. Никакой связи с примитивом параллелизма.

Однако что произойдет, если одно скомпилированное слово вызовет другое? Здесь в дело вступает стек возвратов. Может показаться естественным использовать регистр BP для указателя стека. Однако в 16-битных x86 не существует режима адресации [bp]. Самый близкий к нему - [bp+imm8], что означает, что при обращении к памяти по адресу bp тратится байт, чтобы указать, что вам не нужно смещение. Вот почему я использую регистр di для стека возврата вместо этого. В целом, этот выбор экономит 4 байта.

В любом случае, вот как стек возврата используется для обработки компилированных слов, вызывающих друг друга. Проталкивание в стек возврата особенно удобно, поскольку это просто инструкция stosw.

DOUBLE:    call DOCOL    dw DUP    dw PLUS    dw EXITDOCOL:            ; сокращение от "do colon word".    xchg ax, si ; используется здесь как  mov ax, si ,              ;   но меняет местами.                ; ax - только один байт, а  mov  - два байта.    stosw    pop si ; захватить указатель, вытолкнутый  call .    NEXTEXIT:    dec di    dec di    mov si, [di]    NEXT

Это практически та же стратегия выполнения, что и в Miniforth, с одним простым, но существенным улучшением - значение на вершине стека хранится в регистре BX. Это позволяет пропустить push и pop во многих примитивах:

PLUS:    pop ax    add bx, ax    NEXTDROP:    pop bx    NEXTDUP:      push  bx       NEXT

Однако один случай все еще остается нерешенным. Что произойдет, если слово содержит число, например : DOUBLE 2 \ ;? С этим справляется LIT, который извлекает литерал, следующий за ним, из потока указателей:

DOUBLE:      call DOCOL      dw LIT, 2      dw MULT      dw EXITLIT:      push  bx       lodsw      xchg  bx ,  ax       NEXT

Словарь

FORTH нужен способ найти реализацию слов, которые набирает пользователь. Это роль словаря. Я использую структуру, подобную многим другим небольшим FORTH - односвязный список заголовков слов, непосредственно предваряющий код каждого слова. По традиции, глава списка хранится в переменной LATEST.

В старших битах поля длины имени также хранятся некоторые флаги:

F_IMMEDIATE equ 0x80F_HIDDEN    equ 0x40F_LENMASK   equ 0x1f

Если слово помечено как IMMEDIATE, оно будет выполнено немедленно, даже если в данный момент мы составляем определение. Например, это используется для реализации ;. Если слово помечено как HIDDEN, оно игнорируется при поиске по словарю. Помимо использования в качестве элементарного механизма инкапсуляции, это может быть использовано для реализации традиционной семантики FORTH, когда определение слова может ссылаться на предыдущее слово с тем же именем (а RECURSE используется, когда вам нужно определение, которое компилируется в данный момент). Однако, ближе к концу разработки я удалил код, который действительно делает это, из стандартной реализации : и ;.

Компрессия

Обычно не стоит использовать сжатие, когда и расжатие , и его полезная нагрузка должны уместиться всего в 512 байт. Однако в реализации FORTH есть одна вещь, которая повторяется очень часто, - это реализация NEXT.

ad      lodswffe0    jmp  ax 

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

Я выбрал схему сжатия, в которой каждый байт 0xff заменяется на NEXT, за которым следует поле ссылки, которое вычисляется на основе предыдущего появления байта 0xff. Эта стратегия сэкономила 19 байт, когда я ее внедрил.4

Сначала я использовал для этого байт 0x90, в конце концов, это опкод nop, который я точно не буду использовать. Однако этот байт все еще может встречаться в непосредственном байте инструкции. Сначала это не было проблемой, но когда код перемещался в памяти, различные адреса и смещения становились 0x90 достаточно часто, чтобы стать неприятностью. У 0xff, похоже, такой проблемы нет.

Чтобы создать ссылку, мы копируем значение LATEST на выход декомпрессора и обновляем LATEST, чтобы оно указывало на слово, которое мы только что написали. Это может быть сделано очень компактной последовательностью инструкций, но все равно занимает достаточно байт, чтобы стоило оформить это в виде подпрограммы. Она также используется в реализации :, которая создает словарные статьи во время выполнения.

; Создает ссылку словарного связного списка в DI.MakeLink:      mov  ax ,  di       xchg [LATEST],  ax    ; AX теперь указывает на старую запись,                   ; а LATEST и DI указывают на новую.      stosw      ret

В декомпрессоре использовался интересный прием, когда вместо короткого перехода вперед опкод размещается так, что непосредственный аргумент, который он требует, съедает инструкцию, через которую мы хотим перейти. То есть, вместо

     jmp short .after.write:      stosb.after:пишим3c      db 0x3c ; пропустить stosb ниже, сравнив его опкод с AL       .write:aa      stosb

Таким образом, если какой-то другой код переходит к .write, выполняется stosb, но этот кодовый путь просто выполняет cmp al, 0xaa. Сначала я не подумал об инструкции cmp al, и вместо нее использовал mov в отбрасываемый регистр. Это привело кэффектному отказуиз-за моей неспособности выбрать регистр, который можно безопасно перезаписать.

Илья Курдюков затем продемонстрировал , что тот же самый байткоунт может быть достигнут без подобной "магии". Аналогичная модификация позволила мне устранить и другое проявление этого трюка. Суть в том, что вместо того, чтобы пытаться пропустить stosb, мы выполняем его безоговорочно перед ветвлением кодовых путей, а затем, по сути, отменяем его с помощью dec di, если это необходимо:

SPECIAL_BYTE equ 0xff      mov  si , CompressedData      mov  di , CompressedBegin      mov  cx , COMPRESSED_SIZE.decompress:      lodsb      stosb      cmp  al , SPECIAL_BYTE      jnz short .not_special      dec  di       mov  ax , 0xffad ; lodsw / jmp ax      stosw      mov  al , 0xe0      stosb      call MakeLink.not_special:      loop .decompress

На самом деле генерация сжатого потока более сложна. Поскольку я хочу, чтобы переходы между сжатой и несжатой частями работали, ассемблеру нужно верить, что он пишет код в том месте, где он будет выполняться на самом деле. Сначала я попытался сделать это, корректируя org после каждого SPECIAL_BYTE, но, к сожалению, yasm это не понравилось.

boot.s:137: error: program origin redefined

Очевидно, что необходим отдельный этап постобработки. Я написал макрос для подгонки байтов, которые будет вставлять декомпрессор:

%macro compression_sentinel 0      db SPECIAL_BYTE      dd 0xdeadbeef%endmacro

Это имеет дополнительное преимущество, предoставляя простой автоматизированный способ проверки того, что никакие SPECIAL_BYTE не проскочили случайно.

Мне все еще нужно было выделить место для сжатых данных. Я выбрал следующую схему:

\1. Несжатый код начинается с 7C00 - инициализация, декомпрессия и внешний интерпретатор.

\2. Сжатые данные немедленно следуют за ним, заполняя бутсектор вплоть до момента перед 7E00.

\3. Сразу после этого выделяется буфер декомпрессии, в который yasm выводит содержимое цели.

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

%assign savings 0%macro compression_sentinel 0%assign savings savings+4      db SPECIAL_BYTE      dd 0xdeadbeef%endmacro

Затем я просто вычитаю это значение из размера несжатого сегмента:

CompressedData:      times COMPRESSED_SIZE db 0xccCompressedBegin:; ...CompressedEnd:COMPRESSED_SIZE equ CompressedEnd - CompressedBegin - savings

Постобработка выполняется простым скриптом Python:

SPECIAL_BYTE =  b'\xff'SENTINEL = SPECIAL_BYTE +  b '\xef\xbe\xad\xde'with open('raw.bin', 'rb') as f:      data = f.read()output_offset = data.index( b '\xcc' \  20)chunks = data[output_offset:].lstrip( b '\xcc').split(SENTINEL)assert SPECIAL_BYTE not in chunks[0]compressed =  bytearray (chunks[0])for chunk in chunks[1:]:      assert SPECIAL_BYTE not in chunk      compressed.extend(SPECIAL_BYTE)      compressed.extend(chunk)\# Убедитесь, что для сжатых данных выделено именно то место, которое нужно.\# для сжатых данных.assert  b '\xcc' \  len(compressed) in dataassert  b '\xcc' \  (len(compressed) + 1) not in dataoutput = data[:output_offset] + compressedprint(len(output), 'bytes used')output +=  b '\x00' \  (510 - len(output))output +=  b '\x55\xaa'with open('boot.bin', 'wb') as f:      f.write(output)

Этот же сценарий также генерирует расширенный образ диска, который содержит некоторый код для тестирования в блоке 1:

output +=  b '\x00' \  512output += open('test.fth', 'rb').read().replace( b '\n',  b ' ')output +=  b ' ' \  (2048 - len(output))with open('test.img', 'wb') as f:      f.write(output)

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

; defcode PLUS, "+"; defcode SEMI, ";", F_IMMEDIATE%macro defcode 2-3 0      compression_sentinel%strlen namelength %2      db %3 | namelength, %2%1:%endmacro

Затем это используется для определения примитивов. Код, по сути, переходит в defcode:

defcode PLUS, "+"      pop  ax       add  bx ,  ax defcode MINUS, "-"      pop  ax       sub  ax ,  bx       xchg  bx ,  ax defcode PEEK, "@"      ; ...

Однако DOCOL, EXIT и LIT также используют механизм сжатия для своих NEXT. Поскольку поле ссылки все еще записывается, это создает фиктивные словарные статьи. К счастью, первый опкод EXIT и LIT имеет установленный бит F_HIDDEN, так что это не проблема:

CompressedBegin:

DOCOL:      xchg  ax ,  si       stosw      pop  si  ; grab the pointer pushed by  call       compression_sentinelLIT:      push  bx       lodsw      xchg  bx ,  ax       compression_sentinelEXIT:      dec  di       dec  di       mov  si , [ di ]defcode PLUS, "+"      ; ...

Переменные?

Инструкции немедленной загрузки имеют тенденцию иметь более короткие кодировки, чем загрузки из памяти:

be3412    mov  si , 0x12348b363412  mov  si , [0x1234]

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

      org 0x7c00      jmp 0:startstack:      dw HERE      dw BASE      dw STATE      dw LATESTstart:      ; ...      mov  sp , stack

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

Код инициализации

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

      jmp 0:start      ; ...start:      push  cs       push  cs       push  cs       pop  ds       pop  es       pop  ss       mov  sp , stack      cld

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

31c0    xor  ax ,  ax   ; through AX - 8 bytes8ed8    mov  ds ,  ax 8ec0    mov  es ,  ax 8ed0    mov  ss ,  ax 0e      push  cs      ; through the stack - 6 bytes0e      push  cs 0e      push  cs 1f      pop  ds 07      pop  es 17      pop  ss 

Во-вторых, можно подумать, что во время перенаправления стека возникает небольшое окно состояния гонки и если прерывание произошло между pop ss и mov sp, то может возникнуть хаос, если предыдущее значение SP окажется в неудачном месте памяти. Конечно, я мог бы просто скрестить пальцы и надеяться, что этого не произойдет, если бы 2 байта, необходимые для обертывания этого в пару cli/sti, были слишком большими. Однако оказалось, что этот компромисс не нужен благодаря одному неясному уголку архитектуры x86. Процитируем том 2B Руководства разработчика программного обеспечения x86:

Загрузка регистра SS инструкцией POP5 подавляет или блокирует некоторые отладочные исключения и блокирует прерывания на границе следующей инструкции. (Запрет заканчивается после доставки исключения или выполнения следующей инструкции). Такое поведение позволяет загрузить указатель стека в регистр ESP со следующей инструкцией (POP ESP)6 ,прежде чем может быть доставлено событие.

После установки сегментов, стека и флага направления запускается декомпрессор. Очень важно, что он не использует регистр DL, который содержит номер диска BIOS, с которого мы загрузились. Затем он пихается в реализацию load (которая находится в сжатом сегменте) и заталкивается в стек для последующего использования пользовательским кодом:

      mov [DRIVE_NUMBER],  dl       push  dx  ; for FORTH code

Внешний интерпретатор

На этом этапе мы достигаем внешнего интерпретатора - части системы FORTH, которая обрабатывает пользовательский ввод. Название "внешний интерпретатор" отличает его от внутреннего интерпретатора, который является компонентом, координирующим выполнение в пределах определенного слова, и состоит из NEXT, DOCOL, EXIT и LIT.

Обычно FORTH представляет строительные блоки своего внешнего интерпретатора в виде слов в словаре, таких как

  • REFILL (считывание строки ввода из текущего выполняющегося источника),

    • WORD (разбор слова из входного потока),

    • FIND (искать слово в словаре),

    • NUMBER (преобразование строки в число).

В Miniforth этой практике вообще не уделяется никакого внимания. Заголовки словарей стоят байты, как и общение только через стек. Фактически, WORD и >NUMBER объединяются в одну процедуру, которая выполняет работу обеих. Таким образом, цикл может быть общим, что экономит байты.

Эта монолитная архитектура также позволяет нам решить, что BX и DI не резервируются для вершины стека и указателя стека возврата, соответственно, во время выполнения внешнего интерпретатора. Это значительно помогает справиться с регистровым голоданием в этих сравнительно сложных частях системы. Эти регистры устанавливаются непосредственно перед переходом к слову и сохраняются после возврата.

Ввод с клавиатуры

После завершения инициализации код переходит к ReadLine, процедуре для чтения строки ввода с клавиатуры. Мы также вернемся сюда позже, когда текущая строка ввода будет исчерпана. Буфер ввода находится по адресу 0x500, сразу послеBDA. Хотя идиоматический формат строк для FORTH использует отдельное поле длины, этот буфер NULL-терминирован, так как это легче обрабатывать при разборе. Указатель на неразобранный фрагмент ввода хранится в InputPtr, которая является единственной переменной, не использующей технику самомодификации, так как ее не нужно явно инициализировать и она естественным образом записывается до чтения.

InputBuf equ 0x500 InputPtr equ 0xa02 ; dw

ReadLine:      mov  di , InputBuf      mov [InputPtr],  di .loop:      mov  ah , 0      int 0x16      cmp  al , 0x0d      je short .enter      stosb      cmp  al , 0x08      jne short .write      dec  di       cmp  di , InputBuf ; underflow check      je short .loop      dec  di .write:      call PutChar      jmp short .loop.enter:      call PutChar      mov  al , 0x0a      int 0x10      xchg  ax ,  bx  ; write the null terminator by using the BX = 0 from PutChar      stosbInterpreterLoop:      call ParseWord ; returns length in CX. Zero implies no more input.      jcxz short ReadLine

Прерывание BIOS для получения символа с клавиатуры не печатает клавишу и мы должны сделать это сами. Это делается с помощью функции "TELETYPE OUTPUT", которая уже обрабатывает специальные символы, такие как backspace или newline.

PutChar:      xor  bx ,  bx       mov  ah , 0x0e      int 0x10      ret

У этой функции есть свои недостатки. Например, необходимы "грязные" символы окончания строки CRLF (CR для перемещения курсора в начало строки и LF для перемещения на следующую строку). Кроме того, символ backspace только перемещает курсор на один символ назад, но не стирает его. Чтобы получить поведение, которого мы ожидаем, необходимо напечатать \b \b (справедливости ради, это также происходит на современных терминалах). Я решил это пропустить.

Наконец, в "Списке прерываний" Ральфа Браунаупоминается, что некоторые BIOS сбрасывают BP, когда печатаемый символ вызывает прокрутку экрана. Это нас не касается, так как мы вообще не используем этот регистр.

Парсинг

После того как мы прочитали строку, нам нужно разобрать ее на слова. Это делается по требованию и каждое слово выполняется (или компилируется, в зависимости от состояния), как только оно разобрано. Помимо цикла интерпретатора, ParseWord также вызывается реализацией : (для получения имени определяемого слова).

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

Для начала мы пропускаем все пробельные символы во входном буфере:

; returns; DX = pointer to string; CX = string length; BX = numeric value; clobbers SI and BPParseWord:      mov  si , [InputPtr]      ; repe scasb вероятно, сохранит некоторые байты здесь, если бы реестры были разработаны - SCASB  ; использует DI вместо SI :( - scasb      ; uses DI instead of SI :(.skiploop:      mov  dx ,  si  ; Если мы выйдем на петлю в этой итерации, DX укажет первую букву слова      lodsb      cmp  al , " "      je short .skiploop

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

В этот момент регистр AL загружен первым символом слова. Таким образом, наш следующий цикл должен будет выполнить lodsb в своем конце.

      xor  cx ,  cx       xor  bx ,  bx .takeloop:      and  al , ~0x20      jz short Return ; jump to a borrowed  ret  from some other routine

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

Если мы не обнаружили конец слова, мы увеличиваем счетчик длины и преобразуем цифру в ее числовое значение:

      inc  cx       sub  al , "0" &~0x20      cmp  al , 9      jbe .digit_ok      sub  al , "A" - ("0" &~0x20) - 10.digit_ok      cbw

cbw - это малоизвестная инструкция, которая преобразует знаковое число из байта в слово, но для нас это просто более короткое mov ah, 0. Возможно, аналогичным образом мы используем знаковое умножение imul, потому что у него больше возможностей для использования регистров, чем у беззнакового mul. Используемая здесь форма позволяет умножать на непосредственное значение и не перезаписывать в DX верхнюю половину произведения.7

Эта конкретная инструкция должна быть закодирована вручную, чтобы ширина литерала составляла 2 байта.8

      ; imul bx, bx, <BASE> но yasm настаивает на кодировании непосредственного в один байт...       db 0x69, 0xdbBASE equ $      dw 16      add  bx ,  ax  ; add the new digit

Наконец, мы настраиваемся на следующую итерацию цикла. Мы используем аналогичный трюк, как и раньше, где результат указателя обновляется в каждой итерации, чтобы избежать отдельного декремента в конце, поскольку нам нужно убедиться, что входной указатель не указывает после терминатора.

      mov [InputPtr],  si       lodsb      jmp short .takeloop

Поиск по словарю

После того как слово разобрано, мы пытаемся найти его в словаре. Для каждой записи нам нужно сравнить длину имени, а если она совпадает, то и само имя. Включая F_HIDDEN в маску, мы автоматически обрабатываем и скрытые записи. То, как мы сравниваем длину, может выглядеть немного странно. Цель состоит в том, чтобы сохранить бит F_IMMEDIATE в AL, чтобы нам не пришлось держать рядом указатель на заголовок этого слова. Это одна из умных оптимизаций Ильи Курдюкова.

    InterpreterLoop:          call ParseWord          jcxz short ReadLine        ; Пытаемся найти слово в словаре..    ; SI = указатель словаря    ; DX = указатель строки    ; CX = длина строки    ; Следим за сохранением BX, в котором хранится числовое значение    LATEST equ $+1          mov  si , 0    .find:          lodsw          push  ax  ; сохранить указатель на следующую запись          lodsb          xor  al ,  cl  ; если длина совпадает, то AL содержит только флаги          test  al , F_HIDDEN | F_LENMASK          jnz short .next              mov  di ,  dx           push  cx           repe cmpsb          pop  cx           je short .found    .next:          pop  si           or  si ,  si           jnz short .find              ; Если мы дойдем до этой точки, то это будет число.          ; ....found:      pop  bx  ; отбрасываем указатель на следующую запись      ; Когда мы дойдем до этого места, SI указывает на код слова, а AL содержит ; флаг F_IMMEDIATE

Эта часть показывает еще одно преимущество отсутствия разбиения интерпретатора на многократно используемые фрагменты: мы можем легко выйти на два разных кодовых пути, основываясь на результате поиска.

Должны ли мы его выполнить?

Система может находиться в двух возможных состояниях:

  • интерпретация - все слова должны быть выполнены

  • компиляция - непосредственные слова должны быть выполнены

Другими словами, слово должно быть выполнено, если оно немедленное, или мы его интерпретируем. Мы храним этот флаг в поле immediate инструкции or, так как при компиляции он будет установлен в 0:

      ; Когда мы сюда попадаем, SI указывает на код слова, а AL содержит ; флаг F_IMMEDIATE STATESTATE equ $+1      or  al , 1      xchg  ax ,  si  ;  оба кодовых пути должны иметь указатель в AX      jz short .compile      ; Выполняем слово      ; ...

Наиболее важными словами, которые должны изменить состояние, являются : и ;, но они просто переходят в [ и ] слова, которые позволяют временно вернуться в интерпретируемый режим при компиляции слова. Типичный случай использования - вычисление значения постоянного выражения:

 : third-foo [ foos 3 cells + ] literal @ ;

Поскольку два значения STATE отличаются только на 1, мы можем переключаться между ними с помощью inc и dec. Это имеет тот недостаток, что они больше не являются идемпотентными, но это не должно иметь значения для хорошо написанного кода:

defcode LBRACK, "[", F_IMMEDIATE      inc byte[STATE]defcode RBRACK, "]"      dec byte[STATE]

Выполнение слова

Если мы решили выполнить слово, мы извлекаем BX и DI, и настраиваем SI так, чтобы NEXT перешел обратно к .executed:

; Выполнение слова RetSP RetSP equ $+1      mov  di , RS0      pop  bx       mov  si , .return      jmp  ax .return:      dw .executed.executed:      mov [RetSP],  di       push  bx       jmp short InterpreterLoop

Обработка чисел

Для чисел нет флага F_IMMEDIATE, поэтому для принятия решения нам нужно просто проверить состояние. Это простое сравнение, но если мы будем достаточно умны, здесь можно сэкономить байт. Давайте снова посмотрим на код, который выполняет поиск в словаре. Какое значение будет иметь AH, когда мы дойдем до регистра чисел?

.find:      lodsw      push  ax  ;  сохранить указатель на следующую запись      lodsb      xor  al ,  cl  ; если длина совпадает, то AL содержит только флаги      test  al , F_HIDDEN | F_LENMASK      jnz short .next      mov  di ,  dx       push  cx       repe cmpsb      pop  cx       je short .found.next:      pop  si       or  si ,  si       jnz short .find      ; AH = ?

Видите ли вы это? В этот момент AH равен нулю, так как содержит старшую половину указателя на следующее слово, которое, как мы знаем, является NULL, поскольку мы только что добрались до конца списка. Это позволяет нам проверить значение STATE, не загружая его в регистр или какие-либо непосредственные байты:

      ; Это число. Вставьте его значение - мы выкинем его позже, если окажется, что нужно скомпилировать ; его вместо этого.       push  bx       cmp byte[STATE],  ah       jnz short InterpreterLoop      ; Иначе скомпилируйте литерал. ; ...

Компиляция вещей

Указатель точки процесса компиляции называется HERE. Он начинается сразу после распакованных данных. Функция, которая выписывает слово в эту область, называется COMMA, так как слово FORTH, которое это делает, - ,.

COMMA:HERE equ $+1      mov [CompressedEnd],  ax       add word[HERE], 2      ret

Он используется в прямом смысле для составления как чисел, так и слов. Мы можем разделить хвост, хотя составлении слова он будет перескакивать на середину составления числа:

      ; Иначе, компилируем литерал.       mov  ax , LIT      call COMMA      pop  ax .compile:      call COMMA      jmp short InterpreterLoop

Последним кусочком головоломки являются : и ;. Давайте сначала рассмотрим :. Поскольку ParseWord использует BX и SI, нам нужно сохранить эти регистры. Более того, поскольку мы пишем множество частей заголовка словаря, мы загрузим HERE в DI, чтобы упростить работу. Это большое количество регистров, которые нам нужно переместить. Однако на самом деле нам не нужно изменять ни один регистр, поэтому мы можем просто сохранить все регистры с помощью pusha.

defcode COLON, ":"      pusha      mov  di , [HERE]      call MakeLink    ; link field      call ParseWord      mov  ax ,  cx       stosb            ; length field      mov  si ,  dx       rep movsb        ; name field      mov  al , 0xe8     ; call      stosb   ; поле длины mov si, dx rep movsb ; поле имени mov al, 0xe8 ; вызов stosb ; Смещение определяется как (цель вызова) - (ip после инструкции вызова) ; Получается DOCOL - (di + 2) = DOCOL - 2 - di      mov  ax , DOCOL - 2      sub  ax ,  di       stosw      mov [HERE],  di       popa      jmp short RBRACK ; enter compilation mode   ; войти в режим компиляции

; гораздо короче. Нам просто нужно скомпилировать EXIT и вернуться в режим интерпретации:

defcode SEMI, ";", F_IMMEDIATE      mov  ax , EXIT      call COMMA      jmp short LBRACK

То, как эти слова переходят к другому слову в конце, весьма удобно. Помните, как NEXT записываются как часть кода следующего слова? Одно из слов должно быть последним в памяти, и тогда после него не будет никакого "следующего слова". : и ; - идеальные кандидаты для этого, поскольку им вообще не нужен NEXT.

Загрузка кода с диска

Поскольку мы не хотим вводить дисковые подпрограммы при каждой загрузке, нам нужно предусмотреть способ запуска исходного кода, загруженного с диска. Файловая система была бы отдельным зверем, но в традициях FORTH есть минималистичное решение: диск просто делится на блоки по 1 КБ, в которых хранится исходный код, отформатированный как 16 строк по 64 символа. Затем load ( blknum -- ) выполнит блок с указанным номером.

Мы размещаем блок 0 в LBA 0 и 1, блок 1 в LBA 2 и 3 и так далее. Это означает, что блок 0 частично занят MBR, а LBA 1 используется впустую, но меня это не особенно беспокоит.

Поскольку оригинальная служба BIOS по адресу int 0x13 / ah = 0x02 требует адресации CHS, я решил использовать вариант расширения EDD (ah = 0x42). Это означает, что дискеты не поддерживаются, но я все равно не планировал их использовать.

Чтобы использовать интерфейс EDD, нам нужно создать пакет адреса диска, который выглядит следующим образом:

      db 0x10 ; размер пакета      db 0    ; зарезервировано      dw sector_count      dw buffer_offset, buffer_segment      dq LBA

Мы используем гибридную стратегию для создания этого пакета. Первая часть сохраняется как данные в бутсекторе, а остальное записывается во время выполнения, даже если оно не меняется. Шаблон" должен находиться в таком месте, чтобы мы могли писать после него, поэтому идеальное место - прямо перед сжатыми данными:

>  >  > DiskPacket:       db 0x10, 0 .count:       dw 2 .buffer:

; остальное заполняется во время выполнения, перезаписывая сжатые данные,

; которые больше не нужны

CompressedData: times COMPRESSED_SIZE db 0xcc

Первые четыре байта пакета достаточно случайны, чтобы их можно было жестко закодировать. Однако, когда дело доходит до адреса буфера, мы можем сделать лучше. В любом случае нам нужно будет записать этот адрес в InputPtr. Самый прямой способ сделать это занимает шесть байт:

c706020a0006    mov word[InputPtr], BlockBuf

Однако мы можем получить эту переменную в AX без дополнительных затрат:

b80006          mov  ax , BlockBufa3020a          mov [InputPtr],  ax 

Таким образом, мы можем записать эти два байта дискового пакета с помощью всего 1 байта кода:

defcode LOAD, "load"      pusha      mov  di , DiskPacket.buffer      mov  ax , BlockBuf      mov word[InputPtr],  ax       stosw

Далее нам нужно записать сегмент (0000) и LBA (который заканчивается шестью байтами 00). Мне нравится думать о соответствующих инструкциях следующим образом:

31c0    xor  ax ,  ax     ; LBA zeroesab      stosw         ; segmentd1e3    shl  bx , 1     ; LBA data93      xchg  ax ,  bx    ; LBA dataab      stosw         ; LBA data93      xchg  ax ,  bx    ; segmentab      stosw         ; LBA zeroesab      stosw         ; LBA zeroesab      stosw         ; LBA zeroes

То есть, мы записываем шесть нулей LBA за 5 байт кода. Запись сегмента потребовала только перемещения xor ax, ax ранее, и дополнительных stosw и xchg ax, bx. Таким образом, он занимает нейтральные 2 байта (но нам нужно выписать его в коде, чтобы указатель был правильным для остальной части пакета). Наконец, конечно, у нас есть фактические данные LBA, которые меняются.

Пока AX равен нулю, воспользуемся случаем и поставим нулевой терминатор после буфера:

      mov [BlockBuf.end],  al 

Теперь мы готовы вызвать функцию BIOS. Если она выдаст ошибку, мы просто зациклимся, так как восстановление сложное и самое неприятное осложнение заключается в том, что счетчик секторов в пакете перезаписывается количеством успешно прочитанных секторов, что ломает наш шаблон.

DRIVE_NUMBER equ $+1      mov  dl , 0      mov  ah , 0x42      mov  si , DiskPacket      int 0x13      jc short $      popa      pop  bx 

Числа для печати

u. печатает беззнаковое число, за которым следует пробел. Поскольку при делении числа на разряды с помощью деления первой получается наименее значащая цифра, мы помещаем цифры в стек, а затем выводим и печатаем их в отдельном цикле. Пробел печатается путем выталкивания фальшивой "цифры", которая будет преобразована в пробел. Это также позволяет нам определить, когда мы вытолкнули все цифры и цикл печати останавливается, когда он только что напечатал пробел.

defcode UDOT, "u."    xchg  ax ,  bx     push " " - "0".split:      xor  dx ,  dx       div word[BASE]      push  dx       or  ax ,  ax       jnz .split.print:      pop  ax       add  al , "0"      cmp  al , "9"      jbe .got_digit      add  al , "A" - "0" - 10.got_digit:      call PutChar      cmp  al , " "      jne short .print      pop  bx 

s: Поместить в строку

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

Реализация представляет собой простой цикл, но установка вокруг него заслуживает внимания и мы хотим загрузить входной указатель в SI, но нам также нужно сохранить SI, чтобы мы могли правильно вернуться. Используя xchg, мы можем сохранить его в [InputPtr] на время копирования без дополнительных затрат:

;; Копирует остаток строки в buf.    defcode LINE, "s:" ; ( buf -- buf+len )      xchg  si , [InputPtr].copy:      lodsb      mov [ bx ],  al       inc  bx       or  al ,  al       jnz short .copy.done:      dec  bx       dec  si       xchg  si , [InputPtr]

Указатель назначения хранится в BX. Хотя запись в DI займет всего один стосб, получение указателя в DI и обратно перевешивает это преимущество. В конце мы оставляем в стеке указатель на нулевой терминатор. Таким образом, вы можете продолжить строку, просто используя s: в следующей строке. Поскольку мы не пропускаем пробельные символы, это даже гарантирует правильную расстановку символов.

Другие примитивы

Выбор примитивов для включения в Miniforth - это, пожалуй, самый большой компромисс, который необходимо сделать. Я полностью ожидаю, что некоторые более примитивные слова нужно будет определять во время выполнения, вставляя их в опкоды. В конце концов, здесь нет ни одного слова с ветвлением. Однако я уверен, что эти опкоды можно будет генерировать простым ассемблером FORTH, а не просто жестко закодировать.

Такая базовая арифметика, как +, незаменима. Я определяю и +, и -, хотя, если бы я хотел вписать что-то более важное, я мог бы оставить только - и позже определить : negate 0 swap - ; и : + negate - ;.

Как и в любом низкоуровневом языке программирования, нам нужен способ подглядывать и пихать значения в память. Реализация ! особенно хороша, поскольку мы можем просто заглянуть прямо в [bx]:

defcode PEEK, "@" ; ( addr -- val )    mov bx, [bx]defcode POKE, "!" ; ( val addr -- )    pop word [bx]    pop bx

Существуют также варианты, которые читают и записывают только один байт:

defcode CPEEK, "c@" ; ( addr -- ch )      movzx  bx , byte[ bx ]defcode CPOKE, "c!" ; ( ch addr -- )      pop  ax       mov [bx],  al       pop  bx 

Нам определенно нужны слова для работы со стеком. dup и drop имеют очень простые реализации, а swap определенно слишком полезен, чтобы его пропустить.

defcode DUP, "dup" ; ( a -- a a )      push  bx defcode DROP, "drop" ; ( a -- )      pop  bx defcode SWAP, "swap" ; ( a b -- b a )      pop  ax       push  bx       xchg  ax ,  bx 

Я решил также включить >r и r>, которые позволяют использовать стек возвратов в качестве второго стека для значений (но, очевидно, только в пределах одного слова). Это довольно мощный инструмент. Фактически, в сочетании с dup, drop и swap они позволяют реализовать любое слово для манипуляции стеком, которое вы только можете себе представить.9

defcode TO_R, ">r"      xchg  ax ,  bx       stosw      pop  bx defcode FROM_R, "r>"      dec  di       dec  di       push  bx       mov  bx , [ di ]

Наконец, emit печатает символ. Это достаточно далеко от критического пути bootstrap, чтобы я мог спокойно удалить его в случае необходимости.

defcode EMIT, "emit"      xchg  bx ,  ax       call PutChar      pop  bx 

Заключение

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

Я нашел старый ПК, который могу использовать для своих экспериментов. На нем Miniforth загружается просто отлично:

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

1

У теоретика графов было бы много сильных слов, чтобы описать это, а не только цикл.

2

И даже если бы это было не так, есть много, многопримеров кода x86, написанного с использованием печатаемого подмножества ASCII. Я даже сам однажды сделал этонесколько лет назад.

3 Если вы, дорогой читатель, считаете это неудовлетворительным, я хотел бы пригласить вас в собственное путешествие по бутстрапингу. Это действительно очень весело!

4

Точную экономию на данный момент трудно подсчитать, поскольку некоторые слова не используют добавляемый NEXT.

5

Хотя в этом отрывке справки говорится только о pop ss, аналогичное утверждение содержится в документации по mov.

6

Похоже, это одна из многих ошибок в SDM и использование pop esp для этого не работает. Раздел 6.8.3 ("Маскировка исключений и прерываний при переключении стека") в томе 3A разъясняет, что все одноинструкционные способы загрузки SP работают для этого. Я бы процитировал этот раздел, если бы не тот факт, что, хотя в нем перечислены многие другие типы событий, которые подавляются, в нем не упоминаются реальные прерывания как один из них. Однако в этом разделе упоминаются некоторые интересные крайние случаи. Например, если вы похожи на меня, вам может быть интересно, что произойдет, если много инструкций подряд записываются в SS. Ответ заключается в том, что только первая из них гарантированно подавляет прерывания.

7

Полагаю, имеет смысл, что эта опция доступна только для imul, так как по модульной арифметике единственная разница между знаковым и беззнаковым умножением заключается в верхней половине произведения, которую мы здесь отбрасываем. Иммедиаты могли бы быть полезны и для mul, хотя...

8

Вы можете спросить, почему бы просто не объявить, что BASE - это переменная размером в байт? Ответ заключается в том, что u., которое является словом, печатающим число, использует div word[BASE], так что результат все еще 16-битный.

9

Сюда не входят слова типа PICK, для них нужны циклы. Однако все, что можно определить как ( <список имен> -- <список имен>), является честной игрой. Доказательство этого факта мы оставляем на усмотрение читателя. 10

10

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

github.com/NieDzejkob

Подробнее..

Управляем звуком ПК от активности пользователя с помощью Python

17.06.2021 14:15:17 | Автор: admin

Настройка программного обеспечения

Без промедления начнём. Нам нужно установить следующее ПО:

  • Windows 10

  • Anaconda 3 (Python 3.8)

  • Visual Studio 2019 (Community) - объясню позже, зачем она понадобится.

Открываем Anaconda Prompt (Anaconda3) и устанавливаем следующие пакеты:

pip install opencv-pythonpip install dlibpip install face_recognition

И уже на этом моменте начнутся проблемы с dlib.

Решаем проблему с dlib

Я перепробовал все решения, что нашёл в интернете и они оказались неактуальными - раз, два, три, официальное руководство и видео есть. Поэтому будем собирать пакет вручную.

Итак, первая же ошибка говорит о том, что у нас не установлен cmake.

ERROR: CMake must be installed to build dlib
ERROR: CMake must be installed to build dlibERROR: CMake must be installed to build dlib

Не закрывая консоль, вводим следующую команду:

pip install cmake
Проблем при установке быть не должно

Пробуем установить пакет той же командой (pip install dlib), но на этот раз получаем новую ошибку:

Отсутствуют элементы Visual Studio

Ошибка явно указывает, что у меня, скорее всего, стоит студия с элементами только для C# - и она оказывается права. Открываем Visual Studio Installer, выбираем "Изменить", в вкладке "Рабочие нагрузки" в разделе "Классические и мобильные приложения" выбираем пункт "Разработка классических приложений на С++":

Пошагово
"Изменить""Изменить"Разработка классических приложений на С++Разработка классических приложений на С++Ждем окончания установкиЖдем окончания установки

Почему важно оставить все галочки, которые предлагает Visual Studio. У меня с интернетом плоховато, поэтому я решил не скачивать пакет SDK для Windows, на что получил следующую ошибку:

Не нашли компилятор

Я начал искать решение этой ошибки, пробовать менять тип компилятора (cmake -G " Visual Studio 16 2019"), но только стоило установить SDK, как все проблемы ушли.

Я пробовал данный метод на двух ПК и отмечу ещё пару подводных камней. Самое главное - Visual Studio должна быть 2019 года. У меня под рукой был офлайн установщик только 2017 - я мигом его поставил, делаю команду на установку пакета и получаю ошибку, что нужна свежая Microsoft Visual C++ версии 14.0. Вторая проблема была связана с тем, что даже установленная студия не могла скомпилировать проект. Помогла дополнительная установка Visual C++ 2015 Build Tools и Microsoft Build Tools 2015.

Открываем вновь Anaconda Prompt, используем ту же самую команду и ждём, когда соберется проект (около 5 минут):

Сборка
Всё прошло успешноВсё прошло успешно

Управляем громкостью

Вариантов оказалось несколько (ссылка), но чем проще - тем лучше. На русском язычном StackOverflow предложили использовать простую библиотеку от Paradoxis - ей и воспользуемся. Чтобы установить её, нам нужно скачать архив, пройти по пути C:\ProgramData\Anaconda3\Lib и перенести файлы keyboard.py, sound.py из архива. Проблем с использованием не возникало, поэтому идём дальше

Собираем события мыши

Самым популярным модулем для автоматизации управления мышью/клавиатурой оказался pynput. Устанавливаем так же через (pip install dlib). У модуля в целом неплохое описание - https://pynput.readthedocs.io/en/latest/mouse.html . Но у меня возникли сложности при получении событий. Я написал простую функцию:

from pynput import mousedef func_mouse():        with mouse.Events() as events:            for event in events:                if event == mouse.Events.Scroll or mouse.Events.Click:                    #print('Переместил мышку/нажал кнопку/скролл колесиком: {}\n'.format(event))                    print('Делаю половину громкости: ', time.ctime())                    Sound.volume_set(volum_half)                    break

Самое интересное, что если раскомментировать самую первую строчку и посмотреть на событие, которое привело выходу из цикла, то там можно увидеть Move. Если вы заметили, в условии if про него не слово. Без разницы, делал я только скролл колесиком или только нажатие любой клавиши мыши - все равно просто движение мыши приводит к выходу из цикла. В целом, мне нужно все действия (Scroll, Click, Move), но такое поведение я объяснить не могу. Возможно я где-то ошибаюсь, поэтому можете поправить.

А что в итоге?

Adam Geitgey, автор библиотеки face recognition, в своём репозитории имеет очень хороший набор примеров, которые многие используют при написании статей: https://github.com/ageitgey/face_recognition/tree/master/examples

Воспользуемся одним из них и получим следующий код, который можно скачать по ссылке: Activity.ipynb, Activity.py

Код
# Подключаем нужные библиотекиimport cv2import face_recognition # Получаем данные с устройства (веб камера у меня всего одна, поэтому в аргументах 0)video_capture = cv2.VideoCapture(0) # Инициализируем переменныеface_locations = []from sound import SoundSound.volume_up() # увеличим громкость на 2 единицыcurrent = Sound.current_volume() # текущая громкость, если кому-то нужноvolum_half=50  # 50% громкостьvolum_full=100 # 100% громкостьSound.volume_max() # выставляем сразу по максимуму# Работа со временем# Подключаем модуль для работы со временемimport time# Подключаем потокиfrom threading import Threadimport threading# Функция для работы с активностью мышиfrom pynput import mousedef func_mouse():        with mouse.Events() as events:            for event in events:                if event == mouse.Events.Scroll or mouse.Events.Click:                    #print('Переместил мышку/нажал кнопку/скролл колесиком: {}\n'.format(event))                    print('Делаю половину громкости: ', time.ctime())                    Sound.volume_set(volum_half)                    break# Делаем отдельную функцию с напоминаниемdef not_find():    #print("Cкрипт на 15 секунд начинается ", time.ctime())    print('Делаю 100% громкости: ', time.ctime())    #Sound.volume_set(volum_full)    Sound.volume_max()        # Секунды на выполнение    #local_time = 15    # Ждём нужное количество секунд, цикл в это время ничего не делает    #time.sleep(local_time)        # Вызываю функцию поиска действий по мышке    func_mouse()    #print("Cкрипт на 15 сек прошел")# А тут уже основная часть кодаwhile True:    ret, frame = video_capture.read()        '''    # Resize frame of video to 1/2 size for faster face recognition processing    small_frame = cv2.resize(frame, (0, 0), fx=0.50, fy=0.50)    rgb_frame = small_frame[:, :, ::-1]    '''    rgb_frame = frame[:, :, ::-1]        face_locations = face_recognition.face_locations(rgb_frame)        number_of_face = len(face_locations)        '''    #print("Я нашел {} лицо(лица) в данном окне".format(number_of_face))    #print("Я нашел {} лицо(лица) в данном окне".format(len(face_locations)))    '''        if number_of_face < 1:        print("Я не нашел лицо/лица в данном окне, начинаю работу:", time.ctime())        '''        th = Thread(target=not_find, args=()) # Создаём новый поток        th.start() # И запускаем его        # Пока работает поток, выведем на экран через 10 секунд, что основной цикл в работе        '''        #time.sleep(5)        print("Поток мыши заработал в основном цикле: ", time.ctime())                #thread = threading.Timer(60, not_find)        #thread.start()                not_find()        '''        thread = threading.Timer(60, func_mouse)        thread.start()        print("Поток мыши заработал.\n")        # Пока работает поток, выведем на экран через 10 секунд, что основной цикл в работе        '''        #time.sleep(10)        print("Пока поток работает, основной цикл поиска лица в работе.\n")    else:        #все хорошо, за ПК кто-то есть        print("Я нашел лицо/лица в данном окне в", time.ctime())        Sound.volume_set(volum_half)            for top, right, bottom, left in face_locations:        cv2.rectangle(frame, (left, top), (right, bottom), (0, 0, 255), 2)        cv2.imshow('Video', frame)        if cv2.waitKey(1) & 0xFF == ord('q'):        breakvideo_capture.release()cv2.destroyAllWindows()

Суть кода предельно проста: бегаем в цикле, как только появилось хотя бы одно лицо (а точнее координаты), то звук делаем 50%. Если не нашёл никого поблизости, то запускаем цикл с мышкой.

Тестирование в бою

Ожидание и реальность

Если вы посмотрели видео, то поняли, что результат ещё далёк от реальной эксплуатации.

Признаю честно - до этого момента никогда не сталкивался с многопоточностью на Python, поэтому "с наскоку" тему взять не удалось и результат по видео понятен. Есть неплохая статья на Хабре, описывающая различные методы многопоточности, применяемые в языке. Пока у меня решения нету по этой теме нету - будет повод разобраться лучше и дописать код/статью с учетом этого.

Так же возникает закономерный вопрос - а если вместо живого человека поставить перед монитором картинку? Да, она распознает, что, скорее всего, не совсем верно. Мне попался очень хороший материал по поводу определения живого лица в реальном времени - https://www.machinelearningmastery.ru/real-time-face-liveness-detection-with-python-keras-and-opencv-c35dc70dafd3/ , но это уже немного другой уровень и думаю новичкам это будет посложнее. Но эксперименты с нейронными сетями я чуть позже повторю, чтобы тоже проверить верность и повторяемость данного руководства.

Немаловажным фактором на качество распознавания оказывает получаемое изображение с веб-камеры. Предложение использовать 1/4 изображения (сжатие его) приводит только к ухудшению - моё лицо алгоритм распознать так и не смог. Для повышения качества предлагают использовать MTCNN face detector (пример использования), либо что-нибудь посложнее из абзаца выше.

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

Заключение

На своём примере могу точно сказать - не все руководства одинаково полезны и простая задача может перерасти в сложную. Я понимаю, что большинство материалов является переводом/простым копированием документации. Но если ты пишешь руководство, то проверь хотя бы на тестовой системе, что написанные тобой действия точно повторимы. Пробуйте, экспериментируйте - это хороший повод изучить и узнать новое.

P.S. Предлагаю вам, читатели, обсудить в комментариях статью - ваши идеи, замечания, уточнения.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru