149 lines
6.2 KiB
Markdown
149 lines
6.2 KiB
Markdown
|
|
---
|
|||
|
|
tags: []
|
|||
|
|
aliases:
|
|||
|
|
- TL;DR
|
|||
|
|
date created: 星期四, 十一月 27日 2025, 7:22:37 晚上
|
|||
|
|
date modified: 星期五, 十一月 28日 2025, 12:47:41 凌晨
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 大端序和小端序
|
|||
|
|
|
|||
|
|
## TL;DR
|
|||
|
|
|
|||
|
|
**端序 (Endianness)** 指的是**多字节数据**(如 `int`, `float`)在内存中存储的**字节排列顺序**。
|
|||
|
|
|
|||
|
|
- **大端序 (Big-Endian):** 高位字节存放在低地址(符合人类阅读习惯,从左到右)。**网络传输标准**。
|
|||
|
|
- **小端序 (Little-Endian):** 低位字节存放在低地址(高低位颠倒)。**x86/x64 架构标准**。
|
|||
|
|
- **核心影响:** 在进行网络编程(Socket)或跨平台文件解析时,必须进行字节序转换,否则读出的数值会完全错误。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. 核心概念与可视化
|
|||
|
|
|
|||
|
|
计算机内存就像一条长长的街道,每个“门牌号”(内存地址)只能住一个人(1 个字节,8 bits)。
|
|||
|
|
|
|||
|
|
当我们存储一个需要占多个门牌号的“大家伙”(比如 4 字节的整数 0x12345678)时,就产生了一个问题:应该把头(高位)放在小编号,还是把脚(低位)放在小编号?
|
|||
|
|
|
|||
|
|
假设内存地址从 `0x100` 开始增长,数据是十六进制的 `0x12345678`:
|
|||
|
|
|
|||
|
|
- **高位 (MSB):** `0x12` (数值最大的部分)
|
|||
|
|
- **低位 (LSB):** `0x78` (数值最小的部分)
|
|||
|
|
|
|||
|
|
### 可视化对比
|
|||
|
|
|
|||
|
|
|**内存地址**|**大端序 (Big-Endian)**|**小端序 (Little-Endian)**|
|
|||
|
|
|---|---|---|
|
|||
|
|
|**0x100 (低地址)**|**12** (高位 MSB)|**78** (低位 LSB)|
|
|||
|
|
|**0x101**|34|56|
|
|||
|
|
|**0x102**|56|34|
|
|||
|
|
|**0x103 (高地址)**|**78** (低位 LSB)|**12** (高位 MSB)|
|
|||
|
|
|**人类阅读视角**|`12 34 56 78` (顺眼)|`78 56 34 12` (反人类)|
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. 为什么会有两种标准?(底层原理)
|
|||
|
|
|
|||
|
|
这并非单纯的“习惯不同”,而是基于不同的工程权衡:
|
|||
|
|
|
|||
|
|
### 大端序 (Big-Endian) 的逻辑
|
|||
|
|
|
|||
|
|
- **直观性:** 内存中的顺序与人类手写数字的顺序一致。`123` 就是先写百位,再写个位。
|
|||
|
|
- **符号判断快:** 正负号(符号位)总是在第一个字节(低地址)。CPU 只要读第一个字节就能判断正负,无需读完整个数。
|
|||
|
|
- **应用场景:** **网络协议 (TCP/IP)**、Java 虚拟机、早期的 Motorola 68k 处理器。
|
|||
|
|
|
|||
|
|
### 小端序 (Little-Endian) 的逻辑
|
|||
|
|
|
|||
|
|
- **计算优势:** 计算机做加法是从低位开始算的(需要进位)。CPU 读取数据时,先读到低位(低地址),可以直接开始运算,无需等待高位读取完成。
|
|||
|
|
- **类型转换零开销:** 强制转换数据类型(如 `int32` 转 `int8`)时,**内存地址不需要变**。因为低位都在 `0x100`,只要把读取长度截断即可。而在大端序中,转成 `int8` 需要将地址偏移到 `0x103` 才能拿到低位。
|
|||
|
|
- **应用场景:** **Intel x86/x64 架构**、现代大部分 ARM 芯片(虽然 ARM 支持双端序,但在 Android/iOS 上默认配置为小端)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. 工程中的“坑”:网络字节序 Vs 主机字节序
|
|||
|
|
|
|||
|
|
在网络开发中,这是最容易出错的地方。
|
|||
|
|
|
|||
|
|
- **主机字节序 (Host Byte Order):** 取决于 CPU 架构。Intel CPU 是小端序。
|
|||
|
|
- **网络字节序 (Network Byte Order):** **强制规定为大端序**。
|
|||
|
|
|
|||
|
|
**典型故障流程:**
|
|||
|
|
|
|||
|
|
1. 你的 x86 服务器(小端)发送整数 `1` (`0x00000001`)。
|
|||
|
|
2. 如果不转换直接发,网线上跑的数据是 `01 00 00 00`(小端首字节)。
|
|||
|
|
3. 接收端(假设也是 x86)按照网络标准(大端)解析,认为收到的是 `0x01000000`(十进制 16,777,216)。
|
|||
|
|
4. **结果:** 发送了 1,对方收到了 1600 多万。
|
|||
|
|
|
|||
|
|
解决方案:
|
|||
|
|
|
|||
|
|
使用标准库函数进行显式转换(代码具备可移植性,若架构相同会编译为空操作):
|
|||
|
|
|
|||
|
|
- `htonl()`: Host to Network Long (32-bit)
|
|||
|
|
- `htons()`: Host to Network Short (16-bit)
|
|||
|
|
- `ntohl()`: Network to Host Long
|
|||
|
|
- `ntohs()`: Network to Host Short
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. 代码检测与验证 (C/C++)
|
|||
|
|
|
|||
|
|
这是一个经典面试题,也是检测当前环境端序的最简单方法。
|
|||
|
|
|
|||
|
|
```C
|
|||
|
|
#include <stdio.h>
|
|||
|
|
#include <stdint.h>
|
|||
|
|
|
|||
|
|
// 检查当前系统是否为小端序
|
|||
|
|
int is_little_endian() {
|
|||
|
|
uint32_t num = 1; // 0x00000001
|
|||
|
|
// 将 int 指针强转为 char 指针,只读取内存中第一个字节(低地址)
|
|||
|
|
char *byte_ptr = (char*)#
|
|||
|
|
|
|||
|
|
// 如果低地址存的是 1,说明低位在前 -> 小端序
|
|||
|
|
// 如果低地址存的是 0,说明高位在前 -> 大端序
|
|||
|
|
return (*byte_ptr == 1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
int main() {
|
|||
|
|
uint32_t data = 0x12345678;
|
|||
|
|
uint8_t *p = (uint8_t*)&data;
|
|||
|
|
|
|||
|
|
printf("Current System: %s\n", is_little_endian() ? "Little-Endian (小端)" : "Big-Endian (大端)");
|
|||
|
|
|
|||
|
|
printf("Memory Dump of 0x12345678:\n");
|
|||
|
|
for(int i = 0; i < 4; i++) {
|
|||
|
|
printf("Address +%d: 0x%02x\n", i, p[i]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return 0;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**x86 机器上的输出:**
|
|||
|
|
|
|||
|
|
```Plaintext
|
|||
|
|
Current System: Little-Endian (小端)
|
|||
|
|
Memory Dump of 0x12345678:
|
|||
|
|
Address +0: 0x78
|
|||
|
|
Address +1: 0x56
|
|||
|
|
Address +2: 0x34
|
|||
|
|
Address +3: 0x12
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. 自我反驳与局限性 (Self-Rebuttal)
|
|||
|
|
|
|||
|
|
虽然“网络是大端,x86 是小端”是共识,但以下情况需要注意:
|
|||
|
|
|
|||
|
|
- **单字节数据无关性:** ASCII 字符串(如 "Hello")不受端序影响,因为每个字符只占 1 字节,不存在“内部顺序”问题。只有 `int`、`short`、`long`、`float` 等多字节类型才受影响。
|
|||
|
|
- **位序 (Bit Endianness):** 我们讨论的是**字节序**。在极少数底层协议(如某些旧的串行通信协议)中,**一个字节内部的 8 个 bit** 传输顺序也分大端和小端。但在现代 CPU 和标准网络编程中,通常不需要关心位序,硬件会处理好。
|
|||
|
|
- **双端序架构 (Bi-endian):** ARM 和 PowerPC 架构实际上是可配置的。虽然安卓和 iOS 运行在 ARM 上通常配置为小端,但在某些基站或路由器设备上,ARM 可能运行在大端模式。**不能假设 ARM 永远是小端。**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. 总结
|
|||
|
|
|
|||
|
|
1. **大端序 (Big-Endian):** 顺撇子。高位在低地址。**网络标准。**
|
|||
|
|
2. **小端序 (Little-Endian):** 逆撇子。低位在低地址。**主机 (x86) 标准。**
|
|||
|
|
3. **铁律:** 只要涉及**跨机器通信**(网络)或**跨系统文件交换**,必须显式调用 `ntohl` / `htonl` 系列函数,严禁依赖默认行为。
|