Visual C++ での同一ソース同一バイナリの実現方法を探る

Visual C++ で C/C++ のプログラムをコンパイル(ビルド)すると、ビルドのたびに微妙に異なるバイナリが生成されてしまいます。そのため、バイナリからソースを特定するのが面倒です。この問題を回避する方法を探ります。

動作確認環境

  • Windows 11 Home 21H2
  • Visual Studio Community 2019

課題 1. ビルド日時が埋め込まれる

Visual C++ はバイナリにビルド日時を埋め込みます。
次のプログラムで確認してみましょう。

#include <stdio.h>
int main()
{
    printf("Hello\n");
    return 0;
}

ビルドします。

C:\tmp> cl test1.c
Microsoft(R) C/C++ Optimizing Compiler Version 19.28.29913 for x86
......
/out:test1.exe
......

生成されたバイナリをコピーします。

C:\tmp> copy test1.exe test1.exe.bak

もう一度ビルドします。

C:\tmp> cl test1.c
Microsoft(R) C/C++ Optimizing Compiler Version 19.28.29913 for x86
......
/out:test1.exe
......

さきほどコピーしたバイナリと比較します。

C:\tmp> fc /b test1.exe test1.exe.bak
ファイル test1.exe と test1.EXE.BAK を比較しています
00000108: 73 6B
00018210: 73 6B

2 バイトの差異が検出されました。ビルド日時が異なるためです。

この問題は、以前に当「やや低レイヤー研究所」の「Windows システムファイルのビルド日時が表示されない謎を探る」で書いたように、「/Brepro」オプションで回避できます。現在日時の代わりにバイナリから計算したハッシュ値を書き込む、というオプションです。
試してみましょう。

さきほどのプログラムを「/Brepro」オプション付きでビルドします。

C:\tmp> cl /Brepro test1.c
Microsoft(R) C/C++ Optimizing Compiler Version 19.28.29913 for x86
......
/out:test1.exe
......

生成されたバイナリをコピーします。

C:\tmp> copy test1.exe test1.exe.bak

もう一度「/Brepro」オプション付きでビルドします。

C:\tmp> cl /Brepro test1.c
Microsoft(R) C/C++ Optimizing Compiler Version 19.28.29913 for x86
......
/out:test1.exe
......

さきほどコピーしたバイナリと比較します。

C:\tmp> fc /b test1.exe test1.exe.bak
ファイル test1.exe と test1.EXE.BAK を比較しています
FC: 相違点は検出されませんでした

同じです。差異が解消されました。

Visual Studio の IDE を使う場合は、プロジェクトのプロパティを開き、[C/C++]-[コマンドライン]-[追加のオプション]欄、と[リンカー]-[コマンドライン]-[追加のオプション]欄に「/Brepro」と手入力してください。

課題 2. __DATE__ や __TIME__ が使われている

C/C++ のプログラム内に現在日時を示す __DATE__ や __TIME__ が使われていると、ビルドのたびにバイナリが変わってしまいます。
次のプログラムで確認してみましょう。

#include <stdio.h>
int main()
{
    printf("%s %s\n", __DATE__, __TIME__);
    return 0;
}

ビルドし、実行します。

C:\tmp> cl test2.c
Microsoft(R) C/C++ Optimizing Compiler Version 19.28.29913 for x86
......
/out:test2.exe
......

C:\tmp> test2.exe
Mar 28 2022 23:59:59

生成されたバイナリをコピーします。

C:\tmp> copy test2.exe test2.exe.bak

もう一度ビルドし、実行します。

C:\tmp> cl test2.c
Microsoft(R) C/C++ Optimizing Compiler Version 19.28.29913 for x86
......
/out:test2.exe
......

C:\tmp> test2.exe
Mar 29 2022 00:01:01

異なる日時が表示されました。

さきほどコピーしたバイナリと比較します。

C:\tmp> fc /b test2.exe test2.exe.bak
ファイル test2.exe と test2.EXE.BAK を比較しています
00000108: AD 6F
00018210: AD 6F
00019400: 30 32
00019401: 30 33
00019403: 30 35
00019404: 31 39
00019406: 30 35
00019407: 31 39
00019411: 39 38

9 バイトの差異が検出されました。

__DATE__ と __TIME__ はビルドのタイミングで決まる値なので、どうしようもなく思えるかもしれません。しかし、この問題も「/Brepro」オプションで回避できます。

「/Brepro」付きでビルドし、実行します。

C:\tmp> cl /Brepro test2.c
Microsoft(R) C/C++ Optimizing Compiler Version 19.28.29913 for x86
......
/out:test2.exe
/Brepro
......

C:\tmp> test2.exe
1 1

日付も時刻も「1」になりました。

生成されたバイナリをコピーします。

C:\tmp> copy test2.exe test2.exe.bak

もう一度「/Brepro」付きでビルドし、実行します。

C:\tmp> cl /Brepro test2.c
Microsoft(R) C/C++ Optimizing Compiler Version 19.28.29913 for x86
......
/out:test2.exe
/Brepro
......

C:\tmp> test2.exe
1 1

やはり、日付も時刻も「1」になりました。

さきほどコピーしたバイナリと比較します。

C:\tmp>fc /b test2.exe test2.exe.bak
ファイル test2.exe と test2.EXE.BAK を比較しています
FC: 相違点は検出されませんでした

同じです。__DATE__ や __TIME__ に起因する差異が解消されました。

日付や時刻が「1」でいいのかと思いますが、誤動作防止のためあえて日時とみなせない文字列を出力するようになっているのでしょうか。

課題 3. ビルド回数が埋め込まれる

リリースビルドであっても、デバッグや故障解析のためデバッグ情報の生成を有効にしておくことは多いと思います。その場合、バイナリにビルド回数が埋め込まれてしまいます。

次のプログラムで確認してみましょう。

int main() { }

「/Brepro」オプション付き、かつ、「/Zi」オプション付き(デバッグ情報生成あり)でビルドします。

c:\tmp> cl /Brepro /Zi test3.c
Microsoft(R) C/C++ Optimizing Compiler Version 19.28.29913 for x86
......
/out:test3.exe
......

ヘッダ情報とバイナリを保存しておきます。

c:\tmp> dumpbin /headers test3.exe > test3.txt.bak

c:\tmp> copy test3.exe test3.exe.bak

もう一度ビルドします。

c:\tmp> cl /Brepro /Zi test3.c
Microsoft(R) C/C++ Optimizing Compiler Version 19.28.29913 for x86
......
/out:test3.exe
......

ヘッダ情報とバイナリを保存します。

c:\tmp> dumpbin /headers test3.exe > test3.txt.bak2

c:\tmp> copy test3.exe test3.exe.bak2

2 つのバイナリを比較します。

c:\tmp> fc /b test3.exe.bak test3.exe.bak2
ファイル test3.exe.bak と TEST3.EXE.BAK2 を比較しています
00000108: 01 62
00000109: A6 66
0000010A: 2D 44
0000010B: BD 62
0006E200: 01 02

「/Brepro」オプションを付けたにもかかわらず、差異が発生しました。

ヘッダ情報を比較します。

c:\tmp> fc test3.txt.bak test3.txt.bak2
ファイル test3.txt.bak と TEST3.TXT.BAK2 を比較しています
ファイル test3.txt.bak と TEST3.TXT.BAK2 を比較しています
***** test3.txt.bak
               6 number of sections
        BD2DA601 time date stamp
               0 file pointer to symbol table
***** TEST3.TXT.BAK2
               6 number of sections
        62446662 time date stamp
               0 file pointer to symbol table
*****

***** test3.txt.bak
    -------- ------- -------- -------- --------
    BD2DA601 cv            29 0006F7EC    6E1EC    ......, 1, c:\tmp\test3.pdb
    BD2DA601 feat          14 0006F818    6E218    ......
***** TEST3.TXT.BAK2
    -------- ------- -------- -------- --------
    BD2DA601 cv            29 0006F7EC    6E1EC    ......, 2, c:\tmp\test3.pdb
    BD2DA601 feat          14 0006F818    6E218    ......
*****

「cv」の行にそれぞれ「1」「2」とあります。これがビルド回数です。さらにビルドを繰り返すと「3」「4」「5」と増えていきます。
この値が変わったためバイナリのハッシュ値が代わり、「time date stamp」値も変化しています。

ビルド回数に起因する差異を抑止するには、ビルド前に「.pdb」(Program Database)」ファイルや「.ilk」(Incremantal Link) ファイルを削除し、初回のビルドだと思い込ませます(最初、リンカーの /INCREMENTAL:NO オプションが有効かと思ったのですが、効果がありませんでした)。
試してみましょう。

c:\tmp> del test3.pdb test3.ilk

c:\tmp> cl /Brepro /Zi test3.c
Microsoft(R) C/C++ Optimizing Compiler Version 19.28.29913 for x86
......
/out:test3.exe
......

c:\tmp> copy test3.exe test3.exe.bak
......

c:\tmp> del test3.pdb test3.ilk

c:\tmp> cl /Brepro /Zi test3.c
Microsoft(R) C/C++ Optimizing Compiler Version 19.28.29913 for x86
......
/out:test3.exe
......

c:\tmp> copy test3.exe test3.exe.bak2
......

c:\tmp> fc /b test3.exe test3.exe.bak
ファイル test3.exe と TEST3.EXE.BAK を比較しています
FC: 相違点は検出されませんでした

同じバイナリが生成されました。ヘッダ情報を確認すると、ビルド回数はいずれも「1」になっています。

IDE を使う場合は、「ビルド」ではなく「リビルド」すれば OK です。

課題 4. パス名が埋め込まれる

これもデバッグ情報を付加した場合の話ですが、同じソースコードでも「どこで」ビルドしたかによって異なるバイナリが生成されることがあります。「どこで」というのは、「どのディレクトリで」という意味です。
先ほどのプログラム「test3.c」で確認してみましょう。

ソースを「c:\tmp」に置き、「.pdb」や「.ilk」を削除した上で「/Brepro」「/Zi」付きでビルドします。

c:\tmp> del test3.pdb test3.ilk
......

C:\tmp> cl /Brepro /Zi test3.c
Microsoft(R) C/C++ Optimizing Compiler Version 19.28.29913 for x86
......
/out:test3.exe
......

同じソースを「c:\tmp」ではなく「c:\new_tmp」に置いてビルドします。

C:\new_tmp> del test3.pdb test3.ilk
......

C:\new_tmp> cl /Brepro /Zi test3.c
Microsoft(R) C/C++ Optimizing Compiler Version 19.28.29913 for x86
......
/out:test3.exe
......

生成されたバイナリを比較します。

C:\> fc /b c:\tmp\test3.exe c:\new_tmp\test3.exe
ファイル C:\TMP\test3.exe と C:\NEW_TMP\TEST3.EXE を比較しています
00000108: 90 4A
00000109: C5 BF
0000010A: 6C 80
0000010B: 5E 1C
000070D7: 68 6C
000070DC: 68 6C
......
0006E251: 00 BF
0006E252: 00 80
0006E253: 00 1C

差異が発生しています。

両者のヘッダ情報を比較します。

C:\>dumpbin /headers c:\tmp\test3.exe > c:\tmp\test3.txt

C:\>dumpbin /headers c:\new_tmp\test3.exe > c:\new_tmp\test3.txt

C:\>fc c:\tmp\test3.txt c:\new_tmp\test3.txt
ファイル C:\TMP\test3.txt と C:\NEW_TMP\TEST3.TXT を比較しています
......
***** C:\TMP\test3.txt
......
    5E6CC590 cv ......  C:\tmp\test3.pdb
......

***** C:\NEW_TMP\TEST3.TXT
......
    1C80BF4A cv ...... C:\new_tmp\test3.pdb
......

一方には「c:\tmp\test3.pdb」が、もう一方には「c:\new_tmp\test3.pdb」が埋め込まれています。
つまり、ソースは同じなのに、「どこで」ビルドしたかが反映されてしまっています。

この差異を解消するには、リンカーの「/pdbaltpath」オプションを使って、パス名を含まない「.pdb」ファイル名を指定します。その際、リンカーが一時的に生成する環境変数「%_PDB%」が利用できます。 「%_PDB%」 は「実行ファイルのベース名.pdb」に読み替えられます。

試してみましょう。

c:\tmp> del test3.pdb test3.ilk
......

C:\tmp> cl /Brepro /Zi test3.c /link /pdbaltpath:%_PDB%
Microsoft(R) C/C++ Optimizing Compiler Version 19.28.29913 for x86
......
/out:test3.exe
......

C:\new_tmp> del test3.pdb test3.ilk
......

C:\new_tmp> cl /Brepro /Zi test3.c /link /pdbaltpath:%_PDB%
Microsoft(R) C/C++ Optimizing Compiler Version 19.28.29913 for x86
......
/out:test3.exe
......

生成されたバイナリを比較します。

C:\> fc /b c:\tmp\test3.exe c:\new_tmp\test3.exe
ファイル C:\TMP\test3.exe と C:\NEW_TMP\TEST3.EXE を比較しています
FC: 相違点は検出されませんでした

同じになりました。

IDE を使う場合は、プロジェクトのプロパティを開き、[リンカー]-[コマンドライン]-[追加のオプション]欄に「/pdbaltpath:%_PDB%」と手入力してください。

おわりに

Visual C++ での同一ソース同一バイナリの実現方法を探りました。
まとめると次のようになります。

  • コンパイラやリンカーのオプションに「/Brepro」を指定する。
  • デバッグ情報を付加する場合は、「.pdb」や「.ilk」を削除してからビルドする。あるいはリビルドする。
  • デバッグ情報を付加する場合は、リンカーのオプションに「/pdbaltpath:%_PDB%」を指定する。

以上は個人的に調べた結果であり、包括的で正確な説明になっていない可能性がある点、ご了承ください。