Commit Graph
133 Commits
Author SHA1 Message Date
TermiNexus 4e1be2be88 fix(intr): 修正 isr80 的调用号/arg4 压栈顺序(int 0x80 一直把 arg4 当调用号)
栈向下增长,先压的落在高地址,而要的布局是 [S+0]=调用号(rax)、[S+8]=arg4(r10),所以必须先 push r10 后 push rax;原实现反了 ⇒ mov rax,[rsp+40] 取到 arg4 当调用号、mov r8,[rsp+168] 取到调用号当 arg4 ⇒ syscall_handler 收到错误的 n。

现场(探针实测):用户线程出口跳板 mov eax,34(THREAD_EXIT); int 0x80; jmp $ —— int 0x80 带着错的 n 进 syscall_handler,不匹配任何分支返回 0,iretq 回用户态落到 jmp $ 死转。该线程永不再 yield ⇒ needs_reschedule 无人消费 ⇒ 全系统停止切换:探针显示 cur 冻结在 5、t5 恒为 RUNNING,intr_cs=2B intr_rip=0x7FFFFFDE0007(正是跳板页偏移 7 的 jmp $)rsp=0x7FFFFFDE0000 ⇒ 桌面线程(tid 6)饿死,[perf]/[chk] 永不再打印。

这条路径此前无任何调用者(vpsdk 全走 syscall 指令),所以从未暴露。同时移除定位用的临时探针(timer.vp 心跳/调度器快照、isr.s 的 _intr_*)。
2026-10-16 18:07:57 +08:00
TermiNexus c29c1953df chore(intr): 探针补记被中断代码的 RIP/CS/RSP,用于定位卡死线程停在哪条指令
isr.s: ISR_NOERR/ISR_ERR 的 _frame_ready 里把 r8(RIP)/r9(CS)/rax(RSP) 写进新增的 _intr_rip/_intr_cs/_intr_rsp(只读这三个寄存器,不影响随后压栈)。timer.vp: 心跳行追加 3 个 hex32 字段。定时器 1kHz,所以这三个值几乎总是最后一次定时中断打断的现场:intr_cs=2B 表示该线程卡在用户态 intr_rip 处,=08 表示卡在内核。
2026-10-16 17:58:51 +08:00
TermiNexus 2784b327c7 chore(timer): 临时加 IRQ0 心跳+调度器快照探针,定位「app 退出后 [perf] 停摆」
每 1000 tick 直接写 COM1 一行 [t] <ticks> <current_tid> <thread_count> <各线程 state>,自带 LSR 超时、不依赖 serial 驱动、绝不挂住 ISR。用来区分三种可能:A) 探针也停 = IRQ0/EOI 死了;B) 探针继续但 cur 恒定且该线程 st=RUNNING = 该线程死循环且不再调用带 try_reschedule 的 syscall,needs_reschedule 无人消费;C) cur 在变 = 调度器活着,问题在桌面线程自身。定位后必须删除。
2026-10-16 17:40:51 +08:00
TermiNexus f34fe8a378 fix(sched): 用户线程出口改为跳板页,消除「线程函数 return 即 #PF」
alloc_user_thread 原来在用户栈顶下一格写哨兵返回地址 0:线程函数一旦 return,ret 就把 0 弹进 rip,Ring3 在 VA 0 取指。VA 0 在内核 identity map 里是存在但 supervisor-only 的页,于是现场固定为 rip=0x0 cr2=0x0 err=0x15 (P=1 保护违例 | U=1 用户态 | I/D=1 取指) rsp=线程栈 vtop,日志以 exc in tid=N, terminating 收场 —— 一次正常的线程结束被报成异常崩溃。

实测:Scene3D 渲染线程(唯一的 THREAD_CREATE,其栈顶 vtop 恰为 0x7FFFFFDE0000)在窗口被 taskbar 徽标关闭后 while root.winfo_exists() 退出并 return,日志即 serial.log 489-493 行那三行。

改为一页只含 mov eax,34(THREAD_EXIT); int 0x80; jmp $ 的出口跳板,映射在 [vtop, vtop+0x1000)(线程栈顶之上、栈向下增长永不到达的 64KB 间隙),栈顶哨兵改写成它的 VA。线程函数 return → 跳板 → THREAD_EXIT syscall → 内核干净回收本线程。free_user_thread 同步解映射该页并交还物理页,UserThreadCtx 增 tramp 字段记录。
2026-10-16 17:02:31 +08:00
TermiNexus 3ee35ad586 fix(sched): exit_current_thread 先标记 THREAD_DONE 再回收资源,堵住已退线程被再调度的窗口
原顺序是 free_user_thread 在前、state=THREAD_DONE 在后:在已释放用户栈/上下文但 state 仍为 RUNNING 的窗口里,若发生一次再调度(定时器 IRQ → try_reschedule → _yield 仅按 state 过滤),本线程会被再次选中并带着失效上下文回用户态,在已回收的栈上取指 0(现场 [0E] rip=0x0 err=0x15 cr2=0x0 rsp=0x7fffffde0000,紧跟 [syscall] exit called)。_yield 的选择逻辑(THREAD_READY/RUNNING 才可选)本身已正确,故只需把死亡标记提前。
2026-10-16 16:47:00 +08:00
TermiNexus cc96d00f1a refactor(vesafb): gfx.vp 的 Vec3/Mat4 改为值语义(去指针参数) 2026-10-16 16:40:41 +08:00
TermiNexus 06d24ae1aa chore(kernel): 恢复最小异常现场 dump;EXIT 增加不返回硬保护
isr.s:ISR_ERR 两条入口写 _fault_rip/_fault_err/_fault_rsp;idt.vp:带错误码向量打印 rip/err/rsp(#PF 追加 cr2),返回槽三连仅在出错 RSP 落在内核栈范围时读取(用户栈下方页未映射,读 [rsp-8] 会触发次生 #PF)。syscall.vp:EXIT 在 exit_current_thread 之后禁止返回(返回会一路 ret 回 app 的 _start 末尾、弹到 0 取指,现场 [0E] rip=0x0 err=0x15)。
2026-10-16 16:29:04 +08:00
TermiNexus 3a497e38ec fix(usb): 控制传输短读判失败+描述符链完整性校验;CCB 读改写屏蔽 IRQ1;清理已验证探针
uhci.control_transfer:除错误位外再比对 DATA TD 的 Actual Length(bit10:0)。短读时错误位可能全 0(QEMU/硬件不把短包当错误),只查错误位会漏 —— 现场是 USB 鼠标的 25 字节配置描述符只落下前 9 字节 ⇒ 接口解析为 0 ⇒ [usb_mse] no USB mouse found。

usb._parse_config 改为返回实际走完的偏移,enumerate_port 要求恰好走完 total_len,否则重读、仍不完整即报错返回(fail-fast,不带着半截描述符继续);同时删除两条探针。

keyboard.init/mouse.init 读-改-写 CCB 全程 pic.setMask(1):读 CCB 的 0x20 会把 CCB 值放进 0x60 输出缓冲并拉高 IRQ1,IRQ1 handler 会把它当扫描码抢走(现场 sc=0x47),使回读拿到残留值。改为只在校验失败时打印错误,去掉全部 [kbd] 探针。

清理已验证调试输出:idt 的 IRQ1 handler 探针及其计数器、desktop 的 [dbg] back_fb/row_buf/bg_row_buf、main 的 [DBG] LAPIC/idt32/swint/pic_isr 与 [DB] BootInfo 转储、timer 的 [PIT] 与自旋打印及随之无用的 import。
2026-10-16 15:26:35 +08:00
TermiNexus 82e951ee84 fix(desktop): 建窗与置脏标记加关键区,消除并发导致的共槽/同 z_order
create() 由各 App 线程经 syscall 并发调用(开机多 App 同时建窗最易命中),但 _win_count/_next_z 的读-改-写无互斥:抢占后读到同一 _win_count ⇒ 两个 App 共用同一 AppWindow 槽,标题/坐标/z_order/fb 互相覆盖;读到同一 _next_z ⇒ z_order 相同,渲染排序退化成槽位下标顺序 —— 均表现为更下方窗口的 title 突然显示到最上方。

改为:关键区①只原子分配槽位,结构体填充留在区外(避免长时间关中断);关键区②把 active/z_order/焦点一次性发布(渲染线程只在 active==1 后读该槽)。_bring_to_front 的 _next_z 自增同加关键区;_mark_rows_dirty/_mark_all_dirty/_clear_dirty_flags 同加关键区,避免与 App 线程并发时丢置脏标记(丢标记会让该行永不重绘,旧合成滞留在屏幕上)。
2026-10-16 15:20:49 +08:00
TermiNexus 1b2f6853fc fix(usb): uhci 控制传输逐 TD 校验并暴露错误位,补描述符落盘探针
- _wait_for_complete 原先只查 status TD 的 bit21|bit22,DATA 阶段的 CRC/超时(bit18)、bitstuff(bit17)、babble(bit20) 会被当成成功 ⇒ GET_DESCRIPTOR 拿到零字节缓冲区仍返回 0,最终表现为 [usb] device class=00 proto=00 ep=0、usb_kbd 找不到键盘(现场)。改为对齐 Linux usb-uhci.h:新增 TD_CTRL_ERR_MASK/NAK_MASK 与 _td_error(),control_transfer 收尾逐个校验 setup/data/status 三个 TD,失败时打印三个状态字。

- usb.vp 增加两条探针:[usb] devdesc blen/btype/maxpkt0 与 [usb] cfgdesc total/b0/t0/iface/proto/ep,用于区分数据未落盘与解析未命中。
2026-10-16 15:15:58 +08:00
TermiNexus 89907682ec fix(keyboard): init() 对齐 mouse 写法 - 去掉 0xAD/0xAE 键盘接口禁用序列(CCB 回写失败会停在禁用态),并补 pic.clearMask(1) 2026-10-16 14:14:51 +08:00
TermiNexus d37eac32fe fix(keyboard): init() 改为幂等(避免重复初始化禁用键盘接口);按键时打印扫描码/字符到串口便于验证 2026-10-16 14:12:50 +08:00
TermiNexus 0d0dba476f fix(vkernel): 键盘无法输入 - 键盘/鼠标共享 0x60 统一为单一消费者(IRQ1 ISR→buffer→desktop 轮询),IRQ1 只收键盘字节 2026-10-15 17:24:17 +08:00
TermiNexus 0ee5ec5dcc fix(vkernel/ide): PIO 命令块寄存器连写补 io_wait + 读完核 ERR - 修读盘读错扇区(根目录扇区读回 ELF 头致 app spawn 失败) 2026-10-15 17:14:12 +08:00
TermiNexus 2eea065a67 chore(debug): fat32 dir_find/win_read 加取证探针 - 请求8.3短名 vs 磁盘目录项、读盘失败扇区、win_dirty 写盘痕迹 2026-10-15 17:08:58 +08:00
TermiNexus 152c4c5683 feat(vkernel/vpsdk): 用户线程生命周期 - THREAD_EXIT 显式退出;异常终止时回收用户栈并释放槽位;返回哨兵可辨识 2026-10-15 16:50:58 +08:00
TermiNexus 29e1bc6e34 chore(debug): 移除定位用的 MMAP/get_winbuf/redraw 临时探针(线程模型修复后 PF 与卡死均已消失) 2026-10-15 16:48:19 +08:00
TermiNexus a7faeed21b fix(vkernel): THREAD_CREATE 改用户态线程 - 独立用户栈与内核栈经 drop_to_user_mode 进 Ring3;调度器按线程切 TSS.RSP0/gs[8];同步 _thread_entry 硬编码偏移并加 fail-fast 自检 2026-10-15 16:45:09 +08:00
TermiNexus 89a319e1be chore(debug): 加临时探针定位 MMAP 返回高半污染与 Tk.buf 野指针 - MMAP/get_winbuf/redraw 2026-10-15 16:20:14 +08:00
TermiNexus 9745f8832a fix(vkernel): UHCI frame list 步长越界 12KB 砸坏 buddy 堆(堆损坏根因);paging 页表记录指针缩放;清理临时探针
根因:uhci.vp 两处写成 _frame_list + uint64(i) * 4 —— uint32* 的 +n 已按元素(4字节)缩放,
再乘 4 使实际步长变成 16 字节;1024 次循环写到 16KB,而缓冲只有 UHCI_NUM_FRAMES*4 = 4KB
⇒ 越界 12KB 覆盖相邻 buddy 空闲块头(现场:[0E][PF] cr2=0xba4c000 err=0x3 rip=0x10e598
rax=0x3000=12288),把空闲链 next/prev 写坏 ⇒ 随机 [mm] BAD blk、malloc 失败、app 崩溃。
定位手段:把空闲链表头页在内核页表标只读(paging.init 已置 CR0.WP=1),任何写入立即 #PF
并给出 rip/cr2 直接指认写者(该探针定位完已删除)。

paging:save_pt_range / restore_pt_range 里的 save_buf + i*16 在 uint64* 上实际是 128 字节
步长、+8 实际是 +64 字节(IR 取证:getelementptr i64 ⇒ 按 8 字节元素缩放),改为显式字节
地址推进记录 + 元素索引(+1 = +8 字节),两处布局保持一致。

保留的加固(非探针):memory_block.magic 与 allocate_block/free_block/merge_blocks 的块头
校验、free() 的堆范围校验、slab 建页的 total 边界护栏、页表池按真实 buf_size 计页与
pt_pool_magic/pt_pool_limit 哨兵、#GP 分支补打 rax 便于定位非规范地址写。

elf.vp 新增 ELF_LOAD 开关(=1 正常装载;置 0 可整体跳过 app/.so 装载,便于隔离内核问题)。
2026-10-15 13:35:34 +08:00
TermiNexus ec57603bb3 fix(vkernel): 分配器与页表池加固(块头魔数/双链校验、池边界与哨兵),并清理定位用临时探针
mm: memory_block 加 magic(塞进原有 padding,布局仍 32B);allocate/free/merge 前校验块头(magic、size==2^order、state、prev/next 在堆内且 8 字节对齐),坏块只报不改并限流;free() 增加堆范围校验;slab 建页前校验 total 与地址范围自洽(防 uint64 下溢 + uint16 截断导致成片写页外);NO FREE BLOCK 附带 used/free/total。paging: 页表池按调用者给的真实 buf_size 计算可用页数(此前丢掉 buf_size、上限硬编码 PT_POOL_PAGES ⇒ 池会发到缓冲之外,把页表内容盖到 buddy 空闲块头);加 pt_pool_magic/pt_pool_limit 哨兵与 CORRUPT/EXHAUSTED fail-fast。idt: #GP 分支补打 rax(rax+0x10 即触发访存地址),与 #PF 分支同口径。同时移除本轮定位用临时探针(elf.vp/isr.s/main.vp 恢复原状)。
2026-10-15 12:47:39 +08:00
TermiNexus f8eed7f300 fix(vkernel): GS 归一化的说明改为 asm 外的英文注释(本编译器 asm 模板不接受注释/符号名)
上一版把说明写进 __asm__ 模板里:① 编译器会扫描 asm 模板中的符号名(注释里的 _per_cpu 也被判为需导出而报 TypeError);② 本编译器的 __asm__ 模板本身不接受注释。现把注释全部移出 asm,并改用英文;汇编指令本体不变(cli → mov rsp,ksp → 按 GS_BASE 是否为 0 决定 swapgs → 压 iretq 帧)。
2026-10-14 22:15:26 +08:00
TermiNexus 1f2faf0db8 fix(vkernel): 进用户态前归一化 GS_BASE,修线程从用户态定时器 ISR 切出后 GS 状态错乱导致的三重故障
根因:线程可能是在**别人的用户态定时器 ISR 里**被 _yield 切过来的(timer 路径不经 isrCommonhandler 的 GS 恢复,且切换后 ISR 出口的 swapgs 永不执行),此时 GS_BASE 仍 = _per_cpu。带着这个状态跑用户代码 ⇒ app 第一条 syscall 再 swapgs 就反了 ⇒ mov rsp,gs:[0x8] 读到地址 0x8 的垃圾 ⇒ rsp 变垃圾 ⇒ push 触发 #PF 时异常帧也压不进去 ⇒ 三重故障(QEMU 消失、零输出、ISR 入口探针都照不到)。现场:pid=1(Terminal) 正常、pid=2 起静默三崩。修复:drop_to_user_mode 在 iretq 前按 GS_BASE 是否为 0 决定是否 swapgs(幂等),保证用户代码一律以 GS_BASE=0 运行。
2026-10-14 22:11:19 +08:00
TermiNexus ee29ce37d1 fix(vkernel): 异常处理器 rbp 链回溯改为每跳校验,避免处理 #PF 时再触发 #PF 造成无限重入
旧实现用 mov rax,[rax] / mov rax,[rax+8] 裸链式解引用。当崩溃上下文是 ring3(或被 iret 帧影响)时,链上会取到 iret 帧里压入的 user_rsp=0x7fffffe00000 并对其解引用 ⇒ 在 #PF 处理中再次 #PF,rsp 每次下移 ~0x2A0,无限重入耗尽内核栈 ⇒ 三重故障(QEMU 窗口消失、看不到真正的首故障)。现新增 _SafeStackWord(仅 0x1000..0x80000000 内才解引用),三处分支(#PF/#GP/其他)统一改用安全回溯,并照旧跳过用户态 rbp。
2026-10-14 21:48:58 +08:00
TermiNexus c977caec91 fix(vkernel/vpsdk): 窗口缓冲改走“用户缓冲 + WIN_FLUSH 拷贝”,修 app 写内核页 #PF(err=0x7)
- 现场:四个 app 一画就用户态 #PF,cr2 全在内核堆区(~0xb7c1000/0x9f81000/0xb851000/0xb881000),err=0x7(present + 用户写),RAX 正是各自颜色常量(0x1E1E32/0xC0C0C/0xF0F19)

- 根因:GET_WINBUF 把 desktop 的**内核**窗口后备缓冲地址直接交给 app(该页 U=0),app 当用户指针写 → 必然 #PF,四个 app 全中(tid=5 那条 cr2=0xffffffff52209000 是同一指针继续走坏)

- 内核:WIN_FLUSH 增加源指针 `WIN_FLUSH(win_id, src)`:先 desktop.get_fb 取窗口缓冲、再 desktop.get_fb_bytes 取字节数(配对取,避免 resize 重建导致尺寸/指针不一致),memcpy 后 desktop.flush;syscall_handler 的 WIN_FLUSH 分支透传 arg2;desktop 新增 get_fb_bytes

- app(vpsdk/window.vp):get_winbuf 改为按窗口分配**用户态**缓冲(stdlib.malloc → MMAP,落在 0x20000000 用户堆)并缓存;flush(win_id) 改为 WIN_FLUSH(win_id, 用户缓冲)
2026-10-14 19:30:29 +08:00
TermiNexus 4289d9b190 fix(vkernel/intr): GDT/STAR 根治 —— 用户代码段移到第5项(0x28)、TSS 移 0x30,统一用户 CS=0x2B
- 背景:syscall 返回走 sysretq,硬件按 STAR[63:48]=0x1B 算 CS=+16=0x2B、SS=+8=0x23;而 GDT 原来把用户代码放在第 3 项(0x18)、第 5 项(0x28)放 TSS ⇒ app 实际是“拿 TSS 描述符当代码段”在跑(QEMU 不校验 SYSRET 目标描述符类型才没暴露;严格实现会在 sysretq 处 #GP)

- gdt.vp:GDT 扩到 8 项、limit 0x37→0x3F;新增 gdt[5]=用户代码段(0xFA/0x20,与 gdt[3] 同属性;第 3 项保留为兼容镜像);TSS 改放第 6/7 项(setTSS64(6,…) = 选择子 0x30),ltr 改用 0x30;USER_CODE_SEG→0x28、TSS_SEG→0x30

- process.vp:drop_to_user_mode 首次进用户态的 iretq 也改用 CS=0x2B(与 sysretq 一致,两条入口不再产生不同 CS)

- isr.s:用户返回校验同时接受 0x2B/0x1B(0x1B 仅作兼容镜像兜底);上一个 commit 已修“只认 0x1B 导致用户态一被中断就 cli;hlt 假死”
2026-10-14 19:07:54 +08:00
TermiNexus 79e9dfd5a4 fix(vkernel/intr): ISR 用户返回同时接受 CS=0x2B(sysretq 选择子),修“用户态一被中断就 cli;hlt 假死”
- 现象:日志尾部只有 "B 2B 23",随后整机假死(QEMU 窗口消失、没有 dump)

- 根因:isr\num\()_ret_user 硬编码校验 CS==0x1B;但 app 只要发过一次 syscall,返回走 sysretq 后 CS 就是 STAR[63:48]=0x1B 算出的 0x2B(SS=0x23)→ 之后任何用户态中断/异常(例如定时器抢占)都被判“坏 CS/SS” → 走 cli;hlt 死循环,既假死又掩盖真实异常现场

- 现在同时接受 0x1B 与 0x2B(SS 仍须 0x23);注释里记录 GDT/STAR 隐患:0x2B 当前指向 GDT 第 5 项,而那里是 TSS(QEMU 不校验 SYSRET 目标描述符类型才没暴露),根治需把用户代码段挪到第 5 项
2026-10-14 19:04:07 +08:00
TermiNexus c232c0c0d5 fix(vkernel/intr): syscall 来源标记改放内核栈(可嵌套),修 app 调完 .so 后跑飞
- 现象:app 在用户态成功调 .so(“[so] call returned”)后立刻跑飞:用户态 #BP,rip=0x7c0b9(既不在 app 映像 0x400000-0x41c824、也不在 app 栈 0xbb11000)、rsp=0

- 根因:来源标记原来放在全局 _syscall_from_kernel,但 syscall 会**嵌套**:app 的 SO_CALL1 → 内核侧执行 .so → .so 内部又发 3 次 syscall(SERIAL_PUTS,内核态发起)→ 内层把标记改成 1 → 外层(用户态)返回时误走内核返回分支:jmp rcx 回到用户 rip 却不换回用户栈 → app 用内核栈继续跑 → 栈错位跑飞

- 改为把来源标记 push 在内核栈上(入口 push 0/1,出口 cmp [rsp] 判定),每层各压各的、天然可嵌套;内核返回路径 add rsp,8 弹掉本层标记槽

- 对齐保持:and rsp,~15 之后的深度 = 补齐8 + arg6 8 + call 8 = 24 ≡ 8 (mod 16) ✓(标记槽在 and 之前,不参与)
2026-10-14 18:56:40 +08:00
TermiNexus 5916822184 fix(vkernel/intr): syscall 入口按 rsp bit46 判来源并记标记,支持内核态发起(修三重故障)
- 现场:内核自动启动里直接调 .so 的 log_info(其内部走 `syscall`)→ 入口用 rsp bit47 判来源时,本系统内核栈是**低位**(boot 0x7d8d0 / 线程 ks≈0xbb6b000)被误判成用户态 → 多执行一次 swapgs → gs:[8] 读到垃圾 → rsp 变垃圾 → 三重故障(QEMU 窗口直接消失、无任何 dump)

- 入口改用 bit46(64TB)阈值:用户栈由 drop_to_user_mode 固定在 0x7fff_ffff_...(≈2^47)、内核栈 <1GB;并把来源记进全局 _syscall_from_kernel(不用 gs:[…]:引导早期可能还没 swapgs 过)

- 出口改为读该标记决定 sysretq(用户)或 jmp rcx(内核)。原 rcx>>47 判据两个错:覆盖 rax 返回值;且用户 rip(0x40xxxx) 与内核 rip(0x10xxxx) 都 <2^47,无法区分
2026-10-14 18:47:32 +08:00
TermiNexus 14e69f62f5 fix(vkernel): 不再 unmount 根文件系统,app 运行期才能读盘
- app 是 spawn 之后异步运行的,运行期仍要 fat32.open(load_so / 资产文件);原来在自动启动块后面立刻 fat32.unmount(),导致 app 第一次 LOAD_SO 就 "[so] failed to open file"

- 该 unmount 原本只为防“破坏性 FAT32 测试”改坏 disk.img,而那些测试已删除,留着只剩切断 app 文件访问的副作用

- 卸载能力保留给非根挂载点(U 盘/多分区),将来配挂载点表 + 引用计数
2026-10-14 18:42:24 +08:00
TermiNexus 6b2b24ccdf fix(vkernel/elf): .so 符号“已定义”判据改用 st_shndx,修 log_info 恒 not found
- 根因:get_so_symbol 与两处重定位回退都以 `st_value != 0` 当“已定义”,但导出函数在自身段内偏移可以为 0(实测 slog.so:log_info 的 st_value=0x0、st_shndx=.text)→ 该符号被整条跳过 → 恒报 “[so] symbol not found”,内核自动启动里的 log_info 冒烟调用因此失败

- 改为只看 `st_shndx != SHN_UNDEF(0)`;同样修 R_X86_64_* 与 JMPREL/PLT 两处重定位回退(否则段内偏移 0 的内部符号不会被重定位)
2026-10-14 18:41:45 +08:00
TermiNexus 09b72614d9 fix(vkernel/intr): isr80(int 0x80) 修调用号/arg4 被覆盖 + 收尾原地 iretq;syscall stub 补 SysV 栈对齐
- isr80:prologue 原拿 rax/r10 当临时(取 CS 与 RFLAGS),之后 push rax/push r10 存进保存块的其实是 CS/RFLAGS → handler 收到乱掉的调用号与 arg4。现在入口先 push rax + push r10 保住二者,CS 判定改用内存操作数 `test byte ptr [rsp+24],3`,翻译段按新偏移取 arg4([rsp+168])

- isr80 收尾:原实现把帧字段 pop 进 r8/r9/r10/rax 再重新 push iret 帧,其中 `pop rax(RSP)` 会冲掉 handler 返回值,且 is_user 判断早被 `pop rcx`(arg2) 覆盖(等于没用)。改为跳过统一帧/保存槽回到 CPU 压下的原始帧直接 iretq:环切换与 RSP/SS 由 iretq 依 CS.RPL 自行处理(内核弹 3、用户弹 5),rax=返回值,其余寄存器全部原样恢复

- syscall stub:`and rsp,~15` 后直接 push arg6 使 syscall_handler 入口 rsp≡0;Viper 代码生成依赖标准 SysV(入口 rsp≡8,内核 printf 的 movaps %xmm0,0x60(%rsp) 即证据)→ 补 `sub rsp,8` 于 arg6 之前
2026-10-14 18:38:17 +08:00
TermiNexus 4f9660d23b fix(vkernel/intr): 修正 syscall_entry 两处 rax 覆盖(系统调用号/返回值),用户 syscall 不再恒返回 0
- 入口:原 `mov rax,rsp; shr rax,47`(判入口来源)把 rax 中的系统调用号覆盖成 rsp>>47(用户态=0);随后 `mov rdi,rax` 使 syscall_handler 收到 num=0 → 落入未知调用 `return 0`。改为 `bt rsp,47; jc`(只读 CF,与旧判据等价,不占用 rax)

- 返回:原 `mov rax,rcx; shr rax,47`(判返回去向)把 handler 的返回值覆盖成返回地址>>47(用户地址 < 2^47 → 0)→ app 的 syscall trampoline 永远取到 0。改为 `bt rcx,47; jc`

- 现场:elf 自启动后 app 的 malloc→syscall 29(MMAP) 拿到 0 → 紧接着 `movq %r14,(%rax)` 写 NULL → 用户态 [0E][PF] cr2=0x0 err=0x7(hello rip=0x400153、terminal rip=0x40007c)
2026-10-14 18:31:24 +08:00
TermiNexus 077a150c92 feat(vkernel): 还原 elf 自启动(spawn 四个 app + slog.so 冒烟调用),改写为新语法
- 初版 6813947 的 main.py 在 FAT32 挂载后、破坏性测试前 spawn hello/terminal/gargantua/scene3d 并加载 slog.so 调 log_info;此前为排查内核概率性崩溃被 [DIAG]“app autostart disabled”禁用,现按要求还原

- 新语法改写:c.Addr(x) -> &x;t.CArray[t.CChar,N] -> [N]char;t.CInt -> int;t.CVoid|t.CPtr -> rawptr;c.Asm/c.AsmInp -> __asm__/asminp(调用符号用 intr/syscall.vp SO_CALL1 同款:rdi 传首参 + call 符号地址);字符串常量 -> const str

- 打印统一 [64]char + viperlib.snprintf + serial.puts(&buf),与同文件既有写法一致
2026-10-14 18:25:45 +08:00
TermiNexus 0de7cfad60 refactor(vkernel/paging): _split_1gb_to_pd 形参改为 uint64 物理地址
- 与本文件 MapPage/UnMapPage/_set_page_writable 同口径(都收 uint64 地址),不引入“结构体指针作形参”这种本文件没用过的写法,降低编译器特性风险

- 函数内自行重建 _p*;pdp_pa==0 直接返回 0
2026-10-14 18:19:03 +08:00
TermiNexus c067ca76e3 fix(vkernel/paging): save/restore_pt_range 处理 1GB 大页,避免把大页内存当表项存回/写回
- save_pt_range:PDP 层遇 PTE_PS 整片跳过(前进 1GB)—— 当 PD 用会把大页内存内容当表项存进保存缓冲

- restore_pt_range:PDP 层遇 PTE_PS 跳过 —— 该情形正常无记录(save 已跳过),若出现说明缓冲陈旧/错配,绝不把 garbage 基址当 PT 写回(restore 是写路径,会写坏任意内存)
2026-10-14 18:18:27 +08:00
TermiNexus 1222d6caef fix(vkernel/paging): clone_for_process 处理 1GB 大页(先拆 PD 再逐层拷贝)
- 源端 PDPTE.PS=1 时没有下层 PD:原实现会把大页内存内容当 PD 拷进新页表,再用该垃圾 PD 替换新地址空间的该条目(该区间映射被写坏,后续写入会落到任意物理内存)

- 现在先 _split_1gb_to_pd(kernel_pdp, pdp_idx) 拆成常规 PD(语义不变,512 个 2MB 条目仍映射同一物理 1GB),再沿用既有逐层拷贝;PD 层命中 2MB 时按既有 `if PTE_PS: return` 早退
2026-10-14 18:17:06 +08:00
TermiNexus 07f4dbbb78 feat(vkernel/paging): 1GB 大页拆分(1GB->PD),MapPage/MapLargePage/UnMapPage 不再把大页当 PD 写坏内存
- 新增 _split_1gb_to_pd:PDPTE.PS=1 时新建 PD 并把 1GB 拆成 512 个 2MB 条目(继承 PCD/PWT/USER/NX;新 PDPTE 是**非叶条目**,只带 PRESENT|WRITABLE|USER,绝不带 bit63(NX),否则 & ~0xFFF 派生出的表指针非规范 = 刚修的 #GP 根因);条目不存在/已是 PD 时直接返回 1,建 PD 仍由调用方既有逻辑负责

- MapPage:取到 PDPT 后先拆分,再走既有 2MB/4KB 逻辑(否则 pd.entries 会写进大页内存,读到随机“表项”)

- MapLargePage:同样先拆分,再写入 2MB 条目

- UnMapPage:先拆分再撤除命中的条目(此前会把大页内存内容当表项 → 往派生出的随机地址写 0,即“fb/pitch/w/h 被清零”那类隐性写坏)
2026-10-14 18:14:47 +08:00
TermiNexus 9119c74232 fix(vkernel/paging): 补 1GB 大页(PTE_PS)判据,消除偶发 #GP(0) 与任意内存写坏
- 现场:rip=0x11a93e 在 _set_page_writable 的 testb $1,(%rax,%rcx,8);rax 由 PDPTE & ~0xFFF 得到。固件 identity 映射的 1GB 大页条目会置 NX(bit63),& ~0xFFF 把它一起带出 → 派生“PD 基址”非规范 → 读表项即 #GP(0)(偶发取决于该 VA 是否落进 1GB 大页)

- _set_page_writable:PDP 层补 PTE_PS 分支(1GB 大页无下层 PD,W 位在条目自身),与既有 2MB 分支同口径

- MakePTWritable / MakeRangeRO:PDP 层遇 PTE_PS 跳过 —— 把 1GB 大页当 PD 解引用会把大页内存内容当表项(约一半 qword bit0=1),据此递归/写页表 → 任意内存被改(偶发数据损坏)并算出非规范指针
2026-10-14 18:12:15 +08:00
TermiNexus 30f47ca4f4 fix(vkernel/desktop): 时钟改为按时间每秒刷新 RTC,不再绑到渲染速率
现象:任务栏时间区域不会自己走,必须鼠标移过去才刷新。

根因:_draw_taskbar 里 RTC 缓存按“渲染次数”节流重读 ——
    if _render_count % 30 == 0: rtc.rtc_read_time()
而渲染频率完全取决于活动量:空闲约 1 帧/秒 → 要 30 秒才重读一次 RTC;鼠标一移动渲染
变快,_render_count 迅速累加,很快命中 %30==0 → 时钟立刻跟上。这正是“必须鼠标移动
才刷新”的成因。

修改:
- desktop_thread 的“每秒”分支(cur_sec != _last_clock_sec)里先 rtc.rtc_read_time()
  再标脏任务栏行 —— 时钟严格按秒刷新,与渲染速率解耦。
- _draw_taskbar 移除 _render_count % 30 的 RTC 重读,只取缓存值绘制。
2026-10-14 15:46:29 +08:00
TermiNexus cc061b2ad3 fix(vkernel): 修复 PS/2 鼠标位移符号误判(|位移|≥128 时方向翻转、光标向左飞没)
根因(静态定位,非速度问题):PS/2 协议里第 2/3 字节是位移“幅值”,符号在第 1 字节的
bit4(X)/bit5(Y)。而驱动把位移按 int8 存储/解释,于是 |位移| ≥ 128 时字节最高位被当成
符号位:
- 向右快移(幅值 0x80~0xFF)被读成负数 → _cursor_x 一路减小 → 撞上 if _cursor_x<0
  被夹成 0 → 表现为“光标往左飞没了”;
- int8 装不下 9 位有符号位移(符号位在 flags 里),结构上必然丢符号。

修改:
- drivers/input/mouse/mouse.vp:mouse_packet.dx/dy 与 byte2/byte3 改为 uint8,保存原始
  幅值字节,消除 int8 截断;flags(含符号位)照旧保留。
- services/desktop.vp::_update_mouse:按协议用“幅值 + 符号位”重建带符号位移
  (命中 flags.bit4/bit5 时幅值 -256),再做屏幕 Y 轴反向。
- USB HID 分支不动:USB 报告的 X/Y 本身即 int8 有符号,原解释正确。
2026-10-14 15:12:19 +08:00
TermiNexus a44f55574c fix(vkernel): 鼠标“飞”与刷屏性能 —— 帧缓冲改回写映射 + 鼠标驱动丢弃溢出包/细化分辨率
方案 A(帧缓冲映射):
- 去掉帧缓冲的 PTE_PCD,改为回写映射。原先非缓存映射下每次写屏直通设备,大块刷新极慢;
  QEMU 下回写显示正确(TCG 不模拟缓存不一致),真机严格正确应改 write-combining(PAT),
  已在代码注释里写明。
- 说明:“只刷脏行”本已是现状(_mark_rows_dirty 按区域标脏,全屏刷仅启动那一次)。

方案 C(鼠标驱动加固,drivers/input/mouse/mouse.vp):
- irq_handler 丢弃带 X/Y overflow 位(bit6/bit7)的包:此时设备给的是饱和值,直接采用
  会产生 ±127 级跳变,表现为光标“飞”。
- init 中在 0xF6(Set Defaults) 之后用 0xE8/0x03 把分辨率设为 8 counts/mm,避免默认最粗的
  4 counts/mm 造成每计数位移偏大。
2026-10-14 15:05:02 +08:00
TermiNexus 73a499b0c1 feat(vkernel): 桌面线程每秒校验 .bss 帧参数不变量(fail-fast 抓 .bss 侧写坏)
与 main.vp 的帧参数影子校验互补:那边抓的是 __init__ 栈局部量在启动阶段被写坏,
这边抓的是 .bss 全局在被写坏后、使用前被拦下。

- _SetFrameInvariants():在 start() 完成全部 .bss 赋值后记录影子
  (_fb/_screen_w/_screen_h/_fb_pitch/_back_fb/_row_buf/_bg_row_buf/_dirty_rows)
- _CheckFrameInvariants():桌面线程每秒比对一次,不一致即打印 [FATAL]
  (左右并列显示 实际值/期望值)并停机

这样两类写坏都能在使用前变成可定位现场,而不是继续静默破坏(如把后备缓冲刷到物理 0 起)。
2026-10-14 14:57:59 +08:00
TermiNexus c0c83cdc83 feat(vkernel): __init__ 帧参数加影子副本校验(fail-fast 抓偶发写坏源)
fb/width/height/pitch 局部量曾被偶发清零(→ desktop.start 拿到 fb=0 → 刷新把后备缓冲
写到物理 0 起、覆盖内核自身而卡死/崩宿主 QEMU)。USB/UHCI 路径已排查:TD 块走堆分配、
QH 在 free 前置 terminate、枚举缓冲均为堆分配且长度有钳制,未见明显越界零写。

故加常驻轻量校验(非临时探针,可长期保留):
- _SetFrameShadow:BootInfo 读取后保存影子
- _CheckFrameLocals:在 after_usb / after_vga / pre_desktop 三处比对,不一致即打印
  [FATAL](含期望值与实际值)并停机 —— 把“偶发写坏”变成可定位的现场,
  并明确是哪一个阶段之间被写坏。
2026-10-14 14:56:26 +08:00
TermiNexus 9cf33ab374 fix(vkernel): _render 脏行行界由“夹取兜底”改为 fail-fast(越界即报错停机)
按“禁止兜底、改 fail-fast”的要求:_dirty_y_min/_dirty_y_max 是 .bss 全局,
被写坏时(曾观测 fb/pitch/w/h 被随机清零)原夹取会静默掩盖上游写坏源。
现改为越界立即打印 FATAL 并停机,直接暴露写坏。
合法的空脏行集合状态(_dirty_count==0、_dirty_y_max==-1、_dirty_y_min==h)已放行。
2026-10-14 14:55:40 +08:00
TermiNexus b6cc64a5c9 chore(vkernel): 移除本轮排查加的全部临时诊断探针,并把 fb 为空改为 fail-fast
现象已定位清楚:fb=0 时刷新会把后备缓冲写到物理 0 起,覆盖 IVT/BDA + 低端内存 +
内核镜像自身(边跑边改坏自己正在执行的代码 → 卡死;宿主 QEMU abort 0x40000015)。
本轮实测 fb/pitch 正确时桌面渲染正常、[perf] 数值单调(.bss/中断那条线的修复生效)。

清理内容:
- main.vp:删除 _WhProbe 探针函数与 7 处调用、2 处 mm.DumpBuddy() 调用
- desktop.vp:删除 [rc] 阶段面包屑 7 处、[rc] preflush 转储、[dt] 每秒心跳、[dst] 入口探针
- mm.vp:删除已无调用点的 DumpBuddy()(保留 3 条 malloc 失败诊断——它们是错误信息,非调试噪音)

新增(fail-fast,替代静默兜底):
- desktop.start:fb_ptr == 0 时打印 FATAL 并停机,杜绝“刷新把内核自己写掉”这类灾难
- _render:脏行行界夹取保留(越界会静默写穿帧缓冲),但这是防破坏下限,非修复
2026-10-14 14:52:56 +08:00
TermiNexus d58833d3b5 diag(vkernel): 增加 post_vga / pre_start 探针,夹出 fb/pitch 被清零的位置
[rc] preflush 显示 fb=0x0(帧缓冲指针为 NULL),[dst] entry 显示 pitch=0:
刷新循环把 8MB 后备缓冲写到物理 0x0 起 —— 覆盖 IVT/BDA + 低端内存 + 内核镜像自身
(0x100000 起),边跑边覆盖正在执行的代码 → 卡死;QEMU 因客户机跑飞
abort(0x40000015 = STATUS_FATAL_APP_EXIT)。

VGA 文本(315-317 行)用同一组 fb/pitch 画出来是正常的,说明清零发生在
317 行之后、323 行 desktop.start 之前。故加两条探针把元凶夹死:
  _WhProbe("post_vga",  uint32(fb), pitch)   # 317 行后
  _WhProbe("pre_start", uint32(fb), pitch)   # 323 行前
若 post_vga 正常而 pre_start 为 0,元凶即这三条之一:
serial.puts("你好") / serial.puts("[main] starting desktop") / _WhProbe("pre_desktop")。
2026-10-14 14:47:53 +08:00
TermiNexus 0cee9612ae diag(vkernel): 刷新循环前打印全部参与量(定向内存转储),定位跑飞来源
已给刷新/背景循环夹取行界,但现象不变(仍卡在 “[rc] vblank done” 之后、
没有 “[rc] flush done”)——说明跑飞的不是循环边界(_dirty_y_max),
而是循环体内的量:row_bytes/run_bytes 或指针,或 _dirty_rows 本身。

故在 _wait_vblank() 之后、刷新循环之前,一次性打印所有参与量:
  dc dmin dmax w h pitch row_bytes
  fb back dirty_rows row_buf bg_row_buf
任一异常即为被写坏的对象,且直接给出被写坏后的数值(量级)。
(这就是你说的“内存转储”的定向版:只打这块状态,编译快、迭代方便。)
2026-10-14 14:26:26 +08:00
TermiNexus c9c507c67d fix(vkernel): _render 的刷新/背景循环按屏幕高度夹取行界,避免脏行值被写坏时死循环越界写
面包屑定位:卡在 “[rc] vblank done” 与 “[rc] flush done” 之间 —— 即帧缓冲刷新循环。

_dirty_y_max 是 .bss 全局,一旦被写坏成很大的正数:
- 循环退化成死循环 → 卡死(故 [dt] 心跳一行都不打印);
- run_start*pitch 越写越远,写出 0x80000000 帧缓冲之外 → 落进 MMIO →
  宿主 QEMU 被写崩(unknown software exception 0x40000015 = STATUS_FATAL_APP_EXIT),
  且屏幕只留下顶部已写的那一小块。

两个循环(背景铺底 + 帧缓冲刷新)的行界现夹到 [0, h-1]。这不是根因修复
(.bss 仍被写坏),但:
- 止血:不再死循环、不再把宿主 QEMU 写崩;
- 兼作探针:[dt] 心跳里的 dymin/dymax 若明显越界,即可确认 .bss 被写坏的量级。
2026-10-14 14:18:21 +08:00
TermiNexus cfbb0c1286 diag(vkernel): _render() 各阶段埋 [rc] 面包屑,定位卡死步骤
日志证据:本轮 [dt] 心跳一行都没打印 → 桌面线程没走到“每秒”分支,即卡在第一轮循环里;
而屏幕顶部出现一小块蓝色 → 很可能执行到第一次 _render() 中途卡住(写了一半)。
宿主 QEMU 随后 abort(0x40000015 = STATUS_FATAL_APP_EXIT)。

在 _render() 埋 6 个面包屑:
  [rc] entry / bg done / taskbar done / windows done / cursor done / vblank done / flush done
最后打印的那条 = 卡住的那一步。若连 [rc] entry 都没有,则卡点在其之前
(_update_mouse / _update_keyboard / _heartbeat_check),再补那边。
定位后连同所有 [DIAG] 一并移除。
2026-10-14 14:13:10 +08:00