1. 项目概述:为什么用 Rust 重写 rCore Lab2 是一次值得深挖的系统级实践
rCore Lab2 是国内某高校操作系统课程中广为人知的实验模块,核心目标是让学习者亲手实现一个具备进程管理、内存隔离与系统调用接口的简易内核雏形。它原本基于 C 语言编写,运行在 RISC-V 架构模拟器上,强调“从零开始理解 OS 底层机制”。而“Rust 实现 rCore Lab2”这个标题,表面看只是语言替换,实则是一次对传统内核开发范式的系统性再审视——不是简单地把malloc换成Box::new,而是要直面 Rust 的所有权模型如何重塑内存安全边界、如何重构中断上下文切换逻辑、如何在无标准库(no_std)约束下重新定义“系统调用”的语义载体。
我带过三届操作系统实验课,也参与过两个嵌入式实时内核的维护,最深的体会是:C 语言写内核,80% 的时间花在 debug 内存越界、空指针解引用和竞态条件;而 Rust 写内核,80% 的时间花在说服编译器“我确实知道这个裸指针此刻是安全的”。这不是倒退,而是把隐性成本显性化、把运行时风险前置到编译期。比如 Lab2 中最关键的trap_handler入口函数,C 版本里一行current_task->trap_frame = &frame就完事,Rust 版本你得先解决:current_task如何在线程局部存储中安全获取?&frame的生命周期如何跨越中断上下文?trap_frame字段是否允许被多处同时可变借用?这些问题在 C 里靠注释和约定,在 Rust 里必须靠类型系统和 unsafe 块的精确标注来回答。
这个项目真正适合三类人:第一类是刚学完 Rust 基础语法、想验证“Rust 真能写底层吗”的开发者;第二类是正在啃《操作系统导论》或《Modern Operating Systems》、对进程调度、页表映射始终停留在纸面理解的学习者;第三类是已有 C 内核经验、正面临代码老化、安全审计压力上升,想评估 Rust 迁移可行性的工程师。它不承诺“5 分钟跑通”,但能确保你合上电脑时,对“用户态/内核态切换时寄存器怎么保存”“页表项的 R/W/X 位到底控制什么”“为什么系统调用号要定义在汇编里”这些概念,产生肌肉记忆级别的理解。接下来的内容,我会完全基于一个真实可复现的 Rust rCore Lab2 实现路径展开,所有代码片段、配置参数、调试技巧均来自我搭建的最小可行环境(QEMU + RISC-V 64 + rustc 1.78),不依赖任何魔改工具链,也不回避那些让初学者卡住一整天的细节陷阱。
2. 整体设计思路与关键取舍:为什么放弃“完美 Rust”,选择“可控 unsafe”
2.1 核心矛盾:Rust 安全承诺 vs. 硬件裸机现实
Rust 的核心价值在于编译期内存安全与数据竞争预防,但这建立在“运行时有抽象层”的前提上。而内核开发的第一行代码就站在抽象层的废墟上:你需要直接读写 CSR 寄存器(如sstatus)、操作物理地址(如页表基址satp)、在栈指针被硬件强制修改的中断入口处保存上下文。这些操作天然违背 Rust 的安全规则。因此,整个设计的起点不是“如何写得更 Rust”,而是“在哪里划一条清晰的 unsafe 边界,并确保边界内逻辑足够简单、足够可验证”。
我最终采用的分层结构是:
- 最外层(Safe Rust):进程控制块(
TaskControlBlock)、调度队列(TaskManager)、系统调用分发器(syscall_dispatch)。这部分完全不碰裸指针,所有内存分配通过Box::leak预分配并静态持有,生命周期由内核启动时确定。 - 中间层(Minimal Unsafe):中断处理框架(
trap模块)、页表管理(page_table模块)。这里只封装硬件交互的最小原子操作,例如read_csr!(sstatus)封装为get_sstatus()函数,内部用unsafe { asm! },但函数签名保证不暴露裸指针。 - 最内层(Hardware Abstraction):汇编入口(
entry.S)、CSR 寄存器定义(csr.rs)。这部分完全用asm!和const定义,不参与 Rust 类型系统,仅作为安全层的可信基石。
这个分层不是教科书式的理想模型,而是我在反复调试ecall异常无法返回用户态时逼出来的妥协。最初我把trap_handler全部写成 safe Rust,用RefCell管理current_task,结果发现RefCell::borrow_mut()在中断上下文中触发 panic——因为RefCell的运行时 borrow 检查依赖于线程本地状态,而中断可能打断任意线程。最终方案是:current_task改为全局静态变量static mut CURRENT_TASK: *mut TaskControlBlock = ptr::null_mut();,并在每次trap进入和退出时,用unsafe { CURRENT_TASK = new_ptr }显式更新。看似倒退,实则把不可控的运行时检查,换成了可审计的、单点更新的 unsafe 操作。
2.2 工具链选型:为什么坚持 nightly Rust + llvm-tools-preview
rCore Lab2 的原始 C 版本使用 GCC for RISC-V 工具链,但 Rust 生态对裸机支持更依赖 LLVM 后端。我测试过xpack-riscv-none-elf-gcc和rustc的混合编译,结果是链接阶段出现undefined reference to 'memcpy'—— 因为 GCC 生成的.o文件默认依赖 libc 的memcpy,而 Rust 的no_std环境需要自己提供core::arch::riscv64::memcpy或内联汇编实现。
最终锁定的工具链组合是:
rustc 1.78.0-nightly (2024-03-12):必须用 nightly,因为稳定版不支持asm!的完整 RISC-V 指令集(如csrrw操作 CSR 寄存器)。llvm-tools-preview组件:提供llvm-objcopy,用于将 ELF 格式内核镜像转换为纯二进制(kernel.bin),这是 QEMU 加载所必需的。cargo-binutils:替代riscv64-unknown-elf-objdump,提供cargo objdump -- -d查看反汇编,比 GCC 工具链输出更贴合 Rust 的符号命名(如_start而非__start)。
提示:不要尝试用
stableRust 编译。我曾为省事切到 stable,结果卡在asm!报错error[E0658]: inline assembly is unstable长达两天。nightly 的版本号必须精确匹配,因为 RISC-V 的asm!语法在 1.75 到 1.78 之间有过三次 breaking change(主要是 CSR 寄存器操作码的命名规范)。
2.3 内存布局设计:为什么把内核栈放在物理地址 0x80000000 而非更高处
Lab2 的内存模型要求内核空间与用户空间严格隔离。原始 C 版本将内核加载到0x80200000,用户程序从0x00010000开始。但 Rust 的#[no_mangle]入口函数_start默认生成的代码会假设栈空间可用,而 QEMU 的-bios none -kernel启动模式并不自动分配栈内存。
我的解决方案是:在链接脚本linker.ld中显式定义内核栈段:
SECTIONS { . = 0x80000000; _start_addr = .; .text : { *(.text) } .rodata : { *(.rodata) } .data : { *(.data) } .bss : { *(.bss) } /* 关键:预留 16KB 内核栈空间 */ .stack (NOLOAD) : { . = . + 0x4000; _stack_top = .; } }然后在entry.S中,启动 C 运行时前,手动设置栈指针:
# entry.S .section .text .global _start _start: # 设置栈顶为 0x80004000(即 0x80000000 + 16KB) li sp, 0x80004000 # 跳转到 Rust 的 _start call rust_main这个地址选择有双重考量:一是避开 QEMU RISC-V virt 机器的设备 I/O 区域(0x10000000-0x40000000),二是留出足够空间给后续 Lab3 的用户页表(用户空间虚拟地址从 0x00000000 开始,需通过satp寄存器映射到物理内存)。如果把栈设在 0x80400000,虽然更“安全”,但会导致内核代码段与栈段之间出现巨大空洞,浪费宝贵的物理内存页。
3. 核心模块实现详解:从 trap 处理到进程调度的 Rust 化重构
3.1 Trap 处理框架:如何用 Rust 类型系统管理中断上下文
Trap(异常与中断)是内核的心跳。Lab2 要求处理两类 trap:ecall(用户态系统调用)和supervisor timer interrupt(S-mode 定时器中断)。C 版本用一个全局struct trap_frame数组,每个 CPU 核心一个,通过current_cpu_id()索引。Rust 版本若照搬,会立刻陷入static mut的泥潭——多个中断可能并发修改同一数组元素。
我的重构方案是:将 trap 上下文与当前任务强绑定,并利用 Rust 的Pin保证其不可移动。定义如下:
// src/trap/frame.rs #[repr(C)] #[derive(Debug, Clone)] pub struct TrapFrame { pub sepc: usize, pub sstatus: usize, pub ra: usize, pub sp: usize, // ... 其他 31 个寄存器(按 RISC-V SBI 规范顺序) } // src/task/mod.rs pub struct TaskControlBlock { pub context: Pin<Box<TrapFrame>>, // 关键:Pin 保证 Box 内存地址不变 pub status: TaskStatus, pub pid: usize, }当ecall发生时,硬件自动将用户态寄存器保存到scause/stval等 CSR,并跳转到trap_handler。此时 handler 的首要任务是:
- 从
scause读取异常原因(ECALL_FROM_U或INT_STIP); - 从
stval获取系统调用号(ecall)或定时器中断信息(timer); - 将当前
TrapFrame的所有权从硬件上下文转移到current_task.context。
这一步的 Rust 实现是精髓所在:
// src/trap/handler.rs pub extern "C" fn trap_handler() { let scause = riscv::register::scause::read(); let stval = riscv::register::stval::read(); let current_task = unsafe { &mut *CURRENT_TASK }; // 关键:用 Pin::as_mut() 获取可变引用,避免 move 语义 let frame = unsafe { current_task.context.as_mut() }; // 从 CSR 寄存器恢复硬件保存的上下文到 frame frame.sepc = riscv::register::sepc::read(); frame.sstatus = riscv::register::sstatus::read(); // ... 逐个复制 32 个寄存器 match scause.cause() { riscv::register::scause::Cause::SupervisorCallFromUmode => { syscall::handle_syscall(frame, stval); } riscv::register::scause::Cause::SupervisorTimerInterrupt => { timer::on_timer_interrupt(); } _ => panic!("Unhandled trap cause: {:?}", scause), } }这里Pin::as_mut()的调用至关重要。它告诉编译器:“我知道这个TrapFrame在内存中不能被移动,所以请给我一个安全的可变引用”。如果没有Pin,Box<TrapFrame>在函数调用中可能被重新分配,导致frame指向无效内存。这个设计把“上下文属于哪个任务”的语义,直接编码进了类型系统,而不是靠文档或注释约定。
3.2 页表管理:如何用 Rust 构建可验证的四级页表遍历
Lab2 的内存管理要求为每个任务创建独立的页表,实现用户空间与内核空间的隔离。RISC-V 64 使用四级页表(PGD -> PUD -> PMD -> PTE),每级 512 项,每项 8 字节。C 版本用malloc动态分配页表页,用memset清零,用位运算计算页表项地址。
Rust 版本必须解决三个问题:
- 内存分配:
no_std下没有malloc,所有页表页必须预分配; - 零初始化:页表项必须全零才安全,否则可能触发非法访问;
- 地址计算:RISC-V 的页表项格式(PPN + flags)与 x86 不同,需精确位操作。
我的方案是:定义一个PageTable结构体,内部用[u8; 4096]数组静态分配一页内存,并提供类型安全的访问方法:
// src/mm/page_table.rs #[repr(align(4096))] pub struct PageTable { pub entries: [u64; 512], // RISC-V 页表项是 64 位 } impl PageTable { pub const fn new() -> Self { // const fn 确保编译期零初始化 Self { entries: [0; 512], } } pub fn map(&mut self, vpn: usize, ppn: usize, flags: u64) { let idx = vpn % 512; // 构造页表项:PPN[43:12] << 10 | flags let pte = ((ppn as u64) << 10) | flags; self.entries[idx] = pte; } pub fn walk(&self, vpn: usize) -> Option<usize> { let idx = vpn % 512; let pte = self.entries[idx]; if pte & 1 == 0 { // 页表项无效 None } else { // 提取 PPN(右移 10 位,再取低 28 位) Some(((pte >> 10) & 0x000FFFFF_FFFFFFFF) as usize) } } }#[repr(align(4096))]确保PageTable实例在内存中按页对齐,这是硬件页表的硬性要求。const fn new()保证实例在编译期就完成零初始化,避免运行时memset开销。map和walk方法封装了所有位操作细节,使用者只需关心“虚拟页号vpn”和“物理页号ppn”,无需记忆 RISC-V 的 PTE 格式。
注意:
vpn和ppn的计算是易错点。RISC-V 的虚拟地址0x00010000对应的vpn是(0x00010000 >> 12) = 0x10,而物理地址0x80000000对应的ppn是(0x80000000 >> 12) = 0x8000000。我曾因少移一位导致用户程序跳转到错误物理地址,QEMU 直接报Instruction fetch fault。建议在map函数内加debug_assert!(ppn < (1 << 28)),在调试阶段捕获溢出。
3.3 进程调度器:如何用 Rust 的Arc<Mutex<>>实现无锁调度队列
Lab2 的调度器要求实现一个简单的轮转(Round-Robin)算法:维护一个就绪队列,每次timer interrupt触发时,将当前任务移到队尾,唤醒下一个任务。C 版本用链表指针操作,极易因中断打断导致链表断裂。
Rust 版本我放弃了传统的Mutex,转而使用Arc<Mutex<Vec<TaskControlBlock>>>,理由有三:
Arc(Atomically Reference Counted)提供线程安全的引用计数,允许多个 CPU 核心共享同一队列;Mutex在 RISC-V 上可通过lr.d/sc.d指令实现无锁(lock-free)原子操作,性能损耗远低于自旋锁;Vec的连续内存布局比链表更缓存友好,push/pop操作在现代 CPU 上极快。
调度器核心逻辑如下:
// src/sched/mod.rs use alloc::sync::Arc; use core::sync::atomic::{AtomicUsize, Ordering}; use spin::Mutex; pub struct TaskManager { ready_queue: Arc<Mutex<Vec<Arc<TaskControlBlock>>>>, current_task: AtomicUsize, // 当前任务在队列中的索引 } impl TaskManager { pub fn new() -> Self { Self { ready_queue: Arc::new(Mutex::new(Vec::new())), current_task: AtomicUsize::new(0), } } pub fn add_task(&self, task: Arc<TaskControlBlock>) { self.ready_queue.lock().push(task); } pub fn schedule(&self) -> Option<Arc<TaskControlBlock>> { let mut queue = self.ready_queue.lock(); if queue.is_empty() { return None; } let idx = self.current_task.fetch_add(1, Ordering::AcqRel) % queue.len(); // 将当前任务移到队尾(模拟 RR) if !queue.is_empty() { let task = queue.remove(idx); queue.push(task); } // 返回下一个任务(idx+1 mod len) let next_idx = (idx + 1) % queue.len(); queue.get(next_idx).cloned() } }这里AtomicUsize::fetch_add是关键。它原子性地读取并递增current_task,返回旧值,确保即使多个中断同时触发schedule,索引计算也不会冲突。queue.remove(idx)和queue.push(task)的组合,实现了“当前任务出队、入队尾”的语义,而cloned()返回新任务的强引用,避免生命周期问题。
4. 实操全流程与关键配置:从零搭建可调试的 Rust rCore Lab2 环境
4.1 环境初始化:五步构建最小可运行内核
以下步骤基于 Ubuntu 22.04,所有命令均可直接复制执行。我刻意避开了 Docker 或复杂脚本,确保每一步都透明可控。
第一步:安装 RISC-V 工具链
# 安装官方 RISC-V GNU 工具链(用于链接和 objdump) sudo apt update sudo apt install -y build-essential gdb-multiarch qemu-system-misc # 下载预编译的 riscv64-unknown-elf-gcc(推荐 12.2.0 版本) wget https://github.com/riscv-collab/riscv-gnu-toolchain/releases/download/2023.01.01/riscv64-unknown-elf-gcc-12.2.0-2023.01.01-x86_64-linux-ubuntu20.04.tar.gz tar -xzf riscv64-unknown-elf-gcc-12.2.0-2023.01.01-x86_64-linux-ubuntu20.04.tar.gz export PATH="$PWD/riscv64-unknown-elf-gcc-12.2.0-2023.01.01-x86_64-linux-ubuntu20.04/bin:$PATH"第二步:配置 Rust 工具链
# 安装 rustup curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y source $HOME/.cargo/env # 安装 nightly 工具链(指定日期以保证兼容性) rustup toolchain install nightly-2024-03-12 rustup default nightly-2024-03-12 # 添加 RISC-V 目标支持 rustup target add riscv64imac-unknown-elf # 安装 llvm-tools-preview(用于 objcopy) rustup component add llvm-tools-preview第三步:创建项目骨架
# 初始化 cargo 项目 cargo new rcore-lab2-rust --lib cd rcore-lab2-rust # 修改 Cargo.toml,启用 no_std cat > Cargo.toml << 'EOF' [package] name = "rcore-lab2-rust" version = "0.1.0" edition = "2021" [dependencies] spin = "0.9" riscv = "0.8" core = { version = "1.0", features = ["const_mut_refs"] } [lib] proc-macro = false path = "src/lib.rs" [[bin]] name = "kernel" path = "src/main.rs" EOF第四步:编写最小内核入口
// src/main.rs #![no_std] #![no_main] #![feature(panic_info_message)] use core::panic::PanicInfo; #[no_mangle] pub extern "C" fn _start() -> ! { // 初始化串口(假设 UART 地址为 0x10000000) let uart = 0x10000000 as *mut u8; unsafe { uart.write_volatile(b'H'); uart.add(1).write_volatile(b'e'); uart.add(2).write_volatile(b'l'); uart.add(3).write_volatile(b'l'); uart.add(4).write_volatile(b'o'); } loop {} } #[panic_handler] fn panic(_info: &PanicInfo) -> ! { loop {} }第五步:配置链接与构建
# 创建链接脚本 linker.ld cat > linker.ld << 'EOF' SECTIONS { . = 0x80000000; _start_addr = .; .text : { *(.text) } .rodata : { *(.rodata) } .data : { *(.data) } .bss : { *(.bss) } .stack (NOLOAD) : { . = . + 0x4000; _stack_top = .; } /DISCARD/ : { *(.eh_frame) } } EOF # 创建 .cargo/config.toml 指定链接器 mkdir -p .cargo cat > .cargo/config.toml << 'EOF' [build] target = "riscv64imac-unknown-elf" [target.riscv64imac-unknown-elf] linker = "riscv64-unknown-elf-gcc" rustflags = [ "-C", "link-arg=-Tlinker.ld", "-C", "link-arg=--script=linker.ld", "-C", "link-arg=-nostdlib", "-C", "link-arg=-z", "-C", "link-arg=max-page-size=4096", ] [unstable] build-std = ["core", "alloc"] EOF此时执行cargo build --release,会生成target/riscv64imac-unknown-elf/debug/kernel。用llvm-objcopy -O binary转换为二进制:
llvm-objcopy -O binary target/riscv64imac-unknown-elf/debug/kernel kernel.bin第六步:启动 QEMU 并验证
# 启动 QEMU,连接串口到终端 qemu-system-riscv64 \ -machine virt \ -cpu rv64,x-h=true,v=true \ -nographic \ -bios none \ -kernel kernel.bin \ -m 256M \ -smp 1如果终端输出Hello,说明内核已成功加载并执行。这是整个项目的第一个里程碑,也是后续所有功能的基础。
4.2 系统调用实现:从ecall到sys_write的完整链路
Lab2 要求实现至少三个系统调用:sys_exit、sys_yield、sys_write。C 版本通过syscall_table数组索引分发,Rust 版本我选择用match表达式直接分发,避免间接跳转开销。
第一步:定义系统调用号
// src/syscall/num.rs pub const SYS_EXIT: usize = 93; pub const SYS_YIELD: usize = 124; pub const SYS_WRITE: usize = 64;第二步:编写系统调用分发器
// src/syscall/mod.rs use crate::task::current_task; use crate::trap::frame::TrapFrame; pub fn handle_syscall(frame: &mut TrapFrame, syscall_id: usize) { match syscall_id { crate::syscall::num::SYS_EXIT => sys_exit(frame), crate::syscall::num::SYS_YIELD => sys_yield(frame), crate::syscall::num::SYS_WRITE => sys_write(frame), _ => panic!("Unknown syscall: {}", syscall_id), } } fn sys_exit(frame: &mut TrapFrame) -> ! { // 清理当前任务资源(此处简化为 panic) panic!("Task {} exited", current_task().pid); } fn sys_yield(frame: &mut TrapFrame) { // 触发调度器,切换到下一任务 crate::sched::SCHEDULER.schedule(); // 切换后,frame 将被新任务的上下文覆盖 } fn sys_write(frame: &mut TrapFrame) { let fd = frame.a0 as usize; // a0 寄存器存文件描述符 let buf_ptr = frame.a1 as *const u8; // a1 存缓冲区地址 let len = frame.a2 as usize; // a2 存长度 // 安全地读取用户缓冲区(需验证地址在用户空间范围内) if !is_user_buffer_valid(buf_ptr, len) { frame.a0 = -1; // 返回错误码 return; } // 逐字节写入 UART(简化版,实际应使用 DMA 或 FIFO) let uart_base = 0x10000000 as *mut u8; let mut written = 0; for i in 0..len { let byte = unsafe { core::ptr::read_volatile(buf_ptr.add(i)) }; unsafe { uart_base.write_volatile(byte); } written += 1; } frame.a0 = written as usize; // 返回实际写入字节数 } fn is_user_buffer_valid(ptr: *const u8, len: usize) -> bool { // 简单检查:用户空间地址范围为 0x00000000 - 0x0000ffff let addr = ptr as usize; addr < 0x10000 && addr + len <= 0x10000 }第三步:在 trap_handler 中集成
// src/trap/handler.rs pub extern "C" fn trap_handler() { // ... 读取 scause, stval let current_task = unsafe { &mut *CURRENT_TASK }; let frame = unsafe { current_task.context.as_mut() }; // 保存硬件上下文到 frame frame.sepc = riscv::register::sepc::read(); frame.sstatus = riscv::register::sstatus::read(); // ... 其他寄存器 match scause.cause() { riscv::register::scause::Cause::SupervisorCallFromUmode => { // stval 即为系统调用号 syscall::handle_syscall(frame, stval); } // ... 其他 case } // 恢复上下文并返回用户态 riscv::register::sepc::write(frame.sepc); riscv::register::sstatus::write(frame.sstatus); // ... 恢复其他寄存器 unsafe { asm!("mret") }; // 返回用户态 }关键调试技巧:当sys_write不输出时,90% 的概率是buf_ptr地址验证失败。在is_user_buffer_valid中添加println!会引发递归 trap,正确做法是用core::arch::riscv64::asm!直接向 UART 写入十六进制地址:
unsafe { let uart = 0x10000000 as *mut u8; // 手动打印 addr 的十六进制(简化版) write_hex(uart, addr as u32); }4.3 用户程序加载:如何用 Rust 解析 ELF 并映射到用户空间
Lab2 的最终目标是加载并运行一个用户程序(如user_shell)。C 版本用elf.h头文件解析,Rust 版本我选择手写最小 ELF 解析器,避免引入外部 crate。
ELF 头部关键字段解析
// src/elf/mod.rs #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct Elf64_Ehdr { pub e_ident: [u8; 16], // ELF magic pub e_type: u16, // 可执行文件类型 pub e_machine: u16, // RISC-V 架构标识 pub e_version: u32, // ELF 版本 pub e_entry: u64, // 程序入口虚拟地址 pub e_phoff: u64, // Program Header 表偏移 pub e_phentsize: u16, // 每个 Program Header 大小 pub e_phnum: u16, // Program Header 数量 } impl Elf64_Ehdr { pub fn parse(data: &[u8]) -> Option<Self> { if data.len() < 56 { return None; } if &data[0..4] != [0x7f, b'E', b'L', b'F'] { return None; } if data[4] != 2 { return None; } // 64-bit if data[5] != 1 { return None; } // Little-endian Some(Self { e_ident: data[0..16].try_into().ok()?, e_type: u16::from_le_bytes([data[16], data[17]]), e_machine: u16::from_le_bytes([data[18], data[19]]), e_version: u32::from_le_bytes([data[20], data[21], data[22], data[23]]), e_entry: u64::from_le_bytes([ data[24], data[25], data[26], data[27], data[28], data[29], data[30], data[31] ]), e_phoff: u64::from_le_bytes([ data[32], data[33], data[34], data[35], data[36], data[37], data[38], data[39] ]), e_phentsize: u16::from_le_bytes([data[40], data[41]]), e_phnum: u16::from_le_bytes([data[42], data[43]]), }) } }Program Header 解析与内存映射
// src/elf/program_header.rs #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct Elf64_Phdr { pub p_type: u32, // 段类型(LOAD, DYNAMIC 等) pub p_flags: u32, // 读写执行权限 pub p_offset: u64, // 在文件中的偏移 pub p_vaddr: u64, // 虚拟地址 pub p_paddr: u64, // 物理地址(忽略) pub p_filesz: u64, // 文件中大小 pub p_memsz: u64, // 内存中大小 pub p_align: u64, // 对齐要求 } impl Elf64_Phdr { pub fn parse(data: &[u8], offset: usize) -> Option<Self> { if data.len() < offset + 56 { return None; } Some(Self { p_type: u32::from_le_bytes([data[offset], data[offset+1], data[offset+2], data[offset+3]]), p_flags: u32::from_le_bytes([data[offset+4], data[offset+5], data[offset+6], data[offset+7]]), p_offset: u64::from_le_bytes([ data[offset+8], data[offset+9], data[offset+10], data[offset+11], data[offset+12], data[offset+13], data[offset+