feat(diag): 异常现场完整转储(裸串口通路 + 无错误码向量)

- 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 条异常打印完整现场,之后每次只补 '*',避免异常风暴刷爆串口
This commit is contained in:
TermiNexus committed 2026-10-26 22:41:14 +08:00
1 parent 8c31645c0f
commit 0c47d81767
2 files changed
+163 -4

No files matched your search

+126 -3
View File
@@ -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] :调用者的返回地址(真内核地址)⇒ 反查即可定位"哪个函数的返回槽被写坏";
+37 -1
View File
@@ -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)