なぜ Data::MessagePackのテストが ARM環境でパスしなかったのか ?

Data::MessagePackのテストが ARM環境でテストをパスしていませんでした.
これはバージョン 0.38では解決された問題です.
@さんが msgpack本体の問題を修正し, 私が PP版の
修正を行いました.


結論から言うと, ABIの問題です. ARMには OABIと EABIがあり,
OABIの環境でテストをパスしていませんでした. それについて
示していきます.

エンディアン

ビッグエンディアン, リトルエンディアンという言葉はコンピュータに
ついて勉強をしたことがある人であれば聞いたことがあると思います.
当然 msgpackもその 2つのエンディアンを考慮していました.


でも ARM OABI環境ではそれだけでは対応できていませんでした.
具体例を示します. 以下のようなプログラムを用意します.

#include <stdio.h>

int main (void)
{
    int i;
    union {
        double d;
        unsigned char data[sizeof(double)];
    } test;

    test.d = 1;

    for (i = 0; i < sizeof(double); i++) {
        printf("%p %2x\n", &test.data[i], test.data[i]);
    }
    printf("\n");

    return 0;
}

これをリトルエンディアンのマシン i386で実行すると以下のような結果になります.

  % ./a.out
  0xbf90e020  0 [0]
  0xbf90e021  0 [1]
  0xbf90e022  0 [2]
  0xbf90e023  0 [3]
  0xbf90e024  0 [4]
  0xbf90e025  0 [5]
  0xbf90e026 f0 [6]
  0xbf90e027 3f [7]

次にビッグエンディアンのマシン PowerPCで実行してみましょう.

  % ./a.out
  0x7fc66870 3f [7]
  0x7fc66871 f0 [6]
  0x7fc66872  0 [5]
  0x7fc66873  0 [4]
  0x7fc66874  0 [3]
  0x7fc66875  0 [2]
  0x7fc66876  0 [1]
  0x7fc66877  0 [0]

トルエンディアンと逆になってますね.


ではリトルエンディアン ARM(OABI)だとどうなるでしょう ?
i386と同じになると期待したいところです。ところが実際は以下のようになります。
(以降 ARMはリトルエンディアンということにします。)

  % ./a.out
  0x1fffe0  0 [4]
  0x1fffe1  0 [5]
  0x1fffe2 f0 [6]
  0x1fffe3 3f [7]
  0x1fffe4  0 [0]
  0x1fffe5  0 [1]
  0x1fffe6  0 [2]
  0x1fffe7  0 [3]

トルエンディアンともビッグエンディアンとも異なっています.
どんな順番に並んでいるかを添えて書きました. バイト単位で見ると
トルエンディアンに並んでいますが, ワード(32ビット)単位で見ると,
ビッグエンディアンに並んでいます. これはミドルエンディアン
Mixedエンディアンとよばれたりします.


なおこうなるのは double型だけです。

問題点

ARM(OABI)が外から double型の 1を受け取ったとしましょう. msgpackは
ビッグエンディアンエンコードされるので, 受け取ったバイト列は

"3f f0 00 00 00 00 00 00"

というはずです. リトルエンディアンARMにおいて, msgpackはこのバイト列を

"00 00 00 00 00 00 f0 3f"

にデコードします. しかしこれはさっきみた ARM(OABI)での 1と異なります.
本来であれば

"00 00 f0 3f 00 00 00 00"

にならないといけないのに,

"00 00 00 00 00 00 f0 3f"

になっているわけですから, テストが通らないわけです. これは ARM(OABI)
では 1ではない, 期待しない値です.


送信するときも同様です.

"00 00 f0 3f 00 00 00 00"

というバイト列を msgpackはビッグエンディアンエンコードするので,

"00 00 00 00 3f f0 00 00"

となります. 受け取ったマシンでは, これがデコードされ

"00 00 f0 3f 00 00 00 00" (リトルエンディアン)
"00 00 00 00 3f f0 00 00" (ビッグエンディアン)

というバイト列になります. ビッグエンディアンのマシンにしても
トルエンディアンのマシンにしてもこれを受け取っても double型の 1には見えません.

修正

ARM OABIの場合は低位の 4バイトと高位の 4バイトを入れ替えるという
処理を追加しました.


Perl以外の言語は erlangを除くと純粋なバインディングのようなので,
最新版にすれば問題がおきないはずです.

おわりに

Data::MessagePackのテストがなぜ ARMでパスしなかったのかと
いうことについて示しました.


なんで ARM(OABI)はこんな風になってるのって思う方もいるかもしれ
ません. でもこれは問題はありません. IEEE 754の規格ではそれが
どんな形でメモリに収まっているかまでは規定していません. 整数型で
あればビット演算があるので, 順番通りになっていないと大変なことに
なりますが, 浮動小数点の場合, ビット単位の操作は基本意味をなし
ませんので, そのマシンで読み書きできれば通常問題ありません.

ただ他のマシンと通信をする場合話は別です. このあたりはバイナリ
プロトコルの難しいところと言えるでしょう. 速度が特に必要がない
ならテキストベースのプロトコルにした方がこのような問題が回避
できます.


ちなみに EABI ARMではこの問題は発生しません. EABI ARMで上記の
プログラムを実行すると結果は以下のようになります.

  % ./a.out
  0x1fffd8  0 [0]
  0x1fffd9  0 [1]
  0x1fffda  0 [2]
  0x1fffdb  0 [3]
  0x1fffdc  0 [4]
  0x1fffdd  0 [5]
  0x1fffde f0 [6]
  0x1fffdf 3f [7]

一般的なリトルエンディアンと同じ結果になります。


最近の Linux ARM環境(Android含む)は EABIです. *BSDはライセンスに伴う
ツールチェインのバージョンを上げられない問題から EABI対応できていない
ものもあります. 新規に ARMで何かしたいという場合は EABI対応のツールチェインを
使うことを激しくおすすめします.


ビッグエンディアン ARMではテストしていないので, その環境を持ってる
方はテストして, 問題が起きたら報告していただけると助かります.


コンパイラや OS等 CPUに近い部分の開発なり研究を行った人で
あれば、もしかしてバイト列がっていう風に気づくかもしれませんが、
上のレイヤを主に扱っている人ではちょっと気づきづらい問題だった
かなと思います。私は大学時代そういう研究をし、そういう職に
ついているので、この問題に気づいたのだと思います。私は趣味以外で
Perlを触る、まして msgpackを触ることはありませんが、こんな感じで
持っている知識を反映できていけば、Web業界だけだと不足するかも
しれない部分を多少補えるのかなと思いました。