ヒープブロックの二重解放
ヒープブロックの二重解放は、ヒープから割り当てた同じメモリ領域を二回以上解放しようとすることによって発生します。これにより、ヒープの状態が破損し、アプリケーションがクラッシュする原因となります。この問題は以下に挙げるような点で特定が難しい場合があります。
- 二重解放を行っても、ヒープの構造に問題が生じるだけでアプリケーションはクラッシュしないことがあります。この場合、次にヒープの操作を行ったときにクラッシュすることとなり、原因となったヒープの二重解放を行った箇所が困難です。
- ヒープの破損やアクセス違反として報告されますので、二重解放が原因であることを特定するのが難しい場合があります。
- 例外が発生するのは二回目の解放を行おうとしたときですので、一回目の解放が行われた箇所を特定するのが難しい場合があります。
本記事では、ヒープの二重解放によるアプリケーションのクラッシュの調査方法について解説します。具体的には、WinDbgを使用してヒープの二重解放を検出し、問題の箇所を特定する手順を紹介します。
ヒープの二重解放の例
以下のコードは、ヒープから割り当てたメモリを二回解放しようとする例です。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#pragma optimize( "", off )
void do_bad_thing(char *buffer) {
printf("%s\n", buffer);
free(buffer);
}
int main() {
char *buffer = (char*)malloc(100);
strcpy(buffer, "Hello, World!");
do_bad_thing(buffer);
free(buffer);
printf("End of program.\n");
}
このコードでは、malloc
関数でヒープからメモリを割り当て、do_bad_thing
関数内でそのメモリを使用した後にfree
関数で解放しています。しかし、main
関数でも再度同じメモリをfree
関数で解放しようとしています。これにより、ヒープの二重解放が発生します。
Visual Studio 2022 version 17.14.7でx64|Releaseビルドを実行すると、最後のprintf
関数が実行される前にクラッシュし、イベントログにApplication ErrorのソースでイベントID1000のエラーが以下のメッセージで記録されます。
障害が発生しているアプリケーション名: ConsoleApplication1.exe、バージョン: 0.0.0.0、タイム スタンプ: 0x68764432
障害が発生したモジュール名: ntdll.dll、 バージョン: 10.0.26100.4652、タイム スタンプ: 0x6c6bd922
例外コード: 0xc0000374
フォールト オフセット: 0x000000000011dc15
フォールト プロセス ID: 0x2C10
アプリケーションのフォールトの開始時刻: 0x1DBF580F10A6B99
Faulting アプリケーション パス: C:\source\repos\ConsoleApplication1\x64\Release\ConsoleApplication1.exe
Faulting モジュール パス: C:\WINDOWS\SYSTEM32\ntdll.dll
Report Id: 3377c776-039f-40f8-bafe-8e80509ebc38
調査方法
1. クラッシュダンプの取得
まず、Windows Error Reporting (WER)を使用してクラッシュダンプファイルを取得しましょう。
以下は、WERの設定を行うためのレジストリファイルの例です。このファイルをwer.reg
として保存し、管理者権限でインポートすることでWERの設定を変更できます。
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps]
"DumpFolder"=hex(2):43,00,3a,00,5c,00,4c,00,6f,00,63,00,61,00,6c,00,44,00,75,\
00,6d,00,70,00,73,00,00,00
"DumpType"=dword:00000002
もちろん手動で設定を変更しても構いません。
レジストリを設定した後にアプリケーションを実行して問題を再現させると、以下のようにクラッシュダンプファイルが生成されます。
2. クラッシュダンプの解析
クラッシュダンプを解析するためには、Visual StudioやWinDbgなどのデバッガーを使用します。どちらを使用するかは好みですが、Visual StudioはGUIベースで直感的に操作できるためツールに習熟していなくても比較的簡単に解析できます。一方、WinDbgはコマンドベースで詳細情報の取得や解析作業の自動化が可能なため、経験豊富な開発者には強力なツールです。今回はWinDbgを使用して解析を行います。
まずWinDbgを起動し、クラッシュダンプファイルを開きます。k
コマンドを入力して、スタックトレースを表示すると、以下のような出力が得られます。
0:000> k
# Child-SP RetAddr Call Site
00 000000b6`d193dfa8 00007ffa`53828ffa ntdll!NtWaitForMultipleObjects+0x14
01 000000b6`d193dfb0 00007ffa`53828c63 ntdll!WerpWaitForCrashReporting+0x82
02 000000b6`d193e030 00007ffa`538286c8 ntdll!RtlReportExceptionHelper+0x4d3
03 000000b6`d193e1b0 00007ffa`538abd9f ntdll!RtlReportException+0x78
04 000000b6`d193e1e0 00007ffa`538667e3 ntdll!RtlReportFatalFailure$filt$0+0x33
05 000000b6`d193e210 00007ffa`538a650f ntdll!_C_specific_handler+0x93
06 000000b6`d193e280 00007ffa`537b4527 ntdll!RtlpExecuteHandlerForException+0xf
07 000000b6`d193e2b0 00007ffa`537b33b6 ntdll!RtlDispatchException+0x437
08 000000b6`d193ea00 00007ffa`5385dc15 ntdll!RtlRaiseException+0x206
09 000000b6`d193f870 00007ffa`537777f9 ntdll!RtlReportFatalFailure+0x9
0a 000000b6`d193f8c0 00007ffa`53758c02 ntdll!RtlReportCriticalFailure+0xa9
0b 000000b6`d193f9b0 00007ffa`538624ba ntdll!RtlpHeapHandleError+0x12
0c 000000b6`d193f9e0 00007ffa`537511eb ntdll!RtlpHpHeapHandleError+0x7a
0d 000000b6`d193fa10 00007ffa`537d59ea ntdll!RtlpLogHeapFailure+0x4b
0e 000000b6`d193fa40 00007ffa`510be0fb ntdll!RtlFreeHeap+0x6da
0f 000000b6`d193fb30 00007ff6`d36410e2 ucrtbase!free_base+0x1b
10 000000b6`d193fb60 00007ff6`d3641310 ConsoleApplication1!main+0x42 [C:\source\repos\ConsoleApplication1\ConsoleApplication1.cpp @ 17]
11 (Inline Function) --------`-------- ConsoleApplication1!invoke_main+0x22
12 000000b6`d193fbb0 00007ffa`51c5e8d7 ConsoleApplication1!__scrt_common_main_seh+0x10c
13 000000b6`d193fbf0 00007ffa`5377c34c kernel32!BaseThreadInitThunk+0x17
14 000000b6`d193fc20 00000000`00000000 ntdll!RtlUserThreadStart+0x2c
#0fのフレームからfree
関数を呼び出したときに例外が発生したことが分かります。また、呼び出し元の#10のフレームはConsoleApplication1.cppの17行目でmain
関数内となっています。このスタックトレースから分かることは、free
関数を呼び出してエラーが発生したこと、そしてその呼び出し元がmain
関数であることだけです。
次に、!heap -triage
コマンドを実行してヒープの状態を確認すると、HEAP_FAILURE_BLOCK_NOT_BUSYというエラーが検出されます。これは、解放しようとしたメモリブロックがすでに解放されているか、そもそも渡されたアドレスがヒープから割り当てたブロックではなかったことを示しています。しかし、Stack traceを確認しても、先ほどk
コマンドを実行した時と同様にmain
関数から呼び出されているfree
関数の呼び出しを指していることが分かるだけです。
0:000> !heap -triage
**************************************************************
* *
* HEAP ERROR DETECTED *
* *
**************************************************************
Details:
Heap address: 0000023742a30000
Error address: 0000023742a3c310
Error type: HEAP_FAILURE_BLOCK_NOT_BUSY
Details: The caller performed an operation (such as a free
or a size check) that is illegal on a free block.
Follow-up: Check the error's stack trace to find the culprit.
Stack trace:
Stack trace at 0x00007ffa5390e138
00007ffa537511eb: ntdll!RtlpLogHeapFailure+0x4b
00007ffa537d59ea: ntdll!RtlFreeHeap+0x6da
00007ffa510be0fb: ucrtbase!free_base+0x1b
00007ff6d36410e2: ConsoleApplication1!main+0x42
00007ff6d3641310: ConsoleApplication1!__scrt_common_main_seh+0x10c
00007ffa51c5e8d7: kernel32!BaseThreadInitThunk+0x17
00007ffa5377c34c: ntdll!RtlUserThreadStart+0x2c
また、ヒープブロックは既に解放された後であるため、以下のようにdb
コマンドでヒープブロックの内容を確認しても有意な情報は得られません。
0:000> db 0000023742a3c310
00000237`42a3c310 6f 6e 31 2e 65 78 65 00-83 b3 f1 8b 76 fa 00 00 on1.exe.....v...
00000237`42a3c320 c0 8c a3 42 37 02 00 00-50 01 a3 42 37 02 00 00 ...B7...P..B7...
3. ページヒープとApplication Verifierの有効化およびクラッシュダンプの再取得
ページヒープを有効にすることで、ヒープの状態をより詳細に監視し、問題の発生箇所を特定しやすくなります。ページヒープを有効にすると、ヒープブロックの前のアドレスにデバッグ用の情報が追加され、割り当てや解放のスタックトレースを取得できるようになります。これにより、二重解放が発生したときにどの処理からヒープブロックが解放されていたのかを特定することができます。また、アプリケーションの問題を検出するためのデバッグ用の機能であるApplication Verifierを有効にすると、ダンプファイルからスタックトレースを含むヒープ操作ログを確認できるようになります。
ぺージヒープとApplication Verifierはレジストリの設定でアプリケーション毎に有効化することができます。Image File Execution Options
キー(IFEO)の配下にアプリケーション名のサブキーを作成し、その中にGlobalFlag
とPageHeapFlags
の値を設定します。
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\ConsoleApplication1.exe]
"GlobalFlag"=dword:02000100
"PageHeapFlags"=dword:00000003
もちろんレジストリエディタで直接編集しても構いません。
ページヒープとApplication Verifierの設定はアプリケーションの起動時に適用されるため、設定を変更した後はアプリケーションを再起動する必要があります。再起動後にアプリケーションを実行し、問題を再現させると、Application Verifierによるヒープの検証処理で例外が発生します。ページヒープを有効にする前と同様にWindows Error Reportingの機能でクラッシュダンプファイルが生成されますが、イベントログは例外コードがヒープ破損を示す0xc0000374から検証による検出を示す0xc0000421(STATUS_VERIFIER_STOP)に変わっている点に注意してください。
障害が発生しているアプリケーション名: ConsoleApplication1.exe、バージョン: 0.0.0.0、タイム スタンプ: 0x68764432
障害が発生したモジュール名: verifier.dll、 バージョン: 10.0.26100.1882、タイム スタンプ: 0x55c2884c
例外コード: 0xc0000421
フォールト オフセット: 0x0000000000007642
フォールト プロセス ID: 0x7D8
アプリケーションのフォールトの開始時刻: 0x1DBF58EC6CB5159
Faulting アプリケーション パス: C:\source\repos\ConsoleApplication1\x64\Release\ConsoleApplication1.exe
Faulting モジュール パス: C:\WINDOWS\System32\verifier.dll
Report Id: 8cde91f7-2f31-4131-bb07-fa904a6a539f
情報の取得が終わりページヒープの設定が不要になったら、設定したレジストリは削除しておきましょう。
4. ページヒープとApplication Verifier有効化後のクラッシュダンプの解析
ページヒープとApplicaetion Verifierを有効化した状態でクラッシュダンプを再取得したら、WinDbgを使用して改めて解析を行います。k
コマンドを入力してスタックトレースを表示すると、以下のような出力が得られます。
0:000> k
# Child-SP RetAddr Call Site
00 00000057`dcd2ef78 00007ffa`53828ffa ntdll!NtWaitForMultipleObjects+0x14
01 00000057`dcd2ef80 00007ffa`53828c63 ntdll!WerpWaitForCrashReporting+0x82
02 00000057`dcd2f000 00007ffa`538286c8 ntdll!RtlReportExceptionHelper+0x4d3
03 00000057`dcd2f180 00007ff9`6c4a72b0 ntdll!RtlReportException+0x78
04 00000057`dcd2f1b0 00007ff9`6c4a7642 verifier!VerifierCaptureContextAndReportStop+0xc8
05 00000057`dcd2f780 00007ff9`6c4a5c3e verifier!VerifierStopMessage+0x2f2
06 00000057`dcd2f840 00007ff9`6c4a400e verifier!AVrfpDphReportCorruptedBlock+0x1da
07 00000057`dcd2f900 00007ff9`6c4aaa07 verifier!AVrfpDphCheckNormalHeapBlock+0xda
08 00000057`dcd2f960 00007ff9`6c4bc82d verifier!VerifierCheckPageHeapAllocation+0x67
09 00000057`dcd2f990 00007ffa`510be0fb verifier!AVrfpRtlFreeHeap+0x6d
0a 00000057`dcd2fa20 00007ff9`6c4bee2c ucrtbase!free_base+0x1b
0b 00000057`dcd2fa50 00007ff6`d36410e2 verifier!AVrfp_ucrt_free+0x4c
0c 00000057`dcd2fa80 00007ff6`d3641310 ConsoleApplication1!main+0x42 [C:\source\repos\ConsoleApplication1\ConsoleApplication1.cpp @ 17]
0d (Inline Function) --------`-------- ConsoleApplication1!invoke_main+0x22
0e 00000057`dcd2fad0 00007ffa`51c5e8d7 ConsoleApplication1!__scrt_common_main_seh+0x10c
0f 00000057`dcd2fb10 00007ffa`5377c34c kernel32!BaseThreadInitThunk+0x17
10 00000057`dcd2fb40 00000000`00000000 ntdll!RtlUserThreadStart+0x2c
Application Verifierが有効になっているため verifier.dllがスタックトレースに含まれていることが分かります。しかし、ヒープの二重解放が発生した箇所は依然としてmain
関数の17行目となっています。Application Verifierが検出した内容に関する情報を確認するためには、!avrf
コマンドをオプションなしで実行します。これにより、Application Verifierが検出した問題の詳細情報が表示されます。
0:000> !avrf
Application verifier settings (00048004):
- fast fill heap (a.k.a light page heap)
- lock checks (critical section verifier)
- handle checks
*******************************************************************************
* *
* Exception Analysis *
* *
*******************************************************************************
APPLICATION_VERIFIER_HEAPS_DOUBLE_FREE (7)
Heap block already freed.
This situation happens if the block is freed twice. Freed blocks are marked in a
special way and are kept around for a while in a delayed free queue. If a buggy
program tries to free the block again this will be caught assuming the block was not
dequeued from delayed free queue and its memory reused for other allocations.
The depth of the delay free queue is in the order of thousands of blocks therefore
there are good chances that most double frees will be caught.
Arguments:
Arg1: 0000016fa89e1000, Heap handle for the heap owning the block.
Arg2: 0000016fa9bad740, Heap block being freed again.
Arg3: 0000000000000064, Size of the heap block.
Arg4: 0000000000000000, Not used
KEY_VALUES_STRING: 1
Key : AVRF.Code
Value: 0x7
Key : AVRF.Enabled
Value: 1
Key : AVRF.Exception
Value: 1
Key : Analysis.CPU.mSec
Value: 296
Key : Analysis.Elapsed.mSec
Value: 294
Key : Analysis.IO.Other.Mb
Value: 0
Key : Analysis.IO.Read.Mb
Value: 2
Key : Analysis.IO.Write.Mb
Value: 1
Key : Analysis.Init.CPU.mSec
Value: 6703
Key : Analysis.Init.Elapsed.mSec
Value: 2778088
Key : Analysis.Memory.CommitPeak.Mb
Value: 93
Key : Analysis.Version.DbgEng
Value: 10.0.27829.1001
Key : Analysis.Version.Description
Value: 10.2503.24.01 amd64fre
Key : Analysis.Version.Ext
Value: 1.2503.24.1
Key : Failure.Bucket
Value: VERIFIER_STOP_AVRF_c0000421_ucrtbase.dll!free_base
Key : Failure.Exception.Code
Value: 0xc0000421
Key : Failure.Exception.IP.Address
Value: 0x7ff96c4a7642
Key : Failure.Exception.IP.Module
Value: verifier
Key : Failure.Exception.IP.Offset
Value: 0x7642
Key : Failure.Hash
Value: {7dca632e-243d-6d0d-cbb2-e8766b05a47c}
Key : Failure.ProblemClass.Primary
Value: VERIFIER_STOP
Key : Timeline.OS.Boot.DeltaSec
Value: 199194
Key : Timeline.Process.Start.DeltaSec
Value: 2
Key : WER.OS.Branch
Value: ge_release
Key : WER.OS.Version
Value: 10.0.26100.1
FILE_IN_CAB: ConsoleApplication1.exe.2008.dmp
NTGLOBALFLAG: 2000100
APPLICATION_VERIFIER_FLAGS: 48004
APPLICATION_VERIFIER_LOADED: 1
CONTEXT: (.ecxr)
rax=0000000000000000 rbx=0000000000000007 rcx=00000057dcd2f280
rdx=00000057dcd2f258 rsi=0000016fa9bad740 rdi=0000016fa89e1000
rip=00007ff96c4a728b rsp=00000057dcd2f1b0 rbp=00000057dcd2f2b0
r8=0000000000000078 r9=00000057dcd2f200 r10=000000000010000b
r11=00000057dcd2d720 r12=0000000000000000 r13=0000016fa9bad740
r14=0000000000000064 r15=0000000000000000
iopl=0 nv up ei pl nz na pe nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
verifier!VerifierCaptureContextAndReportStop+0xa3:
00007ff9`6c4a728b 0f1f440000 nop dword ptr [rax+rax]
Resetting default scope
EXCEPTION_RECORD: (.exr -1)
ExceptionAddress: 00007ff96c4a7642 (verifier!VerifierStopMessage+0x00000000000002f2)
ExceptionCode: c0000421 (Verifier stop)
ExceptionFlags: 00000000
NumberParameters: 6
Parameter[0]: 0000000032725641
Parameter[1]: 0000000000000007
Parameter[2]: 0000016fa89e1000
Parameter[3]: 0000016fa9bad740
Parameter[4]: 0000000000000064
Parameter[5]: 0000000000000000
PROCESS_NAME: ConsoleApplication1.exe
ERROR_CODE: (NTSTATUS) 0xc0000421 - A v P [ V Ì Ø Å A » Ý Ì v Z X É G [ ª © Â © Ü µ ½ B
EXCEPTION_CODE_STR: c0000421
EXCEPTION_PARAMETER1: 0000000032725641
EXCEPTION_PARAMETER2: 0000000000000007
EXCEPTION_PARAMETER3: 0000016fa89e1000
EXCEPTION_PARAMETER4: 16fa9bad740
STACK_TEXT:
00000057`dcd2f1b0 00007ff9`6c4a7642 : 00007ff9`6c4dc234 00007ff9`6c4dc288 00000000`00000007 00007ff9`6c4c9800 : verifier!VerifierCaptureContextAndReportStop+0xa3
00000057`dcd2f780 00007ff9`6c4a5c3e : 0000016f`a9bad740 00007ff9`6c4c9490 00000000`00000000 00007ff9`6c4c9260 : verifier!VerifierStopMessage+0x2f2
00000057`dcd2f840 00007ff9`6c4a400e : 00000000`00000040 0000016f`a89e1000 0000016f`a9bad740 0000016f`a89e1000 : verifier!AVrfpDphReportCorruptedBlock+0x1da
00000057`dcd2f900 00007ff9`6c4aaa07 : 0000016f`a89e1000 0000016f`a9bad740 00000000`00000000 00000000`00000000 : verifier!AVrfpDphCheckNormalHeapBlock+0xda
00000057`dcd2f960 00007ff9`6c4bc82d : 0000016f`a9bad740 00000000`00000000 00007ff6`d3642276 00000000`00000064 : verifier!VerifierCheckPageHeapAllocation+0x67
00000057`dcd2f990 00007ffa`510be0fb : 0000016f`a89e0000 00000000`00000000 0000016f`a9bad740 00007ffa`537d5310 : verifier!AVrfpRtlFreeHeap+0x6d
00000057`dcd2fa20 00007ff9`6c4bee2c : 0000016f`a9bad740 00000000`00000000 00007ff6`d3642276 00007ffa`510b5de0 : ucrtbase!free_base+0x1b
00000057`dcd2fa50 00007ff6`d36410e2 : 0000016f`a9bad190 00000000`00000000 00007ff6`d36421f0 00007ff6`d36416b9 : verifier!AVrfp_ucrt_free+0x4c
00000057`dcd2fa80 00007ff6`d3641310 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ConsoleApplication1!main+0x42
(Inline Function) --------`-------- : --------`-------- --------`-------- --------`-------- --------`-------- : ConsoleApplication1!invoke_main+0x22
00000057`dcd2fad0 00007ffa`51c5e8d7 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ConsoleApplication1!__scrt_common_main_seh+0x10c
00000057`dcd2fb10 00007ffa`5377c34c : 00000000`00000000 00000000`00000000 000004f0`fffffb30 000004d0`fffffb30 : kernel32!BaseThreadInitThunk+0x17
00000057`dcd2fb40 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x2c
STACK_COMMAND: ~0s; .ecxr ; kb
SYMBOL_NAME: ucrtbase!free_base+1b
MODULE_NAME: ucrtbase
IMAGE_NAME: ucrtbase.dll
FAILURE_BUCKET_ID: VERIFIER_STOP_AVRF_c0000421_ucrtbase.dll!free_base
OS_VERSION: 10.0.26100.1
BUILDLAB_STR: ge_release
OSPLATFORM_TYPE: x64
OSNAME: Windows 10
IMAGE_VERSION: 10.0.26100.4652
FAILURE_ID_HASH: {7dca632e-243d-6d0d-cbb2-e8766b05a47c}
Followup: MachineOwner
---------
ここで、APPLICATION_VERIFIER_HEAPS_DOUBLE_FREE (7)というメッセージが表示されていることに注目してください。これは、ヒープブロックがすでに解放されている状態で再度解放しようとしたことを示しています。Arg1
にはヒープハンドル、Arg2
には再度解放しようとしたヒープブロックのアドレス、Arg3
にはヒープブロックのサイズが表示されています。ここのSTACK_TEXTの部分では例外を発生させた際のスタックトレースが表示されていますので、さらに解放時のスタックトレースを確認するために!avrf -hp -a <ヒープブロックのアドレス>
コマンドを実行してヒープ操作ログを確認します。
0:000> !avrf -hp -a 0000016fa9bad740
Searching call tracker @ 0000016fa8a49fc0 with 104 valid entries ...
--------------------------------------------------------------
2025-07-15T13:45:50.701Z GlobalIndex 74 ThreadId 43DC
HeapFree: 16FA9BAD740 64 16FA89E0000 43DC
00007ffa510be0fb: ucrtbase!free_base+0x1B
00007ff96c4bee2c: verifier!AVrfp_ucrt_free+0x4C
00007ff6d3641095: ConsoleApplication1!do_bad_thing+0x25
00007ff6d36410d7: ConsoleApplication1!main+0x37
00007ff6d3641310: ConsoleApplication1!__scrt_common_main_seh+0x10C
00007ffa51c5e8d7: kernel32!BaseThreadInitThunk+0x17
00007ffa5377c34c: ntdll!RtlUserThreadStart+0x2C
--------------------------------------------------------------
2025-07-15T13:45:50.701Z GlobalIndex 72 ThreadId 43DC
HeapAlloc: 16FA9BAD740 64 16FA89E0000 43DC
00007ff96c4a27a9: verifier!AVrfDebugPageHeapAllocate+0x499
00007ffa537850c7: ntdll!RtlDebugAllocateHeap+0x387
00007ffa537878da: ntdll!RtlpAllocateHeap+0x246A
00007ffa5374f591: ntdll!RtlpAllocateNTHeapInternal+0x3D1
00007ffa5374f164: ntdll!RtlAllocateHeap+0xAD4
00007ff96c4bc522: verifier!AVrfpRtlAllocateHeap+0x122
00007ffa510b0139: ucrtbase!malloc_base+0x39
00007ff96c4bee85: verifier!AVrfp_ucrt_malloc+0x35
00007ff6d36410b2: ConsoleApplication1!main+0x12
00007ff6d3641310: ConsoleApplication1!__scrt_common_main_seh+0x10C
00007ffa51c5e8d7: kernel32!BaseThreadInitThunk+0x17
00007ffa5377c34c: ntdll!RtlUserThreadStart+0x2C
このログから、ヒープブロックがどのように割り当てられ、解放されたかの詳細な情報が得られます。HeapFree
のエントリは、ヒープブロックが解放されたことを示しており、その後に続くスタックトレースから、どの関数から解放が呼び出されたかが分かります。ここでは、do_bad_thing
関数からfree
が呼び出されていることが確認できます。
一回目と二回目のどちらのヒープの解放を修正するべきかについては、関数やクラスの設計や使用方法に依存します。しかし、一般的にはヒープブロックを解放する責任はヒープブロックを割り当てた側にあるため、ここでのdo_bad_thing
関数のように自分で割り当てたものではないヒープブロックを解放しようとすることは避けたほうが無難でしょう。
ページヒープを使用する場合の注意点
ページヒープを使用することで、ヒープの二重解放の原因特定が容易になりますが、いくつかの注意点があります。
-
パフォーマンスへの影響: ページヒープを使用すると、ヒープ操作のパフォーマンスが大幅に低下する可能性があります。特に、頻繁にヒープ操作を行うアプリケーションでは、パフォーマンスへの影響が顕著になるため、使用する際は注意が必要です。
-
メモリ使用量の増加: ページヒープを有効にすると、各ヒープブロックの前後に読み書きできないページが追加されるため、メモリ使用量が増加します。特に、大量のヒープブロックを使用するアプリケーションでは、メモリ使用量の増加が問題となることがあります。
-
互換性の問題: 一部のアプリケーションやライブラリは、ページヒープを使用することで互換性の問題が発生する可能性があります。特に、低レベルのメモリ操作を行うアプリケーションでは、ページヒープとの相性が悪い場合があります。
-
別の問題の顕在化: ページヒープを有効にすると、目的の箇所以外でメモリ関連の問題が検出される可能性があります。これは未知の問題の発見で有益なものと捉えることもできますが、逆にデバッグ作業を複雑にする要因ともなります。また、目的の箇所で問題を検出するために、他の問題を先に取り除く必要が生じる可能性があります。
-
検出できない場合もある: ページヒープはヒープの様々な問題を検出するための強力なツールですが、すべてのケースで検出できるわけではありません。例えば、特定のデータや手順でのみ問題が発生する処理フローに至るケースでは、ページヒープを有効化したとしてもデータや手順といった再現条件を満たさない限り問題は検出されません。
なお、クラッシュダンプからページヒープを有効化できているか確認したい場合は、プロセス環境ブロックから状態を確認することができます。NtGlobalFlagの値が0x2000100であればページヒープとApplication Verifierが有効になっています。0x2000000はページヒープ、0x0000100はApplication Verifierが有効になっていることを示します。もし有効になっていない場合は、レジストリの設定に誤りがないか確認してください。
0:000> !peb
PEB at 00000057dceff000
InheritedAddressSpace: No
ReadImageFileExecOptions: No
BeingDebugged: No
ImageBaseAddress: 00007ff6d3640000
NtGlobalFlag: 2000100
NtGlobalFlag2: 0
...