C言語で算術オーバーフローと回避方法を教えて!
こういった悩みにお答えします.
本記事の信頼性
- リアルタイムシステムの研究歴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社で自分に合うスクールを見つけましょう.後悔はさせません!
目次
算術オーバーフロー
算術オーバーフロー(オーバーフロー)とは,算術演算の結果がデータ型の範囲を超える(最大値より大きい,もしくは最小値より小さい)場合に発生する事象のことです.
算術オーバーフローが発生すると,正常な整数演算の結果にならず,バグが発生してしまいます.
また,最大値より大きくなるオーバーフローを正のオーバーフロー,最小値より小さくなるオーバーフローを負のオーバーフローと呼びます.
算術オーバーフローには,整数オーバーフローと浮動小数点数オーバーフローがあります.
算術オーバーフローが発生したかどうかチェックするためには,整数と浮動小数点数の最小値と最大値を取得する必要があります.
C言語で処理系依存の整数と浮動小数点数の最小値と最大値の取得方法を知りたいあなたは,こちらからどうぞ.
算術オーバーフローを理解するためには,キャスト演算子による明示的な型変換と,暗黙的な型変換を知っていることが前提になります.
まだ理解していないあなたはこちらからどうぞ.
算術オーバーフローの回避方法や,算術オーバーフローに関連する以下の内容も解説します.
- C言語で符号ありデータ型を使う理由
- 符号エラー
- 切り捨てエラー
- 不完全な範囲チェック
整数オーバーフロー
整数オーバーフローとは,算術オーバーフローの整数の場合の事象です.
整数オーバーフローの例は以下になります.
13行目で正のオーバーフロー,15行目で負のオーバーフロー,17行目で正のオーバーフローが発生します.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> int main(void) { signed char a = 127; /* a == 0x7f */ unsigned int b; printf("a = %d\n", a); a++; /* a == -128 (0x80); */ printf("a = %d\n", a); b = a; /* b == 0xffffff80 */ printf("b = %u\n", b); b += 128; /* b == 0 */ printf("b = %u\n", b); return 0; } |
実行結果は以下になります.
1 2 3 4 5 6 |
$ gcc integer_overflow.c $ a.out a = 127 a = -128 b = 4294967168 b = 0 |
C言語で符号ありデータ型を使う理由
C言語では本来必要がないはずの箇所でも符号ありデータ型が使われています.
この理由として,負の数をマジックナンバーとして利用する習慣があるからです.
負の数はエラーを表現する方法として利用されています.
例えば,printf関数の返り値の型はint型で,返り値の内容は書き込まれた文字数です.
文字数は非負の整数として表現できるので,本来はint型ではなくunsigned int型が適切だと思いますよね.
しかし,printf関数でエラーが発生した場合,そのエラーを伝えるための返り値を設定したくても非負の整数を表すunsigned int型では正常な返り値と区別できません.
なので,printf関数の返り値が負の数の場合にエラーを返すと定義するため,int型が使われています.
符号エラー
符号エラーとは,符号あり整数型から符号なし整数型への変換で,符号の意味が失われるエラーのことです.
符号エラーの例は以下になります.
12行目のsigned char型の値をunsigned short型に代入する際,符号拡張が発生することに注意して下さい.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> int main(void) { signed char a = -5; unsigned short b; b = a; /* sign extension. */ printf("%hu\n", b); /* 65531 */ return 0; } |
実行結果は以下になります.
1 2 3 |
$ gcc sign_error.c $ a.out 65531 |
切り捨てエラー
切り捨てエラーとは,より小さい整数型への変換し,値が小さい方に収まらない場合に発生するエラーです.
切り捨てにより上位ビットの情報が欠損するので注意して下さい.
切り捨てエラーの例は以下になります.
12行目で,512は2進数で表現すると「1000000000」となり,char型が8ビットの場合は切り捨てエラーが発生して0になってしまいます.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> int main(void) { int a = 8; char b = a * 64; printf("%d\n", b); /* 0 = 8 * 64 = 512 in 8-bit char. */ return 0; } |
不完全な範囲チェック
不完全な範囲チェックによりバグが発生します.
不完全な範囲チェックの例は以下になります.
パット見は正しく実装しているようにみえますが...
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #include <string.h> #define N 10 int set_value(int index, int table[N], int value) { if (index >= N) { fprintf(stderr, "Error: %d is out of range.\n", index); return 1; } table[index] = value; return 0; } int main(void) { int index, value; int table[N]; int i; int ret; memset(table, 0, sizeof(table)); printf("Please input an index: "); scanf("%d", &index); printf("Please input a value: "); scanf("%d", &value); if ((ret = set_value(index, table, value)) != 0) { return ret; } for (i = 0; i < N; i++) { printf("table[%d] = %d\n", i, table[i]); } return 0; } |
実行結果は以下になります.
3行目の行末で3,4行目の行末で4を入力したら,8行目で「table[3] = 4」と正しく値が格納されていることがわかります.
また,16行目の行末で10,17行目の行末で123と入力したら,18行目で「Error: 10 is out of range.」と正しく範囲チェックができていることがわかります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
$ gcc range_error.c $ a.out Please input an index: 3 Please input a value: 4 table[0] = 0 table[1] = 0 table[2] = 0 table[3] = 4 table[4] = 0 table[5] = 0 table[6] = 0 table[7] = 0 table[8] = 0 table[9] = 0 $ a.out Please input an index: 10 Please input a value: 123 Error: 10 is out of range. |
このコードは正しく動作するのでしょうか.
それでは,以下のようにindexに負の値を入力してみましょう.
3行目の行末で-3,4行目の行末で-4を入力したら正しく範囲チェックがされず,もちろん正しくtableに値が格納されませんでした.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$ gcc range_error.c $ a.out Please input an index: -3 Please input a value: -4 table[0] = 0 table[1] = 0 table[2] = 0 table[3] = 0 table[4] = 0 table[5] = 0 table[6] = 0 table[7] = 0 table[8] = 0 |
上記の範囲チェックもれが発生した原因は,12行目のif文でindexが負の値を条件に入れていないためです.
なので,以下のように修正すれば正しく動作します.
※配列の添字が符号ありのint型なので,符号なしのsize_t型に変更する方法でも正しく範囲チェックできます.
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #include <string.h> #define N 10 int set_value(int index, int table[N], int value) { if (index < 0 || index >= N) { fprintf(stderr, "Error: %d is out of range.\n", index); return 1; } table[index] = value; return 0; } int main(void) { int index, value; int table[N]; int i; int ret; memset(table, 0, sizeof(table)); printf("Please input an index: "); scanf("%d", &index); printf("Please input a value: "); scanf("%d", &value); if ((ret = set_value(index, table, value)) != 0) { return ret; } for (i = 0; i < N; i++) { printf("table[%d] = %d\n", i, table[i]); } return 0; } |
実行結果は以下になります.
正しく範囲チェックができていることがわかります.
1 2 3 4 5 |
$ gcc range_error2.c $ a.out Please input an index: -3 Please input a value: -4 Error: -3 is out of range. |
整数オーバーフローのチェック方法
整数オーバーフローのチェック方法を紹介します.
具体的には,整数の四則演算について,オーバーフローのチェック方法をまとめていきます.
a + b(一般的にはa op b)の形の演算を考えます.
オーバーフローのチェックのための事前条件と事後条件を解説していきます.
- 事前条件:aとbがどのような条件を満たす時にオーバーフローなのかを演算の前にチェック
- 事後条件:aとbがどのような条件を満たす時にオーバーフローなのかを演算の後にチェック
足し算と引き算のオーバーフローのチェック方法
足し算と引き算のオーバーフローのチェック方法を紹介します.
足し算と引き算は,算術演算やポインタ演算,インクリメントやデクリメントで利用されます.
足し算の事前条件
足し算の事前条件によるオーバーフローのチェックでは,a + bというint型の演算を考えます.(int型より大きい型でも同様です.)
符号なしの場合は以下のようにチェックします.
- UINT_MAX < a + b
- UINT_MAX - a < bならばオーバーフロー
符号ありの場合は下表のようにチェックします.
a | b | オーバーフロー条件 |
---|---|---|
正(≧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ならばオーバーフロー
- それ以外は正常
符号ありの場合は,下表のようにチェックします.
a | b | オーバーフロー条件 |
---|---|---|
正(>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による割り算
割り算のオーバーフローが発生するコードは以下になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #include <limits.h> int main(void) { printf("%d\n", INT_MIN / -1); printf("%d\n", 1 / 0); return 0; } |
GCCでの実行結果は以下になります.
コンパイル時では,div_overflow.cの10行目と11行目で警告が発生していることがわかります.
実行時では,INT_MIN / -1の結果は-2147483648になり,1 / 0の結果はfloating point exceptionが発生していることがわかります.
1 2 3 4 5 6 7 8 9 10 11 |
$ gcc div_overflow.c div_overflow.c: In function 'main': div_overflow.c:10:26: warning: integer overflow in expression of type 'int' results in '-2147483648' [-Woverflow] 10 | printf("%d\n", INT_MIN / -1); | ^ div_overflow.c:11:20: warning: division by zero [-Wdiv-by-zero] 11 | printf("%d\n", 1 / 0); | ^ $ a.out -2147483648 floating point exception (core dumped) a.out |
Clangでの実行結果は以下になります.
GCCとは異なり,コンパイル時では11行目のみ警告が発生しています.
また,実行時ではINT_MIN / -1の結果は-1603614536, 1 / 0の結果は32322208と正しくない結果になっていることがわかります.(値は実行毎に変動します.)
1 2 3 4 5 6 7 8 |
$ clang div_overflow.c div_overflow.c:11:20: warning: division by zero is undefined [-Wdivision-by-zero] printf("%d\n", 1 / 0); ^ ~ 1 warning generated. $ a.out -1603614536 32322208 |
割り算の事前条件
符号なしの場合は,以下のようにチェックします.
- 分子がそのデータ型の最小値で分母が-1のときオーバーフロー(2の補数では,int型の場合「INT_MIN / -1 = INT_MAX + 1」でINT_MAXより大きくなるため)
- 分母が0ならオーバーフロー
- それ以外は正常
割り算の事後条件
ゼロによる割り算で例外が発生するため,一般的には不可能です.
処理系によっては例外を捕捉することができるものが存在します.
詳細は,後ほど追記するかもです.
コンパイラによるオーバーフローのチェック
gccの-ftrapvオプションを利用すると,符号ありの足し算,引き算,掛け算のオーバーフローを検出するとプログラムを停止します.
ただし,割り算や符号なしのオーバーフローは検出できないので注意してください.
-ftrapvオプションで足し算のオーバーフローを検出するコードは以下になります.
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 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #include <limits.h> #include <signal.h> void handler(int n) { fprintf(stderr, "Arithmetic Overflow!\n"); } int main(void) { int a, b, c; signal(SIGABRT, &handler); a = INT_MAX; b = 1; c = a + b; printf("%d + %d = %d\n", a, b, c); return 0; } |
-ftrapvオプションなしでコンパイルした場合の実行結果は以下になります.
2147483647 + 1(INT_MAX + 1)がオーバーフローして-2147483648(INT_MIN)になっていることがわかります.
オーバーフローは検出されませんでした.
1 2 3 |
$ gcc ftrapv.c $ a.out 2147483647 + 1 = -2147483648 |
-ftrapvオプションありでコンパイルした場合の実行結果は以下になります.
オーバーフローを検出してhandler関数を呼び出し,11行目のfprintf関数で「Arithmetic Overflow!」と出力していることがわかります.
1 2 3 4 |
$ gcc ftrapv.c -ftrapv $ a.out Arithmetic Overflow! abort (core dumped) a.out |
任意精度算術ライブラリGMPの利用
C言語の任意精度算術ライブラリのGMP(The GNU Multiple Precision Arithmetic Library)を利用することで,オーバーフローを回避することが可能です.
※Java言語の場合はBigIntegerクラスに相当します.
GMPを利用するとコードの可読性と実行速度は犠牲になるので,トレードオフを考慮して利用を検討しましょう.
GMPをインストールしていない場合は,以下のコマンドでインストールして下さい.
1 |
$ sudo apt-get install libgmp-dev |
C言語でGMPを利用するコードは以下になります.
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 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <gmp.h> #include <limits.h> int main(void) { long aa = LONG_MAX; long bb = 2; mpz_t a, b, c; mpz_init_set_si(a, aa); mpz_init_set_si(b, bb); mpz_init(c); mpz_mul(c, a, b); gmp_printf("%Zd * %Zd = %Zd\n", a, b, c); mpz_clear(a); mpz_clear(b); mpz_clear(c); return 0; } |
実行結果は以下になります.
GCCのオプションに-lgmpを追加することを忘れないようにして下さい.
3行目のLONG_MAX * 2の演算結果がオーバーフローせずに正しく計算できていることがわかります.
1 2 3 |
$ gcc gmp.c -lgmp $ a.out 9223372036854775807 * 2 = 18446744073709551614 |
浮動小数点数オーバーフロー
浮動小数点数オーバーフローとは,算術オーバーフローの浮動小数点数の場合の事象です.
浮動小数点数オーバーフローは,以下の点で整数オーバーフローと異なることに注意して下さい.
- 正のオーバーフローが発生した場合は∞,負のオーバーフローが発生した場合は-∞になること
- 丸め誤差が発生するので,計算機イプシロン(「1より大きい最小の数」と1との差)をεとすると,最大値 + εが∞,最小値 - εが-∞になるとは限らないこと
- -ftrapvオプションでオーバーフローを検出できないこと
正のオーバーフローが発生した場合は∞,負のオーバーフローが発生した場合は-∞になること
正のオーバーフローが発生した場合は∞,負のオーバーフローが発生した場合は-∞になる例は以下になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #include <float.h> int main(void) { float a, b, c, d; a = FLT_MAX; b = 2.0; c = a * b; d = -a * b; printf(" %le * %le = %le\n", a, b, c); printf("%le * %le = %le\n", -a, b, d); return 0; } |
実行結果は以下になります.
3行目のFLT_MAX * 2.0の結果がinf(∞),4行目の-FLT_MAX * 2.0の結果が-inf(-∞)になっていることがわかります.
1 2 3 4 |
$ gcc floating_point_overflow.c $ a.out 3.402823e+38 * 2.000000e+00 = inf -3.402823e+38 * 2.000000e+00 = -inf |
丸め誤差が発生するので,計算機イプシロン(「1より大きい最小の数」と1との差)をεとすると,最大値 + εが∞,最小値 - εが-∞になるとは限らないこと
最大値 + εが∞,最小値 - εが-∞になるとは限らない例は以下になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #include <float.h> int main(void) { float a, b, c, d; a = FLT_MAX; b = FLT_EPSILON; c = a + b; d = -a - b; printf(" %le + %le = %le\n", a, b, c); printf("%le - %le = %le\n", -a, b, d); return 0; } |
実行結果は以下になります.
丸め誤差が発生したことで,3行目と4行目の計算結果がそれぞれinf(∞)と-inf(-∞)になっていないことがわかります.
1 2 3 4 |
$ gcc floating_point_round_error.c $ a.out 3.402823e+38 + 1.192093e-07 = 3.402823e+38 -3.402823e+38 - 1.192093e-07 = -3.402823e+38 |
まとめ
C言語で算術オーバーフローと回避方法を紹介しました.
算術オーバーフローはバグの原因になりますので,チェックや回避方法をきちんと習得しておきましょう.
文字列のバッファオーバーフローとチェック方法を知りたいあなたはこちらからどうぞ.
C言語を独学で習得することは難しいです.
私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.
私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!
独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!