BSTR キャッシュの動作確認

BSTR とは?」 という記事で COM で使われる基本的な文字列型である、 BSTR について説明しました。この資料ではこの BSTR の解放タイミングに関わってくる BSTR キャッシュ について説明と動作確認を行います。

テストプログラムは 「BSTR とは?」と全く同一のものをと使いますので、 実際に手元で動作確認をする方は、そちらを先にお読みください。

BSTR キャッシュとは?

基本的にプログラミング上は SysAllocString で割り当てた BSTR は、 SysFreeString で解放すれば良いです。

しかしながら、特にメモリに関するデバッグをする際に知っておかないと判断を誤る項目がありますので説明します。 それは、BSTR キャッシュ と呼ばれる仕組みです。

BSTR は非常に基本的なデータ型であるために、頻繁に割り当て要求、解放要求が行われます。 Windows の COM システムでは、メモリ割り当てに関するパフォーマンスを最適化するために文字列を直ちに解放せずにキャッシュしておき、 それを使いまわすことが行われます。

これを知らないと、BSTR に関わるメモリがリークしていると勘違いしてしまう場合がありますので、 システムをデバッグするときなどは注意が必要です。

BSTR キャッシュにより SysFreeString でメモリが解放されない様子を実際にみてみよう

では実際に、SysFreeString を呼んだ直後にメモリが解放されるかどうか先の続きとしてデバッグを続けてみましょう。 予想としては前述の BSTR キャッシュによって、解放されないはずですよね。

まずは現状、メモリが割り当てられているはずですからそのヒープエントリを確認しておきましょう。

0:000> !heap
NtGlobalFlag enables following debugging aids for new heaps:    tail checking
    free checking
    validate parameters
Index   Address  Name      Debugging options enabled
  1:   00250000                 tail checking free checking validate parameters
  2:   00010000                 tail checking free checking validate parameters
  3:   00020000                 tail checking free checking validate parameters
  4:   00220000                 tail checking free checking validate parameters
  5:   00390000                 tail checking free checking validate parameters

0:000> !heap -a 00250000
Index   Address  Name      Debugging options enabled
  1:   00250000
    Segment at 00250000 to 00350000 (0000a000 bytes committed)
    Flags:                40000062
    ForceFlags:           40000060
    Granularity:          8 bytes
...
        00258388: 000e8 . 00028 [107] - busy (10), tail fill
        002583b0: 00028 . 00028 [107] - busy (10), tail fill
        002583d8: 00028 . 01c08 [104] free fill
        00259fe0: 01c08 . 00020 [111] - busy (1d)
        0025a000:      000f6000      - uncommitted bytes.

先のデバッグ結果より "Hello" という文字のアドレスは 0x002583bc でしたから、 0x002583b0 にある HEAP_ENTRY が今回の BSTR を含んでいると思われます。

dc コマンドで確認してみましょう。

0:000> dc 002583b0
002583b0  6d7424bf 1800d4da 0000000a 00650048  .$tm........H.e.
002583c0  006c006c 0000006f abababab abababab  l.l.o...........
002583d0  00000000 00000000 e977273b 0000d4da  ........;'w.....
002583e0  002500c4 00253fd8 feeefeee feeefeee  ..%..?%.........
002583f0  feeefeee feeefeee feeefeee feeefeee  ................
00258400  feeefeee feeefeee feeefeee feeefeee  ................
00258410  feeefeee feeefeee feeefeee feeefeee  ................
00258420  feeefeee feeefeee feeefeee feeefeee  ................

確かに "Hello" という文字列が確認できます。

そこで、SysFreeString の呼び出しが返ってきた所でプログラムをブレークします。 このために、SysFreeString にブレークポイントを設定し、そのブレークポイントがヒットしたところで、 SysFreeString のリターンアドレスまでプログラムを実行します。

リターンアドレスは @$ra に入ります。Psuedo レジスタが使えると 64 bit 環境などでも同様のデバッグ手順が踏めるので便利です。

0:000> bp OLEAUT32!SysFreeString
0:000> g
Breakpoint 0 hit
eax=002583bc ebx=7ffdf000 ecx=00000002 edx=00000002 esi=00000000 edi=00000000
eip=76d83e59 esp=0012ff34 ebp=0012ff40 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
OLEAUT32!SysFreeString:
76d83e59 8bff            mov     edi,edi
0:000> kbn
 # ChildEBP RetAddr  Args to Child
00 0012ff30 00401032 002583bc 002583bc 0012ff88 OLEAUT32!SysFreeString
01 0012ff40 0040131d 00000001 00391d40 00391d88 bstr1!main+0x32
02 0012ff88 76b01194 7ffdf000 0012ffd4 76e6b3f5 bstr1!__tmainCRTStartup+0xfb
03 0012ff94 76e6b3f5 7ffdf000 70c863db 00000000 kernel32!BaseThreadInitThunk+0xe
04 0012ffd4 76e6b3c8 00401374 7ffdf000 00000000 ntdll!__RtlUserThreadStart+0x70
05 0012ffec 00000000 00401374 7ffdf000 00000000 ntdll!_RtlUserThreadStart+0x1b
0:000> g @$ra
eax=002583b8 ebx=7ffdf000 ecx=002582b0 edx=002583b8 esi=00000000 edi=00000000
eip=00401032 esp=0012ff3c ebp=0012ff40 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
bstr1!main+0x32:
00401032 eb04            jmp     bstr1!main+0x38 (00401038)

さて、SysFreeString の実行が終わったのでもう一度ヒープのエントリを確認しましょう。

0:000> !heap -a 00250000
Index   Address  Name      Debugging options enabled
  1:   00250000
    Segment at 00250000 to 00350000 (0000a000 bytes committed)
    Flags:                40000062
    ForceFlags:           40000060
...
        002582a0: 00458 . 000e8 [107] - busy (d0), tail fill
        00258388: 000e8 . 00028 [107] - busy (10), tail fill
        002583b0: 00028 . 00028 [107] - busy (10), tail fill
        002583d8: 00028 . 01c08 [104] free fill
        00259fe0: 01c08 . 00020 [111] - busy (1d)
        0025a000:      000f6000      - uncommitted bytes.
0:000>

確かに 002583b0 にあるヒープエントリは busy のままです。解放されていません。
これは BSTR キャッシュによる影響と思われます。

OANOCACHE で BSTR キャッシュは無効になるか?

そこで BSTR キャッシュを無効にするための環境変数 OANOCACHE を 1 に設定した上で、 上記と全く同様の実験を行います。


C:\Debuggers>SET OANOCACHE=1

コマンドプロンプトで上のように環境変数を設定します。 この環境でテスト用のプログラムを実行します。

C:\Debuggers>cdb C:\src\test\bstr1.exe

Microsoft (R) Windows Debugger Version 6.11.0001.404 X86
Copyright (c) Microsoft Corporation. All rights reserved.

...
ntdll!LdrpDoDebuggerBreak+0x2c:
76eae60e cc              int     3
0:000> g
ModLoad: 75890000 758af000   C:\Windows\system32\IMM32.DLL
ModLoad: 76830000 768fc000   C:\Windows\system32\MSCTF.dll
(f30.4e0): Break instruction exception - code 80000003 (first chance)
eax=001883fc ebx=7ffd3000 ecx=00000002 edx=00000002 esi=00000000 edi=00000000
eip=00401027 esp=0012ff3c ebp=0012ff40 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
bstr1!main+0x27:
00401027 cc              int     3
0:000> dv
           bstr = 0x001883fc "Hello"
0:000> !heap
NtGlobalFlag enables following debugging aids for new heaps:    tail checking
    free checking
    validate parameters
Index   Address  Name      Debugging options enabled
  1:   00180000                 tail checking free checking validate parameters
  2:   00010000                 tail checking free checking validate parameters
  3:   00020000                 tail checking free checking validate parameters
  4:   00610000                 tail checking free checking validate parameters
  5:   01380000                 tail checking free checking validate parameters
0:000> !heap -a 00180000
Index   Address  Name      Debugging options enabled
  1:   00180000
    Segment at 00180000 to 00280000 (0000a000 bytes committed)
    Flags:                40000062
...
        001883c8: 000e8 . 00028 [107] - busy (10), tail fill
        001883f0: 00028 . 00028 [107] - busy (10), tail fill
        00188418: 00028 . 01bc8 [104] free fill
        00189fe0: 01bc8 . 00020 [111] - busy (1d)
        0018a000:      000f6000      - uncommitted bytes.
0:000> dc 001883f0
001883f0  7f126fc3 1800c5d6 0000000a 00650048  .o..........H.e.
00188400  006c006c 0000006f abababab abababab  l.l.o...........
00188410  00000000 00000000 03116cbf 0000c5d6  .........l......
00188420  001800c4 00184018 feeefeee feeefeee  .....@..........
00188430  feeefeee feeefeee feeefeee feeefeee  ................
00188440  feeefeee feeefeee feeefeee feeefeee  ................
00188450  feeefeee feeefeee feeefeee feeefeee  ................
00188460  feeefeee feeefeee feeefeee feeefeee  ................

今回は 001883f0 のエントリに "Hello" が割り当てられています。

ちなみに、一番最初に作られるヒープはプロセスヒープといいます。(PEB の ProcessHeap フィールドにどのヒープがプロセスヒープか書いてあります) プロセスヒープの場所さえもランダムに変わっていあたりは、実はひそかに Windows Vista 以降のセキュリティ・エンハンスメントの結果だったりします。

では、SysFreeString の直後までプログラムを実行して、このヒープエントリの利用状況を確認しましょう。

0:000> bp oleaut32!SysFreeString
0:000> g
Breakpoint 0 hit
eax=001883fc ebx=7ffd3000 ecx=00000002 edx=00000002 esi=00000000 edi=00000000
eip=76d83e59 esp=0012ff34 ebp=0012ff40 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
OLEAUT32!SysFreeString:
76d83e59 8bff            mov     edi,edi
0:000> g @$ra
eax=00000001 ebx=7ffd3000 ecx=76e6316f edx=00180180 esi=00000000 edi=00000000
eip=00401032 esp=0012ff3c ebp=0012ff40 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
bstr1!main+0x32:
00401032 eb04            jmp     bstr1!main+0x38 (00401038)
0:000> !heap -a 00180000
Index   Address  Name      Debugging options enabled
  1:   00180000
    Segment at 00180000 to 00280000 (0000a000 bytes committed)
    Flags:                40000062
    ForceFlags:           40000060
...
        00187e88: 00038 . 00458 [107] - busy (440), tail fill
        001882e0: 00458 . 000e8 [107] - busy (d0), tail fill
        001883c8: 000e8 . 00028 [107] - busy (10), tail fill
        001883f0: 00028 . 01bf0 [104] free fill
        00189fe0: 01bf0 . 00020 [111] - busy (1d)
        0018a000:      000f6000      - uncommitted bytes.
0:000> dc 001883f0
001883f0  04116cb8 0000c5d6 001800c4 00184018  .l...........@..
00188400  feeefeee feeefeee feeefeee feeefeee  ................
00188410  feeefeee feeefeee feeefeee feeefeee  ................
00188420  feeefeee feeefeee feeefeee feeefeee  ................
00188430  feeefeee feeefeee feeefeee feeefeee  ................
00188440  feeefeee feeefeee feeefeee feeefeee  ................
00188450  feeefeee feeefeee feeefeee feeefeee  ................
00188460  feeefeee feeefeee feeefeee feeefeee  ................
0:000>

以上のように、今度は 001883f0 からは解放領域として認識されています。

OANOCACHE を設定したために、BSTR キャッシュが無効になり SysFreeString によってメモリが直ちに解放されたと考えられます。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Web/DB プログラミング徹底解説