Рассмотрим следующую библиотеку, которую можно предварительно загрузить перед выполнением любой программы:
// 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!
Можете ли вы объяснить, почему деструктор не вызывается в первом случае?
РЕДАКТИРОВАТЬ: на вышеуказанный вопрос уже был дан ответ, то есть программа вполне может использовать _exit (), abort () для выхода.
Тем не мение:
Есть ли способ заставить данную функцию вызываться при выходе из предварительно загруженной программы?
ls
имеет atexit (close_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
, После этого все стало иметь смысл, и мне просто нужно было проверить, что деструктор вызван.
Если программа выходит через _exit
(POSIX) или _Exit
(C99) или ненормальное завершение программы (abort
, фатальные сигналы и т. д.) тогда нет способа вызвать деструкторы. Я не вижу никакого способа обойти это.
Как говорили другие, программа может вызываться через _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)
man-страницу для получения дополнительной информации, но имейте в виду, что такие сигналы, как SIGKILL
(9) не может быть обработано, и приложение может уничтожить себя, вызвав kill()
, С учетом вышесказанного, и если вы не ожидаете обработки безумных приложений, написанных сумасшедшими обезьянами, вы должны обернуть kill()
тоже.
Другой системный вызов, который вам нужно будет перенести, это execve()
,
Во всяком случае, системный вызов (как _exit
) также может быть запущен непосредственно через сборка int 0x80
инструкция или устаревший _syscallX()
макрос. Как бы вы обернули его, если не из-за пределов приложения (как strace
или же valgrind
)? Ну, если вы ожидаете такого поведения в своих программах, я предлагаю вам отказаться от LD_PRELOAD
техника и начать думать о том, чтобы делать как strace
а также valgrind
делать (используя ptrace()
из другого процесса) или создание модуля ядра Linux для его отслеживания.