最近在学习写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字节对齐要求,后面就一直报错。
在删除代码去定位原因时,发现:
- 
不使用
OnceCell,不报错。 - 
OnceCell里面不是BuddyAllocator不报错。 - 
不调用
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));
            }
        }
    }
}
    
	    
	    
		1
	    
	    
	    共 11 条评论, 1 页
	
	
    
评论区
写评论问题已解决。
在此再次感谢"TinusgragLin"的帮助。如果没有他,相信我不会发现到
sp未对齐的问题。以下是问题的分析和修复。
错误原因分析
riscv的调用规范要求
sp指针是16字节对齐的。参考: RISC-V Calling Conventions
然后在汇编代码中:
尽管
__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字节对齐。修复思路: 将第一个
.align 4移动到.section .bss下方,让__stack_bottom正确的对齐16字节。并在__stack_top前增加一个.align 4,确保即使未来.space填充的字节数未对齐16字节,__stack_top依旧是一定对齐16字节的。妙啊,以后入 risc-v 坑又少一个潜在的坑! [笑]
十分感谢!!!
sp的对齐被riscv要求是16字节对齐的。我的汇编代码写错了,导致sp没有正确的满足16字节对齐的要求。之前完全没有发现。
稍后我会总结我的错误和修复方式。
十分感谢您花时间帮助我,感谢!!!
--
👇
TinusgragLin: 是不是 lld 会假设 sp 8 字节对齐, 但我们没有做这个保证:
是不是 lld 会假设 sp 8 字节对齐, 但我们没有做这个保证:
是的,默认的rust-lld。
--
👇
TinusgragLin: >> 应该是我们的linker不一样的原因
默认的 rust-lld ? 我这边也观察到了!
默认的 rust-lld ? 我这边也观察到了!
你那边的 linker 是哪个? 我看一下可不可以在我这儿复现一下
我的ub_checks 传参是:
这个问题太令人疑惑了。我触发它也很意外,要同时使用
OnceCell,MaybeUninit以及我自定义的BuddyAllocator。我现在很疑惑究竟是我哪里搞错了导致UB,所以错误的触发很奇怪。还是我遇到什么rust的bug了。 :(--
👇
TinusgragLin: 传给 ub_checks 的参数看起来也没问题:
不知道楼主那边是什么值?
应该是我们的linker不一样的原因。
我的
.cargo/config.toml是:在安装了riscv64-elf-gcc后,我修改为
然后使用rust-gdb调试,我也正常执行过去了。
注释掉linker配置后:
再使用rust-gdb调试,在
MaybeUninit::zeroed函数中打印u地址,输出是:地址没对齐,
MaybeUninit::zeroed中的.write_bytes就会panic了。--
👇
TinusgragLin: 楼主你确定那个 panic 不是 todo!() 造成的?我这边用 gdb 单步运行了一下,
zeroed这一行十分顺畅地过去了! (我这边的环境是 rust 1.82,riscv-elf-gcc 14.1.0,cargo config 是:)
传给 ub_checks 的参数看起来也没问题:
不知道楼主那边是什么值?
楼主你确定那个 panic 不是 todo!() 造成的?我这边用 gdb 单步运行了一下,
zeroed这一行十分顺畅地过去了! (我这边的环境是 rust 1.82,riscv-elf-gcc 14.1.0,cargo config 是:)