一覧CS-02-3 · 文字・バイト・データ量

エンディアンと符号付き整数

複数バイトの整数で、保存する順番や符号の指定によって値が変わる理由を学びます。

説明

ファイルや通信データには、長さや番号などの整数が入っています。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

1
import struct; value = 0x12345678; big = struct.pack('>I', value); big.hex(' ')
'12 34 56 78'
2
little = struct.pack('<I', value); little.hex(' ')
'78 56 34 12'
ビッグエンディアンとリトルエンディアン
先頭からの位置0123
big12345678
little78563412
3
struct.calcsize('>I'), len(big), len(little)
(4, 4, 4)
4
hex(struct.unpack('>I', big)[0]), hex(struct.unpack('<I', little)[0])
('0x12345678', '0x12345678')
5
hex(struct.unpack('<I', big)[0])
'0x78563412'
6
[b * 256**(3-i) for i, b in enumerate(big)]
[301989888, 3407872, 22016, 120]
7
sum(b * 256**(3-i) for i, b in enumerate(big)), value
(305419896, 305419896)
8
record = struct.pack('>HH', 0x1234, 0xabcd); record.hex(' ')
'12 34 ab cd'
2フィールドの位置
フィールド範囲バイト
第1整数0〜112 34
第2整数2〜3AB CD
9
struct.pack('<HH', 0x1234, 0xabcd).hex(' ')
'34 12 cd ab'
10
[hex(x) for x in struct.unpack('<HH', record[::-1])]
['0xabcd', '0x1234']
11
raw = bytes.fromhex('ff ff'); struct.unpack('>H', raw)[0], struct.unpack('>h', raw)[0]
(65535, -1)

演習完了