エンディアンと符号付き整数
複数バイトの整数で、保存する順番や符号の指定によって値が変わる理由を学びます。
説明
ファイルや通信データには、長さや番号などの整数が入っています。1バイトは8ビットなので、符号なし整数なら0〜255を表せます。それより大きな値は、複数のバイトを使って表します。
複数バイトの整数では、どのバイトを先に置くかを決める必要があります。この順番をバイト順、またはエンディアンと呼びます。同じバイト列でも、読み込む側が順番を取り違えると、別の整数になります。
このモジュールでは、4バイトの整数を二通りのバイト順で保存し、元の値に戻る場合と、違う値になる場合を調べます。次に、二つの整数を続けて保存した例で、バイト順を変える範囲を説明します。最後に、同じバイト列が正の数にも負の数にもなる理由を学びます。
1ビッグエンディアンの保存順
ビッグエンディアンは上位のバイトから保存する方式です。例として、16進数の12345678を4バイトで保存します。16進数では0〜9とA〜Fを使い、Aは10、Fは15に当たります。
import struct数値とバイト列の変換機能を読み込む; value = 0x1234567816進数の整数; big = struct.pack('>I', value)上位から並ぶ4バイト整数に変換; big.hex(' ')16進数で表示12 34 56 78と表示されました。ビッグエンディアンでは、値の上位に当たるバイトから順に保存します。このため、16進数12345678を左から2桁ずつ分けた並びになります。
2リトルエンディアンの保存順
リトルエンディアンは下位のバイトから保存する方式です。同じ整数で保存順だけを変更します。
little = struct.pack('<I', value)下位から並ぶ4バイト整数に変換; little.hex(' ')16進数で表示78 56 34 12になりました。リトルエンディアンでは下位のバイトから保存します。バイトの順番が変わるだけで、各バイト内のビットを逆に並べるわけではありません。
3どちらの保存順でも4バイト
どちらも4バイトの整数として指定しています。書式に必要な長さと、実際のバイト数を確認します。
struct.calcsize('>I')書式に必要なバイト数, len(big), len(little)実際のバイト数三つとも4です。ビッグエンディアンでもリトルエンディアンでも、今回は同じ4バイトの整数として保存しています。
4保存時と同じ指定で整数に戻す
保存時と同じバイト順を指定し、二つのバイト列を整数に戻します。
hex(struct.unpack('>I', big)[0])上位から並ぶ整数として復元し16進表示, hex(struct.unpack('<I', little)[0])下位から並ぶ整数として復元し16進表示どちらも0x12345678になりました。保存時と読み込み時でバイト順の指定が合っていれば、同じ整数に戻ります。
5バイト順の取り違えによる値の変化
今度は、ビッグエンディアンで保存したbigを、リトルエンディアンの<Iで読み込みます。
hex(struct.unpack('<I', big)[0]bigを逆のバイト順で読み込む)0x78563412になりました。先頭の12が上位ではなく下位のバイトとして扱われ、逆に末尾の78が上位になったためです。ファイルが壊れていなくても、読み込み方を間違えるだけで値は変わります。
6各バイトが整数のどの桁に当たるか
1バイトに256通りの値があるため、隣の上位バイトへ進むごとに桁の重みが256倍になります。4バイトのビッグエンディアンでは、先頭から256³、256²、256¹、256⁰を各バイトの値に掛けます。
[b * 256**(3-i)バイトの値に位置ごとの重みを掛ける for i, b in enumerate(big)位置iとバイトの値bを順に取得]先頭12は10進数で18です。これに256³を掛けると301989888になります。末尾78は10進数で120で、重みは256⁰=1なので、そのまま120です。
7各桁の合計が元の整数になる
各桁の値を足し合わせて、元の整数と比較します。
sum(b * 256**(3-i) for i, b in enumerate(big))各桁の値を合計, value元の整数どちらも305419896になりました。これは16進数12345678と同じ値です。バイト列から整数を得るには、各位置に合った桁の重みで計算します。
8二つの整数を続けて保存したデータ
ファイルなどでは、複数の項目を続けて保存することがあります。一つの項目をフィールド、関連する項目をまとめた一組をレコードと呼びます。ここでは2バイトの整数を二つ並べます。
record = struct.pack('>HH', 0x1234, 0xabcd)2バイト整数を上位バイトから二つ並べる; record.hex(' ')レコード全体を16進表示12 34 AB CDです。先頭の12 34が第1フィールドの整数1234、後ろのAB CDが第2フィールドの整数ABCDを表します。
9バイト順を変える範囲は整数ごと
フィールドの配置はそのままで、二つの整数をそれぞれリトルエンディアンにします。
struct.pack('<HH', 0x1234, 0xabcd)各整数を下位バイトから並べる.hex(' ')16進数で表示34 12 CD ABとなりました。第1フィールドの12 34と、第2フィールドのAB CDが、それぞれ逆のバイト順になります。レコード全体を逆順にする処理とは異なります。
10レコード全体を逆順にした場合
今度はレコード全体のバイト列を逆順にします。その結果を二つの整数として取り出すと、項目の順序がどうなるかを確認してください。
[hex(x)各整数を16進表示 for x in struct.unpack('<HH', record[::-1])全体を逆順にし二つの整数を取り出す]第1整数がABCD、第2整数が1234になり、二つの項目まで入れ替わりました。バイト順を変換するときは、データ形式に従い、どこからどこまでが一つの整数なのかを先に確認します。
11符号付き整数と2の補数
同じFF FFという2バイトを、符号なし整数と符号付き整数として読み込みます。符号付き整数では負の数も表せます。
raw = bytes.fromhex('ff ff')FF FFの2バイト; struct.unpack('>H', raw)[0]符号なし整数, struct.unpack('>h', raw)[0]符号付き整数符号なしでは65535、符号付きでは-1になりました。符号付きhは2の補数という方式を使います。16ビットの最上位ビットが1のとき、符号なしでの値から2¹⁶を引くので、65535−65536=−1です。
このように、バイト列から数値を得るには、バイト順に加えて、整数の長さと符号の有無も指定する必要があります。
まとめ
整数12345678は、ビッグエンディアンでは12 34 56 78、リトルエンディアンでは78 56 34 12と保存されます。バイト順を取り違えると、同じバイト列から別の値が得られます。
複数の整数がある場合は、各フィールドの範囲に従って読み込みます。FF FFが65535にも-1にもなるように、符号の指定も結果に影響します。
操作履歴と解説を振り返れます。
Python 3
import struct; value = 0x12345678; big = struct.pack('>I', value); big.hex(' ')'12 34 56 78'
little = struct.pack('<I', value); little.hex(' ')'78 56 34 12'
| 先頭からの位置 | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| big | 12 | 34 | 56 | 78 |
| little | 78 | 56 | 34 | 12 |
struct.calcsize('>I'), len(big), len(little)(4, 4, 4)
hex(struct.unpack('>I', big)[0]), hex(struct.unpack('<I', little)[0])('0x12345678', '0x12345678')hex(struct.unpack('<I', big)[0])'0x78563412'
[b * 256**(3-i) for i, b in enumerate(big)][301989888, 3407872, 22016, 120]
sum(b * 256**(3-i) for i, b in enumerate(big)), value(305419896, 305419896)
record = struct.pack('>HH', 0x1234, 0xabcd); record.hex(' ')'12 34 ab cd'
| フィールド | 範囲 | バイト |
|---|---|---|
| 第1整数 | 0〜1 | 12 34 |
| 第2整数 | 2〜3 | AB CD |
struct.pack('<HH', 0x1234, 0xabcd).hex(' ')'34 12 cd ab'
[hex(x) for x in struct.unpack('<HH', record[::-1])]['0xabcd', '0x1234']
raw = bytes.fromhex('ff ff'); struct.unpack('>H', raw)[0], struct.unpack('>h', raw)[0](65535, -1)
演習完了