
C言語基礎|文字列の不正なコピー
「コピーできた」に油断すると危険!―文字列リテラルと領域不足が招く“静かな破壊”を防ごう。
文字列コピーは、C言語では日常茶飯事の処理ですよね。ところが、コピー先を1つ間違えるだけで、動いたり落ちたり、環境で挙動が変わったりします。
その原因はたいてい、次のどちらか(あるいは両方)です。
- 書き換えてはいけない場所に書き込んだ
- サイズが足りない場所に書き込んだ
この記事では「なぜ危ないのか」を、ポインタと記憶域の観点からしっかり押さえていきます。怖いのは、失敗がすぐ分からないこと。壊れた瞬間ではなく、少し後で落ちることも多いんです。
サンプルプログラム
短い固定メッセージを指すポインタへ、入力文字列をコピーするプログラム例です。
プロジェクト名:chap11-7-1 ソースファイル名:chap11-7-1.c
Visual Studio でこのプログラムを実行するには、SDLチェック設定を変更しておく必要があります。
1.プロジェクト名を右クリックして、「プロパティ」をクリックします。
2.「C/C++」→「全般」→「SDLチェック」を「いいえ」に切り替えて「OK」をクリックします。
#include <stdio.h>
// 文字列srcをdstへコピー(サイズ検査なし)
char *str_copy(char *dst, const char *src)
{
char *top = dst;
while (*dst++ = *src++) {
}
return top;
}
int main(void)
{
char *msg = "OK"; // 文字列リテラルを指す
char input[128];
printf("現在の表示:%s\n", msg);
printf("新しい表示文字を入力:");
scanf("%127s", input);
// 不正:msgが指す先(文字列リテラル)へコピーしようとする
str_copy(msg, input);
printf("更新後の表示:%s\n", msg);
return 0;
}このプログラムは、環境によって
- すぐ落ちる
- たまたま動いたように見える
- 後から別の場所の破壊で落ちる
など、正しい動作が保証されません。
なぜ正しく実行できないのか:2重の誤り
このプログラムが踏んでいる2つの地雷
| 誤り | やっていること | 何が問題? | よくある症状 |
|---|---|---|---|
| 1. 文字列リテラルを書き換え | msg が指す "OK" の領域へ書き込み | 書き換え可能かは処理系依存(不可が多い) | クラッシュ、保護違反 |
| 2. 空き領域でない場所まで書く可能性 | 短い領域に長い入力をコピー | 後ろが空いている保証がない | 他の変数破壊、挙動が不安定 |
誤り1:文字列リテラルを書きかえている
ポイントはここです。
- char *msg = "OK"; は、配列を用意しているわけではなく
「文字列リテラルの先頭を指すポインタ」を作っています。 - str_copy(msg, input); は、msg が指す先へ1文字ずつ代入します。
図:msg は「文字列の実体」ではなく「指しているだけ」
msg -----> "OK\0" (文字列リテラル)
↑ここへ書き込もうとしてしまう文字列リテラルの領域が書き換え可能かどうかは処理系依存です。
書き換え不可の環境では、ここで正しく動く保証はありません。
誤り2:領域外に書き込みうる(サイズ不足)
"OK" はナル文字を含めて 3バイトです。
でも input に "HELLOWORLD" のような長い文字列が入ったら、コピーは最後まで止まりません('\0' が来るまで進むからです)。
図:短い領域に長い文字列を突っ込むと、はみ出す
コピー先(短い): [ 'O' ][ 'K' ][ '\0' ]
コピー元(長い): [ 'H' ][ 'E' ][ 'L' ][ 'L' ][ 'O' ][ ... ][ '\0' ]
コピーが進むと…
[ 'H' ][ 'E' ][ 'L' ]
↑ ここで既に領域外「後ろが空いている保証はない」ので、運が悪いと
- 近くの変数が壊れる
- 管理情報が壊れて後で落ちる
- たまたま動いたように見える(これが一番危険)
という状態になります。
今回の核心:str_copy は「コピー先の容量」を知らない
str_copy の while は、次の意味です。
- *dst = *src を実行する(1文字コピー)
- その後 dst と src を1つ進める
- コピーした文字が '\0' なら終了、そうでなければ続行
つまり '\0' が来るまで無条件で書き続ける関数です。
だからこそ、呼び出し側は必ず、
- dst が書き込み可能な領域であること
- dst に十分なサイズがあること
を満たしている必要があります。今回の例は両方満たしていません。
登場する命令の書式と役割
scanf の書式
- 書式:scanf(書式文字列, 格納先, ...);
- 何をする命令?:標準入力から読み取って、指定されたメモリへ書き込む
今回の例:
- scanf("%127s", input);
・input へ最大127文字まで読み込む(最後の '\0' のための余裕を残す)
・input は配列なので input 自体が先頭要素へのポインタとして渡される(& は不要)
※scanf("%s", input) のように上限なしにすると、input 側が溢れる危険もあります。ここではそれも避けています。
printf の書式
- 書式:printf(書式文字列, 引数, ...);
- 何をする命令?:値を整形して表示する
- %s:ポインタが指す先を '\0' まで文字列として表示する
どう直すのが安全?(方向だけ押さえよう)
安全な形への置き換え
| やりたいこと | 危険なやり方 | 安全なやり方 |
|---|---|---|
| 書き換える文字列を持ちたい | char *msg = "OK"; をコピー先にする | char msg[128] = "OK"; のように配列で領域確保 |
| 長さが分からない入力をコピー | サイズ無視のコピー関数を使う | コピー先サイズを前提に扱う(長さ制限つき) |
ここで大事なのは「ポインタ変数にコピーしても安全になるわけではない」という点です。
安全なのは“十分な書き込み可能領域”を確保したときだけです。
