最近在学习写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 是:)