심볼 서버로 디버깅 편하게 하기

  • Post author:
  • Post category:칼럼
  • Post comments:1 Comment
  • Post last modified:December 4, 2023

프로그램을 개발하고 유지보수하다 보면 버그가 있기 마련이다. 게임 속의 캐릭터가 벽을 뚫고 지나간다던가, 퀘스트 조건이 충족됐는데도 보상이 이뤄지지 않는 일이 생긴다. 버그야 가지각색이지만 그 중에서도 가장 곤란한 상황은 프로그램이 죽는 것이다. 게임 중에 갑자기 화면이 사라진다던가, 백엔드의 게임 서버가 죽어서 수천 명의 플레이어가 게임을 즐기지 못하는 일이 때로 벌어진다.

게임 클라이언트라면 유저의 집이나 PC 방, 서버라면 IDC 에서 죽기 때문에 프로그래머는 일이 벌어진 한참 뒤에야 “대체 무슨 일이 벌어졌지?”라고 생각하게 된다.

이런 상황 자체는 어찌 개선해볼 바가 없지만 다행히 사후 분석과 조치를 위한 수단은 있다. 프로그램이 죽을 때 남긴 크래시 덤프를 가져다 그 안에 담긴 프로그램의 상태 정보(스택, 힙 등)를 토대로 사후 분석을 시도하면 된다.

 

크래시 덤프와 심볼 파일

디버깅에 필요한 세 가지

프로그램을 디버깅하려면 심볼 파일(프로그램 데이터베이스 파일, PDB)이 필요하다. 심볼 파일에는 프로그램의 상태 정보와 소스 코드를 연결 짓는데 필요한 정보가 담긴다. 그래서 어떤 프로그램을 사후 분석하려면 다음과 같은 세 가지 파일이 필요하다.

  • 크래시 덤프
  • 심볼 파일
  • 소스 코드

 

버전 매칭

여기서 문제는 버전 매칭이다. 소스 코드를 서브버전(Subversion)으로 관리한다고 치자. 소스 코드와 프로그램은 계속해서 바뀌므로 버전이 올라간다. 프로그래머가 덤프 파일을 받으면 해당 덤프 파일을 생성한 프로그램의 버전을 알아내고 그 프로그램에 맞는 소스 코드를 찾아와야 한다.

이런 상황에서 버전을 쉽게 확인하기 위해서 보통 사용하는 방법은 소스 코드의 리비전 번호를 바이너리 파일(.exe, .dll)에 똑같이 기록하는 것이다.

파일/제품 버전

Earlgrey 의 경우 바이너리의 파일 버전 마지막 자리에 리비전 번호를 채워 넣는다.

image

  1. 프로그램이 죽으면 실행 파일의 파일 버전을 보고 소스 코드의 리비전을 알아낸다.
  2. 적합한 소스 코드를 가져와서 디버깅에 필요한 PDB 파일과 실행 파일(엄밀하게 PDB만 있으면 된다)을 빌드한다.
  3. 덤프 파일로 디버깅한다.

구닥다리 – 버전관리시스템을 이용하기

앞서 설명한 과정대로라면 덤프 파일이 올 때마다 빌드를 다시 해야 한다. 대규모 프로젝트는 빌드 시간이 만만치 않으므로 이러한 과정이 매우 귀찮다. 따라서 디버깅에 필요한 소스 코드와 PDB 파일을 함께 보관해야 한다.

image

꽤 많은 팀이 소스 코드와 심볼 파일을 함께 커밋하여 버전을 유지하곤 한다. 버전관리시스템만 있으면 되므로 별다른 인프라나 작업이 필요하지 않다. 하지만 그만큼 단점도 있다.

  • 소스 코드를 고칠 때마다, 또는 릴리즈할 때마다 PDB 파일을 잊지 않고 커밋해야 한다. 실제로 이를 잊어먹어서 한참을 헤매다 올바른 소스 코드를 찾아서 새로 빌드하는 경우가 종종 벌어진다.
  • 버전관리시스템에 쓸데 없는 부하를 더한다. 덤프파일에 맞는 소스 코드만 찾으면 바이너리와 심볼 파일은 생성 가능하므로 이 둘을 굳이 저장할 이유가 없다. 게다가 서브버전과 같은 시스템은 바이너리가 아닌 소스 코드를 다루는데 최적화됐다. 부하를 더하면 서버를 이용하는 사용자 전체가 괴로워질 수 있다.
  • 나중에 살펴볼 심볼 서버 기법은 덤프 파일만 있으면 그에 부합하는 소스 코드와 심볼 파일을 자동으로 가져온다. 그에 비해 이 방법은 번거롭기 짝이 없다.

이보다 후진 관리 방식으로는 파일 서버를 사용하기 등이 있으나 그다지 언급할 가치조차 느끼지 못 하므로 넘어간다.

도서관전쟁(圖書館戰爭) - 뜨아

 

샤방샤방 – 심볼 서버

위와 같이 번거로운 과정을 반드시 따라야 하는 것은 아니다. 그렇다면 이 글을 쓰지도 않았다. 심볼 서버란 훌륭한 대안이 있다.

 

뭐가 좋길래?

아직은 심볼 서버란 게 뭔지 모르지만 일단 구축만 하면 세상살이 참 편해진다. 심볼 서버를 구축한 후의 디버깅 과정은 다음과 같다.

  1. 덤프 파일을 더블 클릭한다.
  2. 디버깅 시작!

덤프 파일만 있으면 Visual Studio 가 그에 부합하는 소스 코드와 심볼 파일을 찾아와서 디버깅을 시작한다.

 

한계도 있다

온라인 게임은 크게 서버와 클라이언트로 나뉜다. 이때 서버는 퍼블리셔가 운영하는 IDC 에 두고 별도의 보안 조치를 하는 반면에 클라이언트는 보안이 취약하다고 가정해야 하는 외부로 배포된다. 그런 까닭에 손쉽게 클라이언트를 해킹하려는 시도가 많다.

개발사는 이러한 시도에 대해 핵쉴드와 같은 보안 솔루션을 도입해 맞선다. 대부분의 해킹이 의도적으로 덤프 파일을 생성해 프로그램 상태를 분석하는 기법에 의존한다. 따라서 보안 솔루션은 이러한 작업에 난항을 겪도록 실행 이미지를 조작하기도 한다. 이런 조작이 가해지면 해커가 아닌 실제 개발자조차 덤프 파일을 통해 정상적인 디버깅을 하기 어려워진다.

따라서 심볼 서버가 100% 제 기능을 하는 곳은 서버 뿐이다. 하지만 보안 솔루션을 적용하지 않은 내부 테스트용 클라이언트라면 여전히 심볼 서버가 충분한 효과를 발휘한다.

 

구축하기

이제부터 Earlgrey 에 구현한 MSBuild 스크립트를 바탕으로 심볼 서버를 어떻게 구축하는지 알아본다.

개인/공개 심볼

심볼은 크게 Private 심볼과 Public 심볼로 나뉜다. Visual Studio 에서 소스 코드를 빌드하면 나오는 .PDB 파일은 기본적으로 개인 심볼이다. 여기엔 디버깅에 필요한 모든 정보가 담긴다.

적절한 도구를 사용하면 개인 심볼을 공개 심볼로 바꿀 수 있다. 공개 심볼은 흔히 소스 코드를 공개하지 않는 SDK 와 함께 제공한다. 소스 코드가 없기 때문에 문제가 생겼을 때 무엇이 문제인지 감조차 잡기 힘들다. 그래서 함수 이름과 전역 변수에 대한 정보만 제공하는 경량화된 심볼을 제공하는 것이다.

하지만 대부분의 온라인 게임에서는 공개 심볼을 쓸 일이 없다. 플레이어는 프로그램의 내부에 대해 전혀 알 필요가 없고 퍼블리셔 역시 마찬가지기 때문이다.

 

준비물

CPU 아키텍처(x86/x64)에 맞는 버전을 골라 다운로드 받으면 된다. 여기서 살펴볼 구현 예제에서는 둘 다 받아놓고 스크립트가 현재 머신의 CPU 아키텍처를 확인해 맞는 버전을 사용한다.

MSBuild 예제이기 때문에 다음 두 가지도 필요하지만 어떤 작업이 필요한지 이해하는데 필수적인 건 아니다.

전체 소스 코드
<Project DefaultTargets="SymbolStore" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

<Import Project="msbuild.xml"/>

<PropertyGroup>
	<PerlRootDir>$(VendorDir)BuildToolstrawberry-perl-5.12.1.0-portable</PerlRootDir>
	<PerlZip>$(PerlRootDir).zip</PerlZip>

	<DebuggingToolsRootDir>$(VendorDir)BuildToolDebugging Tools for Windows</DebuggingToolsRootDir>
	<DebuggingToolsDir>$(DebuggingToolsRootDir)x86</DebuggingToolsDir>
	<DebuggingToolsDir Condition="'$(PROCESSOR_ARCHITECTURE)' == 'AMD64' OR '$(PROCESSOR_ARCHITEW6432)' == 'AMD64'">$(DebuggingToolsRootDir)x64</DebuggingToolsDir>
	<DebuggingToolsZip>$(DebuggingToolsDir).zip</DebuggingToolsZip>
</PropertyGroup>

<PropertyGroup>
	<SymbolStorageDir Condition="'$(SymbolStorageDir)' == ''">$(RootDir)$(OutputDir)Symbols</SymbolStorageDir>
	<SymbolProjectName Condition="'$(SymbolProjectName)' ==''">EarlGrey</SymbolProjectName>
</PropertyGroup>

<Target Name="UnzipPerl">
	 <Unzip
		ZipFileName="$(PerlZip)"
		TargetDirectory="$(PerlRootDir)"
		Condition="!Exists('$(PerlRootDir)')"
		/>
</Target>

<Target Name="UnzipSymbolIndexTool" DependsOnTargets="UnzipPerl">
	 <Unzip
		ZipFileName="$(DebuggingToolsZip)"
		TargetDirectory="$(DebuggingToolsRootDir)"
		Condition="!Exists('$(DebuggingToolsDir)')"
		/>
</Target>

<Target Name="PrepareSymbolIndexTool" DependsOnTargets="UnzipSymbolIndexTool">
</Target>

<Target Name="SymbolIndex" DependsOnTargets="PrepareSymbolIndexTool">
	<Error
		Text="A variable 'BinDir' can not be empty!"
		Condition="'$(BinDir)' == ''"
		/>

	<Exec
		Command="$(Quot)$(DebuggingToolsDir)srcsrvsvnindex.cmd$(Quot) /debug /source=$(Quot)$(TreeRootDir)$(Quot) /symbols=$(Quot)$(RootDir)$(BinDir)$(Quot)"
		WorkingDirectory="$(TreeRootDir)"
		ContinueOnError="false"
		IgnoreExitCode="false"
		/>
</Target>

<Target Name="SymbolStore" DependsOnTargets="SymbolIndex">
	<Exec
		Command="$(Quot)$(DebuggingToolsDir)symstore.exe$(Quot) add /o /r /f $(Quot)$(RootDir)$(BinDir)$(Quot) /s $(Quot)$(SymbolStorageDir)$(Quot) /t $(SymbolProjectName) /compress"
		WorkingDirectory="$(TreeRootDir)"
		ContinueOnError="false"
		IgnoreExitCode="false"
		/>
</Target>

</Project>

 

심볼을 저장할 공유 폴더 만들기

심볼 파일을 저장할 중앙 저장소를 생성한다. 여기서 $(SymbolStorageDir) 값이 해당 폴더의 경로를 나타낸다.

다른 이도 심볼 서버를 써야 하므로 공유 폴더로 만든다. 공개 심볼 같은 경우에는 웹 서버로 구축하기도 하나 온라인 게임에서는 그럴 필요까지는 없다. 여기서는 \SymbolServer 라는 머신에 StorageFolder 라는 공유 폴더가 있다고 치자( \SymbolServerStorageFolder )

 

심볼 인덱싱하기
<Target Name="SymbolIndex" DependsOnTargets="PrepareSymbolIndexTool">
	<Error
		Text="A variable 'BinDir' can not be empty!"
		Condition="'$(BinDir)' == ''"
		/>

	<Exec
		Command="$(Quot)$(DebuggingToolsDir)srcsrvsvnindex.cmd$(Quot) /debug /source=$(Quot)$(TreeRootDir)$(Quot) /symbols=$(Quot)$(RootDir)$(BinDir)$(Quot)"
		WorkingDirectory="$(TreeRootDir)"
		ContinueOnError="false"
		IgnoreExitCode="false"
		/>
</Target>

 

심볼 인덱싱은 .PDB 파일에 현재 소스 코드의 정보(SVN 의 경우 소스 코드의 저장소 주소와 리비전 번호)를 심는 작업이다. 이를 통해 현재 디버깅 중인 덤프 파일에 맞는 소스 코드를 버전 관리 저장소에서 가져온다.

  • /source : 소스 코드의 최상위 폴더, 대부분 trunk 폴더로 잡으면 된다.
  • /symbols : 인덱싱할 심볼 파일을 담은 최상위 폴더.

이 예제에서는 서브버전(SVN)을 다루지만 Debugging for Windows 에 포함된 문서 srcsrv.doc 에 따르면 다른 버전 관리 시스템도 지원한다.

  • p4.pm (Perforce)
  • vss.pm (Visual SourceSafe)
  • tfs.pm (Team Foundation Server)
  • cvs.pm (Concurrent Versions System)
  • svn.pm (Subversion)

MSBuild 스크립트에 익숙하지 않다면 이 코드(Target: SymbolIndex)가 명령 줄에서 어떻게 실행되는지 로그를 보자.

SymbolIndex:
  "D:Workspaceearlgreybranchesupgrade-to-vs2010vendorBuildToolDebugging Tools for Windowsx64srcsrvsvnindex.cmd" /debug /source="D:Workspaceearlgreybranchesupgrade-to-vs2010" /symbols="D:Workspaceearlgreybranchesupgrade-to-vs2010srcWin32-DebugBin"

Debugging Tool for Windows 에 포함된 svnindex.cmd을 실행하여 SVN에 소스 파일의 정보를 조회한 후 .PDB 파일에 적어 넣는다. svnindex.cmd는 실제 인덱싱 작업을 하는 ssindex.cmd에 대한 래퍼로써 Subversion 용으로 인자를 미리 설정해둔 것에 지나지 않는다.

@echo off
@REM ----------------------------------------
@REM Now just a stub to call SSIndex.cmd
@REM ----------------------------------------
@call "%~dp0SSIndex.cmd" -SYSTEM=SVN %*

ssindex.cmd를 실행하면 명령 줄에 작업 진행상황과 결과가 출력된다.

--------------------------------------------------------------------------------
  ssindex.cmd [STATUS] : Server ini file: D:Workspaceearlgreybranchesupgrade-to-vs2010vendorBuildToolDebugging Tools for Windowsx64srcsrvsrcsrv.ini
  ssindex.cmd [STATUS] : Source root    : D:Workspaceearlgreybranchesupgrade-to-vs2010
  ssindex.cmd [STATUS] : Symbols root   : D:Workspaceearlgreybranchesupgrade-to-vs2010srcWin32-DebugBin
  ssindex.cmd [STATUS] : Control system : SVN
  ssindex.cmd [STATUS] : SVN Executable : svn.exe
  ssindex.cmd [STATUS] : SVN Revision   : <N/A>
  ssindex.cmd [STATUS] : SVN Username   : <N/A>
  ssindex.cmd [STATUS] : SVN Password   : <N/A>
  --------------------------------------------------------------------------------
  ssindex.cmd [STATUS] : Running... this will take some time...
  ssindex.cmd [STATUS] : Processing svn.exe properties output ...
  ssindex.cmd [INFO  ] : ... indexing D:Workspaceearlgreybranchesupgrade-to-vs2010srcWin32-DebugBinClient.pdb
  ssindex.cmd [INFO  ] : ... wrote C:UsersAliceAppDataLocalTempindex8DE8.stream to D:Workspaceearlgreybranchesupgrade-to-vs2010srcWin32-DebugBinClient.pdb ...
  ssindex.cmd [INFO  ] : ... indexing D:Workspaceearlgreybranchesupgrade-to-vs2010srcWin32-DebugBinEarlgrey.Database.Test.pdb
  ssindex.cmd [INFO  ] : ... wrote C:UsersAliceAppDataLocalTempindex7889.stream to D:Workspaceearlgreybranchesupgrade-to-vs2010srcWin32-DebugBinServer.pdb ...

로그가 길어서 일부를 잘라냈지만 실제로 무슨 일이 벌어지는지 알기에는 충분하다. 어떤 심볼 파일에 소스 파일의 정보를 기록해 넣었는지 나온다. 혹시 빠진 파일이 없는지 확인해보자.

 

심볼 저장하기
<Target Name="SymbolStore" DependsOnTargets="SymbolIndex">
	<Exec
		Command="$(Quot)$(DebuggingToolsDir)symstore.exe$(Quot) add /o /r /f $(Quot)$(RootDir)$(BinDir)$(Quot) /s $(Quot)$(SymbolStorageDir)$(Quot) /t $(SymbolProjectName) /compress"
		WorkingDirectory="$(TreeRootDir)"
		ContinueOnError="false"
		IgnoreExitCode="false"
		/>
</Target>

나중에 덤프 파일에 맞는 심볼 파일을 디버거가 찾을 수 있도록 해싱하여 저장소에 저장한다. 사용법은 Debugging Tools for Windows 에 포함된 debugger.chm 문서파일에 자세히 나왔으나 여기서 사용한 옵션만 살펴보면 다음과 같다.

  • /o: 전체 과정을 자세히 표시한다.
  • /f: 심볼 파일이 있는 최상위 폴더
  • /r: /f 에 명시한 폴더뿐 아니라 그 하위 폴더까지 탐색한다.
  • /s: 앞서 생성한 심볼 저장소(공유 폴더)의 경로
  • /t: 제품 이름
  • /compress: 파일을 압축해 저장한다. 용량을 아끼기 좋다.

이제 위의 MSBuild 코드(Target: SymbolIndex)가 명령 줄에서 어떻게 실행되는지 로그를 보자.

SymbolStore:
  "D:Workspaceearlgreybranchesupgrade-to-vs2010vendorBuildToolDebugging Tools for Windowsx64symstore.exe" add /o /r /f "D:Workspaceearlgreybranchesupgrade-to-vs2010srcWin32-DebugBin" /s "D:Workspaceearlgreybranchesupgrade-to-vs2010srcWin32-DebugSymbols" /t EarlGrey /compress

symstore.exe를 실행하면 아래와 같은 콘솔 로그를 보게 된다.

  SYMSTORE MESSAGE: 0 alternate indexers registered
  SYMSTORE MESSAGE: LastId.txt reported id 2
  SYMSTORE MESSAGE: Final id is 0000000002
  SYMSTORE MESSAGE: Skipping file D:Workspaceearlgreybranchesupgrade-to-vs2010srcWin32-DebugBinClient.ilk - not a known file type.
  SYMSTORE MESSAGE: Copying D:Workspaceearlgreybranchesupgrade-to-vs2010srcWin32-DebugBinClient.exe to D:Workspaceearlgreybranchesupgrade-to-vs2010srcWin32-DebugSymbolsClient.exe4EDE0C0Cca000Client.ex_ [Force: T, Compress: T]
  SYMSTORE MESSAGE: Copying D:Workspaceearlgreybranchesupgrade-to-vs2010srcWin32-DebugBinClient.pdb to D:Workspaceearlgreybranchesupgrade-to-vs2010srcWin32-DebugSymbolsClient.pdb5F9C526115B94C758888163D7A3439522Client.pd_ [Force: T, Compress: T]
  SYMSTORE MESSAGE: Copying D:Workspaceearlgreybranchesupgrade-to-vs2010srcWin32-DebugBinssleay32.dll to D:Workspaceearlgreybranchesupgrade-to-vs2010srcWin32-DebugSymbolsssleay32.dll4A682DF345000ssleay32.dl_ [Force: T, Compress: T]

  SYMSTORE: Number of files stored = 12
  SYMSTORE: Number of errors = 0
  SYMSTORE: Number of files ignored = 22

로그가 길어서 중간 부분을 모두 지워 버렸다. 무엇보다 중요한 부분은 맨 아래 나오는 결과 요약이다.

SYMSTORE: Number of files stored = 12
SYMSTORE: Number of errors = 0
SYMSTORE: Number of files ignored = 22

몇 개의 심볼 파일을 저장했는지, 오류가 발생하지는 않았는지, 심볼 파일이 아니어서 무시한 바이너리 파일이 없는지 표시된다. 특히 오류 발생 여부에 주의하자!

 

Visual Studio 설정하기
[도구 – 옵션 – 디버깅 – 기호] 로 가서 심볼 저장소의 경로와 로컬 캐시 폴더 등을 명시한다.

image

 

디버깅하기!

덤프 파일을 Visual Studio 에서 열고 실행하면 디버거가 알아서 다음과 같은 일을 한다.

  1. 심볼 저장소를 뒤져서 덤프 파일에 맞는 심볼 파일을 가져온다.
  2. 가져온 심볼 파일에 인덱싱한 소스 코드 정보를 읽어서 버전 관리 시스템에서 필요한 소스 코드를 가져온다.

자, 이제 프로그래머는 문제 분석에만 집중하면 된다!!!

[마리아홀릭 01편] - 미래는 틀림없이 밝을 거야

Author Details
Kubernetes, DevSecOps, AWS, 클라우드 보안, 클라우드 비용관리, SaaS 의 활용과 내재화 등 소프트웨어 개발 전반에 도움이 필요하다면 도움을 요청하세요. 지인이라면 가볍게 도와드리겠습니다. 전문적인 도움이 필요하다면 저의 현업에 방해가 되지 않는 선에서 협의가능합니다.
0 0 votes
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.