メモ:WindowsでのRのクラッシュをWinDbg Previewを使って解析する

原因がよくわからないけどWindowsでだけRがクラッシュする、ということがあって、地道なprintデバッグで調べた結果、問題はR本体側で起こっているということが分かった。 R本体側なのでprintデバッグを仕込めないし(Rをビルドしなおせばいいという話はあるけど)、仕方ないのでいろいろやり方を調べた時のメモ。

どうやらダンプを採る必要があるらしい

問題のクラッシュはCIでも再現していて、

  error: test failed, to rerun pass '--lib'
  
  Caused by:
    process didn't exit successfully: ...略... (exit code: 0xc0000374, STATUS_HEAP_CORRUPTION)

https://github.com/extendr/extendr/runs/4904538570?check_suite_focus=true#step:8:62

というエラーが出ていた。STATUS_HEAP_CORRUPTIONで検索すると以下のページが引っかかった。 AdPlusというツールでダンプを採取するみたいなことが必要になるらしい。

で、AdPlusって何?、と思ったけど検索してもあまり情報が出てこない。 Windowsデバッグツールはいろいろあるけど、WinDbgという方が公式ツールらしくて無難そうだったのでそっちにすることにした。

WinDbgWinDbg Preview

WinDbg、で検索するとこのページが出てくる。

WinDbgには、Windows SDKの一部として配布されている「WinDbg」と、 新しいバージョンである「WinDbg Preview」というやつがあるらしい。 よくわからないのではじめはWinDbgを使ってたけど、

WinDbg Preview is a new version of WinDbg with more modern visuals, faster windows, and a full-fledged scripting experience.

と書かれてるように、WinDbg Previewの方が便利だった。

インストールも、WinDbgWindows SDKインストーラーで適切にチェックボックスを入れてインストールする必要があるけど、 WinDbg PreviewWindows Storeからインストールできてわかりやすかった。

WinDbg Previewをpostmortem debugに使う

いろいろ機能があってぜんぜん使いこなせないけど、今回は、例外が発生したところでデバッグするpostmortem debugというのを使った。 このドキュメントはWinDbg用だけど、たぶんWinDbg Previewでもだいたい同じ。

まず、Powershellもしくはcmd.exeを管理者権限で開いて、

WinDbgX.exe -I

を実行する。すると、WinDbg Previewの画面が立ち上がる。

f:id:yutannihilation:20220122180335p:plain

この画像の右下のように「Successfully set WinDbgNext as the postmortem debugger on this machine」みたいなメッセージが出ていれば、 WinDbg Previewがアプリのクラッシュ時に自動で立ち上がるように設定できた。はず。 このWinDbg Previewのウィンドウは、特に何もしないのでクローズする。

【重要】戻し方

ちなみに、この設定をするとアプリがクラッシュするたびにWinDbg Previewが立ち上がるようになるが、 ずっとそれだとうざいので開発時以外は戻したくなる。

これは、↑のページに書かれているように、WinDbgX.exe -Iレジスタに値を設定してるだけなので、そのレジスタを削除すれば止まる。 レジスタエディタを開いて、 AeDebugで検索すると以下のようなキー(HKEY_xxxの部分はどうやら環境によって違うっぽい?)があるので、 この「Debugger」という項目を削除すればいい。

f:id:yutannihilation:20220122180915p:plain

クラッシュさせる

これで準備が整ったので、クラッシュを再現させる。 今回はこういうRスクリプトで確実にクラッシュするのでこれを実行する。

&"C:\Program Files\R\R-4.1.2\bin\Rscript.exe" -e "library(yadngd); my_device('foo'); dev.off()"

クラッシュすると、 WinDbg Previewの画面が立ち上がる。

f:id:yutannihilation:20220122181343p:plain

Viewのタブから「Stack」をクリックするとスタックが見れる。

f:id:yutannihilation:20220122181718p:plain

GEdestroyDevDescという文字列が見えるので、どうやら、この関数のどちらかのfreeで解放済みのメモリをさらに解放しようとしてる?、ということがわかった。

void GEdestroyDevDesc(pGEDevDesc dd)
{
    int i;
    if (dd != NULL) {
    for (i = 0; i < MAX_GRAPHICS_SYSTEMS; i++) unregisterOne(dd, i);
    free(dd->dev);
    dd->dev = NULL;
    free(dd);
    }
}

(https://github.com/wch/r-source/blob/8ebcb33a9f70e729109b1adf60edd5a3b22d3c6f/src/main/engine.c#L78-L87)

めでたしめでたし(なおこのクラッシュの原因はまだ不明...)