From 0c47d817671b8617bd544b42e3ae4b2e87440e7c Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Mon, 26 Oct 2026 22:41:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(diag):=20=E5=BC=82=E5=B8=B8=E7=8E=B0?= =?UTF-8?q?=E5=9C=BA=E5=AE=8C=E6=95=B4=E8=BD=AC=E5=82=A8=EF=BC=88=E8=A3=B8?= =?UTF-8?q?=E4=B8=B2=E5=8F=A3=E9=80=9A=E8=B7=AF=20+=20=E6=97=A0=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E7=A0=81=E5=90=91=E9=87=8F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - isr.s:ISR_NOERR 也保存 RIP/CS/RFLAGS/RSP,_fault_err 用 -1 表示无错误码; 两类宏都补存 rbx/rbp/r12(用户态 #PF 时 r12 常为出错 store 的目标对象基址) - idt.vp:新增不经驱动/缓冲的裸串口输出(asm.outb),打印 rip/rsp/cs/flags、 u 位、callee-saved、错误码;#UD 附带出错指令字节,#PF 附带 cr2 - 前 32 条异常打印完整现场,之后每次只补 '*',避免异常风暴刷爆串口 --- VKernel/Kernel/intr/idt.vp | 129 ++++++++++++++++++++++++++++++++++++- VKernel/Kernel/intr/isr.s | 38 ++++++++++- 2 files changed, 163 insertions(+), 4 deletions(-) diff --git a/VKernel/Kernel/intr/idt.vp b/VKernel/Kernel/intr/idt.vp index f5af2cb..4a3c547 100644 --- a/VKernel/Kernel/intr/idt.vp +++ b/VKernel/Kernel/intr/idt.vp @@ -76,6 +76,45 @@ extern def isr47() extern def isr80() extern def isr_default() +# =========================================================================== +# 【诊断】异常现场的"裸串口"输出通路 +# +# 为什么不直接用 serial.puts:`[06]` 风暴现场里只能看到裸 `[06]`、看不到任何后续文字, +# 说明 serial.puts 这条路当时是断的(缓冲区/锁/驱动对象状态可能在故障中已损坏,或者它自己 +# 再次触发异常)。而 `[06]` 之所以出得来,是因为它由 isrCommonhandler 开头直接用 `out 0x3F8` +# 写出 —— 只要 CPU 还能 out,这条路就一定能出字。 +# 因此下面几条诊断函数全部走 `asm.outb`,不经过任何驱动/缓冲/全局状态。 +# =========================================================================== +define DIAG_COM1 = 0x3F8 + +# 已输出的完整现场条数:异常风暴时只详细打前若干条,之后每来一次只补一个 '*', +# 避免把串口刷爆(真正的定位信息全在最前面的几条里)。 +_diag_dumps: int = 0 + +_hex_digits: [17]char = "0123456789ABCDEF" + +def _diag_putc(ch: int): + asm.outb(DIAG_COM1, uint8(ch & 0xFF)) + +def _diag_puts(s: const str): + i: int = 0 + while s[i] != 0: + _diag_putc(int(s[i])) + i += 1 + +# 定宽十六进制:从低位往高位填再正序吐出(与 vpui._ptr_to_hex 同款,避免变长移位) +def _diag_hex(v: uint64, digits: int): + buf: [20]char + i: int = digits - 1 + val: uint64 = v + while i >= 0: + nib: int = int(val & uint64(0xF)) + buf[i] = _hex_digits[nib] + val = val >> 4 + i -= 1 + buf[digits] = 0 + _diag_puts(&buf[0]) + def setGate(num: int, base: uint64, sel: uint16, flags: uint8): global idt idt[num].base_low = uint16(base & 0xFFFF) @@ -111,10 +150,81 @@ def isrCommonhandler(interrupt: int): mov al, 93 out dx, al""", op=[ASM_DESCR.CLOBBER_RAX, ASM_DESCR.CLOBBER_RCX, ASM_DESCR.CLOBBER_RDX]) + # 【诊断·完整现场】紧接上面裸打出的 `[VV]`,用同一条裸串口通路补齐现场。 + # isr.s 的两个宏(ISR_NOERR / ISR_ERR)都会填 _fault_*,因此所有向量都有 rip/rsp/cs/flags。 + # 只详细打前 32 条:异常风暴时后面的每条只补一个 '*',避免把串口刷爆。 + global _diag_dumps + _diag_dumps = _diag_dumps + 1 + if _diag_dumps <= 32: + _dg_rip: uint64 = 0 + _dg_rsp: uint64 = 0 + _dg_cs: uint64 = 0 + _dg_fl: uint64 = 0 + __asm__(f"mov {asmout(_dg_rip, ASM_DESCR.OUTPUT_REG)}, [_fault_rip]", + op=[ASM_DESCR.CLOBBER_MEMORY]) + __asm__(f"mov {asmout(_dg_rsp, ASM_DESCR.OUTPUT_REG)}, [_fault_rsp]", + op=[ASM_DESCR.CLOBBER_MEMORY]) + __asm__(f"mov {asmout(_dg_cs, ASM_DESCR.OUTPUT_REG)}, [_fault_cs]", + op=[ASM_DESCR.CLOBBER_MEMORY]) + __asm__(f"mov {asmout(_dg_fl, ASM_DESCR.OUTPUT_REG)}, [_fault_flags]", + op=[ASM_DESCR.CLOBBER_MEMORY]) + _diag_puts(" rip=0x") + _diag_hex(_dg_rip, 16) + _diag_puts(" rsp=0x") + _diag_hex(_dg_rsp, 16) + _diag_puts(" cs=0x") + _diag_hex(_dg_cs, 4) + _diag_puts(" fl=0x") + _diag_hex(_dg_fl, 8) + _diag_puts(" u=") + _diag_putc(48 + int(_dg_cs & uint64(3))) + # callee-saved 现场(两个宏都已在入口保存):r12 常是出错 store 的目标对象基址 + _dg_r12: uint64 = 0 + _dg_rbx: uint64 = 0 + _dg_rbp: uint64 = 0 + __asm__(f"mov {asmout(_dg_r12, ASM_DESCR.OUTPUT_REG)}, [_fault_r12]", + op=[ASM_DESCR.CLOBBER_MEMORY]) + __asm__(f"mov {asmout(_dg_rbx, ASM_DESCR.OUTPUT_REG)}, [_fault_rbx]", + op=[ASM_DESCR.CLOBBER_MEMORY]) + __asm__(f"mov {asmout(_dg_rbp, ASM_DESCR.OUTPUT_REG)}, [_fault_rbp]", + op=[ASM_DESCR.CLOBBER_MEMORY]) + _diag_puts(" rbx=0x") + _diag_hex(_dg_rbx, 16) + _diag_puts(" rbp=0x") + _diag_hex(_dg_rbp, 16) + _diag_puts(" r12=0x") + _diag_hex(_dg_r12, 16) + if (interrupt == 8 or interrupt == 10 or interrupt == 11 or interrupt == 12 + or interrupt == 13 or interrupt == 14 or interrupt == 17): + # 只有 ISR_ERR 系列才真的压了错误码;其余向量的 _fault_err 是 -1 哨兵 + _dg_err: uint64 = 0 + __asm__(f"mov {asmout(_dg_err, ASM_DESCR.OUTPUT_REG)}, [_fault_err]", + op=[ASM_DESCR.CLOBBER_MEMORY]) + _diag_puts(" err=0x") + _diag_hex(_dg_err, 4) + else: + _diag_puts(" err=none") + if interrupt == 6: + # #UD:把出错指令的前 8 字节打出来 —— `0F 0B` 就是 ud2(LLVM 插的 trap), + # 其余字节说明是"数据/坏跳转目标被当代码执行"。 + ipb: uint8* = uint8*(_dg_rip) + _diag_puts(" insn=") + for bi in range(8): + _diag_hex(uint64(ipb[bi]), 2) + _diag_putc(32) + if interrupt == 14: + _dg_cr2: uint64 = 0 + __asm__(f"mov {asmout(_dg_cr2, ASM_DESCR.OUTPUT_REG)}, cr2", + op=[ASM_DESCR.CLOBBER_MEMORY]) + _diag_puts(" cr2=0x") + _diag_hex(_dg_cr2, 16) + _diag_putc(10) + else: + _diag_putc(42) # 【诊断】异常现场:出错 RIP / 错误码 / 出错时 RSP(#PF 另打 cr2)。 - # 由 isr.s 的 ISR_ERR 在入口写入 _fault_* ⇒ #GP/#PF 自己就能说出位置,不必再反查地址。 - # 只有带错误码的向量(8/10/11/12/13/14/17,见 isr.s 里用 ISR_ERR 的那些)才会写 _fault_*, - # 其余向量读到的是上次残留 ⇒ 仅在真写了时打印,避免误导。 + # 由 isr.s 的 ISR_NOERR / ISR_ERR 在入口写入 _fault_*(无错误码向量写 -1); + # 这一段走 serial.puts,属于"锦上添花":真正可靠的现场在上面那段裸串口输出里, + # 这里只在带错误码的向量上补充返回槽三连(serial.puts 自身故障时不会出字,属已知局限)。 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 @@ -129,6 +239,19 @@ def isrCommonhandler(interrupt: int): _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) + # callee-saved 现场(ISR_ERR 入口保存):r12 往往就是出错 store 的目标对象基址 + # (cr2 = r12 + 常量偏移),据此能直接对上"写的是哪个对象"。 + _df_r12: uint64 = 0 + _df_rbx: uint64 = 0 + _df_rbp: uint64 = 0 + __asm__(f"mov {asmout(_df_r12, ASM_DESCR.OUTPUT_REG)}, [_fault_r12]", + op=[ASM_DESCR.CLOBBER_MEMORY]) + __asm__(f"mov {asmout(_df_rbx, ASM_DESCR.OUTPUT_REG)}, [_fault_rbx]", + op=[ASM_DESCR.CLOBBER_MEMORY]) + __asm__(f"mov {asmout(_df_rbp, ASM_DESCR.OUTPUT_REG)}, [_fault_rbp]", + op=[ASM_DESCR.CLOBBER_MEMORY]) + viperlib.snprintf(&_dfbuf, 128, " rbx=0x%lx rbp=0x%lx r12=0x%lx\n", _df_rbx, _df_rbp, _df_r12) + serial.puts(&_dfbuf) # 返回槽三连:出错 RSP 附近的栈字。 # ret=0x… [rsp-8]:若 CPU 是在 `ret` 之后于错地址取指的,这里就是被弹走的坏目标; # ra=0x… [rsp] :调用者的返回地址(真内核地址)⇒ 反查即可定位"哪个函数的返回槽被写坏"; diff --git a/VKernel/Kernel/intr/isr.s b/VKernel/Kernel/intr/isr.s index 3a3906a..1c28ac2 100644 --- a/VKernel/Kernel/intr/isr.s +++ b/VKernel/Kernel/intr/isr.s @@ -7,10 +7,21 @@ .comm _yield_lock, 4, 4 .comm _fs_lock, 4, 4 # 【诊断】异常现场(#GP/#PF 等带错误码的向量在 ISR_ERR 入口保存,isrCommonhandler 打印)。 -# 只保留最小三件套:出错 RIP / 错误码 / 出错时的 RSP(#PF 另读 cr2)。 +# 三件套:出错 RIP / 错误码 / 出错时的 RSP(#PF 另读 cr2);另加 callee-saved 的 rbx/rbp/r12。 .comm _fault_rip, 8, 8 .comm _fault_err, 8, 8 .comm _fault_rsp, 8, 8 +# 【诊断】CS / RFLAGS 现场:判"到底是用户态还是内核态出错""中断/方向/对齐标志是否异常"。 +# 注意 ISR_NOERR(含 #UD=6)原来完全没保存 rip,导致 `[06]` 只剩一个光秃秃的向量号; +# 现在两类宏都保存同一套现场,`_fault_err = -1` 表示"本向量没有错误码"。 +.comm _fault_cs, 8, 8 +.comm _fault_flags, 8, 8 +# 【诊断】callee-saved 寄存器现场(#GP/#PF 专用):r12/rbx/rbp 在 ISR 入口尚未被 +# 任何宏破坏,直接落到全局槽里,供 isrCommonhandler 打印。用户态 #PF 里 r12 常常正是 +# 那条出错 store 的目标对象基址(cr2 = r12 + offset),据此可直接对上是哪个对象。 +.comm _fault_r12, 8, 8 +.comm _fault_rbx, 8, 8 +.comm _fault_rbp, 8, 8 # ISR 入口暂存 caller-saved 寄存器(rax/rcx/r8-r11):本宏会用它们当临时寄存器 # (读 RIP/CS/RFLAGS、算 is_user),若不保存就会破坏被中断代码在这些寄存器里的 # 活跃值 → 概率性内存破坏/异常。出口(iretq 前)恢复。 @@ -75,6 +86,19 @@ isr_default: .globl isr\num isr\num: ISR_SAVE_CLOBBERED + # 【诊断】无错误码向量(#DE/#UD/#BR/#OF… 含 #UD=6)也要保存完整现场: + # 此时 [rsp]=RIP、[rsp+8]=CS、[rsp+16]=RFLAGS 是 CPU 刚压下的原始帧,尚未被本宏改动。 + # 不保存的话 isrCommonhandler 只能打出一个 `[06]`,没有 rip 可供反查。 + mov rax, [rsp] + mov [_fault_rip], rax + mov rax, [rsp + 8] + mov [_fault_cs], rax + mov rax, [rsp + 16] + mov [_fault_flags], rax + mov qword ptr [_fault_err], -1 # -1 = 本向量没有错误码(ISR_ERR 写真实错误码) + mov [_fault_r12], r12 + mov [_fault_rbx], rbx + mov [_fault_rbp], rbp # 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 @@ -86,6 +110,9 @@ isr\num\()_from_kernel: mov r9, [rsp + 8] # CS mov r10, [rsp + 16] # RFLAGS xor r11, r11 # SS = 0 + # 出错时的 RSP:无错误码帧只有 RIP/CS/RFLAGS 三个 qword ⇒ 原始 rsp = 当前 rsp + 24 + lea rax, [rsp + 24] + mov [_fault_rsp], rax xor rax, rax # RSP = 0 add rsp, 24 # remove the 3 CPU-pushed qwords jmp isr\num\()_frame_ready @@ -99,6 +126,7 @@ isr\num\()_from_user: mov r9, [rsp + 8] # CS mov r10, [rsp + 16] # RFLAGS mov rax, [rsp + 24] # RSP (user) + mov [_fault_rsp], rax mov r11, [rsp + 32] # SS (user) # 不再把用户 RSP 写进 per_cpu.user_rsp:该槽已无读者(syscall 返回改为从本线程内核栈取), # 而它是"每 CPU 一个" —— 别的线程写它会让"正挂起的 syscall"返回时拿到别人的栈。 @@ -263,6 +291,10 @@ isr\num\()_ret_user: .globl isr\num isr\num: ISR_SAVE_CLOBBERED + # 【诊断】callee-saved 寄存器此时未被任何宏改动,先保存现场(不影响下面的寄存器用法) + mov [_fault_r12], r12 + mov [_fault_rbx], rbx + mov [_fault_rbp], rbp # 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 @@ -285,6 +317,8 @@ isr\num\()_from_kernel: mov [_fault_err], rdx lea rcx, [rsp + 32] mov [_fault_rsp], rcx + mov [_fault_cs], r9 + mov [_fault_flags], r10 add rsp, 32 # remove 4 CPU-pushed qwords (error + 3 frame) jmp isr\num\()_frame_ready @@ -301,6 +335,8 @@ isr\num\()_from_user: mov [_fault_rip], r8 mov [_fault_err], rdx mov [_fault_rsp], rax + mov [_fault_cs], r9 + mov [_fault_flags], r10 # 同上:不再写 per_cpu.user_rsp(该槽已无读者,且是每 CPU 一个 ⇒ 会串台) add rsp, 48 # remove 6 CPU-pushed qwords (error + 5 frame)