跳转到内容
新建笔记

字节序:小端、大端与协议编解码

字节序描述一个多字节数值的各字节如何按地址或传输顺序排列。这里的“高字节、低字节”指在数值中的权重,与字节本身数值的大小无关,也不是地址高低的同义词。

用一个 32 位数区分两种排列

跳转到“用一个 32 位数区分两种排列”

把 0x12345678 放在从地址 A 开始的四个字节中:

地址递增顺序AA+1A+2A+3
小端 Little-endian78563412
大端 Big-endian12345678

小端把最低有效字节放在最低地址;大端把最高有效字节放在最低地址。无论内存如何排列,数值仍是 0x12345678,通常写出的十六进制数也不会倒着写。

单个字节没有这个多字节排列问题。串行线路先发送哪一位属于位传输顺序,与多字节整数的字节序需要分别规定。字符数组中的字符顺序也不能直接当作整数的字节序。

协议与文件格式应自己规定字节序

跳转到“协议与文件格式应自己规定字节序”

网络字节序通常指大端,例如传统 Internet 协议使用的 16 位或 32 位整数转换接口。但具体应用协议、设备帧和文件格式可以另有规定,不能因为“通过网络发送”就自动采用大端。

大端中位于低地址的最高有效字节,对数值的权重反而最大;这不是网络标准选择它的因果解释。对数据交换更有用的规则是:发送方与接收方都按照同一个格式编码、解码。

场景应明确的内容
网络或串口协议每个字段的位宽、符号、字节序与偏移;多寄存器字段还可能另有字序
二进制文件或数据库记录文件格式规定、版本与字段编码;不能直接保存任意宿主结构体内存
跨平台交换按外部格式解码成宿主数值,写出时再编码;不依赖两端 CPU 恰好相同
库接口接口需要的是宿主数值、网络顺序值还是已经编码的字节,避免重复转换

htons、htonl 分别用于 16 位、32 位宿主到网络顺序的转换;ntohs、ntohl 做反向转换。这些是网络编程环境提供的接口,不是 ISO C/C++ 本身的一组通用函数。大端宿主上它们可以不需要交换字节,小端宿主上通常需要交换;应用程序应按接口约定调用,而不是自行猜测机器类型。

不依赖宿主字节序的 C 编解码

跳转到“不依赖宿主字节序的 C 编解码”

下面以存在 uint8_t、uint32_t 的 C11 环境为例。四字节缓冲区已经由调用方保证有效;若处理外部输入,应先检查剩余长度,再调用解码函数。

#include <assert.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
static uint32_t load_be32(const uint8_t p[4]) {
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
((uint32_t)p[2] << 8) | (uint32_t)p[3];
}
static uint32_t load_le32(const uint8_t p[4]) {
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
static void store_be32(uint8_t p[4], uint32_t value) {
p[0] = (uint8_t)(value >> 24);
p[1] = (uint8_t)(value >> 16);
p[2] = (uint8_t)(value >> 8);
p[3] = (uint8_t)value;
}
static void store_le32(uint8_t p[4], uint32_t value) {
p[0] = (uint8_t)value;
p[1] = (uint8_t)(value >> 8);
p[2] = (uint8_t)(value >> 16);
p[3] = (uint8_t)(value >> 24);
}
int main(void) {
const uint32_t values[] = {0, UINT32_C(0x12345678), UINT32_MAX};
uint8_t be[4], le[4];
for (size_t n = 0; n < sizeof values / sizeof values[0]; ++n) {
store_be32(be, values[n]);
store_le32(le, values[n]);
assert(load_be32(be) == values[n]);
assert(load_le32(le) == values[n]);
}
store_be32(be, UINT32_C(0x12345678));
store_le32(le, UINT32_C(0x12345678));
const uint8_t expected_be[] = {0x12, 0x34, 0x56, 0x78};
const uint8_t expected_le[] = {0x78, 0x56, 0x34, 0x12};
assert(memcmp(be, expected_be, sizeof be) == 0);
assert(memcmp(le, expected_le, sizeof le) == 0);
printf("decoded: %08" PRIx32 "\n", load_be32(be));
return 0;
}

先转成 uint32_t 再左移,避免让有符号 int 的整数提升参与高位移位。代码没有把未对齐的字节指针强转为整数指针,也不依赖结构体填充或宿主对象布局。

架构名称不能代替格式说明

跳转到“架构名称不能代替格式说明”

x86 的常用多字节整数内存表示是小端。Arm、MIPS 等架构家族存在不同字节序能力或配置,SPARC 的常见环境使用大端;读设备文档时仍应确认具体处理器、运行状态、ABI 和外部数据格式。不要据某个 RISC 家族名称认定所有设备都采用同一种排列。