2013-03-11 1 views
4

저는 현재 데이터를로드하고 간단한 작업을 수행하고 결과를 출력하기 위해 스크립트를 구문 분석하는 90 년대 후반에 작성된 일부 C++ 코드를 디버깅하고 있습니다.템플릿이있는 펑터를 사용하여 printf 호출 segfaults (64 비트 전용, 32 비트에서 valgrind clean)

코드를 작성한 사람들은 Functors를 사용하여 구문 분석중인 파일에서 문자열 키워드를 실제 함수 호출에 매핑하고, 함수 인터페이스를 무수히 처리하기 위해 템플릿을 만듭니다 (최대 인수 8 개). 사용자가 스크립트에서 요청할 수 있습니다.

대부분이 부분은 잘 작동합니다. 근년에 일부 64 비트 빌드 시스템에서 segfault를하기 시작했습니다. 놀랍게도 valgrind를 통해 물건을 실행하면 오류가 "funf"내부에서 발생하는 것으로 나타났습니다.이 오류는 펑터 중 하나입니다. 이것이 작동하는 방법을 보여주는 몇 가지 코드 스 니펫이 있습니다.

첫째, 구문 분석입니다 스크립트는 다음 행을 포함

printf("%5.7f %5.7f %5.7f %5.7f\n", cos(j/10), tan(j/10), sin(j/10), sqrt(j/10)); 

곳 왜냐하면, 황갈색, 죄와 SQRT는 또한 내가 가진 사람을 대체 할 경우이 세부 사항, 중요하지 않습니다 (libm의에 해당 펑 있습니다 고정 된 수치 값은 같은 결과를 얻는다).

printf를 호출하면 다음과 같은 방식으로 수행됩니다. 첫째, 템플릿 펑터 :를 호출

template<class R, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8> 
class FType 
{ 
    public : 
     FType(const void * f) { _f = (R (*)(T1,T2,T3,T4,T5,T6,T7,T8))f; } 
     R operator()(T1 a1,T2 a2,T3 a3,T4 a4,T5 a5,T6 a6,T7 a7,T8 a8) 
     { return _f(a1,a2,a3,a4,a5,a6,a7,a8); } 

    private : 
     R (*_f)(T1,T2,T3,T4,T5,T6,T7,T8); 

}; 

그리고 코드는 다른 템플릿 클래스 안에 - 나는 프로토 타입 및 FTYPE를 사용하는 코드의 관련 부분 (물론 I가 넣어 약간의 추가 코드를 보여 디버깅) :

template<class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8> 
static Token 
evalF(
    const void *   f, 
    unsigned int   nrargs, 
    T1    a1, 
    T2    a2, 
    T3    a3, 
    T4    a4, 
    T5    a5, 
    T6    a6, 
    T7    a7, 
    T8    a8, 
    vtok &    args, 
    const Token &   returnType) 
{ 
    Token  result; 

    printf("Count: %i\n",++_count); 

    if(_count == 2) { 
    const char *fmt = *((const char **) &a1); 

    result = printf(fmt,a2,a3,a4,a5,a6,a7,a8); 

    FType<int, const void*,T2,T3,T4,T5,T6,T7,T8> f1(f); 
    result = f1("Hello, world.\n",a2,a3,a4,a5,a6,a7,a8); 
    result = f1("Hello, world2 %5.7f\n",a2,a3,a4,a5,a6,a7,a8); 
    result = f1(fmt,a2,a3,a4,a5,a6,a7,a8); 
    } else { 
    FType<int, T1,T2,T3,T4,T5,T6,T7,T8> f1(f); 
    result = f1(a1,a2,a3,a4,a5,a6,a7,a8); 
    } 
} 

(이 함수는 여러 번 호출되기 때문에) if (_count == 2) 비트가 삽입되었습니다. 정상적인 상황에서는 else 절에서 연산 만 수행합니다. 그것은 fType 생성자를 호출합니다 (어떤 템플릿은 반환 형식을 int로 사용합니다). printf의 펑터 인 "f"를 사용합니다 (디버거에서 검증 됨). F1이 구축되면,이 템플릿 모든 인수 오버로드 된 호출 연산자를 호출하고, Valgrind의 불평하기 시작 :

==29358== Conditional jump or move depends on uninitialised value(s) 
==29358== at 0x92E3683: __printf_fp (printf_fp.c:406) 
==29358== by 0x92E05B7: vfprintf (vfprintf.c:1629) 
==29358== by 0x92E88D8: printf (printf.c:35) 
==29358== by 0x5348C45: FType<int, void const*, double, double, double, double, void const*, void const*, void const*>::operator()(void const*, double, double, double, double, void const*, void const*, void const*) (Interpreter.cc:321) 
==29358== by 0x51BAB6D: Token evalF<void const*, double, double, double, double, void const*, void const*, void const*>(void const*, unsigned int, void const*, double, double, double, double, void const*, void const*, void const*, std::vector<Token, std::allocator<Token> >&, Token const&) (Interpreter.cc:542) 

그래서,이는 경우() 절 내부의 실험되었다. 첫째, printf를 똑같은 인자로 직접 호출 해 보았습니다. (매개 변수 a1을 가진 타입 캐스팅 트릭을 - 형식으로 - 컴파일하기 위해; 그렇지 않으면 T1이 아닌 많은 템플릿 인스턴스에 대해 불평합니다 (char *) printf 예상대로). 이것은 잘 작동합니다.

다음으로 변수가없는 대체 형식 문자열 (Hello, world)을 사용하여 f1을 호출 해 보았습니다. 이것은 또한 잘 작동합니다.

그런 다음 변수 (Hello, World2 % 5.7f) 중 하나를 추가 한 다음 위와 같이 valgrind 오류를 확인하기 시작합니다.

32 비트 시스템에서이 코드를 실행하면 valgrind clean (그렇지 않으면 동일한 버전의 glibc, gcc)입니다.

여러 Linux 시스템 (64 비트 모두)에서 실행되는 경우가 있는데, 때로는 segfault (예 : RHEL5.8/libc2.5 및 openSUSE11.2/libc-2.10.1)가 표시되는 경우가 있습니다. 예를 들어 Fedora 17 및 Ubunutu 12.04와 함께 libc2.15). 그러나 valgrind는 모든 시스템에서 항상 비슷한 방식으로 불평하며 충돌 여부에 관계없이 우연이라고 생각하게 만듭니다.

이 코드를 사용하면 누군가가이 코드를 잘못 찾으면 훨씬 행복해 지겠지만,이 모든 것이 64 비트 glibc에서 일종의 버그를 의심하게 만듭니다!

내가 가진 한 가지 점은 다양한 인수 목록을 파싱하는 것과 관련이 있다는 것입니다. 템플릿으로 얼마나 정확하게 재생됩니까? 런타임까지 형식 문자열을 알지 못하기 때문에 실제로 어떻게 작동하는지 명확하지 않습니다. 따라서 컴파일 할 때 템플릿의 특정 인스턴스를 어떻게 알 수 있습니까? 그러나 이것이 32 비트에서 모든 것이 정상적으로 보이는 이유를 설명하지는 않습니다. 의견

에 응답

업데이트이 정보가 도움이 토론을 위해 당신에게 모두 감사드립니다. 나는 아직 그것을 확인하지는 않았지만 % al 레지스터에 관한 awn의 대답은 아마도 올바른 설명 일 것이라고 생각한다. 그럼에도 불구하고 토론의 이익을 위해 다른 64 비트 시스템에서 재생할 수있는 오류를 재생산하는 완전하고 최소한의 프로그램이 있습니다. 상단에 #define _VOID_PTR이 있으면 void * 포인터를 사용하여 원래 코드에서와 같이 함수 포인터를 전달합니다 (그리고 valgrind 오류가 발생 함). #define _VOID_PTR을 주석으로 처리하면 대신 WhosCraig가 제안한대로 제대로 프로토 타입 화 된 함수 포인터가 사용됩니다. 이 경우의 문제는 컴파일러가 프로토 타입 불일치에 대해 불평하고 있기 때문에 단순히 int (*f)(const char *, double, double) = &printf;을 넣을 수 없다는 것입니다 (어쩌면 저는 단지 두껍고 이것을 수행 할 방법이 있습니까?) - 이것이 원래 문제라고 생각합니다. 저자는 무효 포인터로 주위를 둘러 보려고했다). 이 특별한 경우를 다루기 위해 올바른 명시 적 인수 목록을 사용하여이 wrap_printf() 함수를 만듭니다. 이 버전의 코드를 실행하면 valgrind가 깨끗해집니다. 불행하게도 이것은 void * vs 함수 포인터 저장 문제인지 또는 % al 레지스터와 관련이 있는지 알려주지 않습니다. 나는 후자의 경우에 대부분의 증거 점을 생각하고, 나는 고정 된 인수 목록과 printf()을 포장하는 것은 "옳은 일"할 수있는 컴파일러 강제 것으로 의심 : 사용되는 시스템 V의 AMD64 ABI에 의해

#include <cstdio> 

#define _VOID_PTR // set if using void pointers to pass around function pointers 

template<class R, class T1, class T2, class T3> 
class FType 
{ 
public : 
#ifdef _VOID_PTR 
    FType(const void * f) { _f = (R (*)(T1,T2,T3))f; } 
#else 
    typedef R (*FP)(T1,T2,T3); 
    FType(R (*f)(T1,T2,T3)) { _f = f; } 
#endif 

    R operator()(T1 a1,T2 a2,T3 a3) 
    { return _f(a1,a2,a3); } 

private : 
    R (*_f)(T1,T2,T3); 

}; 

template <class T1, class T2, class T3> int wrap_printf(T1 a1, T2 a2, T3 a3) { 
    const char *fmt = *((const char **) &a1); 
    return printf(fmt, a2, a3); 
} 

int main(void) { 

#ifdef _VOID_PTR 
    void *f = (void *)printf; 
#else 
    // this doesn't work because function pointer arguments don't match printf prototype: 
    // int (*f)(const char *, double, double) = &printf; 

    // Use this wrapper instead: 
    int (*f)(const char *, double, double) = &wrap_printf; 
#endif 

    char a1[]="%5.7f %5.7f\n"; 
    double a2=1.; 
    double a3=0; 

    FType<int, const char *, double, double> f1(f); 

    printf(a1,a2,a3); 
    f1(a1,a2,a3); 

    return 0; 
} 
+0

542 행의 Interpreter.cc 행을 표시하십시오. 그리고 321 행에 대해서도 마찬가지입니까? –

+0

컴파일러가 [variadic templates] (http://en.cppreference.com/w/cpp/language/parameter_pack) 및 ['std :: function'] (http : /en.cppreference.com/w/cpp/utility/functional/function). –

+0

라인 542는 "Hello, world2"입니다. 그 대신 f1()을 호출하는 else 절의 코드를 실행하는 경우 동일한 valgrind 출력을 얻습니다. – echapin

답변

3

을 64 비트 리눅스 (및 많은 다른 Unixes)에 의해, 고정 된 수의 인수와 다양한 수의 인수를 갖는 함수는 매우 다른 호출의 유실을 갖는다.

은 0.99.5 [2], 장 3.2.3 "매개 변수가 전달" "시스템 V 응용 프로그램 바이너리 인터페이스 AMD64 아키텍처 프로세서 보충"초안에서 인용 : 호출의

가변 인자를 사용하는 함수를 호출 할 수 또는 stdargs (프로토 타입이없는 호출 또는 선언에 줄임표 (...)가 포함 된 함수에 대한 호출) % al은 숨겨진 인수로 사용되는 벡터 레지스터의 수를 지정합니다. 지금

, 3 단계 서열

  1. 의 printf (3)는 이러한 가변 인자의 함수이다. 따라서 % al 레지스터가 올바르게 채워질 것으로 기대합니다.

  2. 귀하의 FType :: _ f는 고정 된 인수 개수를 갖는 함수에 대한 포인터로 선언됩니다. 따라서 컴파일러는 % al에 대해 신경 쓰지 않습니다.

  3. FType :: _ f를 통해 printf()를 호출하면 올바르게 채워진 % al (1로 인해)을 예상하지만 컴파일러는 2로 인해 그것을 채우지 않으므로 결과적으로 printf()는 % al에서 "쓰레기"를 찾습니다.

올바르게 초기화 된 값 대신 "가비지"를 사용하면 쉽게 관찰되는 segfaults를 비롯하여 원치 않는 다양한 결과가 발생할 수 있습니다.자세한 내용은

은 다음을 참조하십시오
는 컴파일러가 variadic templates을 처리 할 수 ​​있으므로 C++ 11 호환되며, [1] http://en.wikipedia.org/wiki/X86_calling_conventions#x86-64_calling_conventions
[2] http://x86-64.org/documentation/abi.pdf

+0

나는 증거가 주어질 때 일어나는 일에 대해 가장 그럴듯한 설명처럼 들린다 고 생각한다. 새 테스트 코드에서 printf를 실행 한 직후 디버거에서 % rax의 값을 확인하면 값은 2입니다. 디버거는 그 값이 printf로 이동 한 후에 방금 변경되었음을 나타냅니다. 만약 f1과 printf를 똑같이한다면, 값은 0x3ff0000000000000입니다. 그래서 % al이 0이라는 것을 의미합니다. printf를 실행 한 직후 값이 변경되지 않았습니다. 나는 이것이 꽤 설득력있는 증거라고 생각합니다.이 시험이 당신에게 맞는 것 같습니까? – echapin

+0

@echapin 예. % al은 % xmm 레지스터를 통해 전달 된 인수의 수입니다. 당신은 2 개의 복식을 통과했습니다. % al에는 2가 들어 있습니다. 모두 나에게 맞는 것 같습니다. – awn

1

경우, 그것은 괜찮 년대를 재 배열 매개 변수의 순서를 사용하면 다음과 같이 할 수 있습니다.

template<typename F, typename ...A> 
static Token evalF(vtok& args, const Token& resultType, F f, A... a) 
{ 
    Token result; 

    f(a...); 

    return result; 
} 

예를 볼 때 잘 작동합니다. this example.

+0

제안 해 주셔서 감사합니다. 우리는 모든 솔루션을 구현할 수있는 모든 빌더에 대해 지속적으로 새로운 버전의 gcc를 사용하고 있다고 생각합니다. 그러나, 내 질문이 더 : "왜이 코드가 실패"보다 "이 일을 더 나은 방법"무엇입니까, 올바른 것으로 @awn 응답을 선택합니다. – echapin