Думаю, у каждого программиста есть набор любимых утилит, трюков и знаний. Некоторые он даже использует в работе. Скорее всего, у разных программистов они различаются. Значит, пришло время поделиться. Сегодня я расскажу о своей подборке того, что, вероятно, известно не настолько широко, как оно того заслуживает. Речь пойдёт преимущественно про платформу GNU/Linux. Для начала немного кросс-платформенности. Есть такая утилита — PMD, вероятно, хорошо известная Java-программистам. Это статический анализатор, поддерживающий несколько (не очень много) языков, что и неудивительно — написание содержательных правил анализа вряд ли работа из простых. Но у этой утилиты есть и вторая часть — Copy-Paste Detector (CPD). Для его работы требуется существенно меньшее «понимание» исходного текста, так что логично, что обрабатывает он существенно больше языков. Так вот, бытует мнение, что опенсорсный проект — это когда нужно писать код не просто, чтобы работал, а ещё и чтобы другие программисты не засмеяли. В случае крупных проектов так оно, наверное, и есть, но вот копипаст не настолько заметен. Ментейнерам, увы, тоже. И вот однажды, получив от разработчиков одной программы комментарий, что «Если здесь и здесь эти три строчки вынести во вспомогательный метод, то и в остальных двадцати местах, наверное, тоже», я решил оценить масштабы происходящего. С одной стороны, повторение паттерна из трёх строчек может выглядеть нестрашно. С другой — он явно имеет какой-то смысл, и контрибьюторам-новичкам будет проще, если смысл будет чётко отражён в имени метода. bash.im Недолго думая, я запустил CPD на этом C++-проекте, ожидая увидеть список повторений этого паттерна из трёх строк. Как раз с ним я не столкнулся (он не превысил порог по умолчанию для количества токенов на одно повторение), зато увидел много случаев вроде повторения идентичного метода из 50 строк в двух разных наследниках одного класса. Вряд ли основной разработчик сознательно скопировал такой кусок — видимо, произошло несколько другое: кто-то когда-то добавил этот метод в одном из классов, а в другом pull request через много времени кто-то, недолго думая, его скопировал. Логично, что этот код был не хуже того, что уже есть в репозитории (ха-ха), поэтому был принят. Бывало ли у вас, что, отлаживая программу, вы долго воспроизводите баг, а потом одно неверное движение — Step over вместо Step into — и всё, пичалька, ещё десять минут всё заново воспроизводить! С Mozilla record-replay framework всё несколько по-другому: «прогоняй тесты за завтраком, отлаживай когда вернёшься» говорили они. Этот инструмент не пытается записывать изменения состояния после каждой инструкции (говорят, такой режим есть у gdb...), вместо этого он симулирует исполнение программы… с помощью самой программы. Логика такая: в целом, исполнение процесса полностью детерминировано, за исключением отдельных «возмущений»: Насколько я понимаю, работает это как-то так: все потоки всего записываемого дерева процессов запихиваются в один поток, сводя «описание многопоточности» к количеству инструкций между переключениями контекстов. Из этого следует один недостаток. Когда-то FSF ругался на монополию «архитектуры Wintel», так вот: rr запустится только на «Lintel», то есть только на Linux, и только на свежих (Nehalem и выше) интеловских процессорах, у которых достаточно стабильны performance counter-ы. Были попытки запустить на Ryzen, что-то даже удалось, но есть огрехи. На счёт других ядер ОС — не знаю. С rr вы вначале штатно прогоняете программу, прокликиваете кнопки / вводите команды, всё это с торможением порядка полутора раз, а то и меньше. А потом вы набираете rr replay, и программа начинает исполняться «в виртуальной реальности», в которой всё происходит в точности так же, как при записи. Естественно, окон и всего такого вы не увидите — stdout и stderr сохранены — и на том спасибо. Вместо этого вы увидите gdb (говорят, поддерживаются и графические отладчики вроде QtCreator и CLion, но я пока не пробовал). Зато, если вы случайно ввели n (next), а потом поняли, что не надо было этого делать, вы можете сказать rn (reverse-next) или поставить breakpoint и сказать rc (reverse-continue). Насколько я понимаю, reverse-команды штатно присутствуют в gdb, просто по очевидным причинам редко какой бекенд способен поддержать такое. Особый шик, на мой взгляд, — искать таким способом причину неисправности: вот вы нажали c, и программа остановилась на assertion-е. Поизучав состояние переменных с помощью print вы поняли, какая переменная имеет некорректное значение. Но кто её выставил? Вы просто говорите watch -l <переменная> (вечно путаю синтаксис, надеюсь, не в этот раз), выставляя аппаратную точку останова на запись в память, после чего говорите rc (reverse-continue) — намылить, смыть, повторить... Только не забывайте удалять старые hardware watchpoints по памяти, поскольку их не так уж и много. Во первых, что делать, если хочется отлаживать операционную систему на виртуальной машине? Сам я с таким пока не сталкивался, но, говорят, у QEMU есть gdbserver и вообще. А если хочется того же, что и в rr, но для QEMU? В документации упоминается встроенный record/replay: Кстати, первая версия патчей разработана, если верить той же странице, Институтом Системного Программирования РАН. Хорошо, а если отлаживать хочется один процесс, но на Java? Для GDB имеется HotSpot unwinder. По подобному запросу находится такая страница, где рассказывается, что оно со знанием дела ковыряется в адресном пространстве JVM, предоставляя для команды bt информацию. Обнаружил я это случайно, подключившись к процессу JVM и запросив backtrace. Увы, при подготовке этой статьи на Ubuntu 18.10, unwinder стабильно рушил gdb, но инструмент может, наверное, когда его починят, в чём-то помочь и в режиме rr. Однажды при отладке я столкнулся с ошибкой No space left on device. Казалось бы, обычное дело, у меня постоянно место заканчивается из-за артефактов компиляции. Но тут было нечто странное — df -h показывал по нескольку свободных гигабайтов на каждом разделе, причём, насколько я знаю, это уже с учётом зарезервированного под пользователя root места. Беглый гуглёж привёл меня к совету посмотреть вывод df -i, и действительно, на нужном разделе не было свободных inodes, то есть файлы были мелкие, но много. Думаю, многие знают об strace, а если нет — это часто весьма полезная утилита, показывающая, какие системные вызовы делает некий процесс или целое дерево процессов. Как вы видите, она даже декодирует входные и выходные данные (а вообще, это настраивается). Наверное, меньшее количество народа знает об ltrace — аналогичном инструменте для анализа вызовов между динамическими объектами (программой и библиотеками). Увы, похоже, что разработка ltrace заброшена, что казалось бы, странно, учитывая что инструмент полезный и вообще должен бы управляться той же командой разработчиков, что пилит загрузчик из libc. В таких случаях принято говорить «Вы не поверите...» :) Оказывается, в свежих версиях glibc есть инструмент sotruss. Конфигурируемость у него поменьше, правда, зато актуальный и в glibc. Есть и ложка дёгтя: ни ltrace, ни sotruss не декодируют параметры вызовов, что и неудивительно — нет исчерпывающего списка того, какие вызовы они должны декодировать, тут уже нужна сторонняя метаинформация. Может, её и можно как-то подложить, но, честно говоря, особо не приходилось пользоваться трассировкой библиотек. Однако знать о таких инструментах, думаю, полезно. Загадка: найти time или gettimeofday.
Решение: перезагрузиться с опцией ядра vdso=0 и поискать снова. Дело в том, что некоторые вызовы, такие как gettimeofday, считаются часто вызываемыми и при этом не требующими полноценного переключения в ядро. Поэтому наверху пользовательского адресного пространства ядром подкладывается псевдо-библиотека vDSO, предоставляющая быстрый интерфейс к некоторым системным вызовам. Кстати, там же написано, что seccomp их тоже, скорее всего, не увидит. Знете ли вы... Программист