C言語でgets関数が廃止された理由を教えて!
こういった悩みにお答えします.
本記事の信頼性
- リアルタイムシステムの研究歴12年.
- 東大教員の時に,英語でOS(Linuxカーネル)の授業.
- 2012年9月~2013年8月にアメリカのノースカロライナ大学チャペルヒル校(UNC)コンピュータサイエンス学部で客員研究員として勤務.C言語でリアルタイムLinuxの研究開発.
- プログラミング歴15年以上,習得している言語: C/C++,Python,Solidity/Vyper,Java,Ruby,Go,Rust,D,HTML/CSS/JS/PHP,MATLAB,Verse(UEFN), Assembler (x64,ARM).
- 東大教員の時に,C++言語で開発した「LLVMコンパイラの拡張」,C言語で開発した独自のリアルタイムOS「Mcube Kernel」をGitHubにオープンソースとして公開.
- 2020年1月~現在はアメリカのノースカロライナ州チャペルヒルにあるGuarantee Happiness LLCのCTOとしてECサイト開発やWeb/SNSマーケティングの業務.2022年6月~現在はアメリカのノースカロライナ州チャペルヒルにあるJapanese Tar Heel, Inc.のCEO兼CTO.
- 最近は自然言語処理AIとイーサリアムに関する有益な情報発信に従事.
- (AI全般を含む)自然言語処理AIの論文の日本語訳や,AIチャットボット(ChatGPT,Auto-GPT,Gemini(旧Bard)など)の記事を50本以上執筆.アメリカのサンフランシスコ(広義のシリコンバレー)の会社でプロンプトエンジニア・マネージャー・Quality Assurance(QA)の業務委託の経験あり.
- (スマートコントラクトのプログラミングを含む)イーサリアムや仮想通貨全般の記事を200本以上執筆.イギリスのロンドンの会社で仮想通貨の英語の記事を日本語に翻訳する業務委託の経験あり.
こういった私から学べます.
C言語を独学で習得することは難しいです.
私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.
私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!
独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!
目次
gets関数が廃止された理由
1 |
char *gets(char *s); |
C言語でgets関数が廃止された理由は,バッファオーバーフロー(バッファオーバーラン)を防ぐことができないという致命的な脆弱性があるからです.
バッファオーバーフローとは,用意されたバッファよりも大きなサイズのデータをバッファに書き込む事で,データがバッファ境界からあふれ,バッファの外側のメモリを上書きしてしまう事により,元々そのメモリにあったデータを破壊してしまうことです.
上記のgets関数の宣言では,引数に格納するバッファのアドレスsを指定していますが,データサイズの上限を指定していないことに注意して下さい.
このような理由により,gets関数はC11の規格では廃止されましたが,どのようにバッファオーバーフローが発生するのかを解説していきます.
gets関数を利用したgets.cは以下になります.
11行目で定義したchar型の配列sのサイズはBUFSIZE(値は16)なので,'\0'文字を除くと15文字分の文字を格納できます.
つまり,用意されたバッファのサイズは15で,15より多い文字をバッファに書き込もうとするとバッファオーバーフローが発生します.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #define BUFSIZE 16 int main(void) { char s[BUFSIZE]; gets(s); printf("s = %s\n", s); return 0; } |
上記のgets.cをgccでコンパイルすると以下の警告がでます.コンパイラからgets関数は使うべきではないことを注意されましたね.
1 2 3 4 5 6 7 8 |
$ gcc gets.c gets.c: In function 'main': gets.c:13:3: warning: implicit declaration of function 'gets'; did you mean 'fgets'? [-Wimplicit-function-declaration] 13 | gets(s); | ^~~~ | fgets /usr/bin/ld: /tmp/cchh29QN.o: in function `main': gets.c:(.text+0x28): warning: the `gets' function is dangerous and should not be used. |
最初の実行例は以下になります.2行目で「abc」と入力しています.
正しく実行できているように見えますが,入力する文字数が3と上限の文字数の15以下なので,たまたまうまくいっただけです.
1 2 3 |
$ a.out abc s = abc |
次の実行例は以下になります.2行目で「abcdefghijklmnopqrstuvwxyz」と入力しています.
1 2 3 4 |
$ a.out abcdefghijklmnopqrstuvwxyz s = abcdefghijklmnopqrstuvwxyz *** stack smashing detected ***: terminated |
「abcdefghijklmnopqrstuvwxyz」は入力文字数が26と上限の文字数の15より多いので,バッファオーバーフローが発生しました.
その結果,4行目で「*** stack smashing detected ***: terminated」とOS側でエラー出力されました.
ここで,gets関数を呼び出した後の15行目のprintf関数の出力「s = abcdefghijklmnopqrstuvwxyz」が正常に動作しているのは興味深いですよね.
実は,gets関数でmain関数内のスタック領域が破壊されたことが原因で,17行目のmain関数の終わりでエラーが発生しているからです.
アセンブリ言語レベルで説明すると,main関数の最初に保存したスタックポインタやリターンアドレス等を復帰する時です.
gets.cをgdbで実行すると以下の36行目の「main () at gets.c:17」と表示されるように,gets.cの17行目のmain関数の終わりでバッファオーバーフローが発生していることがわかります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
$ gdb a.out GNU gdb (Ubuntu 12.0.90-0ubuntu1) 12.0.90 Copyright (C) 2022 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <https://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from a.out... (gdb) run Starting program: /home/chishiro/c-language/gets/a.out [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". abcdefghijklmnopqrstuvwxyz s = abcdefghijklmnopqrstuvwxyz *** stack smashing detected ***: terminated Program received signal SIGABRT, Aborted. __pthread_kill_implementation (no_tid=0, signo=6, threadid=140737351513920) at ./nptl/pthread_kill.c:44 44 ./nptl/pthread_kill.c: No such file or directory. (gdb) |
gets関数の代替関数
gets関数の代替関数を紹介していきます.
以下の4つの関数はそれぞれ特徴があるので,1つずつ丁寧に解説します.
- fgets関数
- getline関数
- gets_s関数
- scanf_s関数
fgets関数
1 |
char *fgets(char *s, int size, FILE *stream); |
fgets関数は,streamから最大でsize - 1個の文字を読み込み, sが指すバッファに格納する関数です.
バッファのサイズより多くの読み込みがあった場合,バッファオーバーフローの防止のため,最大個数を読み込んだ後に関数から返ります.
読み込みはEOFまたは改行文字を読み込んだ後で停止します.
読み込まれた改行文字はバッファに格納され, 終端の'\0'が1つバッファの中の最後の文字の後に書き込まれます.
fgets関数で代替したコードは以下になります.
fgets関数でsize - 1個より多くの文字を読み込みたい場合は複数回呼び出します.
なので,以下のようにwhile文の式で使う場合が多いです.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #define BUFSIZE 16 int main(void) { char s[BUFSIZE]; while (fgets(s, BUFSIZE, stdin)) { printf("s = %s\n", s); } return 0; } |
fgets関数でコマンドラインから読み込む時に改行文字を入力しますが,この改行文字('\n')もバッファの中に入ることに注意して下さい.
fgets.cの14行目のprintf関数の引数として渡している文字列内の改行は1個ですが,以下の4~5行目のprintf関数の出力処理では,改行が2個(4~5行目の改行)出力されています.
つまり,「abc」と入力した後に改行文字を入力すると「abc\n」をバッファに格納します.
1 2 3 4 5 6 7 |
$ gcc fgets.c $ a.out abc s = abc ^D(Ctrl + D:LinuxのEOFを入力) $ |
「abcdefghijklmnopqrstuvwxyz」を入力すると15文字の「abcdefghijklmno」と11文字の「pqrstuvwxyz」の2回に分けて読み込まれることに注意して下さい.
1 2 3 4 5 6 7 8 |
$ gcc fgets.c $ a.out abcdefghijklmnopqrstuvwxyz s = abcdefghijklmno s = pqrstuvwxyz ^D(Ctrl + D:LinuxのEOFを入力) $ |
getline関数
1 |
ssize_t getline(char **lineptr, size_t *n, FILE *stream); |
getline関数は,streamから1行全てを読み込み,テキストが含まれているバッファのアドレスを*lineptrに格納します.バッファは'\0'で終端され,改行文字が見つかった場合は改行文字もバッファに格納されます.
ここで,getline関数はGNUによる拡張でC準拠ではないことに注意して下さい.
getline関数はGCC/Clangで利用できますが,Visual Studioで利用できません.
getline関数は,*lineptrにNULL,*nが呼び出し前に0に設定されていた場合,バッファのメモリを動的に確保します.
つまり,内部的にmalloc関数とrealloc関数を呼び出すことにより,バッファオーバーフローを防止します.
プログラムの終了前にfree関数で確保したメモリを解放することを忘れないように注意して下さい.
getline関数で代替したコードは以下になります.
18行目のfree関数でsが指すバッファのメモリを解放していることに注意して下さい.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #include <stdlib.h> int main(void) { char *s = NULL; size_t len = 0; getline(&s, &len, stdin); printf("s = %s\n", s); if (s) { free(s); } return 0; } |
fgets関数と同様に,getline関数では「abc」と入力した後に改行文字を入力すると「abc\n」をバッファに格納します.
1 2 3 4 5 6 |
$ gcc getline.c $ a.out abc s = abc $ |
getline関数では「abcdefghijklmnopqrstuvwxyz」と入力した後に改行文字を入力すると「abcdefghijklmnopqrstuvwxyz\n」をバッファに格納します.
fgets関数はバッファのサイズより多い入力がある場合は上限で読み込みを終了しましたが,getline関数は動的にメモリを確保するので1度で全部読み込んでいることがわかります.
1 2 3 4 5 6 |
$ gcc getline.c $ a.out abcdefghijklmnopqrstuvwxyz s = abcdefghijklmnopqrstuvwxyz $ |
gets_s関数
1 |
char *gets_s(char *buffer, size_t sizeInCharacters); |
gets_s関数は,C11の規格で実装されたgets関数の代替関数です.
gets_s関数の特徴は以下になります.
- 行を返す前に改行文字を'\0'で置換すること
- バッファのサイズより多い入力を受け取った場合,実行時制約への違反となること(強制終了)
gets_s関数の実装はC11の規格のオプションなので,GCC/Clangでは利用できないことに注意して下さい.(GCC 10.2.0/Clang 10.0.0で確認)
Visual Studioではgets_s関数を利用できますので,こちらで実行します.
gets_s関数で代替したコードは以下になります.
gets_s関数はgets関数とほぼ同じで,違いは第2引数にBUFSIZEを指定することです.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #define BUFSIZE 16 int main(void) { char s[BUFSIZE]; gets_s(s, BUFSIZE); printf("s = %s\n", s); return 0; } |
Visual Studioにおける実行結果を説明します.
「abc」(1行目)を入力する場合,以下にのようにコード0(main関数の返り値が0)で正常終了します.
fgets関数の実行結果とは異なり,改行は1個しか出力されないことに注意して下さい.
1 2 3 4 5 |
abc s = abc *.exe (プロセス *) は、コード 0 で終了し ました。 このウィンドウを閉じるには、任意のキーを押してください... |
「abcdefghijklmnopqrstuvwxyz」を入力すると,バッファオーバーフローの防止処理によりコード3で強制終了します.
1 2 3 4 |
abcdefghijklmnopqrstuvwxyz *.exe (プロセス *) は、コード 3 で終了し ました。 このウィンドウを閉じるには、任意のキーを押してください... |
scanf_s関数
1 |
int scanf_s(const char *format [, argument]...); |
scanf_s関数は,stdinからの入力を読み込む書式付き入力変換関数です.
scanf関数と異なり,scanf_s関数は入力文字数を制限できます.
scanf_s関数はバッファのサイズより多い入力を受け取った場合,バッファオーバーフローを防止するため空文字列になります.
scanf_s関数で代替したコードは以下になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #define BUFSIZE 16 int main(void) { char s[BUFSIZE]; scanf_s("%s", s, BUFSIZE); printf("s = %s\n", s); return 0; } |
gets_s関数と同様に,scanf_s関数はGCC/Clangでは利用できないので,Visual Studioを利用します.
Visual Studioにおける実行結果を説明します.
「abc」(1行目)を入力する場合,以下のようにコード0で正常終了します.
1 2 3 4 5 |
abc s = abc *.exe (プロセス *) は、コード 0 で終了しました。 このウィンドウを閉じるには、任意のキーを押してください... |
「abcdefghijklmnopqrstuvwxyz」(1行目)を入力する実行は以下になります.
scanf_s関数の読み込みはバッファオーバーフローの防止のため空文字列になるので,2行目の「s = 」を出力した後にコード0で正常終了していることがわかります.
1 2 3 4 5 |
abcdefghijklmnopqrstuvwxyz s = *.exe (プロセス *) は、コード 0 で終了し ました。 このウィンドウを閉じるには、任意のキーを押してください... |
まとめ
C言語でgets関数が廃止された理由を説明しました.
gets関数は絶対に使ってはいけないので,代替関数のfgets関数,getline関数,gets_s関数,scanf_s関数を使いましょう.
これらの関数の違いは下表になります.
fgets関数 | getline関数 | gets_s関数 | scanf_s関数 | |
---|---|---|---|---|
GCC/Clang | ✓ | ✓ | ||
Visual Studio | ✓ | ✓ | ✓ | |
バッファオーバーフロー の防止方法 | サイズの上限で 読み込み終了. 残りを読み込む場合は 再度呼び出し. | 動的にメモリを確保. | 強制終了. | 入力が空文字列. |
読み込まれた改行文字 | 格納 | 格納 | '\0'文字で置換 | 未格納 |
gets関数を利用したコードインジェクションを知りたいあなたはこちらからどうぞ.
C言語を独学で習得することは難しいです.
私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.
私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!
独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!