メインコンテンツまでスキップ

メモリ パターン

Visual C++(特にMicrosoftのC++ランタイムやデバッグ環境)では、メモリの初期化や未使用領域の検出のために特定のmemory fill patterns(メモリフィルパターン)が使われます。これらは、バグの検出やデバッグを容易にするために設計されています。

メモリ パターン説明
0xBABABABAデバッグ版Cランタイムがアラインメント違反のアクセスの検出に使用するパターン(VS2003.NET)。"ココココ....
0xCCCCCCCCスタック領域のローカル変数の未初期化メモリに使用されるパターン。"フフフフ..."
0xCDCDCDCDデバッグ版Cランタイムがヒープ領域の未初期化メモリに使用するパターン。"ヘヘヘヘ...."
0xDDDDDDDDデバッグ版Cランタイムがヒープ領域の解放済み領域に使用するパターン。"ンンンン..."
0xEDEDEDEDアラインメント違反のアクセスの検出に使用されるガード領域のパターン(VS2005以降)
0xFDFDFDFDデバッグ版Cランタイムがヒープのアンダーランとオーバーランの検出に使用するガード領域のパターン。
0x8123use after free bugを検出するために、delete後のポインター変数に使用される/sdlオプションのpointer sanitizationパターン。https://learn.microsoft.com/ja-jp/cpp/build/reference/sdl-enable-additional-security-checks?view=msvc-170#runtime-checks
0xA0A0A0A0ライト ページ ヒープの有効化時に、割り当てたヒープ ブロックの末尾に埋め込まれるパターン
0xE0E0E0E0ライト ページ ヒープの有効化時に、割り当てたヒープ ブロックで未初期化の領域に使用されるパターン
0xABCDAAAAライト ページ ヒープの有効化時に、割り当てたヒープ ブロックのブロック ヘッダーの開始に使用されるパターン
0xDCBAAAAAライト ページ ヒープの有効化時に、割り当てたヒープ ブロックのブロック ヘッダーの終了に使用されるパターン
0xF0F0F0F0ライト ページ ヒープまたはフル ページ ヒープの有効化時に、解放後の領域に使用されるパターン
0xABCDAAA9ライト ページ ヒープの有効化時に、解放後のヒープ ブロックのブロック ヘッダーの開始に使用されるパターン
0xDCBAAAA9ライト ページ ヒープの有効化時に、解放後のヒープ ブロックのブロック ヘッダーの終了に使用されるパターン
0xC0C0C0C0フル ページ ヒープの有効化時に、割り当てたヒープ ブロックで未初期化の領域に埋め込まれるパターン
0xD0D0D0D0フル ページ ヒープの有効化時に、割り当てたヒープ ブロックの末尾に埋め込まれるパターン
0xABCDBBBBフル ページ ヒープの有効化時に、割り当てたヒープ ブロックのブロック ヘッダーの開始に使用されるパターン
0xDCBABBBBフル ページ ヒープの有効化時に、割り当てたヒープ ブロックのブロック ヘッダーの終了に使用されるパターン
0xABCDBBBAフル ページ ヒープの有効化時に、割り当てたヒープ ブロックのブロック ヘッダーの開始に使用されるパターン
0xDCBABBBAフル ページ ヒープの有効化時に、割り当てたヒープ ブロックのブロック ヘッダーの終了に使用されるパターン

0xBABABABA

0xBAは、デバッグ版のCランタイムがヒープの問題を検出するために使用されるパターンです。Cランタイムによって割り当てられたアラインメントされたヒープ領域の直前4バイトまたは8バイト分に0xBAが埋め込まれ、アラインメント外のアクセスの検出に使用されます。Visual Studio .NET 2003 では0xBAが使われていましたが、0xBABABABAは/LARGEADDRESSWAREフラグが使用されている場合有効なアドレス空間となることから、Visual Studio 2005以降では0xEDに変更されています。なお、Visual C++ 6.0以前はアラインメントされた領域を確保する関数自体がありませんでしたので、アラインメントされたヒープ領域の直前に埋め込まれるパターンも存在しませんでした。

C:\Program Files (x86)\Microsoft Visual Studio .NET 2003\Vc7\crt\src\dbgheap.c
static unsigned char _bAlignLandFill  = 0xBD;   /* fill no-man's land for
aligned routines */

0xCCCCCCCC

0xCCは、Visual C++コンパイラーのオプションで/RTCを有効にしたときに、ローカル変数の未初期化メモリに割り当てられるマジックナンバーです。変数を初期化するためにも値を割り当てるための命令を実行する必要がありますので、C/C++コンパイラーは未初期化のローカル変数に値を割り当てることはせず、単にスタックポインターの位置を確保することだけを行います。そのため、ローカル変数の値は"不定"であり、未初期化のまま使用すると予期しない動作を引き起こす可能性があります。"不定"とはいえ完全にランダムな値になるわけではなく、直前に呼び出された関数のスタックフレームの内容が残っていることが多いです。しかし未初期化の変数に関する動作は未定義で、一定の動作を期待するべきではありません。

/RTCオプションを使用した場合、未初期化のローカル変数に0xCCCCCCCCを割り当てる命令が関数の冒頭に挿入されます。これにより、未初期化のローカル変数を参照した際に0xCCCCCCCCが表示されるため、デバッグ時に未初期化の変数を検出することができます。以下のコードをDebugとReleaseの両方でビルドして実行すると、この様子が確認できると思います。なお、0xCCは半角カナの"フ"に相当するため、未初期化のローカル変数が何らかの形で表示されるときは"フフフフ..."のように表示される場合があります。

#include <stdio.h>

void foo();
void bar();

int main()
{
bar();
foo();
}

__declspec(noinline)
void foo()
{
int a = 0, b, c;
char buffer[16];
int* pa = &a, * pb = &b, * pc = &c;
char* p = buffer;

printf("a = %08X\n", *pa);
printf("b = %08X\n", *pb);
printf("c = %08X\n", *pc);
printf("buffer = %s\n", p);
}

#pragma optimize("", off)
void bar()
{
char buf[256];
for (int i = 0; i < 256; i++)
{
buf[i] = 0x29;
}
}

// Output: /release
// a = 00000000
// b = 29292929
// c = 29292929
// buffer = ))))))))))))))))S」イO,

// Output: /debug
// a = 00000000
// b = CCCCCCCC
// c = CCCCCCCC
// buffer = フフフフフフフフフフフフフフフフフフフフフフフフフフフフフフフフフフフフフフフフフフフフフフフフ蔔・
__declspec(noinline)
void foo()
{
00007FF62C7F1860 push rbp
00007FF62C7F1862 push rdi
00007FF62C7F1863 sub rsp,1F8h
00007FF62C7F186A lea rbp,[rsp+20h]
00007FF62C7F186F lea rdi,[rsp+20h]
00007FF62C7F1874 mov ecx,46h
00007FF62C7F1879 mov eax,0CCCCCCCCh
00007FF62C7F187E rep stos dword ptr [rdi]
00007FF62C7F1880 mov rax,qword ptr [__security_cookie (07FF62C7FE000h)]
00007FF62C7F1887 xor rax,rbp
00007FF62C7F188A mov qword ptr [rbp+1C8h],rax
00007FF62C7F1891 lea rcx,[__DEAAFE20_ConsoleApplication1@cpp (07FF62C803008h)]
00007FF62C7F1898 call __CheckForDebuggerJustMyCode (07FF62C7F1375h)
00007FF62C7F189D nop
int a = 0, b, c;
00007FF62C7F189E mov dword ptr [a],0
char buffer[16];
int* pa = &a, * pb = &b, * pc = &c;
00007FF62C7F18A5 lea rax,[a]
00007FF62C7F18A9 mov qword ptr [pa],rax
00007FF62C7F18B0 lea rax,[b]
00007FF62C7F18B4 mov qword ptr [pb],rax
00007FF62C7F18BB lea rax,[c]
00007FF62C7F18BF mov qword ptr [pc],rax
char* p = buffer;
00007FF62C7F18C6 lea rax,[buffer]
00007FF62C7F18CA mov qword ptr [p],rax

0xCDCDCDCD & 0xDDDDDDDD

0xCDと0xDDは、デバッグ版のCランタイムがヒープの問題を検出するために使用されるパターンです。これらは、未初期化のメモリや解放済みのメモリを検出するために使用されます。0xCDが未初期化の領域に使用されるパターン、0xDDが解放後の領域に使用されるパターンです。0xCdは半角カナの"ヘ"、0xddは半角カナの"ン"に相当するため、これらの領域の値何らかの形で表示されるときは"ヘヘヘヘ..."や"ンンンン..."のように表示される場合があります。

#include <stdio.h>

int main()
{
char *buffer = new char[16];
char *p = buffer;
buffer[15] = '\0';
for (int i=0; i<16; i++)
printf("%02X ", (unsigned char)buffer[i]);
printf("%s\n", buffer);

delete[] buffer;
p[15] = '\0';
for (int i = 0; i < 16; i++)
printf("%02X ", (unsigned char)p[i]);
printf("%s\n", p);

return 0;
}

// Output: /release
// 6C 00 65 00 73 00 20 00 28 00 78 00 38 00 36 00 l
// 6C 00 65 00 73 00 20 00 28 00 78 00 38 00 36 00 l

// Output: /debug
// CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD 00 ヘヘヘヘヘヘヘヘヘヘヘヘヘヘヘ
// DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD 00 ンンンンンンンンンンンンンンン

なお、デバッグ版のCランタイムヒープの実装はWindows SDKに含まれており、C:\Program Files (x86)\Windows Kits\10\Source\10.0.26100.0\ucrt\heap\debug_heap.cppから確認することができます。

C:\Program Files (x86)\Windows Kits\10\Source\10.0.26100.0\ucrt\heap\debug_heap.cpp
// The following values are non-zero, constant, odd, large, and atypical.
// * Non-zero values help find bugs that assume zero-filled data
// * Constant values are good so that memory filling is deterministic (to help
// make bugs reproducible). Of course, it is bad if the constant filling of
// weird values masks a bug.
// * Mathematically odd numbers are good for finding bugs assuming a cleared
// lower bit (e.g. properly aligned pointers to types other than char are not
// odd).
// * Large byte values are less typical and are useful for finding bad addresses.
// * Atypical values (i.e., not too often) are good because they typically cause
// early detection in code.
// * For the case of the no-man's land and free blocks, if you store to any of
// these locations, the memory integrity checker will detect it.
//
// The align_land_fill was changed from 0xBD to 0xED to ensure that four bytes of
// that value (0xEDEDEDED) would form an inaccessible address outside of the lower
// 3GB of a 32-bit process address space.
static unsigned char const no_mans_land_fill{0xFD}; // Fill unaligned no-man's land
static unsigned char const align_land_fill {0xED}; // Fill aligned no-man's land
static unsigned char const dead_land_fill {0xDD}; // Fill free objects with this
static unsigned char const clean_land_fill {0xCD}; // Fill new objects with this

0xFDFDFDFD

0xFDは、デバッグ版のCランタイムがヒープの問題を検出するために使用されるパターンです。Cランタイムによって割り当てられたヒープ領域の前後4バイト分に0xFDが埋め込まれ、バッファーオーバーフローやアンダーフローの検出に使用されます。0xFDはマッピングされるコードポイントがないため、文字として表示が試みられた場合"・"などの代替文字として表示される可能性があります。

#include <stdio.h>
#include <memory.h>

int main()
{
char* buffer = new char[8];
char* p = buffer;
memset(buffer, 0, 8);
for (int i = -8; i < 16; i++)
printf("%02X ", (unsigned char)buffer[i]);
printf("\n");

delete[] buffer;
for (int i = -8; i < 16; i++)
printf("%02X ", (unsigned char)p[i]);
printf("\n");
}

// Output: /release
// 93 4B FC 03 E7 29 00 18 00 00 00 00 00 00 00 00 50 01 A3 DD E3 02 00 00
// 9E 4B FD 0F E7 29 00 00 70 9F A3 DD E3 02 00 00 50 01 A3 DD E3 02 00 00

// Output: /debug
// 5A 00 00 00 FD FD FD FD 00 00 00 00 00 00 00 00 FD FD FD FD 69 00 72 00
// DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD 69 00 72 00

こちらも実装はC:\Program Files (x86)\Windows Kits\10\Source\10.0.26100.0\ucrt\heap\debug_heap.cppから確認することができます。

C:\Program Files (x86)\Windows Kits\10\Source\10.0.26100.0\ucrt\heap\debug_heap.cpp
static unsigned char const no_mans_land_fill{0xFD}; // Fill unaligned no-man's land

// The size of the no-man's land used in unaligned and aligned allocations:
static size_t const no_mans_land_size = 4;

0xEDEDEDED

0xEDは、デバッグ版のCランタイムがヒープの問題を検出するために使用されるパターンです。Cランタイムによって割り当てられたアラインメントされたヒープ領域の直前4バイトまたは8バイト分に0xEDが埋め込まれ、アラインメント外のアクセスの検出に使用されます。Visual Studio .NET 2003 では0xBAが使われていましたが、0xBABABABAは/LARGEADDRESSWAREフラグが使用されている場合有効なアドレス空間となることから、Visual Studio 2005以降では0xEDに変更されています。なお、Visual C++ 6.0以前はアラインメントされた領域を確保する関数自体がありませんでしたので、アラインメントされたヒープ領域の直前に埋め込まれるパターンも存在しませんでした。

#include <stdio.h>
#include <stdlib.h>
#include <memory.h>

int main()
{

char* buffer = (char*)_aligned_malloc(8, 8);
char* p = buffer;
memset(buffer, 0, 8);
for (int i = -4 - sizeof(void*); i < 24; i++)
printf("%02X ", (unsigned char)buffer[i]);
printf("\n");

_aligned_free(buffer);
for (int i = -4 - sizeof(void*); i < 24; i++)
printf("%02X ", (unsigned char)p[i]);
printf("\n");
return 0;
}

// Output: /release
// 72 18 00 89 90 0E 4D AC 66 02 00 00 00 00 00 00 00 00 00 00 65 00 73 00 5C 00 53 00 B2 3A A5 58 72 19 00 80
// 72 18 00 80 90 0E 4D AC 66 02 00 00 00 00 00 00 00 00 00 00 65 00 73 00 5C 00 53 00 B2 3A A5 58 72 19 00 80

// Output: /debug
// 51 02 00 00 ED ED ED ED ED ED ED ED 00 00 00 00 00 00 00 00 CD CD CD CD CD CD CD FD FD FD FD 00 00 00 00 00
// DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD DD 00 00 00 00 00
//

こちらも実装はC:\Program Files (x86)\Windows Kits\10\Source\10.0.26100.0\ucrt\heap\debug_heap.cppから確認することができます。

C:\Program Files (x86)\Windows Kits\10\Source\10.0.26100.0\ucrt\heap\debug_heap.cpp
static unsigned char const align_land_fill  {0xED}; // Fill aligned no-man's land

// The size of the no-man's land used in unaligned and aligned allocations:
static size_t const align_gap_size = sizeof(void *);

0x8123

0x8123は、Visual C++コンパイラーのオプションで/sdlを有効にしたときに、delete演算子で解放されたポインター変数に割り当てられるマジックナンバーです。use-after-freeのバグを検出するために使用されます。

特に何も指定しない場合、delete演算子で解放されたポインター変数は元のアドレスを指したままになります。そのため、解放後にそのポインター変数を参照すると、解放されたメモリ領域にアクセスしてしまい予期しない動作を引き起こす原因になります。/sdlオプションを使用した場合はポインター変数に0x8123が割り当てられるため、当該アドレスを参照した際にアクセス違反例外が発生してアプリケーションがクラッシュします。これにより、use-after-freeのバグを早期に検出することができます。

#include <stdio.h>

int main()
{
char* p = new char;
printf("p = 0x%p\n", p);
delete p;
printf("p = 0x%p\n", p);
*p = 0xFF;
}

// Output: /sdl option enabled
// p = 0x0000020D44208920
// p = 0x0000000000008123

// Output: /sdl option disabled
// p = 0x00000171E1FE8820
// p = 0x00000171E1FE8820
	char* p = new char;
00007FF7E26619DC mov ecx,1
00007FF7E26619E1 call operator new (07FF7E266103Ch)
00007FF7E26619E6 mov qword ptr [rbp+0E8h],rax
00007FF7E26619ED mov rax,qword ptr [rbp+0E8h]
00007FF7E26619F4 mov qword ptr [p],rax
printf("p = 0x%p\n", p);
00007FF7E26619F8 mov rdx,qword ptr [p]
00007FF7E26619FC lea rcx,[string "p = 0x%p\n" (07FF7E266AC10h)]
00007FF7E2661A03 call printf (07FF7E26611D1h)
00007FF7E2661A08 nop
delete p;
00007FF7E2661A09 mov rax,qword ptr [p]
00007FF7E2661A0D mov qword ptr [rbp+108h],rax
00007FF7E2661A14 mov edx,1
00007FF7E2661A19 mov rcx,qword ptr [rbp+108h]
00007FF7E2661A20 call operator delete (07FF7E2661361h)
00007FF7E2661A25 cmp qword ptr [rbp+108h],0
00007FF7E2661A2D jne main+7Ch (07FF7E2661A3Ch)
00007FF7E2661A2F mov qword ptr [rbp+118h],0
00007FF7E2661A3A jmp main+8Fh (07FF7E2661A4Fh)
00007FF7E2661A3C mov qword ptr [p],8123h
00007FF7E2661A44 mov rax,qword ptr [p]
00007FF7E2661A48 mov qword ptr [rbp+118h],rax
printf("p = 0x%p\n", p);
00007FF7E2661A4F mov rdx,qword ptr [p]
00007FF7E2661A53 lea rcx,[string "p = 0x%p\n" (07FF7E266AC10h)]
00007FF7E2661A5A call printf (07FF7E26611D1h)
00007FF7E2661A5F nop
*p = 0xFF;
00007FF7E2661A60 mov rax,qword ptr [p]
00007FF7E2661A64 mov byte ptr [rax],0FFh
0:000> .ecxr
rax=0000000000008123 rbx=0000000000000000 rcx=c27335136c260000
rdx=00007ff7e266abc0 rsi=0000000000000000 rdi=0000000000000000
rip=00007ff7e2661a64 rsp=000000b2468ff8b0 rbp=000000b2468ff8d0
r8=7ffffffffffffffc r9=0000000000000000 r10=0000000000000000
r11=0000000000000246 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0 nv up ei pl zr na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246
ConsoleApplication1!main+0xa4:
00007ff7`e2661a64 c600ff mov byte ptr [rax],0FFh ds:00000000`00008123=??

参考情報