読者です 読者をやめる 読者になる 読者になる

HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

数値演算には要注意

PHP

ちゃんと分かってて使えば問題はないのだろうが、中途半端にかじるとやけどをしやすい数値演算。文字列オブジェクトだと思っていたものが数値として扱われてしまうこともある恐怖の言語だが、まぁ今回はそれには触れないとして(謎)。
以下のプログラムを3つの環境で動かしてみる。

<?php
$var1 = 6844805271;
$var2 = 48312990;
var_dump($var1);
var_dump($var2);
var_dump( $var1 ^ $var2 );

3つの環境とは、次のもの。

  • Mac OS X(64bit環境)で動作するMAMP内のPHPエンジン(たぶん32bit)
  • Mac OS X(64bit環境)で動作する組み込みのPHPエンジン(たぶん64bit)
  • Linux(32bit環境)で動作するPHPエンジン(まぁ32bitでしょ)

よく見ると分かるとおり、冒頭のプログラムの先頭でセットしている数値、32bitで表現できる符号付整数の範囲を超えているのだが、そもそもこのプログラム、どこから出てきたかというと、某暗号化プログラム(何)のエンコード処理で出てきた計算過程の中のひとつ。シフト演算やビット演算が出現しまくりのプログラムであるわけだが、加算、減算などもやっていたりして、それで32bit符号付整数の最大値を超える値が出てきた、と。
まぁ、それも置いといて(待て)、上記プログラムを3つの環境で実行すると、それぞれ次のような結果になる。

  • Mac OS X(64bit環境)で動作するMAMP内のPHPエンジン(たぶん32bit)
float(6844805271)
int(48312990)
int(-48312991)
  • Mac OS X(64bit環境)で動作する組み込みのPHPエンジン(たぶん64bit)
int(6844805271)
int(48312990)
int(6796493321)
  • Linux(32bit環境)で動作するPHPエンジン(まぁ32bitでしょ)
float(6844805271)
int(48312990)
int(-1793441271)

このうち、2番目と3番目の環境で、もともとの暗号化プログラムを実行すると同じ結果が生成されるのに、途中経過が三者三様で一瞬困るのだが、冷静に見てみるとそうでもないことが分かる。
まず、var_dumpの結果floatだと出ているが、どうにか整数として演算されるのだろうと推測して、それぞれの数を16進数、2進数で表現し、それの排他的論理和を計算してみる。

$var1         =  6844805271
              = 0x       197fb7097
              = 0000 0000 0000 0000 0000 0000 0000 0001 1001 0111 1111 1011 0111 0000 1001 0111
$var2         =    48312990
              = 0x         2e1329e
              = 0000 0000 0000 0000 0000 0000 0000 0000 0000 0010 1110 0001 0011 0010 1001 1110
$var1 ^ $var2 =  6796493321
              = 0x       1951a4209
              = 0000 0000 0000 0000 0000 0000 0000 0001 1001 0101 0001 1010 0100 0010 0000 1001

これは2番目の環境での結果と一致する。さらに1番目と3番目の環境での結果も同様にあらわしてみる。

$var2         =    48312990
              = 0x         2e1329e
              = 0000 0000 0000 0000 0000 0000 0000 0000 0000 0010 1110 0001 0011 0010 1001 1110
1番目の環境   =   -48312991
              = 0xfffffffffd1ecd61
              = 1111 1111 1111 1111 1111 1111 1111 1111 1111 1101 0001 1110 1100 1101 0110 0001

$var1 ^ $var2 =  6796493321
              = 0x       1951a4209
              = 0000 0000 0000 0000 0000 0000 0000 0001 1001 0101 0001 1010 0100 0010 0000 1001
3番目の環境   = -1793441271
              = 0xffffffff951a4209
              = 1111 1111 1111 1111 1111 1111 1111 1111 1001 0101 0001 1010 0100 0010 0000 1001

1番目の環境での値はvar2の値と、3番目の環境での値は机上での計算結果とわざと並べてみた。
何かというと、1番目の環境での値をよく見ると、var2と足すと-1になるという関係になっている。また3番目の環境での値は、机上での計算結果の下位32bitだけを取り出した値と等しい。
ここから、先ほどの「2番目と3番目の環境で、もともとの暗号化プログラムを実行すると同じ結果が生成される」という理由はなんとなく見えてみる。もともとこれの前後で0xffffffffとの論理和を計算したりしているので、下位32bitの部分が同じビットパターンであれば、32bit環境だろうが64bit環境だろうが関係ないということなのだろう。
三者三様の結果を見た直後は、float値の内部表現の関係か?などと考えたのだが、なんとも‥


一方、1番目の環境での計算結果は何だろうか?これこそfloat値の内部表現の関係なのか?64bit環境で64bit非対応のPHPエンジンを使用した場合の特性なのか?それとも扱いきれない数値に対するビット演算ではすべて-1(つまり0xffffffff)扱いなのか?

というわけで、今度は次のプログラムを実行してみた。

<?php
$var1 = 6844805271;
$var2 = 48312990;
printf( "%-10s => %s\n", 'var1', var_export($var1, true));
printf( "%-10s => %s\n", 'var1(int)', var_export((int)$var1, true));
printf( "%-10s => %s\n", 'var2', var_export($var2, true));
printf( "%-10s => %s\n", 'var2(int)', var_export((int)$var2, true));
var_dump( $var1 ^ $var2 );

何かというと、各値をint型にキャストしてみている。
すると結果は‥

  • Mac OS X(64bit環境)で動作するMAMP内のPHPエンジン(たぶん32bit)
var1       => 6844805271
var1(int)  => -1
var2       => 48312990
var2(int)  => 48312990
int(-48312991)
  • Mac OS X(64bit環境)で動作する組み込みのPHPエンジン(たぶん64bit)
var1       => 6844805271
var1(int)  => 6844805271
var2       => 48312990
var2(int)  => 48312990
int(6796493321)
  • Linux(32bit環境)で動作するPHPエンジン(まぁ32bitでしょ)
var1       => 6844805271
var1(int)  => -1745129321
var2       => 48312990
var2(int)  => 48312990
int(-1793441271)

予想通り、1番目の環境では32bit整数として表せない数値をint型にキャストすると-1(つまり0xffffffff)になっている。ビット演算をするときはint型にキャストしていると考えれば、1番目の環境での結果にも納得がいく。
まぁ、実際にPHPエンジンの中を見たわけではないので、すべては推測の域を出ないのだが。
まぁ、こんなの普通は分からんよ、と。