출처: http://www.imaso.co.kr/?doc=bbs/gnuboard.php&bo_table=article&wr_id=35295


개발자에게 필요한 디버깅 기초 상식들

우리는 개발자다. 개발자는 프로그램을 개발하는 역할을 담당한다. 일반적으로 프로그램을 작성하는 과정은 기획·분석·설계 등의 작업을 거친 후 코드를 작성하는 개발 단계를 거치고 테스트 및 배포·운영 단계까지로 나눠 볼 수 있으며, 개발자는 주로 코드 작성과 테스트, 배포의 많은 부분을 담당하게 마련이다. 개발자가 프로그램을 작성하는 과정에서 필수 불가결하게 거치게 되는 것이 바로 디버깅이다. 혹자는 프로그램을 작성하는 것은 곧 버그를 만드는 과정이라고 비유할 만큼 개발자들에게 버그는 피할 수 없는 것으로 여겨진다. 따라서 버그에서 자유로운 개발자는 없다고 할 수 있겠다. 가장 훌륭한 개발자는 버그를 만들지 않거나(불가능하다) 버그를 최소화하는 코드를 작성하는 개발자일 것이다. 필자를 비롯한 많은 개발자들이 버그를 최소화하려고 노력하지만 어쩔 수 없이 버그는 발생하게 마련이고 우리들의 근무 시간을 늘리고 자유 시간을 줄이며 수많은 커플을 솔로로 형 변환 시키는 주범이 되곤 한다.

유경상 http://www.simpleisbest.net | 현재 드윈 테크놀러지에 근무하고 있다. COM+, 닷넷 등 주로 마이크로소프트의 기술에 대한 교육과 컨설팅을 맡고 있다. 머리가 백발이 돼서도 기술 서적을 집필하는 것이 꿈이다.

개발자가 프로그램을 작성하는데 있어서 피할 수 없는 것이 버그라면 버그를 최소화하는 기법을 사용하거나 버그가 발생했을 때 이를 해결하는 노하우를 많이 터득하는 것이 좋다. 훌륭한 개발자라면 버그를 예방하기 위해 다양한 기법들을 사용할 것이다. 하지만 필자와 같은 일반 개발자라면 버그가 발생했을 때 이 버그를 빠르게 해결하는 방법을 찾는 것이 더 빠를 수도 있다.
버그를 해결하는 방법은 매우 다양하다. 가장 먼저 떠올릴 수 있는 것이 바로 디버거(debugger)를 사용하는 디버깅(debugging)일 것이다. 그래서 이번 컬럼에서는 디버거를 사용하는 디버깅에서 개발자들이 기본적으로 알아야 할 기초 상식에 대해 이야기하고자 한다.

디버깅을 하고자 소스코드에 중단점을 설정하고 비주얼 스튜디오의 [F5]를 눌렀음에도 불구하고 설정한 중단점이 작동하지 않는 경험을 해 봤을 것이다. 또 스마트 클라이언트나 클릭원스(click-once)와 같은 배포 시나리오에서 디버거를 사용했을 때 원하는 곳에서 중단점이 작동하지 않는 경우를 겪어 본 독자들도 다수 있으리라 생각한다. 이러한 상황에서 “왜 나의 중단점이 작동하지 않는가?”라는 문제를 해결하고자 한다면 반드시 알아야 할 디버거 기초 상식이 이번 컬럼의 주된 내용이다.

디버거, 넌 누구냐
디버거는 원리로 보자면 그다지 복잡한 프로그램이 아니다. 필자도 디버거 같은 프로그램은 매우 수준높은 소수의 사람들만이 작성할 수 있는 것으로 생각한 때가 있었다. 디버거는 디버깅 대상(debugee) 프로세스의 메모리를 읽고 쓸 수 있으며, 중단점과 같은 디버깅 대상 프로세스가 발생하는 다양한 디버그 이벤트에 반응하는 프로그램이다. 디버거는 일반적으로 디버깅 대상 프로세스를 새로 시작하거나 이미 수행중인 프로세스에 디버거를 연결(attach)하게 된다.

일단 디버깅 대상 프로세스와 연결이 끝나면 디버거는 DLL 로드, 중단점, 싱글 스텝(single-step), 예외 발생과 같은 디버그 이벤트를 기다리고 이들 디버그 이벤트 중 하나가 발생하면 그 이벤트를 처리하는 과정을 반복하게 된다.이 반복 루프를 디버거 루프(debugger loop)라고 한다. 디버깅 대상 프로세스와 연결해 다른 프로세스의 메모리를 읽거나 쓰는 작업 혹은 디버그 이벤트를 수신하는 작업은 디버거 혼자서 스스로 처리할 수는 없다. 중단점이나 싱글 스텝과 같은 기능은 디버깅 대상 프로세스가 운영체제로 디버그 트랩(debug trap)을 발생하고 디버그 트랩이 발생하면 운영체제가 디버거에게 디버그 이벤트를 발생시켜 준다.

따라서 디버거는 반드시 운영체제 커널의 도움을 받아야 하며, Windows 커널은 이와 같은 디버깅 지원 기능을 보유하고 있다.

<그림 1>은 운영체제 커널과 디버거 그리고 디버깅 대상 프로세스 사이의 관계를 보여준다.


<그림 1> 커널, 디버거, 디버깅 대상 프로세스의 관계

사실 간단한 디버거는 C/C++ 언어나 P/Invoke를 사용해 C#으로도 어렵지 않게 작성할 수 있다. 하지만 우리가 디버거를 작성하지는 않으므로 디버거의 원리는 이 정도만 알고 있으면 된다. 우리는 이미 작성된 훌륭한 디버거들을 ‘잘’ 사용하기만 해도 훌륭한 개발자 소리를 들을 수 있기 때문이다. 디버거를 작성하는 방법에 대해 관심이 있는 독자라면 디버깅을 다루는 다른 참고자료를 살펴보길 바란다.

왜 중단점이 작동하지 않을까

우리가 디버거를 통해 수행하는 통상적인 디버깅 작업들은 중단점을 설정해 프로그램의 수행을 중단(break)하고 여러 변수들의 값을 조사(inspection)하며, 필요에 따라 코드를 한 단계씩 수행(single-step)하는 작업이라 할 수 있다. 비주얼 스튜디오와 같은 강력한 디버거를 포함하는 개발도구들은 이러한 작업을 너무나 쉽게 해 준다. 우리는 [F9] 키를 눌러 소스상에 중단점을 설정할 수 있고, 이 중단점에 의해 프로그램이 중단되면 로컬 변수의 값이나 객체의 값들을 살펴볼 수 있다. 그리고 [F10] 혹은 [F11] 키를 이용해 코드를 라인 단위로 한 스텝씩 수행할 수도 있다.

디버깅을 시작하는 가장 강력한 기법이 바로 중단점을 설정하는 것이다. 그럼 여기서 중단점이 어떻게 작동하는지 가볍게 살펴보자. 대부분 중단점은 CPU가 인식하는 중단점 기계 명령(INT 3H)이 수행될 때 운영체제 커널로 트랩이 발생하는 하드웨어적인 기능을 사용한다. 운영체제 커널은 중단점 트랩이 발생될 때 중단점이 발생한 프로세스에 디버거가 연결돼 있다면 디버거에게 중단점 디버거 이벤트를 발생시켜준다. 만약 디버거가 아직 연결돼 있지 않다면 JIT(just-in-time) 디버깅을 위해 <화면 1>과 같은 JIT 디버거 관리자 화면을 표시해 준다. JIT 디버거가 구동되면 디버깅을 위한 디버거를 선택할 수 있다.


<화면 1> 비주얼 스튜디오 JIT 디버거

중단점은 중단점 명령이 수행돼야만 디버거로 제어가 넘어간다는 것을 명심하자.

그렇다면 디버거는 어떻게 중단점을 설정하는 것일까? 중단점이 설정되면 디버거는 중단점이 설정된 디버깅 대상 프로세스 주소(address)의 명령어를 중단점 명령으로 바꾼다. 물론 중단점 명령으로 바꾸기 전의 명령은 디버거 내부에 기억해 둔다. 그리고 중단점에 의해 제어가 디버거로 넘어오면 곧바로 기억해 뒀던 원래 명령으로 원상 복구를 해 준다. 이렇게 함으로써 마치 마술처럼 제어가 디버거로 넘어오게 되는 것이다.

여기서 중단점은 프로세스상의 주소에 설정된다는 것에 주목할 필요가 있다. 하지만 우리가 비주얼 스튜디오상에서 중단점을 설정할 때는 메모리상의 주소가 아닌 소스코드상의 위치를 사용한다. 그렇다면 디버거는 어떻게 소스코드상의 위치에서 메모리 주소를 알아내 중단점 명령을 설정하는 것일까? 그에 대한 답은 바로 디버그 심볼(debug symbol)에 있다. 잠시 후 좀 더 상세히 설명하겠지만 디버그 심볼에는 소스코드상에 설정된 중단점을 수행 중인 프로세스의 메모리 주소상의 중단점으로 매핑하기 위해 필요한 모든 정보가 기록돼 있다. 디버거는 바로 이 디버그 심볼을 사용해 소스코드상의 중단점을 설정하는 것이다.

분명히 소스코드상에 중단점을 설정했으나 중단점이 작동하지 않는 경험을 해 본 독자들이 있을 것이다. 만약 이런 상황이 발생한다면 두 가지를 의심해 볼 수 있다. 첫 째, 중단점까지 코드가 수행되지 않은 것이다. 버그나 기타 다양한 이유에서 제어의 흐름이 중단점까지 가지 않은 상황을 의심해 볼 수 있다는 의미다. 둘 째, 디버그 심볼이 디버거에 의해 로드되지 않은 경우도 있을 수 있다. 디버깅이 시작되면 디버거는 소스코드상에 설정된 중단점을 메모리 주소상의 중단점 설정으로 바꾸고자 시도한다. 이 때 디버거는 디버그 심볼을 필요로 한다. 디버거가 적당한 디버그 심볼을 찾지 못한다면 소스상에 설정된 중단점은 활성화되지 않는다.

만약 디버깅 중에 중단점이 제대로 작동하지 않는 등의 문제가 발생되면 디버거가 디버그 심볼을 찾아 로드했는지를 확인하는 것이 좋다. 비주얼 스튜디오나 WinDbg와 같은 디버거는 디버거가 로드한 모듈들의 정보를 제공하는 기능을 갖고 있고, 이 모듈정보 내에서 해당 모듈에 대한 디버그 심볼을 로드했는지의 여부도 알아낼 수 있다. 비주얼 스튜디오의 경우 Debug 쭭 Windows 쭭 Modules 메뉴를 선택하면 <화면 2>와 같은 모듈 창이 나타나고 로드된 모듈들과 각 모듈들의 디버그 심볼의 로드상태를 확인할 수 있다. 만약 디버거가 자신이 디버그 하고자 하는 모듈의 디버그 심볼을 찾지 못했다면 그 모듈에 대한 소스 수준의 중단점 설정이나 소스 수준의 단계별 수행 등의 작업 등은 수행할 수 없게 된다. 그만큼 디버깅에서 중요하며 필수 불가결한 것이 바로 디버그 심볼인 것이다.


<화면 2> Modules 디버그 정보 화면

디버그 심볼
디버그 심볼은 코드를 컴파일해 모듈(DLL, EXE)을 생성하는 과정에서 컴파일러에 의해 만들어 진다. 비주얼 스튜디오를 사용하는 경우 디버그 및 릴리스 빌드를 수행할 때 모두 디버그 심볼 파일인 .PDB 파일이 기본적으로 생성된다. 심볼 파일 생성 여부는 <화면 3>과 같이 프로젝트 속성 쭭 빌드 쭭 고급 설정에서 제어할 수 있다. 디버그 빌드의 경우 디버그 정보가 Full로 설정되며, 릴리스 빌드는 pdb-only가 기본값이다. 디버그 정보가 Full이라 함은 디버깅을 위해 디버그 심볼인 PDB 파일 생성 뿐만 아니라 모듈에 디버그를 위한 상세 정보가 포함되며 디버깅을 방해하는 코드 최적화를 수행하지 않음을 의미한다. 반면 pdb-only 설정은 모듈은 코드 최적화 등의 작업을 수행하고 디버그 심볼만을 생성한다는 의미다.

릴리스 빌드에서 디버그 심볼을 생성한다는 점을 의아해 하는 독자들이 있을지도 모르겠다. 디버그 심볼은 디버깅에만 사용되는 것이 아니다. 디버그 심볼은 성능 측정을 위한 프로파일링(profiling)이나 메모리 덤프(dump)를 분석하는데도 필수적인 요소다. 프로파일링의 경우 어떤 함수(메소드)가 몇 번 호출됐고, 이 호출이 얼마의 시간을 소요했는지를 기록하는데, 기록하는 시점에서는 메모리상의 주소를 사용한다. 하지만 분석 데이터를 표시할 때에는 메모리상의 주소가 아닌 함수의 이름이 필요하다. 이 때 디버그 심볼을 사용하면 해당 메모리 주소가 어떤 이름의 함수인지를 알아낼 수 있는 것이다. 비슷하게 프로그램이 예외나 기타 다른 이유로 크래쉬(crash)됐을 때 생성할 수 있는 메모리 덤프를 분석할 때에도 프로그램의 어디에서 크래쉬가 발생했는지 알아내려면 메모리 덤프상의 메모리 주소가 어느 함수를 나타내는지 그리고 크래쉬가 발생한 시점에서 변수들의 값은 어떠했는지 알아내기 위해 디버그 심볼이 필요하다. 이런 이유에서 릴리스 빌드에서도 디버그 심볼을 생성하는 것이 좋다. C/C++의 기본 릴리스 빌드 설정은 디버그 심볼을 생성하지 않지만 닷넷의 기본 릴리스 빌드 설정은 디버그 심볼을 생성하도록 돼 있다.


<화면 3> 디버그 정보 설정

디버그 심볼을 생성하는 것은 컴파일 시간을 약간 늘어나게 하지만 프로그램의 성능을 떨어뜨리지는 않는다. 디버그 심볼에는 메모리상의 주소를 특정 함수의 이름이나 변수의 이름으로 매핑하는 데 필요한 정보 혹은 특정 변수의 타입이 어떠한지에 대한 정보와 메모리상의 주소를 소스의 특정 위치로 매핑하는 데 필요한 정보들이 포함돼 있을 뿐 프로그램의 런타임에 영향을 주는 정보는 없다. 실제로 프로그램이 수행되는 데 디버그 심볼은 전혀 영향을 미치지 않는다는 점을 잘 기억해 두자. 디버그 심볼 파일의 확장자가 PDB이고 이 PDB라는 이름이 Program DataBase인 이유도 프로그램 심볼에 관련된 데이터 베이스를 제공하기 때문이다.

디버그 심볼 파일은 디버거에게 매우 핵심적인 정보를 제공하기 때문에 상당히 주의깊게 관리된다. 소스코드 중 단 한 라인, 한 글자만 바뀌어도 엉뚱한 곳에 중단점이 설정될 수 있기 때문에 컴파일러는 디버그 심볼 파일을 생성하면서 고유의 GUID를 할당하고, 이 GUID를 모듈에도 기록해 둔다. 그리고 이 GUID는 디버거가 디버그 심볼 파일을 찾아 로드할 때 디버그 심볼이 해당 모듈이 컴파일 될 때 생성된 심볼 파일인가를 확인하는 용도로 사용된다.

예를 들어 컴파일러가 A.DLL을 컴파일 할 때 생성된 A.PDB를 생성한다면 고유의 GUID가 생성된다. 이 GUID는 A.DLL과 A.PDB에 모두 기록돼 있을 것이다. 디버거는 A.DLL에 대한 심볼을 로드할 때 A.DLL에 기록된 GUID와 A.PDB에 기록된 GUID를 비교해 같은 경우에만 해당 심볼을 사용한다. 만약 A.DLL이 재컴파일 된다면 새로 생성되는 A.PDB는 이전 GUID와 다른 GUID를 할당받는다.

물론 이 새로운 GUID는 새로 컴파일 된 A.DLL에도 기록된다. 만약 디버거가 A.DLL에 대한 심볼 파일을 로드할 때 A.PDB에 기록된 GUID와 A.DLL에 기록된 GUID가 일치하지 않는다면 그 심볼 파일은 로드되지 않는다. 따라서 모듈의 버전이 바뀌어 감에 따라 정확한 디버깅을 하고자 한다면 빌드를 할 때마다 PDB 파일 역시 관리해야 하며 특히 사용자에게 배포될 빌드라면 해당 빌드에 대한 PDB 파일을 저장해 둬야만 한다. 그래야만 메모리 덤프 분석 등의 기술 지원이 가능할 것이다.

디버거를 작성할 것이 아니라면 PDB 파일의 내부 구조와 이 PDB 파일에 기록되는 다양한 정보들을 모두 알 필요는 없다. 디버거를 ‘잘’ 사용하기 위해서라면 개발자는 PDB 파일을 생성하는 방법과 이 PDB 파일 내에 변수, 함수, 소스 위치정보 정도가 포함돼 있다는 사실만 알고 있으면 된다. 만약 PDB 파일의 상세한 구조에 관심이 있다면 MSDN이나 디버깅 관련 서적을 참고하길 바란다.

디버그 심볼 로드

앞서 디버거는 디버그 심볼을 찾아 로드한다고 했다. 그렇다면 디버거는 어떻게 디버그 심볼 파일을 찾아서 로드할까. 디버거는 심볼을 로드하기 위해 심볼 엔진을 사용한다. 그리고 심볼 엔진은 미리 정의된 규칙과 다분히 경험적인 방법을 통해 디버그 심볼을 로드한다.

먼저 디버거는 모듈과 동일한 디렉토리에 존재하는 디버그 심볼 파일을 찾는다. 디렉토리에서 디버그 심볼 파일을 찾더라도 디버거는 앞서 언급한 GUID를 통해 디버그 심볼 파일의 유효성을 확인한다. 만약 심볼 파일이 모듈과 일치하지 않는 GUID를 갖고 있다면 이 심볼 파일은 무시되고 다른 곳에서 심볼 파일을 찾는다. 또한 모듈 디렉토리에서 심볼을 찾지 못하면 디버거는 모듈에 기록돼 있는 디버그 심볼 파일을 찾아 로드하는 작업을 시도한다.

모듈이 컴파일 될 때 디버그 심볼 파일을 생성하는 경우 해당 모듈에는 생성된 디버그 심볼 파일의 물리 경로가 기록된다. 디버거는 모듈에 기록된 디버그 심볼 파일을 로드하려는 시도를 한다. 물론 이 경우에도 심볼 파일의 GUID를 통해 유효성 검사는 수행된다. 이 두 가지 경우에서 모두 심볼 파일을 찾지 못했다면 지정된 심볼 경로에서 심볼 파일을 찾으려고 시도한다. PATH 환경 변수와 비슷하게 _NT_ SYMBOL_PATH 환경 변수는 심볼 파일을 찾기 위한 경로를 지정해 줄 수 있는데, 디버거는 이 경로를 사용해 심볼 파일을 찾으려는 시도를 하는 것이다. 다음은 _NT_SYMBOL_PATH의 설정예다.

SET _NT_SYMBOL_PATH=D:₩Temp₩MySymbols;D:₩Proj ects₩Symbols

앞의 심볼 경로 예제는 세미콜론으로 구분된 두 개의 디렉토리를 지정해 디버거가 심볼을 찾을 때 이 두 디렉토리를 검사할 것을 지시하는 것이다.

_NT_SYMBOL_PATH를 지정해 심볼을 찾을 때는 심볼 서버(Symbol Server)라고 하는 특별한 방식이 사용된다. 심볼 서버는 모듈들의 심볼들을 저장하는 데이터베이스로서 하나의 모듈에 대해 매 빌드마다 달라질 수 있는 심볼들을 저장해 두고 디버거가 요청하는 GUID를 가진 심볼을 다운로드 해 주는 서버를 말한다. 심볼 서버는 말이 심볼 서버이지 실제로는 단순한 파일 시스템 구조를 갖는다. 다만 HTTP 혹은 네트워크 폴더 공유를 통해 원격 컴퓨터에 존재하는 심볼을 다운로드 할 수 있는 능력을 갖고 있기 때문에 심볼 서버라는 이름을 사용할 뿐이다. 마이크로소프트는 윈도우 운영체제의 모듈들과 닷넷 프레임워크 어셈블리들에 대한 심볼들을 심볼 서버로서 제공한다.

마이크로소프트의 공용 심볼 서버의 URL은 http://msdl. microsoft.com/download/symbols로, 심볼 경로에 이 URL을 사용할 수 있다. 심볼 서버가 관여되면 매번 심볼을 서버로부터 다운로드한다는 것은 상당히 비효율적으로 보인다. 심볼 파일의 크기는 적게는 수 백 KB에서 크게는 수 십 MB에 달하기 때문이다. 따라서 심볼 서버가 관여되면 로컬 컴퓨터에는 다운로드 된 심볼들을 캐시할 수 있다.

이렇게 심볼 서버의 경로와 심볼 캐시를 모두 NT_SYM BOL_PATH 경로에 설정할 수 있는데, 다음과 같이 SRV 키워드를 포함하는 문법을 사용한다.

SRV*<symbol cache directory>*<symbol server URL or UNC>

SRV 문자열은 심볼 서버를 나타내는 키워드이며, 심볼 캐시 디렉토리와 심볼 서버의 URL 혹은 UNC를 ‘*’ 문자로 구분해서 나타낸다.

다음은 심볼 서버를 포함하는 _NT_SYMBOL_PATH 설정의 예를 보여준다.

SET _NT_SYMBOL_PATH=D:₩Project₩Symbols;SRV*D:₩Symbols*
http://msdl.microsoft.com/download/symbols

이 예에서 디버거는 먼저 D:₩Projects₩Symbols 폴더에서 심볼을 찾으려고 시도하고 이곳에서 심볼을 찾지 못하면 심볼 캐시 디렉토리인 D:₩Symbols 폴더를 찾는다. 여기에서도 심볼을 찾지 못하면 http://msdl.microsoft. com/download/symbols URL이 지시하는 심볼 서버에서 심볼을 다운로드해 캐시 디렉토리(이 경우 D:₩Symbols)에 기록하고 로드한다.

NT_SYMBOL_PATH 환경 변수를 설정하면 모든 디버거가 이 설정을 사용할 수 있다. 하지만 많은 경우 이러한 환경 변수를 설정하는 것이 귀찮을 뿐만 아니라 약간의 오타로 인해 디버거가 심볼을 찾지 못할 수도 있다. 따라서 대부분의 디버거는 _NT_SYM BOL_ PATH 뿐만 아니라 디버거 고유의 심볼 경로를 지정하는 방법을 제공한다. 비주얼 스튜디오 역시 예외는 아니어서 Tools 쭭 Options 쭭 Debugging 쭭 Symbols 메뉴에서 <화면 4>와 같이 심볼 경로를 지정할 수 있도록 돼 있다.

<화면 4> 비주얼 스튜디오의 심볼 경로 지정

지금까지 설명한 내용을 바탕으로 비주얼 스튜디오가 어떻게 심볼을 찾는지 테스트를 수행해 보자. 명확한 테스트를 위해 D:₩Temp 디렉토리에 <리스트 1>과 같은 코드를 temp.cs 라는 이름으로 저장하고 디버그 정보를 포함해 컴파일되도록 커맨드 라인상에서 다음과 같은 명령을 수행해 보자.

csc /debug temp.cs

컴파일을 수행했다면 D:₩Temp 디렉토리에는 temp. exe와 더불어 temp.pdb 파일이 생성되고, Temp.exe를 수행시키면 JIT 디버거가 구동될 것이다. 윈도우 비스타 이상일 경우에는 디버그를 묻는 대화상자가 먼저 나타날 수도 있다. 이 때는 디버그를 선택해 JIT 디버거가 구동되도록 해야 한다. JIT 디버거에서 비주얼 스튜디오를 선택해 디버거로서 비주얼 스튜디오가 사용되도록 한다.

<리스트 1> 테스트를 위한 간단한 코드
using System;
 
class Program
{
   static void Main(string[] args)
   {
      throw new Exception("Test");
   }
}

비주얼 스튜디오 디버거가 구동되면 정확하게 예외가 발생한 지점에서 수행이 중단된 채로 디버거에게 제어가 넘어올 것이다. 신기한 것은 디버거가 어찌 알았는지 temp.cs 파일을 로드해 소스상의 정확한 예외 발생 위치에서 중단됐음을 표시한다는 것이다. 디버거가 소스 파일까지 찾을 수 있는 비결은 PDB 파일에 기록된 소스 정보다. PDB 파일에는 심볼 정보뿐만 아니라 소스 파일에 대한 정보까지 포함돼 있다. 따라서 디버거는 심볼 파일을 이용해 예외 발생에 의해 중단된 메모리 주소로부터 역으로 소스코드의 위치를 알아낼 수 있으며, PDB 파일에 소스코드의 경로 역시 기록돼 있으므로 해당 소스파일을 찾을 수 있는 것이다. 만약 D:₩Temp 디렉토리에 temp.cs 파일이 존재하지 않는다면 비주얼 스튜디오는 소스 파일을 선택하라는 소스코드 열기 대화상자를 표시하게 된다. 사용자가 소스파일 선택을 취소하는 경우 소스 수준의 디버깅은 할 수 없다. 하지만 여전히 호출 스택이나 로컬 변수 등의 정보는 살펴볼 수 있으며 어셈블리 수준의 디버깅도 가능하다.

앞의 <화면 2>와 같은 모듈 정보를 열어 보자. 모듈 디버그 화면은 temp.exe 모듈에 대해 temp.pdb 파일을 D:₩Temp 디렉토리에서 로드했다는 것을 보여줄 것이다.

비주얼 스튜디오를 닫고 temp.exe를 D:₩에 복사한 후 D:₩디렉토리에서 temp.exe를 수행시켜보자. 마찬가지로 JIT 디버거가 구동될 것이다. 다시 비주얼 스튜디오를 선택해 보자. 

:₩Temp 디렉토리에서 temp.exe를 수행시켰을 때와 마찬가지로 심볼이 로드되고 소스코드 역시 로드됐을 것이다. 그리고 모듈 정보 화면을 다시 열어 보면 심볼이 D:₩Temp에서 로드됐음을 알 수 있을 것이다. 이로써 temp.exe에 기록돼 있는 PDB 파일의 위치를 통해 디버거가 심볼을 찾아냈음을 알 수 있다.

심볼 경로를 찾는 것을 테스트 해 보자. D:₩Temp에서 temp.pdb 파일을 삭제하고 다시 temp.exe를 구동해 보자. 마찬가지로 JIT 디버거가 구동될 것이다. 동일하게 비주얼 스튜디오를 디버거로 선택하자. 비주얼 스튜디오 디버거가 구동된 후에는 이전과 사뭇 다른 모습을 보게 될 것이다. 소스코드 파일도 로드되지 않았을 것이고 소스코드를 찾을 수 없다는 화면(비주얼 스튜디오 2010) 혹은 대화상자(비주얼 스튜디오 2008)가 나타날 것이다. 모듈 디버그 화면을 불러보면 temp.pdb 파일을 찾을 수 없다는 것을 알 수 있다. 디버거가 심볼을 찾기 위해 검색한 디렉토리들을 살펴보고 싶다면 모듈 디버그 화면에서 모듈을 선택하고 마우스 오른쪽 버튼을 클릭해 심볼 로드 정보 메뉴를 선택하면 된다. <화면 5>의 심볼 로드 정보 대화상자는 디버거가 심볼 파일을 찾기 위해 검색한 디렉토리들과 심볼을 로드하지 못한 이유를 나열해 준다. 만약 디버깅 중에 심볼이 로드되지 않았다면 반드시 이 대화상자의 내용을 검토해 왜 디버그 심볼이 로드되지 않았는지 확인할 필요가 있다. <화면 5>는 앞서 설명했던 심볼 로드 순서에 따라 로컬 디렉토리, 모듈에 기록된 심볼 파일 위치, 심볼 경로, 심볼 서버 및 캐시를 찾고 있음을 보여준다. 비주얼 스튜디오의 경우 Windows 디렉토리에서도 심볼을 검색하는데, 이는 과거의 모듈들의 심볼 위치가 Windows 디렉토리였기 때문이다. 디버거가 경험적인(heuristic) 방법을 통해 심볼을 찾는다고 하는 이유가 바로 여기에 있다.


<화면 5> 심볼 로드 정보 대화 상자


디버깅에 응용

지금까지 살펴 본 내용을 실제 디버깅에 응용해 보자. 먼저 디버깅에 관련된 내용을 간단히 정리해 보자면 디버거는 디버깅에 필요한 정보의 대부분을 디버그 심볼로부터 얻는다. 디버그 심볼은 모듈(DLL, EXE)이 컴파일 될 때 컴파일러에 의해 생성되며, 모듈내에는 디버그 심볼 파일의 경로 및 GUID가 기록된다. 디버거는 심볼 파일이 필요하면 모듈에 기록된 디버그 심볼의 위치를 검색하고 로드를 시도한다. 만약 모듈에 기록된 디버그 심볼을 로드하지 못하면 심볼 경로를 순차적으로 검색하게 된다. 이 내용을 잘 기억해 두면 디버깅을 할 때 중단점이 작동하지 않는 등의 문제를 해결할 수 있다.

대부분의 경우 내 컴퓨터에서 빌드한 모듈은 내 컴퓨터에서 디버깅이 가능하다. 내 컴퓨터에서 빌드된 모듈이 서버에 복사되고 서버로부터 스마트 클라이언트나 클릭원스와 같은 배포 시나리오를 거치더라도 내 컴퓨터상에서는 디버깅이 가능하다는 것이다. 이유는 이렇다. 내 컴퓨터에서 빌드한 모듈은 내 컴퓨터상에 생성된 디버그 심볼의 경로를 모듈내에 기록하고 있다. 따라서 디버거가 디버그 심볼을 로드하고자 할 때 내 컴퓨터에 존재하는 디버그 심볼을 찾아내고 로드할 수 있다. 물론 이러한 가정은 모듈상에 기록된 디버그 심볼의 GUID 값과 내 컴퓨터에 존재하는 디버그 심볼의 GUID 값이 일치하는 경우에만 가능할 것이다. 만약 빌드된 모듈을 서버로 복사한 후 모듈을 다시 빌드하면 서버상에 존재하는 모듈에 기록된 디버그 심볼의 GUID와 로컬 컴퓨터에 존재하는 심볼의 GUID 값이 일치하지 않을 것이므로 디버거는 심볼을 로드하지 않을 것이다.

항상 내 컴퓨터에서 빌드된 모듈이 서버를 통해 배포되지는 않는다. 대부분의 경우 빌드 서버를 통해 빌드될 것이므로 모듈의 유효한 디버그 심볼은 서버상에 존재할 것이다. 이러한 상황에서 서버에서 배포된 모듈에 설정된 중단점은 유효하지 않다. 디버거가 디버그 심볼을 찾지 못하기 때문이다. 다른 개발자가 빌드한 모듈을 내 컴퓨터에서 디버그하는 경우도 마찬가지 상황이라 할 수 있다. 이러한 상황은 필자와 같이 기술지원이나 성능 튜닝에 관련된 컨설팅을 해야하는 경우 자주 발생한다. 모듈에 기록된 디버그 심볼의 경로는 이 모듈을 빌드한 개발자의 컴퓨터 경로일 것이기 때문이다.

이러한 상황이라면 빌드 서버나 모듈을 빌드한 개발자로부터 디버그 심볼 파일을 구해 빌드 당시의 폴더를 만들고 그 폴더에 디버그 심볼을 복사하는 것을 생각해 볼 수 있다. 하지만 모듈을 빌드한 컴퓨터의 물리 폴더 구조를 내 컴퓨터에 만드는 것보다는 심볼 경로에 디버그 심볼을 복사하는 것이 더 좋은 방법이다. 디버거는 모듈에 기록된 디버그 심볼을 찾지 못하면 심볼 경로를 검색하면서 디버그 심볼을 찾을 것이기 때문이다.

더 진보된 방법으로는 앞서 언급한 바 있는 심볼 서버를 구축하는 것이다. 심볼 서버는 파일 시스템 기반의 간단한 데이터 베이스이다. 디버거는 공유 폴더 혹은 HTTP 서버를 통해 심볼 서버로부터 심볼을 다운로드하고 심볼 캐시에 심볼 파일을 저장해 둘 수 있다. 심볼 서버를 통해 모듈이 어느 컴퓨터에서 빌드됐든 상관없이 심볼을 다운로드 해 디버깅에 사용할 수 있는 것이다.

심볼 서버를 구축하는 방법은 어렵지 않다. Windows SDK에 포함되어 있는 Windows Debugging Tools을 설치하거나 마이크로소프트 다운로드 사이트에서 이 도구를 다운로드 하면 심볼 서버를 구축하는데 필요한 도구와 도움말을 모두 얻을 수 있다. 지면 관계상 심볼 서버를 구축하는 방법에 대해 더는 언급하지 않겠다. 디버깅에 관련된 도서들이나 Windows Debugging Tools 도움말을 통해 심볼 서버 구축 방법을 알아 낼 수 있을 것이다.

닷넷 프레임워크 소스 디버깅

심볼 파일, 심볼 서버의 원리를 최대한으로 활용하는 전형적인 예는 닷넷 프레임워크 소스 디버깅일 것이다. 마이크로소프트는 닷넷 프레임워크 3.5부터 프레임워크 소스의 일부를 공개해 디버깅에 사용될 수 있도록 하고 있다. 닷넷 프레임워크를 빌드 할 때 생성된 디버그 심볼을 다운로드 할 수 있도록 제공되며, 이 디버그 심볼에는 프레임워크 소스를 다운로드 할 수 있는 인터넷 URL이 기록돼 있다. 따라서 디버거가 이 디버그 심볼을 로드하면 인터넷상에 존재하는 닷넷 프레임워크 소스를 다운로드 해 디버깅 할 수 있는 것이다.

한 가지 혼동하지 말아야 할 것은 마이크로소프트가 제공하는 심볼의 종류가 두 가지라는 것이다. 마이크로소프트의 공용 디버그 심볼과 닷넷 프레임워크 소스 디버깅용 심볼이 바로 그것들이다.

공용 디버그 심볼은 모든 운영체제 모듈과 닷넷 프레임워크 모듈에 대한 것이지만 로컬 변수에 대한 정보나 소스에 대한 정보가 제거돼 있다. 이 디버그 심볼을 사용하면 호출 스택상에서 어떤 운영체제 모듈의 어떤 함수가 호출됐는지 확인할 수 있지만 함수내의 로컬 변수값을 살펴볼 수는 없다(공용 디버그 심볼은 마이크로소프트의 심볼 서버 URL인 http://msdl.microsoft.com/download/ symbols에서 다운받을 수 있다).

닷넷 프레임워크 소스 디버그 심볼은 mscorlib.dll, system.dll, system.windows.forms.dll, system.web.dll과 같은 일부 모듈들에 대한 것으로서 디버깅에 필요한 모든 정보를 담고 있다. 게다가 소스코드의 위치가 소스서버로 지정돼 있어 디버거가 소스를 다운받을 수도 있다.

닷넷 프레임워크의 소스 수준 디버깅을 위해서는 비주얼 스튜디오에서 디버그 설정을 해야 한다. <화면 6>과 같이 디버깅 옵션 대화 상자에서 Enable Just My Code 옵션을 해제하고 Enable .NET Framework source stepping 옵션과 Enable source server support 옵션을 선택한 후 확인을 누르면 비주얼 스튜디오는 캐시를 위해 닷넷 프레임워크의 심볼들을 다운로드 할 것이다. 다운로드 된 디버그 심볼들은 이미 <화면 4>에서 설정해 뒀던 심볼 캐시 디렉토리에 기록된다.


<화면 6> 닷넷 프레임워크 소스 디버깅 설정

이제 프로그램에서 닷넷 프레임워크의 메소드 혹은 속성을 액세스하는 적당한 위치에서 중단점을 설정하고 디버깅을 시작한다. 설정한 중단점에 의해 디버거가 구동되면 마우스의 오른쪽 버튼을 클릭하면 나타나는 메뉴에서 Step into specific 메뉴를 선택한다(<화면 7> 참조). 이 메뉴를 선택함에 따라 소스 서버로부터 소스를 다운로드 한다는 보안 경고 대화상자가 나타날 것이다. 확인을 누르면 비주얼 스튜디오는 소스 서버로부터 소스코드를 다운로드 해 디버깅을 계속 진행할 것이다. 여러분의 비주얼 스튜디오에 로드된 소스는 닷넷 프레임워크의 소스이며, 필요에 따라 코드의 수행을 계속 따라 가거나 변수의 값을 확인하는 등의 일련의 작업을 수행할 수도 있다. 반복적으로 프레임워크 소스를 따라 가고자 한다면 다운로드 된 닷넷 프레임워크 소스에 중단점을 지정할 수도 있다. 다운로드 된 소스는 <화면 4>에서 설정한 심볼 캐시의 src 하위 디렉토리내에 존재한다.


<화면 7> Step into Specific

닷넷 프레임워크 소스 디버깅은 내 코드의 문제점을 파악하는데 커다란 도움을 줄 수 있으며, 닷넷 프레임워크의 작동 방식을 이해하는데도 도움이 된다. 이러한 정보는 내 코드의 버그를 미연에 방지하거나 오류 발생시 문제 해결을 하는데도 도움을 준다. 아쉬운 점은 모든 닷넷 프레임워크 소스가 아니라 일부 모듈들에 대해서만 소스 수준의 디버깅이 가능하다는 점이다.

지금까지 디버깅을 보다 효율적으로 활용하기 위해 개발자가 알아야 할 디버거의 원리와 디버깅에 필수적인 디버그 심볼에 대해 살펴봤다. 시간과 지면의 제약으로 인해 다소 부족한 감이 있지만 이 정도의 정보로도 클릭원스와 같은 배포 시나리오에서 클라이언트측의 효과적인 디버깅에 활용될 수 있으리라 믿는다. 디버깅에 대해 더 많은 정보를 알고 있을수록 보다 효과적으로 디버깅을 할 수 있으며 문제를 해결하는 시간을 단축시킬 수 있다는 점을 명심하자.


+ Recent posts