Commit Graph
130 Commits
Author SHA1 Message Date
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 11ad0de988 fix(vpsdk): WINBUF_MAX_BYTES 改单字面量 4194304,避免最小宽度算术回绕成 0(三重故障诱因)
- `1024 * 1024 * 4` 按最小宽度(i16)做算术:1024×1024 先回绕成 0 ⇒ 缓冲只剩几字节 ⇒ flush 时内核 memcpy(dst, src, 240KB) 从 app 的几字节缓冲后狂读 ⇒ 内核越界读 → 异常路径再出事即三重故障

- 改成单字面量 4194304(不依赖任何算术)。与 V3_POOL 同属“最小宽度算术”坑:静态求值侧已在 61f23d9 按结果值加宽,这处是表达式侧,先按单字面量规避
2026-10-14 19:35:37 +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 f42d8a5248 revert(HelloWorld): 回调恢复为 &_on_submit(编译器已支持对函数取址)
上一提交为绕过 CastError 曾改成直接传函数名;现编译器已修(ViperC 70c271d:取址分支
补 Ptr(Ptr(Func)) 分支),`&func` 会正确给出 Ptr(Func)(一级),故恢复原始写法。
2026-10-14 16:38:18 +08:00
TermiNexus 5da1e3f62a fix(HelloWorld): 回调改传函数名本身,去掉多余的 &(函数名已是地址)
vpui.Button(..., &_on_submit) 把函数地址又取了一层 → fn**(二级),与形参
callback: t.CPtr(一级 i8*)级数不符;新收紧的隐式转换规则下报 CastError:
  implicit pointer-to-pointer conversion is forbidden: pointer level differs
  (source level 2 -> target level 1);
  source: level 2, pointee !llvm.ptr -> target: level 1, pointee i8
改为直接传 _on_submit,与代码库既有写法一致(如 idt.irqInstallHandler(12, irq_handler, "mouse"))。
2026-10-14 16:31:24 +08:00
TermiNexus b5f51415ca fix(vpsdk): so_call1 的 arg1 改为 rawptr(裸地址用通配指针,避免级数不符误报)
dynlib.so_call1 的 arg1 只是原样交给系统调用的裸地址;原先声明为 t.CPtr(具体类型的
一级指针),调用方传字符串字面量(二级指针)会命中 CastError:
  implicit pointer-to-pointer conversion is forbidden: pointer level differs
  (source level 2 -> target level 1)
改用 rawptr(未定/通配指针,符合既有规则“任一侧 void*/未定(rawptr)允许”)。
2026-10-14 15:57:38 +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 e6aabf5cf2 feat(build): build.py 支持 --onlyapp(只编译库与应用,跳过内核)
只改应用/库时可省去内核编译时间:--onlyapp 时跳过 BuildKernel,其余(BuildApps /
BuildBootloader / BuildDisk / RunQemu)照常。与 --onlyrun 不冲突(--onlyrun 优先级更高,
完全跳过构建)。依旧可与 --noclean / --noapp / --kernel-only 组合。
2026-10-14 15:34:40 +08:00
TermiNexus d8ca5e256e chore(disk): 更新重编后的 bootx64.efi(含 .bss 预留修复,与 boot.c 保持一致) 2026-10-14 15:22:16 +08:00
TermiNexus 8b0d24e0d5 feat(build, kernel): 内核链接直接产出带符号 kernel.elf,再由 objcopy 二次产出 kernel.bin
按方案调整:
- VKernel/project.vpj:linker 去掉 --oformat binary / --strip-all,output 改为 kernel.elf。
- build.py:用 objcopy -O binary --set-section-flags .bss=alloc,load,contents 由 kernel.elf
  二次产出 kernel.bin,再拷到 disk/。同源产出 ⇒ 两者布局逐字节一致,日志里的 rip 可直接用
  objdump / nm / readelf 落到函数名;原来那段依赖 phase2cmd.bat 的改写字样已整体删除。
- --set-section-flags .bss=...contents 把 .bss 一并写进裸镜像(ELF 里 .bss 默认 NOBITS),
  于是 bootloader 按“文件大小”预留即可覆盖 .bss(boot.c 里另有 256KB 余量,双保险)。
- linker.ld:移除此前对 lld 无效的 LONG(0) 写法,.bss 恢复为干净定义。

注意:需完整重编(不能只 --onlyrun)才会生成新的 kernel.elf / kernel.bin。
2026-10-14 15:22:05 +08:00
TermiNexus 1ebb744418 fix(boot, build): bootloader 预留补上内核 .bss(多留 256KB);build 跳过路径过期的 kernel.elf
崩溃症状:paging.init 清 CR0.WP 的窗口里遍历页表时 #GP(cr0=0x80000033 → WP=0),
随内核体积变化时有时无。

根因分析(已实测确认):kernel.bin 由 ld.lld --oformat binary 产出,链接脚本里那条
“强制 .bss 为 PROGBITS”的写法对 lld 无效 —— 实测 kernel.bin 只有 249088 字节(0x3CD00),
而 .bss 结尾在 ~0x14B900,即约 60KB 的 .bss 尾部并未写进文件。bootloader 只按文件大小
预留物理页(61 页 → 0x100000..0x13D000),于是 .bss 尾部落在未预留的 EfiConventionalMemory
中:bootloader 自身的分配(页表、缓冲等)可能落进去,内核一写 .bss 就把它们破坏 ——
表现就是 paging.init 遍历到被破坏的页表项而 #GP,且随体积变化“时有时无”。

修改:
- boot/boot.c:PagesNeeded 在文件大小之外显式多预留 64 页(256KB),覆盖 .bss 并留余量。
- build.py:生成 kernel.elf 前逐个校验 .bat 里的 .obj 是否存在,缺失即跳过并给出提示
  (--rebuild-includes 后 includes.binary_mlir 下目标文件名会变,.bat 路径会过期)。

注意:本修复需要同时重编 bootloader(python build.py --bootloader)才生效。
2026-10-14 15:17:13 +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 59ba295532 feat(build): build.py 支持 --onlyrun(跳过编译/打包,复用现有 disk.img 反复跑 QEMU)
为反复复现偶发写坏提供快速通道:脚本拆成 BuildKernel / BuildApps / BuildBootloader /
BuildDisk 与 RunQemu 五段,--onlyrun 时只执行 RunQemu(清空 serial.log → 启动 QEMU →
打印串口日志),不再重编内核/应用/引导器、也不再重新打包 disk.img。行为与原来完全一致,
只是把“构建段”与“运行段”分开,便于跳过前者。用法可直接循环:python build.py --onlyrun
2026-10-14 15:01:30 +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
TermiNexus 164228e15a diag(vkernel): 桌面线程每秒心跳,打印脏行/渲染/窗口/光标状态
现象:日志停在 “[desktop] thread started”,QEMU 卡死;屏幕只有上方一小块变蓝、
UEFI 文本未被覆盖 —— 指向渲染/刷新阶段的 _dirty_* 状态不正确(而非整屏写入卡住)。

在 desktop_thread 主循环的“每秒”分支打印:
  [dt] sec=… dc=… dymin=… dymax=… rc=… win=… cx=… cy=…
作用:
  - 若完全没有 [dt] 输出 → 线程在首轮之前就卡住(_mark_all_dirty 或循环顶部);
  - 若有输出且 sec 递增 → 线程在跑,问题在渲染/刷新(dc/dymax 数值即可解释
    “只刷顶部一小块”)。
定位后连同其它 [DIAG] 一并移除。
2026-10-14 14:08:56 +08:00
TermiNexus 3bb6a9a102 diag(vkernel): desktop.start 入口加形参探针(w/h/pitch 是否原样到达)
已排除/确认:
- main 的 __init__ 局部量正确(5 个 [wh] 探针全对,pre_desktop 仍为 1360/768);
- 调用点机器码正确(0x100A86: mov 0x10(%rsp),%esi = 1360);
- mm 模块 34 个函数全部合规(无“用到 callee-saved 却未 push”的函数)。

故在 desktop.start 入口打印收到的 w/h/pitch:
- 若此处已为 0 → 实参在调用/ABI 传递环节丢失;
- 若此处正常 → 丢失发生在函数内部(malloc/memset 之后,w 存于 ebx)。
据此继续定点,定位后连同所有 [DIAG] 一并移除。
2026-10-14 13:58:02 +08:00
TermiNexus 38ccc9cdf6 diag(vkernel): main __init__ 插 width/height 探针,定位局部槽被清零的阶段
“[desktop] dirty_rows/back_fb malloc FAILED”的实质是 width/height 被随机清零:
malloc(0) 直接返回 NULL;伙伴系统经 DumpBuddy 实测健康(free≈160MB,order 3/5..12/15 有块)。
ISR 不保存 rax/rcx/r8-r11 的问题已修(0c63969),但现象仍在 → 主线程栈另有写坏源。

在 __init__ 的 5 个阶段打印 w/h(decl → pre_pg → post_mm → pre_usb → pre_desktop),
二分出被写坏的区间;定位到具体调用后,连探针一起移除。
2026-10-14 13:50:48 +08:00
TermiNexus 21f4a6aeca fix(build): kernel.elf 链接命令改写做稳健化,避免误覆盖 kernel.bin
原改写用字面 replace("--oformat binary"/"kernel.bin"),若 .bat 里的写法是
"--oformat=binary" 或输出名大小写/路径不同就会匹配不中——那种情况下执行的
仍是原始命令,会把 kernel.bin 覆盖成非预期产物。现改为正则同时兜住
"--oformat binary"/"--oformat=binary"、"-o <path>"/"--output=<path>",
并加保护:改写后若不包含 kernel.elf 则直接跳过,绝不执行未改写的命令。
2026-10-14 13:47:36 +08:00
TermiNexus 66eb70b131 feat(vkernel): #GP 分支补 rbp 链回溯(与 #PF 分支同款)
#GP 此前只打印 rip/err/rsp/cr4/cr0,看不到调用链;#PF 与“其他异常”分支早已有
[STK] rbp 回溯。现给 #GP 补上同样的 5 级回溯(rbp + 4 个返回地址),
内核态 rbp 低地址直接回溯,用户态(>=0x80000000)跳过以免读用户栈嵌套 #PF。

用于定位当前 “paging.init 的 MakePTWritable 页表遍历读到坏表项 → #GP
rip=0x11a7be” 的调用来源。
2026-10-14 13:46:57 +08:00
TermiNexus 449f2dbc37 fix(mm): 加分配失败分步诊断;build.py 改用 --phase2command 产出 kernel.elf
- mm.vp: 在 malloc(slab 路径)、_create_page、allocate_block 三处失败分支各加一条
  [mm] 诊断打印,用于定位“[desktop] dirty_rows malloc FAILED”
  (768B → ≤SLAB_THRESHOLD → slab size class 5(1024B) → _create_page 需 order≥1 伙伴块;
   DumpBuddy 显示伙伴空闲链健康,故需进一步区分是哪一步返回 NULL)
- build.py: kernel.elf 改为调用 viperc --phase2command 拿到“精确链接命令”再改写产出。
  自行 glob .obj 去链会因对象顺序不同得到与 kernel.bin 不一致的段布局
  (实测前 247KB 里 230KB 不同),无法把日志 rip 映射回函数。
2026-10-14 13:36:01 +08:00
TermiNexus cb5eb679c1 chore(boot): 重编 bootx64.efi(含 0x60000~0x80000 栈区占位) 2026-10-14 13:28:09 +08:00
TermiNexus 54e88ce957 feat(build): 额外链接带符号 kernel.elf;mm 增加 DumpBuddy 诊断
- build.py: 用同一批 .obj + 同一 linker.ld 再链一份
  VKernel/__vpcbuild__/output/kernel.elf(仅去掉 --oformat binary/--strip-all),
  段布局与 kernel.bin 一致 → 可把日志里的 rip 用 objdump/nm 映射回函数
- mm.vp: 新增 export DumpBuddy(),打印 arena start/end 与 total/used/free
  及各阶空闲链表首块(上限 8 个),定位分配失败是“链表空”还是“块指针坏”
- main.vp: mm.init 之后、desktop.start 之前各调一次 DumpBuddy(),
  用于排查“[desktop] dirty_rows malloc FAILED”
  (768B → ≤SLAB_THRESHOLD → slab size class 5(1024B) → _create_page 需 order≥1 伙伴块)
2026-10-14 13:27:40 +08:00
TermiNexus 091efd11fc chore(build): build.py 增加 --bootloader(make -C boot / GNU-EFI)
bootx64.efi 不由 viperc 产出,长期停留在旧件——改了 boot.c 却仍在用旧引导器
(本次即踩到:boot.c 的栈区占位因 bootloader 未重编而未生效)。
- 传 --bootloader 时执行 make -C boot(MSYS make → WSL Ubuntu → objcopy 直接
  产出 disk/EFI/BOOT/bootx64.efi)
- 未传时若 boot/boot.c 比 disk/EFI/BOOT/bootx64.efi 新,打印过期提醒
2026-10-14 13:11:47 +08:00
TermiNexus 0c6396912b fix(vkernel): ISR 保存/恢复 rax/rcx/r8-r11,修中断破坏被中断代码寄存器
根因:isr.s 的 ISR_NOERR/ISR_ERR 宏把 rax/rcx/r8-r11 当临时寄存器用
(读 RIP/CS/RFLAGS、算 is_user),却从未保存/恢复原值,rcx 更是根本不在
保存列表里。于是任意一次中断(定时器 1000Hz)只要落在“这些寄存器里存有
活跃值”的瞬间,就会破坏被中断代码的活跃值:
  - r8 被写成被中断处的 RIP;rcx 被清 0(xor ecx,ecx / setnz cl 造 is_user)

实测吻合:DumpMapping 把 tag 放 rcx、va 放 r8,中断落点恰在其中 → 打印
“ [PG] (None) va=0x11ab82 ”,其中 0x11ab82 正是 DumpMapping 内部的 RIP;
同一机制把 main.vp 的 width 槽写成 0 → “ [desktop] back_fb malloc FAILED ”。
最初日志里的 [0D][GP] rip=0x130d45(vsnprintf 读 %d 变参)亦由此而来。
旧 TPC 编译的寄存器分配没踩到该窗口故“正常”,新版编译器踩到 → 概率性破坏
(对应 VKERNELERR.md 记录的“新版概率性崩溃”)。

- isr.s: 新增 ISR_SAVE_CLOBBERED / ISR_RESTORE_CLOBBERED 与 _isr_scratch;
  入口第一件事保存 rax/rcx/r8-r11,各返回路径在 push 完 iretq 帧之后恢复
  (iretq 从栈取帧,恢复寄存器不影响返回)
- mm.vp: arena 起点抬高后做 4KB 对齐(KernelEnd 未必页对齐),避免伙伴系统块错位
2026-10-14 13:06:26 +08:00
TermiNexus 0919a058a8 fix(vkernel): 预留内核 .bss 与栈区,修堆 arena 覆盖未预留内核内存
根因:bootloader 只按 kernel 文件大小 AllocatePages(.bss 不占文件),于是 .bss
尾部(ticks/_perf_* 等全局量)与主线程栈区(0x60000~0x80000)在 memmap 里仍是
EfiConventionalMemory,被内核 mm 的堆 arena 当作空闲内存占用 → 内核全局量被写坏、
堆元数据被反向破坏,最终桌面线程栈上的 va_list 被改坏,vsnprintf 读 %d 变参时
#GP(rip=0x130d45, err=0x0)。

- linker.ld: .bss 末尾加 LONG(0) 强制 PROGBITS,使 kernel.bin 的 FileSize 涵盖
  .bss,bootloader 的 PagesNeeded 得以预留整块内核镜像(含 .bss)
- mm.init: 新增 KernelAddr/KernelEnd,挑 arena 时把与内核镜像 [kernel_PhysAddr,
  __bss_end) 重叠的 region 起点抬到镜像末端(不整段丢弃)
- main.vp: 取 __bss_end 并传入 mm.init
- boot.c: ExitBootServices 前用 AllocatePages(AllocateAddress, EfiLoaderData)
  占位内核栈区 0x60000~0x80000,使其在 memmap 中标记已用

另含在途整理:多处补显式收窄转换(uint8()/uint16() 等)、kbd_svc 构造改为
kbd_svc.__init__() 等。
2026-10-14 12:54:43 +08:00
TermiNexus 52728bc8da refactor(gdt): setTSS64 改用 gdt_entry64 整体写入 TSS 描述符
以 gdt_entry64*(&gdt[num]) 复用同一地址写入 16 字节,替代原先手写拆分
gdt[num]/gdt[num+1] 的方式;语义与字节布局均不变。
2026-10-13 15:45:59 +08:00