エンディアン:ビッグとリトル(バス図付き)
ビッグエンディアンは最上位バイトを最小アドレスに置き、リトルエンディアンはそれを反転します。0x12345678のバス図とレイアウト、x86とTCP/IPが異なる理由を解説します。
要約: エンディアンは、マルチバイト値をメモリに配置する順序の規約です。ビッグエンディアンは最上位バイトを最小アドレスに置き、多くのネットワークプロトコルのフィールドや歴史的な PowerPC システムで採用されています。リトルエンディアンは最下位バイトをそこに置き、x86 や標準的な RISC-V プロファイルで採用されています。バイト列がインターフェースを越える場合は、アーキテクチャ、動作モード、プロトコルを個別に確認する必要があります。
32ビット整数は4バイトを取ります。メモリは1バイトずつアドレス指定されます。したがって、32ビット値をメモリへ書くとき、どのバイトを最小アドレスに置くのでしょうか? その選択がエンディアンです。これは、コンピューターアーキテクチャにおいて、合理的な2つの答えが並存し、間違えると気付かないまま誤った数を作る数少ない決定の1つです。
「エンディアン」の意味
32ビット値 を考えます。4つのバイトがあります。
- — 最上位バイト(MSB)
- — 最下位バイト(LSB)
これをアドレス のメモリへ保存するには、4つのバイトを 、、、 に置く必要があります。主な選択肢は2つです。
ビッグエンディアン。 MSBを先に保存します。
| アドレス | ||||
|---|---|---|---|---|
| バイト |
リトルエンディアン。 LSBを先に保存します。
| アドレス | ||||
|---|---|---|---|---|
| バイト |
同じ32ビット値、同じ4バイト、まったく異なるメモリレイアウトです。ビッグエンディアンマシンで を書くプログラムと、リトルエンディアンマシンで .. から4バイトを1バイトずつ読むプログラムは、 を再構成します。同じバイトでも、数は違います。
名称は、Danny Cohenが1980年のエッセイで Gulliver’s Travels から借りたものです。リリパット人は、ゆで卵のどちらの端を先に割るかをめぐって戦争をしました。このたとえの要点は、選択は恣意的でありながら、集団が争いを始めるほど強くそれに固執するということでした。
読み出しのバス図
エンディアンの規約はCPUのロード/ストアユニットによって実行されます。CPUがアドレス から32ビットロードを実行すると、アドレスバスに をアサートし、データバスから4バイトを受け取ります。
CPU Memory
+--------+ address=A +---------+
| load | ───────────────>| byte 0 | A : 0x12 (big) or 0x78 (little)
| 32-bit | | byte 1 | A+1 : 0x34 (big) or 0x56 (little)
| | data[31:0] | byte 2 | A+2 : 0x56 (big) or 0x34 (little)
| | <───────────────| byte 3 | A+3 : 0x78 (big) or 0x12 (little)
+--------+ +---------+
ビッグエンディアンCPUでは、最小アドレスにあるバイト0が32ビットレジスタの最上位レーンへシフトされ、バイト3は最下位レーンに入ります。リトルエンディアンCPUでは、バイト0が最下位レーンに入り、バイト3が最上位レーンに入ります。
REGISTERとRAMのコンポーネントリファレンスは、この転送に参加する記憶の基本要素を説明しています。エンディアンは、それらの間の純粋な配線規約です。幅広いレジスタのどのバイトを、メモリのどのバイトアドレスへつなぐかという規約です。
同期メモリアクセスでは、通常、バス上のバイトは幅広いデータパスを通じて同時に提示されます。より狭い8ビットバスでは、同じバイトが4サイクルにわたって順次到着します。エンディアンの規約はどちらの場合にも同じように適用されます。
実例:Cにおける0x12345678
次の疑似コード断片を考えてください。
uint32_t x = 0x12345678;
uint8_t *p = (uint8_t *)&x;
printf("%02x %02x %02x %02x\n", p[0], p[1], p[2], p[3]);
ビッグエンディアンマシンでは 12 34 56 78 です。リトルエンディアンマシンでは 78 56 34 12 です。ポインタ はメモリを1バイトずつ進み、そのバイト順は正確にメモリ上のレイアウト、すなわちエンディアンによって異なるものです。
これは標準的なエンディアン調査です。同時に、一般的なバグの源でもあります。ポインタキャストの技巧で整数から個々のバイトを「抽出」するコードはエンディアンに依存し、明示的なバイトスワップなしには一方のホストでしか動きません。
どのアーキテクチャがどちらを選ぶか?
| アーキテクチャ | 既定のエンディアン |
|---|---|
| x86、x86-64 | リトルエンディアン |
| RISC-V | リトルエンディアン(ビッグエンディアン変種も存在) |
| ARM | バイエンディアン、設定可能。リトルエンディアンが圧倒的に一般的 |
| PowerPC | 歴史的にはビッグエンディアン。後のコアはバイエンディアン |
| MIPS | バイエンディアン。既定はSoCにより異なる |
| SPARC | ビッグエンディアン(V9はバイエンディアン) |
| Motorola 68k | ビッグエンディアン |
| z/Architecture(IBMメインフレーム) | ビッグエンディアン |
| ネットワークバイト順(TCP/IP、IPv4、IPv6ヘッダ) | ビッグエンディアン |
現在の傾向は確実にリトルエンディアンへ向いています。x86とARMを合わせると、稼働中CPUの圧倒的大多数を占めます。ネットワークプロトコルがビッグエンディアンのままなのは歴史的な理由です。TCP/IPが仕様化された1970年代後半には、ビッグエンディアンのDEC PDPとIBMメインフレームがなお基調を決めていました。
なぜ2つの陣営が残るのか
どちらの側にも実際の利点がありますが、決定的なものはありません。
ビッグエンディアンを支持する理由
上から下への読み方が、人が数を書く方法と一致する。 はメモリで最初に見るバイトであり、紙に最初に書く桁でもあります。ビッグエンディアン構造体の16進ダンプは直接読み取れます。
ネットワークバイト順。 インターネットは仕様上ビッグエンディアンです(RFC 791、RFC 1700)。バイナリプロトコルをワイヤー上へ直列化するところではどこでも、ビッグエンディアンが確立済みの規約です。
符号比較が直接的である。 2の補数整数の符号ビットは最上位バイトにあります。ビッグエンディアンマシンではそれを最小アドレスで見るため、符号拡張ロードは最初にフェッチしたバイトを見るだけでよいのです。
リトルエンディアンを支持する理由
LSBがLSBにある。 アドレス のバイトは、アドレス の整数の最下位バイトです。これは、同じアドレスへの32ビットポインタと8ビットポインタが、どちらも最下位バイトを指すことを意味します。int* を char* にキャストしてもアドレスは動かないため、16ビットレジスタから32ビット値の下位バイトを読むのは追加操作なしでできます。
算術キャリーの順序。 多倍長加算は最下位バイトから始め、キャリーを上へ、つまり高いアドレスへ伝播させます。リトルエンディアンマシンは自然なメモリ走査順でオペランドを読み、結果を書きます。バッファリングが限定されていた1978年の8086では、これは実際の性能上の論点でした。
幅に依存しないロード。 アドレス の32ビット値は、アドレス の16ビット値に続く のもう1つの16ビット値と同じであり、 から始まる4つの8ビット値とも同じです。幅をまたぐ型パニングは最下位バイトでは見えません。これが、x86ソフトウェアが歴史的に大胆なキャストを許容してきた理由の一部です。
どちらの陣営も勝っていません。ARMはアーキテクチャ上の妥協です。バイエンディアンであり、カーネルが起動時に選びます。
エンディアンが重要になるとき
1台のマシン上での日常的なプログラミングでは、エンディアンは見えません。CPUは自らの整数型に対してバイト順を正しく扱い、コンパイラは正しいロードとストアを出力し、プログラムは考える必要がありません。
見えるようになるのは、整数ではなくバイトを読み書きし、それらのバイトが別の場所でも同じ意味を持たなければならない瞬間です。
バイナリファイル形式。 ELFヘッダ、PNGチャンク、MP4ボックス、GIF、BMP、ZIP。一部はビッグエンディアン(PNG)、一部はリトルエンディアン(BMP、ZIP)、少数は混在です。エンディアンの違うホストでELFヘッダをスワップなしに読むと、無意味なオフセットを読み出します。
ネットワークプロトコル。 TCP、UDP、IP、ICMP、DNS、NTPはすべてビッグエンディアンです。Cライブラリー関数 htonl、htons、ntohl、ntohs(「host to network long/short」とその逆)は、ホストバイト順とネットワークバイト順を変換するために存在します。ビッグエンディアンホストでは何もしません。リトルエンディアンホストではバイトをスワップします。
#include <arpa/inet.h>
uint32_t host_value = 0x12345678;
uint32_t net_value = htonl(host_value); // 0x78563412 on x86
// 0x12345678 on a big-endian host
ワイヤー上のバイトは両ホストで同じです。メモリ内表現が異なります。
デバイスプロトコル。 SPIフラッシュ、I2C EEPROM、センサーレジスタの大半は、データシートでバイト順を選び、ホストに従うことを求めます。多くはビッグエンディアンで、一部は混在です。
型パニング。 ポインタまたはunionを通して幅の異なる整数型の間でキャストし、固定オフセットのバイトを期待すること。一方のホストでは「そのまま動き」、もう一方では気付かないまま失敗するパターンです。
混在エンディアンシステムのメモリマップドレジスタ。 ビッグエンディアンCPUがリトルエンディアン周辺機器を制御する場合(または逆)、各レジスタアクセスで明示的なバイトスワップ命令が必要です。
一般則は次です。バイトがCPUの排他的な管理を離れた途端、エンディアンは強制すべき契約になる。 1台のホスト上の1つのプロセス内では無視できます。マシン、ファイル、プロトコル、周辺機器をまたぐなら無視できません。
混在エンディアンとミドルエンディアン
バイト順の物語における脚注として、歴史上の一部のシステムは ミドルエンディアン 順序を使いました。32ビット値を2つの16ビット半分としてビッグエンディアン順に保存する一方、各半分の内部はリトルエンディアンとする方式です。PDP-11は、一部の動作モードで32ビット値にこのレイアウトを使いました。その結果、4バイトとして保存された は になります。純粋なビッグでも純粋なリトルでもありません。現代のシステムはミドルエンディアンを使いませんが、PDP時代のツールが作った古いバイナリ形式には時折現れます。
一部のArmシステムも、従来のBE-32とバイト不変のBE-8を区別していました。重要な教訓は、CPU名から周辺機器やファイルのレイアウトを推測しないことです。正確なアクセスについては、アーキテクチャ、バス、デバイスの文書を使ってください。
フェッチ・デコード・実行サイクルにおけるエンディアン
CPUが命令を実行するとき、メモリから命令語をフェッチする必要があります。ビッグエンディアンCPUでは、命令語は上位バイトから先に配置されます。リトルエンディアンCPUでは、下位バイトから先です。デコード論理はそれに合致しなければなりません。通常動作では見えません。CPUは自分自身の命令ストリームだけをデコードするため、エンディアンは一貫しているからです。しかし、たとえば別のターゲット向けに構築されたバイナリをデバッガーや逆アセンブラが調べるように、ホストをまたいでバイナリを読むときには重要になります。
完全な命令フェッチパイプラインは、フェッチ・デコード・実行サイクルの記事で扱っています。エンディアンは、メモリとプログラムカウンタの間で整合しなければならない最後の細部です。
なぜどちらの順序も普遍的なハードウェアの勝者ではないのか
x86上で の16進ダンプ、すなわち 78 56 34 12 を見ると、逆向きに感じます。目は左から右へ読み、桁は反転した対になり、頭の中での変換は扱いにくいものです。
リトルエンディアンには便利な性質があります。マルチバイト整数のアドレスは、同時にその最下位バイトのアドレスでもあります。1バイトずつの多倍長加算は、キャリーを伝播させながら増加するアドレス方向へ進めます。リトルエンディアン整数をその場で拡張しても、下位部分のアドレスを保てます。
ビッグエンディアンは補完的な性質を持ちます。バイトが桁の重要度順に現れるため、メモリダンプや辞書順比較は最上位部分から始まります。ネットワークバイト順は、マルチオクテット値にこの最上位オクテット優先の規約を使います。
現代のプロセッサーは通常、語全体をロードし、必要に応じてロード/ストア経路でバイトを並べ替えます。エンディアンはメモリとインターフェースの境界では依然として重要ですが、レジスタ内のリップルキャリー加算のブール結果を変えません。歴史的な採用は、アーキテクチャと互換性の決定を反映しており、普遍的に優れた1つの回路を反映したものではありません。
実行時にエンディアンを検出する
Cでの小さな実行時チェックは、Cが任意のオブジェクトへのエイリアスを許す unsigned char ポインタを通して、オブジェクト表現を調べられます。
int is_little_endian(void) {
unsigned int x = 1;
return *(const unsigned char *)&x == 1;
}
最小アドレスのバイトが1なら、最下位バイトが先です。エキゾチックな混在エンディアン実装では、偽の結果だけで通常のビッグエンディアン順を証明できるわけではありません。
<endian.h>、<sys/endian.h>、または __BYTE_ORDER__ のようなコンパイラマクロをC99に帰属させてはいけません。これらはプラットフォームまたはコンパイラの機能です。C23は <stdbit.h> を通じてネイティブエンディアンの識別子を追加しましたが、展開済みコンパイラの対応とプロジェクトの言語モードは異なります。移植可能な直列化では、より堅牢な設計はワイヤー上のバイトを明示的に定義し、シフト、マスク、または htonl/ntohl のような文書化されたライブラリー手続きで整数を組み立てまたは分解することです。
DigiSimでのシミュレーションでは、低いアドレスのバイトをロードしてレジスタの上位バイトへ置く小さな読み出し・変更・書き込みループを作り、期待パターンに対して結果を確認できます。これは配線の違いが値へどう影響するかを手で感じる方法です。2進数の3,500年の旅の記事は、人が読みやすい桁の並びが機械の利便性とどう相互作用するかという、より長い文脈にこれを置きます。
次に進む内容
エンディアンは、バイナリデータを交換するために必要なバイトレベル規約の1つです。ほかには整数符号化そのもの(2の補数による符号付き算術)と浮動小数点形式(IEEE 754)があります。フェッチ・デコード・実行の詳細解説は、これらの表現を命令処理へつなげます。
基本RAMメモリシステムテンプレートを開いて、バイトアドレス指定メモリの動作を見ましょう。次に32ビット値を4つのアドレスにまたがって書き、異なるバイト順で読み戻して、配線の違いを直接体感してください。



