C LANGUAGE TECHNOLOGY

【C言語】gets関数が廃止された理由と代替関数

悩んでいる人

C言語でgets関数が廃止された理由を教えて!

こういった悩みにお答えします.

本記事の信頼性

  • リアルタイムシステムの研究歴12年.
  • 東大教員の時に,英語でOSの授業.
  • 2012年9月~2013年8月にアメリカのノースカロライナ大学チャペルヒル校コンピュータサイエンス学部2021年の世界大学学術ランキングで20位)で客員研究員として勤務.C言語でリアルタイムLinuxの研究開発
  • プログラミング歴15年以上,習得している言語: C/C++,Java,Python,Ruby,HTML/CSS/JS/PHP,MATLAB,Assembler (x64,ARM).
  • 東大教員の時に,C++言語で開発した「LLVMコンパイラの拡張」,C言語で開発した独自のリアルタイムOS「Mcube Kernel」GitHubにオープンソースとして公開

こういった私から学べます.

gets関数が廃止された理由

C言語でgets関数が廃止された理由は,バッファオーバーフロー(バッファオーバーラン)を防ぐことができないという致命的な脆弱性があるからです.

バッファオーバーフローとは,用意されたバッファよりも大きなサイズのデータをバッファに書き込む事で,データがバッファ境界からあふれ,バッファの外側のメモリを上書きしてしまう事により,元々そのメモリにあったデータを破壊してしまうことです.

上記のgets関数の宣言では,引数に格納するバッファのアドレスsを指定していますが,データサイズの上限を指定していないことに注意して下さい.

このような理由により,gets関数はC11の規格では廃止されましたが,どのようにバッファオーバーフローが発生するのかを解説していきます.

gets関数を利用したgets.cは以下になります.

11行目で定義したchar型の配列sのサイズはBUFSIZE(値は16)なので,'\0'文字を除くと15文字分の文字を格納できます.

つまり,用意されたバッファのサイズは15で,15より多い文字をバッファに書き込もうとするとバッファオーバーフローが発生します.

上記のgets.cをgccでコンパイルすると以下の警告がでます.コンパイラからgets関数は使うべきではないことを注意されましたね.

最初の実行例は以下になります.2行目で「abc」と入力しています.

正しく実行できているように見えますが,入力する文字数が3と上限の文字数の15以下なので,たまたまうまくいっただけです.

次の実行例は以下になります.2行目で「abcdefghijklmnopqrstuvwxyz」と入力しています.

「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関数の終わりでバッファオーバーフローが発生していることがわかります.

gets関数の代替関数

gets関数の代替関数を紹介していきます.

以下の4つの関数はそれぞれ特徴があるので,1つずつ丁寧に解説します.

  • fgets関数
  • getline関数
  • gets_s関数
  • scanf_s関数

fgets関数

fgets関数は,streamから最大でsize - 1個の文字を読み込み, sが指すバッファに格納する関数です.

バッファのサイズより多くの読み込みがあった場合,バッファオーバーフローの防止のため,最大個数を読み込んだ後に関数から戻ります.

読み込みはEOFまたは改行文字を読み込んだ後で停止します.

読み込まれた改行文字はバッファに格納され, 終端の'\0'が1つバッファの中の最後の文字の後に書き込まれます.

fgets関数で代替したコードは以下になります.

fgets関数でsize - 1個より多くの文字を読み込みたい場合は複数回呼び出します.

なので,以下のようにwhile文の式で使う場合が多いです.

fgets関数でコマンドラインから読み込む時に改行文字を入力しますが,この改行文字('\n')もバッファの中に入ることに注意して下さい.

fgets.cの14行目のprintf関数の引数として渡している文字列内の改行は1個ですが,以下の4~5行目のprintf関数の出力処理では,改行が2個(4~5行目の改行)出力されています.

つまり,「abc」と入力した後に改行文字を入力すると「abc\n」をバッファに格納します.

「abcdefghijklmnopqrstuvwxyz」を入力すると15文字の「abcdefghijklmno」と11文字の「pqrstuvwxyz」の2回に分けて読み込まれることに注意して下さい.

getline関数

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が指すバッファのメモリを解放していることに注意して下さい.

fgets関数と同様に,getline関数では「abc」と入力した後に改行文字を入力すると「abc\n」をバッファに格納します.

getline関数では「abcdefghijklmnopqrstuvwxyz」と入力した後に改行文字を入力すると「abcdefghijklmnopqrstuvwxyz\n」をバッファに格納します.

fgets関数はバッファのサイズより多い入力がある場合は上限で読み込みを終了しましたが,getline関数は動的にメモリを確保するので1度で全部読み込んでいることがわかります.

gets_s関数

gets_s関数は,C11の規格で実装されたgets関数の代替関数です.

gets_s関数の特徴は以下になります.

  • 行を返す前に改行文字を'\0'で置換すること
  • バッファのサイズより多い入力を受け取った場合,実行時制約への違反となること(強制終了)

gets_s関数の実装はC11の規格のオプションなので,GCC/Clangでは利用できないことに注意して下さい.(GCC 10.2.0/Clang 10.0.0で確認)

Visual Studioではgets_s関数を利用できるのでWindows 10で実行します.

gets_s関数で代替したコードは以下になります.

gets_s関数はgets関数とほぼ同じで,違いは第2引数にBUFSIZEを指定することです.

Visual Studioにおける実行結果を説明します.

「abc」(1行目)を入力する場合,以下にのようにコード0(main関数の戻り値が0)で正常終了します.

fgets関数の実行結果とは異なり,改行は1個しか出力されないことに注意して下さい.

「abcdefghijklmnopqrstuvwxyz」を入力すると,バッファオーバーフローの防止処理によりコード3で強制終了します.

scanf_s関数

scanf_s関数は,stdinからの入力を読み込む書式付き入力変換関数です.

scanf関数と異なり,scanf_s関数は入力文字数を制限できます.

scanf_s関数はバッファのサイズより多い入力を受け取った場合,バッファオーバーフローを防止するため空文字列になります.

scanf_s関数で代替したコードは以下になります.

gets_s関数と同様に,scanf_s関数はGCC/Clangでは利用できないので,Windows 10上のVisual Studioで実行します.

Visual Studioにおける実行結果を説明します.

「abc」(1行目)を入力する場合,以下のようにコード0で正常終了します.

「abcdefghijklmnopqrstuvwxyz」(1行目)を入力する実行は以下になります.

scanf_s関数の読み込みはバッファオーバーフローの防止のため空文字列になるので,2行目の「s = 」を出力した後にコード0で正常終了していることがわかります.

まとめ

C言語でgets関数が廃止された理由を説明しました.

gets関数は絶対に使ってはいけないので,代替関数のfgets関数,getline関数,gets_s関数,scanf_s関数を使いましょう.

これらの関数の違いは下表になります.

 fgets関数getline関数gets_s関数scanf_s関数
GCC/Clang
Visual Studio
バッファオーバーフロー
の防止方法
サイズの上限で
読み込み終了.
残りを読み込む場合は
再度呼び出し.
動的にメモリを確保.強制終了.入力が空文字列.
読み込まれた改行文字格納格納'\0'文字で置換未格納

C言語を独学で習得することは難しいです.

私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.

友だち追加

独学が難しいあなたは,C言語を学べるおすすめのオンラインプログラミングスクール3社で自分に合うスクールを見つけましょう.

-C LANGUAGE, TECHNOLOGY
-, , , , , , ,