トラック8・🗃️配列

境界外アクセスという重大事故

読む目安 約14分・ゴール:境界外アクセスが未定義動作であることを理解し、危険性と防ぎ方を説明できる

アプリでこのトピックを学ぶ(無料・登録不要)

このトピックの要点

このトピックは、C言語を学ぶうえで最も重要な安全教育です。文法よりも先に、体に刻んでください。

int a[3] と宣言したのに a[3] や a[5] に触れる。これを境界外アクセスと呼びます。そしてCは、それを黙って通します。

#include <stdio.h>

int main(void)
{
    int a[3] = {10, 20, 30};
    int x = 99;

    a[3] = 777;                  // 配列の外!
    printf("x = %d\n", x);       // x = 777 になることがある
    return 0;
}

x は一度も書き換えていないのに、値が変わることがあります。配列のすぐ隣に置かれていた x の場所を、a[3] が踏んだからです。下のビジュアライザで、その瞬間を見てください。

🧪 メモリの図解:この図はアプリ内で1行ずつ動かせます。アプリでこのトピックを開く

なぜCは止めてくれないのか

多くの言語は、範囲外の添字を検出してエラーで止めてくれます。Cがそうしないのは、チェックの費用を払わないという設計思想だからです。要素にアクセスするたびに「範囲内か?」と確認すれば、その分だけ必ず遅くなります。Cは「プログラマが正しく書く」ことを前提に、その確認を省いて速度を取りました。OSや組込み機器がCで書かれてきた理由でもあり、同時に、事故がプログラマの責任になる理由でもあります。

いちばん怖いのは「すぐ落ちない」こと

境界外アクセスは未定義動作です。何が起きるかは決まっていません。

「動いたから正しい」がまったく通用しないのが、この事故の恐ろしさです。数か月後、まったく別の場所を修正した日に突然表面化します。しかも壊れるのは「はみ出した側」ではなく「踏まれた側」なので、調査は無関係な変数から始まります。原因にたどり着くまでの遠回りが、この事故の本当のコストです。

セキュリティの入口でもある

配列の外へ書き込める状態は、攻撃者から見れば「プログラムの記憶を書き換えられる穴」です。歴史上の深刻な脆弱性の多くが、このバッファオーバーフローに起因しています。第6部で正面から扱いますが、いまの段階では「配列の外に書くのは、鍵をかけ忘れた玄関と同じ」と覚えておいてください。

防ぎ方

  1. ループ条件は必ず i < n。<= を書いたら一度立ち止まる
  2. 要素数は #define などで1か所にまとめ、配列とループで同じ名前を使う
  3. 添字を計算で作るときは、その式が取りうる最小値と最大値を必ず確かめる
  4. 外から来た数値(入力・計算結果)を添字にする前に、if (i >= 0 && i < n) で確認する
  5. コンパイラの警告を必ず有効にする(多くの単純なはみ出しは検出できます)

とくに4番目が実戦では効きます。添字が変数で決まるときは、触る前に範囲を確かめる。それだけで多くの事故は防げます。

#include <stdio.h>

#define N 3

int main(void)
{
    int a[N] = {10, 20, 30};
    int i = 3;                     // 外から来た値のつもり

    if (i >= 0 && i < N) {         // 触る前に確認する
        printf("%d\n", a[i]);
    } else {
        printf("範囲外です\n");
    }
    return 0;
}
師範Cは止めてくれん。止めるのは、書き手の作法じゃ

まとめ: 境界外アクセスは未定義動作。落ちないことすらある。だからこそ「範囲は 0 〜 n−1」を、書くたびに確認する習慣が最大の防御になります。

アプリでこのトピックを学ぶ(無料・登録不要)

確認問題

確認問題 1

int a[3]; に対して a[5] = 1; を実行すると、どうなりますか。

  1. コンパイル時にエラーになり、実行できない
  2. 実行時に必ずエラーメッセージが出て停止する
  3. 何が起きるか決まっていない(未定義動作)。正常に動いてしまうこともある
  4. 自動的に配列が6要素に広がる
答えを見る

正解:C(何が起きるか決まっていない(未定義動作)。正常に動いてしまうこともある)

Cは境界チェックをしないので、多くの場合コンパイルも実行も通ってしまいます。隣の変数を壊す・時々しか症状が出ない・環境を変えると壊れる、といった形で後から牙をむくのが最も危険な点です。

確認問題 2

配列の全要素を表示したいのですが、最後に意味不明な数字が1つ余分に出ます。原因の行はどれですか。

1#include <stdio.h>
2
3int main(void)
4{
5    int a[5] = {1, 2, 3, 4, 5};
6
7    for (int i = 0; i <= 5; i++) {
8        printf("%d\n", a[i]);
9    }
10    return 0;
11}
答えを見る

バグのある行:7行目

ループ条件が i <= 5 なので、最後に存在しない a[5] を読んでいます。7行目を for (int i = 0; i < 5; i++) に直します。要素数が5なら添字は 0〜4 までです。

要素数と同じ値まで添字を進めてしまう「オフバイワンエラー」です。配列のループ条件は < が基本で、<= を書いたときは必ず立ち止まって確認しましょう。

「配列」の目次

  1. 配列とは——箱の行列
  2. 添字と0始まりの理由
  3. 配列とループの黄金コンビ
  4. 境界外アクセスという重大事故
  5. 配列の初期化いろいろ
  6. 多次元配列
  7. 配列を関数に渡す——入口編
  8. 集計・最大値・検索の定石

もっと先へ:ポインタ・メモリ・ファイル入出力・セキュアコーディングを含む全26トラックと、段位検定・模試のフルセットは完全版に収録しています。

完全版の販売ページは準備中です。

ほかのトラック

📚 姉妹教材:手を動かして覚える Linux 教材 — Linuxとインフラの仕組みを地図で学ぶ

Web版