Собираем Perl прямиком из 1987 года

Прочитав новость «Код интерпретатора Perl официально перенесён на GitHub» на ресурсе LINUX.ORG.RU я решил взглянуть на репозиторий Perl 5, который теперь уже находится на GitHub’е.

Удивительно, как трепетно и качественно его перенесли, сохранив не только абсолютно всю 32-летнюю историю проекта, но и багрепорты (попали в Issues), патчи (попали в PRs), релизы и ветки. Надпись «32 years ago» рядом с файлами вызывает невольную улыбку.

Что ещё делать в этот унылейший пятничный вечер, когда на улице неприятно моросит дождь со снегом, а все уличные дорожки погрязли в осенней слякоти? Правильно, красноглазить! Так что я ради эксперимента и интереса решил взять и собрать древний Perl на современной x86_64-машинке с последней версией GCC 9.2.0 в качестве компилятора. Сможет ли такой старый код пройти проверку временем?



Демонстрация работы twm, одного из первых оконных менеджеров для X Window System, на современном дистрибутиве Arch Linux.

Чтобы было совсем уж аутентичненько и некромантненько, я развернул виртуальную машину с голыми иксами и оконным менеджером twm, который тоже родом из 1987 года. Кто знает, может быть Larry Wall писал свой Perl используя именно twm, так сказать bleeding edge technology того времени. Используемый дистрибутив — Arch Linux. Просто потому что в его репозитории есть некоторые полезные вещи, которые впоследствии мне пригодились. Итак, поехали!

Содержание:

1. Подготовка окружения
2. Конфигурирование исходного кода
3. Ошибки файла грамматики yacc
4. Ошибки компиляции кода на «C»
5. Исправление некоторых ошибок Segmentation fault
6. Подведение итогов и полезные ссылки

1. Подготовка окружения

Сперва устанавливаем на развёрнутую операционную систему в виртуальной машине весь необходимый для сборки и редактирования исходного кода джентльменский набор утилит и компиляторов: gcc, make, vim, git, gdb и т. д. Некоторые из них уже установлены, а другие доступны в мета-пакете base-devel, его нужно обязательно установить, если он не установлен. После того, как окружение готово к активным действиям, получаем копию исходного кода Perl 32-летней выдержки!

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

Мы скачиваем лишь небольшой объём данных и в итоге репозиторий с исходным кодом первой версии Perl занимает всего 150 КБ.

В то тёмное и дремучее время не было такой элементарной вещи, как autotools (счастье-то какое!), однако в корне репозитория имеется скрипт Configure. В чём же дело? А дело в том, что Larry Wall и является изобретателем подобных скриптов, которые позволяли сгенерировать Makefile’ы под самые разношёрстные UNIX-машины того времени. Как гласит одноимённая статья про эти скрипты в английской Википедии, Larry Wall ещё три года до написания Perl’а поставлял файл Configure с некоторым своим софтом, например, программой для чтения новостей rn. Впоследствии Perl не стал исключением и для его сборки использовался уже обкатанный на многих машинах скрипт. Позже эту идею подхватили и другие разработчики, например, программисты из компании Trolltech. Они использовали для конфигурации сборки своего фреймворка Qt похожий скрипт, который многие путают с configure из autotools. Именно зоопарк таких вот скриптов от разных разработчиков и послужил толчком к созданию средства для их упрощённой и автоматической генерации.

<< Перейти к содержанию

2. Конфигурирование исходного кода

Скрипт Configure «старой закалки», что уже видно по его Shebang‘у, в котором имеется пробел:

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

Хватит лирики, переходим к делу! Запускаем скрипт и видим интересное предположение, которое оказывается не совсем верным:

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

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

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

Туда можно прописать что-нибудь интересное, например, -m32 для сборки 32-битного исполняемого файла или библиотеку, которая требуется при линковке. На последний вопрос скрипта:

Я ответил положительно. Древняя утилита makedepend, судя по её страничке в Википедии, была создана в самом начале жизни проекта Athena, для облегчения работы с Makefile’ами. Этот проект подарил нам X Window System, Kerberos, Zephyr и повлиял на множество других привычных сегодня вещей. Всё это замечательно, но вот только откуда эта утилита в современном Linux-окружении? Она давно уже никем и нигде не используется. Но если посмотреть внимательно в корень репозитория, оказывается, что Larry Wall написал её скриптовую версию-заменитель, которую нам заботливо распаковал и выполнил конфигурационный скрипт.

Выполнение makedepend завершилось с некоторыми странными ошибками:

Возможно именно они повлекли за собой проблему из-за которой сгенерированные сборочные файлы Makefile оказались немного пожёванными:

Лезть в дебри замысловатой shell-лапши утилиты makedepend мне решительно не хотелось и я решил тщательно присмотреться к Makefile’ам, в которых выявилась странная закономерность:

Видимо какая-то утилита неправильно вставила свои аргументы в выхлоп. Взяв в руки топор утилиту sed я решил немного поправить это дело:

На удивление трюк сработал и Makefile’ы заработали как надо!

<< Перейти к содержанию

3. Ошибки файла грамматики yacc

Было бы просто невероятно, если бы 32-летний код взял и собрался без каких-либо проблем. К сожалению, чудес не бывает. Изучая дерево исходного кода я наткнулся на файл perl.y, представляющий собой описания грамматики для утилиты yacc, которая в современных дистрибутивах давно заменена на bison. Скрипт, находящийся по пути /usr/bin/yacc, просто вызывает bison в режиме совместимости c yacc. Вот только эта совместимость не является полной и при обработке этого файла сыпется огромная куча ошибок, исправлять которые я не умею да и не очень хочу, ведь есть альтернативное решение, о котором я узнал совсем недавно.

Буквально год или два назад Helio Chissini de Castro, являющийся разработчиком KDE, занимался похожей работой и адаптировал KDE 1, 2 и Qt 1, 2 под современные окружения и компиляторы. Я заинтересовался его работой, скачал исходные коды проектов, но при сборке наткнулся на подобный подводный камень из-за несовместимости yacc и bison, которые использовались для построения древней версии метакомпилятора moc. Впоследствии мне удалось найти решение этой проблемы в виде замены bison на утилиту byacc (Berkeley Yacc), которая оказалась совместимой со старыми грамматиками для yacc и была доступна во многих дистрибутивах Linux.

Простая замена yacc на byacc в системе сборке меня тогда выручила, правда ненадолго, поскольку чуть позже в новых версиях byacc всё-таки сломали совместимость с yacc, отломив отладку, связанную со сущностью yydebug. Поэтому пришлось немного исправлять грамматику утилиты.

Итак, стратегия исправления ошибок построения в файле perl.y была предсказана предыдущим опытом: устанавливаем утилиту byacc, меняем yacc на byacc во всех Makefile, затем вырезаем отовсюду yydebug. Эти действия решили все проблемы с этим файлом, ошибки исчезли и компиляция продолжилась.

<< Перейти к содержанию

4. Ошибки компиляции кода на «C»

Древний код Perl’а пестрил ужасами вроде давным-давно устаревшей и всеми забытой нотации определений функций вида K&R:

Подобные особенности встречались, например, в коде Microsoft Word 1.1a, который тоже достаточно древний. Первый стандарт языка программирования «C», под названием «C89» появится лишь через два года. Современные компиляторы умеют работать с таким кодом, а вот некоторые IDE не осиливают разбирать подобные определения и подсвечивают их как синтаксические ошибки, например, раньше таким грешил Qt Creator до того, как парсинг кода в нём перевели на библиотеку libclang.

Компилятор GCC 9.2.0, изрыгая огромное количество предупреждений, взялся компилировать древний код первой версии Perl. Простыни из предупреждений были настолько велики, что для того чтобы добраться до ошибки, приходилось пролистывать несколько страниц выхлопа вверх. На моё удивление, большинство ошибок компиляции были типовыми и в основном связанными с предопределёнными дефайнами, которые играли роль флажков для сборки.



Работа современного компилятора GCC 9.2.0 и отладчика GDB 8.3.1 в оконном менеджере twm и эмуляторе терминала xterm.

Под дефайном STDSTDIO Larry Wall экспериментировал с какой-то древней и нестандартной библиотекой языка программирования «C», а под дефайном DEBUGGING была отладочная информация с пресловутым yydebug, про который я упоминал выше. По умолчанию эти флажки были включены. Выключив их в файле perl.h и добавив несколько забытых дефайнов мне удалось значительно уменьшить количество ошибок.

Другой тип ошибок — переопределения ныне стандартизированных функций стандартной библиотеки и слоя POSIX. В проекте имеется свой malloc(), setenv() и другие сущности, которые создавали конфликты.

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

На моё удивление патч для кода 32-летней давности получился настолько крошечным, что его можно целиком привести здесь:

Отличный результат для 32-летнего кода! Ошибка линковки undefined reference to `crypt' была исправлена добавлением в Makefile директивы -lcrypt соответствующей библиотеки libcrypt, после чего я наконец-то получил вожделенный исполняемый файл интерпретатора Perl:

<< Перейти к содержанию

5. Исправление некоторых ошибок Segmentation fault

После практически беспроблемной компиляции удача отвернулась от меня. Сразу после запуска собранного интерпретатора Perl’а я получил несколько странных ошибок и Segmentation fault в конце:

Грепнув исходный текст по фразе Corrupt malloc, оказалось, что вместо системного malloc() вызывается какой-то кастомный аллокатор родом из 1982 года. Интересно, что в одном из строковых литералов в его исходном коде прописано Berkeley, а в комментарии рядом — Caltech. Сотрудничество между этими университетами видимо тогда было очень сильно. В общем, я закомментировал этот хакерский аллокатор и пересобрал исходный код. Ошибки порчи памяти исчезли, а Segmentation fault остался. Значит дело было не в этом и теперь нужно расчехлять отладчик.

Запустив программу под gdb я обнаружил что падение происходит при вызове функции создания временного файла mktemp() из libc:

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

Первая мысль, которая наверняка вам тоже пришла в голову — заменить небезопасную функцию mktemp() на mkstemp(), что я именно и сделал. Предупреждение линковщика исчезло, но Segmentation fault в этом месте всё равно остался, только теперь он был в функции mkstemp().

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

Получается mktemp() пытается поменять литерал по маске, который находится в секции .rodata, что заведомо обречено на провал. Или всё-таки 32 года назад подобное было допустимо, встречалось в коде и даже как-то работало?

Конечно, замена char *e_tmpname на char e_tmpname[] исправила этот Segmentation fault и я смог получить то, на что убил целый вечер:

Исполнение из командной строки мы проверили, а что насчёт файла? Я скачал первый попавшийся «Hello World» для языка программирования Perl из интернета:

Затем я попробовал его запустить, но, увы, меня снова ждал Segmentation fault. На этот раз совершенно в другом месте:

В функции yyerror() обнаружился следующий интересный момент, привожу оригинальный сниппет:

Снова ситуация похожа на ту, про которую я писал выше. Снова модифицируются данные в секции .rodata. Может быть это просто опечатки из-за Copy-Paste и вместо tname хотели написать tmpbuf? Или же действительно за подобным стоит какой-то скрытый смысл? В любом случае, замена char *tokename[] на char tokename[][32] убирает ошибку Segmentation fault и Perl говорит нам следующее:

Получается, не нравятся ему всякие новомодные use strict, вот что он нам оказывается пытался сказать! Если удалить или закомментировать эти строчки в файле, то программа запускается:

<< Перейти к содержанию

6. Подведение итогов и полезные ссылки

Фактически я добился своей цели и заставил древний код родом из 1987 года не только компилироваться, но и работать в современном Linux-окружении. Несомненно, там ещё осталась большая куча различных ошибок Segmentation faults, возможно связанных с размером указателя на 64-битной архитектуре. Всё это можно вычистить посидев несколько вечерков с отладчиком наперевес. Вот только это не слишком приятное и довольно нудное занятие. Ведь изначально этот эксперимент планировался как развлечение на скучный вечер, а не как полноценная работа, которая будет доведена до конца. Имеется ли какая-нибудь практическая польза от проведённых действий? Может быть когда-нибудь какой-нибудь цифровой археолог наткнётся на эту статью и она ему будет полезна. Но в реальном мире даже опыт, извлечённый из подобных изысканий, по моему мнению, не слишком уж ценен.

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

Некоторые полезные ссылки:

  1. Официальный GitHub-репозиторий Perl 5 с 32-летней историей.
  2. Публикация этой статьи на ресурсе Habr, от меня.
  3. Публикация этой статьи на ресурсе LINUX.ORG.RU, от меня.

P.S. Спешу огорчить любителей деструктивных однострочников, здесь подобное не работает. Возможно версия Perl’а слишком уж старая для таких развлечений.
P.P.S. Всем добра и приятных выходных. Спасибо пользователю kawaii_neko за небольшое исправление.

Update 28-Oct-2019: Пользователь форума LINUX.ORG.RU, использующий ник utf8nowhere, привёл в своём комментарии к этой статье довольно интересные ссылки, информация из которых не только проясняет ситуацию с изменяемыми строковыми литералами, но и даже рассматривает описанную выше проблему использования функции mktemp()! Позволю себе процитировать эти источники, в которых рассказано про различные несовместимости между нестандартизированным «K&R C» и «GNU C»:

Incompatibilities of GCC
There are several noteworthy incompatibilities between GNU C and K&R (non-ISO) versions of C.

GCC normally makes string constants read-only. If several identical-looking string constants are used, GCC stores only one copy of the string.
One consequence is that you cannot call mktemp with a string constant argument. The function mktemp always alters the string its argument points to.

Another consequence is that sscanf does not work on some systems when passed a string constant as its format control string or input. This is because sscanf incorrectly tries to write into the string constant. Likewise fscanf and scanf.

The best solution to these problems is to change the program to use char-array variables with initialization strings for these purposes instead of string constants. But if this is not possible, you can use the -fwritable-strings flag, which directs GCC to handle string constants the same way most C compilers do.

Источник: Using the GNU Compiler Collection (GCC 3.3) Official Manual.

Флаг компилятора -fwritable-strings был объявлен устаревшим в GCC 3.4 и окончательно удалён в GCC 4.0.

ANSI C rationale | String literals
String literals are specified to be unmodifiable. This specification allows implementations to share copies of strings with identical text, to place string literals in read-only memory, and perform certain optimizations. However, string literals do not have the type array of const char, in order to avoid the problems of pointer type checking, particularly with library functions, since assigning a pointer to const char to a plain pointer to char is not valid. Those members of the Committee who insisted that string literals should be modifiable were content to have this practice designated a common extension (see F.5.5).

Existing code which modifies string literals can be made strictly conforming by replacing the string literal with an initialized static character array. For instance,

can be changed to:

Источник: Rationale for American National Standard for Information Systems, Programming Language C.

Пользователь VarfolomeyKote4ka предложил интересный грязненький хак, который позволяет обойти ошибки Segmentation faults при попытке изменения данных в секции .rodata путём преобразования её в секцию .rwdata. В интернете не так давно появилась очень интересная статья «From .rodata to .rwdata – introduction to memory mapping and LD scripts» за авторством программиста guye1296, как раз рассказывающая о том как провернуть подобный трюк. Для облегчения получения требуемого результата автор статьи подготовил довольно объёмный скрипт для стандартного линковщика ldrwdata.ld. Достаточно скачать этот скрипт, положить его в корень директории с исходным кодом Perl, подправить флажок LDFLAGS следующим образом: LDFLAGS = -T rwdata.ld, затем пересобрать проект. В итоге имеем следующее:

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

<< Перейти к содержанию

Manuals, Others

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *