비주얼 스튜디오에서 변수 조사하기 1 – 디버그 식

소프트웨어 디버깅의 가장 핵심은 관심 있는 변수에 어떤 상황에 어떤 값으로 저장되어 있는지 조사하는 것일 겁니다. 만일 윈도즈(Windows)에서 비주얼 스튜디오(Visual Studio)를 이용해 개발을 한다면 대부분의 개발자가 원하는 위치에 중단점(Break Point)를 설정해서 조사식창(Watch Window) 등을 이용해서 값을 확인합니다. 그런데 조사식창을 비롯해 각종 변수의 값을 확인할 수 있는 창은 단순히 값만을 보여 주는 것이 아니라 창 내부에서 연산을 수행할 수도 있고 사용자가 원하는 다양한 형태로 값을 보여줄 수 있는 기능을 제공합니다. MSDN(영어)에 있는 디버거에서 데이터 보기(Viewing Data in the Debugger)와 디버거에서 사용하는 구문(Expressions in the Debugger)절의 내용 중에서 네이티브 코드(Native Code)를 디버깅할 때 필요한 내용을 정리해 보았습니다.

비주얼 스튜디오에서는 지역(Locals), 자동(Autos), 조사식(Watch) 창과 간략한 조사식(QuickWatch) 대화 상자, 데이터 팁(DataTips) 등을 통해 변수 값을 조사할 수 있는 기능을 제공합니다. 지역, 자동 창과 데이터 팁은 변수 이름 통해 값을 확인하는 반면 조사식 창과 간략한 조사식 대화 상자는 이름에서도 알 수 있듯이 식(또는 구문, Expression)을 바탕으로 값을 확인할 수 있습니다. 따라서 이러한 식을 잘 사용하면 보다 효과적으로 변수 값을 확인할 수 있습니다.

사용할 수 있는 식은 크게 네이티브 C++ 언어 구문(Native C++ Language Expressions), 어셈블리 언어 구문(Assembly Language Expressions), 의사 변수(Pseudovariables) 등 세가지 구문을 사용할 수 있고 여기에 문맥 연산자(Context Operator), 형식 지정자(Format Specifiers) 등을 이용해 식을 꾸밀 수 있습니다.

C++ 언어 구문

다음과 같은 제약 사항을 제외하고 모든 구문이 가능하다고 합니다.

쉼표 연산자(Comma Operator) ‘,‘와 조건 연사자(Conditional Operator) ‘? :‘ 사용 불가

다음과 같이 쉼표 연산자와 조건 연산자는 디버그 식으로 사용할 수 없습니다.

exp1, exp2
exp1 ? exp2 : exp3

다음과 같은 객체 구조에 C 객체의 value라는 변수 값을 조사하기 위해서는 너무나도 당연하게 반드시 C.A::value로 지정해야 합니다.

익명 네임스페이스(Anonymous Namespaces) 사용 불가

익명 네임스페이스에 있는 변수 값을 조사하기 위해서는 꾸며진(decorated) 변수 이름을 사용해야 합니다. 가령 다음과 같은 코드에서 test라는 변수 값을 보기 위해서는(int*)?test@?A0xccd06570@mars@@3HA라는 이름을 사용해야 합니다.

그런데 꾸며진 이름을 만들어 내려면 비주얼 C++ 컴파일러가 이름 꾸미는 방법을 확실히 알고 수동으로 만들거나 링크할 때 /MAP 옵션을 통해 알아내야 하는데 쉬운 일이 아닙니다. 결국 디버깅 쉽게 하려면 익명 네임스페이스는 사용하지 말아야겠죠. 디버깅이 목적이 아니라고 할 지라도 익명 네임스페이스를 사용하는 것은 별로 좋은 습관이 아니라고 생각합니다.

생성자, 소멸자 사용 불가

직접 또는 간접적으로 임시 객체를 생성하는 생성자, 소멸자를 사용할 수 없습니다. 이는 형 변환, new, delete 연산자 등을 사용할 수 없다는 것도 의미하게 됩니다.
단 , 다음과 같이 객체가 내장형(built-in type)에 대한 형 변환 연산자를 갖고 있으면 (int)a 와 같은 형 변환 식의 사용이 가능하다고 해서 해 보았는데 정상적으로 동작하지 않았습니다.

struct A
{
    operator int();
}
A a;

또한 MSDN에는 객체를 선언 또는 반환하는 함수는 호출할 수 있다고 나와 있으나 다음과 같은 코드에 대해서 A::create1, A::create2, create3 함수에 대해서는 정상적으로 식이 실행되지만 create1, create2, create3 함수 모두 Access violation이 일어났습니다.

struct A
{
    A(void) : i(0) {}
    A(int _i) : i(_i) {}
    int i;
    A create1(void);
    A create2(void);
};
A A::create1(void)
{
    A a(i + 10);
    return a;
}
A A::create2(void)
{
    return A(i + 10);
}
A create1(int _i)
{
    A a(_i);
    return a;
}
A create2(int _i)
{
    return A(_i);
}
int create3(int _i)
{
    return A(_i).i;
}
A create4(int _i)
{
    A a(_i);
    return a.create1();
}

위의 함수들은 모두 내부에서 임시 객체를 생성하는 호출자가 불리게 되었는데 맴버인 경우에는 잘 동작하고 전역 함수에서는 문제가 있는 걸로 봐서 제가 정확하게 의미를 파악하지 못했다고 할 수밖에 없었습니다.

상속

상속에서 주의할 점은 파생 객체에서 부모 객체로 형 변환은 가능하지만 부모 객체에서 파생 객체로 형 변환은 불가능합니다.

내장(Intrinsic) 또는 인라인(Inlined) 함수

내장 또는 인라인 함수는 일반 함수로 한 번 이상 나와야만 호출할 수 있다고 되어있습니다. 일반 함수로 한 번 이상 나온 함수라는 뜻이 코드에서 한 번 이상 호출된다는 것을 의미하는 것 같습니다. 예를 들어 다음과 같은 코드처럼 11 줄의 호출을 주석으로 막고 a.create()를 사용하면 값 열에 멤버 함수가 없다는 오류가 발생하게 됩니다.

struct A
{
    int get(void)
    {
        return 0;
    }
};
int _tmain(int argc, _TCHAR* argv[])
{
    A a;
    //a.get();
    return 0;
}

맴버 연산자

맴버 연산자를 사용할 수 있으나 같은 연산자가 const와 비-const 모두 있을 경우에는 호출이 불가합니다. 그렇다면 const와 비-const가 모두 있는 일반 함수는 어떨까요? 물론 정상적으로 호출되지 않습니다. 하지만 일반 함수의 경우에는 값 열에 심볼이 모호하다라는 오류가 나는 반면 연산자의 경우에는 해당 연산자가 없다라는 오류가 납니다. 사용한 예제 코드는 아래와 같습니다.

struct A
{
    int i;
    int get(void) const { return i;}
    int get(void) { return ++i; }
    A operator+(const A& _a) const
    {
        A a;
        a.i = _a.i + i;
        return a;
    }
    A operator+(const A& _a)
    {
        A a;
        a.i = _a.i + (++i);
       return a;
    }
};
int main()
{
    A a1;
    const A&
    a2 = a1;
    a1.get();
    a2.get();
    A a3 = a1 + a2;
    A a4 = a2 + a1;
    return 0;
}

범위 연산자(Scope Operator) 우선 순위

디버거 식의 범위 연산자는 소스에서 사용되는 범위 연산자와는 달리 낮은 우선 순위를 갖습니다. 디버거 식에서 범위 연산자의 우선 순위는 기본(Base) 및 후위(Prefix) 연산자(->, ++, --)와 단항(Unary) 연산자(!, &, * 등) 사이입니다.

기호 형식(Symbol Formats)

모든 디버그 정보(Full Debug Information, /Zi 또는 /ZI)를 사용해서 컴파일 된 모듈에 있는 심볼의 경우 소스 코드에 있는 것도 동일한 형식으로 사용할 수 있지만 명식적인 __cdecl, __fastcall 또는 다른 디버그 옵션을 사용할 경우에는 적절한 꾸며진 이름을 사용해야 합니다.

어셈블리 구문

어셈블리 구문은 제가 어셈블리에 약해서 넘어 가도록 하겠습니다.

의사 변수

의사 변수는 실제 코드에는 정의되어 있지 않지만 디버거에서 특정 정보를 확인하기 위해 제공하는 특수 변수로 일반 변수 사용하는 것과 같은 방식으로 식에서 사용할 수 있습니다. 예를 들어 현재 응용 프로그램에 할당된 핸들을 개수를 확인하려면 $handle이라는 의사변수를 이용해 확인할 수 있습니다. 보다 자세한 의사 변수는 MSDN을 참고하시기 바랍니다.

문맥 연산자

문맥 연산자는 중단점, 변수 이름, 구문의 문맥을 지정하는 용도로 사용하는 것으로 다음과 같은 용법을 갖고 있습니다.

  • {[function],[source_file],[module]} location
  • {[function],[source_file],[module]} variable_name
  • {[function],[source_file],[module]} expression

Test.cpp 파일의 10번째 줄에 중단점을 설정하려면 다음과 같은 구문을 사용할 수 있습니다.

{,Test.cpp,}@10

같은 방식으로 msvcr90d.dll에 있는 _crtBreakAlloc 변수를 조사하기 위해서는 다음과 같은 구문을 사용할 수 있습니다.

{,,msvcr90d.dll}_crtBreakAlloc

참고로 위 변수에 값 열에서 값을 설정하고 프로그램을 수행하며 프로그램 수행 후 설정한 값에 해당 번째의 힙(Heap) 할당 순간에 프로그램이 중단되도록 하는 변수입니다. 메모리 누수 검사를 위해 중단점 설정에 유용하게 사용됩니다.
만일 문맥 연산자에서 함수만 설정할 경우에는 다음과 같이 쉼표를 생략할 수 있습니다.

{Foo}@100

디버거에서 식이 해석될 때 기호의 검색 순서는 MSDN을 참고하시기 바랍니다.

형식 지정자


끝으로 살펴 볼 디버그 식 구성요소는 형식 지정자로 디버그 식에 지정된 구문의 결과가 어떤 형식으로 값 열에 나타나게 하는지 지정하는 것입니다. 가령 exp라는 정수형 구문이 0x0065라는 값을 갖고 있다고 가정합니다. 이 값을 정수가 아닌 문자 값으로 보길 원한다면 exp 뒤에 쉼표와 함께 형식 지정자 c를 지정하면 값 열에 101 'e'과 같이 문자가 표시됩니다. 다른 형식 지정자는 MSDN을 참고하시기 바랍니다.

형식 지정자는 조금 확장해서 사용자 정의 데이터 타입의 출력 형식을 사용자가 원하는 형태로 지정할 수 있습니다. 비주얼 스튜디오 2005, 2008 기준으로 %VSINSTALLDIR%\Common7\Packages\Debugger 디렉터리에 있는 autoexp.dat 파일이 있는데 이 파일에 기술된 규칙에 따라 원하는 형태로 사용자 지정 데이터 타입의 출력 형식을 추가 지정할 수 있습니다.

autoexp.dat의 규칙은 해당 파일에 지정되어 있는데 자세한 규칙 사용법과 이를 이용한 다양한 출력 방식, 디버그 식을 입력하는 방법, 그리고 단순히 식을 조사하는 것뿐만 아니라 식에 값을 지정하는 방법과 이에 따른 파생 작업에 대해서는 다음 번 포스팅에 다루도록 하겠습니다.

Creative Commons License
이 저작물은 크리에이티브 커먼즈 저작자표시-비영리-동일조건변경허락 2.0 대한민국 라이선스에 따라 이용할 수 있습니다.