バイナリデータを扱うプログラムを書く上で、避けて通る事の出来ないバイトオーダーの問題。
それなりに頻出問題だと思うのだが、軽くググってみただけではまとまった解説が見つからなかった。自分用も兼ねてここにまとめて見る。
バイトオーダーとは多バイトデータのメモリへの配置順のことで、エンディアンやエンディアンネスとも呼ばれる。
バイトオーダーはコンピュータシステムごとに異なり、現在では、x86系などで使用されるリトルエンディアン(LE)と、PowerPC1)/MC68000/SPARCなどで使用されるビッグエンディアン(BE)の2つに大別される。スマートフォンの台頭で一般的となったARM系はLEで使われることが多い。
例えば「0x1234CDEF」という32ビット値のメモリでの配置は、それぞれのエンディアンで以下のようになる。
番地 | リトル | ビッグ |
---|---|---|
0000 | EF | 12 |
0001 | CD | 34 |
0002 | 34 | CD |
0003 | 12 | EF |
リトルエンディアンはデータの下位バイト、ビッグエンディアンはデータの上位バイトから、それぞれメモリの下位番地より順次配置される。人間から見ればビッグエンディアンの方が分かりやすいが、コンピュータ的にはリトルエンディアンの方が都合がいいらしい。また、データの下位バイトをメモリの下位番地に配置する、という意味ではリトルエンディアンの方が理にかなっているとも考えられる。
ではここで、データの保存について考えてみる。データの保存とは、とどのつまりメモリの内容をファイルに書き出すことである。
0x1234CDEFの例でいえば、メモリ番地0000~0003をファイルに書き出すことになり、リトルエンディアン環境ではファイルの内容は「EFCD3412」となる。
次に、そのファイルからメモリの状態を復元、つまりデータの読み込みを考える。
ファイル内容を先頭から順にメモリに展開すると、番地0000から上位番地へ向かって EF CD 34 12 の順で配置される。このメモリ内容は、リトルエンディアン環境ならば0x1234CDEFと正しく解釈される一方、ビッグエンディアン環境では0xEFCD3412と解釈され元の値とは異なってしまう。
このように、データ保存環境のエンディアンとデータ読込環境のエンディアンが異なれば、保存時に意図したデータとは異なるデータになってしまう。これでは宜しくない。
異なるエンディアンで保存されたデータを正しく読み込むには、データの並び順を変えてやらなければならない。これがバイトスワップである。
先の例であれば、データをメモリに展開する際に
という処理を施せばよい。
この処理を行うC言語のマクロは、以下のようになる。一応、1バイト = 8ビットが前提の実装となっているが、それ以外の環境に触れている方はこのページを読まないと思うので気にしない事にする。
#define _BYTE1(x) ( x & 0xFF ) #define _BYTE2(x) ( (x >> 8) & 0xFF ) #define _BYTE3(x) ( (x >> 16) & 0xFF ) #define _BYTE4(x) ( (x >> 24) & 0xFF ) #define BYTE_SWAP_16(x) ((uint16_t)( _BYTE1(x)<<8 | _BYTE2(x) )) #define BYTE_SWAP_32(x) ((uint32_t)( _BYTE1(x)<<24 | _BYTE2(x)<<16 | _BYTE3(x)<<8 | _BYTE4(x) ))
BS_INT16(x)が2バイトデータのバイトスワップを、BS_INT32(x)が4バイトデータのバイトスワップを行うマクロである。つまり、BS_INT32(0xEFCD3412)とすれば、0x1234CDEFというデータを得る事ができる。
動作原理を見て見よう。
まず、BS_BYTEn(x)のマクロで、右シフトと論理積を使いデータ列から1バイトずつデータを得る。そして、得たデータを左シフトし論理和を取る事でバイトオーダーの変換を行っている。具体的な動作例は以下を参考されたい。
■BS_BYTEn(x)の動作。
[1バイト目] 1110 1111 1100 1101 0011 0100 0001 0010 (元データ0xEDCD3412) AND) 0000 0000 0000 0000 0000 0000 1111 1111 (0xFFで論理積を取る) -------------------------------------------- 0000 0000 0000 0000 0000 0000 0001 0010 → 0x12 (得られるデータ)
[2バイト目] 1110 1111 1100 1101 0011 0100 0001 0010 (元データ0xEDCD3412) >>8= 0000 0000 1110 1111 1100 1101 0011 0100 (8ビット右シフトする) AND) 0000 0000 0000 0000 0000 0000 1111 1111 (0xFFで論理積を取る) -------------------------------------------- 0000 0000 0000 0000 0000 0000 0011 0100 → 0x34 (得られるデータ)
同様に3,4バイト目もそれぞれ16,24ビット右シフトしてデータを得る。
■BS_INT16(x)の動作
BS_BYTE1(x) << 8= 0000 0000 0000 0000 0001 0010 0000 0000 (0x12を8ビット左シフト) BS_BYTE2(x) OR) 0000 0000 0000 0000 0000 0000 0011 0100 (0x34と論理和を取る) --------------------------------------------------------- 0000 0000 0000 0000 0001 0010 0011 0100 → 0x1234 unsigned short = 0001 0010 0011 0100 (unsigned shortでキャストして2バイトのデータにする)
BS_INT32(x)も、シフトの数と論理積の段数が増えるだけで、動作原理は同じである。
この原理を用いれば、どんな多バイトデータでもエンディアンの変換を行うことができる。変換の方向は関係なく、BEなデータを与えればLEのデータになるし、その逆もまた然りだ。