Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 1개 있습니다.)

C# - 재현 가능한 빌드(reproducible builds) == Deterministic builds

아래의 글에,

[플밍노트] 재현 가능한 빌드, 재현 가능한 시드
; http://www.jiniya.net/ng/2018/07/reproducible-builds

"재현 가능한 빌드"에 대한 이야기가 나옵니다.

윈도우 10부터 재현 가능한 빌드(reproducible builds)로 방향을 정했고, 그 정책의 일환으로 타임스탬프를 타임스탬프가 아닌 해시 값으로 변경했다는 것이다. 재현 가능한 빌드라는 건 한마디로 정리하면 동일 소스 코드로 컴파일을 하면 동일 바이너리가 생성된다는 것을 보장한다는 개념이다. 즉, 니가 소스 코드를 한 줄도 안 고쳤다면 넌 어디에서건 다시 컴파일해도 동일한 바이너리를 가지게 된다는 의미다


오호... 그렇다면 C# 컴파일러에도 동일한 기능이 구현되어 있지 않았을까요? 역시나 검색해 보니 다음의 글이 나옵니다.

Deterministic builds in Roslyn - April 5th, 2016 
; http://blog.paranoidcoding.com/2016/04/05/deterministic-builds-in-roslyn.html

즉, C# 컴파일러도 "/deterministic" 옵션을 제공하며 이것을 켠 경우에 다음의 3가지 값들이 "동일한 소스 코드"라면 바뀌지 않고 유지된다는 것입니다.

MVID: a GUID identifying the PE which is newly generated for every PE produced by the compiler 1.
PDB ID: a GUID identifying the PDB matching PDB which is newly generated on every build.
Date / Time stamp: Seconds since the epoch which is calculated on every build.

실제로 한번 테스트를 해볼까요? ^^

Visual Studio로 기본 콘솔 프로젝트를 만들고 빌드 후, dumpbin.exe로 확인해 보면 PDB ID와 Date / Time stamp를 확인할 수 있습니다.

C:\> dumpbin /HEADERS ConsoleApp1.exe
Microsoft (R) COFF/PE Dumper Version 14.14.26433.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file ConsoleApp1.exe

PE signature found

File Type: EXECUTABLE IMAGE

FILE HEADER VALUES
             14C machine (x86)
               3 number of sections
        5B568E67 time date stamp Tue Jul 24 11:26:47 2018
... [생략]...
  Debug Directories

        Time Type        Size      RVA  Pointer
    -------- ------- -------- -------- --------
    5B568E67 cv           11C 00002648      848    Format: RSDS, {EE2C5EFD-C1B4-46D9-8730-6CA1889D1B18}, 1, E:\ConsoleApp1\ConsoleApp1\obj\Debug\ConsoleApp1.pdb
... [생략]...

MVID의 경우에는 dumpbin으로는 안 나오고 ildasm.exe를 실행해 보면 역어셈블된 il 파일에 다음과 같이 주석으로 담겨 있습니다.

...[생략]...
.module ConsoleApp1.exe
// MVID: {4AC6DAEA-D641-4696-AB54-DCB39211C9B2}
.imagebase 0x00400000
.file alignment 0x00000200
...[생략]...

일단, 소스 코드 변경 없이 그대로 다시 "Rebuild"를 한 후 PDB ID, MVID, time stamp를 확인해 보면 다음과 같이 바뀐 것을 볼 수 있습니다.

5B568F18 time date stamp Tue Jul 24 11:29:44 2018

Debug Directories

    Time Type        Size      RVA  Pointer
-------- ------- -------- -------- --------
5B568F18 cv           11C 00002648      848    Format: RSDS, {F97D62CB-E0D6-4944-BCC5-CC4B49F11D01}, 1, E:\ConsoleApp1\ConsoleApp1\obj\Debug\ConsoleApp1.pdb

// MVID: {691860F5-CE27-4483-846C-5F35EAC80AEC}

자, 그럼 이제 deterministic 빌드를 해볼까요? ^^ csc.exe에서는 /deterministic 옵션을 주면 되지만 Visual Studio에서는 msbuild를 이용하기 때문에 "Deterministic builds in Roslyn" 글의 설명에 따라 csproj 프로젝트 파일에 다음의 옵션을 추가하면 됩니다.

<Deterministic>true</Deterministic>

위의 옵션을 적용 후 최초 빌드 시에는 PDB ID, MVID, timestamp 모두 바뀝니다.

F99E2A84 time date stamp

Debug Directories

    Time Type        Size      RVA  Pointer
-------- ------- -------- -------- --------
94630E89 cv            76 00002664      864    Format: RSDS, {A3EC929A-FDFB-4EB2-AA83-70D680502520}, 1, E:\ConsoleApp1\ConsoleApp1\obj\Debug\ConsoleApp1.pdb

// MVID: {78A79916-27E0-4CE0-AD63-70ABB20217AD}

하지만 그 후부터는 Rebuild를 하면, 즉 이미 출력되어 있던 바이너리를 모두 삭제하고 동일한 소스 코드로부터 입력받은 경우 동일한 바이너리를 생성합니다. 실제로 출력된 바이너리에 대해 hex 비교를 해도 완전히 동일하다고 나옵니다.




혹시 적용 후 생각지 못한 점이 있을까요? 한 가지 있다면, deterministic 빌드인 경우 timestamp가 더 이상 시간 값이 아닌 hash라는 점입니다.

[deterministic 적용 전 - 시간 데이터 출력]
5B568E67 time date stamp Tue Jul 24 11:26:47 2018

[deterministic 적용 후 - hash 값이기 때문에 시간 데이터를 출력 못함]
F99E2A84 time date stamp

이 때문에 PDB Viewer 같은 도구를 이용해 해당 바이너리의 생성 시간을 알 수 없습니다. 아마 대부분의 개발 업체에서 바이너리로부터 생성 시간을 얻어와 어떤 자료로 이용하는 경우는 없을 것이기 때문에 크게 문제 될 것은 없어 보입니다.

또한, "Deterministic builds in Roslyn" 글에서도 언급하고 있지만 AssemblyVersionAttribute이나 AssemblyFileVersionAttribute 특성에 "*"를 지정해 자동 빌드 번호 증가를 명시한 경우에는 버전 정보가 달라지므로 당연히 deterministic 빌드 옵션을 켜도 매번 바뀔 수밖에 없다고 합니다. 아마도 이것 역시 대부분의 개발 업체에서는 명시적인 버전 지정을 할 것이기 때문에 제약 사항이 되진 않을 것입니다.

따라서, 이제부터는 특별한 사유가 없다면 닷넷 프로젝트의 경우 "Deterministic" 옵션을 켜두시는 것이 권장됩니다.




소스 코드의 변경은 컴파일러가 받아들이는 토큰 값을 기준으로 한 것이 아닙니다. 가령, 코드 끝에 공백을 삽입하고 저장한 경우 그것은 소스 코드가 변경된 것이나 다름없습니다. 그래서 그런 경우에도 동일 바이너리가 생성되지는 않습니다.

그 외에, /pathmap 옵션도 있습니다.

/pathmap - <PathMap>source=dest</PathMap>

이 옵션은 PDB를 위한 것인데요, PDB의 경우 디버깅뿐만 아니라 콜 스택을 뜨는 상황(ex: 예외 발생에 따른 이벤트 로그)에서 소스 코드가 위치한 파일 및 라인 번호를 보여주는데 사용됩니다. 따라서 소스 파일의 위치가 바뀌면 pdb도 바뀌게 되어 바이너리가 달라질 수밖에 없습니다. 그럴 때 pathmap을 지정해 공통 경로를 가질 수 있게 할 수 있어 역시나 pdb 파일이 바뀔 필요가 없어지는 것입니다.

그나저나, 이 옵션이 Visual Studio 2015의 두 번째 업데이트부터 추가되었었군요. ^^

New C# and VB features in Visual Studio 2015 Update 2
; https://devblogs.microsoft.com/dotnet/whats-new-for-c-and-vb-in-visual-studio/




(2024-04-01 업데이트) 그리고 이것이 윈도우 10 운영체제부터 DLL 빌드에도 반영돼 PE 헤더의 TimeDateStamp가 더 이상 날짜를 의미하지 않습니다.

Why are the module timestamps in Windows 10 so nonsensical?
; https://devblogs.microsoft.com/oldnewthing/20180103-00/?p=97705





[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 4/1/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2018-08-01 04시49분
[이성환] 성태님 덕분에 알게된 이 기능이 정말 제대로 완소 기능이네요!
PDB 생성 당시 사용된 소스코드가 있다면 비주얼스튜디오에서 BP 찍고 바로 디버깅도 가능하군요. 스택 로그만으로 코드를 따라가야했던 불편함에서 어느 정도 해방되었네요.

감사합니다 >ㅁ<
[guest]
2018-08-01 09시05분
아래의 기능과 혼동하신 거 아닌가요? ^^

(GitHub 등과 직접 연동해) 소스 코드 디버깅을 쉽게 해 주는 SourceLink
; http://www.sysnet.pe.kr/2/0/11630

원래 PDB 생성 당시 사용된 소스 코드가 있다면 디버깅이 가능했습니다. 단지, 이 글의 기능은 PDB 생성이 소스 코드가 변경되지 않는다면 동일한 PDB가 나오는 정도입니다. 이로 인해 심볼 서버 등을 구성하는 상황에서 PDB 파일의 공간을 절약할 수 있다는 장점이 있습니다.
정성태
2018-08-02 03시48분
[이성환] 아.. 제가 잘못 이해한 것일 수도 있겠네요. ;ㅁ;
근데 제가 말씀드린 부분은 PDB 를 별도로 보관하지 않을 경우라고 봐야하겠네요.

완성된 바이너리 패키지를 내보낼 때에는 PDB를 포함하지 않도록 하는데(패키징 시 별도로 PDB를 보관하지 않은 상태)
나중에 해당 바이너리로 디버깅을 하려고 하면,바이너리 패키징 당시 사용한 소스코드로 커밋을 돌려서 리빌드하게 되고,
패키징된 바이너리와 현재 리빌드해 생성된 PDB 와 연결이 안 됐던 경험이 있어서요.
(심볼 로딩은 됐지만 BP hit 안 되는 상황 등등..)
특히 리모트 디버깅 상황에서 자주 겪었던 문제라.. 이게 진짜 그 문제 때문인지는 기억이 잘 안 나네요.

근데 Deterministic 사용하니까 PDB 를 보관하지 않아도 로컬 개발 위치에서 리빌드해서 그냥 잘 연결돼서리... -ㅂ-

그런 것이었슴다...(__)
[guest]
2018-08-02 03시59분
그런 시나리오라고는 제가 전혀 예상치 못했군요. 말씀하신 상황에서도 유용한게 맞습니다. ^^
정성태

... [31]  32  33  34  35  36  37  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12843정성태10/3/20216582스크립트: 29. 파이썬 - fork 시 기존 클라이언트 소켓 및 스레드의 동작파일 다운로드1
12842정성태10/1/202124744오류 유형: 763. 파이썬 오류 - AttributeError: type object '...' has no attribute '...'
12841정성태10/1/20218369스크립트: 28. 모든 파이썬 프로세스에 올라오는 특별한 파일 - sitecustomize.py
12840정성태9/30/20218406.NET Framework: 1119. Entity Framework의 Join 사용 시 다중 칼럼에 대한 OR 조건 쿼리파일 다운로드1
12839정성태9/15/20219485.NET Framework: 1118. C# 11 - 제네릭 타입의 특성 적용파일 다운로드1
12838정성태9/13/20219110.NET Framework: 1117. C# - Task에 전달한 Action, Func 유형에 따라 달라지는 async/await 비동기 처리 [2]파일 다운로드1
12837정성태9/11/20218046VC++: 151. Golang - fmt.Errorf, errors.Is, errors.As 설명
12836정성태9/10/20217627Linux: 45. 리눅스 - 실행 중인 다른 프로그램의 출력을 확인하는 방법
12835정성태9/7/20218937.NET Framework: 1116. C# 10 - (15) CallerArgumentExpression 특성 추가 [2]파일 다운로드1
12834정성태9/7/20217309오류 유형: 762. Visual Studio 2019 Build Tools - 'C:\Program' is not recognized as an internal or external command, operable program or batch file.
12833정성태9/6/20216767VC++: 150. Golang - TCP client/server echo 예제 코드파일 다운로드1
12832정성태9/6/20217580VC++: 149. Golang - 인터페이스 포인터가 의미 있을까요?
12831정성태9/6/20216138VC++: 148. Golang - 채널에 따른 다중 작업 처리파일 다운로드1
12830정성태9/6/20218354오류 유형: 761. Internet Explorer에서 파일 다운로드 시 "Your current security settings do not allow this file to be downloaded." 오류
12829정성태9/5/202110002.NET Framework: 1115. C# 10 - (14) 구조체 타입에 기본 생성자 정의 가능파일 다운로드1
12828정성태9/4/20218129.NET Framework: 1114. C# 10 - (13) 단일 파일 내에 적용되는 namespace 선언파일 다운로드1
12827정성태9/4/20218121스크립트: 27. 파이썬 - 웹 페이지 데이터 수집을 위한 scrapy Crawler 사용법 요약
12826정성태9/3/202110366.NET Framework: 1113. C# 10 - (12) 문자열 보간 성능 개선 [1]파일 다운로드1
12825정성태9/3/20217910개발 환경 구성: 603. GoLand - WSL 환경과 연동
12824정성태9/2/202116987오류 유형: 760. 파이썬 tensorflow - Dst tensor is not initialized. 오류 메시지
12823정성태9/2/20216730스크립트: 26. 파이썬 - PyCharm을 이용한 fork 디버그 방법
12822정성태9/1/202111935오류 유형: 759. 파이썬 tensorflow - ValueError: Shapes (...) and (...) are incompatible [2]
12821정성태9/1/20217469.NET Framework: 1112. C# - .NET 6부터 공개된 ISpanFormattable 사용법
12820정성태9/1/20217786VC++: 147. Golang - try/catch에 대응하는 panic/recover [1]파일 다운로드1
12819정성태8/31/20217905.NET Framework: 1111. C# - FormattableString 타입
12818정성태8/31/20217145Windows: 198. 윈도우 - 작업 관리자에서 (tensorflow 등으로 인한) GPU 연산 부하 보는 방법
... [31]  32  33  34  35  36  37  38  39  40  41  42  43  44  45  ...