传递给函数的指针意外更改

Pointer passed to function changes unexpectedly

我正在设计一个附加到 Pthreads 的基于预加载器的锁跟踪实用程序,我 运行 遇到了一个奇怪的问题。该程序通过提供在 运行 时间替换相关 Pthreads 函数的包装器来工作;这些做一些日志记录,然后将 args 传递给真正的 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

这里是 glibc 2.31 中 __pthread_cond_wait 的代码(如果你正常调用 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 & 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 && 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 && start_addr < (size_t) buf[a] &&
                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 的无锁并发 hashmap(映射 hist_entry 的 tid->per-thread 向量),它的迭代器也保证是线程安全的。 GNU 文档说 backtrace() 是线程安全的,并且 CPP 文档中没有提到 steady_clock::now() 的数据竞争。 get_tid() 只是调用 pthread_self() 使用与包装函数相同的方法,并将其结果转换为 size_t.

哈,想通了!问题是 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 (&cond->cond, newcond, NULL))
        /* Somebody else just initialized the condvar.  */
        free (newcond);
    }

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

如您所见,此版本对当前版本进行了尾调用,这可能就是为什么需要这么长时间才能检测到的原因:GDB 通常非常擅长检测被尾调用省略的帧,但我猜它没有不要检测到这个,因为函数具有“相同”的名称(并且错误不会影响互斥函数,因为它们不会公开多个版本)。 This blog post 进行了更详细的介绍,巧合的是专门针对 pthread_cond_wait()。我在调试时多次执行此函数,并对其进行了调整,因为对 glibc 的每次调用都包含在多个间接层中;当我在 pthread_cond_wait 符号而不是行号上设置断点时,我才意识到发生了什么,它在这个函数处停止。

无论如何,这解释了指针变化现象:发生的事情是旧的、不正确的函数被调用,将 pthread_cond_t 对象重新解释为包含指向 pthread_cond_t 对象的指针的结构,分配一个新的 pthread_cond_t 该指针,然后将新分配的指针传递给新的正确函数。旧函数的框架被尾调用忽略了,在离开旧函数后的 GDB 回溯中,看起来正确的函数是直接从我的包装器中调用的,参数神秘地改变了。

解决这个问题的方法很简单:GNU 提供了 libdl 扩展 dlvsym(),它类似于 dlsym(),但也有一个版本字符串。使用版本字符串“GLIBC_2.3.2”查找 pthread_cond_wait 可解决问题。请注意,这些版本通常不对应于当前版本(即 pthread_create()/exit() 具有版本字符串“GLIBC_2.2.5”),因此需要在每个版本上查找它们功能基础。可以通过查看 glibc 源代码中函数定义附近的 compat_symbol() 或 versioned_symbol() 宏来确定正确的字符串,或者使用 readelf 查看符号的名称在编译库中(我的有“pthread_cond_wait@@GLIBC_2.3.2”和“pthread_cond_wait@@GLIBC_2.2.5”)。