C# と VB.NET の質問掲示板

ASP.NET、C++/CLI、Java 何でもどうぞ

C# と VB.NET の入門サイト

AccessViolationExceptionについて

[トピック内 6 記事 (1 - 6 表示)]  << 0 >>

■84847 / inTopicNo.1)  AccessViolationExceptionについて
  
□投稿者/ makoto (37回)-(2017/08/09(Wed) 09:37:00)

分類:[.NET 全般] 

いつもお世話になります。

現在、VB6で作られたプログラムを.Netに置き換える作業を行っているのですが、
そのプログラムの中でInternetReadFileメソッドを使用したプログラムが
あるのですが、InternetReadFileメソッドにてSystem.AccessViolationExceptionが
起こる原因として考えられるものはどういったものがあるでしょうか?
AccessViolationExceptionの詳細な情報を得ることが出来ずに
作業が難航しています。

●環境

■InternetReadFileメソッドを使用しているプログラム側
・OS :Windows Server2012 R2 Standard(64Bit)
・開発環境:Visual Studio 2015
・ビルド環境
.Net Framework4.6
.Any CPUでビルド

■要求先
・OS :Windows Server2012 R2 Standard(64Bit)
・IIS :Version 8.5.9600

●エラー詳細
System.AccessViolationException はハンドルされませんでした。
Message: 型 'System.AccessViolationException' のハンドルされていない例外が mscorlib.dll で発生しました
追加情報:保護されているメモリに読み取りまたは書き込み操作を行おうとしました。他のメモリが壊れていることが考えられます。

●プログラム抜粋

Dim lngInetHandle As Integer 'インターネットハンドルを格納
Dim lngUrlHandle As Integer 'URLハンドルを格納
Dim lngArraySize As Integer 'データを受け取るバッファ bytRecvData のサイズ
Dim intRet As Short 'InternetReadFile の戻り値を格納
Dim lngTotalRead As Integer '受信したデータの総サイズ 兼 バッファへのポインタ
Dim lngReadSize As Integer '受信したデータのサイズ
Dim lngRet AS Integer 'バイト数

'インターネットハンドルを作成
lngInetHandle = InternetOpen("WinInet Test", INTERNET_OPEN_TYPE_PRECONFIG, vbNullString, vbNullString, 0)

'URLハンドルを作成
lngUrlHandle = InternetOpenUrl(lngInetHandle, 該当のURL, vbNullString, 0, &H80000000, 0)

lngArraySize = WI_INITBUFSIZE - 1
ReDim bytRecvData(lngArraySize) 'バッファの初期サイズを確保する

Do
'次回受信時、バッファのサイズをオーバーしてしまう危険性がある場合
If (lngTotalRead + 1024) > lngArraySize Then
'バッファサイズを 32767 バイト増やす
lngArraySize = lngArraySize + 32767
ReDim Preserve bytRecvData(lngArraySize)
End If

'インターネット上のデータを読み込む
intRet = InternetReadFile(lngUrlHandle, bytRecvData(lngTotalRead), 1024, lngReadSize)
' ↑↑↑↑↑↑↑↑↑AccessViolationExceptionが発生している箇所↑↑↑↑↑↑↑↑↑

lngTotalRead = lngTotalRead + lngReadSize '読み込んだ総サイズを計算
'受信サイズが0、または読み込み失敗の場合、ループを抜ける
If (lngReadSize = 0) Or (intRet = 0) Then Exit Do
Loop

If lngTotalRead Then '実際にデータが読み込まれた場合
'配列サイズを実サイズに削る
ReDim Preserve bytRecvData(lngTotalRead - 1)
lngRet = lngTotalRead '読み込んだバイト数を返す
Else
Erase bytRecvData '総受信サイズが 0 の場合、配列を消去
End If

Call InternetCloseHandle(lngUrlHandle) 'URLハンドルを閉じる

●該当のURLでやっていること

・ASP(Classic)をリクエスト
・ASPではServer.CreateObjectでDLLを呼び出している。
・DLLではリクエストパラメータからの値をキーにOracleからデータを取得
・取得したデータを元にCSVファイルを作成
・CSVファイルを元にサードパーティ製の帳票作成ツールを使用して帳票を作成


●その他

・該当のURLを直接ブラウザから要求した場合は、問題なく動作する。(帳票が作成される)


引用返信 編集キー/
■84848 / inTopicNo.2)  Re[1]: AccessViolationExceptionについて
□投稿者/ 魔界の仮面弁士 (1379回)-(2017/08/09(Wed) 13:23:04)
No84847 (makoto さん) に返信
> ●該当のURLでやっていること
> ・ASP(Classic)をリクエスト
とりあえずサーバーサイドの問題では無さそうなので、
VB2015 側のコードに着目して障害対応すれば大丈夫かと思います。


> そのプログラムの中でInternetReadFileメソッドを使用したプログラムが
アンマネージな API に頼らず、System.Net.WebClient.OpenRead あるいは DownloadFile 等を
使うわけにはいかないのでしょうか。


> Dim lngInetHandle As Integer 'インターネットハンドルを格納
各種ハンドルを As Integer で扱わないようにしましょう。
API 宣言も含め、As IntPtr で統一するべきです。(あるいは HandleRef とか)


> intRet = InternetReadFile(lngUrlHandle, bytRecvData(lngTotalRead), 1024, lngReadSize)
> ' ↑↑↑↑↑↑↑↑↑AccessViolationExceptionが発生している箇所↑↑↑↑↑↑↑↑↑

肝心の API 宣言部(Declare もしくは DllImport 部)が無いので、以下は予想になりますが:

ByRef As Byte に対して、bytRecvData(lngTotalRead) を渡す方法は使えません。
(配列の個々の要素のポインタが連続していることは保証されていません)

オフセット指定での受信が必要な場合は、バッファ指定を As IntPtr に
変更して、ピン止めしたアドレスを渡すようにします。

' 配列をピン止めしてGCの対象から外す
Dim gch As GCHandle = GCHandle.Alloc(bytRecvData, GCHandleType.Pinned)
' 配列の先頭のアドレスを取得
Dim ptr As IntPtr = gch.AddrOfPinnedObject()
'
' lngTotalRead の分だけオフセット指定したい場合
Dim pBuffer As IntPtr = IntPtr.Add(ptr, lngTotalRead)
'
' === 受信処理 ==
'
'ピン止め解除
gch.Free()


あるいは、Byte 配列を使うのではなく、Marshal クラスの
AllocCoTaskMem / ReAllocCoTaskMem / FreeCoTaskMem で確保した領域に
受信するようにして、Marshal.Copy メソッドでマネージ配列に書き戻すとか。


--- 以下蛇足 ---

> lngArraySize = WI_INITBUFSIZE - 1
ここでは WI_INITBUFSIZE という定数を使っているのに
> If (lngTotalRead + 1024) > lngArraySize Then
> intRet = InternetReadFile(lngUrlHandle, bytRecvData(lngTotalRead), 1024, lngReadSize)
などでは、「1024」というマジックナンバーが使われていますね。記述を揃えるべきかと。


> lngArraySize = lngArraySize + 32767
> ReDim Preserve bytRecvData(lngArraySize)
Preserve による配列サイズの変更は、
 「別のメモリ領域を新たに確保」→「元の配列から転写」→「古い配列側のメモリを解放」
という 3 段階の処理を経て行われるので、データが大きくなるにつれ
作業効率が加速度的に悪化していきます。

「受信バッファ」と「最終出力結果」の変数を分離しては如何でしょう。
たとえば、バッファ自体は 1024 バイト単位での読み取りで固定しておき、
読み取った結果を MemoryStream に Write していくようにします。

これなら Preserve での増減を行う必要がなくなります。
処理結果を bytRecvData に書き戻す処理は、
MemoryStream の ToArray メソッドを使えば OK です。



> If (lngReadSize = 0) Or (intRet = 0) Then Exit Do
Or ではなく、OrElse を使うよう習慣付けましょう。


> If lngTotalRead Then '実際にデータが読み込まれた場合
上記は、暗黙の型変換により、If CBool(lngTotalRead) Then 相当の意味になりますね。

If 条件に指定できる式は、Boolean もしくは Boolean? のみです。
VB.NET 化に向けて、「Option Strict On」で動作するようコードを見直してみてください。

If 条件程度であれば、さほど大きな問題にはなりませんが、
API 呼び出しで暗黙変換が起きてしまうのは、あまり望ましく無いでしょう。


> Else
> Erase bytRecvData '総受信サイズが 0 の場合、配列を消去
> End If
Erase だと Nothing になってしまうので、
 bytRecvData = New Byte() {}
もしくは
 ReDim bytRecvData(0 To -1)
の方が、利用側にとっては扱いやすいと思います。


そのほか、移植作業に伴い、接頭辞 lng で As Integer なのや
接頭辞 int で As Short なのも見直しておいた方が良いかと思います。
ハンガリアン記法を使うなら、システムハンガリアンではなく
アプリケーションハンガリアンを推奨しておきます。

余裕があれば、API 宣言も 〜A 系から 〜W 系に直しておきましょう。
引用返信 編集キー/
■84854 / inTopicNo.3)  Re[2]: AccessViolationExceptionについて
□投稿者/ makoto (38回)-(2017/08/09(Wed) 14:41:29)
魔界の仮面弁士様

回答ありがとうございます。
System.Net.WebClient.OpenReadを使用する方法と、現行のプログラムの修正
の2つを試してみようと思います。


>アンマネージな API に頼らず、System.Net.WebClient.OpenRead あるいは DownloadFile 等を
>使うわけにはいかないのでしょうか。
現行のプログラムをそのまま使用せず新しい方法も視野に入れてみます。

>各種ハンドルを As Integer で扱わないようにしましょう。
>API 宣言も含め、As IntPtr で統一するべきです。(あるいは HandleRef とか)
今回使用している以外のAPIにもIntegerで宣言している箇所があるので、
必要なものはIntPtrに修正致します。

>肝心の API 宣言部(Declare もしくは DllImport 部)が無いので、以下は予想になりますが:
以下のように宣言しています。
Public Declare Function InternetReadFile Lib "wininet.dll" (ByVal hFile As Integer, _
ByRef sBuffer As String, _
ByVal lNumBytesToRead As Integer, _
ByRef lNumberOfBytesRead As Integer) As Short


>ByRef As Byte に対して、bytRecvData(lngTotalRead) を渡す方法は使えません。
>(配列の個々の要素のポインタが連続していることは保証されていません)
>オフセット指定での受信が必要な場合は、バッファ指定を As IntPtr に
>変更して、ピン止めしたアドレスを渡すようにします。
>' 配列をピン止めしてGCの対象から外す
>Dim gch As GCHandle = GCHandle.Alloc(bytRecvData, GCHandleType.Pinned)
>' 配列の先頭のアドレスを取得
>Dim ptr As IntPtr = gch.AddrOfPinnedObject()
>' lngTotalRead の分だけオフセット指定したい場合
>Dim pBuffer As IntPtr = IntPtr.Add(ptr, lngTotalRead)
>'
>' === 受信処理 ==
>'
>'ピン止め解除
>gch.Free()
>あるいは、Byte 配列を使うのではなく、Marshal クラスの
>AllocCoTaskMem / ReAllocCoTaskMem / FreeCoTaskMem で確保した領域に
>受信するようにして、Marshal.Copy メソッドでマネージ配列に書き戻すとか。

上記の処理を参考にプログラムを修正しようと思います。


>などでは、「1024」というマジックナンバーが使われていますね。記述を揃えるべきかと。
説明用に1024と記載しました。
実際は以下のように宣言しています。
Private Const WI_READ_READSIZE As Short = 1024

>> lngArraySize = lngArraySize + 32767
>> ReDim Preserve bytRecvData(lngArraySize)
>Preserve による配列サイズの変更は、
> 「別のメモリ領域を新たに確保」→「元の配列から転写」→「古い配列側のメモリを解放」
>という 3 段階の処理を経て行われるので、データが大きくなるにつれ
>作業効率が加速度的に悪化していきます。
>「受信バッファ」と「最終出力結果」の変数を分離しては如何でしょう。
>たとえば、バッファ自体は 1024 バイト単位での読み取りで固定しておき、
>読み取った結果を MemoryStream に Write していくようにします。
>これなら Preserve での増減を行う必要がなくなります。
>処理結果を bytRecvData に書き戻す処理は、
>MemoryStream の ToArray メソッドを使えば OK です。
パフォーマンスに対しての考慮を全くしていなかったので、
現在の問題点が解決してからプログラムの修正を
行おうとおもいます。

>> If (lngReadSize = 0) Or (intRet = 0) Then Exit Do
>>Or ではなく、OrElse を使うよう習慣付けましょう。
>> If lngTotalRead Then '実際にデータが読み込まれた場合
>上記は、暗黙の型変換により、If CBool(lngTotalRead) Then 相当の意味になりますね。
>I>f 条件に指定できる式は、Boolean もしくは Boolean? のみです。
>VB.NET 化に向けて、「Option Strict On」で動作するようコードを見直してみてください。
>If 条件程度であれば、さほど大きな問題にはなりませんが、
>API 呼び出しで暗黙変換が起きてしまうのは、あまり望ましく無いでしょう。
現在、「Option Strict Off」になっているので、Onに変更します。



>そのほか、移植作業に伴い、接頭辞 lng で As Integer なのや
>接頭辞 int で As Short なのも見直しておいた方が良いかと思います。
>ハンガリアン記法を使うなら、システムハンガリアンではなく
>アプリケーションハンガリアンを推奨しておきます。
>余裕があれば、API 宣言も 〜A 系から 〜W 系に直しておきましょう。

変数は移植前のものをそのまま使用しているので、リネームを
するようにします。

引用返信 編集キー/
■84863 / inTopicNo.4)  Re[3]: AccessViolationExceptionについて
□投稿者/ ねこまっしぐら (6回)-(2017/08/09(Wed) 20:49:57)
横からですが

> Public Declare Function InternetReadFile Lib "wininet.dll" (ByVal hFile As Integer, _
ByRef sBuffer As String, _
ByVal lNumBytesToRead As Integer, _
ByRef lNumberOfBytesRead As Integer) As Short

私も以前ReadFileのAccessViolationで悩んだのですが
その時は3番目をByRefにしたら動いたのですが…
(違ったらごめんなさい)
ちなみにこちらでは(ByVal Int,ByVal Byte(),ByVal Int,ByRef Int,ByVal Int)です
引用返信 編集キー/
■84868 / inTopicNo.5)  Re[3]: AccessViolationExceptionについて
□投稿者/ 魔界の仮面弁士 (1382回)-(2017/08/10(Thu) 11:44:28)
2017/08/10(Thu) 11:49:35 編集(投稿者)

No84854 (makoto さん) に返信
> 以下のように宣言しています。
> Public Declare Function InternetReadFile Lib "wininet.dll" (ByVal hFile As Integer, _
> ByRef sBuffer As String, _
> ByVal lNumBytesToRead As Integer, _
> ByRef lNumberOfBytesRead As Integer) As Short

「ByRef sBuffer As String」に「bytRecvData(lngTotalRead)」を渡していたことに
なりますが、bytRecvData(lngTotalRead) は String ではありませんよね?

おそらくは「bytRecvData As Byte()」か「bytRecvData() As Byte」ですよね。
Option Strict On を使うことで、このようなデータ型の指定ミスを防げます。



Win32 API としての戻り値は BOOL 型です。これは 4 バイトの型です。
VB6 では As Long ですが、VB.NET では「As Boolean」か「As Integer」が妥当です。
より丁寧に書くなら、ちょっと冗長ですが「As <MarshalAs(UnmanagedType.Bool)> Boolean」とします。


第一引数は _In_ HINTERNET とのことなので、ここは ByVal hFile As IntPtr にします。
提示頂いたコードは As Integer でしたが、その場合 x64 では使えません(x86ビルドなら動きますが)。
ちなみに VBA6 なら ByVal hFile As Long、VBA7 なら ByVal hFile As LongPtr です。


第二引数は _Out_ LPVOID なので、
 Byte配列全体を渡すなら <Out()> ByVal hFile As Byte()
 Byte配列のオフセットで渡すなら ByVal hFile As IntPtr
が使えます。あるいは、その両方の型をオーバーロードしておいても良いでしょう。


第三引数は _In_ DWORD なので、ByVal dwNumberOfBytesToRead As UInteger です。
ただ、2GB を超えることはそうそうないと思うので、元のコードのように
ByVal Integer にしておいても問題にはならないでしょう。


第四引数は _Out_ LPDWORD なので、<Out()> ByRef lpdwNumberOfBytesRead As UInteger です。
こちらも先と同様の理由で、元のコードの ByRef Integer でも代用できます。





No84863 (ねこまっしぐら さん) に返信
> ちなみにこちらでは(ByVal Int,ByVal Byte(),ByVal Int,ByRef Int,ByVal Int)です

……? 本当に 5 つの引数を指定していたのでしょうか?

「Int」が Int16/Int32/Int64 の何を指しているのも曖昧ですが。

-- 追記 --
あぁ、なるほど。
InternetReadFile ではなく ReadFile の事例ということですね。
その場合、5 つの引数の型はこんな感じになると思います。
 ByVal IntPtr
 ByVal Byte() / ByVal IntPtr
 ByVal UInteger
 ByRef UInteger
 ByRef OVERLAPPED構造体/ByVal OVERLAPPEDクラス/ByVal IntPtr
引用返信 編集キー/
■84872 / inTopicNo.6)  Re[1]: AccessViolationExceptionについて
□投稿者/ makoto (39回)-(2017/08/10(Thu) 19:36:24)
魔界の仮面弁士様、ねこまっしぐら様

返信が遅れてしまい申し訳ございません。
回答ありがとうございます。

魔界の仮面弁士様の回答にあった
System.Net.WebClient.OpenReadを使用することで
機能を実現することが出来ました。
(コードレビューがあるのでこの方法が良いかはまだわからないですが...)

上記方法でレビューが通らなった場合は、皆様にご指摘をいただいた
内容でInternetReadFileを使用した方法で修正しようと思います。

この度は、たくさんの回答をいただき誠にありがとうございました。



解決済み
引用返信 編集キー/

このトピックをツリーで一括表示


トピック内ページ移動 / << 0 >>

このトピックに書きこむ