我正在设计一个基于预加载程序的锁跟踪实用程序,它连接到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在我的包装器函数中似乎没有改变(即,在崩溃点,它仍然等于帧9中的0x5…,这是对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];
}
history是来自libcd的无锁并发hashmap(映射hist_entry的tid->每个线程向量),它的迭代器也保证是线程安全的。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);
}
正如你所看到的,这个版本的tail调用当前版本,这可能就是为什么它需要这么长时间才能检测到的原因:GDB通常非常擅长检测被tail调用忽略的帧,但我猜它没有检测到这个,因为函数具有;相同的";name(该错误不会影响互斥体函数,因为它们不会公开多个版本)。这篇博客文章更详细地介绍了pthread_cond_wait()。我在调试时多次使用这个函数,并对其进行了某种程度的调优,因为对glibc的每次调用都包含在多层间接层中;当我在pthread_cond_wait符号上设置了一个断点,而不是行号时,我才意识到发生了什么,它在这个函数处停止了。
无论如何,这解释了指针变化的现象:发生的情况是,调用了旧的、不正确的函数,将pthread_cond_t对象重新解释为包含指向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"),因此需要在每个函数的基础上查找它们。可以通过查看glibc源中函数定义附近的compat_symbol()或version_symbol(