bmstu-iu9 / refal-5-lambda

Компилятор Рефала-5λ
https://bmstu-iu9.github.io/refal-5-lambda
Other
78 stars 35 forks source link

`strip` портит exe-шники #364

Open Mazdaywik opened 2 years ago

Mazdaywik commented 2 years ago

Проблема

В пулл реквесте #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):

Описанные выше преимущества хотелось бы сохранить. Особенно второе — независимость уже собранного компилятора Рефала-5λ от наличия компилятора C++ и третье — автономность исполнимых файлов.

Частные решения

Добавление опции --strip к rlc

Можно добавить опции --strip и --strip-command=… к компилятору. Вторая опция задаёт имя утилиты strip (непустое на unix-like или при использовании MinGW GCC, пустое в остальных случаях), первая включает эту команду после создания префикса и до записывания интерпретируемого кода (если --strip-command=… пусто, то ничего не делает).

Так можно стрипать исполнимые файлы в процессе сборки.

Решение является частным, т.к. не решает проблему с вызовом внешней strip. В частности, решение ничего не даёт при использовании внешнего сценария вроде создания пакета пакетным менеджером (сценарий из #363).

Добавление утилиты rl-strip

Утилита отделяет префикс от интерпретируемого кода, стрипает его, возвращает интерпретируемый код на место.

Решение является частным по той же причине, что и предыдущее.

Нормальное решение

Пока не знаю.

cab404 commented 2 years ago

А можно ли использовать секции, а не оффсеты? Это бы позволило в strip указывать секции, которые стрипать нельзя.

Mazdaywik commented 2 years ago

Можно, но сложно. Для того, чтобы добавить новую секцию (в данном случае, секцию с байткодом) в исполнимый файл, нужно этот файл распарсить, вставить секцию и поправить заголовок (вписать туда новую секцию, поправить смещения остальных, если надо). Фактически, самим написать strip наоборот.

У решения есть преимущества:

Т.е. те свойства, которые нужно сохранить, сохранятся.

У этого решения мне видится два недостатка:

Второй недостаток можно купировать поддержкой и старого, и нового механизма одновременно: если формат файлов «strip’у наоборот» известен, то в него втыкается новая секция, если неизвестен, вставляемый код тупо приписывается в конец. На рантайм это никак не повлияет, если он по-прежнему будет просто искать сигнатуру внутри файла.

Отчасти, это же помогает бороться и с первым недостатком — можно добавлять поддержку новых форматов инкрементно.

Спасибо, @cab404, за интересное предложение. Я подумаю над этим.

STrusov commented 2 years ago

А это действительно проблема? Когда писал ebuild для Gentoo, просто добавил RESTRICT="strip"

Экономить место на накопителе? Мне для хорошей вещи не жалко, а тем более на фоне остальной гигантомании. А если считать байты, то приклеивание к интерпретируемому коду (байткоду) интерпретоатора это уже трата места (поскольку приводит к дублированию интерпрататоров). Насколько понимаю Unix-way, интерпретатор следует вызываеть через хэш-банг в начале файла с байткодом. В частности, так реализовано в OCaml.

cab404 commented 2 years ago

Можно, но сложно. Для того, чтобы добавить новую секцию (в данном случае, секцию с байткодом) в исполнимый файл, нужно этот файл распарсить, вставить секцию и поправить заголовок (вписать туда новую секцию, поправить смещения остальных, если надо). Фактически, самим написать 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

Mazdaywik commented 2 years ago

Это не совсем правда. Достаточно написать скрипт для линкера, и сказать ему, что нужно положить и в какую секцию.

🤦‍♂️ Скрипты для линкеров Microsoft Visual C++, Borland C++ Compiler 5.5.1 и Open Watcom в студию. 🤦‍♂️ Компилятор Рефала-5λ должен работать на машинах, где вообще не установлен никакой компилятор C++.

Насколько понимаю Unix-way, интерпретатор следует вызываеть через хэш-банг в начале файла с байткодом. В частности, так реализовано в OCaml.

Я в первую очередь ориентируюсь на Windows, а Windows хэш-банги не умеет. Кроме того, хэш-банг делает интерпретируемые файлы неавтономными, а у меня одна из целей — автономность. Экзешник работает на любой машине, не требует рантайма, интерпретатора и прочих зависимостей.

А это действительно проблема? Когда писал ebuild для Gentoo, просто добавил RESTRICT="strip"

Проблема в том, что есть сценарии использования, в которых к моим exe-шникам применяют strip. И эти сценарии мне тоже хочется поддерживать.

STrusov commented 2 years ago

Я в первую очередь ориентируюсь на 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"
Mazdaywik commented 2 years ago

Я в первую очередь ориентируюсь на Windows, а Windows хэш-банги не умеет.

Так там и проблемы нет, значит и предпринимать ничего не надо …

Проблема на Windows тоже есть. Вместо strip может быть и упаковщик, и шифровщик (правда, я не уверен, что они широко используются), и редактор ресурсов. Например, если окажется, что замена иконки редактором ресурсов приводит, как и strip, к обрезанию хвоста-байткода, то мне это не нравится.

… (с другой стороны, интерпретатор мог бы получать имя файла с байткодом в параметрах - вдруг такая возможность окажется интересной).

Есть возможность скомпилировать программу в чистый баткод и выполнить его интерпретатором rlgo, если Вы об этом. Но такой байткод не автономный: либо на машине должен быть установлен интерпретатор, либо интерпретатор нужно распространять в комплекте с байткодом.

Экзешник работает на любой машине, не требует рантайма, интерпретатора и прочих зависимостей.

Боюсь, что зависимости имеются:

Да, я помню, что у меня при использовании g++ GNU’сный рантайм компилируется динамически. Нужно там какую-то опцию командной строки добавить, чтобы библиотеки линковались статически. Но это для меня на данный момент второстепенный вопрос.

Дистрибутив для Windows (setup.exe, который) я собираю при помощи BCC, у него исполнимые файлы получаются автономными.

Unix-подобные системы (Linux и macOS) у меня пока развиваются по остаточному принципу.

Проблема в том, что есть сценарии использования, в которых к моим exe-шникам применяют strip. И эти сценарии мне тоже хочется поддерживать.

Имеется ввиду, установка пакета в NixOS? …

Имеются ввиду редакторы ресурсов и другие подобные утилиты, см. выше.

STrusov commented 2 years ago

Вместо 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

Mazdaywik commented 2 years ago

Теперь знаю, что эта штука называется оверлеем. Спасибо!

Но упаковщик может испортить байткод в том смысле, что смещение, с которого он начинается, может оказаться не равным 4096, впрочем, эта проблема решаемая.

Выходит, что только strip не поддерживает оверлеи.

Но в любом случае, добавление своей секции в исполнимый файл (PE EXE, ELF и MachO) — задача для меня интересная и в перспективе я её решу.