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)
12643정성태5/14/20218235오류 유형: 718. 서버 유형의 COM+ 사용 시 0x80080005(Server execution failed) 오류 발생
12642정성태5/14/20219157오류 유형: 717. The 'Microsoft.ACE.OLEDB.12.0' provider is not registered on the local machine.
12641정성태5/13/20218898디버깅 기술: 179. 윈도우용 .NET Core 3 이상에서 Windbg의 sos 사용법
12640정성태5/13/202111797오류 유형: 716. RDP 연결 - Because of a protocol error (code: 0x112f), the remote session will be disconnected. [1]
12639정성태5/12/20218680오류 유형: 715. Arduino: Open Serial Monitor - The module '...\detection.node' was compiled against a different Node.js version using NODE_MODULE_VERSION
12638정성태5/12/20219584사물인터넷: 63. NodeMCU v1 ESP8266 - 펌웨어 내 파일 시스템(SPIFFS, LittleFS) 및 EEPROM 활용
12637정성태5/10/20219284사물인터넷: 62. NodeMCU v1 ESP8266 보드의 A0 핀에 다중 아날로그 센서 연결 [1]
12636정성태5/10/20219428사물인터넷: 61. NodeMCU v1 ESP8266 보드의 A0 핀 사용법 - FSR-402 아날로그 압력 센서 연동파일 다운로드1
12635정성태5/9/20218738기타: 81. OpenTabletDriver를 (관리자 권한으로 실행하지 않고도) 관리자 권한의 프로그램에서 동작하게 만드는 방법
12634정성태5/9/20217863개발 환경 구성: 572. .NET에서의 신뢰도 등급 조정 - 외부 Manifest 파일을 두는 방법파일 다운로드1
12633정성태5/7/20219303개발 환경 구성: 571. UAC - 관리자 권한 없이 UIPI 제약을 없애는 방법
12632정성태5/7/20219449기타: 80. (WACOM도 지원하는) Tablet 공통 디바이스 드라이버 - OpenTabletDriver
12631정성태5/5/20219380사물인터넷: 60. ThingSpeak 사물인터넷 플랫폼에 ESP8266 NodeMCU v1 + 조도 센서 장비 연동파일 다운로드1
12630정성태5/5/20219733사물인터넷: 59. NodeMCU v1 ESP8266 보드의 A0 핀 사용법 - CdS Cell(GL3526) 조도 센서 연동파일 다운로드1
12629정성태5/5/202111490.NET Framework: 1057. C# - CoAP 서버 및 클라이언트 제작 (UDP 소켓 통신) [1]파일 다운로드1
12628정성태5/4/20219416Linux: 39. Eclipse 원격 디버깅 - Cannot run program "gdb": Launching failed
12627정성태5/4/202110130Linux: 38. 라즈베리 파이 제로 용 프로그램 개발을 위한 Eclipse C/C++ 윈도우 환경 설정
12626정성태5/3/202110148.NET Framework: 1056. C# - Thread.Suspend 호출 시 응용 프로그램 hang 현상 (2)파일 다운로드1
12625정성태5/3/20219094오류 유형: 714. error CS5001: Program does not contain a static 'Main' method suitable for an entry point
12624정성태5/2/202112821.NET Framework: 1055. C# - struct/class가 스택/힙에 할당되는 사례 정리 [10]파일 다운로드1
12623정성태5/2/20219524.NET Framework: 1054. C# 9 최상위 문에 STAThread 사용 [1]파일 다운로드1
12622정성태5/2/20216337오류 유형: 713. XSD 파일을 포함한 프로젝트 - The type or namespace name 'TypedTableBase<>' does not exist in the namespace 'System.Data'
12621정성태5/1/20219727.NET Framework: 1053. C# - 특정 레지스트리 변경 시 알림을 받는 방법파일 다운로드1
12620정성태4/29/202111865.NET Framework: 1052. C# - 왜 구조체는 16 바이트의 크기가 적합한가? [1]파일 다운로드1
12619정성태4/28/202112364.NET Framework: 1051. C# - 구조체의 크기가 16바이트가 넘어가면 힙에 할당된다? [2]파일 다운로드1
12618정성태4/27/202110928사물인터넷: 58. NodeMCU v1 ESP8266 CP2102 Module을 이용한 WiFi UDP 통신 [1]파일 다운로드1
... 31  32  33  34  35  36  37  38  [39]  40  41  42  43  44  45  ...