C言語

【重要】C言語で安全なプログラムを書くテクニック

この記事で分かること

  • C言語の配列にはサイズ情報が無い
  • バッファオーバーフローしない安全なプログラムを書く方法
  • sizeof / _countof の使い方や動作原理について

この記事では、C言語の 配列に関する重要な特性 について解説します。

配列のサイズに関する挙動を正確に理解することは、C言語を理解することにおいて最も重要な点と言っても良いと思います。
特に他の言語からプログラミングを覚えた方にとっては直感的に分かりにくい仕様であるため、その違いについてよく理解する必要があります。

配列サイズの理解が足りないため引き起こされる、バッファオーバーフロー(バッファオーバーラン)は非常に多い不具合なので、この機会にしっかり理解して回避するようにしましょう!

ゴイチ

今回は地味ながら非常に重要な知識になります!

結論

C言語の配列関連で安全なプログラムを書くには、以下のテクニックを使用します。

  • 配列を関数に渡す場合、必ず配列のサイズも一緒に渡すようにする
  • 配列を受けとった関数側では、処理の実行前に配列のサイズのチェックをする

なぜこのような面倒なことをしなければならないのかは、次章以降で詳しく解説します。

配列自身は自分の大きさを知らない

現在主流になっているほぼ全ての言語では、配列(オブジェクト)自身が自分のサイズを知っていて、それを返すメソッドを持っています。
例えば、以下のような形式で配列のサイズを取得することができます。

array.length
array.size()

これは配列自身が自分のサイズを知っているからできることです。

しかし、50年以上も前(1972年)に作られたC言語には配列自身にサイズの情報が無く、自分の大きさを返すメソッドがありませんでした。
そのことが原因で、配列で確保している領域以上のデータをメモリに書き込むことができてしまい、深刻なセキュリティの不具合を多く生むことになりました。
そのため、C言語は徐々に扱いが難しい危険な言語と見なされるようになりました。

配列自身がサイズを持っていない言語でどのようにバッファーオーバーフローを回避していくのか、そのテクニックを知ることがC言語において安全なプログラムを書くカギになります。

配列サイズの取得方法

C言語で配列のサイズを取得するには、sizeof演算子を使う必要がありますが、sizeofで取得できるのはメモリ上でのByte長になります。

配列の要素数を取得するには、以下の計算を行います。

int array_size = sizeof(array) / sizeof(array[0]);

ただし、MicrosoftのVisual C/C++で開発をしている場合、配列の要素数を取得できる便利なマクロがすでに定義されています。
以下にマクロの内容を紹介します。

#define _countof(array) (sizeof(array) / sizeof(array[0]))

_countof マクロが利用できる場合、簡単に配列の要素数を求めることができます。(使用する場合は、stdlib.h をincludeする)

このようなマクロが定義されていない場合、以下のようなマクロを自分で定義してみても良いでしょう。

#define COUNTOF(array) (sizeof(array) / sizeof(array[0]))

サンプルコード

配列を関数に渡した時の挙動、及び sizeof / _countof 演算子の動きが分かるようなサンプルコードを書いてみました。

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

void func(int array[], size_t array_size);

int main() {
	int array[128];
	printf("main() size  of array: %zd\n", sizeof(array));
	printf("main() count of array: %zd\n", _countof(array));
	printf("main() address of array: 0x%016llx\n", array);

	func(array, _countof(array));

	return 0;
}

void func(int array[], size_t array_size) {
	// 配列サイズのチェック
	if (array_size < 128) {
		return;
	}

	printf("func() size of array: %zd\n", sizeof(array));
	printf("func() parameter array_size: %zd\n", array_size);
	printf("func() address of array: 0x%016llx\n", array);
}
// 出力結果
main() size  of array: 512
main() count of array: 128
main() address of array: 0x0000006b004ff9e0
func() size of array: 8
func() parameter array_size: 128
func() address of array: 0x0000006b004ff9e0

この例では、array[128] というローカル変数を定義して、main関数とfunc関数それぞれで配列サイズとアドレスを表示しています。
出力結果から、main関数とfunc関数で出力されているアドレスは同じ値なので、どちらも同じ変数を指しています。
しかし、sizeofで取得したサイズはそれぞれで異なっていることが分かります。

sizeof演算子とは、Wikipediaにも記載がある通り「原則としてコンパイル時に計算される演算子」であり、実行時にサイズを求めている訳ではありません。

上記の例では、main関数の中でローカル変数が定義されているため、main関数内ではsizeofで正確なサイズが表示できますが、func関数内ではコンパイル時点ではどのような大きさの配列が渡されるか分からないため、引数(ポインタ)の大きさである 8を表示してしまっています。(ポインタのサイズはCPUによって変わります)
このような sizeofの使い方は不正であり、原因が掴みにくい不具合の元になります。

そのため、関数側に配列を渡す場合は、呼び出し側から配列のサイズも一緒に渡すように実装するのが鉄則です。(例外はありません

ゴイチ

面倒ですがC言語ではこのようにするしかないです。

関数側では配列を渡されたら、必ず処理の前に配列のサイズが十分であるかをチェックしてから処理を開始する必要があります。

C言語は確保されている以上のメモリ領域に書き込んだり、読み出したりしても何も警告が出ないため、意図せずメモリを破壊したり、無関係なデータを読んでしまうリスクがあります。
そのリスクを回避するために、処理の前には必ず配列サイズのチェックを行う必要があります。

安全なC言語のプログラムを書くのに絶対に必要なことなので、ぜひテクニックとして覚えて下さい。

まとめ

最後に、この記事の内容をまとめます。

要点まとめ

  • C言語の配列は自分自身のサイズを知らない
  • sizeof演算子で配列のサイズを取得できるが、求めることができるのはコンパイル時にサイズが分かる場合だけ
  • 配列を関数に渡す際は、配列と配列のサイズの両方を渡すようにする

C言語初学者にとって、ハマりやすいポイントとして配列にまつわる仕様について紹介しました。
安全なプログラムを書くという目的において非常に重要な知識になりますので、初級者から中級者へのステップアップとしてぜひ覚えて下さい!

結局のところ、配列自身がサイズを持っていないことがすべての原因ですので、そのことをしっかりと理解していればメモリにまつわる不具合は回避できるのではないかと思います。

ゴイチ

それでは、また他の記事でお会いしましょう!

この記事は役に立ちましたか?

  • この記事を書いた人
アバター画像

ゴイチ

ソフトウェアエンジニア歴20年。 C/C++, C#, Java, Kotlinが得意で、組込系・スマホ・大規模なWebサービスなど幅広いプログラミング経験があります。 現在は某SNSの会社でWebエンジニアをしています。

-C言語
-,