3、字节序与位操作:大端小端、位掩码、移位操作,Python中的struct模块

做广播数据解析,说白了就是在跟字节打交道。你从空中抓下来一包数据,里面每个bit都有它的使命。但这里有个坑——同样的字节,不同的解读方式,结果天差地别。

我记得刚入行那会儿,调一个温湿度传感器,明明文档说温度在第3~4字节,我读出来却是乱码。折腾了半天,才发现是字节序搞反了。嗯,从那以后,我每次看协议文档,第一件事就是确认:这玩意儿是大端还是小端?

3.1 大端与小端:谁先谁后?

先讲个最直观的理解。你想想看,我们写数字"1234",左边是高位,右边是低位。这叫大端序——高位字节在前。

但有些处理器偏不,它要把低位字节放在前面。比如0x1234,它存成0x34 0x12。这就是小端序。

核心区别一句话:

  • 大端(Big-Endian):高位字节在低地址,符合人类阅读习惯
  • 小端(Little-Endian):低位字节在低地址,符合CPU处理习惯

我在项目中遇到过最典型的场景:BLE广播包。蓝牙核心规范里,大部分多字节字段都是小端序。比如广播信道PDU里的长度字段、UUID、公司ID,统统小端。但有些私有协议偏要用大端,你说气不气人?

场景 常见字节序 举例
BLE广播包 小端 UUID、公司ID、长度字段
TCP/IP协议栈 大端(网络字节序) IP地址、端口号
Modbus RTU 大端 寄存器值、CRC
Intel x86架构 小端 本地内存存储
ARM架构(可配置) 默认小端 多数嵌入式系统

避坑指南:我曾经在解析一个私有广播协议时,文档里只写了"数据格式遵循标准网络字节序"。结果我按大端解析了三天,怎么都对不上。最后翻出逻辑分析仪抓波形,才发现人家说的"标准"是BLE标准——小端!所以,别信文档,信实测。

3.2 位掩码与移位操作:庖丁解牛

广播数据里,很多信息不是整字节存的。比如一个字节8个bit,可能前3bit表示类型,后5bit表示长度。这时候怎么办?位掩码 + 移位就是你的手术刀。

3.2.1 位掩码:只取我想要的

位掩码,说白了就是一个二进制"筛子"。你想取哪几位,就把对应的bit置1,其他置0。

# 假设一个字节 data = 0b10110110
# 我想取低4位(bit0~bit3)
mask = 0b00001111  # 0x0F
result = data & mask  # 结果:0b00000110

# 我想取高2位(bit6~bit7)
mask = 0b11000000  # 0xC0
result = (data & mask) >> 6  # 先与,再右移6位

我个人习惯把常用的掩码定义成常量,比如:

MASK_LOW_NIBBLE = 0x0F  # 低半字节
MASK_HIGH_NIBBLE = 0xF0  # 高半字节
MASK_BIT0 = 0x01
MASK_BIT1 = 0x02
MASK_BIT2 = 0x04
MASK_BIT3 = 0x08
# ...以此类推

小技巧:在Python里,你可以用 bin() 函数把结果转成二进制字符串来调试。比如 bin(data & mask),一眼就能看出对不对。

3.2.2 移位操作:对齐到正确位置

取出来之后,往往还要右移到最低位,才能得到真正的数值。反过来,组装数据时要左移到正确位置。

# 解析场景:一个字节,高3bit是类型,低5bit是长度
raw_byte = 0b10110010

# 取类型(高3bit)
type_raw = (raw_byte & 0xE0) >> 5  # 0xE0 = 0b11100000
# 结果:0b101 = 5

# 取长度(低5bit)
length_raw = raw_byte & 0x1F  # 0x1F = 0b00011111
# 结果:0b10010 = 18

# 组装场景:类型=3,长度=25,合成一个字节
type_val = 3
length_val = 25
combined = ((type_val & 0x07) << 5) | (length_val & 0x1F)
# 结果:0b01111001 = 0x79

我在项目中遇到过最经典的案例:BLE广播包中的AD Type和AD Data。每个AD Structure的第一个字节是长度(Length),第二个字节是类型(Type),后面是数据(Data)。但有些私有协议会把类型和长度压缩到一个字节里——这时候位操作就派上用场了。

3.3 Python struct模块:打包与解包

手动移位虽然灵活,但遇到多字节、复杂结构时,代码会变得又臭又长。这时候,struct模块就是你的救星。

3.3.1 基本用法

struct的核心就两个函数:pack() 和 unpack()。一个把Python数据变成字节流,一个把字节流变回Python数据。

import struct

# 打包:把整数、浮点数等变成字节
# 格式:'<H' 表示小端无符号短整型(2字节)
packed = struct.pack('<H', 0x1234)
# 结果:b'4\x12' (0x34, 0x12)

# 解包:把字节变回整数
unpacked = struct.unpack('<H', packed)
# 结果:(0x1234,)

格式字符串速查表:

格式符 C类型 Python类型 字节数
B unsigned char int 1
b signed char int 1
H unsigned short int 2
h signed short int 2
I unsigned int int 4
i signed int int 4
f float float 4
d double float 8
s char[] bytes 指定长度

3.3.2 字节序控制

struct的格式字符串,第一个字符控制字节序:

前缀 字节序 对齐方式
@ 本地(默认) 本地对齐
= 本地 标准对齐
< 小端 标准对齐
> 大端 标准对齐
! 网络序(大端) 标准对齐
# 小端 vs 大端对比
import struct

data = 0x12345678

# 小端打包
little = struct.pack('<I', data)
# 结果:b'xV4\x12' (0x78, 0x56, 0x34, 0x12)

# 大端打包
big = struct.pack('>I', data)
# 结果:b'\x124Vx' (0x12, 0x34, 0x56, 0x78)

# 网络序(也是大端)
network = struct.pack('!I', data)
# 结果:b'\x124Vx'

注意:我曾经在解析一个BLE广播包时,用 struct.unpack('<H', ...) 解包UUID,结果总是对不上。后来发现,BLE的UUID虽然是2字节小端,但有些广播包里的UUID是16位完整UUID,有些是16位短UUID。前者用 !H(网络序),后者用 <H(小端)。所以,先确认协议版本,再选字节序。

3.3.3 实战:解析一个BLE广播包

假设我们收到一个BLE广播包,结构如下:

  • 前导码:1字节(固定0xAA)
  • 接入地址:4字节(固定0x8E89BED6)
  • PDU头:2字节(小端)
  • PDU负载:可变长度
  • CRC:3字节
import struct

def parse_ble_packet(raw_bytes):
    """
    解析BLE广播包(简化版)
    """
    # 跳过前导码和接入地址(共5字节)
    offset = 5
    
    # 解析PDU头(2字节,小端)
    pdu_header = struct.unpack_from('<H', raw_bytes, offset)[0]
    offset += 2
    
    # 从PDU头中提取PDU类型(高4bit)和长度(低8bit)
    pdu_type = (pdu_header >> 12) & 0x0F
    pdu_length = pdu_header & 0x00FF
    
    # 解析PDU负载
    pdu_payload = raw_bytes[offset:offset + pdu_length]
    offset += pdu_length
    
    # 解析CRC(3字节,小端)
    crc = struct.unpack_from('<I', raw_bytes[offset:offset+3] + b'\x00')[0]
    
    return {
        'pdu_type': pdu_type,
        'pdu_length': pdu_length,
        'pdu_payload': pdu_payload,
        'crc': crc
    }

# 示例用法
raw = b'\xAA\xD6\xBE\x89\x8E\x0C\x00\x02\x01\x06\x03\x03\xAB\xCD\x12\x34\x56'
result = parse_ble_packet(raw)
print(result)

3.4 本章知识体系

下面这张图,帮你把本章的核心逻辑串起来:

字节序与位操作知识体系 字节序与位操作知识体系 广播数据解析核心 字节序 位操作 struct模块 大端序 小端序 网络字节序 位掩码 左移/右移 与/或/非 pack() unpack() 格式字符串 BLE广播包 TCP/IP协议 Modbus 字段提取 数据组装 标志位判断 多字节解析 协议转换 CRC校验 核心原则:先确认字节序,再动手解析 位操作用于细粒度控制,struct用于批量处理 图3-1 字节序与位操作知识体系

3.5 实战建议

最后,给你几个我在项目中总结的实战建议:

  1. 先确认字节序:拿到协议文档,第一件事就是找"字节序"三个字。找不到?找"Endian"或"Byte Order"。还找不到?找逻辑分析仪抓包实测。
  2. 位掩码用十六进制:别写二进制,太长了。0x0F比0b00001111好读得多。我习惯用 0x0F、0xF0、0xFF 这种。
  3. struct格式字符串先写字节序前缀:我每次写struct,第一件事就是加 < 或 >。不加?默认用本地字节序,换台机器就崩。
  4. 多用 unpack_from():别用 unpack(),它只能从开头解。 unpack_from() 可以指定偏移量,配合循环解析多个字段,代码干净很多。
  5. 写单元测试:字节序和位操作最容易出边界问题。我每次写完解析函数,都会写几个测试用例:全0、全1、边界值、随机值。跑一遍,心里踏实。

我的小习惯:在代码里加一个 assert 来验证字节序假设。比如 assert struct.unpack('<H', b'\x01\x02')[0] == 0x0201。一旦平台变了,这个assert会第一时间告诉你——字节序不对!