mainvisual

プログラマにとってオーバーフローは重大な問題です。 原因や場所が特定されにくいため、長期間放置される可能性が高く、 セキュリティの大きなバグに繋がるおそれがあるためです。

golangの数値型には、intint32byteなど、 11種類の型(byte型はuint8型に含めた)があります。

今回は、その型を使って、桁あふれオーバーフローをさせてみたました。 この話は、コンピュータの仕組みがわかっていない人は理解が出来ない可能性が高いです。 10進数と2進数の概念については事前知識とします。

int型の変数をbyte型の変数にキャストすると、オーバーフローが発生

golangのbyte型では0~255までの値しか格納する事が出来ません。 ですので、以下のような書き方はコンパイルエラーとなります。

b := byte(256) 
// constant 256 overflows byte
// コンパイルエラー

ですが、次のようにキャストしてあげると、コンパイルは通ります。 この場合、bの値は’0’です。

i := 256
b := byte(i)
fmt.Println(b) // => 0

???なんで “0” 何でしょう???

それがわかるようになるには、 内部で2進数の演算がどのように行われているかを理解する必要があります。

オーバーフローの原因

10進数と2進数の対応表は次のようになります。

10進数 2進数
0 0000 0000
1 0000 0001
2 0000 0010
253 1111 1101
254 1111 1110
255 1111 1111

これを踏まえて説明します。

まず、int型は実装環境に依存されるので、ここでは32ビット環境で動作させたとします。 すると、i := 256の時点で、メモリ内部で数値はこのように表現されます。

int 256の時のメモリ空間の配置図

見ての通り、メモリ上の変数iは、0000 0000 0000 0000 0000 0001 0000 0000となっています。

ここで、この変数を8ビットである、byte型にキャストをして代入します。 すると、メモリ内部で数値はこのように変化します。

int 256をbyteに変換した時のメモリ空間配置図

byte型は8バイトまでの値しか扱う事が出来ないため、 それよりも上位の桁は無視してキャストを行います。

そのため、2進数で下8桁の数が有効となり、結果 “0” と判定されるというわけです。

オーバーフローが起こると何が困るの?

オーバーフローが起こると、何が困るのでしょう? 残念ながら、これをすぐに思いついて説明するのはなかなか難しいと思います。

斯くして自分も、「オーバーフローが起きるような実装をしてはいけない」と いろいろ言われてきたにも関わらず、 オーバーフローを意識するようなことが普段無かった(軽量プログラミング言語しか使わなかった)ため、 どのような場面で意識する必要があるのかわかりませんでした。

また、C,C++の時とgolangの時では、重要度が違ってきます。 C,C++の場合では、「バッファオーバーフロー」というもっと重大な脆弱性になります。 詳細は割愛します。

golangの場合は、値がおかしくなるだけで、 システムには重要な影響は与えないようにコンパイラが設計されています。

結論からいうとgolangの場合で困るのは、次の3点ぐらいでしょう。

  1. “18446744073709551616” よりも大きな数を計算しなければならない時
  2. 浮動小数点型よりも、もっと細かい精度で計算しなければならない時
  3. 大きな数値の大小を比較して条件分岐をする場合

しかし、プログラマは責任を持って、 この問題は起こさないようにするべきです。

オーバーフローを防ぐ解決法

“1.” “2.“の場合、uint64で扱える最大値よりも大きな数を計算する場合、 math/lang パッケージに多倍長整数の Int型 を使うことで対処します。

“3.” の場合では、演算を行な前に、演算結果がオーバーフローするかしないかを判定することにより 対処します。

大きな数を計算する場合

golangには、uint64よりも大きな数を扱える、多倍長整数型が存在します。 これは、math/lang パッケージに多倍長整数の Int型を使用します。

四則演算を+,-,*,/ですることが出来ず、専用のメソッドを使って計算を行います。

package main

import (
    "fmt"
    "math/big"
)

func main() {
    a := big.NewInt(9223372036854775807)
    b := big.NewInt(10)
    result_add := new(big.Int).Add(a, b)
    result_sub := new(big.Int).Sub(a, b)
    result_mul := new(big.Int).Mul(a, b)
    result_div := new(big.Int).Div(a, b)
    fmt.Println(result_add)  // 9223372036854775817
    fmt.Println(result_sub)  // 9223372036854775797
    fmt.Println(result_mul)  // 92233720368547758070
    fmt.Println(result_div)  // 922337203685477580
}

演算時、常にオーバーフローをチェックする方法

math パッケージには、各型の最大値を格納している定数があります。 それを利用して、オーバーフローをするかしないかを判断する方法です。

package main

import (
    "fmt"
    "math"
)

func main() {
    a1 := uint32(4294967295)
    b1 := uint32(1)

    a2 := uint32(10)
    b2 := uint32(20)

    fmt.Println(isOverflow(a1, b1))
    fmt.Println(isOverflow(a2, b2))
}

func isOverflow(a, b uint32) bool {
  if (math.MaxUint32 - a) < b {
    return false
  }else{
    return true
  }
}

ここで、注意してほしいのが、ifの(math.MaxUint32 - a) < bです。 これを見て、たぶん、こんな風に思った人もいるはずです。

「条件文、わかりにくくて気持ち悪いなぁ。 math.MaxUint32 < a + bってすればスマートなのに」

実は、そのような条件分岐にすると、思った挙動になりません。

4294967295 = 1111 1111 1111 1111 1111 1111 1111 1111なので、それに1を足すと、 1 0000 0000 0000 0000 0000 0000 0000 0000となります。

32ビット表現なので、オーバーフロー(桁あふれ)を起こして、 「オーバーフローの原因」の章で説明したような事が起こり、 a + b0 になります。 すると、本来はtrueのはずがfalseとなります。

要注意ですね

まとめ

PythonやRubyはどんなに大きな値でも、なんとか計算をしてくれます。 しかし、golangなどのコンパイル型の言語だと、オーバーフローなど、 いろいろなことについて気をつけなければならないことがわかりました。

今後、golangで開発をしてく際には、大きな数値を扱うこともあると思います。 そのようなとき、普段から気をつけて置かなければならないなぁと思いました。

多分、どこでオーバーフローをしているなんて気が付かないと思いますから。