< 返回版块

small-white0-0 发表于 2024-11-20 14:42

Tags:no_std,panic,MaybeUninit

最近在学习写riscv的no_std rust。但是,在实现堆分配器时,采用了MaybeUninit去初始化链表。但是,在运行时创建堆分配器对象,就会报错"ptr::write_bytes requires that the destination pointer is aligned and non-null"。

debug下来发现是 MaybeUninit::zeroed中调用的MaybeUninit::<T>::uninit()创建的变量的地址,不满足类型的8字节对齐要求,后面就一直报错。

在删除代码去定位原因时,发现:

  1. 不使用OnceCell,不报错。

  2. OnceCell里面不是BuddyAllocator不报错。

  3. 不调用OnceCell::get_or_init()不报错。

我不理解为何使用MaybeUnint创建的变量地址会没有对齐。这个是我做错了什么,导致触发UB吗?有知道的大佬可以告诉一下吗?感激不尽。

程序是跑在 archlinux中的qemu 9.1.1的riscv虚拟机中,运行命令是:

cargo build && qemu-system-riscv64 -machine virt -nographic -bios none  -m 1024M -kernel ./target/riscv64imac-unknown-none-elf/debug/demo -S -s

下面是简化后的,可以触发报错的代码。会报错的一行是init_heap中的_test变量处。

//! target 是:riscv64imac-unknown-none-elf
//!
//! 该程序的连接脚本是:
//! MEMORY {
//! DRAM : ORIGIN = 0x80000000, LENGTH = 320M
//! }
//!
//! ENTRY(_start)
//!
//! SECTIONS {
//!     . = 0x80000000;
//!
//!     .text : {
//!         *(.text.entry)
//!         *(.text)
//!     } > DRAM
//!
//!     .rodata : {
//!         *(.rodata)
//!     } > DRAM
//!
//!     .data : {
//!         *(.data)
//!     } > DRAM
//!
//!     .bss : {
//!         *(.bss)
//!     } > DRAM
//!
//!     . = ALIGN(16);
//!     _kernel_end = .;
//! }
//!
#![no_std]
#![no_main]

use core::arch::global_asm;
use core::cell::OnceCell;

global_asm!(
    r#"
    .align   2
    .section .text.entry
    .global  _start
_start:
    la       sp, __stack_top
    call     start
    j        .

    .align   4
    .section .bss
    .global  __stack_bottom
__stack_bottom:
    .space   409600
__stack_top:
"#
);

#[macro_export]
macro_rules! entry {
    ($f:ident) => {
        #[no_mangle]
        extern "C" fn start() -> ! {
            let f: fn() -> ! = $f;
            f()
        }
    };
}

use mm::spin_sync::SpinLock;
use mm::BuddyAllocator;

#[global_allocator]
pub static STATIC_HEAP: SpinLock<OnceCell<BuddyAllocator<20>>> = SpinLock::new(OnceCell::new());
pub fn init_heap() {
    use core::mem::MaybeUninit;
    use mm::phy_list;

    let _test: MaybeUninit<phy_list::PhyList> = MaybeUninit::zeroed();
    // 上面的zeroed就会panic了

    STATIC_HEAP.lock().get_or_init(|| todo!());
}

entry!(main);

fn main() -> ! {
    init_heap();
    loop {}
}

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    loop {}
}

mod mm {
    use core::{
        alloc::{GlobalAlloc, Layout},
        cell::OnceCell,
        mem::{self},
        ptr::NonNull,
    };
    use spin_sync::SpinLock;

    pub mod phy_list {
        #![allow(unused)]
        //! 会利用指向空间记录下一个节点的地址,从而实现链表的功能
        //! 该链表用于管理预分配的静态内存的分配和释放
        //! 不能进行通用链表操作。

        use core::ptr::{self, NonNull};
        #[repr(transparent)]
        #[derive(Debug, Clone, Copy)]
        pub struct PhyList {
            head: Option<NonNull<usize>>,
        }
    }

    pub mod spin_sync {
        use core::cell::UnsafeCell;

        pub struct SpinLock<T> {
            lock: core::sync::atomic::AtomicBool,
            data: UnsafeCell<T>,
        }
        pub struct SpinGuard<'a, T> {
            inner: &'a SpinLock<T>,
        }
        unsafe impl<T> Sync for SpinLock<T> {}
        impl<T> SpinLock<T> {
            pub const fn new(data: T) -> Self {
                Self {
                    lock: core::sync::atomic::AtomicBool::new(false),
                    data: UnsafeCell::new(data),
                }
            }
            pub fn lock(&self) -> SpinGuard<T> {
                while self.lock.swap(true, core::sync::atomic::Ordering::Acquire) {
                    // spin
                }
                SpinGuard { inner: self }
            }
        }
        impl<'a, T> Drop for SpinGuard<'a, T> {
            fn drop(&mut self) {
                self.inner
                    .lock
                    .store(false, core::sync::atomic::Ordering::Release);
            }
        }
        impl<'a, T> core::ops::Deref for SpinGuard<'a, T> {
            type Target = T;
            fn deref(&self) -> &Self::Target {
                // SAFETY: SpinGuard is only created when SpinLock is locked.
                unsafe { &*self.inner.data.get() }
            }
        }
        impl<'a, T> core::ops::DerefMut for SpinGuard<'a, T> {
            fn deref_mut(&mut self) -> &mut Self::Target {
                // SAFETY: SpinGuard is only created when SpinLock is locked.
                unsafe { &mut *self.inner.data.get() }
            }
        }
    }

    /// 伙伴分配器
    /// N: 空闲链表的数量,
    /// BALIGN: 对齐的位数,即2^BALIGN,要求不小于usize的字节数,一般是2^3=8,此时BALIGN=3。
    /// N+BALIGN 必须小于usize的位数(一般是64),否则会溢出。
    /// FIXME: 由于rust目前没法限制const泛型的范围,所以这里只能在使用的时候保证N和ALIGN的范围。
    pub struct BuddyAllocator<
        const N: usize,
        const BALIGN: u8 = { mem::size_of::<usize>().trailing_zeros() as u8 },
    > {
        free_list: [phy_list::PhyList; N],
        total_size: usize,
        allocated_size: usize,
    }

    impl<const N: usize, const BALIGN: u8> BuddyAllocator<N, BALIGN> {
        pub fn allocate(&mut self, layout: Layout) -> Option<NonNull<u8>> {
            todo!()
        }

        pub fn deallocate(&mut self, ptr: NonNull<u8>, layout: Layout) {
            todo!()
        }
    }

    unsafe impl GlobalAlloc for SpinLock<OnceCell<BuddyAllocator<20, 3>>> {
        unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
            self.lock()
                .get_mut()
                .map(|allocator| allocator.allocate(layout))
                .flatten()
                .map_or(core::ptr::null_mut(), |ptr| ptr.as_ptr())
        }

        unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
            let ptr = NonNull::new(ptr);
            if let Some(ptr) = ptr {
                self.lock()
                    .get_mut()
                    .map(|allocator| allocator.deallocate(ptr, layout));
            }
        }
    }
}

评论区

写评论
作者 small-white0-0 2024-11-20 23:41

问题已解决。

在此再次感谢"TinusgragLin"的帮助。如果没有他,相信我不会发现到sp未对齐的问题。

以下是问题的分析和修复。

错误原因分析

riscv的调用规范要求sp指针是16字节对齐的。

参考: RISC-V Calling Conventions

The stack grows downwards (towards lower addresses) and the stack pointer shall be aligned to a 128-bit boundary upon procedure entry.

然后在汇编代码中:

    .align   4
    .section .bss
    .global  __stack_bottom
__stack_bottom:
    .space   409600
__stack_top:

尽管__stack_bottom被要求16字节对齐,然后放置了409600字节的空间。但是debug时发现,__stack_top的地址,依旧没有16字节对齐。由于sp的初始值就没有保证16字节对齐,而后续的rust程序是假设sp是16字节对齐的,进而导致在MaybeUninit::zeroed中变量地址未对齐的panic。

.align 4没有生效的原因:因为.align 4.section .bss上方,在切换段后,.align 4的对齐不能对__stack_bottom生效。

修复

__stack_top 进行16字节对齐。

@@ -47,11 +47,12 @@
     call     start
     j        .
 
-    .align   4
     .section .bss
     .global  __stack_bottom
+    .align   4
 __stack_bottom:
     .space   409600
+    .align   4
 __stack_top:
 "#
 );

修复思路: 将第一个.align 4移动到.section .bss下方,让__stack_bottom正确的对齐16字节。并在__stack_top前增加一个.align 4,确保即使未来.space填充的字节数未对齐16字节,__stack_top依旧是一定对齐16字节的。

TinusgragLin 2024-11-20 22:44

sp的对齐被riscv要求是16字节对齐的

妙啊,以后入 risc-v 坑又少一个潜在的坑! [笑]

作者 small-white0-0 2024-11-20 22:22

十分感谢!!!

sp的对齐被riscv要求是16字节对齐的。我的汇编代码写错了,导致sp没有正确的满足16字节对齐的要求。之前完全没有发现。

稍后我会总结我的错误和修复方式。

十分感谢您花时间帮助我,感谢!!!

--
👇
TinusgragLin: 是不是 lld 会假设 sp 8 字节对齐, 但我们没有做这个保证:

(gdb) p $sp
$1 = (*mut ()) 0x80064d64
TinusgragLin 2024-11-20 22:02

是不是 lld 会假设 sp 8 字节对齐, 但我们没有做这个保证:

(gdb) p $sp
$1 = (*mut ()) 0x80064d64
作者 small-white0-0 2024-11-20 21:46

是的,默认的rust-lld。

--
👇
TinusgragLin: >> 应该是我们的linker不一样的原因

你那边的 linker 是哪个? 我看一下可不可以在我这儿复现一下

默认的 rust-lld ? 我这边也观察到了!

core::intrinsics::write_bytes::precondition_check (addr=0x80064d04, align=8)
TinusgragLin 2024-11-20 21:24

应该是我们的linker不一样的原因 你那边的 linker 是哪个? 我看一下可不可以在我这儿复现一下

默认的 rust-lld ? 我这边也观察到了!

core::intrinsics::write_bytes::precondition_check (addr=0x80064d04, align=8)
TinusgragLin 2024-11-20 21:18

应该是我们的linker不一样的原因

你那边的 linker 是哪个? 我看一下可不可以在我这儿复现一下

作者 small-white0-0 2024-11-20 21:12

我的ub_checks 传参是:

core::ub_checks::is_aligned_and_not_null (ptr=0x80064d14, align=8) at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/ub_checks.rs:120
120         !ptr.is_null() && ptr.is_aligned_to(align)

这个问题太令人疑惑了。我触发它也很意外,要同时使用OnceCellMaybeUninit以及我自定义的BuddyAllocator。我现在很疑惑究竟是我哪里搞错了导致UB,所以错误的触发很奇怪。还是我遇到什么rust的bug了。 :(

--
👇
TinusgragLin: 传给 ub_checks 的参数看起来也没问题:

core::ub_checks::is_aligned_and_not_null (ptr=0x80064c48, align=8)
    at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/ub_checks.rs:120
0x00000000800003cc in core::intrinsics::write_bytes::precondition_check (addr=0x80064c48, align=8)
    at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/intrinsics.rs:3497

不知道楼主那边是什么值?

作者 small-white0-0 2024-11-20 21:04

应该是我们的linker不一样的原因。

我的.cargo/config.toml是:

[build]
target = "riscv64imac-unknown-none-elf"

[target.riscv64imac-unknown-none-elf]
rustflags = ["-C", "link-arg=-Tlink.ld"]

在安装了riscv64-elf-gcc后,我修改为

[build]
target='riscv64imac-unknown-none-elf'
[target.riscv64imac-unknown-none-elf]
linker='riscv64-elf-gcc'
rustflags=['-C', 'link-args=-T link.ld -nostdlib']

然后使用rust-gdb调试,我也正常执行过去了。

注释掉linker配置后:

[build]
target='riscv64imac-unknown-none-elf'
[target.riscv64imac-unknown-none-elf]
# linker='riscv64-elf-gcc'
rustflags=['-C', 'link-args=-T link.ld -nostdlib']

再使用rust-gdb调试,在MaybeUninit::zeroed函数中打印u地址,输出是:

(gdb) p &u
$1 = (*mut core::mem::maybe_uninit::MaybeUninit<test_s::mm::phy_list::PhyList>) 0x80064d14

地址没对齐,MaybeUninit::zeroed中的.write_bytes就会panic了。

--
👇
TinusgragLin: 楼主你确定那个 panic 不是 todo!() 造成的?我这边用 gdb 单步运行了一下,zeroed 这一行十分顺畅地过去了! (我这边的环境是 rust 1.82,riscv-elf-gcc 14.1.0,cargo config 是:

[build]
target='riscv64imac-unknown-none-elf'
[target.riscv64imac-unknown-none-elf]
linker='riscv64-elf-gcc'
rustflags=['-C', 'link-args=-T link.ld -nostdlib']

TinusgragLin 2024-11-20 19:38

传给 ub_checks 的参数看起来也没问题:

core::ub_checks::is_aligned_and_not_null (ptr=0x80064c48, align=8)
    at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/ub_checks.rs:120
0x00000000800003cc in core::intrinsics::write_bytes::precondition_check (addr=0x80064c48, align=8)
    at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/intrinsics.rs:3497

不知道楼主那边是什么值?

TinusgragLin 2024-11-20 19:14

楼主你确定那个 panic 不是 todo!() 造成的?我这边用 gdb 单步运行了一下,zeroed 这一行十分顺畅地过去了! (我这边的环境是 rust 1.82,riscv-elf-gcc 14.1.0,cargo config 是:

[build]
target='riscv64imac-unknown-none-elf'
[target.riscv64imac-unknown-none-elf]
linker='riscv64-elf-gcc'
rustflags=['-C', 'link-args=-T link.ld -nostdlib']

1 共 11 条评论, 1 页