プロセッサ・アーキテクチャ

エンディアン

メモリ上のバイト並び順(ビッグ/リトル)。

概要

エンディアン(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);

用途・ユースケース

シリアル通信プロトコルの実装

UARTSPII2Cでマルチバイト値を送受信する場合:

// 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ビット値を「下位ワードが先、ワード内はビッグエンディアン」という独自順で格納しました。現代の設計では使われませんが、レガシーシステムとの互換性対応で遭遇することがあります。

関連用語

参考リンク