我們時常會以 由左到右、由上到下 的方式書寫,
例: 一數 『 九 千 四 百 八 十 七 』,
通常習慣寫成:『 9487 』,而不是『 7849 』,
(雖然,有些國家或族群的習慣可能是後者)
你 不能說他錯,因為這只是習慣的不同。
位元組順序 (Byte Order),或稱 端序 (Endianness),即是指 位元組 的排列順序,
同理,不同的硬體架構、網路協議… 其用法不盡相同,
沒有絕對的好壞,只有適合與否。
目錄
端 (Endian)
如前述的 『 9487 』範例一般,
以 最高有效位元組 (Most Significant Byte, MSB) [註1] 逐一儲存位元組者,稱為 大頭端 (big-endian)。
反之,如『 7849 』般,
以 最低有效位元組 (Least Significant Byte, LSB) [註1] 逐一儲存位元組者,稱為 小頭端 (little-endian)。
[註1]:
注意,連結內容為: 最高/低 有效位元,而非 位元組 !
僅做理解名詞用。
以儲存 0x1234ABCD 為例
大頭端 (big-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 取得
檢測
以下提供一些檢測本機「預設」位元組順序 (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 *) #
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),
執行結果就是:
— — 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 需注意的細節,
使用了錯誤的順序,便會造成檔案損毀或例外錯誤。
範例原始檔 🤖🤖🤖
在《位元組順序 (Byte Order or Endianness) — big-endian vs. little-endian》中有 1 則留言
Hi 請問 這文章能借轉到臉書私人社團嗎?