コース目次 / 第3章
文字列とバッファ — C に境界チェックは無い
C文字列がヌル終端されたバイト列にすぎないこと、strcpyが渡された箱の大きさを一切知らないことを確認します。バッファのサイズと、書き込む長さの不一致が、この先の穴の芯だと理解します。
C言語には、他の多くの言語にある専用の文字列型がありません。「文字列」とは、char の配列で、末尾に 0(ヌル文字)が置かれたもの——それだけの約束事です。
文字列は、ただのバイト列+終端の目印
#include <stdio.h>
int main(void) {
char greeting[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
printf("%s\n", greeting); // "Hello" と表示される
printf("配列のサイズ: %zu\n", sizeof(greeting)); // 6(中身は5文字+終端)
return 0;
}
printf(“%s”, …) は、渡されたアドレスから、\0(値が0のバイト)に出会うまで、ひたすら読み続けます。「文字列の長さ」を別途どこかに持っているわけではありません——終端の目印を見つけるまで、読み続けるだけです。この「どこまで読むかは、目印を見つけるまで分からない」という設計が、次に見る穴の土台になります。
strcpy は、箱の大きさを、一切知らない
C言語の代表的な文字列コピー関数 strcpy(コピー先, コピー元) を見ます。
char buf[8];
strcpy(buf, "こんにちは"); // buf は何バイト? コピー元は何バイト?
strcpy がすることは、たった一つ——コピー元を、\0 に出会うまでコピー先に書き写す。これだけです。buf が何バイト確保されているかは、strcpy にはまったく分かりません。関数の引数には、コピー先の「アドレス」しか渡っておらず、「そこに何バイト書いてよいか」という情報は、どこにも含まれていないからです。
もし、コピー元がコピー先より長かったら?
#include <stdio.h>
#include <string.h>
int main(void) {
char buf[8]; // 8バイトの箱
strcpy(buf, "AAAAAAAAAAAAAAAAAAAAAAAA"); // 24文字+終端を、8バイトの箱に
printf("buf: %s\n", buf);
return 0;
}
これをコンパイルすると、gcc は親切にも警告を出します。
warning: '__builtin_memcpy' writing 25 bytes into a region of size 8
overflows the destination [-Wstringop-overflow=]
コンパイラは「8バイトの箱に25バイト書き込もうとしている」と、静的解析で気づいて警告してくれます。でも、これは警告であって、エラーではありません——コンパイルは通り、実行ファイルはできてしまいます。gcc の警告は親切な助け船ですが、すべての危険なケースを見抜けるわけではありません(次の章で見るように、動的に決まる長さの入力では、コンパイラは見抜けません)。
バッファのサイズと、書き込む長さの不一致
この章の結論を、一言に畳みます。
「箱の大きさ」と「書き込む長さ」が食い違ったとき、C言語は何も守ってくれません。境界チェックは、プログラマが自分で書かない限り、存在しない。strcpy の代わりに、コピー先のサイズを指定できる strncpy のような関数もありますが、それも正しく使わなければ同じ穴が空きます。
sec-injection で見た「データとコードを混ぜない」と、根っこは似ています——C言語は、あなたが渡したものを、あなたが指示したとおりに実行するだけで、意図と食い違っていないかは確認しません。
持ち帰る一言
C文字列は、終端の目印があるだけのバイト列。strcpyは、箱の大きさを知らない。 コピー先のサイズと、書き込む長さが食い違えば、何の防御もなくあふれます。次の章で、そのあふれが実際に何を壊すのかを、手を動かして観察します。
こうなっていればOK
卒業まであと2章です。
この章はまだ完了していません。
保存できませんでした(プライベートブラウズ中かもしれません)。この端末に進捗は残りませんが、先へは進めます。