メモ: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という方が公式ツールらしくて無難そうだったのでそっちにすることにした。
WinDbg? WinDbg 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の方が便利だった。
インストールも、WinDbgはWindows SDKのインストーラーで適切にチェックボックスを入れてインストールする必要があるけど、 WinDbg PreviewはWindows Storeからインストールできてわかりやすかった。
WinDbg Previewをpostmortem debugに使う
いろいろ機能があってぜんぜん使いこなせないけど、今回は、例外が発生したところでデバッグするpostmortem debugというのを使った。 このドキュメントはWinDbg用だけど、たぶんWinDbg Previewでもだいたい同じ。
まず、Powershellもしくはcmd.exeを管理者権限で開いて、
WinDbgX.exe -I
を実行する。すると、WinDbg Previewの画面が立ち上がる。
この画像の右下のように「Successfully set WinDbgNext as the postmortem debugger on this machine」みたいなメッセージが出ていれば、 WinDbg Previewがアプリのクラッシュ時に自動で立ち上がるように設定できた。はず。 このWinDbg Previewのウィンドウは、特に何もしないのでクローズする。
【重要】戻し方
ちなみに、この設定をするとアプリがクラッシュするたびにWinDbg Previewが立ち上がるようになるが、 ずっとそれだとうざいので開発時以外は戻したくなる。
これは、↑のページに書かれているように、WinDbgX.exe -I
はレジスタに値を設定してるだけなので、そのレジスタを削除すれば止まる。
レジスタエディタを開いて、 AeDebug
で検索すると以下のようなキー(HKEY_xxx
の部分はどうやら環境によって違うっぽい?)があるので、
この「Debugger」という項目を削除すればいい。
クラッシュさせる
これで準備が整ったので、クラッシュを再現させる。 今回はこういうRスクリプトで確実にクラッシュするのでこれを実行する。
&"C:\Program Files\R\R-4.1.2\bin\Rscript.exe" -e "library(yadngd); my_device('foo'); dev.off()"
クラッシュすると、 WinDbg Previewの画面が立ち上がる。
Viewのタブから「Stack」をクリックするとスタックが見れる。
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); } }
めでたしめでたし(なおこのクラッシュの原因はまだ不明...)