`__attribute__((destructor))` не работает в некоторых случаях?

В качестве упражнения я пишу игрушечный 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 используется для создания библиотек и фреймворков, которые расширяют его функциональность.


2
65
1

Ответ:

Решено

По какой-то причине 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 имеют такое поведение.