diff --git a/VKernel/Kernel/intr/idt.vp b/VKernel/Kernel/intr/idt.vp index 1dffc39..a369e45 100644 --- a/VKernel/Kernel/intr/idt.vp +++ b/VKernel/Kernel/intr/idt.vp @@ -111,6 +111,49 @@ def isrCommonhandler(interrupt: int): mov al, 93 out dx, al""", op=[ASM_DESCR.CLOBBER_RAX, ASM_DESCR.CLOBBER_RCX, ASM_DESCR.CLOBBER_RDX]) + # 【诊断】异常现场:出错 RIP / 错误码 / 出错时 RSP(#PF 另打 cr2)。 + # 由 isr.s 的 ISR_ERR 在入口写入 _fault_* ⇒ #GP/#PF 自己就能说出位置,不必再反查地址。 + # 只有带错误码的向量(8/10/11/12/13/14/17,见 isr.s 里用 ISR_ERR 的那些)才会写 _fault_*, + # 其余向量读到的是上次残留 ⇒ 仅在真写了时打印,避免误导。 + if (interrupt == 8 or interrupt == 10 or interrupt == 11 or interrupt == 12 + or interrupt == 13 or interrupt == 14 or interrupt == 17): + _df_rip: uint64 = 0 + _df_err: uint64 = 0 + _df_rsp: uint64 = 0 + __asm__(f"mov {asmout(_df_rip, ASM_DESCR.OUTPUT_REG)}, [_fault_rip]", + op=[ASM_DESCR.CLOBBER_MEMORY]) + __asm__(f"mov {asmout(_df_err, ASM_DESCR.OUTPUT_REG)}, [_fault_err]", + op=[ASM_DESCR.CLOBBER_MEMORY]) + __asm__(f"mov {asmout(_df_rsp, ASM_DESCR.OUTPUT_REG)}, [_fault_rsp]", + op=[ASM_DESCR.CLOBBER_MEMORY]) + _dfbuf: [128]char + viperlib.snprintf(&_dfbuf, 128, " rip=0x%lx err=0x%lx rsp=0x%lx\n", _df_rip, _df_err, _df_rsp) + serial.puts(&_dfbuf) + # 返回槽三连:出错 RSP 附近的栈字。 + # ret=0x… [rsp-8]:若 CPU 是在 `ret` 之后于错地址取指的,这里就是被弹走的坏目标; + # ra=0x… [rsp] :调用者的返回地址(真内核地址)⇒ 反查即可定位"哪个函数的返回槽被写坏"; + # ra2=0x… [rsp+8]:再上一层,用于交叉确认。 + # 返回槽三连:只在**内核栈**情形下读 —— 用户态出错时 _fault_rsp 是用户栈指针, + # 它下方那一页往往没映射,读 [rsp-8] 会再触发一次 #PF(现场:紧跟一行 + # [0E] cr2=0x7fffffde0000,就是这里读出来的次生故障)。 + if _df_rsp >= 0x100000 and _df_rsp < 0x80000000: + _df_ret: uint64 = 0 + _df_ra: uint64 = 0 + _df_ra2: uint64 = 0 + __asm__(f"mov {asmout(_df_ret, ASM_DESCR.OUTPUT_REG)}, [{asminp(_df_rsp, ASM_DESCR.REG_ANY)} - 8]", + op=[ASM_DESCR.CLOBBER_MEMORY]) + __asm__(f"mov {asmout(_df_ra, ASM_DESCR.OUTPUT_REG)}, [{asminp(_df_rsp, ASM_DESCR.REG_ANY)}]", + op=[ASM_DESCR.CLOBBER_MEMORY]) + __asm__(f"mov {asmout(_df_ra2, ASM_DESCR.OUTPUT_REG)}, [{asminp(_df_rsp, ASM_DESCR.REG_ANY)} + 8]", + op=[ASM_DESCR.CLOBBER_MEMORY]) + viperlib.snprintf(&_dfbuf, 128, " ret=0x%lx ra=0x%lx ra2=0x%lx\n", _df_ret, _df_ra, _df_ra2) + serial.puts(&_dfbuf) + if interrupt == 14: + _df_cr2: uint64 = 0 + __asm__(f"mov {asmout(_df_cr2, ASM_DESCR.OUTPUT_REG)}, cr2", + op=[ASM_DESCR.CLOBBER_MEMORY]) + viperlib.snprintf(&_dfbuf, 128, " cr2=0x%lx\n", _df_cr2) + serial.puts(&_dfbuf) s: sched.Scheduler* = sched._sched_ptr # 如果调度器未初始化,直接进入安全停机状态 if s == 0: diff --git a/VKernel/Kernel/intr/isr.s b/VKernel/Kernel/intr/isr.s index 93a58de..98853e8 100644 --- a/VKernel/Kernel/intr/isr.s +++ b/VKernel/Kernel/intr/isr.s @@ -6,11 +6,11 @@ .comm _yield_cnt, 4, 4 .comm _yield_lock, 4, 4 .comm _fs_lock, 4, 4 +# 【诊断】异常现场(#GP/#PF 等带错误码的向量在 ISR_ERR 入口保存,isrCommonhandler 打印)。 +# 只保留最小三件套:出错 RIP / 错误码 / 出错时的 RSP(#PF 另读 cr2)。 .comm _fault_rip, 8, 8 .comm _fault_err, 8, 8 .comm _fault_rsp, 8, 8 -.comm _fault_rbp, 8, 8 -.comm _fault_rax, 8, 8 # ISR 入口暂存 caller-saved 寄存器(rax/rcx/r8-r11):本宏会用它们当临时寄存器 # (读 RIP/CS/RFLAGS、算 is_user),若不保存就会破坏被中断代码在这些寄存器里的 # 活跃值 → 概率性内存破坏/异常。出口(iretq 前)恢复。 @@ -75,8 +75,6 @@ isr_default: .globl isr\num isr\num: ISR_SAVE_CLOBBERED - # 入口最早保存崩溃 rax(后续 mov rax,[rsp+8] 读 CS 会覆盖它) - mov [_fault_rax], rax # Check CS RPL to determine if from Ring 3 mov rax, [rsp + 8] # CS from interrupt frame test al, 3 # RPL bits: 0 = Ring 0, 3 = Ring 3 @@ -102,8 +100,8 @@ isr\num\()_from_user: mov r10, [rsp + 16] # RFLAGS mov rax, [rsp + 24] # RSP (user) mov r11, [rsp + 32] # SS (user) - # Save user RSP to per_cpu for potential later use - mov gs:[0x0], rax + # 不再把用户 RSP 写进 per_cpu.user_rsp:该槽已无读者(syscall 返回改为从本线程内核栈取), + # 而它是"每 CPU 一个" —— 别的线程写它会让"正挂起的 syscall"返回时拿到别人的栈。 add rsp, 40 # remove the 5 CPU-pushed qwords isr\num\()_frame_ready: @@ -128,7 +126,6 @@ isr\num\()_frame_ready: push rsi push rdi push rbp - mov [_fault_rbp], rbp push r8 push r9 push r10 @@ -137,9 +134,6 @@ isr\num\()_frame_ready: push r13 push r14 push r15 - # Save faulting RIP and error code for debug - mov [_fault_rip], r8 - mov [_fault_err], rdx call isr\num\()_handler pop r15 pop r14 @@ -267,8 +261,6 @@ isr\num\()_ret_user: .globl isr\num isr\num: ISR_SAVE_CLOBBERED - # 入口最早保存崩溃 rax(后续 mov rax,[rsp+16] 读 CS 会覆盖它) - mov [_fault_rax], rax # Stack at entry (with error code): # From Ring 0: [rsp]=error_code, [rsp+8]=RIP, [rsp+16]=CS, [rsp+24]=RFLAGS # From Ring 3: [rsp]=error_code, [rsp+8]=RIP, [rsp+16]=CS, [rsp+24]=RFLAGS, [rsp+32]=RSP, [rsp+40]=SS @@ -286,7 +278,10 @@ isr\num\()_from_kernel: mov r10, [rsp + 24] # RFLAGS xor r11, r11 # SS = 0 xor rax, rax # RSP = 0 - lea rcx, [rsp + 32] # faulting RSP = entry_rsp + 32 (err + RIP + CS + RFLAGS) + # 【诊断】保存异常现场:rdx=错误码 / r8=RIP;出错 RSP = 入口 rsp + 32(err+RIP+CS+RFLAGS) + mov [_fault_rip], r8 + mov [_fault_err], rdx + lea rcx, [rsp + 32] mov [_fault_rsp], rcx add rsp, 32 # remove 4 CPU-pushed qwords (error + 3 frame) jmp isr\num\()_frame_ready @@ -300,8 +295,11 @@ isr\num\()_from_user: mov r10, [rsp + 24] # RFLAGS mov rax, [rsp + 32] # RSP (user) mov r11, [rsp + 40] # SS (user) - mov [_fault_rsp], rax # faulting RSP = user RSP - mov gs:[0x0], rax # save user RSP + # 【诊断】同 kernel 路径:rdx=错误码 / r8=RIP;用户态出错 RSP 就是被压下的用户 RSP + mov [_fault_rip], r8 + mov [_fault_err], rdx + mov [_fault_rsp], rax + # 同上:不再写 per_cpu.user_rsp(该槽已无读者,且是每 CPU 一个 ⇒ 会串台) add rsp, 48 # remove 6 CPU-pushed qwords (error + 5 frame) isr\num\()_frame_ready: @@ -326,7 +324,6 @@ isr\num\()_frame_ready: push rsi push rdi push rbp - mov [_fault_rbp], rbp push r8 push r9 push r10 @@ -335,9 +332,6 @@ isr\num\()_frame_ready: push r13 push r14 push r15 - # Save faulting RIP and error code for debug - mov [_fault_rip], r8 - mov [_fault_err], rdx call isr\num\()_handler pop r15 pop r14 @@ -444,11 +438,15 @@ syscall_entry: jnc syscall_from_kernel syscall_from_user: swapgs - mov gs:[0x0], rsp - mov rsp, gs:[0x8] + mov gs:[0x10], rsp # 瞬时暂存用户 rsp:用**无人写入**的 0x10 槽(不再用 0x0 —— 那个槽 + # 是"每 CPU 一个"且被 int 0x80/中断写过,容易被别的线程污染)。 + # 入口段 IF=0 且不切线程,故这几条指令之间不可能被打断。 + mov rsp, gs:[0x8] # 切内核栈 + push qword ptr gs:[0x10] # 【修复】用户 rsp 存进**本线程内核栈**:返回只从这里取 push 0 # 来源标记(用户态发起):出口据此选 sysretq jmp syscall_common syscall_from_kernel: + push 0 # 占位(内核路径无用户 rsp;保持帧布局与用户路径一致) push 1 # 来源标记(内核态发起):出口据此 jmp rcx 返回,且不 swapgs syscall_common: push rcx @@ -505,13 +503,20 @@ syscall_common: cmp qword ptr [rsp], 0 jne syscall_ret_kernel syscall_ret_user: - mov rsp, gs:[0x0] # 用户路径直接切回用户栈(本层标记槽留在内核栈,下次入口由 TSS.RSP0 重置) + # 【修复】用户 rsp 从**本线程内核栈**取(入口压入),不再读 per_cpu.user_rsp —— + # 那个槽是"每 CPU 一个",别的线程的 int 0x80 / Ring-3 中断会把自己的 rsp 写进去, + # 正在挂起的 syscall 返回时就会拿到"别人的栈"(现场:rsp=cr2=0x100000000)。 + # 帧布局(14 个寄存器 pop 完后的 rsp 指向处):[rsp]=来源标记(0),[rsp+8]=用户 rsp。 + # 注意:这里**不借任何通用寄存器**(rax/rdx/rsi/rdi/r8-r15 都是 app 的活值;rcx 是 + # sysretq 的返回地址;r11 还要用于 popfq)⇒ 用 `mov rsp, [rsp+8]`(x86 成熟惯用法: + # 源操作数先读、再写 rsp)。 + push r11 # RFLAGS 落到内核栈 + popfq # 此后 IF=1,但仍在 Ring0 且 GS 仍是 &_per_cpu ⇒ 与 Ring-0 中断路径一致,安全 + mov rsp, [rsp + 8] # 切回用户栈(不动通用寄存器) swapgs - push r11 - popfq sysretq syscall_ret_kernel: - add rsp, 8 # 内核路径:弹掉本层标记槽,恢复到入口 rsp + add rsp, 16 # 内核路径:弹掉 占位槽 + 来源标记,恢复到入口 rsp push r11 popfq jmp rcx @@ -554,8 +559,11 @@ isr80: test byte ptr [rsp + 24], 3 # 入口帧 CS([S+24])的 RPL;内存操作数,不占用寄存器 jz isr80_frame_ready # 内核态入口:无需 swapgs swapgs - mov rax, [rsp + 40] # RSP (user) - mov gs:[0x0], rax # per_cpu.user_rsp(与 syscall 路径同款簿记) + # 注意:这里**不要**写 gs:[0x0]!per_cpu.user_rsp 是"每 CPU 一个"的槽,而 SYSCALL 路径 + # 在入口写入、出口读取它。若 int 0x80(其它线程在用户态发起)也在同一槽里写一次, + # 就会把正在进行的 syscall 的用户 rsp 覆盖成"别人的 rsp" ⇒ 那条 syscall 返回时 sysretq + # 用错 rsp ⇒ 用户态在错误栈上执行(现场:rsp=cr2=0x100000000 = mm.vm_alloc 的首个虚拟地址)。 + # int 0x80 的返回直接用 CPU 压下的原始帧(RIP/CS/RFLAGS/RSP/SS 都在栈上),无需该槽。 isr80_frame_ready: # 5 个占位 push:统一帧只是用来固定下面 15 个保存槽的偏移。收尾直接在 **CPU 压下的原始帧** diff --git a/VKernel/Kernel/intr/syscall.vp b/VKernel/Kernel/intr/syscall.vp index fc9430a..212668c 100644 --- a/VKernel/Kernel/intr/syscall.vp +++ b/VKernel/Kernel/intr/syscall.vp @@ -110,6 +110,18 @@ def DRAW_TEXT(win_id: int, x: int, y: int, text: const str, color: uint32) -> in def EXIT(code: int) -> int: serial.puts("[syscall] exit called\n") desktop.destroy_by_pid(process.current()) + # 真正的退出:退役本线程并让出,**绝不返回用户态**。 + # 只 destroy 窗口不够:app 的 _start 在 process.exit(0) 之后还有 `return 0`,而内核是以 + # rsp = USER_STACK_VTOP 进入 _start 的(elf.vp:379),VTOP 正是用户栈映射 + # [VTOP-64K, VTOP) 的上界之外那一字节 ⇒ 一旦 _start 收尾 `ret` 就会去读 [VTOP]: + # #PF cr2=rsp=0x7fffffe00000,rip 在 _start 内,rbp=0,rax=退出码(实测现场)。 + # 口径与异常终止路径一致(idt.vp 的 free_user_thread + state=THREAD_DONE + 不再回用户态)。 + sched.Scheduler.exit_current_thread() + # 【硬保护】这里绝不允许返回:一旦返回就会沿调用链回到 app 的 _start 末尾,那里 `ret` + # 会弹到一个 0 返回地址并去 0 取指(现场:[0E] rip=0x0 err=0x15 = 用户态取指缺页, + # 紧随 [syscall] exit called,tid=5)。停留在此让调度器彻底不再调度本线程。 + while True: + sched.Scheduler._yield() return 0 def WAIT_WINDOW(win_id: int) -> int: