新增: ath9k PCIe WiFi驱动(被动扫描/beacon解析), shell命令行(help/clear/echo/version/date/reboot/halt), Consolas 18px TTF字体, render_ascii.py 修改: dkm_net_api扩展scan接口, FirstInit无WiFi设备时跳过, DSK加载shell.elf, 非首次启动直接进shell
48 KiB
48 KiB
经验记录
本文件记录项目开发和架构设计过程中的经验与教训。
2026-06-29 — 记录 UTSM 架构与算法设计
经验
- 架构分层: 将 UTSM 拆成 UTSM、UTRW、DMP、PCKC,并和 SAS-R0-PCQ、DRR 明确边界,能避免调度、加密、恢复逻辑互相污染。
- 热路径约束: 将段定位设计为
segment_slot + generation + epoch的 O(1) 路径,能保留 Per-CPU O(1) 调度器的性能目标。 - 文档同步: 同时维护模块 README、算法记录和架构记录,方便后续实现时按模块拆分代码。
教训
- 避免破坏连续内存: UUID 不应嵌入数据区形成孔洞,否则会破坏 cache line、DMA、checkpoint 和批量复制路径。
- 恢复根要独立: 看门狗如果只是普通内核线程,无法在调度器损坏或关中断卡死时可靠恢复,必须作为 DRR 独立设计。
- 不要在调度路径做重活: 调度切换中不能扫描 capability、计算 MAC 或重加密,否则会破坏 O(1) 调度目标。
2026-06-29 — 优化 UTSM 恢复与 checkpoint 设计
经验
- 先补恢复底座: DRR Emergency Pool、A/B checkpoint metadata、Dirty Shard 是 UTSM 进入实现前最关键的三项稳定性优化。
- 分片降低抖动: Dirty Shard 让 checkpoint 从扫描大 bitmap 变为处理 dirty shard,符合“不扫描全内存”的约束。
- 双槽保证稳定点: A/B metadata 通过先写 inactive slot、CRC 校验、最后原子切换 active slot,能避免半写 checkpoint 破坏旧稳定点。
教训
- 恢复路径不能依赖普通堆: 普通 allocator OOM 时,如果 DRR 也依赖它,系统会失去最后恢复能力。
- checkpoint 提交顺序必须固定: metadata、dirty index、MAC root、CRC、flush、active slot 切换的顺序不能随意调整。
- Dirty 计数要避免共享争用: Dirty Shard 的 dirty_count 应结合 per-CPU dirty list,避免多核写入时形成 cache line 热点。
2026-06-29 — 添加 SYSTEM 驱动 manifest
经验
- 根目录语义明确: SYSTEM 打包成 IMG 后就是系统根目录,启动路径和驱动路径应统一以 SYSTEM 为根来描述。
- JSON 清单更适合扩展: manifest.json 能直接表达 stage、required、depends、provides、driver class 等加载信息。
- 分阶段加载清晰: platform、boot、filesystem、optional 四阶段能区分启动关键驱动和普通可选驱动。
教训
- 不要只依赖目录扫描: 启动驱动顺序需要 manifest 控制,否则依赖和失败策略不稳定。
- required 标志必须明确: 启动关键驱动失败应进入 DRR/panic,可选驱动失败只标记 FAILED。
- stage0/stage1 需要 boot module 兜底: 文件系统未就绪前,早期驱动不能完全依赖 SYSTEM/driver 的普通遍历。
2026-06-29 — 设计 DKM 驱动模块 ABI
经验
- ABI 固定入口: 每个驱动统一导出
driver_desc、driver_init、driver_exit,能让 DSM loader 简化加载和错误处理。 - kernel_api 降低耦合: 驱动通过
dkm_kernel_api调用 log、mem、UTSM、IRQ、PCI、DMA、VFS、NET、DRR 等服务,避免直接绑定任意内核符号。 - 早期模块双路径: stage0/stage1 优先从 boot module table 加载,VFS 可用后再从 SYSTEM 路径加载,能覆盖文件系统未启动阶段。
教训
- ELF 支持范围要收窄: 第一版只支持必要 x86_64 relocation,禁止 TLS、IFUNC、lazy binding 和外部动态库,避免 loader 复杂度失控。
- 状态机要显式: DISCOVERED 到 ACTIVE 以及各类 FAILED 状态必须明确,DRR 才能做恢复和归因。
- manifest 和 driver_desc 都要校验: manifest 描述启动策略,driver_desc 描述模块自声明,二者不一致时应拒绝或降级加载。
2026-06-29 — 编写 CODE/DKM/README.md
经验
- 模块 README 要可实现: DKM README 不只描述概念,还应列出 loader、parser、relocation、state table 等第一版实现清单。
- 运行路径要闭环: 从
SYSTEM/driver/manifest.json到driver_init再到 DRR recovery 的链路需要在模块文档中完整串起来。 - 文档分层有利于实现:
RE/驱动模块ABI设计.md作为完整规范,CODE/DKM/README.md作为模块落地说明,职责清晰。
教训
- 不要把 DKM 写成泛泛说明: 驱动模块系统涉及启动顺序、ELF、ABI、UTSM、DRR,README 必须明确边界和第一版范围。
- 早期加载限制要写清: stage0/stage1 不能依赖 VFS 和普通 heap,否则启动链会循环依赖。
- 实现清单要保留: 后续写代码时应按 manifest parser、boot module lookup、ELF loader、relocation、driver state table 的顺序推进。
2026-06-29 — 接入 DKM 内置驱动最小链路
经验
- 先内置再外置: 在实现外部 ELF
.drvloader 前,先用内置console_early驱动验证driver_desc -> driver_init -> ACTIVE,能降低 DKM 首次接入风险。 - kernel_api 是驱动边界: 即使内置驱动也通过
dkm_kernel_api调用 log 服务,能保持未来外部驱动 ABI 一致。 - 验证点要清晰: 串口输出
[DKM] builtin driver active和[DKM:console_early] serial console online,说明 ABI 校验、handle 创建和 init 调用路径已跑通。
教训
- 不要直接跳到外部 ELF loader: manifest parser、boot module、relocation、符号解析同时实现会放大调试范围,应先固定 ABI 和状态机。
- 外部
.drv仍未执行: 当前SYSTEM/driver下的.drv只是镜像内容,必须实现 DSM/ELF loader 后才会被加载。 - 内置驱动也应走同一状态机: 避免未来外部加载器和内置路径行为不一致。
2026-06-29 — 接入 Limine boot module 与 DKM ELF 符号扫描
经验
- 先识别再加载: 外部
.drv应先通过 boot module 枚举和 ELF64 header 校验,再解析 section table / symtab / strtab,最后定位driver_desc/driver_init/driver_exit。 - Limine request 需要 linker 保留: module request 必须放入
.limine_requests*区段,并在 linker script 中显式保留 start/request/end,否则 bootloader 不会返回 response。 - .drv 是 ET_REL 可重定位文件: 当前 stub 驱动编译为
-r可重定位目标文件,附带了.symtab/.strtab和完整符号表,内核可以直接遍历 symtab 找到目标符号的 section 和 offset。 - streq 足够: 内核内联
streq("driver_desc")已足够定位所需符号,不需要 hash 或完整的字符串匹配库。
教训
- module 配置要在 limine.conf 中显式声明: 仅把
.drv放进SYSTEM/driver不会被 bootloader 自动预加载。 - 有符号表 ≠ 可调用: 符号找到了,但 ET_REL 没有 load address / vaddr,relocation 未完成前不能直接跳转。
- boot module 适合 stage0/stage1: 文件系统未就绪前,早期驱动应优先通过 boot module 进入内核。
2026-06-29 — 实现 ET_REL 外部驱动装载与 relocation
经验
- section offset + image base 才能得到正确地址: ET_REL relocation 中符号值为 section 内偏移,重定位写入的目标地址必须是
image_base + section_offset + symbol_value + addend,只算 offset 会导致低地址指针。 - NX 是早期加载器的隐性障碍: arena 位于
.bss,默认只有 PF_R|PF_W,调用driver_init需要可执行内存。SAS-R0 下通过 linker script PHDRS 给 data 段加 PF_X 是最简方案。 - Et_REL 只需 R_X86_64_64 即可起步: 当前 stub driver 只有三种字符串指针重定位,全部是 R_X86_64_64,loader 复杂度极低。
教训
- 不能只看 ELF dump 输出验证重定位: 必须用 hex 指针输出确认运行时地址落在正确区间,否则空白输出难以区分是空字符串还是 NULL 指针。
- arena 分配的内存在真实硬件上可能不可执行: QEMU 模拟器不严格检查 NX 时不会暴露问题,但 Limine 的 page table 会强制 NX,未来应在 loader 中分配单独的可执行区域。
- PHDRS 合并 .data/.bss 前要确认 startup 代码没有依赖 .bss 零初始化边界: 当前 boot.S 没有依赖,合并安全。
2026-06-29 — 实现 manifest.json 解析与分阶段驱动加载
经验
- 最小 JSON parser 足以处理 manifest: 只需要 tokenizer + 字符串读取 + 递归对象/数组跳过,无需完整 JSON 库。
- Limine 路径有前导
/:boot():/driver/...返回的路径以/开头,manifest 中的路径driver/...不含前导/,匹配时需要 strip。 - 按 stage 分阶段加载天然隔离故障: platform → boot → filesystem → optional,每 stage 内部 required 驱动失败只终止当前 stage,不阻断后续 stage 的 optional 驱动。
教训
- JSON value skipper 必须处理所有类型: 对象
{}、数组[]、字符串、数字、布尔,缺一不可,否则 parser 在遇到未知 value 类型时 break。 - PowerShell Set-Content 会写 BOM: 对 C 源文件应避免使用,否则 BOM 字符
U+FEFF在文件开头导致 clang 报错extraneous closing brace。 - arena 不够 14 个驱动: 当前 4MB arena 在加载全部 14 个驱动后已接近容量上限,后续需要实现真正的物理页分配器。
2026-06-29 — 实现第一个真实 DKM 驱动:PIT timer
经验
- 真实驱动需要 BSS: 驱动中声明
static const struct dkm_log_api *g_log会产生.bsssection(SHT_NOBITS + SHF_ALLOC),loader 必须为其分配零填充空间。 - R_X86_64_PC32 和 R_X86_64_32S 是代码重定位主流: inline asm 的
outb/inb使用立即数端口,编译器生成 PC32 对.bss的引用和 32S 对.rodata.str1.1的引用。 - PIT 校准可在无 IRQ 下工作: 通过 latch command 读取 PIT 当前计数值,在 mode 0 terminal count 后计数值回绕,可做精确的 busy-wait 延迟。
教训
- driver_desc 不能 static: ELF 符号
driver_desc必须是 global(visibility("default")),否则 symtab 中符号为 LOCAL,loader 找不到。 - 真实驱动的 .text 有
R_X86_64_PC32重定位: stub 驱动的driver_init只是xor eax,eax; ret没有重定位,但真实的outb/inb内联汇编会产生 PC32 对 g_log 全局变量的引用。
2026-06-29 — 修复 UTSM 早期启动崩溃
经验
- 先验证崩溃阶段: 串口只到
[UTSM] boot时,实际还未进入 DKM 驱动加载,不能把早期 runtime 崩溃误判成缺真实驱动。 - 早期日志不能无限等待硬件:
serial_putc()对 COM1 ready 位无限 busy-wait 会让内核卡死,early serial 必须有超时或降级策略。 - 未初始化 FPU/SSE 前禁止编译器生成 SIMD 指令: Clang 即使在 freestanding 下也可能生成
xorps/movups,早期内核未启用 SSE 会触发异常,应使用-mno-sse -mno-sse2 -mno-mmx -msoft-float。
教训
- 进入 C 前要固定 CPU 状态:
_start应先cli和cld,否则未建 IDT 前被中断或方向标志异常会导致三重故障。 - 驱动不能直接下载复用: Linux/Windows 已编译驱动不符合 DKM ABI,真实驱动必须按 Deshab 的
driver_desc/driver_init/kernel_api原生实现。 - stub
.drv只是镜像样本: 在 DSM manifest parser、ELF loader、relocation 和dkm_kernel_api实现前,SYSTEM/driver下的.drv不会被内核加载执行。
2026-06-29 — 编写 UTSM 内核代码首阶段骨架
经验
- 先闭环再扩展: 先实现 Limine 入口、serial 日志、UTSM init、segment 创建、UTRW read/write 和 selftest,可以快速把设计变成可验证路径。
- 保持 64B line 接口: 即使第一版使用占位 XOR stream,也保留 cache line 级加解密接口,后续替换真实加密不会影响 UTRW 主路径。
- 构建脚本要先检查工具链: build.ps1 明确检查 clang、ld.lld、make,能快速定位环境缺失,而不是让 make 报模糊错误。
教训
- Windows make 会受环境影响: MinGW make 会把
CC默认为cc,Makefile 中应强制指定CC := clang,不能只用?=。 - 当前环境缺少 LLVM 工具链: 本机 PATH 中没有 clang 和 ld.lld,当前无法生成
SYSTEM/boot/utsm.elf,需要先安装 LLVM/Clang 或提供交叉工具链。 - 第一版不要引入完整系统复杂度: DRR、DKM、真实 checkpoint、buddy/slab 和多 CPU 调度都应在 selftest 闭环后逐步接入。
2026-06-29 — 自动发现 LLVM 并成功构建 UTSM
经验
- 工具链可自动发现: LLVM 安装在
C:\Program Files\LLVM\bin但不在 PATH 时,build.ps1 可以扫描常见目录并把绝对路径传给 Makefile。 - 高半区内核要使用 kernel code model: 链接地址位于
0xffffffff80000000时,CFLAGS/ASFLAGS 需要-mcmodel=kernel,否则会出现R_X86_64_32 out of range。 - 清理旧 object 很关键: 修改编译参数后必须先 clean,否则旧 object 仍按旧 code model 生成,链接错误会继续存在。
教训
- 不要只检查 PATH: Windows 上 LLVM 可能已安装但没写入 PATH,构建脚本应扫描常见安装目录。
- PowerShell 路径含空格要传绝对参数:
C:\Program Files\LLVM\bin\clang.exe这类路径应通过 make 变量传递,避免 shell 展开错误。 - MinGW make 的中文临时目录警告可忽略但需关注: 当前
TMP/TEMP Illegal byte sequence未阻断构建,但后续若引发临时文件问题,应在 build.ps1 中设置 ASCII 临时目录。
2026-06-29 — 打包 SYSTEM 为 IMG 并添加 QEMU 启动脚本
经验
- 不依赖外部镜像工具也能先闭环: build.ps1 直接生成 FAT16 raw IMG,可以在没有 mtools/oscdimg/xorriso 的环境下把 SYSTEM 打包出来。
- 构建脚本应负责完整产物链: 现在
build.bat -> build.ps1 -> utsm.elf -> ISO/deshab.img形成了连续流程。 - QEMU 启动脚本应自动找程序:
ISO/run_qemu.bat会从 PATH 和常见安装目录找qemu-system-x86_64.exe,使用-serial stdio方便看内核串口日志。
教训
- IMG 命名不等于 ISO 文件系统: 当前产物是 FAT16 raw 磁盘镜像
deshab.img,目录名叫 ISO 只是输出目录,后续若需要真正 ISO9660 应单独实现。 - Limine 是否能从该 IMG 启动仍需 QEMU 验证: 镜像已包含 EFI/BOOT/BOOTX64.EFI、limine.conf 和 utsm.elf,但最终启动链需要实际运行 QEMU 检查。
- 临时构建文件需要过滤: 打包 SYSTEM 时应排除
*.tmp*,避免旧的utsm.elf.tmp...被写入镜像。
2026-06-29 — 修正 UEFI 镜像为 GPT + FAT32 ESP
经验
- UEFI 磁盘镜像应有 GPT: 仅裸 FAT 分区不等于可启动 UEFI 磁盘,镜像需要保护 MBR、GPT 主/备表和 ESP 分区。
- ESP 应使用 FAT32: QEMU/UEFI 固件对 ESP 的预期是 FAT32 文件系统,
EFI/BOOT/BOOTX64.EFI应位于该分区内。 - 可直接验证关键签名: 构建后检查 MBR
AA55、保护分区类型EE、GPT 签名EFI PART、FAT32 标记,能快速确认镜像结构正确。
教训
- 裸文件系统镜像不够: 之前的 FAT16 raw 镜像缺少分区表,UEFI 固件不能按磁盘启动语义识别。
- PowerShell 数字字面量要注意有符号转换:
0xffffffff在某些上下文会变成-1,应使用4294967295或显式 UInt64。 - run_qemu 必须显式使用 UEFI 固件: 仅
qemu-system-x86_64 -drive ...不保证走 UEFI,需要加载edk2-x86_64-code.fd或OVMF_CODE.fd。
2026-06-29 — 实现 6 个真实 DKM 驱动(timer/acpi/pci/console_fb/bootfs/vfs)
经验
- kernel_api 渐进暴露平台能力: rsdp_address → fb_address → boot_modules_response,按需添加字段避免了 ABI 大范围修改。
- 驱动独立编译必须保持 struct 布局一致: 驱动本地定义的
dkm_kernel_api必须包含所有字段(含 padding),否则偏移量错位导致读取错误。 - Limine framebuffer 让早期图形可行: 通过 Limine framebuffer request 获取线性帧缓冲,在无 GPU 驱动下即可实现 8x8 字体渲染。
教训
- PCI 扫描不需要 IRQ: 纯配置空间枚举完全独立于中断系统,pci 驱动可以第一个实现。
- bootfs/vfs 是软件驱动,不依赖硬件: 利用 Limine boot module 可直接构造内存文件系统,无需块设备。
- console_fb 的 bpp/pitch 必须从 bootloader 读取: 帧缓冲格式随 QEMU/固件变化,硬编码会导致花屏。
2026-06-30 — 记录 devfs/fat32/IDT/IRQ/ps2kbd/e1000 阶段成果
经验
- 先做内核 IRQ 底座再写中断驱动: IDT 0–47 stub、PIC remap、统一
idt_handler和irq_register先闭环后,ps2kbd与e1000都能通过同一 IRQ 注册路径接入。 - HHDM 是早期 MMIO 的最短路径: 通过 Limine HHDM request 暴露
kernel_api.hhdm_offset,e1000 可直接把 PCI BAR0 物理地址映射到高半区虚拟地址,避免在页表/MMIO allocator 未完善前阻塞网卡初始化。 - 软件文件系统可先用 boot module 验证: devfs 不依赖块设备,fat32 可用
driver/test.fat32合成镜像验证 BPB/FAT/目录项/文件读取,为后续 AHCI/NVMe 块设备接入保留接口经验。 - 当前真实驱动进入 10/14 阶段: 已完成 timer、acpi、pci、console_fb、bootfs、vfs、devfs、fat32、ps2kbd、e1000,剩余真实驱动集中在 APIC、块设备和 virtio_net。
教训
- 汇编文件命名会影响 object 冲突:
idt.S和idt.c同名会同时输出idt.o,应将汇编入口拆成idt_stubs.S,避免覆盖或重复符号。 - 中断 stub 宏要显式区分 error code: x86_64 异常中部分 vector 自动压入 error code,stub 生成不正确会破坏统一栈帧,导致
iretq返回失败或异常连锁。 - 驱动本地 ABI struct 必须同步:
irq_register、hhdm_offset等字段加入后,所有真实驱动本地dkm_kernel_api定义都必须保持相同偏移,否则会读取到错误函数指针或平台地址。 - QEMU 过滤日志可能隐藏有效输出: FAT32 README 内容、VFS dump、e1000 reset warning 等行如果没有统一前缀,使用 Select-String 时可能被漏看;驱动日志应带稳定模块前缀。
- e1000 reset timeout 暂不等于驱动失败: 当前已能完成 PCI 探测、MAC 读取、IRQ 注册和 link setup,reset timeout 需要后续结合硬件状态轮询/RX-TX 初始化继续收敛。
2026-06-30 — 实现 APIC discovery 驱动
经验
- APIC 应先 discovery 后接管: 当前内核 IRQ ack 仍走 PIC EOI,stage0
apic先做 CPUID、IA32_APIC_BASE、MADT、LAPIC MMIO 只读探测,并继续保留 PIC 路由,能避免破坏现有ps2kbd/e1000IRQ 路径。 - MADT 可不依赖 ACPI 驱动:
apic在 manifest 中早于acpi,因此直接使用kernel_api.rsdp_address解析 RSDP/XSDT/MADT,枚举 LAPIC、IOAPIC 和 interrupt source override。 - HHDM 适合只读 LAPIC 验证: 通过
kernel_api.hhdm_offset + lapic_phys可读取 LAPIC ID/version/SVR,验证 MMIO direct map 可用于平台控制器探测。 - QEMU 验证链路明确: 日志显示
MADT found、IOAPIC addr=0xfec00000、LAPIC version reg=0x50014、discovery complete; PIC routing retained,随后pci继续加载并UTSM SELFTEST PASS。
教训
- 不要提前切换 IOAPIC: 未扩展 IDT/vector allocator/APIC EOI 前,不能禁用 PIC、重编 IOAPIC RTE 或启用 LAPIC timer,否则中断投递和 EOI 后端会不一致。
- 驱动也要用 kernel code model: APIC 驱动代码较大时,Clang 会生成更多字符串地址 relocation;使用普通 code model 会让部分高半区字符串日志异常,需用
-mcmodel=kernel编译真实.drv。 - APIC required 驱动不能误失败:
apic是 stage0 required 且为pci提供irq依赖,除非api/log/irq_register缺失,否则 discovery 不完整也应返回 0 并保持 PIC fallback。
2026-06-30 — 实现 AHCI discovery 驱动
经验
- 块设备先枚举再 DMA: AHCI 第一版只做 PCI class 探测、BAR5/ABAR HHDM 映射、HBA CAP/GHC/PI/VS 和 port 寄存器枚举,不发命令,能在无 DMA allocator/页固定接口前安全验证控制器路径。
- AHCI 可作为 block provider 先接入:
ahci是 stage1 optional,依赖pci/irq并提供block,即使还未提供真实读写,也能让 manifest/VFS/fat32 依赖链进入下一阶段测试。 - QEMU ICH9 AHCI 验证稳定: QEMU 下探测到
8086:2922,ABAR 位于0x81084000,PI 为0x3f,6 个 implemented ports 可读,port 2 显示 ATAPI signature0xeb140101。
教训
- PCI class dword 位移容易错: 读取 offset
0x08时,prog_if 在 bits 15:8,subclass 在 bits 23:16,class_code 在 bits 31:24;按 byte offset 直觉解析会导致 AHCI 控制器找不到。 - 不要提前清端口/发命令: 未建立 command list/FIS/PRDT DMA 内存、cache flush 和中断完成路径前,不能贸然 stop/start port 或写 PxCI。
- HHDM MMIO 只读仍要保守: 写 PCI command 只启用 MEM/BUSM,AHCI HBA 寄存器阶段保持只读,避免破坏固件/仿真器当前端口状态。
2026-06-30 — 实现 NVMe discovery 驱动
经验
- NVMe 也应先 discovery: 第一版只做 PCI class
01/08/02探测、BAR0/BAR1 解析、IRQ line 读取,不创建 admin queue、不写 CC、不提交命令,能安全接入 stage1 storage 链路。 - 测试设备需显式加入 QEMU: 默认启动脚本只挂 virtio 磁盘,不会出现 NVMe 控制器;临时加入
-device nvme后可探测到 QEMU NVMe1b36:0010。 - 高位 BAR 要主动保护: QEMU 将 NVMe BAR0 放到
0x000000c000004000,当前 HHDM/页表路径不能安全访问该 4G 以上 PCI MMIO,因此驱动记录 BAR 后跳过 register read,系统仍能 SELFTEST PASS。
教训
- 不能假设所有 BAR 都在 4G 以下: e1000/AHCI 的低位 BAR 可直接 HHDM 读取,但 NVMe 常见为 64-bit BAR,需要后续实现高位 PCI MMIO 映射或页表映射接口。
- MMIO 读前必须检查映射能力: 直接读未映射高位 BAR 会导致异常或 QEMU 退出,真实驱动必须在访问寄存器前验证地址范围和映射策略。
- NVMe admin queue 不能提前做: 未准备 contiguous DMA、doorbell stride、completion polling/IRQ 和 cache coherency 前,不应写 CC.EN 或 ASQ/ACQ。
2026-06-30 — 实现 virtio_net discovery 驱动并完成 14/14 真实驱动
经验
- virtio-net 可先做 capability discovery: 第一版只识别 PCI vendor/device、BAR、IRQ line 和 modern virtio PCI capabilities,不做 feature negotiation、不建 virtqueue,能安全完成网络驱动入口闭环。
- 兼容 transitional/modern 识别: QEMU
virtio-net-pci以1af4:1001形式出现,按 virtio transitional ID 计算 virtio_id=1 即可识别 net device,同时仍能枚举 modern capability 链。 - 14 个外部
.drv全部真实化: manifest 中 timer、apic、acpi、pci、console_fb、ahci、nvme、bootfs、vfs、fat32、devfs、e1000、virtio_net、ps2kbd 均已替换为可执行 DKM 驱动。
教训
- 测试设备要显式挂载: 默认 QEMU 脚本没有 virtio-net,需要临时添加
-netdev user和-device virtio-net-pci才能验证驱动路径。 - virtio capability 链只应只读枚举: 未建立 DMA ring 和设备状态机前,不应写 common_cfg/device_status 或 queue_select/queue_enable。
- 多个网卡测试会改变 PCI 拓扑: 加入 virtio-net 后 e1000 可能不存在或位置变化,驱动必须把“设备未找到”视为 optional 正常路径。
2026-06-30 — 规划 14/14 驱动完成后的下一阶段路线
经验
- 先补底座再做数据路径: 14 个驱动已完成 discovery/加载闭环,下一步最优先不是继续堆功能,而是补物理页分配、页表/MMIO 映射、DMA buffer 和屏障约定。
- 块设备应先于文件系统深化: AHCI/NVMe 的 IDENTIFY/READ 能把 FAT32 从
test.fat32迁移到真实 block provider,是后续 VFS 能力扩展的关键路径。 - IRQ 后端需要独立升级: 现有 PIC fallback 已稳定,APIC/IOAPIC/MSI 应作为单独阶段推进,避免在设备数据路径中同时改变中断后端。
教训
- 不要在 discovery 驱动里提前发命令: AHCI/NVMe/virtio/e1000 都需要 DMA、队列、IRQ 完成路径;缺少底座时发命令会把问题混在一起。
- 高位 PCI MMIO 是必须解决的问题: NVMe BAR0 已暴露 4G 以上映射缺口,后续不能继续依赖简单 HHDM 假设覆盖所有设备 MMIO。
- 文档状态要及时修正: CLAUDE.md 曾残留旧的“待实现 nvme/virtio_net”尾部,完成阶段性目标后必须同步路线,避免后续开发误判当前状态。
2026-06-30 — 编写 DSK 主内核 README
经验
- 明确两阶段内核边界:
utsm.elf是 bootloader 加载的首阶段内核,deshab.elf是 DSK 主内核;文档中必须明确 UTSM 后续要继续执行到 DSK。 - 先定义 boot context: UTSM 跳转 DSK 前应通过版本化
dsk_boot_context传递 HHDM、RSDP、framebuffer、boot modules、DKM API/driver table、UTSM/DRR 状态等信息。 - 先做最小 DSK 闭环: DSK 第一版只需接收 context、打印 boot banner、执行 selftest,即可验证
CODE/dsk -> SYSTEM/system/deshab64/deshab.elf的链路。
教训
- 不要让 DSK 重复早期启动职责: Limine request、early serial、DKM 首轮加载和 discovery 应保留在 UTSM 阶段,DSK 接管长期运行期。
- ELF loader 第一版要收窄范围: DSK loader 应先只支持 ELF64、PT_LOAD、入口跳转,不引入动态链接、TLS、demand paging 或符号解析。
- 构建路径要固定: 主内核产物固定为
SYSTEM/system/deshab64/deshab.elf,后续 build.ps1 和 limine/bootfs 逻辑都应围绕该路径实现。
2026-06-30 — 实现 UTSM 到 DSK 最小跳转闭环
经验
- 先用 FAT32 路径预加载闭环:
deshab.elf已放入SYSTEM/system/deshab64/,Limine 从 FAT32 ESP 预加载,UTSM 按/system/deshab64/deshab.elf定位模块并加载,符合后续替换为真实 FAT32/block provider 的路径语义。 - 最小 ELF loader 足够验证架构: 第一版只支持 ELF64、ET_DYN/ET_EXEC、PT_LOAD、一次性复制/清零和入口跳转,已经能从 UTSM 进入 DSK 并传递 context。
- DSK 不返回符合主内核语义: DSK 校验
dsk_boot_context后输出 HHDM/RSDP/framebuffer/boot modules/DKM API 等信息,SELFTEST PASS后 halt,完成主内核接管模型验证。
教训
- 构建脚本要显式集成 DSK: build.ps1 必须同时构建
CODE/UTSM和CODE/dsk,并检查SYSTEM/system/deshab64/deshab.elf是否存在,否则镜像中不会包含主内核。 - PowerShell/MinGW make 路径状态要谨慎:
Push-Location/make -C在当前环境中出现路径解析问题,最终采用显式$DskDir与 make 工作目录方式稳定构建。 - 当前还不是真实 FAT32 自读取: 现在 UTSM 通过 Limine module 拿到
deshab.elf,下一步要把来源替换为 AHCI/NVMe block provider + FAT32 读取。
2026-06-30 — 建立 DMA API 并验证 AHCI IDENTIFY/READ
经验
- DMA 底座先从低位页开始: 通过 Limine memmap 选取 4G 以下 usable 区间,配合 HHDM 形成
phys/virt/size三元组,足以支撑 AHCI CLB/FIS/command table/data buffer。 - kernel_api.dma 是块设备公共入口: 将
dkm_dma_api.alloc_pages暴露给 DKM 后,AHCI 不再依赖私有内存假设,后续 NVMe/e1000/virtio-net 也可以复用。 - AHCI 最小数据路径已跑通: 挂临时 SATA 测试盘时,AHCI 能完成 SATA disk IDENTIFY,并执行 READ DMA EXT 读取 LBA0;默认无 SATA 盘时会跳过命令并继续进入 DSK。
教训
- AHCI command header 布局必须严格按规范: 初版把
prdtl/prdbc字段布局写错,导致 HBA 看到错误 PRDT 信息,IDENTIFY 一直超时。 - 不要对 ATAPI/空端口发 ATA IDENTIFY: 默认 QEMU 场景只有 ATAPI/空端口时应跳过命令,否则会产生误导性 timeout。
- 测试盘场景要和默认启动分开: 默认
run_qemu.bat保持稳定启动;SATA 测试盘用于验证 AHCI 数据路径,后续可整理成独立自动化场景。
2026-06-30 — 接入 block provider 并让 FAT32 优先读真实块设备
经验
- block provider registry 解耦驱动和文件系统: UTSM 提供
kernel_api.block.register_device/read后,AHCI 只负责注册ahci0和实现 LBA read,FAT32 不需要知道 AHCI 细节。 - 保留 boot module 回退很关键: 默认 QEMU 没有 SATA disk 时,FAT32 会继续读取
driver/test.fat32,保证启动链和 DSK 跳转不受真实块设备实验影响。 - SATA FAT32 镜像验证了真实路径: 把
test.fat32扩展成 SATA 测试盘后,AHCI 注册 block provider,FAT32 通过 block provider 读取 BPB/root/README.TXT,并继续进入 DSK。
教训
- ABI 字段只能追加不能插入:
dkm_kernel_api.block必须追加到结构尾部,不能插在dma后面,否则会改变已有驱动本地 ABI 字段偏移。 - block read 需要支持多扇区分块: AHCI 当前 command table 只有一个 PRDT entry,公共 read 回调内部按最多 8 sector 分块,避免一次请求过大。
- FAT32 应先校验 BPB 再切换来源: block provider 读取成功但 BPB 无效时必须回退 boot module,否则普通测试盘会让 FAT32 误判失败。
2026-06-30 — DSK 加载:优先 FAT32 block provider,回退 Limine module
经验
- 两阶段加载优先级清晰: DSK loader 先尝试
kernel_api.block+ 内嵌 FAT32 解析,读取根目录DESHAB ELF文件;无块设备或文件不存在时自动回退 Limine boot module,保证默认场景稳定。 - FAT32 data_start 必须乘以 fat_count:
data_start_sec = rsvd + fat_count * spf,漏掉第二份 FAT 会导致 root directory cluster 错位。 - 静态 BSS 缓冲区比 arena 分配更可靠: DSK loader 的 64KB BPB/FAT 缓冲区、4KB cluster buffer、64KB file data buffer 使用 BSS 静态数组,绕过了 UTSM arena 4MB 限制,并已扩大 arena 到 16MB。
教训
- arena 容量需提前评估: 原 4MB arena 在加载 14 个 DKM 驱动后剩余空间不足,导致
kmem_alloc_aligned(64KB)失败,已调整为 16MB。 - AHCI 单扇区 block read 在重入时可能不稳定: batch read 128 扇区总是成功,但单扇区连续读偶尔超时;DSK loader 最终全部改用 cluster 级批量读取解决。
- 调试时 hex dump 比 ASCII 日志可靠: FAT32 目录项包含空格,ASCII 日志容易遗漏;用
log_hex64输出原始字节准确定位了data_start_sec偏移错误。
2026-06-30 — DSK 首次启动链路 + FirstInit 用户设置向导 + mouseInit
经验
- PIE ELF 必须处理 R_X86_64_RELATIVE: DSK 用
-pie编译 FirstInit/mouseInit,加载后所有全局指针/数组引用需要重定位。在dsk_load_elf中解析PT_DYNAMIC→DT_RELA/DT_RELASZ→ 应用R_X86_64_RELATIVE(*slot = load_bias + addend),否则解引用全局数据会 page fault。 - PIE 指针数组需改用栈上字符数组: DSK 自身的
static const char *fi_txt_path[]含R_X86_64_RELATIVE重定位项,但 UTSM loader 不处理.rela.dyn。改用栈上char[]逐字节赋值避免重定位依赖。 __attribute__((unused))在 -O2 下可能导致函数被优化掉:fb_char/fb_text标记为 unused 后,编译器可能内联消除或改变调用约定,导致按键时崩溃。去掉 unused 属性即可修复。- FAT32 文件缓冲区必须足够大: FirstInit.elf 从 26KB 增长到 88KB 后超过 DSK 的
g_fdata[65536],导致加载失败。扩大到 256KB 解决。 - PS/2 控制器写命令在 QEMU 早期阶段可能阻塞: mouseInit 的
ps2_write_cmd写0x64端口后wait_input_clear死循环。安全 stub 版本只读状态寄存器,不写控制器。 - 预渲染中文位图是最简字体方案: 用 Python/Pillow 从 simhei.ttf 渲染指定中文字符串为 8bpp 灰度 C 数组,嵌入 ELF,避免运行时 TTF 解析。
教训
- ELF 文件大小监控: 新增功能后 ELF 体积可能超过静态缓冲区。应在
dsk_load_elf中校验found_size <= sizeof(g_fdata)并打印日志。 - 圆角矩形绘制需完整边界检查:
draw_card的圆角填充如果缺少sq <= 0检查,isqrt_int会收到负数转无符号的大值,导致越界写入。 - 帧缓冲 blend 不应读取 framebuffer:
blend(line[c], fg, a)读取帧缓冲可能触发 page fault 或性能问题。改为blend(bg, fg, a)纯写入。 - block API 偏移量必须精确:
dkm_kernel_api中block字段在0xA8(168),之前误用0x60(96 =rsdp_address),导致g_block_read永远为 NULL。 - AHCI 单扇区读不稳定: 批量读 256 扇区成功,但回退到单扇区读会挂死。DSK 的
fat32_read_root_file应尽量在 256 扇区缓冲内完成所有操作。
当前已完成
启动链路:
Limine → utsm.elf → DKM 14 驱动加载 → DSK (deshab.elf)
→ 旋转加载动画 (双缓冲, 顺时针, comet-tail)
→ FAT32 读取 firstInit.txt
→ firstInit=0:
DSK → mouseInit.elf (安全 stub) → 返回 DSK
DSK → FirstInit.elf
→ 环淡出 → "欢迎使用 Deshab" → 设置提示
→ 账户设置卡片 (圆角矩形, 白色输入框, 18px 中文)
→ 键盘输入: 计算机名/用户名/密码
→ SHA256 密码 + XOR 加密配置缓冲
→ "设置完成" 提示
→ firstInit≠0: 正常启动 (待实现)
字体系统:
CODE/font/mkfont.py → simhei_16/24/32.dbf (DBF 格式)
CODE/font/render_embedded.py → text_bitmaps.c (预渲染位图)
SYSTEM/system/font/simhei.ttf (原始 TTF)
初始化程序架构:
DSK (deshab.elf) 负责调度所有系统初始化程序
mouseInit.elf — DSK 直接加载调用, 返回 DSK
FirstInit.elf — DSK 加载跳转, 只做用户设置向导
下一步计划
- 修复 FirstInit 按键崩溃: 定位
fb_text在-O2下的优化问题(已移除 unused 属性,待验证) - 实现鼠标光标移动: 从 PS/2 IRQ12 读取鼠标包,更新光标位置
- user.conf 写盘: 需要
dkm_block_api.write+ AHCI write DMA + FAT32 文件覆写 - 网络配置界面: FirstInit 第二步,IP/DHCP/网关设置
- FirstInit 完成后跳转: 设置完成后写 firstInit=1,跳转正常启动路径
- block write API: 在
dkm_block_api中增加write函数,AHCI 实现 WRITE DMA - FAT32 write 支持: 文件创建、目录项写入、FAT 链更新
- 内存与 MMIO 基础设施: 物理页分配器、页表映射、高位 PCI MMIO
- IRQ 后端升级: APIC EOI/IOAPIC/MSI-X
- 网络数据路径: e1000 RX/TX, virtio-net virtqueue
2026-06-30 — 优化 FirstInit framebuffer 首次启动 UI
经验
- 渐变背景需要统一擦除路径: DSK spinner 与 FirstInit 共享
BG_TOP/BG_BOTTOM语义,并通过按 y 计算背景色的局部擦除函数维护一致背景,可避免动画/转场留下纯色块。 - 圆角矩形应只绘制有效区域: 直接逐像素判断 rounded-rect coverage,只写圆角范围内的像素,比“先填满矩形再擦角”更适合卡片叠层和输入框,能避免角落脏色块。
- 嵌入字体尺寸可独立调优: 将
render_embedded.py的字号从 32px 降到 24px 后,text_bitmaps.c中 welcome 位图从 240x38 缩到 180x30,卡片在 1280x800 内更轻盈。
教训
- 帧缓冲 sprite 也要匹配底层背景: DSK spinner 的离屏 sprite 若仍用纯色 bg 初始化,即使屏幕先画了渐变,也会在 spinner 方块区域破坏渐变连续性。
- 构建产物可能被 QEMU 锁定:
ISO/deshab.img被正在运行的qemu-system-x86_64.exe占用时,build.ps1 可完成编译/链接但会在 GPT IMG 打包阶段失败;重新打包前需关闭 QEMU。 - Python 环境不等于有 Pillow: 系统只有 WindowsApps python stub,Blender/WeCom 自带 Python 也缺 PIL;可临时使用 Blender Python +
.build_tmp/pydeps安装 Pillow 生成字体位图,避免污染项目源码。
2026-07-01 — 添加 netman 联网器与网络配置加载
经验
- 系统初始化器应由 DSK 调度:
netman.elf与mouseInit.elf一样作为系统组件,由deshab.elf直接从 FAT32 加载调用,避免 FirstInit 接管系统初始化职责。 - 早期配置要有扁平 8.3 兜底: 用户路径保留为
SYSTEM/system/deshab64/network/conf/conf.conf,但 SATA 测试镜像额外写入 root 目录NETCONF.CNF,方便早期 loader 在没有 VFS 路径解析时读取。 - 先做配置管理闭环: 在
kernel_api.net=0、e1000/virtio-net 仍缺 RX/TX 数据路径时,先实现 netman 配置解析、状态输出和启动调度,为后续 net API/协议栈接入留下稳定入口。
教训
- 完整联网不能只写 UI: FirstInit 的网络设置页只产生配置,真正联网还必须补
kernel_api.net、netdev 注册、NIC RX/TX、ARP/IP/UDP/DHCP/DNS。 - 打包工具也要同步编译: 修改
.build_tmp/mkfat32.c后必须重新生成mkfat32.exe,否则 build.ps1 会继续使用旧打包逻辑,导致新 ELF/配置未进入 SATA 镜像。 - 早期读取文件要限制格式: netman 第一版读取 root 8.3 文件,避免在没有完整 VFS/FAT32 路径遍历前引入复杂目录解析和写盘风险。
2026-07-01 — 实现最小 net API 与 netdev 注册查询闭环
经验
- 新增 ABI 能力优先复用保留槽位:
dkm_kernel_api.net原本就是占位字段,只把类型从const void *收窄为const dkm_net_api *并赋值net_get_api(),不在其前方插入字段,可保持 net 偏移稳定。 - 网络底座先用固定表: 早期驱动按 stage 顺序单线程加载,固定 8 槽 netdev registry 具备 O(1) 注册与查询,避免在无协议栈阶段引入锁、队列和 DMA 生命周期复杂度。
- discovery-only 驱动也能形成管理闭环: e1000/virtio_net 先注册 name/MAC/flags,netman 即可枚举设备,为后续 RX/TX ring、virtqueue 和协议栈提供稳定入口。
教训
- 本地驱动 ABI 镜像要同步: DKM 驱动当前各自内联
dkm_kernel_api布局,新增 typed net API 时必须同步 e1000/virtio_net 的本地结构定义,否则字段访问容易退化成裸偏移错误。 - 初始化器指定字段要按真实结构位置:
block位于dkm_kernel_api尾部,不能因阅读局部字段时把.block放到.dma后导致重复初始化 warning。 - build.bat 不会自动重编外部 DKM 源: 修改
CODE/DKM/*/*.c后需确保对应.drv产物更新到SYSTEM/driver,否则镜像可能仍打包旧驱动。
2026-07-01 — e1000 最小 RX/TX ring 与构建接入
经验
- 先暴露能力位再接协议栈: e1000 注册 netdev 时增加
TX_READY/RX_READY标志,netman 可以明确区分“只发现设备”和“具备收发 ring”的设备。 - DMA ring 使用低 4G 连续页更稳: 当前 DMA allocator 已提供 HHDM 虚拟地址与低 4G 物理地址,适合先搭 e1000 descriptor ring 和 2KB buffer 的最小闭环。
- 网络设备应纳入主构建: 把 e1000/virtio_net
.drv编译加入 build.ps1,避免手动编译驱动后忘记重新打包,导致 QEMU 仍加载旧驱动。
教训
- QEMU 测试脚本要提供真实 NIC: 不加
-device e1000e时 PCI 里没有8086:10d3,e1000 驱动只能走 not found 路径,无法验证 RX/TX ring。 - 网卡中断早期开启容易刷屏或阻塞: 在 IOAPIC/MSI 路由完善前,e1000 中断保持 masked,收发先走 polling 路径更安全。
- 串口过滤会掩盖启动卡点: 管道过滤日志可避免 100MB 输出限制,但若启动阶段卡住,只看到少量匹配行;需要结合未过滤日志或关键阶段日志判断。
2026-07-02 — 修 DMA 与 e1000 buffer 卡死
经验
- DMA 分配本身已可用: 通过逐页
alloc_pages(1)加分段 raw_log 定位,确认 RX/TX descriptor ring 和每个 2KB buffer 页都能成功分配返回,卡点不在 allocator 的分配/清零逻辑。 - 静态 BSS DMA pool 是临时方案: 当前
dma.c用内核 BSS 里的 16MiBg_dma_static_pool加virt_to_phys()反推物理地址,作为 bump allocator 可分配,但网卡 bus-master 真实写入时的物理地址可靠性没有保证。 - 稳定边界要固化: e1000 现在在分配 descriptor ring 后直接返回 -6,只注册
LINK_UP,保证系统稳定启动到 netman/FirstInit,避免反复试错阻塞其他开发。
教训
- 能靠日志“跑过”的 bug 往往是内存/时序问题: e1000 buffer/ring enable 阶段“加 raw_log 能多跑几步、去掉就更早卡”,是典型的 DMA 目标内存被网卡写坏或时序竞争,不能靠加日志掩盖。
- bump allocator 不足以支撑真实 bus-master DMA: 需要一个真正的物理页分配器(memmap USABLE → page bitmap,显式标记 kernel/module/framebuffer/arena/stack/DMA pool 已用),才能根治 e1000 RX/TX、AHCI PRDT、virtqueue 的连续物理内存需求。
- 不要无限试错: 反复缩小 ring 深度、搬动日志无法根治底层物理内存问题,应固化到已知稳定状态并明确记录卡点,把真正的物理页分配器作为下一步。
2026-07-02 — 物理页 bitmap 分配器根治 e1000 DMA
经验
- HHDM 直映射是 DMA 地址一致性的关键: 把
dma.c从"BSS 静态 pool + virt_to_phys 反推"改为"memmap USABLE → 物理页 bitmap"后,分配返回virt = hhdm_offset + phys,保证 CPU 访问的虚拟地址与网卡 bus-master 使用的物理地址指向同一页,e1000 完整 RX/TX ring 启用不再卡死。 - 依赖 Limine USABLE 语义可省掉手动避让: Limine 的 USABLE 区域已排除 kernel/modules/framebuffer/bootloader 结构,直接用它建 bitmap 就不需要再手动 trim kernel/arena/stack,代码更简单也更不易错。
- 验证要看能力位闭环: netdev flags 从
0x1(仅 LINK_UP)变成0x7(LINK_UP|TX_READY|RX_READY),netman 打印tx=ready rx=ready,说明底层 DMA + ring enable 全链路打通。
教训
- 反推物理地址不可靠: 用内核高半虚拟地址(BSS)反推物理地址喂给设备 DMA,物理页与 CPU 映射不保证一致,是之前 e1000 一启用 RCTL/TCTL 就卡死的根因;DMA 缓冲必须来自明确的物理页分配。
- 低 4G 约束要显式: e1000 等 32 位 BAR 设备的 DMA 描述符/缓冲需落在低 4G,bitmap 分配器直接把上界钳到
0x100000000并保留最低 1MiB,避免 legacy 区。
2026-07-02 — e1000 DHCP 联网闭环
经验
- PCI I/O 汇编必须 volatile:
outl(0xCF8)/inl(0xCFC)访问 PCI config space 时必须使用asm volatile和 memory clobber,否则-O2下可能丢弃或重排,导致读取 BAR0 时仍拿到 vendor/device ID。 - 验证 TX 要看 descriptor 完成位: 只看到写 TDT 不代表网卡已发送;轮询 TX descriptor
DD位可以区分“API 调用成功”和“设备真正消费 descriptor”。 - DHCP 包要兼容 BOOTP 最小长度: DHCP/BOOTP payload 补齐到 548 字节并计算 UDP checksum 后,QEMU user-net 能正常返回 OFFER/ACK。
教训
- BAR 读错会伪装成 DMA/RX 问题: BAR0 被读成
0x100e8086时,MMIO 实际指向错误地址,表现为STATUS=0、TX descriptor 不完成,容易误判为 ring 或 DHCP 问题。 - 早期网络调试要分层验证: 先确认 PCI BAR → MMIO STATUS → TX DD → RX frame → DHCP 解析,避免在协议层反复修改掩盖底层设备访问错误。
2026-07-02 — FirstInit 无线网络选择界面
经验
- 复用现有卡片/输入框最稳: FirstInit 已有
draw_rounded_input、鼠标包解析和键盘轮询,直接替换旧网络选项页为 SSID 列表 + 密码框 + Connect 按钮,可以避免新增 UI 框架。 - 配置先写入 user.conf 缓冲: 当前缺 FAT32 write API,网络选择结果先进入加密配置缓冲,字段使用
network.mode=wifi、network.ssid、network.password、network.ip=dhcp、network.dns=auto,后续写盘可直接复用。
教训
- 真实 Wi-Fi 扫描 API 尚未存在: 当前界面只能展示模拟检测到的 SSID,后续需要网卡/无线驱动和扫描 API 后再替换数据源。
- FirstInit 仍不应接管系统联网: UI 只收集用户选择和密码;实际联网仍由 netman/驱动负责,避免用户向导和系统初始化职责混在一起。
2026-07-02 — netman 完整早期联网闭环
经验
- 联网闭环要分层验收: DHCP 只证明拿到 IP,还需要 ARP 解析网关 MAC、DNS 查询外部域名,才能证明 TX/RX 与 QEMU user-net 实际可用。
- QEMU user-net 网关与 DNS 分离: DHCP 返回网关
10.0.2.2,DNS 常为10.0.2.3,应从 DHCP option 3/6 读取,缺失时再 fallback。 - 保留正式阶段日志有助于验证:
DHCP final rc、ARP final rc、DNS final rc、network final rc足以判断完整联网状态,不需要保留临时 debug 日志。
教训
- DNS parser 边界必须谨慎: DNS response 使用压缩 name,解析 answer 时必须处理
0xC0指针并检查rdlen,否则容易越界。 - 崩溃判断必须靠串口证据: 用户观察到“崩溃重启”后,插桩证明确认联网阶段成功并进入 FirstInit;不能在没有阶段锚点证据时直接回滚功能。
2026-07-02 — 修复 FirstInit 网络页 SSID 指针数组崩溃
经验
- FirstInit PIE 中避免全局指针数组: DSK 的 PIE loader 只处理有限重定位场景,UI 页面里的字符串列表应优先使用二维字符数组或栈上字符数组,避免
const char *arr[]解引用崩溃。 - 绘制函数要用阶段锚点定位: 对网络页按
redraw → row → fill → stroke → ssid → meta分层插桩,可以精确定位崩溃在第一条 SSID 文本绘制。
教训
- 看似普通的字符串数组也是重定位风险:
static const char *g_wifi_ssids[]在普通 C 程序中安全,但在当前 DSK 加载 FirstInit 的 PIE 环境下会变成运行期指针重定位风险。 - 不要用跳过流程的调试改动进入最终版本: 为了快速复现可以临时跳过账号/偏好页,但验证后必须恢复真实流程,避免引入与用户路径不一致的行为。
2026-07-03 — 用 Consolas TTF 替换 8x8 位图字体
经验
- 裸金属 OS "用系统的 TTF" 的正确解读: Deshab 没有 TTF 光栅化服务,"系统的 TTF" 应解读为构建时用宿主机 Windows 系统 TTF 字体(
C:\Windows\Fonts\consola.ttf)预渲染为灰度位图,嵌入 FirstInit ELF,运行时仍走 alpha-blend 位图渲染路径。 - Consolas 18px 渲染参数: advance=10px,'A' bbox=(0,3,10,14),'g' bbox=(0,5,10,18),11x18 格子完美适配(10px 字形 + 1px 间距,ASCII_STEP=11)。等宽字体适合 UI 输入框/菜单/按钮。
- 统一灰度位图渲染路径: 中文字体已用
fb_bitmap_alpha()8bpp 灰度 alpha-blend,ASCII 字体改用同样的灰度位图 + alpha-blend 后,字形抗锯齿效果与中文一致,且fb_char()逻辑大幅简化(移除 8x8→10x16 缩放和边缘检测)。 - 构建时预渲染脚本: 新增
CODE/font/render_ascii.py,用 PIL 从 Consolas TTF 渲染 ASCII 32-126 共 95 个字符到CODE/firstInit/ascii_bitmaps.c(18810 字节 glyph data),通过#include嵌入 main.c。
教训
- 8x8 位图字体缩放到 10x16 形状怪异: 手工 8x8 位图字体在小写字母上使用满高设计,2x2 近邻缩放后字形不自然;TTF 预渲染直接生成目标尺寸的灰度位图,避免缩放失真。
- 字符范围要覆盖全部可打印 ASCII: 原
fb_char用ch >= ' ' && ch <= 'z'(0x7a)截断,导致{|}~显示为空格;改为ch > '~'(0x7e)后覆盖全部 95 个可打印字符。 - 布局偏移要跟随字高调整: ASCII_H 从 16 变为 18 后,硬编码的
y+(h-16)/2需改为y+(h-ASCII_H)/2,wifi 行的y+17(选中标记居中)需改为y+16((50-18)/2=16)。