В качестве упражнения я пишу игрушечный malloc(3), загруженный LD_PRELOAD
. У меня есть функция с аннотацией __attribute__((destructor))
для вывода списка распределений и их статуса при выходе в целях отладки, но я обнаружил, что в некоторых случаях она не работает. В частности, он работает с локально скомпилированным кодом, но не с системными двоичными файлами, такими как /bin/ls
(в Arch Linux). Однако функция с тегом constructor
работает в обоих случаях.
Простое воспроизведение проблемы:
main.c
:
#include <stdio.h>
// compile with: clang -o main main.c
int main() {
printf("main\n");
}
wrap.c
:
#include <stdio.h>
// compile with: clang -o wrap -shared -fPIC wrap.c
void __attribute__((constructor)) say_hi() {
printf("hi y'all\n");
}
void __attribute__((destructor)) say_bye() {
printf("bye y'all\n");
}
Деструктор работает с main.c
:
$ LD_PRELOAD=./wrap ./main
hi y'all
main
bye y'all
Деструктор не работает с /bin/ls
:
$ LD_PRELOAD=./wrap /bin/ls
hi y'all
main main.c wrap wrap.c
Я не могу понять, как это отладить. LD_DEBUG=all
не показывает ничего полезного. ldd main
и ldd /bin/ls
выглядят сопоставимо. Оба двоичных файла динамически связаны. Насколько я понимаю, в документы GCC нет никаких предостережений, о которых я должен знать.
🤔 А знаете ли вы, что...
C используется для создания библиотек и фреймворков, которые расширяют его функциональность.
По какой-то причине GNU ls
закрывается stdout
перед выходом через обработчик atexit
, который предположительно запускается перед вашим деструктором.
Так что, вероятно, ваш деструктор работает нормально, но ничего не печатает, потому что вы пишете в закрытый поток.
Вам, вероятно, потребуется, чтобы ваш деструктор и ваша оболочка в целом выполняли ведение журнала более надежным способом. Было бы лучше открыть необработанный fd и write()
зайти в него, но даже в этом случае некоторые программы сделают for (i = 0; i < 1024; i++) close(i);
что также закроет ваш log fd. Единственным действительно безопасным способом может быть open/write/close
каждый раз или поддерживать свой собственный буфер журнала где-то в памяти и вручную записывать его при заполнении.
Судя по комментариям, вероятная причина в том, что stdout
может быть направлен в файл, а так как он буферизован, то часть данных может не выписываться до закрытия потока. В этот момент может произойти ошибка (переполнен диск, ошибка ввода-вывода и т. д.). Если бы ls
оставил закрытие stdout
коду запуска/остановки библиотеки C, как это делает ваш простой main.c
, то эта ошибка осталась бы незамеченной, а ls
все равно завершился бы со статусом 0; родительский процесс никак не мог узнать, что выходной файл неполный. Таким образом, вызывая fclose(stdout)
явно, ls
может обработать сбой, сообщить о нем, если это возможно, и выйти с ненулевым статусом, чтобы родитель знал, что не доверяет выходным данным.
Как указывает ShadowRanger, все основные утилиты GNU имеют такое поведение.