4、字节序:大端序与小端序、网络字节序、如何判断系统字节序、字节序转换
字节序这东西,说白了就是「多字节数据在内存里怎么排排坐」。我早年刚接触嵌入式时,被它坑过不止一次。两台设备明明协议对好了,数据就是解析不对——后来发现,一个用大端,一个用小端,鸡同鸭讲。
这一章咱们就把字节序彻底掰扯清楚。
4.1 大端序与小端序:两种世界观
假设有一个 32 位整数 0x12345678,它在内存里占 4 个字节。这 4 个字节怎么放?
- 大端序(Big-Endian):高位字节存低地址。也就是
0x12在最前面,像我们平时写数字的顺序。 - 小端序(Little-Endian):低位字节存低地址。
0x78在最前面,反着来。
举个例子,地址从 0x1000 开始:
| 地址 | 大端序 | 小端序 |
|---|---|---|
| 0x1000 | 0x12 | 0x78 |
| 0x1001 | 0x34 | 0x56 |
| 0x1002 | 0x56 | 0x34 |
| 0x1003 | 0x78 | 0x12 |
你想想看,大端序更符合人类的阅读习惯,小端序则更贴近 CPU 的运算逻辑——从低地址开始取数,直接就能做加法。
核心记忆点:大端 = 高位在前,小端 = 低位在前。别搞反了。
4.2 网络字节序:为什么必须统一
网络传输是跨平台的。你一台 x86 小端机器,跟一台 PowerPC 大端机器通信,如果不统一字节序,数据就是乱的。
所以 TCP/IP 协议栈规定:网络字节序统一使用大端序。所有多字节数据在发送前,必须转成大端;接收后再转回本机字节序。
我个人习惯在写网络程序时,凡是遇到 uint16_t 或 uint32_t,一律用 htons / htonl 包装一下。哪怕本机就是大端,也写上——这是给读代码的人一个明确信号:这里涉及网络传输。
小技巧:用 Wireshark 抓包时,看到的十六进制数据就是网络字节序。如果你发现抓到的 0x1234 在内存里变成了 0x3412,说明本机是小端,需要转换。
4.3 如何判断系统字节序
写跨平台代码时,经常需要知道当前系统是大端还是小端。我见过有人写死宏定义,结果换了个架构就崩了。
最可靠的方法:运行时检测。
#include <stdio.h>
#include <stdint.h>
int is_little_endian() {
uint16_t test = 0x0001;
uint8_t *p = (uint8_t *)&test;
return p[0] == 0x01; // 低地址存的是低位字节
}
int main() {
if (is_little_endian())
printf("小端序\n");
else
printf("大端序\n");
return 0;
}
原理很简单:0x0001 的低位字节是 0x01,高位是 0x00。如果低地址存的是 0x01,那就是小端。
我曾经在移植一个旧项目时,发现对方用 #ifdef __BIG_ENDIAN__ 来判断,结果在 ARM 上死活不对。后来改成运行时检测,问题就解决了。
注意:不要依赖编译器预定义宏来判断字节序。不同编译器、不同版本、不同平台,宏的名字和定义方式都不一样。运行时检测最安全。
4.4 字节序转换:手动还是用库
标准库提供了现成的转换函数,POSIX 和 Windows 都有:
| 函数 | 作用 | 平台 |
|---|---|---|
htons() |
Host to Network Short (16位) | POSIX / Winsock |
htonl() |
Host to Network Long (32位) | POSIX / Winsock |
ntohs() |
Network to Host Short | POSIX / Winsock |
ntohl() |
Network to Host Long | POSIX / Winsock |
如果本机是大端,这些函数就是空操作;如果是小端,就做字节交换。你不需要关心内部实现,直接用就行。
但有些场景——比如解析二进制文件格式——你可能需要手动转换。这时候我建议写一个通用宏:
#include <stdint.h>
// 交换16位字节序
#define SWAP16(x) ((uint16_t)( \
((x) >> 8) | \
((x) << 8) \
))
// 交换32位字节序
#define SWAP32(x) ((uint32_t)( \
((x) >> 24) | \
((x) >> 8) & 0x0000FF00 | \
((x) << 8) & 0x00FF0000 | \
((x) << 24) \
))
嗯,这里要注意:宏的参数要加括号,防止运算符优先级问题。我见过有人写 SWAP16(a | b) 结果展开成一团乱麻。
4.5 字节序与二进制文件格式
很多二进制文件格式会在文件头里声明字节序。比如:
- ELF 文件:
e_ident[EI_DATA]字段,1 表示小端,2 表示大端。 - BMP 图片:文件头固定小端,但
BI_BITFIELDS压缩格式可能混用。 - TIFF 图片:前两个字节是
0x4949(小端)或0x4D4D(大端)。
解析这类文件时,第一步就是读字节序标志,然后根据它决定后续字段怎么读。我当年写 BMP 解析器时,默认按小端读,结果遇到一个用大端保存的 BMP 文件,直接崩了。后来加了个字节序检测,才搞定。
避坑指南:解析任何二进制格式,先读字节序标志。不要假设文件一定是小端或大端。
4.6 一张图看懂字节序
下面这张 SVG 图,把大端、小端、网络字节序、转换关系串起来了:
4.7 实战:解析一个跨平台二进制文件
假设你要解析一个自定义的二进制文件,文件头结构如下:
typedef struct {
uint16_t magic; // 魔数,固定 0xABCD
uint8_t endian; // 0x01 小端,0x02 大端
uint32_t data_len; // 数据长度
uint32_t checksum; // 校验和
} FileHeader;
解析步骤:
- 先读
magic,验证文件格式。 - 读
endian,确定字节序。 - 根据字节序,决定后续字段是否需要转换。
代码示例:
#include <stdio.h>
#include <stdint.h>
#include <arpa/inet.h> // 提供 ntohl / ntohs
int parse_header(FILE *fp, FileHeader *hdr) {
// 先按小端读入
fread(&hdr->magic, 2, 1, fp);
fread(&hdr->endian, 1, 1, fp);
fread(&hdr->data_len, 4, 1, fp);
fread(&hdr->checksum, 4, 1, fp);
// 判断字节序
if (hdr->endian == 0x02) {
// 文件是大端,本机是小端时需要转换
hdr->magic = ntohs(hdr->magic);
hdr->data_len = ntohl(hdr->data_len);
hdr->checksum = ntohl(hdr->checksum);
}
// 如果文件是小端,且本机也是小端,什么都不用做
return 0;
}
嗯,这里有个细节:magic 是 2 字节,用 ntohs;data_len 和 checksum 是 4 字节,用 ntohl。别搞混了。
个人经验:我习惯在解析函数里统一用网络字节序函数,而不是自己写 if-else 做字节交换。这样代码更清晰,也更容易移植。
4.8 小结
字节序是二进制世界里绕不开的话题。大端和小端只是两种不同的排列方式,没有谁对谁错。关键是:读写双方必须约定一致。
网络字节序强制使用大端,解决了跨平台通信的问题。判断系统字节序用运行时检测最可靠。转换函数用标准库的 htons / htonl 系列,或者自己写宏。
解析二进制文件时,永远先读字节序标志。这是我在多次踩坑后,最想告诉你的话。