Delphi Programming / Object Pascal
サイトのトップページへ リンクのページへ ロゴマーク
[掲載 2018年08月31日] [更新 2018年09月17日] Delphi 一般・その他
   
メモリアクセス違反の例外と配列の範囲外操作
動作確認等 Windows 7 U64(SP1) + Delphi XE5(UP2) Pro VCL-32
[整理番号 - 0180]







メモリアクセス違反の例外と配列の範囲外操作


本ページは,配列の範囲外操作とメモリへのアクセス違反の例外に関する記事とサンプルです.

配列は要素数を設定して使用します.そして,その要素番号 (インデックス) を指定して配列要素にアクセスします.設定したインデックスの範囲を越えてアクセスすると,メモリのアクセス違反の例外が発生することがあります.

メモリアクセス違反の例外エラーは,メモリへの不正なアクセスが原因で発生します.生成または確保していないオブジェクトへの操作,解放済みのオブジェクトに対する操作,あるいは無効なメモリ領域に対する操作等で発生します.配列の範囲外の操作では,メモリ上のオブジェクトのデータを操作してしまうことによってメモリアクセス違反の例外が発生することがあります.



[備考 1]
本ページには配列の範囲外操作のテストコードがあります.現実問題としてこのようなテストをすることにはほとんど意味があるとは考えられません.
配列の範囲外操作,つまり,メモリの不正アクセスの結果,メモリアクセス違反の例外 (デバッガ例外通知) が発生する可能性がある,という予測はできます.しかし,具体的にどのような現象が発生して,どのような結果になるかを予測することには無理があります.
このことは,メモリの不正アクセス全般に言えることです.


[備考 2]
メモリアクセス違反の例外が配列の範囲外の操作が原因で発生しても,メモリアクセス違反の例外が配列の範囲外の操作の部分で発生するとは限りません.メモリアクセス違反の例外が発生した場合,その原因を特定するのは難しいと言えます.
メモリアクセス違反をチェックするツール類もありますが,それらはコーディングのミスを指摘してくれるわけではありません.




01_配列の要素数設定と範囲外の操作例


動的配列の要素数は SetLength 手続きで設定できます.以下のサンプルは SetLength で動的配列の要素数を設定して,その要素の範囲外を操作する例です.
テストで使用している配列のインデックスは 0 から 9 ですが,インデックスが 15 でもエラーが発生しません.

メモリ上の数値,文字あるいはオブジェクトのデータはバイナリデータとして格納されています.それらのバイナリデータは,自分が何のオブジェクトのデータであるかという情報は持っていません.
自分が数値であるとか,文字であるとか,配列要素の数値であるとか,Button1 のプロパティの一部であるという情報は持っていません.単なるバイナリデータです.


配列で要素数を設定すると,設定したその要素のインデックスの範囲内はアクセスが保証されますが,それ以外は保証されていません.しかしメモリ領域は連続していますから,プログラムでアクセス可能なメモリ領域であればアクセス可能です.



[備考]
[Button2] のコードを実行すると,配列要素にデータを代入していないのに123 という値が取得できています.これはいわゆる「ゴミ」と呼ばれる値です.

[Button1] イベント内で定義した配列変数はローカル変数です.ローカル変数は,定義した関数やイベント内だけで有効です.その関数やイベントの実行が終了すると,それらのローカル変数が使用したメモリ領域は解放されます.メモリ領域が解放されてもメモリ上のデータは残されたままです.
メモリ領域を解放するということは,そのメモリ領域が自由に使用可能になったという意味です. そのため [Button2] のコード内で定義した配列がその領域を使用します.[Button2] のコード内で定義した配列は初期化していませんから,メモリ上に残されているデータを取得して表示していることになります.


図1
配列の要素数を超えた要素へのアクセス結果
  • [Button1] をクリックした後に [Button2] をクリックした結果
  • 123 は [Button1] のコードで設定した値
    これはメモリ上の「ゴミ」
  • メモリアクセス違反の例外は発生しない

リスト1
//=============================================================================
//  配列の設定した範囲外に値をセットする例
//  本サンプルではエラーにはならない
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LIntArry : array of Integer;
  Lindex   : Integer;
begin
  SetLength(LIntArry, 10);

  LIndex := 15;
  LIntArry[Lindex] := 123;
end;

//=============================================================================
//  配列の設定した範囲外の値を取得する例
//  本サンプルではエラーにはならない
//  上のコードを実行してから実行すると結果は123となる
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LIntArry : array of Integer;
  Lindex   : Integer;
  LValue   : Integer;
begin
  Memo1.Lines.Clear;

  SetLength(LIntArry, 10);

  LIndex := 15;
  LValue := LIntArry[Lindex];
  Memo1.Lines.Add(IntToStr(LValue));
end;


静的配列の場合は,以下のコードのように,範囲外の要素番号 (インデックス) を数値で直接指定するとコンパイル時にエラーとなります.
配列の要素数を,変数の定義の時に設定する配列を静的配列と言います.


[dcc32 エラー] Unit1.pas(35): E1012 定数式が範囲を越えました


リスト2
//=============================================================================
//  静的配列では要素番号を直接指定するとコンパイルエラーとなる
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LIntArry : array [0..9] of Integer;
begin
  LIntArry[15] := 123;
end;




02_配列の範囲外操作でメモリアクセス違反の例外が発生する例


メモリアクセス違反の例外が発生するまで配列の要素にアクセスするサンプルです.操作する要素のインデックスを大きくしていきます.

IDE 上で [F9] を押下します.[Button1] クリックのコードは配列要素への書き込みでずか,メモリアクセス違反の例外を意味する,以下のような内容の [デバッガの例外通知] のダイアログが現れます.[継続] ボタンをクリックするとフォームが表示されますが,フォームを閉じるとまた [デバッガの例外通知] のダイアログが現れます.

[デバッガの例外通知] のダイアログの表示に注目してください.配列要素への書き込みですが,read of になっています.つまり読み出しエラーとなっています.このエラーは,配列要素への代入そのもののエラーではありません.
このサンプルは [言語の例外で通知] のチェックを外していますが,最初に発生するメモリアクセス違反の例外を try 〜 except 句で捕捉することはできません.


プロジェクト Project1.exe は例外クラス $C0000005 (メッセージ 'access violation at 0x004096bc: read of address 0x0000732e')を送出しました。

プロジェクト Project1.exe は例外クラス $C0000005 (メッセージ 'access violation at 0x004056fd: read of address 0x041a4c9c')を送出しました。

プロジェクト Project1.exe は例外クラス $C0000005 (メッセージ 'access violation at 0x00407642: read of address 0x0213dd30')を送出しました。

Runtime error 216 at 00407642


図2
メモリへのアクセス違反による [デバッガの例外通知]

現れた [デバッガの例外通知] のダイアログを [継続] ボタンで閉じると [Runtime error] のダイアログが現れます.



図3
フォームを閉じた後に現れるダイアログ

[Button2] のコードは配列要素からの読み出しのサンプルです.こちらも IDE 上で [F9] 押下で実行すると [デバッガの例外通知] のダイアログがいくつか現れます.[デバッガの例外通知] のダイアログを [継続] ボタンをクリックして閉じると下図の読み取り違反のダイアログが現れます.これもメモリアクセス違反による例外エラーのダイアログです.
配列要素の読み出しの場合は Runtime error は発生しません.



プロジェクト Project1.exe は例外クラス $C0000005 (メッセージ 'access violation at 0x004096d8: read of address 0x0000732e')を送出しました。

プロジェクト Project1.exe は例外クラス $C0000005 (メッセージ 'access violation at 0x00409690: read of address 0x0000732e')を送出しました。

モジュール 'Project1.exe' のアドレス 00409690 でアドレス 0000732E に対する読み取り違反がおきました。


図4
読み取り違反のダイアログ

下図は表示されたフォームの画像です.



図5
例外のダイアログを [継続] で閉じるとフォームが表示される
  • 数値は例外が発生する前の要素のインデックス

EXE を直接実行した場合は当然 [デバッガの例外通知] のダイアログは現れません.EXE を直接実行たした場合は下図のようなダイアログが表示されます.



図6
EXE 直接実行で [Button1] クリック
  • フォームを閉じて,かなり経ってから現れる
  • この後に Runtime error のダイアログが現れる
図7
EXE 直接実行で [Button2] クリック
  • 表示されるのはこのダイアログだけ

リスト3
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics,Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    Button2: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
    FStrArray : array of string;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  テスト用の配列の作成
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
var
  Lindex : Integer;
begin
  SetLength(FStrArray, 100);

  for Lindex := Low(FStrArray) to High(FStrArray) do begin
    FStrArray[Lindex] := FormatFloat('000#', LIndex);
  end;
end;

//=============================================================================
//  配列要素の範囲外に値を設定
//  ある要素番号になるとデバッガ例外通知が発生する
//  フォームを閉じた後もデバッガ例外通知が発生する
//
//  最後にRuntime errorのダイアログが現れる
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LStr   : string;
  Lindex : Integer;
begin
  Memo1.Lines.Clear;

  LStr := 'ABC';
  LIndex := High(FStrArray);
  while True do begin
    try
      FStrArray[Lindex] := LStr;
    except
      break;
    end;
    inc(Lindex);
  end;

  Memo1.Lines.Add(IntToStr(Lindex));
  Memo1.Lines.Add(FStrArray[Lindex]);
end;

//=============================================================================
//  配列要素の範囲外から値を取得
//  ある要素番号になるとデバッガ例外通知が発生する
//  フォームを閉じた後は例外のダイアログは現れない
//
//  Runtime errorのダイアログは表示されない
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LStr   : string;
  Lindex : Integer;
begin
  Memo1.Lines.Clear;

  LStr := 'ABC';
  LIndex := High(FStrArray);
  while True do begin
    try
      LStr := FStrArray[Lindex];
    except
      break;
    end;
    inc(Lindex);
  end;

  Memo1.Lines.Add(IntToStr(Lindex));
  Memo1.Lines.Add(FStrArray[Lindex]);
end;

end.




03_範囲外操作は他のオブジェクトのデータを破損さぜる


上のサンプルでは,要素のインデックスが小さい値の時は例外は発生しません.
上のサンプルも最初のサンプルも配列要素のアクセス用のコードしかありません.しかし,実際のアプリケーションでは,コンポーネント類を含めて多くのオブジェクトが存在します.EXE の実行開始時にメモリ上に展開して格納されるオブジェクトのデータもあれば,実行時にメモリ上に展開して配置されるオブジェクトのデータもあります.

それらがメモリ上にどのように配置,格納されるのかを把握することはできません.配列の範囲外のインデックスの要素に書き込みした場合,そのメモリ位置に何かのオブジェクトのデータが存在すると,その内容が変わってしまいます.するとそのオブジェクトは正常に機能しなくなります.

上のサンプルで配列要素への書き込みであるのに,読み出しの例外エラーが発生するのは,このオブジェクトのメモリの破損とも言える現象が原因です.

例えば TButton のメモリ上のデータは単なるバイナリデータですが,そのバイナリデータは TButton と言うクラス型の形式に従ったデータとして格納されていなければ TButton のオブジェクトとしては機能しません.

下に示すサンプルは,整数型の動的配列の要素に値を代入する例です.整数型の配列 FIntArry1 の各要素に値を代入した後,FIntArry2 配列が範囲外の要素に値を代入したため,最初の配列 FIntArry1 の配列要素の値が変わってしまっています.
このサンプルは,配列変数をローカル変数にしたり,アクセスする範囲外の要素を変更すると,メモリ上の配置も変わりますから,当然,挙動も違ってきます.

このサンプルでは,[Button1] をもう一度クリックするとメモリアクセス違反の例外 (読み出し) が発生します.これは配列が範囲外の領域を操作したためです.



図8
下のサンプルの実行結果
  • FIntArry1 配列要素の値のリスト
  • FIntArry2 配列が範囲外の操作を行ったため,配列要素の値だけでなく,要素数も変っている
図9
FIntArry1 配列要素の本来の値のリスト
  • FIntArry2 が範囲外の操作をしない場合

リスト4
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics,Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    CheckBox1: TCheckBox;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
    FIntArry1 : array of Integer;
    FIntArry2 : array of Integer;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  配列の要素数を設定
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  SetLength(FIntArry2, 5);
  SetLength(FIntArry1, 5);
end;

//=============================================================================
//  各配列要素に値を代入
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  Lindex   : Integer;
  LFmtText : string;
begin
  Memo1.Lines.Clear;
  Memo1.Lines.BeginUpdate;

  for Lindex := 0 to Length(FIntArry1) - 1 do begin
    FIntArry1[Lindex] := LIndex;
  end;

  for Lindex := 0 to 10 do begin
    FIntArry2[Lindex] := LIndex + 100;
  end;

  for Lindex := 0 to Length(FIntArry1) - 1 do begin
    LFmtText := FormatFloat('00#', Lindex) + ' : ';
    Memo1.Lines.Add(LFmtText + IntToStr(FIntArry1[Lindex]));
  end;

  Memo1.Lines.EndUpdate;
end;

end.


以下のコードは,上のサンプルの OnCreate イベントと [Button1] クリックのコードを変更していますが,配列に代入している値は上のサンプルと同じです.
[Button1] クリックのコードでは,配列要素の値を加算して,その結果を表示します.しかし,値は正確ではありません.しかも,このサンプルはエラーを発生しません.計算結果の間違いに気が付かないままプログラムを終了してしまう可能性があります.



図10
範囲外操作でもエラーが発生しないことがある
  • 112 は間違った結果
  • 正しい値は 4

リスト5
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics,Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
    FIntArry1 : array of Integer;
    FIntArry2 : array of Integer;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  配列の要素数を設定して各列要素に値を代入
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
var
  Lindex : Integer;
begin
  SetLength(FIntArry2, 5);
  SetLength(FIntArry1, 5);

  for Lindex := 0 to Length(FIntArry1) - 1 do begin
    FIntArry1[Lindex] := LIndex;
  end;

  for Lindex := 0 to 10 do begin
    FIntArry2[Lindex] := LIndex + 100;
  end;
end;

//=============================================================================
//  配列要素に代入した値を元にした計算結果を表示
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LValue : Integer;
  Lindex : Integer;
begin
  LValue := FIntArry1[1] + FIntArry1[3];
  MessageBox(Handle, PChar(IntToStr(LValue)), '結果', MB_ICONINFORMATION);

  for Lindex := 0 to 4 do begin
    FIntArry1[Lindex] := LIndex;
  end;
end;

end.




04_配列の要素番号の下限と上限は Low 関数と High 関数で取得


配列の範囲外の操作は予期しないメモリアクセス違反の例外を発生することがあります.また,メモリアクセス違反の例外が発生しなくても,メモリ上のデータが書き換えられることによって,予期しない結果となることもあります.
エラーが発生しない場合,その予期しない結果に気づかない可能性も考えられます.

ここまでのサンプルには配列操作用のコードしかありませんからエラーは特定できます.しかし,実際のアプリケーションで例外が発生した場合,それが配列操作が原因であるという判定は困難です.メモリアクセス違反の例外は,配列操作だけで発生するとは限りません.
配列操作による例外の発生を未然に防止するには,設定した範囲以外のインデックスの要素以外にはアクセスしないようにすることです.

Low 関数を使用すると配列のインデックスの下限値が取得できます.配列が空の時は Low 関数は要素番号の下限値を返します.配列のインデックスの上限値は High 関数で取得できます.配列が空の時,High 関数は -1 を返すので for ループでも安心して利用できます.
これらの関数を利用することも,配列操作のミスを防止する方法の 1 つと言えます.



図11
Low 関数と High 関数を使用した配列の操作例

リスト6
//=============================================================================
//  配列要素のインデックスの下限値は Low 関数で取得できる
//  配列要素のインデックスの上限値は High 関数で取得できる
//
//  あるいは for in do を使用する
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LIntArry : array [1..5] of Integer;
  Lindex   : Integer;
  LValue   : Integer;
begin
  Memo1.Lines.Clear;

  //配列要素に値を代入
  for Lindex := Low(LIntArry) to High(LIntArry) do begin
    LIntArry[Lindex] := Lindex;
  end;

  //配列要素の値を取得して表示
  for LValue in LIntArry do begin
    Memo1.Lines.Add(FormatFloat('00#', LValue));
  end;
end;

//=============================================================================
//  配列要素のインデックスの下限値は Low 関数で取得できる
//  配列要素のインデックスの上限値は High 関数で取得できる
//
//  あるいは for in do を使用する
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LIntArry : array [1..5] of Integer;
  Lindex   : Integer;
  LValue   : Integer;
begin
  Memo1.Lines.Clear;

  //配列要素に値を代入
  //逆順で代入する例
  for Lindex := High(LIntArry) downto Low(LIntArry) do begin
    LIntArry[Lindex] := Lindex;
  end;

  //配列要素の値を取得して表示
  for LValue in LIntArry do begin
    Memo1.Lines.Add(FormatFloat('00#', LValue));
  end;
end;


上のサンプルのように,配列の下限値と上限値を関数で取得すると,コンパイル時にそれらの値は変数に割り当てられます.
一般的に,このような値は実値よりも変数を使用した方が処理が高速になります.下図はその違いをテストした結果です.ただし,最適化を有効ににした場合はその違いはほとんどありません.
ビルド構成が Release の時は,最適化のデフォルト値は True です.



[備考]
ビルド構成等,コンパイルの動作仕様を変更するオプションを変更した場合,一般的にはプロジェクトのビルド(再構築) が必要です.[Shift] + [F9] でビルド (再構築) できます.
コンパイルのオブジョンをコンパイラ指定で指定する場合はビルド (再構築) は必要ありません.


図12
ループの処理回数の実値と変数指定の違い
  • 最適化が無効な場合
  • 最初の値は変数で指定した時
  • 次が実値で指定した時
図13
ループの処理回数の実値と変数指定の違い
  • 最適化が有効な場合
  • 最初の値は変数で指定した時
  • 次が実値で指定した時

リスト7
  public
    { Public 宣言 }
    FLoopCnt   : Integer;
    FArrElemnt : Cardinal;
  end;

var
  Form1: TForm1;

implementation

uses
  MMSystem;

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Memo1.Lines.Clear;

  FLoopCnt   := 10;
  FArrElemnt := 200000000;
end;

//=============================================================================
//  配列操作のループに変数を使用
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LTimeStart : Cardinal;
  LTimeSpane : Cardinal;
  LIntArry   : array of Integer;
  LCount     : Integer;
  Lindex     : Integer;
begin
  SetLength(LIntArry, FArrElemnt);

  //配列操作のループに変数を使用
  LTimeStart := timeGetTime;
  for LCount := 0 to FLoopCnt do begin
    for Lindex := Low(LIntArry) to High(LIntArry) do begin
      LIntArry[Lindex] := Lindex;
    end;
  end;

  LTimeSpane := timeGetTime - LTimeStart;
  Memo1.Lines.Add(IntToStr(LTimeSpane) + ' ms');
end;

//=============================================================================
//  配列操作のループに変数ではなく実値を使用
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LTimeStart : Cardinal;
  LTimeSpane : Cardinal;
  LIntArry   : array of Integer;
  LCount     : Integer;
  Lindex     : Integer;
begin
  SetLength(LIntArry, FArrElemnt);

  LTimeStart := timeGetTime;
  for LCount := 0 to FLoopCnt do begin
    for Lindex := 0 to 200000000 - 1 do begin
      LIntArry[Lindex] := Lindex;
    end;
  end;

  LTimeSpane := timeGetTime - LTimeStart;
  Memo1.Lines.Add(IntToStr(LTimeSpane) + ' ms');
end;




05_配列の範囲のチェック方法


範囲チェックのコンパイラ指令を使用すると範囲のチェックができます.
以下は,本ページの最初のサンプルに範囲チェックのコンパイラ指令を設定して実行した結果です.
範囲チェックの機能は,デフォルトでは無効になっています.



図14
範囲チェックを有効した結果
  • 配列以外の範囲チェックも行う
図15
例外処理で独自のダイアログを表示

[プロジェクト] [オプション] のダイアログから,この範囲チェックを有効にすると,プロジェクト全体で範囲チェックを有効にすることができます.



[備考]
このようなコンパイルの動作仕様を変更するオプションを変更した場合,一般的にはプロジェクトのビルド(再構築) が必要です.[Shift] + [F9] でビルド (再構築) できます.
コンパイルのオブジョンをコンパイラ指定で指定する場合はビルド (再構築) は必要ありません.


図16
プロジェクトの範囲チェックの設定方法

リスト8
implementation

{$R *.dfm}

{$RANGECHECKS ON}

//=============================================================================
//  配列の設定した範囲外に値をセットする例
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LIntArry : array of Integer;
  Lindex   : Integer;
begin
  SetLength(LIntArry, 10);

  LIndex := 15;
  LIntArry[Lindex] := 123;
end;

//=============================================================================
//  配列の設定した範囲外の値を取得する例
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LIntArry : array of Integer;
  Lindex   : Integer;
  LValue   : Integer;
begin
  Memo1.Lines.Clear;

  SetLength(LIntArry, 10);
  try
    LIndex := 15;
    LValue := LIntArry[Lindex];
    Memo1.Lines.Add(IntToStr(LValue));
  except
    on E: ERangeError do begin
      MessageBox(Handle, PChar(E.ToString), '例外', MB_ICONINFORMATION);
    end;
  end;
end;