.NET ReadyToRun 바이너리의 symstore 심볼 서버 업로드 문제

ReadyToRun(R2R)이란?

.NET의 배포 방식과 실행 환경은 다양해서 나중에 따로 글을 써서 정리 한번 하려고 한다.

원래대로라면 .NET의 코드는 JIT(Just-in-Time) 컴파일러에 의해 런타임에 네이티브로 컴파일 된다.
하지만 ReadyToRun 방식으로 배포를 하면 일부는 네이티브로 미리 컴파일 하고, 나머지는 런타임에 컴파일이 된다.

어셈블리의 크기가 증가하는 것 이외에는 크게 제한 사항이 없기 때문에 적용하기 쉽다.

R2R 미니 덤프 디버깅 불가

보통 게임 서버에서 크래시가 났다면 원인을 파악하기 위해 메모리 전체를 포함한 풀 덤프를 남긴다.

메모리 전체를 쓰는 것은 오래걸리고 중간에 잘못될 여지가 있기 때문에
콜스택과 같이 최소 정보만을 담은 미니 덤프를 먼저 남기고 다음에 풀 덤프를 남기곤 한다.

프로젝트에서는 pdb뿐 아니라 dll, exe과 같은 바이너리도 심볼 서버에 저장하고 있었는데,
ReadyToRun 배포 옵션을 켠 미니 덤프에 해당하는 바이너리를 심볼 서버에서 불러오지 못하는 문제가 있었다.

원인 파악

풀 덤프는 정상적으로 디버깅이 가능했다.
자세히 확인해보지는 못했지만 아마 풀덤프 메모리에 R2R이 적용된 바이너리가 로드되어 있어서 문제가 없는 것 같았다.

확인해보니 심볼 서버에 R2R으로 용량이 커진 바이너리는 적재되고 있지 않았다.
symstore는 해당 파일을 타입 인식 실패로 업로드하지 않았다

에러 메시지

skip (not a known file type. ErrorLevel is 13)

심볼 서버에 용량이 커지기 전 바이너리를 올려보았으나,
symstore 및 디버거가 사용하는 키에는 사이즈 정보가 있기 때문에 이 바이너리는 사용 불가했다.

TimeDataStamp(4bytes) + SizeOfImage(3bytes)

Portable Executable - Wikipedia

R2R이 적용되면 바이너리 크기가 커지기 때문에 뒷 부분이 달라졌다

  • 이전: CF33DFB3388000
  • 이후: CF33DFB37d2000

미니 덤프에 해당하는 R2R 바이너리를 수동으로 찾아서 덤프 파일과 같이 두고 디버깅을 하거나
풀 덤프만을 사용하면 디버깅이 가능하긴 했지만 불편했다.

시도 1 - 실패

MINIDUMP_TYPE으로 어느 것을 미니 덤프에 남길지 정해줄 수 있다.

MINIDUMP_TYPE (minidumpapiset.h) - Win32 apps | Microsoft Learn

미니 덤프에 로드된 R2R 바이너리를 포함시키면 되지 않을까

MiniDumpWithModuleHeaders을 사용하면 바이너리가 포함되기는 하나, 디버거가 인식하지 못했다.
가능하더라도 용량이 커져 미니 덤프의 의미가 없어졌다.

시도 2 - 해결

symstore는 어떤 옵션을 줘도 R2R로 커진 바이너리를 업로드 하지 않았다.
업로드하고 싶은 파일은 심볼 파일이 아니라 단순한 바이너리이기 때문에 직접 심볼 서버에 올려줘도 디버거가 찾는데 문제가 없을 것 같았다.

빌드 스크립트가 파이썬으로 작성되어 있었기 때문에 아래와 같이 파이썬으로 PE 헤더 정보로 심볼 키를 구해 심볼 서버에 직접 업로드 했다

pip install pefile
def get_symbol_key(filename):
	pe = pefile.PE(filename)
	return format(pe.FILE_HEADER.TimeDateStamp, 'x').upper() + format(pe.OPTIONAL_HEADER.SizeOfImage, 'x').upper()

이제 미니 덤프에서도 정상적으로 바이너리를 찾을 수 있게 되었다

SingleFile으로 배포한 경우 문제

.NET은 기본적으로 .exe는 부트스트랩에 불과하고 실제 코드는 .dll에서 로드되어 실행된다.

배포하기 편리하기 위해 이를 하나의 exe로 묶어주는 SingleFile이라는 옵션이 존재하는데
문제는 SingleFile+ReadyToRun 할 경우에 크기가 커진 .dll 파일을 출력 폴더에서 찾을 수 없었다.

출력 폴더 예시

  • Output/Release
    • Program.exe (통합 파일)
  • Output/Release/Win-x64
    • Program.dll (R2R 적용하기 전 원본 사이즈)

C#은 결정론적 컴파일이기 때문에 SingleFile을 on/off 두 번 빌드해서
통합 파일은 배포하고 R2R 적용된 dll은 수동으로 심볼 서버에 올리면 해결되긴 한다.

빌드 시간을 절약하기 위해서 SingleFile로 배포된 파일에서 하위 바이너리를 추출해서 심볼 서버로 업로드 하는 과정을 추가했다.

ilspycmd 설치

dotnet tool install --global ilspycmd

사용

ilspycmd -d -o 출력폴더 Program.exe(통합파일)