ネイティブ アプリ開発者に捧ぐ！Azure DevOps をシンボル サーバーとして使って気持ちよくデバッグしよう！

これは Advent Calendar 2021 - Azure DevOps の 23 日目の記事です。

こんにちは。
今日は存在があまり知られていない Azure DevOps の機能の 1 つである "シンボル サーバー" についてお話します。

## よくある悲しい話

もし皆さんがソフトウェア開発会社に勤められていて、Windows 向けのネイティブ アプリを開発しているとすると、こんな経験がありませんか？

>お客様「あのー、御社の製品○○を使っているんですが、突然アプリがクラッシュするんですけど...」
あなた「ご迷惑おかけしてすみません。どのような操作をされましたか？」
お客様「えーっと...なんか突然落ちたんで、よくわからないです。クラッシュ時のダンプ ファイルはあるので、送ることはできます。」
あなた「ありがとうございます。確認させていただきます。」
(数時間後...)
あなた「(うーん、確かにアプリ内でクラッシュしてるみたいだけど、使っているバージョンが古くてシンボル情報も一致しないからこれ以上は調査できないなぁ。最新バージョンを送って再現するか確認してもらうか...）」
(その後...)
あなた「お客様、申し訳ありませんが新しいバージョンをお送りしますので、再現するかどうかご確認いただけますか？」
お客様「...(えー、簡単に言うけど、違うバージョンに置き換えるの大変なんだけどなぁ）」

※今回はクラッシュを例にしましたが、メモリ リーク、ハング、などの問題でも同様です。


このように、ネイティブ アプリケーションを開発している場合、お客様によって使用されているバージョンが異なるということはよくあります。
そして、ネイティブ アプリケーションをデバッグするにはお客様が使用しているバイナリ(モジュール)に合致したシンボル ファイルが必要となります。
シンボル情報を正しく管理していないと、当時出荷した製品のシンボル情報が社内になく、再調査するには新しいバージョンまたは再ビルドしたバージョンで再現させる必要がある、という顛末になりがちです。
（そして、なぜか調査用に送ったバージョンでは発生せずに、モヤモヤした気持ちのまま終わるというケースもあることでしょう...）

そんなときに使うのが、"シンボル サーバー" です。
事前にシンボル サーバーにシンボル情報を保存しておけば、ダンプ情報に含まれるモジュール情報から適切なシンボルが自動取得され、すぐに調査をすることができます。

シンボルサーバーの詳細については下記のドキュメントが参考になります。

https://docs.microsoft.com/ja-jp/windows/win32/debug/symbol-servers-and-symbol-stores

たとえば、マイクロソフトではパブリック シンボルをシンボル　サーバーとして一般に公開しています。

https://docs.microsoft.com/ja-jp/windows-hardware/drivers/debugger/microsoft-public-symbols


社内にシンボルサーバーを設置し、出荷時にシンボル情報を保存しておけば良いのですが、次のような課題が残ります。

 - シンボル サーバーの構築、運用のコスト
 - シンボル サーバーへの発行忘れ

しかし、Azure DevOps を使えばシンボル サーバーの役割は付属していますし、シンボル サーバーへの発行も自動化することができます。

## Azure Pipelines を使ってシンボル サーバーに発行する

シンボル サーバーは Azure DevOps のサービスの 1 つである `Azure Artifacts` が提供する機能です。
パッケージ管理として知られているサービスですが、シンボル情報の管理先としても機能します。

Azure Artifacts にシンボルを発行するには、Azure Pipelines 上で `PublishSymbols` タスクを使用する必要があります。

https://docs.microsoft.com/ja-jp/azure/devops/pipelines/tasks/build/index-sources-publish-symbols?view=azure-devops

実際に Visual C++ のプロジェクトをビルドし、シンボル情報の発行、ビルド成果物を Pipeline Artifacts として発行（アップロード）する YAML ファイルの例を以下に示します。 (Visual Studio 2022 でビルドするため `windows-2022` を使用しています)

```yaml
trigger:
- master

pool:
  vmImage: 'windows-2022'

variables:
  solution: '**/*.sln'
  buildPlatform: 'x86'
  buildConfiguration: 'Release'

steps:
- task: NuGetToolInstaller@1

- task: NuGetCommand@2
  inputs:
    restoreSolution: '$(solution)'

- task: VSBuild@1
  inputs:
    solution: '$(solution)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: PublishSymbols@2
  inputs:
    SearchPattern: '$(BuildConfiguration)\**\*.pdb'
    SymbolServerType: 'TeamServices'

- task: CopyFiles@2
  inputs:
    Contents: '$(BuildConfiguration)\**\*.exe'
    TargetFolder: '$(build.artifactstagingdirectory)'

- publish: '$(build.artifactstagingdirectory)'
  artifact: drop
```

:::note info
今回は Git + YAML パイプラインを例に示しましたが、Team Foundation バージョン管理 (TFVC) + クラシック パイプラインでも実現可能です。
:::


## デバッグ時にシンボルサーバーを使用する

実際にシンボル サーバーに発行したシンボル情報をデバッグで使用してみましょう。

今回は、Azure Pipelines を使ってお客様にリリースした「とあるバージョン」でアプリケーションがクラッシュしたため、お客様からダンプを受領したと仮定します。
まずは通常通り、WinDbg などのデバッガーでクラッシュ時のダンプを開きます。

```
"C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\windbg.exe" -z C:\works\sample.dmp
```

デバッガが起動すると以下のような画面が表示されました。

```
Microsoft (R) Windows Debugger Version 10.0.22000.194 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.


Loading Dump File [C:\works\sample.dmp]
User Dump File: Only application data is available

Symbol search path is: srv*
Executable search path is: 
Windows 10 Version 19044 UP Free x64
Product: WinNt, suite: TerminalServer SingleUserTS
Machine Name:
Debug session time: Sat Nov 27 14:11:12.611 2021 (UTC + 0:00)
System Uptime: 0 days 0:59:20.292
Process Uptime: not available
.....
Loading Wow64 Symbols
............
(1fcc.77c): Access violation - code c0000005 (first chance)
For analysis of this file, run !analyze -v
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
*** WARNING: Unable to verify checksum for HorrorApp.exe
HorrorApp+0x1071:
00651071 c70714000000    mov     dword ptr [edi],14h  ds:002b:00000000=????????
```

文字の中に `Access violation - code c0000005` という文字が見えます。
どうやらよからぬアドレスにアクセスしてしまいクラッシュしたようです。

では、スタックを表示 (k) して発生箇所を確認してみます。

```
0:000:x86> k
 # ChildEBP RetAddr      
WARNING: Stack unwind information not available. Following frames may be wrong.
00 0075f7bc 768afa29     HorrorApp+0x1071
01 0075f7cc 775c7a9e     KERNEL32!BaseThreadInitThunk+0x19
02 0075f828 775c7a6e     ntdll_77560000!__RtlUserThreadStart+0x2f
03 0075f838 00000000     ntdll_77560000!_RtlUserThreadStart+0x1b
0:000:x86> lm
start    end        module name
00650000 00658000   HorrorApp C (no symbols)           
72f60000 72fcd000   MSVCP140   (deferred)             
74420000 74435000   VCRUNTIME140   (deferred)             
74900000 7490a000   CRYPTBASE   (deferred)             
75cd0000 75df0000   ucrtbase   (deferred)             
76810000 7688a000   ADVAPI32   (deferred)             
76890000 76980000   KERNEL32   (pdb symbols)          C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\sym\wkernel32.pdb\0E86B33FBDC82F2F4415F9A1D2DCE6B31\wkernel32.pdb
76c90000 76d05000   sechost    (deferred)             
76f90000 771a5000   KERNELBASE   (deferred)             
771d0000 7728f000   msvcrt     (deferred)             
77290000 7734f000   RPCRT4     (deferred)             
77550000 7755a000   wow64cpu   (deferred)             
77560000 77703000   ntdll_77560000   (pdb symbols)          C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\sym\wntdll.pdb\B6EB6DFF017F36A18E8034D67B4DA9941\wntdll.pdb
97c50000 97cd3000   wow64win   (deferred)             
989b0000 98a09000   wow64      (deferred)             
00007ffe`98a50000 00007ffe`98c45000   ntdll      (deferred)
```
まだシンボル サーバーを指定していないので、自分のアプリ (HorrorApp) のシンボルが解決されていません。
ロード モジュールの一覧 (lm) でも `HorrorApp C (no symbols) ` と表示されていることがわかります。

さて、いよいよシンボルサーバーの出番です。
シンボル情報を発行した Azure Artifacts をシンボル サーバーとして参照します。
WinDbg で Azure Artifacts をシンボルサーバーとして設定するには、3 ステップ必要です。

  1. 事前にシンボル情報を参照可能な個人用アクセス トークン (PAT) を生成する
  2. WinDbg 上で `.sympath+  https://artifacts.dev.azure.com/<YourOrg>/_apis/symbol/symsrv` を実行する
  3. 資格情報のダイアログに 1 で取得した PAT をパスワードに入力する（ユーザー名は空欄のままで OK）

シンボル サーバーを追加後、再度スタック情報を表示 (k) します。

```
0:000:x86> k
 # ChildEBP RetAddr      
00 (Inline) --------     HorrorApp!A::set+0x1b [D:\a\1\s\HorrorApp\main.cpp @ 10] 
01 0075f774 00651577     HorrorApp!main+0x71 [D:\a\1\s\HorrorApp\main.cpp @ 23] 
02 (Inline) --------     HorrorApp!invoke_main+0x1c [d:\a01\_work\10\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 78] 
03 0075f7bc 768afa29     HorrorApp!__scrt_common_main_seh+0xfa [d:\a01\_work\10\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288] 
04 0075f7cc 775c7a9e     KERNEL32!BaseThreadInitThunk+0x19
05 0075f828 775c7a6e     ntdll_77560000!__RtlUserThreadStart+0x2f
06 0075f838 00000000     ntdll_77560000!_RtlUserThreadStart+0x1b
```

今度はしっかり表示されました！

どうやら A クラスの set メソッドを呼び出した時に Access Violation が発生したようですね。
このままデバッグすることも可能ですが...せっかくならソースコードを見て調査したいところです。

## 対応するソースコードも簡単に取得できる

実は Azure Pipelines のパイプライン上で `PublishSymbols` タスクを実行すると、Azure Artifacts へシンボルを発行する際に Azure Repos 上のどの時点のコードを使ってビルドしたのか、という情報がシンボル情報にしっかり格納されています。

なので、WinDbg 上で `.srcpath srv*` を実行すると対応するコードが自動的に取得され、表示されます。
(ビルドの履歴やコード履歴から、ビルド当時のコードを発掘する必要はありません！)

![WinDbgWithSrc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32697/5a504127-289a-e0a9-d423-cf292fa58460.png)

ソースコードも表示されて、ずっとデバッグしやすくなりましたね。

さて、せっかくなので最後までデバッグしてみましょう。
set メソッドで問題が発生していますが、呼び出し元である main 関数を見てみるため、フレームを切り替えます。

```
0:000:x86> .frame 1
01 0075f774 00651577     HorrorApp!main+0x71 [D:\a\1\s\HorrorApp\main.cpp @ 23] 
```

![WinDbgWithSrc2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32697/379125b7-5424-704a-e24d-7da1640ef39c.png)

フレームを変更すると、ちゃんとコード上でハイライトも移動します。
`pA2->set(20);` を呼んだときに起きたようです。
pA2 のアドレスはどうなっているでしょうか？引数を表示してみます (dv)。

```
0:000:x86> dv
            pA1 = 0x008d5568
            pA2 = 0x00000000
```

`pA2 = 0x00000000` になっていますね。
表示されているソースコード上も `A* pA2 = NULL;` になっていますので、これが原因とわかりました。


シンボル情報があるだけでも便利ですが、対応するソースコードが手軽に入手できるとさらに調査が楽ですね。


## よくあるご質問 (FAQ)
#### Q1. このシンボル サーバー機能は無料で使えますか？
A1. はい、無料で使えます！ただし、Azure Artifacts の容量を使用しますので、無料枠（2GiB) を超えると課金が発生します。
課金が怖い場合は、保持ポリシー (後述) や顧客へ出荷したバージョンに限ってシンボル発行するなど、うまく節約してみてください。

#### Q2. シンボル情報はずっと保持されますか？
A2. いいえ、生成したビルドと同じ期間保持されます。
ビルドの保持ポリシー（既定は 30 日など) でビルドが削除されると、自動的に対応するシンボル情報も削除されます。

#### Q3. バージョン管理は別のサービスで行っているのですが、Azure Pipelines と Azure Artifacts を使えば、シンボルサーバー機能が使えますか？
A3. いいえ、使えません。Azure Pipeline の `PublishSymbols` タスクは、Azure Repos 上のリポジトリのみに対応しています。
ただ、お使いのバージョン管理方式が Git ならば、Azure Repos 上のリポジトリとミラーリングするなど、回避策はあると思います。

:::note info
実は、シンボル情報から対応するソースコードを取得する際に tf.exe と呼ばれる Visual Studio Team Explorer に含まれるコマンドラインを使います。
このコマンドは Team Foundation バージョン管理 (TFVC) および Azure Repos 上の Git 専用のコマンドライン ツールです。
:::

まとめ
====
本日は Azure DevOps が提供するサービスの 1 つである Azure Artifacts が持つ "シンボルサーバー" 機能についてお話しました。
これまでの流れでお気づきのとおり、この機能は Azure Repos + Azure Pipelines + Azure Artifacts という Azure DevOps 上の 3 サービスが連携することで実現できる機能で、他の単独のリポジトリ サービスやビルド サービスでは実現できない、まさに Azure DevOps ならではの機能です。

今回はなるべく「低レイヤーな開発でも使える」、また「本番やお客様環境で発生した問題調査でも使える」、ということをイメージしていただくためにダンプ ファイルを WinDbg でデバッグしましたが、Visual Studio のデバッグでも、ライブ デバッグでもお使いいただけます。もちろん、.NET の開発でも。

今時は GitHub や GitLab だよねー！という声もチラホラと聞こえる 2021 年ですが、「Azure DevOps も捨てたモノじゃないんですよ！！」という魅力を少しでもお伝えできていたら嬉しいです。

それでは良い DevOps ライフを！