fix(keyboard): init() 改为幂等(避免重复初始化禁用键盘接口);按键时打印扫描码/字符到串口便于验证

This commit is contained in:
TermiNexus committed 2026-10-16 14:12:50 +08:00
1 parent befed3904a
commit d37eac32fe
3 files changed
+79 -214

No files matched your search

@@ -2,6 +2,7 @@ import asm
import intr.idt as idt
import drivers.serial.uart.serial as serial
import string
import viperlib
import t, c
@@ -17,7 +18,9 @@ buffer: [KBD_BUF_SIZE]uint8
buf_head: static uint32 = 0
buf_tail: static uint32 = 0
buf_count: static uint32 = 0
kbd_irq_cnt: static uint64 = 0
kbd_irq_cnt: static uint64 = 0
# 硬件初始化只做一次(init 有多个调用点,见 init() 内注释)
_kbd_inited: static int = 0
shift_l: static uint8 = 0
shift_r: static uint8 = 0
@@ -70,6 +73,11 @@ def irq_handler() -> int:
if (st & 0x20) != 0:
return 0
scancode: uint8 = asm.inb(KBD_DATA)
# 按键验证:把 IRQ1 收到的扫描码打到串口(高位 0x80 = 释放),
# 用来确认"键盘中断是否到达"这一环。
_kmsg: [48]char
viperlib.snprintf(&_kmsg, 48, "[kbd] sc=0x%x\n", int(scancode))
serial.puts(&_kmsg)
if buf_count < KBD_BUF_SIZE:
buffer[buf_tail] = scancode
buf_tail = buf_tail + 1
@@ -106,6 +114,14 @@ def has_data() -> int:
return buf_count
def init():
global _kbd_inited
# 幂等:init() 有两个调用点(devfs 注册 /dev/input/keyboard、kbd_svc.start),
# 重复执行会再次发 0xAD 禁用键盘接口,而那次 CCB 写入往往失败(键盘数据串入
# CCB 读取,回读为 0)⇒ 0xAE 恢复不了 ⇒ 接口停在禁用态 ⇒ 按键不再产生任何字节
# (现场:ki 恒为 2/3,desktop 轮询永远看不到键盘字节,键盘完全无法输入)。
if _kbd_inited != 0:
return
_kbd_inited = 1
drain: uint32
drain = 0
while (asm.inb(KBD_STATUS) & 0x01) != 0:
+12 -169
View File
@@ -24,20 +24,6 @@ export idt: [256]idt_entry = []
typedef irq_handler_t = callable[[], int]
def _SafeStackWord(addr: uint64) -> uint64:
"""异常处理专用安全解引用:仅当 addr 落在**内核**区间(0x1000..0x80000000)才读。
异常处理里绝不允许再触发一次 #PF:一旦读到用户地址(典型是 iret 帧里压入的
user_rsp=0x7fffffe00000),就会在 #PF 处理中再次 #PF → 无限重入 → 内核栈耗尽
→ 三重故障(QEMU 窗口直接消失,且**看不到真正的首故障**)。
"""
if addr < 0x1000 or addr >= 0x80000000: return 0
v: uint64 = 0
__asm__(f"""mov rax, {asminp(addr, ASM_DESCR.REG_ANY)}
mov {asmout(v, ASM_DESCR.OUTPUT_REG)}, [rax]""",
op=[ASM_DESCR.CLOBBER_RAX])
return v
extern def isr0()
extern def isr1()
extern def isr2()
@@ -125,160 +111,6 @@ def isrCommonhandler(interrupt: int):
mov al, 93
out dx, al""",
op=[ASM_DESCR.CLOBBER_RAX, ASM_DESCR.CLOBBER_RCX, ASM_DESCR.CLOBBER_RDX])
# Print CR2 for page faults (ISR 14)
if interrupt == 14:
cr2: uint64 = 0
__asm__(f"""mov {asmout(cr2, ASM_DESCR.OUTPUT_REG)}, cr2""",
op=[ASM_DESCR.CLOBBER_RAX])
fault_rip: uint64 = 0
fault_err: uint64 = 0
fault_rsp: uint64 = 0
__asm__(f"""mov {asmout(fault_rip, ASM_DESCR.OUTPUT_REG)}, [_fault_rip]
mov {asmout(fault_err, ASM_DESCR.OUTPUT_REG)}, [_fault_err]
mov {asmout(fault_rsp, ASM_DESCR.OUTPUT_REG)}, [_fault_rsp]""",
op=[ASM_DESCR.CLOBBER_RAX])
buf: [100]char
viperlib.snprintf(&buf, 100, "[PF] cr2=0x%lx rip=0x%lx err=0x%lx rsp=0x%lx\n", cr2, fault_rip, fault_err, fault_rsp)
serial.puts(buf)
# [CR3] 当前 pml4:定位栈区/对象写缺页是否因 pml4 被 switch_pid 切走
# (内核 pml4=0xf801000,用户 pml4=0x9781xxx —— 若显示用户 pml4 而 rsp
# 在 bootloader 栈区 0x60000-0x80000,则是进程切换后未切回内核页表)。
cur_cr3: uint64 = 0
__asm__(f"mov {asmout(cur_cr3, ASM_DESCR.OUTPUT_REG)}, cr3",
op=[ASM_DESCR.CLOBBER_RAX])
cr3_buf: [80]char
viperlib.snprintf(&cr3_buf, 80, "[CR3] pml4=0x%lx\n", cur_cr3)
serial.puts(cr3_buf)
# [临时] syscall 用户 rsp 对照:入口值 vs 返回时待恢复值。
# 两者不同 ⇒ 内核在 syscall 期间覆盖了 gs:[0x0];两者相同且都异常 ⇒ app 自己弄坏了 rsp。
sc_ent: uint64 = 0
sc_ret: uint64 = 0
__asm__(f"""mov rax, [_sc_ent_rsp]
mov {asmout(sc_ent, ASM_DESCR.OUTPUT_REG)}, rax
mov rax, [_sc_ret_rsp]
mov {asmout(sc_ret, ASM_DESCR.OUTPUT_REG)}, rax""",
op=[ASM_DESCR.CLOBBER_RAX])
sc_buf: [160]char
viperlib.snprintf(&sc_buf, 160, "[SYSCALL-RSP] entry=0x%lx ret=0x%lx\n", sc_ent, sc_ret)
serial.puts(sc_buf)
# [RAX] 崩溃时 rax(isr.s 保存):定位 cr2 与 rax 关系(rax-偏移=cr2)
f_rax: uint64 = 0
__asm__(f"""mov {asmout(f_rax, ASM_DESCR.OUTPUT_REG)}, [_fault_rax]""",
op=[ASM_DESCR.CLOBBER_RAX])
rax_buf: [80]char
viperlib.snprintf(&rax_buf, 80, "[RAX] rax=0x%lx\n", f_rax)
serial.puts(rax_buf)
# [STK] rbp 链回溯(定位栈损坏源)。**每一跳都必须先校验再解引用**(_SafeStackWord):
# 旧实现是 `mov rax,[rax]; mov rax,[rax+8]` 的裸链式解引用,一旦链上出现用户地址
# (实测就是 iret 帧里压入的 user_rsp=0x7fffffe00000)就会在 #PF 处理中再触发 #PF
# → 无限重入、rsp 每次下移 ~0x2A0 → 内核栈耗尽 → 三重故障,且真正的首故障被淹没。
# 用户态 rbp(>=0x80000000)直接不回溯。
fr0: uint64 = 0
__asm__(f"""mov rax, [_fault_rbp]
mov {asmout(fr0, ASM_DESCR.OUTPUT_REG)}, rax""",
op=[ASM_DESCR.CLOBBER_RAX])
stk_buf: [160]char
if fr0 < 0x1000 or fr0 >= 0x80000000:
viperlib.snprintf(&stk_buf, 160, "[STK] rbp=0x%lx out of kernel range, chain skipped\n", fr0)
serial.puts(stk_buf)
else:
fr1: uint64 = _SafeStackWord(fr0 + 8)
rbp1: uint64 = _SafeStackWord(fr0)
fr2: uint64 = _SafeStackWord(rbp1 + 8)
rbp2: uint64 = _SafeStackWord(rbp1)
fr3: uint64 = _SafeStackWord(rbp2 + 8)
rbp3: uint64 = _SafeStackWord(rbp2)
fr4: uint64 = _SafeStackWord(rbp3 + 8)
viperlib.snprintf(&stk_buf, 160, "[STK] rbp=0x%lx ret: 0x%lx 0x%lx 0x%lx 0x%lx\n",
fr0, fr1, fr2, fr3, fr4)
serial.puts(stk_buf)
elif interrupt == 13:
gp_rip: uint64 = 0
gp_err: uint64 = 0
gp_rsp: uint64 = 0
gp_cr4: uint64 = 0
gp_cr0: uint64 = 0
__asm__(f"""mov {asmout(gp_rip, ASM_DESCR.OUTPUT_REG)}, [_fault_rip]
mov {asmout(gp_err, ASM_DESCR.OUTPUT_REG)}, [_fault_err]
mov {asmout(gp_rsp, ASM_DESCR.OUTPUT_REG)}, [_fault_rsp]
mov rax, cr4
mov {asmout(gp_cr4, ASM_DESCR.OUTPUT_REG)}, rax
mov rax, cr0
mov {asmout(gp_cr0, ASM_DESCR.OUTPUT_REG)}, rax""",
op=[ASM_DESCR.CLOBBER_RAX])
gp_buf: [140]char
viperlib.snprintf(&gp_buf, 140, "[GP] rip=0x%lx err=0x%lx rsp=0x%lx cr4=0x%lx cr0=0x%lx\n",
gp_rip, gp_err, gp_rsp, gp_cr4, gp_cr0)
serial.puts(gp_buf)
# [GP-RAX]:崩溃时的 rax(isr.s 保存的 _fault_rax)。
# #GP(err=0) 最常见于"写/读非规范地址"(如 mm 空闲链 next/prev 被写坏后
# `mov [rax+0x10], rcx`):rax 就是那个坏基址,+0x10 即触发的那次访存地址。
# 现场缺这一条,只剩"[STK] rbp=0x0"无从定位,故按 #PF 分支的口径补齐。
g_frax: uint64 = 0
__asm__(f"""mov rax, [_fault_rax]
mov {asmout(g_frax, ASM_DESCR.OUTPUT_REG)}, rax""",
op=[ASM_DESCR.CLOBBER_RAX])
gp_reg_buf: [140]char
viperlib.snprintf(&gp_reg_buf, 140, "[GP-RAX] rax=0x%lx (rax+0x10=0x%lx)\n",
g_frax, g_frax + 16)
serial.puts(gp_reg_buf)
# [STK] rbp 链回溯(与 #PF 分支同款安全口径:每跳先校验再解引用,见 _SafeStackWord)
gfr0: uint64 = 0
__asm__(f"""mov rax, [_fault_rbp]
mov {asmout(gfr0, ASM_DESCR.OUTPUT_REG)}, rax""",
op=[ASM_DESCR.CLOBBER_RAX])
gp_stk_buf: [160]char
if gfr0 < 0x1000 or gfr0 >= 0x80000000:
viperlib.snprintf(&gp_stk_buf, 160, "[STK] rbp=0x%lx out of kernel range, chain skipped\n", gfr0)
serial.puts(gp_stk_buf)
else:
gfr1: uint64 = _SafeStackWord(gfr0 + 8)
grbp1: uint64 = _SafeStackWord(gfr0)
gfr2: uint64 = _SafeStackWord(grbp1 + 8)
grbp2: uint64 = _SafeStackWord(grbp1)
gfr3: uint64 = _SafeStackWord(grbp2 + 8)
grbp3: uint64 = _SafeStackWord(grbp2)
gfr4: uint64 = _SafeStackWord(grbp3 + 8)
viperlib.snprintf(&gp_stk_buf, 160, "[STK] rbp=0x%lx ret: 0x%lx 0x%lx 0x%lx 0x%lx\n",
gfr0, gfr1, gfr2, gfr3, gfr4)
serial.puts(gp_stk_buf)
else:
# 其他异常(#UD 等):打印 rip/rsp 定位执行位置(isr.s 已保存 _fault_rip)
ex_rip: uint64 = 0
ex_rsp: uint64 = 0
__asm__(f"""mov {asmout(ex_rip, ASM_DESCR.OUTPUT_REG)}, [_fault_rip]
mov {asmout(ex_rsp, ASM_DESCR.OUTPUT_REG)}, [_fault_rsp]""",
op=[ASM_DESCR.CLOBBER_RAX])
ex_buf: [100]char
viperlib.snprintf(&ex_buf, 100, "[EX] int=%d rip=0x%lx rsp=0x%lx\n", interrupt, ex_rip, ex_rsp)
serial.puts(ex_buf)
# [RAX] 崩溃时 rax(isr.s 保存)
f_rax: uint64 = 0
__asm__(f"""mov {asmout(f_rax, ASM_DESCR.OUTPUT_REG)}, [_fault_rax]""",
op=[ASM_DESCR.CLOBBER_RAX])
rax_buf: [80]char
viperlib.snprintf(&rax_buf, 80, "[RAX] rax=0x%lx\n", f_rax)
serial.puts(rax_buf)
# [STK] rbp 链回溯(与 #PF 分支同款安全口径:每跳先校验再解引用,见 _SafeStackWord)
fr0: uint64 = 0
__asm__(f"""mov rax, [_fault_rbp]
mov {asmout(fr0, ASM_DESCR.OUTPUT_REG)}, rax""",
op=[ASM_DESCR.CLOBBER_RAX])
stk_buf: [160]char
if fr0 < 0x1000 or fr0 >= 0x80000000:
viperlib.snprintf(&stk_buf, 160, "[STK] rbp=0x%lx out of kernel range, chain skipped\n", fr0)
serial.puts(stk_buf)
else:
fr1: uint64 = _SafeStackWord(fr0 + 8)
rbp1: uint64 = _SafeStackWord(fr0)
fr2: uint64 = _SafeStackWord(rbp1 + 8)
rbp2: uint64 = _SafeStackWord(rbp1)
fr3: uint64 = _SafeStackWord(rbp2 + 8)
rbp3: uint64 = _SafeStackWord(rbp2)
fr4: uint64 = _SafeStackWord(rbp3 + 8)
viperlib.snprintf(&stk_buf, 160, "[STK] rbp=0x%lx ret: 0x%lx 0x%lx 0x%lx 0x%lx\n",
fr0, fr1, fr2, fr3, fr4)
serial.puts(stk_buf)
s: sched.Scheduler* = sched._sched_ptr
# 如果调度器未初始化,直接进入安全停机状态
if s == 0:
@@ -366,7 +198,16 @@ export def isr31_handler(): isrCommonhandler(31)
export def isr32_handler(): timer.timer_handler()
export def isr33_handler():
global _kbd_irq_seen
handler: irq_handler_t = irqGetHandler(1)
# 键盘验证:IRQ1 是否真的到达 IDT,以及 handler 是否已安装。
# handler=0x0 → 到达了但没装上(键盘仍不可用,问题在安装侧);
# 整行不出现 → IRQ1 根本没到(问题在控制器/设备侧)。只打前 8 次。
if _kbd_irq_seen < 8:
_kbd_irq_seen += 1
_kb: [64]char
viperlib.snprintf(&_kb, 64, "[kbd] irq1 handler=%lx\n", uint64(handler))
serial.puts(&_kb)
if handler:
__asm__(f"""call {asminp(handler, ASM_DESCR.REG_ANY)}""",
op=[ASM_DESCR.CLOBBER_MEMORY, ASM_DESCR.CLOBBER_RAX,
@@ -577,7 +418,9 @@ def init_irqs():
setGate(46, uint64(isr46), CODE_SEG, 0x8E)
setGate(47, uint64(isr47), CODE_SEG, 0x8E)
irq_handlers: static [16]irq_handler_t = [None]
irq_handlers: static [16]irq_handler_t = [None]
# 键盘验证:IRQ1 到达次数(仅用于打印前若干次)
_kbd_irq_seen: static int = 0
def irqInstallHandler(irq: int, handler: irq_handler_t, name: str) -> int:
global irq_handlers
+50 -44
View File
@@ -887,6 +887,12 @@ def _update_keyboard():
continue
_held_key_add(sc, 0)
ascii_ch: int = _ps2_scancode_to_ascii(sc)
# 按键验证:把本帧解出的字符打到串口,确认"键盘 → 扫描码 → 字符"全链路正常。
# 不要求有焦点窗口(便于在无窗口时也能验证键盘本身)。
if ascii_ch != 0:
_kmsg: [64]char
viperlib.snprintf(&_kmsg, 64, "[kbd] key='%c' code=%d sc=0x%x\n", ascii_ch, ascii_ch, int(sc))
serial.puts(&_kmsg)
if ascii_ch != 0 and _focused_win >= 0 and _focused_win < _win_count and _wins[_focused_win].active:
push_evt: InputEvent
push_evt.type = EVENT_TYPE_KEY
@@ -1870,43 +1876,43 @@ def desktop_thread(arg: rawptr) -> int:
sched.Scheduler.try_reschedule()
return 0
# .bss 不变量影子 + 校验:_fb/_screen_w/_screen_h/_fb_pitch/_back_fb/_row_buf/_bg_row_buf/
# _dirty_rows 都是 .bss 全局,初始化后应恒定不变(曾观测到它们被偶发写坏/清零,
# 导致刷新写到物理 0 起、覆盖内核自身)。桌面线程每秒比对一次,命中即 fail-fast。
_InvFb: uint64 = 0
_InvW: int = 0
_InvH: int = 0
_InvPitch: int = 0
_InvBack: uint64 = 0
_InvRow: uint64 = 0
_InvBgRow: uint64 = 0
_InvDirty: uint64 = 0
def _SetFrameInvariants():
global _InvFb, _InvW, _InvH, _InvPitch, _InvBack, _InvRow, _InvBgRow, _InvDirty
_InvFb = uint64(_fb)
_InvW = _screen_w
_InvH = _screen_h
_InvPitch = _fb_pitch
_InvBack = uint64(_back_fb)
_InvRow = uint64(_row_buf)
_InvBgRow = uint64(_bg_row_buf)
_InvDirty = uint64(_dirty_rows)
def _CheckFrameInvariants():
if (uint64(_fb) != _InvFb or _screen_w != _InvW or _screen_h != _InvH or _fb_pitch != _InvPitch
or uint64(_back_fb) != _InvBack or uint64(_row_buf) != _InvRow
or uint64(_bg_row_buf) != _InvBgRow or uint64(_dirty_rows) != _InvDirty):
b: [224]char
viperlib.snprintf(&b, 224,
"[FATAL] .bss invariants broken: fb=0x%lx/%lx w=%d/%d h=%d/%d pitch=%d/%d back=0x%lx/%lx row=0x%lx/%lx bgr=0x%lx/%lx dr=0x%lx/%lx\n",
uint64(_fb), _InvFb, _screen_w, _InvW, _screen_h, _InvH, _fb_pitch, _InvPitch,
uint64(_back_fb), _InvBack, uint64(_row_buf), _InvRow,
uint64(_bg_row_buf), _InvBgRow, uint64(_dirty_rows), _InvDirty)
serial.puts(&b)
while True:
sched.Scheduler._yield()
# .bss 不变量影子 + 校验:_fb/_screen_w/_screen_h/_fb_pitch/_back_fb/_row_buf/_bg_row_buf/
# _dirty_rows 都是 .bss 全局,初始化后应恒定不变(曾观测到它们被偶发写坏/清零,
# 导致刷新写到物理 0 起、覆盖内核自身)。桌面线程每秒比对一次,命中即 fail-fast。
_InvFb: uint64 = 0
_InvW: int = 0
_InvH: int = 0
_InvPitch: int = 0
_InvBack: uint64 = 0
_InvRow: uint64 = 0
_InvBgRow: uint64 = 0
_InvDirty: uint64 = 0
def _SetFrameInvariants():
global _InvFb, _InvW, _InvH, _InvPitch, _InvBack, _InvRow, _InvBgRow, _InvDirty
_InvFb = uint64(_fb)
_InvW = _screen_w
_InvH = _screen_h
_InvPitch = _fb_pitch
_InvBack = uint64(_back_fb)
_InvRow = uint64(_row_buf)
_InvBgRow = uint64(_bg_row_buf)
_InvDirty = uint64(_dirty_rows)
def _CheckFrameInvariants():
if (uint64(_fb) != _InvFb or _screen_w != _InvW or _screen_h != _InvH or _fb_pitch != _InvPitch
or uint64(_back_fb) != _InvBack or uint64(_row_buf) != _InvRow
or uint64(_bg_row_buf) != _InvBgRow or uint64(_dirty_rows) != _InvDirty):
b: [224]char
viperlib.snprintf(&b, 224,
"[FATAL] .bss invariants broken: fb=0x%lx/%lx w=%d/%d h=%d/%d pitch=%d/%d back=0x%lx/%lx row=0x%lx/%lx bgr=0x%lx/%lx dr=0x%lx/%lx\n",
uint64(_fb), _InvFb, _screen_w, _InvW, _screen_h, _InvH, _fb_pitch, _InvPitch,
uint64(_back_fb), _InvBack, uint64(_row_buf), _InvRow,
uint64(_bg_row_buf), _InvBgRow, uint64(_dirty_rows), _InvDirty)
serial.puts(&b)
while True:
sched.Scheduler._yield()
def start(fb_ptr: rawptr, w: int, h: int, pitch: int = 0) -> sched.Thread*:
global _fb, _screen_w, _screen_h, _fb_pitch, _cursor_x, _cursor_y
global _bg_row_buf, _back_fb, _row_buf
@@ -2626,13 +2632,13 @@ def is_active(win_id: int) -> int:
if win_id < 0 or win_id >= _win_count: return 0
return _wins[win_id].active
def get_fb_bytes(win_id: int) -> int:
"""窗口后备缓冲的字节数(WIN_FLUSH 从 app 的用户缓冲拷贝时用;须与 get_fb 配对取)。"""
if win_id < 0 or win_id >= _win_count: return 0
aw: AppWindow* = &_wins[win_id]
if not aw.active: return 0
return aw.fb_w * aw.fb_h * 4
def get_fb_bytes(win_id: int) -> int:
"""窗口后备缓冲的字节数(WIN_FLUSH 从 app 的用户缓冲拷贝时用;须与 get_fb 配对取)。"""
if win_id < 0 or win_id >= _win_count: return 0
aw: AppWindow* = &_wins[win_id]
if not aw.active: return 0
return aw.fb_w * aw.fb_h * 4
def get_fb(win_id: int) -> uint32*:
if win_id < 0 or win_id >= _win_count: return None
aw: AppWindow* = &_wins[win_id]