0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

VB.NETからWindows APIの呼び出し(PInvoke)をステップインして動作確認を行うための手順(AccessViolationException)

Posted at

前書き

  • VB6のソースをVB.NETに移行した場合に苦労するのがAPI呼び出しエラーの修正です。困ったときの助けになればと思います。

  • Visual Studioのネイティブデバッグ機能を使い、Windows API側に引き渡されている引数の値をAPI側から確認する手順を記載します。

    • .NET側からはポインタや、アドレスの参照先がわからないのでネイティブデバッグします。
  • Declareの定義を修正、もしくは引数を修正後、本当に想定した値がセットされているのか確認することができます。

  • Visual Studioの初期設定では、ネイティブコードデバッグが有効になっていないため、あちこちの設定を変更します。

  • 動作確認用のソースはこちらで公開しています。

環境

  • Visual Studio 2019 (Visual Basic)

    VS2015以降であれば、ほぼ同じだと思います。

サンプルソース

  • CreateDCの呼び出しでエラーになります(バグっています。VB6のソースをコンバートしたイメージで書いています。)

    • プリンターの描画領域サイズをGetDeviceCaps()で取得するサンプルコードです。

    • DeleteDC()の3番目、4番目はnullを指定します(nullがないので0で代用)。本来はDEVMODE構造体を渡すのですが、nullでも動作上は問題ないです。

Module DebugPInvoke
    ' API定義
    Declare Function CreateDC Lib "gdi32" Alias "CreateDCA" _
        (ByVal lpDriverName As String, ByVal lpDeviceName As String, 
        ByVal lpOutput As String, ByVal lpInitData As Object) As Integer
    Declare Function GetDeviceCaps Lib "gdi32" Alias "GetDeviceCaps" _
        (ByVal hdc As Integer, ByVal nIndex As Integer) As Integer
    Declare Function DeleteDC Lib "gdi32" Alias "DeleteDC" (ByVal hdc As Integer) As Integer

    Sub Main()
        Const PRINTER_NAME = "Microsoft Print to PDF"

        ' DeviceContext取得
        Dim hDC = CreateDC("WINSPOOL", PRINTER_NAME, 0, 0)

        ' プリンターの描画サイズを取得
        Dim width_pix = GetDeviceCaps(hDC, 8) ' 8:HORZRES, ピクセル単位の画面の幅
        Dim height_pix = GetDeviceCaps(hDC, 10) '10:VERTRES, ピクセル単位の画面の高さ
        Debug.WriteLine(PRINTER_NAME & " : " & width_pix & " × " & height_pix)

        DeleteDC(hDC)
    End Sub
End Module
  • 例外発生画面

    F5で実行すると例外が発生します。

    Exception_CreateDC.png

    このエラーメッセージでは、どこに原因があるか見当もつきません。ネイティブコードをデバッグできるように設定して、API側から引数を調べてみます。

デバッグのための準備

  1. ネイティブデバッグの有効化

    プロジェクトのプロパティーデバッグネイティブコードデバッグを有効にするにチェック

    Enable_NativeDebug.png

  2. アドレスレベルのデバッグを有効にする

    メニューバーのツールオプションデバッグ全般アドレスレベルのデバッグを有効にするにチェック

  3. マイコードのみを有効にするのチェックを外します(ネイティブコードにステップインできるようにするため)

    メニューバーのツールオプションデバッグ全般マイコードのみを有効にするのチェックを外す

    Enable_AddressLevelDebug.png

  4. シンボルサーバーからpdb読み込み有効化(スタックトレースにネイティブコードの関数名を表示するため)

    メニューバーのツールオプションデバッグシンボルMicrosoftシンボルサーバーにチェック

    Enable_SymbolServer.png

    ※デバッグ開始時に多少時間がかかるようになります。

これで完了です。

ネイティブコード側からAPIの引数を確認する

ここではネイティブコードにステップインするため、デバッグを開始してから設定をを行います。

  1. CreateDC()行にブレークポイントセット(F9)

  2. F5でデバッグを開始して、ブレークポイント(CreateDC)で止まるのを待ちます(シンボル情報をダウンロードするため少し時間がかかる)

  3. 呼び出し履歴を表示

    メニューバーのデバッグウィンドウ呼び出し履歴

    Enable_ExternalCode.png

    マイコードのみを有効にするのチェックを外したため、ネイティブコードの関数名も表示されています。

  4. ソースコードを右クリックして逆アセンブリへ移動をクリック

    MoveTo_Assemble.png

    ソースコードが逆アセンブルされた状態で表示されます

F5を押してエラー箇所まで実行する

  1. 例外が発生した箇所gdi32full.dll!_GdiConvertToDevmodeW@4で止まり、表示される例外ダイアログもネイティブコードの例外エラー(0xC0000005 メモリアクセス違反)に変わります。

    Exception_ExternalCode.png

    Stacktrace_External.png

CrateDC()側から引数を確認する (スタックのベースポインタ(EBP)から探します)

  1. 呼び出し履歴からgdi32.dll!_CreateDCA@16()を選択します

  2. メモリウィンドウを表示します(スタックとポインタの参照先を表示するため2つ表示します)

    メニューバーのデバッグウィンドウメモリ1メモリ2をクリック

    ※表示されない場合はCtrl + Alt + M, 1 (CtrlとAltとMを同時押しした後、1を押す)

  3. メモリウィンドウを右クリックして 4バイトの変数16進数で表示 を選択します

    32bitのプロセスで動いているため、4バイト単位に区切って表示をします。

  4. メモリ1のアドレスにebpと入力します。(ベースポインタ⇒スタック上に確保されているデータの格納領域の基点)

    Memory_CreateDC.png

    ebpはCPUのレジスタの名前です。詳細は下記のページなどを参照してくださいい。

    x86アセンブリ言語での関数コール

    スタックフレーム - 関数に渡される引数を知る

  • 左上から順に、ひとつ前のフレームのEBP、リターンアドレス(関数の戻り先アドレス)、引数1、引数2、引数3、引数4です。

    アドレス 説明
    008ff234 ひとつ前のフレームのEBP
    6cdbf36d リターンアドレス
    00d6c874 引数1
    00d90864 引数2
    00d8bcac 引数3
    00000003 引数4
  • 引数1「0x00D6C874」をメモリ2で見ると、確かに WINSPOOL が入っていることがわかります

    Memory_Args1.png

    同様に引数2にはMicrosoft Print to PDFが入っています。

    Memory_Args2.png

  • 引数3と引数4を見ると、ソース上では0(null)を渡したつもりですが、なぜか0ではない値(00d8bcac,00000003)がセットされてます。

    Dim hDC = CreateDC("WINSPOOL", PRINTER_NAME, 0, 0)
  • 引数3の値のアドレスを見ると文字列で'0'が入っています。⇒引数の型をStringで宣言しているため、数値の0が文字に変換され、そのポインタがCrateDC()に渡されていることがわかります。

    Memory_Args3.png

  • 引数4の00000003は何かわかりませんが、少なくとも期待していた0ではない値がセットされていることがわかります。

    VB側で渡した値と異なる原因はDeclareの宣言がまちがっているためです。修正してもう一度試してみます

APIの定義を修正して、もう一度引数を確認してみます

  • 3番目の引数と、4番目の引数をIntPtrに変更します
Declare Function CreateDC Lib "gdi32" Alias "CreateDCA" _
   (ByVal lpDriverName As String, _
   ByVal lpDeviceName As String, _
   ByVal lpOutput As IntPtr, _
   ByVal lpInitData As IntPtr) As Integer

もう一度デバッグで確認すると、3番目の引数と4番目の引数に0(null)がセットされていることが確認できました。

Memory_FixBug.png

  • 例外で停止しなくなるため逆アセンブリへ移動した後、下記call命令の位置にブレークポイントをセットして停止させた後、F11でステップインする必要があります。

    Stepin_CreteDC.png

後書き

あまり使わない機能ですので、不正確な部分や、説明が不足している箇所があるかもしれません。
添削や過不足あれば、お気軽にご指摘ください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?