概要
エンディアン(Endianness)は、複数バイトからなるデータをメモリに格納する際のバイトの並び順を指します。上位バイト(最上位バイト、MSB: Most Significant Byte)を先に格納する「ビッグエンディアン」と、下位バイト(最下位バイト、LSB: Least Significant Byte)を先に格納する「リトルエンディアン」の2種類が主流です。
例として、32ビット値 0x12345678 をアドレス 0x1000 に格納する場合:
ビッグエンディアン: リトルエンディアン:
アドレス 値 アドレス 値
0x1000 0x12 0x1000 0x78
0x1001 0x34 0x1001 0x56
0x1002 0x56 0x1002 0x34
0x1003 0x78 0x1003 0x12
エンディアンが問題になるのは、異なるアーキテクチャ間でデータを共有する場合(シリアル通信・ネットワーク・ファイル形式)、またはバイト単位でメモリを直接操作する場合です。
現代の組み込みシステムでは:
- リトルエンディアン: x86・ARM(デフォルト)・RISC-V
- ビッグエンディアン: ネットワークプロトコル(TCP/IP標準)・一部のDSP・SPARC・PowerPC(デフォルト)
- バイエンディアン(可変): ARM(エンディアン切り替え可)・MIPS
歴史・背景
「エンディアン」という言葉はジョナサン・スウィフトの小説「ガリバー旅行記」(1726年)に登場する、卵をどちらの端から割るかで戦争をするLilliputとBlefuscuの国を由来としています。Danny Cohen(ネットワーク研究者)が1980年のペーパー「On Holy Wars and a Plea for Peace」でこの用語をコンピュータ科学に転用しました。
初期のミニコンピュータ(PDP-11等)やIntel 8080(1974年)がリトルエンディアンを採用。一方でIBM、Motorola 68000系、ネットワークプロトコルの標準化ではビッグエンディアンが採用されました。
ARMはARM7TDMI時代からバイエンディアン(設計時に選択)として、現代のCortex-Mはリトルエンディアン固定(一部バイエンディアン対応)が主流です。
ネットワーク通信では「ネットワークバイトオーダー = ビッグエンディアン」がRFC 1700で規定されており、TCP/IPプロトコルはすべてビッグエンディアンでヘッダを記述します。
技術仕様
バイエンディアンプロセッサ
一部のプロセッサはソフトウェアでエンディアンを切り替えられます:
| プロセッサ | デフォルト | 切り替え方法 |
|---|---|---|
| ARM Cortex-A | リトルエンディアン | CPSR/SCSRのEビット(EE)で設定 |
| ARM Cortex-M3/M4 | リトルエンディアン | ほとんどの実装でリトルエンディアン固定 |
| MIPS | ビッグエンディアン | コンフィグレーションピン |
| PowerPC | ビッグエンディアン | HIDレジスタで設定 |
| SPARC V9 | ビッグエンディアン | 命令ごとに指定可 |
各プロトコル・フォーマットのエンディアン
| プロトコル/フォーマット | エンディアン | 備考 |
|---|---|---|
| TCP/IP(IPv4・IPv6) | ビッグエンディアン | ネットワークバイトオーダー |
| USB | リトルエンディアン | 全ディスクリプタ |
| CAN | ビッグエンディアン | デフォルト |
| Modbus RTU | ビッグエンディアン | |
| SPI(仕様依存) | デバイス依存 | MSBファーストが多い |
| BLE(Bluetooth LE) | リトルエンディアン(ATT/GATT) | アドレスはリトルエンディアン |
| PNG | ビッグエンディアン | |
| JPEG/EXIF | リトルエンディアン(主) | |
| WAV | リトルエンディアン | |
| ELF(x86-64) | リトルエンディアン | |
| ELF(MIPS) | ビッグエンディアン |
動作原理
ユニオンによるエンディアン確認
#include <stdint.h>
// エンディアン確認の古典的方法
int is_little_endian(void) {
union {
uint32_t i;
uint8_t c[4];
} u = {0x12345678};
return u.c[0] == 0x78; // リトルエンディアンなら最下位バイトが先
}
// コンパイル時確認(GCC拡張)
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
#define HOST_IS_LITTLE_ENDIAN 1
#else
#define HOST_IS_LITTLE_ENDIAN 0
#endif
バイトスワップ
エンディアン変換にはバイトスワップ(bswap)操作を使います:
#include <stdint.h>
// 手動実装
static inline uint16_t bswap16(uint16_t x) {
return (uint16_t)((x >> 8) | (x << 8));
}
static inline uint32_t bswap32(uint32_t x) {
return ((x & 0xFF000000) >> 24) |
((x & 0x00FF0000) >> 8) |
((x & 0x0000FF00) << 8) |
((x & 0x000000FF) << 24);
}
// GCC組み込み関数(1命令に最適化される)
uint32_t swapped = __builtin_bswap32(value);
uint16_t swapped16 = __builtin_bswap16(value16);
// ARM Cortex-M: REV命令に変換される(1サイクル)
// x86: BSWAP命令に変換される(1サイクル)
ネットワークバイトオーダー変換
POSIXではネットワーク変換用の標準関数が提供されています:
#include <arpa/inet.h> // LinuxのPOSIX環境
// または
#include <netinet/in.h>
// ホスト→ネットワーク(ビッグエンディアン)変換
uint32_t net_val32 = htonl(host_val32); // Host TO Network Long
uint16_t net_val16 = htons(host_val16); // Host TO Network Short
// ネットワーク→ホスト変換
uint32_t host_val32 = ntohl(net_val32); // Network TO Host Long
uint16_t host_val16 = ntohs(net_val16); // Network TO Host Short
組み込みベアメタル環境では、これらの関数が使えない場合があるため自前で実装します:
// 組み込みベアメタル用マクロ
#define HTONL(x) bswap32(x) // リトルエンディアンホストの場合
#define NTOHL(x) bswap32(x)
// または条件コンパイルで対応
#if defined(__LITTLE_ENDIAN__)
#define HTONS(x) bswap16(x)
#define HTONL(x) bswap32(x)
#else
#define HTONS(x) (x)
#define HTONL(x) (x)
#endif
ポインタキャストによるエンディアン操作(注意)
uint32_t value = 0x12345678;
uint8_t *bytes = (uint8_t *)&value; // 厳密なエイリアシングに注意
// リトルエンディアン(x86/ARM)の場合:
// bytes[0] == 0x78, bytes[1] == 0x56, bytes[2] == 0x34, bytes[3] == 0x12
// 推奨: memcpyを使う(最適化でも正しく動作)
uint32_t safe_read;
memcpy(&safe_read, some_byte_ptr, 4);
用途・ユースケース
シリアル通信プロトコルの実装
// I2C 16ビットレジスタ読み取り(ビッグエンディアンデバイスの場合)
uint8_t buf[2];
i2c_read(dev_addr, reg_addr, buf, 2);
// デバイスがビッグエンディアンでホストがリトルエンディアン
uint16_t value = ((uint16_t)buf[0] << 8) | buf[1];
// デバイスがリトルエンディアンの場合
uint16_t value = ((uint16_t)buf[1] << 8) | buf[0];
// または: memcpy(&value, buf, 2); // リトルエンディアンホストで直接使える
CAN バスフレームのエンディアン
CAN バスのデータフィールド(0〜8バイト)はビッグエンディアン(Motorola バイトオーダー)とリトルエンディアン(Intel バイトオーダー)の両方が混在します。CANdb++やDBC形式でシグナルごとにバイトオーダーを定義します。
// CANフレーム(ビッグエンディアン信号)のデコード例
uint8_t can_data[8] = {0x01, 0x23, 0x45, 0x67, ...};
// ビット0から16ビット、ビッグエンディアン
uint16_t signal = ((uint16_t)can_data[0] << 8) | can_data[1]; // 0x0123
ネットワークパケット処理
TCP/IPスタックではヘッダのエンディアン変換が必須です:
// IPv4ヘッダの解析(ネットワークバイトオーダー = ビッグエンディアン)
struct ip_header {
uint8_t version_ihl;
uint8_t dscp_ecn;
uint16_t total_length; // ビッグエンディアンで格納
uint16_t identification;
// ...
} __attribute__((packed));
// ホスト環境で使う際は変換が必要(リトルエンディアンホストの場合)
struct ip_header *hdr = (struct ip_header *)packet;
uint16_t len = ntohs(hdr->total_length); // ビッグ→リトルエンディアン変換
ファームウェアバイナリのバイトオーダー
フラッシュメモリに書き込むファームウェアイメージも、ターゲットのエンディアンに合わせる必要があります:
// ARM Cortex-M(リトルエンディアン)のベクタテーブル
// フラッシュ先頭の8バイト
const uint32_t vectors[] __attribute__((section(".isr_vector"))) = {
0x20010000, // 初期スタックポインタ(リトルエンディアンで格納)
(uint32_t)reset_handler, // リセットベクタ
// ...
};
実装・開発のポイント
エンディアン問題のデバッグ
エンディアン起因のバグは値が「逆順」になるのが特徴です:
// デバッグ時のエンディアン確認
void print_bytes(const void *ptr, size_t size) {
const uint8_t *p = (const uint8_t *)ptr;
for (size_t i = 0; i < size; i++) {
printf("%02X ", p[i]);
}
printf("\n");
}
uint32_t val = 0x12345678;
print_bytes(&val, 4);
// リトルエンディアン: 78 56 34 12
// ビッグエンディアン: 12 34 56 78
cmakeでのエンディアン検出
include(TestBigEndian)
TEST_BIG_ENDIAN(IS_BIG_ENDIAN)
if(IS_BIG_ENDIAN)
target_compile_definitions(myapp PRIVATE WORDS_BIGENDIAN)
endif()
Wiresharkでのエンディアン確認
ネットワークプロトコル解析ではWiresharkがエンディアン変換を自動で行って値を表示してくれます。独自プロトコルの場合はWireshark DissectorをLuaで実装する際にエンディアンを正しく指定します:
-- Wireshark Lua Dissector でのエンディアン
local f_value_be = ProtoField.uint16("myproto.value_be", "Big Endian Value", base.HEX)
local f_value_le = ProtoField.uint16("myproto.value_le", "Little Endian Value", base.HEX, nil, nil, nil, ENC_LITTLE_ENDIAN)
他技術との比較
ビッグエンディアン vs リトルエンディアン(トレードオフ)
| 観点 | ビッグエンディアン | リトルエンディアン |
|---|---|---|
| 人間の読みやすさ | 直感的(0x12345678が順番通り) | 逆順になり読みにくい |
| ネットワーク標準 | TCP/IP標準 | 追加変換が必要 |
| x86/ARM採用 | 非採用(デフォルト) | ネイティブ |
| 下位バイトアクセス | 下位アドレス+オフセット | アドレスそのまま |
| マルチバイト比較 | 先頭から大小比較可能 | - |
リトルエンディアンの利点は「8ビット・16ビット・32ビット値をすべて同じ先頭アドレスで読める」ことです。例えば0x1000番地の32ビット値の下位8ビットも0x1000番地で読めます。ビッグエンディアンでは下位8ビットは0x1003番地になります。
ミドルエンディアン・ミックスドエンディアン
まれに「ミドルエンディアン」や「PDP-エンディアン」と呼ばれる変則的な並び順も存在します。例えばPDP-11は32ビット値を「下位ワードが先、ワード内はビッグエンディアン」という独自順で格納しました。現代の設計では使われませんが、レガシーシステムとの互換性対応で遭遇することがあります。