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

Mono

Мигель де Икаса от Midnight Commander до Mono в .NET 5

01.12.2020 12:23:05 | Автор: admin


Мигель де Икаса создал целый ряд громких проектов: GNOME, Mono, Xamarin, Midnight Commander Для одних айтишников это человек-легенда, сделавший поразительно много для опенсорса и .NET-экосистемы. Другие ничего не знают о нём, но постоянно пользуются плодами его трудов.


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


Как человек, создавший GNOME, мог оказаться в Microsoft? СЕО какого ключевого IT-проекта современности долго был его главным сподвижником? Почему Ричард Столлман назвал Мигеля предателем сообщества свободного ПО, а некоторые другие говорят, что он всю жизнь занимается клонированием?




Начало


Де Икаса родился в Мехико в 1972-м. Он получал высшее математическое образование, но так и не завершил обучение: поворотной точкой в его судьбе стал момент, когда он наткнулся на манифест GNU.


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


Повод вскоре возник. Ранее при использовании DOS он успел привыкнуть к Norton Commander, и в UNIX-среде ему страшно не хватало такого двухпанельного файлового менеджера. Он написал Ричарду Столлману, засучил рукава, и в 1994-м первым его заметным вкладом стал Midnight Commander вариация на тему Нортона.



Midnight Commander на современном 5K-экране


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


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


Следующей его заметной задачей после Midnight Commander стала работа над переносом Linux на SPARC. И благодаря этому опыту в 1997-м его позвали на собеседование в Microsoft (там тогда хотели принести Internet Explorer в том числе на SPARC). Казалось бы, для ценителей свободного софта эта компания на тот момент была обителью зла, как в принципе можно было задумываться о работе там? Но Мигель отправился на собеседование и утверждает, что уговаривал тогда сотрудников Microsoft освободить исходный код IE.


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




GNOME


После выхода Windows 95 стало очевидно, что Linux на десктопе очень уступает проприетарному конкуренту. В 1996-м появился проект KDE, призванный сделать так, чтобы в Linux всё наконец выглядело консистентно и приятно. Он стал набирать популярность казалось бы, можно только порадоваться. Но когда де Икаса обратил внимание Ричарда Столлмана на этот проект, тот заметил, что зависимость от Qt делала KDE несвободным. Попытка Мигеля связаться с авторами Qt (чтобы для этого случая было отдельное лицензирование) не увенчалась успехом. И возникла новая практическая задача для GNU: сделать среду с преимуществами KDE, но свободную. Получается, что и здесь во многом был перенос чужого опыта на новые рельсы.


В 1997-м де Икаса и другой мексиканский разработчик Федерико Мена с благословения Столлмана взялись за задачу. И в 1999-м была выпущена первая версия GNOME (поначалу название означало GNU Network Object Model Environment). Интересно, что важным достижением GNOME Мигель считает не только сам продукт, но и организационную структуру с советом директоров по его словам, впоследствии это повлияло на многие другие опенсорсные проекты, включая .NET Foundation.


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



В связи с GNOME Мигель писал ещё и программу Gnumeric. Здесь тоже был прямой аналог чужой работы: автор и сам сравнивает этот проект с Excel (с уточнением, что кое-что у него тогда получилось лучше, чем у Microsoft). Мигель вложил в Gnumeric немало сил, но позже прекратил его развивать (и за дело взялись другие люди). По его словам, это произошло не по собственной воле: когда шли переговоры с Sun Microsystems об открытии исходного кода StarOffice (так появился OpenOffice), Sun поставили это условием.


Открытый код это прекрасно, но надо было на чём-то и зарабатывать. Мигель объединил усилия с Нэтом Фридманом, которого к этому моменту уже давно знал по IRC-сети LinuxNet. Вместе они создали компанию, которая поначалу называлась International GNOME Support, позже переименовалась в Helix Code, а затем стала называться Ximian.


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


Но через пару лет Мигель представил новый проект, который был уже совсем не просто поддержкой GNOME...




Mono


На дворе самое начало нулевых. Microsoft стандартизовали свою новую платформу .NET, тем самым открыв дорогу независимым имплементациям, но компания всё ещё никак не ассоциируется со словом открытая. Что в такой ситуации могло заставить линуксоидов, зарабатывающих на GNOME, обратить внимание на .NET? Зачем притаскивать на свою платформу что-то настолько далёкое от неё?


Мигель объясняет это так. В Ximian ощущали, что неудобно писать всё на С/С++, и согласились с дихотомией Аустерхаута: если для основополагающих вещей хорошо подходят системные языки, то вот для высокоуровневого склеивания этих вещей лучше подойдут языки попроще и поудобнее. Но какой именно язык для этого взять?



Сам Аустерхаут для этого создал Tcl, но Столлман велел не использовать Tcl в рамках GNU Project (породив тем самым большой холивар). Альтернативы вроде Python и Scheme оказались недостаточно производительными (вспомним, что в те времена компьютеры были куда менее мощными, чем сейчас). И вот тут-то новая платформа от Microsoft обратила на себя внимание.


Казалось бы, линуксоидам должна была оказаться ближе Java, чем враждебный .NET: она ведь изначально про запуск на любых платформах (write once, run anywhere), а не только на майкрософтовских. Но интересно, что в этом её преимуществе и оказалась подстава. Java была доступна на Linux бесплатно, но не была свободной и для GNU-формата не подходила. Теоретически можно было создать свободную реализацию но это было не сделать скромными силами Ximian, это должен был быть большой опенсорсный проект с участием сообщества. А сообществу было непонятно, зачем тратить силы на освобождение Java, когда имеющаяся несвободная и так бесплатно работала. Вот с .NET история другая: его на платформе не было вообще, если мы не сделаем значит, никакого не будет.


И в 2001-м де Икаса представил на конференции O'Reilly новый проект Mono, призванный принести .NET на Linux. То есть, по сути, снова пошёл по излюбленному пути: увидел крутую штуку на одной платформе и понёс на другую.



Поддержка со стороны сообщества оказалась достаточной для развития проекта, но была совсем не единодушной. Ситуация, когда на Linux имплементируют технологию от Microsoft, закономерно вызывала опасения: в том же 2001-м была описана стратегия компании Embrace, extend and extinguish (поддержать, расширить и уничтожить). Что, если Microsoft пока позволяет Mono существовать, но когда линуксовая экосистема окажется плотно завязана на C#, патентными судами выбьет почву у всех из-под ног?


И когда в 2009-м де Икаса принял участие в опенсорсной инициативе Microsoft CodePlex Foundation, Ричард Столлман человек, текст которого в начале 90-х изменил весь курс жизни де Икасы назвал его предателем сообщества свободного ПО. Мигель ответил сдержанно: Я думаю, есть целый мир разных возможностей, и если Ричард хочет обсудить, как мы можем улучшить опенсорсный/свободный софт, он знает мою почту.


Как бы то ни было, Mono развивался, и его стала использовать маленькая компания (всего из нескольких человек), которая тогда называлась Over the Edge Entertainment. Она в 2005-м выпустила игровой движок может, вы когда-нибудь слышали, он называется Unity В 2007-м эта компания спросила: тут Apple выпустила телефон, сможете запустить Mono на нём? И в итоге благодаря айфону всего лишь за год выросла на порядок. Похоже, успеху C# в геймдеве мы в значительной степени обязаны де Икасе.


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


Зато из бурного успеха Unity на iPhone он извлёк идею для своего следующего шага...




Xamarin


Раз эта айфонная штука явно взлетает, и там есть спрос на C# для игр, почему бы и обычные iOS-приложения не писать на C#? Изначально у Mono появились мобильные направления MonoTouch и Mono for Android, но позже всё изменилось по организационным причинам.


Компания Ximian ещё в 2003-м была куплена другой компанией Novell, но тогда это принципиально не поменяло ситуацию де Икаса и Фридман продолжили работать в том же направлении. А вот в 2011-м сама Novell была поглощена компанией The Attachmate Group, новые владельцы решили не развивать часть направлений и устроили сокращения. В том числе эта судьба постигла команду Mono и самого де Икасу.


Конечно, безработица ему не грозила, такого специалиста наверняка хотели бы заполучить многие компании. Но он не пошёл ни в одну из них. Вместе с тем же самым Нэтом Фридманом, с которым он 12 годами ранее основал Ximian, теперь де Икаса создал Xamarin и туда перешла большая часть его прежней команды из Novell. Первым делом новая компания выкупила у Attachmate разработки, которыми эта команда раньше и занималась: вы ведь всё равно не собираетесь их развивать.


Если ранее у Mono мобильные начинания были пристройками к появившемуся до них, то новая компания в своей работе над Mono-based products сразу ставила мобильную разработку во главу угла. В анонсе первыми пунктами её деятельности сразу оказывались new commercial .NET offering for iOS/Android. А в интервью тех лет Мигель сказал, что в Novell ограничивали мобильное развитие Mono, так что увольнение в чём-то оказалось даже к лучшему, можно стало развернуться как следует.



Название Ximian отсылало к латинскому simian (обезьяна), Mono к испанскому обезьяна, а Xamarin к обезьяньему роду тамаринов.


Вероятно, вы в курсе, чем это кончилось. Компания Microsoft со временем становилась всё больше за кроссплатформенность и открытый исходный код, так что ей всё ближе оказывались идеи де Икасы, которые он пропагандировал ещё с 90-х. В 2014-м она анонсировала .NET Core, идущий в эту сторону. А в 2016-м купила Xamarin со словами теперь дадим всем его возможности бесплатно. И, например, IDE Xamarin Studio позже была перебрендирована в Visual Studio for Mac.


Так Мигель де Икаса и Нэт Фридман, долго работавшие с технологиями Microsoft снаружи компании, в итоге оказались внутри неё. И Фридман сейчас возглавляет другую крупную покупку Microsoft GitHub. А что теперь де Икаса? Проект Mono появился в 2001-м как сторонняя реализация .NET для Linux, которую Microsoft никогда не сделает а как он вписывается в мир .NET спустя 19 лет, когда Microsoft сам уже кроссплатформеннее некуда?


В целом вписывается так: если раньше Mono был отдельным проектом, то сейчас его наработки влились в свежий .NET 5. Например, там соседствуют две виртуальные машины, подходящие для разных случаев: CoreCLR и MonoVM.


Конечно, когда соединяются такие крупные штуки, возникает много разных вопросов. Ну вот мы завтра в эфире DotNext как раз и будем задавать Мигелю вопросы: и о прошлом, и о настоящем. Узнаем больше подробностей о его прежних проектах и разберёмся, как именно выглядит его нынешняя деятельность в Microsoft. А кроме наших вопросов, он ответит и на зрительские так что если после этого текста захотелось узнать о чём-то подробнее, сможете получить ответ из первых рук.
Подробнее..

Разгоняем REACTOR

12.06.2021 18:20:44 | Автор: admin

Кому будет интересно?

Реактор сегодня - это стильно, модно, молодежно. Почему многие из нас практикуют реактивное программирование? Мало кто может ответить однозначно на этот вопрос. Хорошо - если Вы понимаете свой выигрыш, плохо - если реактор навязан организацией как данность. Большинство аргументов "ЗА" - это использование микросервисной архитектуры, которая в свою очередь обязывает микросервисы часто и много коммуницировать между собой. Для коммуникации в большинстве случаев выбирают HTTP взаимодействие. Для HTTP нужен легковесный веб-сервер, а что первое приходит на ум? Tomcat. Тут появляются проблемы с лимитом на максимальное количество сессий, при превышении которого веб-сервер начинает реджектить запросы (хотя лимита этого не так уж и легко достичь). Здесь на подмогу приходит реактор, который подобными лимитами не ограничен, и, например, Netty в качестве веб-сервера, который работает с реактивностью из коробки. Раз есть реактивный веб-сервер, нужен реактивный веб-клиент (Spring WebClient или Reactive Feign), а раз клиент реактивный, то вся эта жуть просачивается в бизнес логику, Mono и Flux становятся Вашими лучшими друзьями (хотя по началу есть только ненависть :))

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

Блокирующий и неблокирующий код

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

Лидер здесь - HTTP взаимодействие, вариантов масса, выбирай любой. Я предпочитаю Reactive Feign от Playtika, в комбинации со Spring Boot + WebFlux + Eureka мы получаем очень годную сборку для микросервисной архитектуры.

Давайте по-простому: НЕблокирующий код, это обычно всё, в названии чего есть reactive, а блокирующий - все оставшееся :) Hibernate + PostgreSQL - блокирующий, отправить почту через JavaMail - блокирующий, скинуть сообщение в очередь IBMMQ - блокирующий. Но есть, например, реактивный драйвер для MongoDB - неблокирующий. Отличительной особенностью блокирующего кода, является то, что глубоко внутри произойдет вызов метода, который заставит Ваш поток ждать (Thread.sleep() / Socket.read() и многие подобные), что для реактора - как нож в спину. Что же делать? Большинство бизнес логики завязано на базу данных, без нее никуда. На самом деле достаточно знать и уметь делать 2 вещи:

  • Необходимо понимать где блокирующий код. В этом может помочь проект BlockHound или его аналоги (тут тема для отдельной статьи)

  • Исполнение блокирующего кода необходимо переключать на пулы, готовые его выполнять, например: Schedulers.boundedElastic(). Делается это при помощи операторов publishOn & subscribeOn

Разгоняемся сами

Перед тем, как продолжить, необходимо немного размяться!

Уровень 1

    @Test    fun testLevel1() {        val result = Mono.just("")            .map { "123" }            .block()        assertEquals("123", result)    }

Начнем с простого, такой код обычно пишут начинающие reactor программисты. Как начать цепочку? Mono.just и ты на коне :) Оператор map трансформирует пустую строку в "123" и оператор block делает subscribe.

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

Уровень 2

    fun nonBlockingMethod1sec(data: String)     = data.toMono().delayElement(Duration.ofMillis(1000))    @Test    fun testLevel2() {        val result = nonBlockingMethod1sec("Hello world")            .flatMap { nonBlockingMethod1sec(it) }            .block()        assertEquals("Hello world", result)    }

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

Уровень 3

    fun collectTasks() = (0..99)    @Test    fun testLevel3() {        val result = nonBlockingMethod1sec("Hello world")            .flatMap { businessContext ->                collectTasks()                    .toFlux()                    .map {                        businessContext + it                    }                    .collectList()            }            .block()!!        assertEquals(collectTasks().toList().size, result.size)    }

Начинаем добавлять самое интересное - Flux! У нас появляется метод collectTasks, который собирает массив из сотни чисел, и далее мы делаем из него Flux - это будет наш список задач. К каждой задаче мы применяем трансформацию через оператор map. Оператор collectList собирает все результаты в итоговый список для дальнейшего использования.

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

Уровень 4

    fun collectTasks() = (0..100)        @Test    fun testLevel4() {        val result = nonBlockingMethod1sec("Hello world")            .flatMap { businessContext ->                collectTasks().toFlux()                    .flatMap {                        Mono.deferContextual { reactiveContext ->                            val hash = businessContext + it + reactiveContext["requestId"]                            hash.toMono()                        }                    }.collectList()            }            .contextWrite { it.put("requestId", UUID.randomUUID().toString()) }            .block()!!        assertEquals(collectTasks().toList().size, result.size)    }

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

Уровень 5

    fun collectTasks() = (0..1000)        fun doSomethingNonBlocking(data: String)        = data.toMono().delayElement(Duration.ofMillis(1000))        fun doSomethingBlocking(data: String): String {        Thread.sleep(1000); return data    }    val pool = Schedulers.newBoundedElastic(10, Int.MAX_VALUE, "test-pool")    private val logger = getLogger()    @Test    fun testLevel5() {        val counter = AtomicInteger(0)        val result = nonBlockingMethod1sec("Hello world")            .flatMap { _ ->                collectTasks().toFlux()                    .parallel()                    .runOn(pool)                    .flatMap {                        Mono.deferContextual { _ ->                            doSomethingNonBlocking(it.toString())                                .doOnRequest { logger.info("Added task in pool ${counter.incrementAndGet()}") }                                .doOnNext { logger.info("Non blocking code finished ${counter.get()}") }                                .map { doSomethingBlocking(it) }                                .doOnNext { logger.info("Removed task from pool ${counter.decrementAndGet()}") }                        }                    }.sequential()                    .collectList()            }            .block()!!        assertEquals(collectTasks().toList().size, result.size)    }

Вот мы и добрались до итогового варианта! Часть с реактивным контекстом была опущена для более наглядной демонстрации того, зачем мы здесь собрались. У нас появились два новых метода: doSomethingNonBlocking (3) & doSomethingBlocking (6) - один с неблокирующим ожиданием в секунду, второй с блокирующим. Мы создали пул потоков для обработки задач (10), добавили счетчик активных задач в реакторе (15). У нас появился оператор parallel (19) и обратный ему sequential (29). Задачи мы назначили на свежесозданный пул (20). Для понимания, что же происходит внутри, добавили логирование внутри операторов doOnRequest (вызывается перед исполнением метода), doOnNext (вызывается после исполнения метода). Основная задумка - на примере, определить сколько задач одновременно выполняется в реакторе и за какое время цепочка завершит свою работу.

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

И вот здесь начинается самое интересное. Попробуйте ответить на несколько вопросов. Как Вы считаете, сколько времени будет выполнятся данная цепочка? В ней 100 задач, в каждой задаче неблокирующее ожидание в 1 секунду, блокирующее ожидание в 1 секунду, и у нас в наличии пул из 10 потоков? (Вполне годная задачка на собеседование senior reactor developer :))

Правильный ответ

Около 12 секунд. Рассуждаем от блокирующего :) Блокирующее ожидание никуда не деть, и тут имеем 100 блокирующих секунд на 10 потоков, итого 10 секунд. Неблокирующее ожидание заметно нам лишь в первый раз, далее оно незаметно запускается в передышках между блокирующим. Не забываем про одну секунду сбора "бизнес контекста" перед запуском задач.

А теперь уберем строку (26) .map { doSomethingBlocking(it) } . Освободим наш реактор от блокирующего кода, интересно, сколько теперь времени займет выполнение цепочки?

Правильный ответ

2 секунды! 1 на сбор "бизнес контекста" и 1 на выполнение всех задач. Реактор запустит 100 задач одновременно. Но ведь у нас пул из 10 потоков? Как так? Первый разрыв шаблона.

Мы идем до конца и увеличиваем количество задач в методе collectTasks() до ... 1000? а может быть сразу до 15000? Как долго реактор будет выполнять столько задач?

Правильный ответ

2 секунды! 1 на сбор "бизнес контекста" и 1 на выполнение всех задач. Реактор запустит ВСЕ задачи одновременно. Второй разрыв шаблона. Где предел?

А это вообще легально?

Как же так и как это контролировать? Почему это опасно? Что если внутри параллельной обработки Вы решите вызвать другой микросервис? Если у вас 30000 задач, и по завершению каждой, Вам нужно отправлять запрос соседнему микросервису, Вы с удивлением можете обнаружить, что реактор непременно постарается выполнить все вызовы одновременно (Вы ведь используете реактивный web-client или реактивный feign, верно?) Открытие такого большого количества сокетов повлечет за собой превышение лимита открытых файловых дескрипторов в системе, что как минимум создаст проблемы с невозможностью создания новых сокетов в системе и помешает другим сервисам, а как максимум повалит Вам на сервере SSH и Вы потеряете доступ к серверу. Сомневаюсь, что в этот момент, программист будет кричать "зато смотри как быстро работает".

Разрыв шаблона. Thread Pool & Reactor

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

Классический thread pool - двери. Больше дверей - больше пропускная способность, все работает быстрее.

Теперь встречайте reactor! Вы видите двери? Нет никаких дверей

Реактор это большой мешок с подарками, или воздушная труба, задачи в которую валятся и летают там пока не выполнятся. А кто эти люди в желтом? Это наши epoll реактивные потоки, которые ни в коем случае нельзя нагружать блокирующими задачами. Можно провести аналогию с прорабами или инженерами. Они здесь, чтобы управлять процессом, а не чтобы выполнять тяжелую работу. Займите одного инженера тяжелой задачей, и когда к нему придет следующий рабочий с вопросом "что делать дальше?", он не сможет ответить, потому что был занят. Вот так и появляются таймауты в реактивном коде. Казалось бы микросервис стоит без нагрузки, выполняет какие-то задачки, а один из 500 запросов к нему падает с тайм-аутом, и непонятно почему. Велика вероятность что инженер был занят блокирующей задачей! Заботьтесь о своих инженерах и поручайте тяжелую работу специально обученным рабочим, например, Schedulers.boundedElastic().

Как контролировать эту "трубу", в которую валится всё без контроля? Вот мы и подошли к кульминации

Конфигурируем реактор!

В своей дефолтной конфигурации, параллельная обработка в реакторе зависит от количества ядер процессора сервера, на котором запускается код, поэтому, к своему удивлению, Вы получите разные результаты, проверяя работу реактора в тесте на локальной машине с 4-8 ядрами и production сервере с 32 ядрами.

Парад настроек открывает parallel с его аргументом parallelism

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

Но одного parallelism недостаточно, реактор все еще будет нагребать задач как не в себя.

Мало кто обращал внимание что у оператора flatMap (только того что запускается на Flux) есть перегрузки с интересными аргументами, а именно maxConcurrency

maxConcurrency очень важен, по дефолту значение стоит Integer.MAX_VALUE (определяет сколько неблокирующих задач может выполняться одновременно на одной рельсе. Понимаете теперь откуда аппетит у реактора?

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

Количество запусков цепочки напрямую влияет на количество одновременно выполняемых задач.

Подведем небольшой итог:

  • parallel (parallelism)

  • flatMap (maxConcurrency)

  • Количество запусков цепочки

Эти три параметра являются множителями, для расчета количества одновременных задач.

По дефолту это Кол-во ядер * Integer.MAX_VALUE * Количество запусков цепочки

Напротив же, запустив данный код для 5 задач длительностью в секунду мы получим цепочку работающую 5 секунд. Теперь всё под контролем!

        val result = nonBlockingMethod1sec("Hello world")            .flatMap { _ ->                collectTasks().toFlux()                    .parallel(1)                    .runOn(pool, 1)                    .flatMap({                        Mono.deferContextual { _ ->                            doSomethingNonBlocking(it.toString())                        }                    }, false, 1, 1)                    .sequential()                    .collectList()            }            .block()!!

Стоп, или не всё?

Thread Pool

Зачем же нужен пул потоков в реакторе? Думайте о нем как о двигателе для Вашего автомобиля. Чем пул мощнее - тем блокирующие задачи будут разбираться быстрее, а если потоков мало, то и блокирующие задачи задержатся у вас надолго! А куда же мы без блокирующих вызовов? На количество одновременно выполняемых задач в реакторе он не влияет, вот это поворот :)

Надеюсь, Вы не пробовали использовать Schedulers.parallel() для исполнения Вашего блокирующего кода? =) Несмотря на свое подходящее название (ну называется он parallel, значит и нужен для параллельной обработки) использовать этот пул можно только для неблокирующего кода, в доке указано что он живет с одним воркером, и содержит в себе только особенные, реактивные потоки.

Распределение задач по рельсам

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

Зеленые прямоугольники это наши задачи, которые распределяются в реакторе по алгоритму round-robin, что в случае с синтетическими данными дает красивую картинку.

Хорошо загруженный реактор (задачи равномерно распределены). 54 блокирующих задачи (каждая по 1сек), round-robin распределение по 6 рельсамХорошо загруженный реактор (задачи равномерно распределены). 54 блокирующих задачи (каждая по 1сек), round-robin распределение по 6 рельсам

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

Плохо загруженный пул (задачи распределены не равномерно)54 блокирующих задачи (каждая по 1сек кроме 2ух), round-robin распределение по 6 рельсамПлохо загруженный пул (задачи распределены не равномерно)54 блокирующих задачи (каждая по 1сек кроме 2ух), round-robin распределение по 6 рельсам

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

Бороться с этим можно несколькими способами

  • concatMap вместо flatMap (посмотрите в профилировщик на ваш пул, передумаете)

  • правильно планировать задачи, чтобы исключить аномалии (почти невозможно)

  • дробить каждую задачу на много мелких, и также запускать их в параллельную обработку чтобы нивелировать проблемы с распределением (вполне рабочий вариант)

  • prefetch (наш выбор!)

Параметр prefetch у flatMap & runOn позволяет определить, сколько задач будет взято на одну рельсу на старте, а затем при достижении некоторого порога выполнения задач, реквесты будут повторяться с этим количеством. Значение по умолчанию - 256. Сменив значение на 1, можно заставить реактор использовать механизм "work stealing", при котором, рельсы и потоки, которые освободились, будут забирать задачи себе на выполнение и картина получится гораздо более приятная.

Хорошо загруженный пул (задачи равномерно распределены)54 блокирующих задачи (каждая по 1сек кроме 2ух), round-robin распределение по 6 рельсамPrefetch !Хорошо загруженный пул (задачи равномерно распределены)54 блокирующих задачи (каждая по 1сек кроме 2ух), round-robin распределение по 6 рельсамPrefetch !

На этом у меня всё. Будет интересно прочесть Ваши замечания и комментарии, на 100% истину не претендую, но все результаты подкреплены практическими примерами, на Spring Boot + Project Reactor 3.4. Всем спасибо!

Подробнее..
Категории: Kotlin , Java , Concurrency , Parallel , Reactor , Parallelism , Mono , Threads , Flux , Pool , Prefetch

Представляем Owlcat Mono Profiler для Unity

04.12.2020 12:06:11 | Автор: admin

Официальная часть мероприятия

Добрый день. Я работаю программистом в компании Owlcat Games, которая выпустила одну из самых успешных российских компьютерных RPG Pathfinder: Kingmaker и сейчас работает над её продолжением, Pathfinder: Wrath of the Righteous. В ходе портирования первой игры нашей студии на консоли, мы столкнулись с проблемой поиска утечек памяти. Штатные инструменты движка Unity и целевых платформ оказались по разным причинам не слишком удобны для борьбы с утечками, и поэтому мы решили написать свой инструмент, о котором я и расскажу ниже.

Owlcat Mono Profiler предназначен для исследования использования памяти Mono в играх на движке Unity. Он доступен всем желающим в виде собранных бинарных файлов (под Windows) и исходного кода на Github. В отличие от встроенного профайлера Unity, а также пакета Memory Profiler, он не требует снятия снимков состояния памяти, а производит постоянный мониторинг Mono-кучи, что позволяет выявлять не только утечки, но и пики аллокаций, и избыточные повторяющиеся аллокации. По сравнению с платформо-специфичными инструментами, такими как Memory Analyzer для PS4, он корректно отображает события, происходящие с памятью, управляемой сборщиком мусора.

На этом покончим с формальностями, и перейдём к cool story.

Фатальный недостаток всех прочих инструментов

Началось всё с того, что мы выяснили, что память в нашей игре подтекает. На PC это не было проблемой, поскольку течёт она не то чтобы водопадом, да и памяти даже на слабых машинах в наше время будет поболее, чем у PlayStation 4 или XBox One. Плюс, Windows, когда кончается память, начинает скидывать лишнее в своп, а консоли - просто убивают твоё приложение, и иди, разбирайся, где накосячил.

Встроенные инструменты Unity пришлось отмести практически сразу: в Unity 2018.4 они с нашей игрой фактически не работали (снятие одного снимка состояния памяти могло занять 8+ часов, а на PlayStation мне ни разу не удалось его дождаться в принципе). В 2019.x стало сильно лучше, но перейти на неё мы не могли - смена мажорной версии движка в Unity ломает слишком многое.

В комплект инструментов для PlayStation 4 входит совершенно потрясающий Memory Analyzer. Серьёзно, это один из лучших инструментов для анализа потребление памяти, какие я только видел (хотя и не лишённый некоторых мелких недостатков). Уже одна только возможность помечать любые функции с подходящей сигнатурой как alloc/realloc/free делает его невероятно полезным для любой игры, использующей собственные аллокаторы, memory pool'ы и т.п.

Но есть проблема. Дело в том, что Mono, в том виде, в каком его используют в Юнити, содержит в себе видавший виды сборщик мусора BoehmGC. Это проверенный временем проект, но к сожалению он написан таким образом, что во многом представляет из себя помесь чёрного ящика и чёрной дыры, в которую можно что-то засунуть, но нельзя достать. В частности, он не предоставляет никакого способа узнать о моменте удаления объекта.

Почему сложно написать профайлер памяти для Unity

А теперь давайте сделаем шаг назад и посмотрим, как вообще работает сборщик мусора. Я до поступления в команду Owlcat Games работал, в основном, с C++, поэтому чисто теоретически про сборку мусора что-то знал, но на практике с ней не сталкивался, и имел, как потом выяснилось, в корне неверные представления о том, как этот зверь устроен. Если вы в этой области собаку съели, то дальнейшие мои объяснения вам покажутся чересчур упрощёнными и может быть даже в чём-то ошибочными, но надеюсь они подойдут для объяснения той простой мысли, что написать профайлер памяти для языка с GC - это вам не два байта переслать.

Итак, что делает сборщик мусора? Он берёт себе у системы кусок памяти И никогда его не возвращает (во всяком случае, именно так ведёт себя BoehmGC на PS4). В этом куске памяти, он по запросу пользователя выделяет маленькие кусочки под конкретные объекты - там тоже не всё так просто, но это не важно. Важно что факт аллокации памяти отследить очень просто - есть несколько функций, которые прямо так и называются, gc_malloc_что_нибудь. А вот факт деаллокации памяти отследить слегка сложнее. Это в C++ кто-то должен сказать объекту "умри". Тут же про него просто "забывают", то есть, перестают на него ссылаться. Конечно, сборщик мусора не следит за всеми записями в память, чтобы заметить, что последняя ссылка на объект протухла. Вместо этого, раз-в-сколько-то времени (на самом деле - обычно когда для очередной аллокации не хватает памяти) он говорит "так, всем стоять, сейчас я разберусь, кто тут живой, а кто мёртвый", и отправляется шерстить всю выделенную памяти в поисках ссылок на объекты. В конце этого процесса, если есть какие-то объекты, на которые он ссылок не нашёл, их-то он и удаляет, а точнее - помечает их память как свободную и доступную для выделения.

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

Бьёмся головой в стену.

Следующей мыслью, пришедшей мне в голову, было добавить всем вообще аллоцируемым объектам финалайзеры. Финалайзер - это такая функция, которая как раз обязательно вызывается, когда объект удаляется, вроде деструктора в C++. Но и на этом пути меня не ждала победа: да, в рамках il2cpp, имеющего открытый исходный код, я мог вставить свой костыль, но это нельзя было делать оголтело, ведь у объекта УЖЕ мог быть финалайзер, а значит надо было его как-то извлекать, запоминать и подменять своим Наверное, если копать в этом направлении дольше, может что-то и получилось бы, но сама идея менять исходный код кусков Unity мне не нравилась, не говоря уже о том, что это решение не заработало бы на PC, где мы не используем il2cpp во имя лёгкости моддинга игры.

Дальше я отправился в Гугл, искать, а как вообще люди профилируют память в Mono? Ответ нашёлся на первой странице Гугла, в официальной документации. Вот только в версии Mono, используемой Unity, описанный там встроенный профайлер был благополучно выпилен. Кроме того, поиск так же показал, что почти все средства анализа логов, снятых при помощи встроенного профайлера, заброшены, устарели или недописаны, так что особой надежды на них не было, даже если бы мне удалось как-то вернуть эту функциональность (например, пересобрав Mono для Unity - что, правда, не сработало бы на PlayStation!).

Мы пойдём другим путём!

Однако, бродя по дебрям сети, я наткнулся на Heap-Prof, давно неактуальный и заброшенный профайлер памяти для Mono, из которого, однако, мне удалось почерпнуть интересную идею. Идея заключалась в том, чтобы тупо повторять всю работу, которую делает реальный сборщик мусора:

  • Регистрировать аллокации, когда они происходят, создавать событие "объект создан".

  • Ловить события сборки мусора (типа "сборка мусора завершена") и в этот момент проверять, какие из наших объектов всё ещё живы. Для всех, кто не жив - создавать событие "объект удалён".

Довольно быстро, я перенёс и осовременил код heap-prof в dll, которую подгрузил плагином к Юнити, достал при помощи GetProcAddress функции Mono, позволяющие всё это проделать, и И игра упала. В функции mono_object_is_alive. Попытки понять, от чего такое происходит, и как вообще эта функция работает, привели меня к письму одного из авторов Mono, Massimiliano Mantione, опубликованному в почтовой рассылке Mono-dev в 2009 году. Во сием послании, он в точности описывал мои проблемы с heap-prof, и в частности писал, "The problem is that this is not reliable: "mono_object_is_alive" was not meant to be a public function. And in fact sometimes the heap snapshots are wrong (or the profiler crashes).". К сожалению, в качестве решения он предлагал улучшить API для профайлера в НОВОМ сборщике мусора, SGen, на который Unity в своей версии Mono так никогда и не перешли

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

Тут надо сделать очередное небольшое отступление, и рассказать о том, как рассуждал Шульц как сборщик мусора, собственно, ищет ссылки на объекты. Опять же, в ОЧЕНЬ упрощённом виде. Ищет он их тупо - берёт память какого-нибудь объекта, и прямо по ней идёт и смотрит - вот это значение похоже на адрес в куче? Есть у нас объект с таким адресом вообще? Если на оба вопроса "да" - то это ссылка на объект, и этот объект смело можно помечать как живой. Острые умом подметят, что если эдак пройтись по всей куче итеративно несколько раз - не останется ни одного объекта (например, если в куче всего два объекта, A и B, A ссылается на B, то мы сначала удалим A - потому что на него никто не ссылается, а на следующей итерации удалим и B, потому что теперь на него тоже никто не ссылается). Для этого у сборщика мусора есть корневые объекты, которые удалять обычным образом нельзя, и вот от них уже идут ссылки на всех остальных.

BoehmGC, к сожалению, представляет собой упомянутую чёрную дыру - зарегистрировать в нём корневые объекты можно, а вот спросить у него, какие корни зарегистрированы - никак нельзя. Но Mono решает эту проблему за нас, и вызывает коллбэки каждый раз, когда регистрирует или удаляет корневые объекты. А я-то уж было приготовился лезть в переменную с адресами корневых объектов по отступу в памяти Ладно, прячем шашку в штаны и продолжаем наши экзерсисы.

"Я вас настиг! Какой я молодец"

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

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

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

Помимо прочих достоинств, наш профайлер умеет профайлить релизные билды игры, и даже без необходимости заранее встраивать плагин в сборку (можно и чужие игры профайлить, если у вас возникло желание помочь авторам). Достаточно только иметь PDB файл от нужной версии Unity Player: он нужен для того, чтобы достать адреса некоторых функций, которые нужно перехватить, в частности, для того, чтобы вовремя запустить профайлер, а также для получения событий об окончании кадра (события для удобства группируются по кадрам, а не по времени). К сожалению, Unity не предоставляет даже графическим плагинам возможности узнать о конце кадра другим способом, так что пришлось взять в руки Microsoft Detours и лезть в недра.

Есть у выбранного подхода и недостатки. Профайлер довольно заметно замедляет игру, процентов на 20 в обычных кадрах, а в момент сборки мусора может подвесить её даже на 5-10 секунд (в зависимости от количества объектов). Также, для профайлера требуется довольно много памяти на той машине, где, собственно, запущена игра: на ~2 миллиона аллокаций нужно ~200Mb памяти. Для базы клиента/UI, может потребоваться до нескольких гигабайт памяти, что представляется несущественным ограничением, так как в крайнем случае, можно запускать клиент/UI профайлера на другой машине (он соединяется с самим профайлером по сети).

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

Текущая версия профайлера имеет интерфейс, написанный на Qt5, и теоретически должна быть относительно легко портируема на другие операционные системы (это в наших планах, но не в приоритете, так как основная часть разработчиков игр, всё-таки, работает под операционной системой Microsoft). В качестве БД для хранения событий, используется SQLite с временными (частично находящимися в памяти) базами, но есть идеи о переходе на memory mapped database для ещё большей скорости. Я обдумывал возможность интеграции профайлера в саму Unity, но это представляется не идеальным решением, так как иногда хочется попрофайлить игру в редакторе, не собирая билдов (когда пробуешь разные варианты исправлений, например), а в этом случае, профайлер, встроенный в редактор и потому также производящий аллокации managed памяти - очень плохая идея.

Дальнейшие планы

Профайлер открыт для свободного использования всеми желающими. Я надеюсь, что он окажется полезным кому-то кроме нашей компании. Несомненно, найдутся в нём и ошибки, которые надо исправлять, и возможные улучшения интерфейса и функционала. Жду ваших предложений (и пулл-риквестов!) на Гитхабе. Я надеюсь, что эта программа станет первой частью нашего инструментария для отладки игр на Unity, Owlcat Grooming Toolkit. В отдалённых планах есть так же CPU профайлер с открытым исходным кодом, который мог бы стать бесплатной альтернативой dotTrace, которую можно было бы раздавать игрокам для диагностики без зазрения совести.

Подробнее..

Из песочницы Защита .Net кода от реверс инженеринга с помощью ConfuserEx 0.6.0

23.10.2020 16:09:30 | Автор: admin

В статье рассказывается об опыте боевого применения обфускатора ConfuserEx 0.6.0 для защиты сервиса .Net под Windows и Mono. Дело было в далеком 2016 году, но, я думаю, тема не потеряла актуальность и сейчас.


ConfuserEx (http://yck1509.github.io/ConfuserEx/) один из бесплатных обфускаторов для .Net с открытым исходным кодом. Поддерживает работу в среде Windows .NET Framework и Mono.
Содержит большое число модулей, реализующих различные методы защиты кода (переименование, запутывание потока выполнения, шифрование ресурсов и констант, защита от отладки и профилирования, упаковщики). ConfuserEx дает возможность расширения функциональности, путем написания собственных модулей защиты.


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


Документация


У проекта имеется достаточно подробная документация в формате WiKi.


Пользовательский интерфейс


ConfuserEx поддерживает работу в UI режиме, а так же режиме командной строки.


Режим командной строки


ConfuserEx\bin\Confuser.CLI.exeConfuserEx.CLI: No input files specified.Usage:Confuser.CLI -n|noPause <project configuration>Confuser.CLI -n|noPause -o|out=<output directory> <modules>    -n|noPause : no pause after finishing protection.    -o|out     : specifies output directory.    -probe     : specifies probe directory.    -plugin    : specifies plugin path.    -debug     : specifies debug symbol generation.</source>

ConfuserEx\bin\Confuser.CLI.exe -n LicenseManagerService.crproj [INFO] ConfuserEx v0.6.0-custom Copyright (C) Ki 2014 [INFO] Running on Microsoft Windows NT 6.1.7601 Service Pack 1, .NET Framework v4.0.30319.0, 64 bits[DEBUG] Discovering plugins... [INFO] Discovered 10 protections, 1 packers.[DEBUG] Resolving component dependency... [INFO] Loading input modules... [INFO] Loading 'LicenseManagerService\bin\x86\Release\LicenseManagerService.exe'... [INFO] Initializing...[DEBUG] Building pipeline... [INFO] Resolving dependencies...[DEBUG] Checking Strong Name...[DEBUG] Creating global .cctors...[DEBUG] Executing 'Name analysis' phase...[DEBUG] Building VTables & identifier list...[DEBUG] Analyzing... [INFO] Processing module 'LicenseManagerService.exe'...[DEBUG] Executing 'Invalid metadata addition' phase...[DEBUG] Executing 'Renaming' phase...[DEBUG] Renaming...[DEBUG] Executing 'Anti-debug injection' phase...[DEBUG] Executing 'Anti-dump injection' phase...[DEBUG] Executing 'Anti-ILDasm marking' phase...[DEBUG] Executing 'Encoding reference proxies' phase...[DEBUG] Executing 'Constant encryption helpers injection' phase...[DEBUG] Executing 'Resource encryption helpers injection' phase...[DEBUG] Executing 'Constants encoding' phase...[DEBUG] Executing 'Anti-tamper helpers injection' phase...[DEBUG] Executing 'Control flow mangling' phase...[DEBUG] Executing 'Post-renaming' phase...[DEBUG] Executing 'Anti-tamper metadata preparation' phase...[DEBUG] Executing 'Packer info extraction' phase... [INFO] Writing module 'LicenseManagerService.exe'...[DEBUG] Encrypting resources... [INFO] Finalizing... [INFO] Packing...[DEBUG] Encrypting modules... [INFO] Protecting packer stub...[DEBUG] Discovering plugins... [INFO] Discovered 11 protections, 1 packers.[DEBUG] Resolving component dependency... [INFO] Loading input modules... [INFO] Loading 'LicenseManagerService\bin\x86\Release\LicenseManagerService.exe'... [INFO] Initializing...[DEBUG] Building pipeline... [INFO] Resolving dependencies...[DEBUG] Checking Strong Name...[DEBUG] Creating global .cctors...[DEBUG] Executing 'Name analysis' phase...[DEBUG] Building VTables & identifier list...[DEBUG] Analyzing... [INFO] Processing module 'LicenseManagerService.exe'...[DEBUG] Executing 'Packer info encoding' phase...[DEBUG] Executing 'Invalid metadata addition' phase...[DEBUG] Executing 'Renaming' phase...[DEBUG] Renaming...[DEBUG] Executing 'Anti-debug injection' phase...[DEBUG] Executing 'Anti-dump injection' phase...[DEBUG] Executing 'Anti-ILDasm marking' phase...[DEBUG] Executing 'Encoding reference proxies' phase...[DEBUG] Executing 'Constant encryption helpers injection' phase...[DEBUG] Executing 'Resource encryption helpers injection' phase...[DEBUG] Executing 'Constants encoding' phase...[DEBUG] Executing 'Anti-tamper helpers injection' phase...[DEBUG] Executing 'Control flow mangling' phase...[DEBUG] Executing 'Post-renaming' phase...[DEBUG] Executing 'Anti-tamper metadata preparation' phase...[DEBUG] Executing 'Packer info extraction' phase... [INFO] Writing module 'LicenseManagerService.exe'...[DEBUG] Encrypting resources... [INFO] Finalizing...[DEBUG] Saving to 'C:\Users\pash76\AppData\Local\Temp\ehwkjzxt.brh\mqqtgvji.gxk\LicenseManagerService\bin\x86\Release\LicenseManagerService.exe'...[DEBUG] Executing 'Export symbol map' phase... [INFO] Finish protecting packer stub.[DEBUG] Saving to 'D:\pash76\Develop\License_manager\Confused\LicenseManagerService\bin\x86\Release\LicenseManagerService.exe'...[DEBUG] Executing 'Export symbol map' phase... [INFO] Done.Finished at 9:35, 0:03 elapsed.

Файл проекта


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


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


Альтернативой правил является декларативный способ пометкой кода атрибутами.


<project outputDir=".\Confused" baseDir=".\" xmlns="http://personeltest.ru/away/confuser.codeplex.com">    <rule pattern="true" inherit="false">        <protection id="anti ildasm" />        <protection id="anti tamper" action="remove" />        <protection id="constants">            <argument name="mode" value="dynamic" />            <argument name="decoderCount" value="13" />            <argument name="elements" value="SIP" />            <argument name="cfg" value="false" />        </protection>        <protection id="ctrl flow" />        <protection id="anti dump" action="remove" />        <protection id="anti debug" />        <protection id="invalid metadata" action="remove" />        <protection id="ref proxy" />        <protection id="resources">            <argument name="mode" value="dynamic" />        </protection>        <protection id="rename">            <argument name="mode" value="sequential" />        </protection>    </rule>    <packer id="compressor">        <argument name="key" value="dynamic" />        <argument name="compat" value="true" />    </packer>    <module path="LicenseManagerService\bin\x86\Release\LicenseManagerService.exe">        <rule pattern="            match('UAVLicenseManager\.CentOSSystemInfoProvider::ShellCommand.*')            or match(' ?LicenseManagerService\.Program(::)?')            or match(' ?LicenseManagerService\.UAVLicenseManagerService(::)?')            or (                match(' ?UAVLicenseManager\.License(::)?')                 and is-public()                 and (member-type('type') or member-type('field') or member-type('property'))                )            or match(' ?UAVLicenseManager\.LicenseKey(::)?')            or match(' ?UAVLicenseManager\.LicenseOwner(::)?')            or match(' ?UAVLicenseManager\.Location(::)?')            or match(' ?UAVLicenseManager\.LicenseLimit(::)?')            " inherit="true">            <protection id="rename" action="remove" />        </rule>    </module></project>

Модули защиты


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


Защита Name Protection нарушает работу механизмов Reflection и сериализации. Типы, которые необходимо сериализовывать или обрабатывать с использованием рефлексии необходимо добавлять в исключения с помощью правил или с помощью атрибутов.


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


Модуль Windows Mono
Anti Debug Protection
Anti Dump Protection
Anti IL Dasm Protection
Anti Tamper Protection
Compressor (в режиме совместимости)
Constants Protection (режим cfg не поддерживается)
Control Flow Protection
Invalid Metadata Protection
Name Protection
Reference Proxy Protection
Resources Protection

Упаковщики


Упаковщик, помимо уменьшения размера выходного файла, позволят закодировать скрыть весь исполняемый IL-код. В результате в dotPeek можно увидеть лишь служебный код ConfuserEx. Без упаковщика, в dotPeek становится доступным гораздо больше информации.



Кастомизация защиты


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


ConfuserEx\src\Confuser.Core\ConfuserEngine.cs


static void Inspection(ConfuserContext context) {    context.Logger.Info("Resolving dependencies...");    foreach (var dependency in context.Modules                                      .SelectMany(module => module.GetAssemblyRefs().Select(asmRef => Tuple.Create(asmRef, module)))) {        try {            AssemblyDef assembly = context.Resolver.ResolveThrow(dependency.Item1, dependency.Item2);        }        catch (AssemblyResolveException ex) {            context.Logger.ErrorException("Failed to resolve dependency of '" + dependency.Item2.Name + "'.", ex);            throw new ConfuserException(ex);        }    }    context.Logger.Debug("Checking Strong Name...");    foreach (ModuleDefMD module in context.Modules) {        var snKey = context.Annotations.Get<StrongNameKey>(module, Marker.SNKey);        if (snKey == null && module.IsStrongNameSigned)            context.Logger.WarnFormat("[{0}] SN Key is not provided for a signed module, the output may not be working.", module.Name);        else if (snKey != null && !module.IsStrongNameSigned)            context.Logger.WarnFormat("[{0}] SN Key is provided for an unsigned module, the output may not be working.", module.Name);        else if (snKey != null && module.IsStrongNameSigned &&                 !module.Assembly.PublicKey.Data.SequenceEqual(snKey.PublicKey))            context.Logger.WarnFormat("[{0}] Provided SN Key and signed module's public key do not match, the output may not be working.", module.Name);    }    var marker = context.Registry.GetService<IMarkerService>();    context.Logger.Debug("Creating global .cctors...");    foreach (ModuleDefMD module in context.Modules) {        TypeDef modType = module.GlobalType;        if (modType == null) {            modType = new TypeDefUser("", "<Module>", null);            modType.Attributes = TypeAttributes.AnsiClass;            module.Types.Add(modType);            marker.Mark(modType, null);        }        MethodDef cctor = modType.FindOrCreateStaticConstructor();        if (!marker.IsMarked(cctor))            marker.Mark(cctor, null);    }    //context.Logger.Debug("Watermarking...");    //foreach (ModuleDefMD module in context.Modules) {    //    TypeRef attrRef = module.CorLibTypes.GetTypeRef("System", "Attribute");    //    var attrType = new TypeDefUser("", "ConfusedByAttribute", attrRef);    //    module.Types.Add(attrType);    //    marker.Mark(attrType, null);    //    var ctor = new MethodDefUser(    //        ".ctor",    //        MethodSig.CreateInstance(module.CorLibTypes.Void, module.CorLibTypes.String),    //        MethodImplAttributes.Managed,    //        MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName);    //    ctor.Body = new CilBody();    //    ctor.Body.MaxStack = 1;    //    ctor.Body.Instructions.Add(OpCodes.Ldarg_0.ToInstruction());    //    ctor.Body.Instructions.Add(OpCodes.Call.ToInstruction(new MemberRefUser(module, ".ctor", MethodSig.CreateInstance(module.CorLibTypes.Void), attrRef)));    //    ctor.Body.Instructions.Add(OpCodes.Ret.ToInstruction());    //    attrType.Methods.Add(ctor);    //    marker.Mark(ctor, null);    //    var attr = new CustomAttribute(ctor);    //    attr.ConstructorArguments.Add(new CAArgument(module.CorLibTypes.String, Version));    //    module.CustomAttributes.Add(attr);    //}}

Кроме того, рекомендуется изменить имя .net модуля ConfuserEx. По умолчанию используется имя koi.
Искать поиском по исходникам


ConfuserEx\src\Confuser.Protections\Compress\Compressor.csConfuserEx\src\Confuser.Runtime\Compressor.csConfuserEx\src\Confuser.Protections\Compress\ExtractPhase.csConfuserEx\src\Confuser.Protections\Compress\StubProtection.cs

Примеры результатов


Сборка защищена без применения упаковщика (для возможности дизассемблера dotPeek). Файл проекта с настройками приведен выше.


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


using System;using System.Collections.Generic;using System.Linq;using System.Runtime.CompilerServices;using System.ServiceProcess;using System.Text;using System.Threading.Tasks;[assembly: InternalsVisibleTo("LicenseManagerServiceTests")]namespace LicenseManagerService{    static class Program    {        /// <summary>        /// Главная точка входа для приложения.        /// </summary>        ///        static void Main()        {            ServiceBase[] ServicesToRun;            ServicesToRun = new ServiceBase[]            {                new UAVLicenseManagerService()            };            ServiceBase.Run(ServicesToRun);        }    }}

Дизассемблированый код в dotPeek



Для типа LicenseManagerService.Program, с помощью правил в файле проектов, был отключен модуль Name Protection (имена сохранились). Видны результаты работы модуля Control Flow Protection.


// Decompiled with JetBrains decompiler// Type: LicenseManagerService.Program// Assembly: LicenseManagerService, Version=1.0.5980.24716, Culture=neutral, PublicKeyToken=null// MVID: A6EB17CC-65EE-4E2D-B66C-24E166429A4A// Assembly location: D:\pash\Develop\License_manager\Confused\LicenseManagerService\bin\x86\Release\LicenseManagerService.exeusing System.Runtime.InteropServices;using System.ServiceProcess;namespace LicenseManagerService{  internal static class Program  {    private static void Main()    {      ServiceBase[] serviceBaseArray1 = new ServiceBase[1]      {        (ServiceBase) new UAVLicenseManagerService()      };label_1:      int num1 = 1005209177;      ServiceBase[] serviceBaseArray2;      while (true)      {        int num2 = 1280737639;        uint num3;        switch ((num3 = (uint) (num1 ^ num2)) % 3U)        {          case 0U:            goto label_1;          case 1U:            serviceBaseArray2 = serviceBaseArray1;            num1 = (int) num3 * 1248105312 ^ 483770479;            continue;          default:            goto label_4;        }      }label_4:      Program.\u200E(serviceBaseArray2);    }    static void \u200E([In] ServiceBase[] obj0)    {      ServiceBase.Run(obj0);    }  }}

Защита констант в коде


private readonly string ShellCommandNetworkAdapterMACAddress =    @"ip -o link show | grep -m 1 'UP.*LOWER_UP.*ether\|LOWER_UP.*UP.*ether' | sed -n 's/.*ether \(.*\) brd.*/\1/p' | tr -d '\n[:blank:]'";


Для всех элементов кода, начинающихся с ShellCommand, с помощью правил в файле проектов, был отключен модуль Name Protection (имена сохранились). Видны результаты работы модуля Constants Protection.


internal sealed class _ob : _qA{  private readonly string ShellCommandNetworkAdapterMACAddress = \u003CModule\u003E.\u206E<string>(3331371713U);  private readonly string ShellCommandNetworkAdapterCaption = \u003CModule\u003E.\u206F<string>(4243712535U);</source>
Подробнее..
Категории: Net , Net frameowrk , Mono , Obfuscated code

Категории

Последние комментарии

  • Имя: Макс
    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