文章

大小端序, Rust 字节数组泛型转换

大小端序, 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)
0x000x0A0x0D (LSB)
0x010x0B0x0C
0x020x0C0x0B
0x030x0D (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],其中前两个字节对应了 MyStructfield1 字段, 由于 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 字节)的整数倍。

本文由作者按照 CC BY 4.0 进行授权