LD_PRELOAD не работает должным образом
Рассмотрим следующую библиотеку, которая может быть предварительно загружена перед выполнением любой программы:
// g++ -std=c++11 -shared -fPIC preload.cpp -o preload.so
// LD_PRELOAD=./preload.so <command>
#include <iostream>
struct Goodbye {
Goodbye() {std::cout << "Hello\n";}
~Goodbye() {std::cout << "Goodbye!\n";}
} goodbye;
Проблема заключается в том, что, хотя конструктор глобальной переменной goodbye
всегда вызывается, деструктор не вызывается для некоторых программ, например ls
:
$ LD_PRELOAD=./preload.so ls
Hello
Для некоторых других программ деструктор вызывается как ожидалось:
$ LD_PRELOAD=./preload.so man
Hello
What manual page do you want?
Goodbye!
Можете ли вы объяснить, почему деструктор не вызывается в первом случае?
EDIT: на этот вопрос уже был дан ответ, это программа, которая может использовать функции _exit(), abort() для выхода.
Однако:
Есть ли способ заставить заданную функцию вызываться, когда выгруженная программа заканчивается?
Ответы
Ответ 1
ls
имеет atexit (close_stdout);
как его код инициализации. Когда он заканчивается, он закрывает stdout (т.е. close(1)
), поэтому ваши операции cout
, printf
или write(1, ...
ничего не печатают. Это не означает, что деструктор не называется. Вы можете проверить это, например. создавая новый файл в вашем деструкторе.
http://git.savannah.gnu.org/cgit/coreutils.git/tree/src/ls.c#n1285 вот строка в GNU coreutils ls.
Это не просто ls
, это делает большинство из Coreutils. К сожалению, я не знаю точной причины, по которой они предпочитают ее закрывать.
Боковое замечание о том, как это можно найти (или, по крайней мере, то, что я сделал), может помочь в следующий раз или с программой без доступа к исходному коду:
Сообщение деструктора печатается с помощью /bin/true
(простейшая программа, о которой я мог подумать), но не печатается с помощью ls
или df
. Я начал с strace /bin/true
и strace /bin/ls
и сравнил последние системные вызовы. Он показал close(1)
и close(2)
для ls
, но не для true
. После этого все стало иметь смысл, и мне просто нужно было проверить, вызван ли деструктор.
Ответ 2
Если программа выходит через _exit
(POSIX) или _exit
(C99) или ненормальное завершение программы (abort
, фатальные сигналы и т.д.), тогда не может быть вызвано деструкторы. Я не вижу никакого способа обойти это.
Ответ 3
Как и другие, программа может вызывать через _exit()
, _exit()
или abort()
, и ваши деструкторы даже не заметят. Чтобы решить эти случаи, вы можете переопределить эти функции, просто создав обертку, как показано ниже:
void
_exit(int status)
{
void (*real__exit)(int) __attribute__((noreturn));
const char *errmsg;
/* Here you should call your "destructor" function. */
destruct();
(void)dlerror();
real__exit = (void(*)(int))dlsym(RTLD_NEXT, "_exit");
errmsg = dlerror();
if (errmsg) {
fprintf(stderr, "dlsym: _exit: %s\n", errmsg);
abort();
}
real__exit(status);
}
Но это не решит всех возможностей программы, которые не будут работать без ваших знаний в библиотеке, потому что это не единственные точки выхода, которые может иметь приложение. Он также может инициировать системный вызов exit
с помощью функции syscall()
, и, чтобы избежать этого, вы также должны были бы его обернуть.
Другой способ выхода программы - получение необработанного сигнала, поэтому вы также должны обрабатывать (или обертывать?) все сигналы, которые могут вызвать смерть программы. Прочтите справочную страницу signal(2)
для получения дополнительной информации, но имейте в виду, что сигналы типа SIGKILL
(9) не могут быть обработаны, и приложение может уничтожить себя, вызвав kill()
. С учетом сказанного и, если вы не ожидаете обработки безумных приложений, написанных сумасшедшими обезьянами, вы также должны обернуть kill()
.
Другой системный вызов, который вам нужно будет обернуть, - execve()
.
В любом случае системный вызов (например, _exit
) также можно запускать непосредственно с помощью команды сборка int 0x80
или устаревшего _syscallX()
макрос. Как вы его обертываете, если не извне приложения (например, strace
или valgrind
)? Ну, если вы ожидаете такого поведения в ваших программах, я предлагаю вам отказаться от метода LD_PRELOAD
и начать думать о том, как делать strace
и valgrind
do (используя ptrace()
из другого процесса) или создание модуля ядра Linux для его отслеживания.