Археология исходников

Археология исходников

Автор: Виктор Шепелев

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

Предпосылки старения

Что такое вообще "археология исходников" и зачем она нужна? Примем в качестве первого приближения, что это анализ исходного кода некоторой устаревшей системы (а может быть, программы или библиотеки) с целью понять ее логику – и впоследствии эту логику изменить, воспроизвести в другой системе или встроить данную систему в новую и т. п. «Археологией» приходится заниматься и в тех случаях, когда знаковая система анализируемых исходников нам незнакома – то есть, «двигаясь» по тексту, написанному на "незнакомом языке", необходимо этот «язык» изучить.

Я не случайно взял «язык» в кавычки. Дело в том, что обычное представление о «древнем» коде, как о коде на языке программирования вроде Fortran или Cobol, – крайне упрощенное. Как раз по Фортрану вполне можно найти учебник или справочник. Здесь нет необходимости восстанавливать его правила только по исходному тексту программы. Проблема в том, что любой достаточно объемный кусок кода, помимо «основного» языка программирования, содержит и использует множество «подъязыков» и знаковых систем. Это служебные процедуры и классы, это используемые библиотеки и API операционной системы, это различные паттерны и встроенные алгоритмы. Единственная строчка кода может породить множество вопросов и потребовать многих дней сосредоточенного анализа для выяснения смысла и логики.

Таким образом, понятие «древности» кода (что есть, по сути, характеристика его непрозрачности для чтения и изменения в силу «забытости» правил и логики его написания) определяется не только и не столько языком программирования. Код может использовать устаревшие (а некогда общедоступные) библиотеки, даже документацию по которым сегодня найти весьма затруднительно. Код может быть написан для устаревшей ОС и широко использовать ее сервисы (например, для чтения/записи файлов, работы с сетевыми или графическими примитивами); или для устаревшего «железа» под специфику его процессора, памяти и портов. И, наконец, код может быть "устаревшим условно" – например, в нашей организации решили отказаться от библиотеки Х, так как не осталось людей, в ней разбирающихся, а кое-какой код, ее использующий, все еще актуален и активно применяется. При этом Х может быть вполне современной библиотекой, но в контексте нашей конторы исходники, ее использующие, – "древние".

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

Совместная работа нескольких авторов (или группы разработчиков) еще больше осложняет задачу – за счет «интерференции» понимания происходящего в проекте (то есть все тех же знаковых систем, на которые разные участники могут смотреть немного по-разному).

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

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

Технологии производства окаменелостей

Читатель, знакомый с различными методологиями разработки, может заметить, что «недревнеющий» код, код постоянно обновляемый и частично переписываемый, напоминает о гибких agile-методологиях, с их непрерывной интеграцией и постоянным рефакторингом. Кроме того, мы знаем, что "код, не покрытый тестами, не существует".

Противопоставить этому можно многоступенчатые крайне формализованные процессы типа RUP (Rational Unified Process), где все аспекты работы проекта единожды специфицированы, документированы и изменения в «стерильный» код вносятся крайне неохотно. В нашей «археологической» метафоре такие методологии разработки, кажется, самой природой предназначены сразу производить «окаменелости» – монолитно, надежно, монументально, сто лет простоит. Но вот любое исследование или внесение изменений…

Куда копать?

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

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

Кстати говоря, чтение чужого хорошо написанного кода является неплохим подспорьем для самообучения (в том числе – обучения собственно искусству чтения).

Продолжим. Для анализа чужого кода, особенно кода крупного проекта, "с высоты птичьего полета" (построение ментальной модели, выяснение основных знаковых систем) исходники принято сводить к набору "основных сущностей" [Мы здесь не останавливаемся на том, что "объективные знаковые системы" – использованные языки программирования, API и библиотеки – все же придется изучить. Впрочем, для некоторых частных случаев, наиболее востребованных, существуют автоматизированные средства перевода с одного языка на другой (например, Cobol->Java)]. Для популярных языков программирования, в «обиходе» которых существует множество инструментов анализа и проектирования, это может выглядеть как автоматизированное построение диаграмм классов (иерархий наследования и включения и т. п.) или функций и процедур (иерархий вызовов). Здесь еще может помочь спецификация или другая документация на проект (на более низких уровнях, как правило, любая документация малорелевантна коду).

Как только мы спускаемся уровнем ниже "общего плана" – до отдельных строк кода, параметров функций и времени жизни переменных [Знаменитый теоретик computer sciense Гради Буч называет это "археологией при помощи зубной щетки"], – никакого другого метода понять и разобраться, кроме чтения строки за строкой, не остается.

Более того, и чтение кода без готовых, заранее поставленных вопросов – то есть просто "просмотр для галочки" – практически бесполезно. Самый же эффективный метод исследования – «деятельное» чтение, то есть участие в работе исследуемых исходников. В зависимости от свойств исследуемых кусков кода и окружающей среды "деятельное чтение" может означать, например, многократное выполнение «древней» программы с постепенным изменением различных частей ее кода; или прогонку в отладчике, с отслеживанием значений разных переменных; или «заимствование» непонятного куска в «чистую» среду (программу-заглушку) для выяснения принципов работы этого куска. Здесь же могут помочь модульные тесты (специальный код, тестирующий различные аспекты функциональности) – если они прилагались к исходникам, это куда полезнее любой спецификации; если же нет – могут быть написаны с нуля как один из видов деятельности в процессе исследования.

Придирчивый читатель мог бы заметить, что автор упускает из виду "объективно древние" исходники, которые по той или иной причине – нет старого компилятора, нет старой ОС, нет старой библиотеки – просто не могут быть собраны и запущены. Автор не упускает. В этом случае "деятельное чтение" просто становится сложнее – приходится изучать "древнюю драгоценность" почти вслепую; в простейшем случае (чтение интереса ради) – «запуская» код в уме; в более сложном (необходимо таки заставить старую библиотеку работать; или переписать старый алгоритм на новом языке) – по кусочку переводя проект "на новые рельсы" и тщательно анализируя результат.

Короче говоря, практически единственный честный и полноценный способ исследования "археологических древностей" – полное переписывание; не обязательно «честное», оно может выглядеть просто как удаление частей исходника и постепенное их возвращение на место – тем не менее других вариантов "качественного ознакомления" с артефактом человечество пока не придумало [Другой вопрос, многие ли «древности» стоят таких трудов?].

Но самое смешное, что когда «старый» код [Или извлеченные из него идеи] станет «новым», твоим, родным, частью проекта, – он неизбежно начнет стареть, чуть только отведешь глаза, перестанешь обновлять и пересматривать эти части исходников. Остается лишь повторять, как и все, вслед за Черной Королевой: "Здесь, знаешь ли, приходится бежать со всех ног, чтобы только остаться на том же месте!"

Примеры и иллюстрации

Пример довольно простого кода, который для понимания может потребовать длительных изысканий, – быстрый алгоритм нахождения обратного квадрата числа (1/яx), приписываемый разработчику Quake Джону Кармаку. Алгоритм использует метод быстрых приближений Ньютона и включает следующую «очевидную» строчку с использованием магического числа (сопутствующий ей комментарий, как правило, встречается во всех популярных описаниях алгоритма):

i = 0x5f3759df – (i >> 1); // what the f..k?

Пример переписывания «мнимо древнего» кода – одно из первых в истории веб-приложений бизнес-класса, Viaweb. Оно было написано знаменитыми хакерами Полом Грэмом и Робертом Моррисом на Lisp и через некоторое время куплено Yahoo и переименовано в Yahoo!Store. Впоследствии Yahoo, озабоченная экзотичностью Lisp’а, заставила своих программистов провести «археологические изыскания» для переписывания приложения с нуля на Perl и C++. По слухам, код современного Yahoo!Store очень похож на интерпретатор Lisp (иллюстрируя тем самым 10-е правило Гринспуна).

Пример проблемы объективной древности – Strongtalk, эффективная реализация языка Smalltalk с возможностью статической типизации была некогда куплена Sun, а через много лет возвращена программистскому сообществу как open source. Естественно, все исходники виртуальной машины (языки – C и ассемблер) требовали древних версий компиляторов. И если C-часть была «переведена» на современный диалект языка довольно быстро, то ассемблерные куски до сих пор требуют Borland TASM 4.0 (еще DOS’овская версия), поэтому приходится распространять как исходный код, так и скомпилированные «энтузиастами-археологами» файлы.

Пример «археологических изысканий ради понимания алгоритма» – мы знаем, что формат файла Microsoft Word как таковой существует. Спецификация на него известна и даже доступна публично (какой-то из антимонопольных процессов потребовал этого от корпорации), но ее стиль и степень подробности не позволяют «просто написать соответствующий код» – многие нюансы требуют экспериментирования. Поэтому гораздо более полезный справочник по форматам Microsoft – опенсорсные библиотеки для чтения этих форматов (например, antiword или Apache POI, а лучше – «перекрестный допрос» обеих библиотек, дабы лучше понять, что является особенностью формата, а что особенностью библиотеки).

Пример ситуации, где старение сильнее автора кода, – программа Sketchpad, созданная в 1963 году. Она была во многом революционной, еще бы – первая интерактивная CAD, первая объектно-ориентированная система… Но несмотря на то, что автор программы по-прежнему жив и в здравом уме, сомнительно, что сегодняшний вице-президент Sun Айвен Сазерленд смог бы подробно объяснить логику исходников, некогда написанных аспирантом Айвеном Сазерлендом на макроассемблере машины TX-2.