大小端序, Rust 字节数组泛型转换
将 u8 数组转换为 Rust 中自定义的 Struct 过程中相关问题记录
大端序与小端序
程序中的多字节变量会被存储为连续的字节序列,比如 u32
类型就需要连续的 4 个字节,那么这里就涉及到对字节顺序的两种排列方式:
基础概念:最低有效字节(
Least Significant Byte,LSB
),最高有效字节(Most Significant Byte,MSB
)。
- 大端序 (
Big Endian
): 数据的低字节LSB
放在内存的高位地址 / 数据的低位地址存储变量的高位字节MSB
数据 - 小端序 (
Little Endian
): 数据的低字节LSB
放在内存的低地址 / 数据的低地址存储变量的低位字节LSB
数据
如整型数据 10 进制 168496141 的 16 进制是 0X0A0B0C0D
, 共占据 4 个字节 0X0A
, 0X0B
, 0X0C
, 0X0D
。
将 0X0A0B0C0D
转换为二进制
1
2
3
4
5
0x0A = 0000 1010
0x0B = 0000 1011
0x0C = 0000 1100
0x0D = 0000 1101
0X0A0B0C0D = 0000 1010 0000 1011 0000 1100 0000 1101
LSB 在最右侧, MSB 在最左侧
1
2
3
4
0000 1010 0000 1011 0000 1100 0000 1101
---0A---- ---0B---- ---0C---- ---0D----
| |
MSB LSB
将二进制转换为十进制
1
2^0 + 2^2 + 2^3 + 2^10 + 2^11 + 2^16 + 2^17 + 2^19 + 2^25 + 2^27 = 168,496,141
在内存地址增长方向上,它的小端序是 [0X0D, 0X0C, 0X0B, 0X0A]
,大端序是 [0X0A, 0X0B, 0X0C, 0X0D]
内存地址 | 大端序 (Big Endian) | 小端序 (Little Endian) |
---|---|---|
0x00 | 0x0A | 0x0D (LSB ) |
0x01 | 0x0B | 0x0C |
0x02 | 0x0C | 0x0B |
0x03 | 0x0D (LSB ) | 0x0A |
Rust 将字节数组转换为自定义 Struct
1
2
3
4
/// transmute `byte slice` to `T`.
pub fn transmute_from_u8<T>(v: &[u8]) -> &T {
unsafe { &*v.as_ptr().cast::<T>() }
}
测试函数如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#[test]
fn test_transmute_from_u8_to_struct() {
#[derive(Debug, PartialEq)]
struct MyStruct {
field1: u16,
field2: u8,
}
// MyStruct 占据的字节数
let need_bytes: usize = std::mem::size_of::<MyStruct>();
// 初始化字节数组与空间
let mut aligned_bytes: Vec<u8> = Vec::<u8>::with_capacity(need_bytes);
unsafe {
aligned_bytes.set_len(need_bytes);
println!("aligned bytes size:{}, capacity:{}", aligned_bytes.len(), aligned_bytes.capacity());
// 获取指向 vec buffer 的可变指针
let ptr: *mut u8 = aligned_bytes.as_mut_ptr();
// 覆写内存区域的数据
std::ptr::write(ptr.add(0), 0x34);
std::ptr::write(ptr.add(1), 0x12);
std::ptr::write(ptr.add(2), 0x78);
// 将 ptr 转换为字节切片, 长度为 4, 最后一个字节用于观察越界内存
let byte_slice: &[u8] = std::slice::from_raw_parts(ptr, 4);
// 遍历字节切片并打印每个字节的值
for (index, &byte) in byte_slice.iter().enumerate() {
println!("Byte {}: 0x{:02X}, {}", index, byte, byte);
}
}
// 将字节切片转为自定义的 MySturct 类型
let value: &MyStruct = transmute_from_u8::<MyStruct>(&aligned_bytes);
assert_eq!(*value, MyStruct {field1:0x1234,field2:0x78})
}
测试用例输出如下
1
2
3
4
5
aligned bytes size:4, capacity:4
Byte 0: 0x34, 52
Byte 1: 0x12, 18
Byte 2: 0x78, 120
Byte 3: 0x00, 0
Rust 内部使用小端序, 小端序写入数据时:低地址对应低位字节,高地址对应高位字节
在字节数组 aligned_bytes
内部的排列方式是 [0x34, 0x12, 0x78]
,其中前两个字节对应了 MyStruct
的 field1
字段, 由于 rust 存储时使用的是小端序,所以 field1
实际存储的数据是 0x1234
(十进制 4660)。
1
2
aligned_bytes: [0011 0100 | 0001 0010 | 0111 1000]
aligned_bytes: [--0x34--- | --0x12--- | --0x78---]
更具体的,为什么 field1
字段存储的是 0x1234
,这里给出详细的解释:在字节数组 aligned_bytes
中,aligned_bytes[0]
是最低地址,aligned_bytes[1]
是次低地址,那么存储在内存中的 field1
是低地址对应数据低字节,即 0x34
处于 field1
的最低位,0x12
处于 field1
的高位,那么 field1
的值就是 0x1234
字节对齐
1
std::mem::size_of::<MyStruct>()
上述代码实际在获取 MySturct
占据的字节数时会考虑字节对齐的影响。u16
类型的对齐要求是 2 字节,u8
类型的对齐要求是 1 字节,由于 u16
的对齐要求更严格,因此 MyStruct
的对齐方式将由 field1
的对齐要求决定,即 2 字节对齐。为了满足结构体的对齐要求,编译器会在 field2
后面插入 1 个字节的填充,使得结构体的大小为 4 字节,是对齐方式(2 字节)的整数倍。