2019年9月26日木曜日

デバッグ情報の扱い~PDBファイルについて~

PDBファイルとデバッグ情報の関係などについて調べたことを整理した。
また、EXEをターゲットとした際に、(1) /Z7 でデバッグ用にビルドする場合、(2) /Zi でデバッグ用にビルドする場合、(3)/Zi と /O1 でリリース用にビルドする場合について、作成されるファイルとファイルサイズがどうなるかを比較した。

知識

コンパイラPDBとリンカPDB
PDBファイルには、コンパイラにより作成されるものとリンカにより作成されるものがある[1]。
前者を「コンパイラPDB」、後者を「リンカPDB」と呼び区別することにする。
 
<コンパイラPDB>
  • デフォルトの名前は、プロジェクト内でコンパイルされれば"プロジェクト名.pdb"、プロジェクト外でコンパイルされれば"vcx0.pdb"となる(xはVisual Studioのバージョン)
  • このPDBへの名前とパスがOBJファイルに含まれる

<リンカPDB>
  • デフォルトの名前は"$(TARGET).pdb"である
  • このPDBへの名前とパスがEXEファイルとDLLファイルに含まれる
  • 完全なデバッグ情報を含み、デバッグの際に必要となる

コンパイラのデバッグ情報出力の設定
コンパイラでデバッグ情報を出力する方法は複数存在する[2]。

<デバッグ情報をOBJファイルに含める場合>
  • コンパイラオプション /Z7 を指定することで、デバッグシンボルのすべてがOBJファイルに含まれるようになる
  • コンパイラPDBは作成されないが、OBJファイルがその分大きくなる
  • デバッグシンボルには、関数名と行数、変数の名前と型の情報がすべて含まれる

<デバッグ情報をコンパイラPDBに含める場合>
  • コンパイラオプションに /Zi もしくは /ZI を指定することで、デバッグシンボルをコンパイラPDBとしてOBJファイルと分けて作成することができる。その分、OBJファイルは /Z7 を指定した場合よりも小さくなる
  • /Zi を指定すると、最適化オプション(/OPT)のデフォルト値が変更される。
    (イメージのサイズが大きくなり、速度が遅くなってしまう)
  • /ZI は /Zi と同様にコンパイラPDBを出力するが、Eidt and Continueをサポートするという点で異なる

<デバッグ情報が必要ない場合>
  • /Z{7|i|I}を指定しなければよい。

リンカのデバッグ情報出力の設定
  • リンカオプション /DEBUG を指定してリンクすることで、デバッグシンボルを含むリンカPDBを出力することができる
  • リンカPDBの名前とパスがターゲット(EXEもしくはDLL)に含められる。別の見方をすれば、ターゲット自体にデバッグ情報が埋め込まれることはないということである
  • デバッグ時には、このリンカPDBが必要である


デバッグ時とリリース時

デバッグオプションと最適化オプションは、関連性はあるが同じものではない。
リリースのために最適化されたバイナリを作る場合も、デバッグに必要なPDBファイルを作成することができる。

開発中からデバッグ情報をPDBファイルに出力するようにしておき、リリース時には最適化オプションを付け加える[3]のがよいだろう。

テスト

(1) /Z7 でデバッグ用にビルドする場合、(2) /Zi でデバッグ用にビルドする場合、(3)/Zi と /O1 でリリース用にビルドする場合について、作成されるファイルとファイルサイズがどうなるかを確かめてみた。

実行環境
OS 名: Microsoft Windows 8.1
システム モデル: dynabook KIRA V73/PS
システムの種類: x64-based PC
プロセッサ: Intel64 Family 6 Model 61 Stepping 4 GenuineIntel ~2200 Mhz
物理メモリの合計: 8,103 MB

Visual Studioのバージョン: 14
コンパイラ: Microsoft (R) C/C++ Optimizing Compiler Version 19.00.24215.1 for x86

テストコード(共通)

main.cc

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <windows.h>
  4. #include "./sub.h"
  5.  
  6. namespace {
  7. constexpr wchar_t WINDOW_NAME[] = L"MyWindow";
  8. constexpr wchar_t CLASS_NAME[] = L"MyWindowClass";
  9. } // namespace
  10.  
  11. int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
  12. LPTSTR lpsCmdLine, int nCmdShow) {
  13. (void)hInstance;
  14. (void)hPrevInstance;
  15. (void)lpsCmdLine;
  16. (void)nCmdShow;
  17.  
  18. FILE* fp = nullptr;
  19. AllocConsole();
  20. _wfreopen_s(&fp, L"CONOUT$", L"w", stdout);
  21. _wfreopen_s(&fp, L"CONOUT$", L"w", stderr);
  22. _wfreopen_s(&fp, L"CONIN$", L"r", stdin);
  23.  
  24. // Use C++ features.
  25. PrintVector();
  26. system("pause");
  27.  
  28. FreeConsole();
  29. return 0;
  30. }


sub.cc
  1. #include "./sub.h"
  2. #include <stdio.h>
  3. #include <vector>
  4.  
  5. void PrintVector() {
  6. std::vector<int> array(3);
  7. array[0] = 0;
  8. array[1] = 1;
  9. array[2] = 2;
  10. for (unsigned int i = 0; i < array.size(); i++) {
  11. fwprintf(stdout, L"array[%d] = %d\n", i, array[i]);
  12. }
  13. }

sub.h

  1. #ifndef _SUB_H_
  2. #define _SUB_H_
  3.  
  4. void PrintVector();
  5.  
  6. #endif // _SUB_H_


CPPFLAGS およびLFLAGS の設定(makefileより抜粋)
nmakeのためのmakefileではCPPFLAGSにコンパイラオプションを、LFLAGSにリンカオプションを組み立てている。

(1) /Z7 でデバッグ用にビルドする場合
  1. CPPFLAGS = /nologo /W4 /EHsc /DUNICODE /D_UNICODE /Z7
  2. LFLAGS = /NOLOGO /SUBSYSTEM:WINDOWS /MAP:$(MAP) /PDB:$(PDB)

(2) /Zi でデバッグ用にビルドする場合
  1. CPPFLAGS = /nologo /W4 /EHsc /DUNICODE /D_UNICODE /Zi
  2. LFLAGS = /NOLOGO /SUBSYSTEM:WINDOWS /MAP:$(MAP) /PDB:$(PDB)

(3)/Zi と /O1 でリリース用にビルドする場合
  1. CPPFLAGS = /nologo /W4 /EHsc /DUNICODE /D_UNICODE /Zi /O1
  2. LFLAGS = /NOLOGO /SUBSYSTEM:WINDOWS /DEBUG /MAP:$(MAP) /PDB:$(PDB)

なお、
  1. MAP = main.map
  2. PDB = main.pdb
である。

結果と考察

それぞれの場合におけるファイルサイズを下表に示す(サイズの単位:Bytes)。

main.obj sub.obj vc140.pdb main.pdb main.exe
(1) /Z7 でデバッグ用にビルドする場合 92064 150624 N/A 6180864 441344
(2) /Zi でデバッグ用にビルドする場合 18224 50324 233472 6180864 441344
(3) /Zi と /O1 でリリース用にビルドする場合 19305 52899 233472 6115328 425984


  • /Z7 を付けた場合、コンパイラPDBである vc140.pdb が作成されず、/Zi を付けた場合よりもOBJファイルが3~5倍ほど大きくなった。
  • /Z7、/Zi でOBJファイルのサイズが違っても、最終的に作成されるリンカPDBのサイズは同じになった。
  • /O1 オプションを付けてファイルサイズを最適化すると、EXEのサイズがちょっとだけ小さくなった。また、リンカPDBのサイズも小さくなるった。


まとめ


  • PDBファイルには、コンパイラにより作成されるものとリンカにより作成されるものがある。
  • デバッグ情報をオブジェクトファイルに含めた場合と、コンパイラPDBに含めた場合とで、リンカPDBのサイズに違いはなかった
  • リリース時には、/Z{7|i|I} に加えて最適化オプションを指定するのがよい


感想

PDBファイルについては「なんとなく」でやっていた所があったので、オプションなどと合わせて整理できてよかった。

なんとなく設定している系のオプションは、/MT などほかにもあるので、適宜調べていきたい。

今回はEXEファイルがターゲットの場合でテストをしたが、ライブラリファイル(*.lib)がターゲットの場合では、PDBの扱いに違いがあると思われる。
ライブラリファイルを扱う必要が生じた際に調べたい。

GitHub

今回のコードの最新版はここに上げておく。

参考文献


  1. PDB Files – What are they and how to generate them.
  2. /Z7, /Zi, /ZI (Debug Information Format)
  3. よく使うコンパイル・リンカオプション


0 件のコメント:

コメントを投稿

コメント表示は承認制に設定しています