C LANGUAGE TECHNOLOGY

【C言語】算術オーバーフローと回避方法

悩んでいる人

C言語で算術オーバーフローと回避方法を教えて!

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

本記事の信頼性

  • リアルタイムシステムの研究歴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にオープンソースとして公開

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

算術オーバーフロー

算術オーバーフロー(オーバーフロー)とは,算術演算の結果がデータ型の範囲を超える(最大値より大きい,もしくは最小値より小さい)場合に発生する事象のことです.

算術オーバーフローが発生すると,正常な整数演算の結果にならず,バグが発生してしまいます.

また,最大値より大きくなるオーバーフローを正のオーバーフロー,最小値より小さくなるオーバーフローを負のオーバーフローと呼びます.

算術オーバーフローには,整数オーバーフローと浮動小数点数オーバーフローがあります.

算術オーバーフローが発生したかどうかチェックするためには,整数と浮動小数点数の最小値と最大値を取得する必要があります.

C言語で処理系依存の整数と浮動小数点数の最小値と最大値の取得方法を知りたいあなたは,こちらからどうぞ.

【C言語】処理系依存 最小値と最大値
【C言語】処理系依存の整数と浮動小数点数の最小値と最大値の取得

こういった悩みにお答えします. こういった私から学べます. 目次1 処理系依存の整数と浮動小数点数の最小値と最大値の取得2 処理系依存の整数の最小値と最大値の取得3 処理系依存の浮動小数点数の最小値と ...

続きを見る

算術オーバーフローを理解するためには,キャスト演算子による明示的な型変換と,暗黙的な型変換を知っていることが前提になります.

まだ理解していないあなたはこちらからどうぞ.

C言語 キャスト
【C言語】キャスト演算子による明示的な型変換【暗黙的な型変換も紹介】

こういった悩みにお答えします. こういった私から学べます. 目次1 キャスト演算子【明示的な型変換】1.1 キャスト演算子でオーバーフローの回避1.2 キャスト演算子で汎用ポインタ型(void *)か ...

続きを見る

算術オーバーフローの回避方法や,算術オーバーフローに関連する以下の内容も解説します.

  • C言語で符号ありデータ型を使う理由
  • 符号エラー
  • 切り捨てエラー
  • 不完全な範囲チェック

整数オーバーフロー

整数オーバーフローとは,算術オーバーフローの整数の場合の事象です.

整数オーバーフローの例は以下になります.

13行目で正のオーバーフロー,15行目で負のオーバーフロー,17行目で正のオーバーフローが発生します.

実行結果は以下になります.

C言語で符号ありデータ型を使う理由

C言語では本来必要がないはずの箇所でも符号ありデータ型が使われています.

この理由として,負の数をマジックナンバーとして利用する習慣があるからです.

負の数はエラーを表現する方法として利用されています.

例えば,printf関数の戻り値の型はint型で,戻り値の内容は書き込まれた文字数です.

文字数は非負の整数として表現できるので,本来はint型ではなくunsigned int型が適切だと思いますよね.

しかし,printf関数でエラーが発生した場合,そのエラーを伝えるための戻り値を設定したくても非負の整数を表すunsigned int型では正常な戻り値と区別できません.

なので,printf関数の戻り値が負の数の場合にエラーを返すと定義するため,int型が使われています.

符号エラー

符号エラーとは,符号あり整数型から符号なし整数型への変換で,符号の意味が失われるエラーのことです.

符号エラーの例は以下になります.

12行目のsigned char型の値をunsigned short型に代入する際,符号拡張が発生することに注意して下さい.

実行結果は以下になります.

切り捨てエラー

切り捨てエラーとは,より小さい整数型への変換し,値が小さい方に収まらない場合に発生するエラーです.

切り捨てにより上位ビットの情報が欠損するので注意して下さい.

切り捨てエラーの例は以下になります.

12行目で,512は2進数で表現すると「1000000000」となり,char型が8ビットの場合は切り捨てエラーが発生して0になってしまいます.

不完全な範囲チェック

不完全な範囲チェックによりバグが発生します.

不完全な範囲チェックの例は以下になります.

パット見は正しく実装しているようにみえますが...

実行結果は以下になります.

3行目の行末で3,4行目の行末で4を入力したら,8行目で「table[3] = 4」と正しく値が格納されていることがわかります.

また,16行目の行末で10,17行目の行末で123と入力したら,18行目で「Error: 10 is out of range.」と正しく範囲チェックができていることがわかります.

このコードは正しく動作するのでしょうか.

それでは,以下のようにindexに負の値を入力してみましょう.

3行目の行末で-3,4行目の行末で-4を入力したら正しく範囲チェックがされず,もちろん正しくtableに値が格納されませんでした.

上記の範囲チェックもれが発生した原因は,12行目のif文でindexが負の値を条件に入れていないためです.

なので,以下のように修正すれば正しく動作します.

※配列の添字が符号ありのint型なので,符号なしのsize_t型に変更する方法でも正しく範囲チェックできます.

実行結果は以下になります.

正しく範囲チェックができていることがわかります.

整数オーバーフローのチェック方法

整数オーバーフローのチェック方法を紹介します.

具体的には,整数の四則演算について,オーバーフローのチェック方法をまとめていきます.

a + b(一般的にはa op b)の形の演算を考えます.

オーバーフローのチェックのための事前条件と事後条件を解説していきます.

  • 事前条件:aとbがどのような条件を満たす時にオーバーフローなのかを演算の前にチェック
  • 事後条件:aとbがどのような条件を満たす時にオーバーフローなのかを演算の後にチェック

足し算と引き算のオーバーフローのチェック方法

足し算と引き算のオーバーフローのチェック方法を紹介します.

足し算と引き算は,算術演算やポインタ演算,インクリメントやデクリメントで利用されます.

足し算の事前条件

足し算の事前条件によるオーバーフローのチェックでは,a + bというint型の演算を考えます.(int型より大きい型でも同様です.)

符号なしの場合は以下のようにチェックします.

  • UINT_MAX < a + b
  • UINT_MAX - a < bならばオーバーフロー

符号ありの場合は下表のようにチェックします.

abオーバーフロー条件
正(≧0)正(≧0)INT_MAX - a < b
正(≧0)オーバーフローしない
正(≧0)オーバーフローしない
a < INT_MIN - b

足し算の事後条件

足し算の事後条件によるオーバーフローのチェックでは,c = a + bというint型の演算を考えます.

符号なしの場合は,以下のようにチェックします.

  • c < aまたはs < bの場合,オーバーフロー
  • それ以外は正常

符号ありの場合は,以下のようにチェックします.

  • a ≧ 0かつc < bならばオーバーフロー
  • a < 0かつc > bならばオーバーフロー
  • それ以外は正常

引き算の事前条件

引き算の事前条件によるオーバーフローのチェックでは,a - bというint型の演算を考えます.

符号なしの場合は,以下のようにチェックします.

  • a < bの場合,オーバーフロー
  • それ以外は正常

符号ありの場合は,以下のようにチェックします.

  • オペランドが同じ符号ならオーバーフローは発生しない.(a ≧ 0かつb ≧ 0,またはa < 0かつb < 0)
  • a > 0 > bの場合,a > INT_MAX + bならばオーバーフロー
  • b > 0 > aの場合,a < INT_MIN + bならばオーバーフロー
  • a == 0かつb = INT_MINのときオーバーフロー
  • それ以外は正常

引き算の事後条件

引き算の事後条件によるオーバーフローのチェックでは,c = a - bというint型の演算を考えます.

符号なしの場合は,以下のようにチェックします.

  • c > aならばオーバーフロー
  • それ以外は正常

符号ありの場合は,以下のようにチェックします.

  • b ≧ 0 の場合,c > aならばオーバーフロー
  • b < 0 の場合,c < aならばオーバーフロー
  • それ以外は正常

掛け算のオーバーフローのチェック方法

掛け算のオーバーフローのチェック方法を紹介します.

c = a * bで,aとbは32ビットのint型,cは64ビットのlong型で考えます.

掛け算は,足し算や引き算と比較してオーバーフローしやすいので注意して下さい.

一般的には,aとbのとり得る最大値の2倍以上の記憶領域を使って計算するとオーバーフローを検出しやすいです.

また,掛け算のオーバーフローを検出するのは簡単ではありません.

条件式も複雑になります.

掛け算の事前条件

符号なしの場合は,以下のようにチェックします.

  • b == 0のとき正常
  • a > UINT_MAX / bならばオーバーフロー
  • それ以外は正常

符号ありの場合は,下表のようにチェックします.

abオーバーフロー条件
正(>0)正(>0)INT_MAX / b < a
正(>0)負(≦0)INT_MIN / a > b
負(≦0)正(>0)INT_MIN / b > a
負(≦0)負(≦0)a != 0 && (INT_MAX / a > b)

掛け算の事後条件

符号なしの場合は,以下のようにチェックします.

  • c > UINT_MAX  ならばオーバーフロー
    • UINT_MAX * UINT_MAX < ULONG_MAX
    • ⇔ 4,294,967,295 * 4,294,967,295 = 18,446,744,065,119,617,025 < 18,446,744,073,709,551,615
  • それ以外は正常

符号ありの場合は,以下のようにチェックします.

  • p の上位32ビットがすべて 0 あるいはすべて 1 の場合は正常
  • それ以外はオーバーフロー

割り算のオーバーフローのチェック方法

割り算のオーバーフローのチェック方法を紹介します.

割り算は2の補数表現を利用する場合,以下の2種類のオーバーフローがあります.

  • INT_MIN / -1(値がINT_MAXより大きいのでオーバーフロー)
  • 0による割り算

割り算のオーバーフローが発生するコードは以下になります.

GCCでの実行結果は以下になります.

コンパイル時では,div_overflow.cの10行目と11行目で警告が発生していることがわかります.

実行時では,INT_MIN / -1の結果は-2147483648になり,1 / 0の結果はfloating point exceptionが発生していることがわかります.

Clangでの実行結果は以下になります.

GCCとは異なり,コンパイル時では11行目のみ警告が発生しています.

また,実行時ではINT_MIN / -1の結果は-1603614536, 1 / 0の結果は32322208と正しくない結果になっていることがわかります.(値は実行毎に変動します.)

割り算の事前条件

符号なしの場合は,以下のようにチェックします.

  • 分子がそのデータ型の最小値で分母が-1のときオーバーフロー(2の補数では,int型の場合「INT_MIN / -1 = INT_MAX + 1」でINT_MAXより大きくなるため)
  • 分母が0ならオーバーフロー
  • それ以外は正常

割り算の事後条件

ゼロによる割り算で例外が発生するため,一般的には不可能です.

処理系によっては例外を捕捉することができるものが存在します.

詳細は,後ほど追記するかもです.

コンパイラによるオーバーフローのチェック

gccの-ftrapvオプションを利用すると,符号ありの足し算,引き算,掛け算のオーバーフローを検出するとプログラムを停止します.

ただし,割り算や符号なしのオーバーフローは検出できないので注意してください.

-ftrapvオプションで足し算のオーバーフローを検出するコードは以下になります.

-ftrapvオプションなしでコンパイルした場合の実行結果は以下になります.

2147483647 + 1(INT_MAX + 1)がオーバーフローして-2147483648(INT_MIN)になっていることがわかります.

オーバーフローは検出されませんでした.

-ftrapvオプションありでコンパイルした場合の実行結果は以下になります.

オーバーフローを検出してhandler関数を呼び出し,11行目のfprintf関数で「Arithmetic Overflow!」と出力していることがわかります.

浮動小数点数オーバーフロー

浮動小数点数オーバーフローとは,算術オーバーフローの浮動小数点数の場合の事象です.

浮動小数点数オーバーフローは,以下の点で整数オーバーフローと異なることに注意して下さい.

正のオーバーフローが発生した場合は∞,負のオーバーフローが発生した場合は-∞になること

正のオーバーフローが発生した場合は∞,負のオーバーフローが発生した場合は-∞になる例は以下になります.

実行結果は以下になります.

3行目のFLT_MAX * 2.0の結果がinf(∞),4行目の-FLT_MAX * 2.0の結果が-inf(-∞)になっていることがわかります.

丸め誤差が発生するので,計算機イプシロン(「1より大きい最小の数」と1との差)をεとすると,最大値 + εが∞,最小値 - εが-∞になるとは限らないこと

最大値 + εが∞,最小値 - εが-∞になるとは限らない例は以下になります.

実行結果は以下になります.

丸め誤差が発生したことで,3行目と4行目の計算結果がそれぞれinf(∞)と-inf(-∞)になっていないことがわかります.

まとめ

C言語で算術オーバーフローと回避方法を紹介しました.

算術オーバーフローはバグの原因になりますので,チェックや回避方法をきちんと習得しておきましょう.

文字列のバッファオーバーフローとチェック方法を知りたいあなたはこちらからどうぞ.

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

こういった悩みにお答えします. こういった私から学べます. 目次1 gets関数が廃止された理由2 gets関数の代替関数2.1 fgets関数2.2 getline関数2.3 gets_s関数2.4 ...

続きを見る

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

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

友だち追加

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

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