함수에 전달 된 포인터가 예기치 않게 변경됨
Pthread에 연결되는 프리 로더 기반 잠금 추적 유틸리티를 설계하고 있는데 이상한 문제가 발생했습니다. 이 프로그램은 런타임에 관련 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가 내 래퍼 함수에서 변경되지 않는 것으로 보이는 코어 덤프에서 볼 수 있습니다 (즉, REAL_FN 호출 인 충돌 지점의 프레임 9에서 여전히 0x5 ...와 같습니다). 내용을 보면 어떤 포인터가 올바른지 알 수는 없지만 대상 응용 프로그램에서 내 래퍼로 전달 된 포인터라고 가정합니다. 두 포인터 모두 프로그램 데이터에 대한 유효한 세그먼트를 가리 킵니다 (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의 잠금없는 동시 해시 맵 (hist_entry의 tid-> 스레드 별 벡터 매핑)이며, 반복자도 스레드로부터 안전합니다. GNU 문서에서는 backtrace ()가 스레드로부터 안전하며 steady_clock :: now ()에 대한 CPP 문서에 언급 된 데이터 경합이 없다고 말합니다. 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-call합니다. 이것이 감지하는 데 시간이 오래 걸리는 이유 일 것입니다. GDB는 일반적으로 tail 호출로 제거 된 프레임을 감지하는 데 꽤 능숙하지만이 버전은 감지하지 못한 것 같습니다. 함수의 이름이 "동일"하기 때문입니다 (여러 버전을 노출하지 않기 때문에 오류가 뮤텍스 함수에 영향을주지 않음). 이 블로그 게시물 은 우연히 pthread_cond_wait ()에 대해 훨씬 더 자세히 설명합니다. glibc에 대한 모든 호출이 여러 계층의 간접적으로 래핑되기 때문에 디버깅하는 동안이 함수를 여러 번 밟아서 조정했습니다. 줄 번호 대신 pthread_cond_wait 기호에 중단 점을 설정했을 때 무슨 일이 벌어지고 있는지 깨달았고이 함수에서 중지되었습니다.
어쨌든, 이것은 변화하는 포인터 현상을 설명합니다 : 이전의 잘못된 함수가 호출되고, pthread_cond_t 객체를 pthread_cond_t 객체에 대한 포인터를 포함하는 구조체로 재 해석하고, 해당 포인터에 대해 새로운 pthread_cond_t를 할당 한 다음 새로 할당 된 하나는 새롭고 올바른 기능입니다. 이전 함수의 프레임은 꼬리 호출에 의해 제거되고 이전 함수를 떠난 후 GDB 역 추적에 대해 신비하게 변경된 인수와 함께 내 래퍼에서 직접 올바른 함수가 호출되는 것처럼 보입니다.
이에 대한 수정은 간단했습니다. GNU는 dlsym ()과 비슷하지만 버전 문자열도받는 libdl 확장 dlvsym ()을 제공합니다. 버전 문자열 "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 ").