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

Instead of putting a hash in the Portable Executable timestamp field, why not create a separate field for the hash?
; https://devblogs.microsoft.com/oldnewthing/20240815-00/?p=110131





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

[연관 글]






[최초 등록일: ]
[최종 수정일: 8/20/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분
그런 시나리오라고는 제가 전혀 예상치 못했군요. 말씀하신 상황에서도 유용한게 맞습니다. ^^
정성태

... 76  77  78  79  80  81  82  [83]  84  85  86  87  88  89  90  ...
NoWriterDateCnt.TitleFile(s)
11735정성태10/12/201819744VS.NET IDE: 129. Visual Studio - 특정 문자(열)를 개행 문자로 바꾸는 방법
11734정성태10/10/201814862Linux: 4. Synology NAS(DS216+II)에 FTDI 장치 연결 후 C#(.NET Core)으로 DTR 제어파일 다운로드1
11733정성태10/10/201817197Linux: 3. Synology NAS(DS216+II)에서 FTDI 장치를 C/C++로 제어
11732정성태10/10/201816891디버깅 기술: 119. windbg 분석 사례 - 종료자(Finalizer)에서 예외가 발생한 경우 비정상 종료(Crash) 발생파일 다운로드1
11731정성태10/9/201816654개발 환경 구성: 409. C# - REST API를 이용해 Azure Kudu 서비스 이용 - 웹 앱 확장 처리파일 다운로드1
11730정성태10/9/201815447개발 환경 구성: 408. C# - REST API를 이용해 Azure Kudu 서비스 이용 - 파일 처리파일 다운로드1
11729정성태10/9/201817588Windows: 150. 윈도우에서 ARP Cache 목록 확인 및 삭제하는 방법
11728정성태10/9/201816025사물인터넷: 50. Audio Jack 커넥터의 IR 적외선 송신기 [1]
11727정성태10/8/201816951오류 유형: 491. Visual Studio의 리눅스 SSH 원격 연결 - "Connectivity Failure. Please make sure host name and port number are correct."
11726정성태10/7/201819393사물인터넷: 49. 라즈베리 파이를 이용해 원격 컴퓨터의 전원 스위치 제어파일 다운로드1
11724정성태10/5/201818105개발 환경 구성: 407. 유니코드와 한글 - "Hangul Compatibility Jamo"파일 다운로드1
11723정성태10/4/201814156개발 환경 구성: 406. "Docker for Windows" 컨테이너 내의 .NET Core 응용 프로그램에서 직렬 포트(Serial Port, COM Port) 사용 방법
11722정성태10/4/201816730.NET Framework: 798. C# - Hyper-V 가상 머신의 직렬 포트와 연결된 Named Pipe 간의 통신파일 다운로드1
11721정성태10/4/201817217.NET Framework: 797. Linux 환경의 .NET Core 응용 프로그램에서 직렬 포트(Serial Port, COM Port) 사용 방법파일 다운로드1
11720정성태10/4/201818656개발 환경 구성: 405. Hyper-V 가상 머신에서 직렬 포트(Serial Port, COM Port) 사용
11719정성태10/4/201819572.NET Framework: 796. C# - 인증서를 윈도우에 설치하는 방법
11718정성태10/4/201814544개발 환경 구성: 404. (opkg가 설치된) Synology NAS(DS216+II)에 cmake 설치
11717정성태10/3/201817472사물인터넷: 48. 넷두이노의 C# 네트워크 프로그램 [1]
11716정성태10/3/201817748사물인터넷: 47. Raspberry PI Zero (W)에 FTDI 장치 연결 후 C/C++로 DTR 제어파일 다운로드1
11715정성태10/3/201816929사물인터넷: 46. Raspberry PI Zero (W)에 docker 설치
11714정성태10/2/201815976사물인터넷: 45. Raspberry PI에 ping을 hostname으로 하는 방법
11713정성태10/2/201818742개발 환경 구성: 403. Synology NAS(DS216+II)에 docker 설치 후 .NET Core 2.1 응용 프로그램 실행하는 방법
11712정성태10/2/201823635.NET Framework: 795. C# - 폰트 목록을 한글이 아닌 영문으로 구하는 방법 [3]
11711정성태10/2/201819192오류 유형: 490. 윈도우 라이선스 키 입력 오류 0xc004f050, 0xc004e028
11710정성태10/2/201817628.NET Framework: 794. C# - 같은 모양, 다른 값의 한글 자음을 비교하는 호환 분해 [5]
11709정성태9/30/201815757개발 환경 구성: 402. .NET Core 콘솔 응용 프로그램을 docker로 실행/디버깅하는 방법 [1]
... 76  77  78  79  80  81  82  [83]  84  85  86  87  88  89  90  ...