refal-5-lambda
refal-5-lambda copied to clipboard
`strip` портит exe-шники
Проблема
В пулл реквесте #363 @cab404 обнаружил, что утилита strip
на Linux портит исполнимые файлы. Pull request посвящён создания пакета Рефала-5λ в пакетном менеджере Nix.
Действительно, исполнимый файл состоит из интерпретатора и приписанного к нему исполняемого кода, причём интерпретируемый код должен начинаться со смещения, кратного 4096
, т.е. устройство подобно устройству SFX-архива.
Команда strip
не подозревает об этой структуре исполнимого файла и из префикса-интерпретатора не только удаляет секции с отладочной информацией, но и просто дропает весь хвост с интерпретируемым кодом.
Обрезание осуществляется в процессе подготовки пакета соответствующими утилитами.
Обходной путь
@cab404 добавил в скрипт сборки опцию, запрещающую strip
’ать исполнимые файлы:
https://github.com/bmstu-iu9/refal-5-lambda/compare/fa1f8f862a73e7ca6194f67cd146ca158919d70e..ad842a440995f997cc04a014a46daa7c1922bbac
https://github.com/bmstu-iu9/refal-5-lambda/blob/ad842a440995f997cc04a014a46daa7c1922bbac/flake.nix#L17-L19
Требования к решению и другие размышления
Ожидаемое поведение должно быть таким:
$ rlc program.ref
* Compiling program.ref:
** Compilation suceeded **
$ ./program
Hello!
$ strip program
$ ./program
Hello!
Если реализовать новое API для нативных функций как #324, то сборка в режиме прямой кодогенерации (rlc --scratch -Od program.ref
) обеспечит ожидаемое поведение, т.к. в этом режиме программы не будут содержать интерпретируемого кода. Это неплохо, но это не решает проблему программ, скомпилированных в режиме интерпретации.
Актуальная реализация режима интерпретации (префикс-интерпретатор + интерпретируемый код) имеет следующие преимущества (см. также #48):
-
Она почти одинаково работает на всех поддерживаемых платформах (Windows, Linux, macOS):
- Компиляция:
- вызываем компилятор C++ (можно встроить почти любой),
- к полученному exe’шнику добавляем в конец байты выравнивания для кратности 4096,
- приписываем исполнимый код,
- вызываем команду, заданную в параметре командной строки
--chmod-x-command
.¹
- Запуск:
- узнаём имя исполнимого файла обращением к ОС,²
- открываем файл при помощи стандартного
fopen()
, ищем в нём сигнатуру и загружаем интерпретируемый код.
Есть только две платформоспецифичности:
- ¹
--chmod-x-command
устанавливается скриптом на Bash вchmod +x
, bat-скриптом в пустую строку. - ² Вызывать
fopen(argv[0], "rb")
нельзя, т.к.argv[0]
на unix-подобных ОС просто содержит нулевой аргумент: если программа лежит вPATH
, то вargv[0]
будет лежать имя программы без пути; при запуске под отладчикомargv[0]
может быть и пустой строкой. Поэтому приходится вызывать ту или иную API-функцию (абстрагированную какrefalrts::api::get_main_module_name()
) для определения пути к запущенному файлу.
- Компиляция:
-
Компиляция в режиме интерпретации не требует компилятора C++. Дистрибутив может содержать заранее скомпилированные интерпретаторы-префиксы, к которым Рефал-5λ приписывает интерпретируемый код.
-
Исполнимые файлы полностью автономные — не требуют наличия на машине ни интерпретатора, ни рантайма.
-
И при этом реализация относительно проста.
Описанные выше преимущества хотелось бы сохранить. Особенно второе — независимость уже собранного компилятора Рефала-5λ от наличия компилятора C++ и третье — автономность исполнимых файлов.
Частные решения
Добавление опции --strip
к rlc
Можно добавить опции --strip
и --strip-command=…
к компилятору. Вторая опция задаёт имя утилиты strip
(непустое на unix-like или при использовании MinGW GCC, пустое в остальных случаях), первая включает эту команду после создания префикса и до записывания интерпретируемого кода (если --strip-command=…
пусто, то ничего не делает).
Так можно стрипать исполнимые файлы в процессе сборки.
Решение является частным, т.к. не решает проблему с вызовом внешней strip
. В частности, решение ничего не даёт при использовании внешнего сценария вроде создания пакета пакетным менеджером (сценарий из #363).
Добавление утилиты rl-strip
Утилита отделяет префикс от интерпретируемого кода, стрипает его, возвращает интерпретируемый код на место.
Решение является частным по той же причине, что и предыдущее.
Нормальное решение
Пока не знаю.
А можно ли использовать секции, а не оффсеты? Это бы позволило в strip указывать секции, которые стрипать нельзя.
Можно, но сложно. Для того, чтобы добавить новую секцию (в данном случае, секцию с байткодом) в исполнимый файл, нужно этот файл распарсить, вставить секцию и поправить заголовок (вписать туда новую секцию, поправить смещения остальных, если надо). Фактически, самим написать strip
наоборот.
У решения есть преимущества:
- Это решит проблему со
strip
. - Сохранит независимость Рефала-5λ от компилятора C++ при компиляции в режиме интерпретации.
- Скомпилированные файлы останутся автономными.
Т.е. те свойства, которые нужно сохранить, сохранятся.
У этого решения мне видится два недостатка:
- Трудоёмкость. Нужно будет поддержать форматы исполнимых файлов поддерживаемых ОС:
- Windows: PE x86,
- Windows: PE x64,
- Linux: ELF x86,
- Linux: ELF x64,
- macOS: MachO x64,
- (macOS: MachO x86 — не уверен, что нужно поддерживать).
- Трудность переноса — перенести на какую-нибудь новую ОС, скажем, QNX, будет сложнее. Если сейчас достаточно написать функцию
refalrts::api::get_main_module_name()
, то в будущем потребуется изучить формат исполнимых файлов новой ОС.
Второй недостаток можно купировать поддержкой и старого, и нового механизма одновременно: если формат файлов «strip
’у наоборот» известен, то в него втыкается новая секция, если неизвестен, вставляемый код тупо приписывается в конец. На рантайм это никак не повлияет, если он по-прежнему будет просто искать сигнатуру внутри файла.
Отчасти, это же помогает бороться и с первым недостатком — можно добавлять поддержку новых форматов инкрементно.
Спасибо, @cab404, за интересное предложение. Я подумаю над этим.
А это действительно проблема?
Когда писал ebuild для Gentoo, просто добавил
RESTRICT="strip"
Экономить место на накопителе? Мне для хорошей вещи не жалко, а тем более на фоне остальной гигантомании. А если считать байты, то приклеивание к интерпретируемому коду (байткоду) интерпретоатора это уже трата места (поскольку приводит к дублированию интерпрататоров). Насколько понимаю Unix-way, интерпретатор следует вызываеть через хэш-банг в начале файла с байткодом. В частности, так реализовано в OCaml.
Можно, но сложно. Для того, чтобы добавить новую секцию (в данном случае, секцию с байткодом) в исполнимый файл, нужно этот файл распарсить, вставить секцию и поправить заголовок (вписать туда новую секцию, поправить смещения остальных, если надо). Фактически, самим написать
strip
наоборот.
Это не совсем правда. Достаточно написать скрипт для линкера, и сказать ему, что нужно положить и в какую секцию.
https://home.cs.colorado.edu/~main/cs1300/doc/gnu/ld_3.html#SEC8
Так же есть атрибуты GCC:
https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#Common-Function-Attributes
Это не совсем правда. Достаточно написать скрипт для линкера, и сказать ему, что нужно положить и в какую секцию.
🤦♂️ Скрипты для линкеров Microsoft Visual C++, Borland C++ Compiler 5.5.1 и Open Watcom в студию. 🤦♂️ Компилятор Рефала-5λ должен работать на машинах, где вообще не установлен никакой компилятор C++.
Насколько понимаю Unix-way, интерпретатор следует вызываеть через хэш-банг в начале файла с байткодом. В частности, так реализовано в OCaml.
Я в первую очередь ориентируюсь на Windows, а Windows хэш-банги не умеет. Кроме того, хэш-банг делает интерпретируемые файлы неавтономными, а у меня одна из целей — автономность. Экзешник работает на любой машине, не требует рантайма, интерпретатора и прочих зависимостей.
А это действительно проблема? Когда писал ebuild для Gentoo, просто добавил
RESTRICT="strip"
Проблема в том, что есть сценарии использования, в которых к моим exe-шникам применяют strip
. И эти сценарии мне тоже хочется поддерживать.
Я в первую очередь ориентируюсь на Windows, а Windows хэш-банги не умеет.
Так там и проблемы нет, значит и предпринимать ничего не надо (с другой стороны, интерпретатор мог бы получать имя файла с байткодом в параметрах - вдруг такая возможность окажется интересной).
Экзешник работает на любой машине, не требует рантайма, интерпретатора и прочих зависимостей.
Боюсь, что зависимости имеются:
$ ldd /usr/lib/refal-5-lambda/bin/rlc-core
linux-vdso.so.1
libdl.so.2 => /lib64/libdl.so.2
libstdc++.so.6 => /usr/lib/gcc/x86_64-pc-linux-gnu/11.2.0/libstdc++.so.6
libm.so.6 => /lib64/libm.so.6
libgcc_s.so.1 => /usr/lib/gcc/x86_64-pc-linux-gnu/11.2.0/libgcc_s.so.1
libc.so.6 => /lib64/libc.so.6
/lib64/ld-linux-x86-64.so.2
Не во всех дистрибутивах Linux используется glibc и совместимая версия libstdc++. Как раз сборка интерпретатора под конкретный дистрибутив и обеспечит исполненние отдельного файла с интерпретируемым кодом. Если это кому-то действительно потребуется, и когда потребуется, тогда он сможет сделать поддержку хэш-бангов, не отвлекая автора.
Проблема в том, что есть сценарии использования, в которых к моим exe-шникам применяют
strip
. И эти сценарии мне тоже хочется поддерживать.
Имеется ввиду, установка пакета в NixOS? У меня аналогичный сценарий, но другой дистрибутив, не увидел проблемы. Пока рассмотрено два пакетных менеджера (Nix и Portage), оба позволяют заблокировать вызов strip. Это значит, что "проблеме" подвержено и другое ПО. Вот часть выдачи поверхностного поиска:
mail-client/thunderbird-bin/thunderbird-bin-78.14.0.ebuild:RESTRICT="strip"
www-client/firefox-bin/firefox-bin-93.0.ebuild:RESTRICT="strip"
dev-scheme/guile/guile-3.0.7.ebuild:RESTRICT="strip"
eclass/golang-base.eclass:RESTRICT="strip"
dev-lisp/gcl/gcl-2.6.10.ebuild:RESTRICT="strip"
dev-lang/julia-bin/julia-bin-1.5.2.ebuild:RESTRICT="strip"
dev-lang/fpc/fpc-3.2.2.ebuild:RESTRICT="strip"
Я в первую очередь ориентируюсь на Windows, а Windows хэш-банги не умеет.
Так там и проблемы нет, значит и предпринимать ничего не надо …
Проблема на Windows тоже есть. Вместо strip
может быть и упаковщик, и шифровщик (правда, я не уверен, что они широко используются), и редактор ресурсов. Например, если окажется, что замена иконки редактором ресурсов приводит, как и strip
, к обрезанию хвоста-байткода, то мне это не нравится.
… (с другой стороны, интерпретатор мог бы получать имя файла с байткодом в параметрах - вдруг такая возможность окажется интересной).
Есть возможность скомпилировать программу в чистый баткод и выполнить его интерпретатором rlgo
, если Вы об этом. Но такой байткод не автономный: либо на машине должен быть установлен интерпретатор, либо интерпретатор нужно распространять в комплекте с байткодом.
Экзешник работает на любой машине, не требует рантайма, интерпретатора и прочих зависимостей.
Боюсь, что зависимости имеются:
Да, я помню, что у меня при использовании g++
GNU’сный рантайм компилируется динамически. Нужно там какую-то опцию командной строки добавить, чтобы библиотеки линковались статически. Но это для меня на данный момент второстепенный вопрос.
Дистрибутив для Windows (setup.exe
, который) я собираю при помощи BCC, у него исполнимые файлы получаются автономными.
Unix-подобные системы (Linux и macOS) у меня пока развиваются по остаточному принципу.
Проблема в том, что есть сценарии использования, в которых к моим exe-шникам применяют
strip
. И эти сценарии мне тоже хочется поддерживать.Имеется ввиду, установка пакета в NixOS? …
Имеются ввиду редакторы ресурсов и другие подобные утилиты, см. выше.
Вместо
strip
может быть и упаковщик, и шифровщик (правда, я не уверен, что они широко используются), и редактор ресурсов. Например, если окажется, что замена иконки редактором ресурсов приводит, как иstrip
, к обрезанию хвоста-байткода, то мне это не нравится.
Корректная поддержка оверлея (хвоста-байткода) - это ответственность редактора ресурсов или упаковщика.
"UPX handles overlays like many other executable packers do: it simply copies the overlay after the compressed image. This works with some files, but doesn't work with others, depending on how an application actually accesses this overlayed data." https://github.com/upx/upx/blob/devel/doc/upx.pod#overlay-handling-options
Теперь знаю, что эта штука называется оверлеем. Спасибо!
Но упаковщик может испортить байткод в том смысле, что смещение, с которого он начинается, может оказаться не равным 4096, впрочем, эта проблема решаемая.
Выходит, что только strip
не поддерживает оверлеи.
Но в любом случае, добавление своей секции в исполнимый файл (PE EXE, ELF и MachO) — задача для меня интересная и в перспективе я её решу.