Перехват вызовов DTrace

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

Изучение DTrace стоит начать с его “эпицентра” (как утверждают авторы фрэймворка) – функции dtrace_probe. В целом, код достаточно понятен и содержит в себе большое количество комментариев. После внимательного изучения этой функции становится ясно, что искомое находится чуть дальше, в функции fbt_provide_module, которую можно найти в файле fbt.c директории bsd/dev/dtrace исходных кодов XNU. Эта функция как раз и используется для патчинга перехватываемых функций.
Итак, что же он делает в случае с 64 битной архитектурой (для 32 битной или PPC используется тот-же принцип)? Сам процесс установки перехватчика выглядит следующим образом: у каждого модуля берется таблица символов (struct nlist_64) и каждый глобальный символ, имеющий имя, сверяется со списком требуемых “проб”. При этом DTrace отказывается перехватывать ряд системных функций, например, такие как собственные функции (dtrace_*), функции, отвечающие за отладку (kdp_*, kdbg_*, kernel_debug), работу таймеров (etimer_*, clock_*, absolutetime_to_*), управление питанием (acpi_*), и ряд других. Если найденную функцию необходимо перехватить, то делается проверка того, что функция начинается со следующей последовательности команд (адрес начала функции получается из той же таблицы символов):

push %rbp                       ; 0x55
mov %rsp, %rbp                  ; 0x48, 0x89, 0xE5

Затем ищется конец функции. Если все прошло успешно, то создается “проба”, в которой сохраняется внутренняя информация, включая информацию о найденных адресах. На этом создание пробы завершается и начинается обработка следующей функции из списка.

Ну, а теперь самое интересное. После того как проба становится активной (функция fbt_enable), второй байт от начала перехватываемой функции подменяется на 0xF0. Копирование происходит по физическим адресам в памяти при помощи функции ml_nofault_copy. В результате возникает гарантировано неверная команда и, как следствие, возникновение исключения при попытке ее обработки процессором. Ну, а дальше все довольно очевидно. Возникающее исключение перехватывается DTrace, и идет обработка сработавшей “пробы” с последующим вызовом оригинальной функции.

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

Грабля в launchd

Неожиданно нарвался на граблю там, где уж совсем не ожидал ее найти, а именно в launchd. Вообще, launchd крайне удобен в управлении демонами, например при помощи него мы при запуске GUI из под пользователя поднимаем рутовый демон. Делается это довольно просто, создается plist и в переменную WatchPath указывается директория, по изменении которой должно запуститься приложение из ProgrammArgument. Сам plist помещается в директорию LaunchAgents для пользовательских приложений и LauchDaemons для системных.
Описанная выше часть работает как часы, и все бы ничего, если бы не возникла необходимость запустить по событию не только демон, но и само GUI. Казалось-бы все просто – добавляем WatchPath со ссылкой на ту же директорию, что и в случае с нашим демоном и вперед. Но вот тут то и зарыта грабля – демон стартует великолепно, а GUI нет. Чисто теоретически, такого происходить не должно, особенно с учетом того, что за директорией LaunchAgents следит launchd запущенный из-под пользователя, а за LauchDaemons рутовый. Но, это только теория, на практике все несколько иначе.
Лечиться созданием отдельной директории, используемой для запуска GUI приложения, которое уже в свою очередь поднимает демон.

Работа в Москве

Похоже, что московское АйТи таки оправилось от кризиса (ну, либо думает что оправилось).  В крайнем случае в области разработки на C++ зарплаты уверенно ползут вверх. С помощью МоегоКруга и hh.ru, получилось собрать довольно интересную информацию, такую как:

  • Зарплату в 150 тыр на старте давать никто не хочет (блин, обидно), хотя, по слухам, где-то такие ЗП есть!
  • На 130 тыр вакансии уже есть, но их чертовски мало. Тем не менее, очень хорошему разработчику на C++ или архитектору дать могут.
  • До оплаты в 100-110 тыр созрели почти все более-менее уважающие себя компании. Это очень приятный момент.
  • Ниже 100.. ну это уже не интересно, хотя, надо признать, таких вакансий не мало (если не сказать что дохрена), но и требования там довольно не серьезные.

При этом, мэйнстрим, такой как .NET на оборот проседает. Ну, это и не удивительно, чем больше предложение, тем ниже цена. Очень низкий порог входа дает о себе знать.

Что не так с Java?

Меня долгое время мучил вопрос, почему приложения на Java такие тормозные? Если верить синтетическим тестам на The Computer Language Benchmarks Game, то в среднем приложения на Java работают всего-то в 2 раза медленнее при использовании памяти в 10 раз больше по отношению к C++ приложениям. В принципе, это не такой уж и большой разрыв. Хотя в этих тестах нет приложений на Objective-C, я думаю что результат для был-бы приблизительно тем же, ну может памяти тратилось бы не в 10, а в 5 раз больше, а ведь c приложениями на Objective-C можно нормально работать. Так в чем же дело?

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

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

Одно радует, теперь понятно почему на десктопах количество приложений написанных на C# или Java ничтожно мало