DOBON.NET DOBON.NETプログラミング掲示板過去ログ

Marshal.AllocCoTaskMemの16バイト境界

環境/言語:[windows7、VB.net2008]
分類:[.NET]

初投稿ですが、他トピックにはいつもお世話になっています。

今回VB.Net2008にてCで作られたDLLを呼び出して使用するのですが、メモリの確保が必要となりました。
DLLの動作としては、アドレスとサイズ(と場所)を指定して呼び出したのちに、そのアドレスをMarshal.Copyでバッファにコピーすることで様々な情報を扱おうとするものです(Byteデータ)


DLL自体は動作し、問題なく使用できているのですが、特定の確保したアドレスを使用する箇所でMarshal.Copyを行うと高頻度で「保護されているメモリに読み取りまたは書き込み操作を行おうとしました。・・・」のエラーが発生します。

Copy自体は
Marshal.Copy(ptr, buf, 0, size)
のように書いてあり、他にも多数使用しています
(他のアドレスの箇所では発生しません)

しかもこの関数呼び出し直後は発生せず、bufの内容もちゃんとCopyされていますが、なぜかAltキー押下やプログラム終了時などに上記エラーが発生します。



そこでMarshal.AllocCoTaskMemかと思い見直していましたが、現状おかしな箇所が私には分からず投稿しました。
確保しているサイズ、コピー先のバッファサイズも同じ値でした


DLLの仕様上16バイトの区切りでなければならなく、以下のようにソースを書きました。

Public Shared Sub ByteAligen(ByVal size As Integer, ByRef retPtr As IntPtr)
Dim ptr As IntPtr
Dim pBufferAlign As UInt32

ptr = Marshal.AllocCoTaskMem(size + &H8) ' allocate memory(require 8 byte align)

If (CUInt(ptr) And &H8) Then
pBufferAlign = CUInt(ptr) + 8
Else
pBufferAlign = CUInt(ptr)
End If

retPtr = CType(pBufferAlign, IntPtr)
End Sub



該当のMashal.Copyをコメントにすれば発生しないため、エラー内容からしても確保したメモリ絡みだとは思うのですが、なにぶん解決には至っておりません。
分かる方がいらっしゃいましたらよろしくお願いいたします。
■No30792に返信(Aruさんの記事)

確保した領域の解放を行っていないのではないでしょうか?
確保したメモリの内容をDLL実行後マネージメモリ内にコピーして
確保した領域は解放するようにした方がよいと思います。

または領域確保にAllocCoTaskMemではなくAllocHGlobalを使用した方がいいとか?
考えられる可能性。

・Marshal.Copy で確保した以上のメモリ領域に書き込みしている。
・Marshal.FreeCoTaskMem に渡すポインタを間違えている。
・x64 環境で x64 プロセスとして実行しているので UInt32 型ではポインタを表す型として足りていないのでアドレスが変になって書き込んでいる。

そもそも、AllocCoTaskMem で取得したポインタをずらしてどこかに消してしまったら、安全に解放できない気がしますが…。
> DLLの仕様上16バイトの区切りでなければならなく、以下のようにソースを書きました。
これもしっかり実装されているか確認してみて下さい。
多分16バイト区切りなら
((Size + 15) \ 16) * 16 の大きさを確保しないといけない気がします。
ご返答ありがとうございます。
確保領域がギリギリなのが原因なのか?と思いかなり多めに指定したところ、とりあえず落ちなくはなりましたが、未だ納得はできません。
関数の仕様上、ギリギリではダメだと書いてあるなら納得もするのですが・・・



shuさん
繰り返しその領域を使用するのでプログラムの最初に確保し、終了時に解放することを考えています。
色々調べた結果、現状IDisposeを使いFreeCoTaskMemしております。

>または領域確保にAllocCoTaskMemではなくAllocHGlobalを使用した方がいいとか?
これはどちらがいいのか分かりませんが、AllocHGlobalも検討してみようと思います



Azuleanさん
>・Marshal.Copy で確保した以上のメモリ領域に書き込みしている。
これが一番原因としてあると思い、確保領域は何度も見直しましたが机上では問題なさそうです。
しかし多めに確保したら例外が出ないことから、何か見落としていたのかと思っております。
Cの文字列のように終端文字みたいな、余計なものがあるのでしょうか・・・

>・Marshal.FreeCoTaskMem に渡すポインタを間違えている。
申し訳有りません、整理して記載したので書いてありませんでしたが解放用にアドレスを別途引数でByRefしています。
ずらしていたらずらす前のアドレスを、ずらしていなければそのアドレスを別に保存しておき、それを使用してFreeしています。
解放絡みの話でもあるので、省略すべきではありませんでした。
一応、解放自体は出来ている・・・と思います。


>・x64 環境で x64 プロセスとして実行しているので UInt32 型ではポインタを表す型として足りていないのでアドレスが変になって書き込んでいる
足りていないというのはオーバーフローのような、サイズ不足などでしょうか?
勉強不足で申し訳有りません。




もうしばらく検討、動作確認をし、例外が発生しないようであれば納得はいきませんが一旦解決とさせていただこうと思います。
調べていたらC++?のほうにはaligned_mallocという境界指定して確保するものがあるみたいですね・・・これが使えればもっと楽なんでしょうか・・・?
■No30796に返信(shuさんの記事)
やはり単純にサイズ絡みでしょうか・・・
ありがとうございます、再度領域サイズを確認、検討してみます
■No30797に返信(Aruさんの記事)
> >または領域確保にAllocCoTaskMemではなくAllocHGlobalを使用した方がいいとか?
> これはどちらがいいのか分かりませんが、AllocHGlobalも検討してみようと思います

私見に過ぎませんが、大きな差は出ないと予想されます。

> >・Marshal.Copy で確保した以上のメモリ領域に書き込みしている。
> これが一番原因としてあると思い、確保領域は何度も見直しましたが机上では問題なさそうです。
> しかし多めに確保したら例外が出ないことから、何か見落としていたのかと思っております。
> Cの文字列のように終端文字みたいな、余計なものがあるのでしょうか・・・

これはその DLL が必要とするメモリ量が第三者には見えませんので、Aru さんが正しさを検証するしかありません。
ただ、私も大丈夫なはず、抜け漏れはないはずと思っていても、考慮漏れや不具合を残すことをしばしば経験しているので、一度他の人にも机上計算を検証してもらってはいかがでしょうか。たとえば、一緒に仕事をされている方など。


> 一応、解放自体は出来ている・・・と思います。

了解です。


> 足りていないというのはオーバーフローのような、サイズ不足などでしょうか?
> 勉強不足で申し訳有りません。

64bit 環境で 64bit プロセスで実行するとポインターが 8 バイトになります。
UInt32 は 4 バイトなのでポインターを表現するには足りないと言うことになります。
(64bit 環境ではない、あるいは 32bit プロセスとして実行しているのであれば無関係ですが、移植性は低いことになります)
見返していて気づいた点を。。。

■No30796に返信(shuさんの記事)
>>DLLの仕様上16バイトの区切りでなければならなく、以下のようにソースを書きました。
> これもしっかり実装されているか確認してみて下さい。
> 多分16バイト区切りなら
> ((Size + 15) \ 16) * 16 の大きさを確保しないといけない気がします。

Aru さん、こちらは確認されていますか?
元質問のコードは 8 バイト区切りに見えます。
(そもそも、8 足すだけでは 8 バイトの境界への調整ができていませんが…。割ったあまりを元に調整が必要なはず?)
他部分作りこみもあって確認が遅れて申し訳有りません。

Azuleanさん
ご指摘点等確認しました。
サイズ等は実質一人で作業しているのに近いため、お願いするのが難しい現状です・・・
自分なりに、一度間をおいて再検証してみたいと思います。

> ((Size + 15) \ 16) * 16 の大きさを
過去に指摘されておきながら今だ検証していなく申し訳ありませんが、512Byteの場合527Byte分をAllocHGlobalする必要があるということでしょうか?
その後、戻り値のアドレスを必要に応じて調整するのでしょうか・・・


私自身、アライメント境界に関する勉強不足があると思い、再度調べなおしております。
8バイト区切りでは?との指摘もあり、以下のように再度書き直してみました。
上記のサイズのこともあり、まだ検討する要素は多そうです。

Cでしたが、以下のサイトを参考にさせていただきました。
http://pieceofnostalgy.blogspot.jp/2011/12/net-framework.html


また、前回のコードには省いてしまいましたが、解放用にアドレスを格納するretPtr2というものがあります。これを用いてFreeしています。


Public Shared Sub ByteAligen(ByVal size As Integer, ByRef retPtr As IntPtr, ByRef retPtr2 As IntPtr)
Dim ptr As IntPtr
Dim pBufferAlign As UInt64
Dim offset As Integer
Dim Align As Integer = 16

ptr = Marshal.AllocHGlobal(size + Align - 1) ' allocate memory
pBufferAlign = CULng(ptr)

offset = Align - CInt(pBufferAlign Mod Convert.ToInt64(Align))
If offset = Align Then
offset = 0
End If

retPtr2 = ptr
retPtr = CType(pBufferAlign + offset, IntPtr)

End Sub


十分な検証時間が無く、幸いにも(?)土日が休みですので、更なる検証は月曜になってしまいますが、動作中は問題なく動いているようです。
(ALTキーで落ちたりはしていません)

ただ、最終的なFreeのところで落ちることがありました。
Handle is invalidということで、無効なハンドルらしいのですが、AllocHGlobalの戻り値(retPtr2)を渡しているつもりなのですが・・・
書きもれてました

>(64bit 環境ではない、あるいは 32bit プロセスとして実行しているのであれば無関係ですが、移植性は低いことになります)
現状は、windows7 32bitOSでの動作を行っております。
今回UINT64にて書き直しましたが、32bitであり、サイズ的にも問題なく収まるということでしょうか
2012/08/04(Sat) 00:30:52 編集(投稿者)

「16 バイト境界でなければならない」というのは、以下のどちらですか?

(1)DLL に渡すバッファの先頭アドレスが 16 の倍数でなければならない。
(2)DLL に渡すバッファのサイズが 16 の倍数でなければならない。

(1)であれば 16 バイト余分に確保し、確保されたアドレスから次の 16 の倍数になるように (16 - あまり) を足してください。

(2)であれば必要なバッファサイズより大きい 16 の倍数になるように調整してください。

このどちらであるかは、あなたにしかわかりません。
DMAバウンダリーエラーですよね?・・・

それと、AllocCoTaskMem は、今回適していないと思いますが。
AllocHGlobal の方でしょう。

16バイト境界と言うことは、確保したメモリの先頭アドレスは
yyyy:xxx0(イメージ)と言う表現とならないといけないという
ことになります。(C言語で書かれているという部分からの推測)

C言語DLLに渡すアドレスは、確保したメモリアドレスを渡す
のではなく、16バイト境界に調整したアドレスを渡すことになり
ます。が、返ってきた時点では、確保した際のアドレスで解放す
ることになります。

その辺が間違っていなければ、残る問題は、確保したサイズ以上
の領域に書き込みが行われて保護違反している・・・と言うこと
にしかなりません。

どの程度のメモリ確保なんですか?

※ .NET とC言語の橋渡しに、C++ CLIでラッパークラス作って
  アンマネージとマネージの橋渡しを作った方が、簡単と言う
  か、安全。
  最悪、バイト配列のコピーで代用できるし・・・
  (サイズがでかいと遅いけど・・・)

以上。参考まで
ご返信ありがとうございます。

Azulean さん
>(1)DLL に渡すバッファの先頭アドレスが 16 の倍数でなければならない。
>(2)DLL に渡すバッファのサイズが 16 の倍数でなければならない。
ちょっと自分でもごちゃ混ぜになっていましたが、DLLの仕様を再確認したところ、両方のようです。
16の倍数のサイズが確保されており、かつアドレスも16の倍数でなければならない…ということです。
サイズは、基本的に16、256、512などの指定を行っていますのでやはり問題はアドレスのほうでしょうか


オショウ さん
>C言語DLLに渡すアドレスは、確保したメモリアドレスを渡す
>のではなく、16バイト境界に調整したアドレスを渡すことになり
>ます。が、返ってきた時点では、確保した際のアドレスで解放す
>ることになります。
そう思い、関数では確保した直後のアドレスも返すようにし、それで解放しているのですが解放時にエラーになることがあるようです。
ハンドルが無効です、と・・・既に解放されてるとか?そのような処理はないのですが・・・


>※ .NET とC言語の橋渡しに、C++ CLIでラッパークラス作って
>  アンマネージとマネージの橋渡しを作った方が、簡単と言う
>  か、安全。
なるほど、自分はC++ CLIは作成したことがありませんが、この機会にやってみるのもいいかもしれません。
何よりあまりこの問題でハマっているわけにもいかず・・・
作成したC++ CLIの成果物(全くの初心者なので少し調べたところDLLでしょうか)に対しサイズを渡し、IntPtrを戻して使用する感じでしょうか
(この場合も確保用と解放用にアドレスを二つ持つ必要がありそうですが)
発生したエラーから様々な点を再検証した結果、直接的な引き金はやはりMarshal.Copyのサイズオーバーでした。
確保したサイズ以上のバッファ領域をコピーしようとしたためです。
自分との想定外の場所で起こっていたためなかなか原因が特定できず寄り道していましたが、皆さんの助言もあって解決したように思います。
最も想定しやすい原因だっただけに発見が遅れたのが悔やまれますが、サイズ確保に関して色々な助言がありコードも修正したので無駄ではなかったように思います

結局、最終的にこちらに載せたVB.net上のコードでも確保、解放は問題なさそうですが、オショウさんの助言もありC++CLIでの運用を行っています。
(それもあり結局VB.netのほうは繰り返し確認は行っていません)
現状のソースは以下です。


VB.netの場合
ref2を用いてFreeしないとエラーとなる

Public Shared Sub ByteAligen(ByVal size As Integer, ByRef retPtr As IntPtr, ByRef retPtr2 As IntPtr)
Dim ptr As IntPtr
Dim pBufferAlign As UInt64
Dim offset As Integer
Dim Align As Integer = 16

ptr = Marshal.AllocHGlobal(size + Align - 1) ' allocate memory
pBufferAlign = CULng(ptr)
offset = Align - CInt(pBufferAlign Mod Convert.ToInt64(Align))
If offset = Align Then
offset = 0
End If

retPtr2 = ptr
retPtr = CType(pBufferAlign + offset, IntPtr)
End Sub


C++CLIの場合
.netFreamwork4ならばIntPtrにAddメソッドがあるのでそちらのほうがよいと思います
上ではINT64でしたが、環境上32bitなのでINT32でも良いかと思い32を使っています
解放は、Staticに持ったptrFreeAddrをAllocMemoryコール後に別関数にてReturn
それを用いて解放
(いまいちスマートじゃない気がしましたが、とりあえずということで・・・)

IntPtr AllocMemory(int size)
{
IntPtr p;
Int32 BufferAlign;
int Align;
int offset;

Align = 16;

p = System::Runtime::InteropServices::Marshal::AllocHGlobal(size + Align - 1);
ptrFreeAddr = p;

BufferAlign = ptrFreeAddr.ToInt32();

offset = Align - (int)(BufferAlign % (UInt32)Align);
if (offset == Align){
offset = 0;
}

IntPtr retAddr = IntPtr(p.ToInt32() + offset);
return retAddr;
}

↓解放用Addr取得
IntPtr RetFreeAddress()
{
return ptrFreeAddr;
}


もうしばらく様子を見てみることになりますが、今までの経験上、起きていたタイミングでの例外も全く見えなくなったので問題ないかと思い、これにて解決とさせていただきます。
ありがとうございました。
解決済み!
■No30817に返信(Aruさんの記事)

提示されたコードで気になるとこがあるのですが
>     Public Shared Sub ByteAligen(ByVal size As Integer, ByRef retPtr As 
ここのsizeにわたされる値はAlign = 16 の倍数になっているのでしょうか?
なっていない場合があるとするとわたされたsizeを16の倍数に切上げてAlign-1を足さないとはみ出てしまうきがします。

例)Align=4, Size=5
□または■が確保領域、■または×がDLLで書かれるであろう領域
012301230123
確保領域のOffset=0
■■■■■■■■             '--- Size + Align - 1の場合
■■■■■■■■             '--- (Sizeを4の倍数に切上げ) + Align - 1の場合
確保領域のOffset=1
 □□□■■■■■×××     '--- Size + Align - 1の場合
 □□□■■■■■■■■     '--- (Sizeを4の倍数に切上げ) + Align - 1の場合
確保領域のOffset=2
  □□■■■■■■××     '--- Size + Align - 1の場合
  □□■■■■■■■■□   '--- (Sizeを4の倍数に切上げ) + Align - 1の場合
確保領域のOffset=3
   □■■■■■■■×     '--- Size + Align - 1の場合
   □■■■■■■■■□□ '--- (Sizeを4の倍数に切上げ) + Align - 1の場合
解決済み!

DOBON.NET | プログラミング道 | プログラミング掲示板