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분
그런 시나리오라고는 제가 전혀 예상치 못했군요. 말씀하신 상황에서도 유용한게 맞습니다. ^^
정성태

... 46  47  48  49  50  51  52  [53]  54  55  56  57  58  59  60  ...
NoWriterDateCnt.TitleFile(s)
12310정성태9/3/202010336오류 유형: 644. Windows could not start the Elasticsearch 7.9.0 (elasticsearch-service-x64) service on Local Computer.
12309정성태9/3/202010103개발 환경 구성: 507. Elasticsearch 6.6부터 기본 추가된 한글 형태소 분석기 노리(nori) 사용법
12308정성태9/2/202011344개발 환경 구성: 506. Windows - 단일 머신에서 단일 바이너리로 여러 개의 ElasticSearch 노드를 실행하는 방법
12307정성태9/2/202012133오류 유형: 643. curl - json_parse_exception / Invalid UTF-8 start byte
12306정성태9/1/202010328오류 유형: 642. SQL Server 시작 오류 - error code 10013
12305정성태9/1/202011182Windows: 172. "Administered port exclusions"이 아닌 포트 범위 항목을 삭제하는 방법
12304정성태8/31/202010140개발 환경 구성: 505. 윈도우 - (네트워크 어댑터의 우선순위로 인한) 열거되는 IP 주소 순서를 조정하는 방법
12303정성태8/30/202010295개발 환경 구성: 504. ETW - 닷넷 프레임워크 기반의 응용 프로그램을 위한 명령행 도구 etrace 소개
12302정성태8/30/202010216.NET Framework: 936. C# - ETW 관련 Win32 API 사용 예제 코드 (5) - Private Logger파일 다운로드1
12301정성태8/30/202010509오류 유형: 641. error MSB4044: The "Fody.WeavingTask" task was not given a value for the required parameter "IntermediateDir".
12300정성태8/29/20209936.NET Framework: 935. C# - ETW 관련 Win32 API 사용 예제 코드 (4) CLR ETW Consumer파일 다운로드1
12299정성태8/27/202010859.NET Framework: 934. C# - ETW 관련 Win32 API 사용 예제 코드 (3) ETW Consumer 구현파일 다운로드1
12298정성태8/27/202010603오류 유형: 640. livekd - Could not resolve symbols for ntoskrnl.exe: MmPfnDatabase
12297정성태8/25/20209805개발 환경 구성: 503. SHA256 테스트 인증서 생성 방법
12296정성태8/24/202010223.NET Framework: 933. C# - ETW 관련 Win32 API 사용 예제 코드 (2) NT Kernel Logger파일 다운로드1
12295정성태8/24/20209673오류 유형: 639. Bitvise - Address is already in use; bind() in ListeningSocket::StartListening() failed: Windows error 10013: An attempt was made to access a socket ,,,
12293정성태8/24/202010996Windows: 171. "Administered port exclusions" 설명
12292정성태8/20/202012292.NET Framework: 932. C# - ETW 관련 Win32 API 사용 예제 코드 (1)파일 다운로드2
12291정성태8/15/202011223오류 유형: 638. error 1297: Device driver does not install on any devices, use primitive driver if this is intended.
12290정성태8/11/202011881.NET Framework: 931. C# - IP 주소에 따른 국가별 위치 확인 [8]파일 다운로드1
12289정성태8/6/20209393개발 환경 구성: 502. Portainer에 윈도우 컨테이너를 등록하는 방법
12288정성태8/5/20209390오류 유형: 637. WCF - The protocol 'net.tcp' does not have an implementation of HostedTransportConfiguration type registered.
12287정성태8/5/20209852오류 유형: 636. C# - libdl.so를 DllImport로 연결 시 docker container 내에서 System.DllNotFoundException 예외 발생
12286정성태8/5/202010704개발 환경 구성: 501. .NET Core 용 container 이미지 만들 때 unzip이 필요한 경우
12285정성태8/4/202011110오류 유형: 635. 윈도우 10 업데이트 - 0xc1900209 [2]
12284정성태8/4/202010401디버깅 기술: 169. Hyper-V의 VM에 대한 메모리 덤프를 뜨는 방법
... 46  47  48  49  50  51  52  [53]  54  55  56  57  58  59  60  ...