You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

CPL=3时硬件中断触发仅置Write位的x86-64页错误原因排查

问题

基于phil-opp博客用Rust编写自定义内核,已完成四级页表复制、用户态页表创建、裸函数映射到虚拟地址0x40000000000、栈初始化并跳转至用户态,同时实现了syscall/sysret的系统调用处理器(触发时仅打印消息)。

发现PIC定时器中断总是在CPL=0时触发(因系统调用处理代码长于用户态循环);禁用打印后,循环数百次后定时器中断在CPL=3时触发,但此时CPU未调用中断处理器,反而抛出错误码为2(仅Write位置位)的x86-64页错误,不符合预期,求问题原因。


相关代码与QEMU日志

GDT

static ref GDT: (gdt::GlobalDescriptorTable, Selectors) = {
    let mut gdt = gdt::GlobalDescriptorTable::new();
    let kernel_code_selector = gdt.add_entry(gdt::Descriptor::kernel_code_segment());
    let kernel_data_selector = gdt.add_entry(gdt::Descriptor::kernel_data_segment());
    let tss_selector = gdt.add_entry(gdt::Descriptor::tss_segment(&TSS));
    let user_data_selector = gdt.add_entry(gdt::Descriptor::user_data_segment());
    let user_code_selector = gdt.add_entry(gdt::Descriptor::user_code_segment());
    (gdt, Selectors { kernel_code_selector, kernel_data_selector, tss_selector, user_code_selector, user_data_selector })
};

IDT

const DOUBLE_FAULT_IST_INDEX: u16 = 0;
let mut IDT: idt::InterruptDescriptorTable = idt::InterruptDescriptorTable::new();
IDT.breakpoint.set_handler_fn(interrupts::breakpoint::breakpoint_handler);
IDT.double_fault.set_handler_fn(interrupts::double_fault::double_fault_handler).set_stack_index(DOUBLE_FAULT_IST_INDEX);
IDT.page_fault.set_handler_fn(interrupts::page_fault::page_fault_handler);
IDT.general_protection_fault.set_handler_fn(interrupts::general_protection_fault::general_protection_fault_handler);
IDT.stack_segment_fault.set_handler_fn(interrupts::stack_segment_fault::stack_segment_fault_handler);
IDT.segment_not_present.set_handler_fn(interrupts::segment_not_present::segment_not_present_handler);
IDT.invalid_tss.set_handler_fn(interrupts::invalid_tss::invalid_tss_handler);
IDT.debug.set_handler_fn(interrupts::debug::debug_handler);
IDT[interrupts::HardwareInterrupt::Timer.as_usize()].set_handler_fn(interrupts::timer::timer_handler);
IDT.load();

TSS

let mut tss = tss::TaskStateSegment::new();
tss.interrupt_stack_table[DOUBLE_FAULT_IST_INDEX as usize] = {
    const STACK_SIZE: usize = 4096 * 5;
    static mut STACK: [u8; STACK_SIZE] = [0; STACK_SIZE];
    let stack_start = x86_64::VirtAddr::from_ptr(unsafe { &STACK });
    let stack_end = stack_start + STACK_SIZE;
    stack_end
};

定时器中断处理器

pub extern "x86-interrupt" fn timer_handler(_stack_frame: idt::InterruptStackFrame) {
    print!(".");
    cpu::pic_end_of_interrupt(0x20);
}

用户态应用

#[naked]
#[no_mangle]
#[allow(named_asm_labels)]
pub unsafe fn userspace_app_1() {
    asm!("\
        push 0
        prog1start:
        mov rax, 1234h
        pop rdi
        inc rdi
        push rdi
        mov rsi, 3
        mov rdx, 4
        mov r8, 5
        syscall
        jmp prog1start
    ", options(noreturn));
}

QEMU中断日志

7: v=20 e=0000 i=0 cpl=3 IP=0033:0000040000000066 pc=0000040000000066 SP=002b:0000060000000ff8 env->regs[R_EAX]=00000000515ca11a
RAX=0000000000001234 RBX=0000000000006062 RCX=0000040000000066 RDX=0000000000000004
RSI=0000000000000003 RDI=00000000001e91c5 RBP=0000008040201000 RSP=0000060000000ff8
R8 =0000000000000005 R9 =0000060000000f78 R10=0000000000203080 R11=0000000000000206
R12=0000000100000000 R13=0000000000005fea R14=0000018000000000 R15=0000000000006692
RIP=0000040000000066 RFL=00000206 [-----P-] CPL=3 II=0 A20=1 SMM=0 HLT=0
ES =0000 0000000000000000 ffffffff 00cf1300
CS =0033 0000000000000000 ffffffff 00a0fb00 DPL=3 CS64 [-RA]
SS =002b 0000000000000000 ffffffff 00c0f300 DPL=3 DS   [-WA]
DS =002b 0000000000000000 ffffffff 00cff300 DPL=3 DS   [-WA]
FS =0000 0000000000000000 0000ffff 00009300 DPL=0 DS   [-WA]
GS =0000 0000000000000000 0000ffff 00009300 DPL=0 DS   [-WA]
LDT=0000 0000000000000000 0000ffff 00008200 DPL=0 LDT
TR =0018 0000000000276014 00000067 00008900 DPL=0 TSS64-avl
GDT=     0000000000276090 00000037
IDT=     000000000026dd80 00000fff
CR0=80010011 CR2=0000000000000000 CR3=00000000002b4018 CR4=00000020
DR0=0000000000000000 DR1=0000000000000000 DR2=0000000000000000 DR3=0000000000000000 
DR6=00000000ffff0ff0 DR7=0000000000000400
CCS=0000000000000004 CCD=0000060000000fa8 CCO=EFLAGS
EFER=0000000000000d01
check_exception old: 0xffffffff new 0xe
     8: v=0e e=0002 i=0 cpl=3 IP=0033:0000040000000066 pc=0000040000000066 SP=002b:0000060000000ff8 CR2=fffffffffffffff8
RAX=0000000000001234 RBX=0000000000006062 RCX=0000040000000066 RDX=0000000000000004
RSI=0000000000000003 RDI=00000000001e91c5 RBP=0000008040201000 RSP=0000060000000ff8
R8 =0000000000000005 R9 =0000060000000f78 R10=0000000000203080 R11=0000000000000206
R12=0000000100000000 R13=0000000000005fea R14=0000018000000000 R15=0000000000006692
RIP=0000040000000066 RFL=00000206 [-----P-] CPL=3 II=0 A20=1 SMM=0 HLT=0
ES =0000 0000000000000000 ffffffff 00cf1300
CS =0033 0000000000000000 ffffffff 00a0fb00 DPL=3 CS64 [-RA]
SS =002b 0000000000000000 ffffffff 00c0f300 DPL=3 DS   [-WA]
DS =002b 0000000000000000 ffffffff 00cff300 DPL=3 DS   [-WA]
FS =0000 0000000000000000 0000ffff 00009300 DPL=0 DS   [-WA]
GS =0000 0000000000000000 0000ffff 00009300 DPL=0 DS   [-WA]
LDT=0000 0000000000000000 0000ffff 00008200 DPL=0 LDT
TR =0018 0000000000276014 00000067 00008900 DPL=0 TSS64-avl
GDT=     0000000000276090 00000037
IDT=     000000000026dd80 00000fff
CR0=80010011 CR2=fffffffffffffff8 CR3=00000000002b4018 CR4=00000020
DR0=0000000000000000 DR1=0000000000000000 DR2=0000000000000000 DR3=0000000000000000 
DR6=00000000ffff0ff0 DR7=0000000000000400
CCS=0000000000000004 CCD=0000060000000fa8 CCO=EFLAGS
EFER=0000000000000d01

分析与解决方案

从QEMU日志的CR2=fffffffffffffff8和错误码2(写操作、地址不存在)可以定位问题核心:

1. 根本原因:TSS的RSP0字段未初始化

当CPU从用户态(CPL=3)响应硬件中断切换到内核态(CPL=0)时,需要将用户态的栈帧(RFLAGS、CS、RIP等寄存器值)压入内核栈。此时CPU会从TSS的rsp0字段获取内核栈的指针,但当前TSS仅初始化了interrupt_stack_table(供double fault使用),完全未设置rsp0的值。

未初始化的rsp0会是一个无效地址(比如默认的0),CPU尝试从这个地址向下压栈时,就会访问到fffffffffffffff8这类未映射的地址,触发写操作的页错误。

2. 修复步骤

为TSS添加rsp0的初始化,分配一个独立的内核栈:

let mut tss = tss::TaskStateSegment::new();
// 初始化rsp0,指向内核栈顶
tss.rsp0 = {
    const KERNEL_STACK_SIZE: usize = 4096 * 8;
    static mut KERNEL_STACK: [u8; KERNEL_STACK_SIZE] = [0; KERNEL_STACK_SIZE];
    let stack_start = x86_64::VirtAddr::from_ptr(unsafe { &KERNEL_STACK });
    stack_start + KERNEL_STACK_SIZE
};
// 保留原有的double fault IST初始化
tss.interrupt_stack_table[DOUBLE_FAULT_IST_INDEX as usize] = {
    const STACK_SIZE: usize = 4096 * 5;
    static mut STACK: [u8; STACK_SIZE] = [0; STACK_SIZE];
    let stack_start = x86_64::VirtAddr::from_ptr(unsafe { &STACK });
    let stack_end = stack_start + STACK_SIZE;
    stack_end
};

同时需确保rsp0指向的内核栈地址已经在页表中正确映射(内核页表通常默认映射该区域,若未映射需补充映射规则)。

3. 额外验证点

  • IDT中定时器中断描述符无需修改DPL:硬件中断不受DPL限制,可从任意CPL触发,默认DPL=0符合要求。
  • PIC的EOI操作pic_end_of_interrupt(0x20)正确,定时器中断对应IRQ0,映射到IDT向量0x20。

内容的提问来源于stack exchange,提问作者lorinet3

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.23 01:15:11