計算機組織/概論

位元組順序 (Byte Order or Endianness) — big-endian vs. little-endian

我們時常會以 由左到右、由上到下 的方式書寫,
例: 一數 『 九 千 四 百 八 十 七 』,
通常習慣寫成:『 9487 』,而不是『 7849 』,
(雖然,有些國家或族群的習慣可能是後者)
不能說他錯,因為這只是習慣的不同。
 
位元組順序 (Byte Order),或稱 端序 (Endianness),即是指 位元組 的排列順序
同理,不同的硬體架構、網路協議… 其用法不盡相同,
沒有絕對的好壞,只有適合與否。
 
Right and left arrow
 


 

端 (Endian)

如前述的 『 9487 』範例一般,
最高有效位元組 (Most Significant Byte, MSB) [註1] 逐一儲存位元組者,稱為 大頭端 (big-endian)
 
反之,如『 7849 』般,
最低有效位元組 (Least Significant Byte, LSB) [註1] 逐一儲存位元組者,稱為 小頭端 (little-endian)
 
[註1]:
注意,連結內容為: 最高/低 有效位元,而非 位元 !
僅做理解名詞用。
 
 

以儲存 0x1234ABCD 為例

大頭端 (big-endian):
big-endian
 
 
小頭端 (little-endian):
little-endian
 
(另還有 Middle-endian,不再累述)
 
 

由來

端 (Endian) 的由來,相當有趣 😂:

「endian」一詞,來源於十八世紀愛爾蘭作家喬納森·斯威夫特(Jonathan Swift)的小說《格列佛遊記》(Gulliver’s Travels)。
小說中,小人國為水煮蛋該從大的一端(Big-End)剝開還是小的一端(Little-End)剝開而爭論,
爭論的雙方分別被稱為「大頭派」和「小頭派」。  — 維基百科

 


 

主機位元組順序 (Host Byte Order)

 

big-endian

每台計算機因應其指令集架構 (Instruction Set Architecture, ISA),
字組定址 (word addressing)、位元組順序 (Byte Order) 不盡相同,
早期的 MIPS架構 就是 大頭端 陣營 !
 
big-endian 適合人類習慣,
逐位元組 Memory dump 超方便閱讀 😆
且在許多情況下 (如: 欲做數值排序、估計值、符號判斷…),
直接檢索 最高有效位元組,相當有用。
 
 

little-endian

最廣為人知的 小頭端 (little-endian) 就屬 — intel x86、x86-64 處理器啦 !
 
但既然大頭端這麼直覺,為何還要小頭端呢? 🤔
一個常見的觀點是 — 相同位址 (same address):
小頭端 (little-endian) 中,一個值不論是用 8、16、32.. 位元的方式儲存,
都可藉由相同的基底位址存取,簡化了硬體的設計,並做到向下兼容。
 
Ex:
8-bit data = 0xCD
換成 16-bit = 0x00CD
換成 32-bit = 0x000000CD
 

皆透過 相同位址 0x0000 取得

 
little-endian-same-address
 


 

檢測

以下提供一些檢測本機「預設」位元組順序 (Byte Order) 的方法:
 
 

C

C 語言,可藉由 指標轉型 與 間接運算子(*) 達成:

#include <stdio.h>

int main() {
    int i = 1;
    char *c = (char *) &i;

    if (*c)
        printf("LITTLE_ENDIAN\n");
    else
        printf("BIG_ENDIAN\n");

    return 0;
}

 
或者,藉由觀察位址變化:

#include <stdio.h>

int main() {
    int num = 0x1234ABCD;

    // 指標轉型: 將位址轉型為 指向 char
    char *ptrNum = (char *) &num;

    for (int i = 0; i < 4; i++)
        printf("%p: %02x \n", (void *) ptrNum, (unsigned char) *ptrNum++);

    return 0;
}

 
 

Java

Java 能使用 ByteOrder 類別:

import java.nio.ByteOrder;

public class Main {

    public static void main(String[] args) {
        System.out.println(ByteOrder.nativeOrder());
    }
}

 
 

C#

C# 使用 BitConverter 類別:

Console.WriteLine( "IsLittleEndian:  {0}", 
    BitConverter.IsLittleEndian );

 
 

PHP

PHP 則是使用 pack 方法:

<?php

$result = "BIG_ENDIAN";
$i = 0x12345678;

// 將 i 依本機 Byte Order 打包為 unsigned long
$uLong = pack('L', $i);

// 將 $uLong 依 little-endian 打包為 unsigned long
if ($i === current(unpack('V', $uLong))) {
    $result = "LITTLE_ENDIAN";
}

echo $result;

 
我目前使用的 MacBook Pro (Retina, 13-inch, Early 2015),
執行結果就是:
mac-info
 
— — LITTLE_ENDIAN 😇
 


 

bi-endianness

不是我少打一個 g (big) !!!

 
為了提高性能、兼容性…
現今許多架構 (如: PowerPC、MIPS、ARM、IA-64…) 皆可透過『切換』的方式,
來支援 big、little 兩種順序,也就是 雙端 (bi-endianness) 啦!
 
這也是為何,上方說的是:
檢測『預設』位元組順序 (許多個人電腦,皆預設為 little-endian)。
 


 

網路位元組順序 (Network Byte Order)

由於,不同的主機架構端序不盡相同,彼此在網路上傳輸,就需有順序規範:

使 IP 位址, Port, 封包… 能夠通用

 
也就是 網路位元組順序 (Network Byte Order) 啦 !
 
大部分網路協定 (ex: TCP、UDP、IPv4、IPv6…),
皆是使用 大頭端 (big-endian),因此兩者通常被視為等價。
 
 

轉換

像是,十進位的數字 80 (= 5016),
儲存在 little-endian 中,可能長這樣:

 
 
在 big-endian,則變成:

 
也就是 little-endian 中的     134217728010 😑…
 
幸好,Berkeley Socket 定義了一組函式 (通常是 巨集),
以使 主機位元組順序 與 網路位元組順序 能夠互相轉換。
 
也就是鼎鼎大名的:

1. htons
Return host_uint16 converted to network byte order

2. htonl
Return host_uint32 converted to network byte order

3. ntohs
Return net_uint16 converted to host byte order

4. ntohl
Return net_uint32 converted to host byte order

[註]
(h 指的是 host (主機),n 是 network,u 是 unsigned (無號),
s 是 short integer (短整數),l 是 long integer (長整數) [註2])
 
 
[註2]:
需要小心的是,這是函數「原型」的命名方式:

早期多數系統,short integer 與 long integer,分別是 16 位元 與 32 位元。
然而,現今的 long integer 通常已不再是 32 位元!

 
許多 socket program,都看的到這幾個函式的蹤影,
來看個使用範例:
 
(礙於篇幅,不加入 socket 部分,
待撰寫 socket 的使用時,再補充說明)

#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <memory.h>

int main(int argc, char **argv) {
    struct sockaddr_in svaddr;

    char *address = "127.0.0.1";
    int port_num = 9527;

    /* Clear structure */
    memset(&svaddr, 0, sizeof(struct sockaddr_in));

    svaddr.sin_family = AF_INET;

    // 將 port 由 本機位元組順序 轉換為 網路位元組順序
    svaddr.sin_port = htons(port_num);

    printf("--------(1)--------\n");
    printf("欲轉換 port: %i\n", port_num);
    printf("-------Result-------\n");
    printf("htons: %i\n", svaddr.sin_port);

    printf("\n\n");

    // 將 address 由 本機位元組順序 轉換為 網路位元組順序
    if (inet_pton(AF_INET, address, &svaddr.sin_addr) <= 0) {
        printf("inet_pton failed for address %s\n", address);
        exit(EXIT_FAILURE);
    }

    printf("--------(2)--------\n");
    printf("欲轉換位址: %s\n", address);
    printf("-------Result-------\n");
    printf("inet_pton: %p\n", svaddr.sin_addr);

    return 0;
}


/*
 * Result:
 *
 * --------(1)--------
 * 欲轉換 port: 9527
 * -------Result-------
 * htons: 14117
 *
 * --------(2)--------
 * 欲轉換位址: 127.0.0.1
 * -------Result-------
 * inet_pton: 0x100007f
 */

 
中間有另一個重要的函式 — inet_pton
用以取代傳統的 inet_aton、inet_addr…。
 
可以將 IPv4、IPv6位址,轉換為 網路位元組順序,
其中 p 是 presentation (表示式),n 是 network。
 
Q: 如果本機是 big-endian,是否就不需用這些函式?

是的,但考量到程式的可攜性,
仍應使用這些函式,避免不必要的問題。

 


 

總結

除了 IP 與 埠號,資料格式本身,也必須定義位元組順序、編組或序列化格式…,
如:訊框 (frame), 遠端程序呼叫 (RPC) 的 外部資料表示方式 (XDR)、XML…。
 
因此,不論是跨網路 或是 儲存裝置 的資料傳輸,
位元組順序 (Byte Order),時常是 coding 需注意的細節,
使用了錯誤的順序,便會造成檔案損毀或例外錯誤。
 
範例原始檔 🤖🤖🤖
 
 

作者: 鄭中勝

喜愛音樂,但不知為何總在打程式 ?
期許能重新審視、整理自身所學,幫助有需要的人。

發表迴響