Указатель, переданный функции, неожиданно изменяется

#c #pthreads #glibc #condition-variable #ld-preload

#c #указатели #pthreads #ld-предварительная загрузка

Вопрос:

Я разрабатываю утилиту трассировки блокировок на основе предварительного загрузчика, которая подключается к Pthreads, и я столкнулся со странной проблемой. Программа работает, предоставляя оболочки, которые заменяют соответствующие функции Pthreads во время выполнения; они выполняют некоторое протоколирование, а затем передают аргументы реальной функции Pthreads для выполнения работы. Очевидно, что они не изменяют переданные им аргументы. Однако при тестировании я обнаружил, что указатель на переменную условия, переданный моей оболочке pthread_cond_wait(), не соответствует указателю, который передается базовой функции Pthreads, которая быстро завершается сбоем с сообщением «средство futex вернуло неожиданный код ошибки», что, насколько я понял, обычно указывает на недопустимую синхронизациюобъект передан. Соответствующая трассировка стека из GDB:

 #8  __pthread_cond_wait (cond=0x7f1b14000d12, mutex=0x55a2b961eec0) at pthread_cond_wait.c:638
#9  0x00007f1b1a47b6ae in pthread_cond_wait (cond=0x55a2b961f290, lk=0x55a2b961eec0)
    at pthread_trace.cpp:56
 

Я довольно озадачен. Вот код для моей оболочки pthread_cond_wait():

 int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* lk) {
        // log arrival at wait
        the_tracer.add_event(lktrace::event::COND_WAIT, (size_t) cond);
        // run pthreads function
        GET_REAL_FN(pthread_cond_wait, int, pthread_cond_t*, pthread_mutex_t*);
        int e = REAL_FN(cond, lk);
        if (e == 0) the_tracer.add_event(lktrace::event::COND_LEAVE, (size_t) cond);
        else {
                the_tracer.add_event(lktrace::event::COND_ERR, (size_t) cond);
        }
        return e;
}

// GET_REAL_FN is defined as:
#define GET_REAL_FN(name, rtn, params...) 
        typedef rtn (*real_fn_t)(params); 
        static const real_fn_t REAL_FN = (real_fn_t) dlsym(RTLD_NEXT, #name); 
        assert(REAL_FN != NULL) // semicolon absence intentional
 

И вот код для __pthread_cond_wait в glibc 2.31 (это функция, которая вызывается, если вы обычно вызываете pthread_cond_wait , у нее другое имя из-за проблем с управлением версиями. Приведенная выше трассировка стека подтверждает, что это функция, на которую указывает REAL_FN):

 int
__pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex)
{
  /* clockid is unused when abstime is NULL. */
  return __pthread_cond_wait_common (cond, mutex, 0, NULL);
}   
 

Как вы можете видеть, ни одна из этих функций не изменяет cond , но это не одно и то же в двух кадрах. Изучение двух разных указателей в дампе ядра показывает, что они также указывают на разное содержимое. Я также вижу в дампе ядра, что cond, похоже, не изменяется в моей функции-оболочке (т. Е. Он по-прежнему равен 0x5 … в кадре 9 в точке сбоя, которая является вызовом REAL_FN). Я не могу точно определить, какой указатель правильный, посмотрев на их содержимое, но я бы предположил, что это тот, который передается в мою оболочку из целевого приложения. Оба указателя указывают на допустимые сегменты для данных программы (помеченные как ALLOC, LOAD, HAS_CONTENTS).

Мой инструмент определенно каким-то образом вызывает ошибку, целевое приложение работает нормально, если оно не подключено. Чего мне не хватает?

ОБНОВЛЕНИЕ: на самом деле, похоже, это не то, что вызывает ошибку, потому что вызовы моей оболочки pthread_cond_wait() выполняются много раз, прежде чем возникает ошибка, и демонстрируют аналогичное поведение (значение указателя меняется между кадрами без объяснения причин) каждый раз. Я оставляю вопрос открытым, потому что я все еще не понимаю, что здесь происходит, и я хотел бы узнать.

ОБНОВЛЕНИЕ 2: в соответствии с запросом, вот код для tracer.add_event():

 // add an event to the calling thread's history
// hist_entry ctor gets timestamp amp; stack trace
void tracer::add_event(event e, size_t obj_addr) {
        size_t tid = get_tid();
        hist_map::iterator hist = histories.contains(tid);
        assert(hist != histories.end());
        hist_entry ev (e, obj_addr);
        hist->second.push_back(ev);
}

// hist_entry ctor:
hist_entry::hist_entry(event e, size_t obj_addr) :
        ts(chrono::steady_clock::now()), ev(e), addr(obj_addr) {

        // these are set in the tracer ctor     
        assert(start_addr amp;amp; end_addr);

        void* buf[TRACE_DEPTH];
        int v = backtrace(buf, TRACE_DEPTH);
        int a = 0;
        // find first frame outside of our own code
        while (a < v amp;amp; start_addr < (size_t) buf[a] amp;amp;
                end_addr > (size_t) buf[a])   a;
        // skip requested amount of frames
        a  = TRACE_SKIP;
        if (a >= v) a = v-1;
        caller = buf[a];
}
 

histories — это параллельная хэш-карта без блокировки из libcds (отображение векторов tid-> per-thread для hist_entry ), и его итераторы также гарантированно потокобезопасны. В документах GNU говорится, что функция backtrace() потокобезопасна, и в документах CPP для steady_clock::now() не упоминается о гонках данных для steady_clock::now(). get_tid() просто вызывает pthread_self(), используя тот же метод, что и функции-оболочки, и преобразует его результат в size_t .

Комментарии:

1. похоже, пришло время для отладки ассемблерного кода?

2. Что the_tracer.add_event делать? В частности, интересно, как он принимает второй параметр (по значению или по ссылке).

3. Параметр передается по значению (и не обрабатывается как указатель, как вы можете видеть из приведения)

4. Какой компилятор вы используете? Похоже, вы неправильно используете переменные аргументы макроса, что, я думаю, приводит к тому, что вы передаете только один параметр REAL_FN .

5. Я использую gcc 9.3. Это определение макроса правильно сформировано; вам разрешено называть свои переменные, если вы хотите: gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html

Ответ №1:

Хах, понял это! Проблема в том, что Glibc предоставляет несколько версий pthread_cond_wait() для обратной совместимости. Версия, которую я воспроизвожу в своем вопросе, является текущей версией, которую мы хотим вызвать. Версия, которую обнаружила dlsym(), является обратно совместимой версией:

 int
__pthread_cond_wait_2_0 (pthread_cond_2_0_t *cond, pthread_mutex_t *mutex)
{
  if (cond->cond == NULL)
    {
      pthread_cond_t *newcond;

      newcond = (pthread_cond_t *) calloc (sizeof (pthread_cond_t), 1);
      if (newcond == NULL)
        return ENOMEM;

      if (atomic_compare_and_exchange_bool_acq (amp;cond->cond, newcond, NULL))
        /* Somebody else just initialized the condvar.  */
        free (newcond);
    }

  return __pthread_cond_wait (cond->cond, mutex);
}
 

Как вы можете видеть, эта версия вызывает хвост текущей версии, что, вероятно, и объясняет, почему это заняло так много времени для обнаружения: GDB обычно довольно хорошо обнаруживает кадры, пропущенные хвостовыми вызовами, но я предполагаю, что он не обнаружил этот, потому что функции имеют «одинаковое» имя (иошибка не влияет на функции мьютекса, поскольку они не предоставляют несколько версий). В этом сообщении в блоге рассматривается гораздо более подробно, по совпадению, конкретно о pthread_cond_wait() . Я много раз проходил через эту функцию во время отладки и вроде как отключал ее, потому что каждый вызов в glibc обернут несколькими уровнями косвенности; Я понял, что происходит, только когда установил точку останова для символа pthread_cond_wait вместо номера строки, и он остановился на этой функции.

В любом случае, это объясняет феномен изменения указателя: происходит то, что вызывается старая неправильная функция, переосмысливает объект pthread_cond_t как структуру, содержащую указатель на объект pthread_cond_t, выделяет новый pthread_cond_t для этого указателя, а затем передает вновь выделенный указатель новой правильной функции. Фрейм старой функции удаляется конечным вызовом, и для обратной трассировки GDB после выхода из старой функции похоже, что правильная функция вызывается непосредственно из моей оболочки с таинственно измененным аргументом.

Исправить это было просто: GNU предоставляет расширение libdl dlvsym() , которое похоже на dlsym(), но также принимает строку версии. Поиск pthread_cond_wait со строкой версии «GLIBC_2.3.2» решает проблему. Обратите внимание, что эти версии обычно не соответствуют текущей версии (т.Е. pthread_create() / exit() имеют строку версии «GLIBC_2.2.5»), поэтому их необходимо искать для каждой функции. Правильная строка может быть определена либо путем просмотра макросов compat_symbol() или versioned_symbol(), которые находятся где-то рядом с определением функции в исходном коде glibc, либо с помощью readelf для просмотра имен символов в скомпилированной библиотеке (у меня есть «pthread_cond_wait@@GLIBC_2.3.2 » и «pthread_cond_wait@@GLIBC_2.2.5 «).