Подтвердить что ты не робот

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() для выхода.

Однако:

Есть ли способ заставить заданную функцию вызываться, когда выгруженная программа заканчивается?

4b9b3361

Ответ 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 для его отслеживания.