diff --git a/CLAUDE.md b/CLAUDE.md index 1043958..fd9c184 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -136,7 +136,7 @@ DKM/DSM 驱动加载系统已完工:内置 `console_early` + manifest.json 解 当前已实现 14 个真实 DKM 驱动: - **timer** (stage0): PIT 校准, 100ms busy-wait demo -- **apic** (stage0): CPUID Local APIC 探测, IA32_APIC_BASE MSR, ACPI MADT 枚举, LAPIC MMIO 只读寄存器验证;当前保留 PIC IRQ 路由 +- **apic** (stage0): CPUID Local APIC 探测, IA32_APIC_BASE MSR, ACPI MADT 枚举, LAPIC enable + IOAPIC 重定向表编程(ISO 表感知); B7: FUCK `apic_route=1` 时 IOAPIC→LAPIC 接管(注册 UTSM LAPIC EOI 钩子 + PIC 全屏蔽 + TPR=0 + tick vec 0x20 链路自验,失败自动完整降级回 PIC); `apic_route_legacy=1` 额外放开 IRQ1/12/11 持续投递(e1000 强制 level/active-low); `apic_diag=1` HPET 绝对时基对拍 + 键鼠流观察窗 - **acpi** (stage0): Limine RSDP → XSDT, ACPI 表枚举 - **pci** (stage0): PCI config space 扫描, 设备枚举 - **console_fb** (stage1): Limine framebuffer, 淡蓝色背景清屏(原四色圆弧加载环已移除,启动画面改由 DSK 静态 Logo 接管) @@ -150,16 +150,16 @@ DKM/DSM 驱动加载系统已完工:内置 `console_early` + manifest.json 解 - **e1000** (stage3): PCI `8086:10d3` 探测, BAR0 HHDM MMIO 映射, MAC 读取, IRQ 注册, 基础 link setup - **virtio_net** (stage3): PCI virtio-net `1af4:1001` 探测, BAR 枚举, modern virtio PCI capability 枚举;暂不进行 feature negotiation / virtqueue / 收发包 -当前内核平台能力:IDT 0–47 stub、异常处理、PIC remap 到 `0x20–0x2f`、IRQ handler 注册与 EOI、Limine RSDP/framebuffer/module/HHDM request。 +当前内核平台能力:IDT 0–255 stub 全覆盖、异常处理、PIC remap 到 `0x20–0x2f`、IRQ handler 注册与 EOI、UTSM 侧 LAPIC EOI 钩子(`idt_register_apic_eoi`,IRQ 分发末尾调用,spurious 0xFF 特判跳过)、IOAPIC→LAPIC 路由接管(B7,legacy IRQ1/12/11 可持续投递)、Limine RSDP/framebuffer/module/HHDM request。 -kernel_api 已暴露能力:log, rsdp_address, fb_address/width/height/pitch/bpp, boot_modules_response, irq_register, hhdm_offset, dma.alloc_pages。 +kernel_api 已暴露能力:log, rsdp_address, fb_address/width/height/pitch/bpp, boot_modules_response, irq_register, hhdm_offset, dma.alloc_pages, block provider registry, mmio/mm_map_mmio/mm_unmap_mmio, register_apic_eoi(尾部追加,ABI 兼容)。 当前 manifest 中 14 个外部 `.drv` 均已替换为真实 DKM 驱动并可加载执行。 下一阶段优先路线: 1. **内存与 MMIO 基础设施**: 实现物理页分配器、页表映射接口、低位/高位 PCI MMIO 映射,解决 NVMe 4G 以上 BAR 和后续 DMA 映射问题。 2. **DMA 与块设备数据路径**: 建立 contiguous DMA buffer、cache/屏障约定、PRDT/队列内存管理,然后推进 AHCI IDENTIFY/READ 与 NVMe admin queue/identify。 -3. **IRQ 后端升级**: 在现有 PIC fallback 稳定基础上扩展 IDT/vector allocator/APIC EOI/IOAPIC redirection,逐步迁移设备 IRQ,最后接入 MSI/MSI-X。 +3. **IRQ 后端升级**: ~~APIC EOI/IOAPIC redirection~~(B7 已完成:UTSM EOI 钩子 + legacy IRQ1/12/11 持续投递,QEMU 全链路验证;tick 2x 定论 = PIT mode3 IOAPIC edge 双触发 ~200Hz)。剩余:vector allocator 规范化、设备 IRQ 从 PIC 默认全面迁移、MSI/MSI-X(e1000/NVMe)。 4. **网络数据路径**: 在 DMA/IRQ 完成后推进 e1000 RX/TX ring 与 virtio-net feature negotiation/virtqueue/RX-TX。 5. **VFS 与真实块设备接入**: 将 FAT32 从测试镜像迁移到 AHCI/NVMe block provider,完善挂载、读取、目录遍历和错误路径。 6. **DKM 工程化收尾**: 统一驱动本地 ABI 头、清理 warning、补充自动化 QEMU 场景(e1000/virtio-net/nvme/ahci)。 diff --git a/CODE/DKM/apic/apic.c b/CODE/DKM/apic/apic.c index 21aa7d6..7628235 100644 --- a/CODE/DKM/apic/apic.c +++ b/CODE/DKM/apic/apic.c @@ -1,4 +1,4 @@ -/* DKM APIC Driver — APIC discovery + IOAPIC init + LAPIC enable +/* DKM APIC Driver — APIC discovery + IOAPIC init + LAPIC enable + 路由接管 * Stage 0, required, depends on "timer", provides "irq" and "apic". * * 真机路径: @@ -8,6 +8,40 @@ * 3. MADT ISO (Interrupt Source Override) 处理: 按 ACPI 规范修正 IRQ→GSI 映射 * 4. SIPI 多核启动框架 (预留, 未实现) * + * B7 路由接管 (apic_route): + * FUCK [drivers] apic_route=1 时, 本驱动在初始化末尾尝试把中断路由 + * 从 8259 PIC 切换到 IOAPIC→LAPIC 路径: + * a. 经 kernel_api.register_apic_eoi 向 UTSM 注册 LAPIC EOI 钩子 + * (B7 阶段2 接口; idt_handler 两个 IRQ 分支在分发末尾调用之) + * b. 注册 PIT tick 计数 handler 于 vector 0x20 — 即 idt_handler 的 + * PIC 分支 (0x20-0x2F), 与键盘 0x21/鼠标 0x2C/网卡 0x2B 同一代码 + * 路径; handler 返回 0 由钩子完成 LAPIC EOI, 重复投递 >=2 即证明 + * "legacy vector 分支 + 钩子 EOI" 全链路可持续 (阶段1 用 0xE0 自 + * EOI 只证明了 APIC 分支) + * c. 按 ISO 表解析 PIT GSI (QEMU q35: IRQ0→GSI2), 解屏蔽该条目 + * d. LAPIC TPR=0 全放行, PIC IMR 全 1 屏蔽 (QEMU 无功能 IMCR 寄存器, + * PIC 全屏蔽即等效切换; 8259 与 IOAPIC 同时监听 IRQ 线, 全屏蔽避免双投递) + * e. PIT ch0 mode3 100Hz 产生真实硬件中断, 500ms 窗口内计数 >=2 次 + * 即证明 IOAPIC→LAPIC→IDT→handler→钩子EOI 全链路 + * f. 验证失败完整降级: IOAPIC 全 mask + 恢复原 PIC IMR + 注销 handler + * 与 EOI 钩子, 系统行为与切换前完全一致 + * + * B7 阶段2 (apic_route_legacy): + * EOI 钩子落地后, legacy IRQ1(键盘 0x21)/IRQ12(鼠标 0x2C)/IRQ11(e1000 + * 0x2B) 重定向项可持续投递, 不再是 one-shot。ISA 线按 MADT ISO 极性/触发 + * (缺省 edge/active-high); PCI 线 (IRQ11) 无 ISO 覆盖时强制 level/ + * active-low (PCI INTx 规范; QEMU 内部 pci irq 电平语义与之匹配, + * Linux/Windows 同配置)。e1000 handler 读 ICR 清源后电平 deassert, + * 由钩子 EOI 完成 LAPIC 侧清扫。 + * + * apic_diag=1 诊断: + * - HPET 绝对时基对拍: 解析 ACPI HPET 表 (QEMU q35 有, 100MHz 主计数器), + * 用主计数器实测 tsc_calibrate 的 10ms 窗口与 route 验证的 500ms 窗口, + * 定论 "tick 2x" 根因 (TSC 校准测半 vs PIT mode3 双触发) + * - PS/2 鼠标流: 初始化 AUX 端口 + 注册 IRQ12 诊断 handler, 配合 + * QEMU monitor mouse_move 验证 IRQ12 持续投递 (计数 + 观察窗) + * diag 只增观测不改变量路由语义, 默认 0 关闭。 + * * QEMU 兼容: * - QEMU 默认只提供 1 个 IOAPIC, 24 条 GSI * - 实机可能有多个 IOAPIC, GSI 范围更大 @@ -61,6 +95,11 @@ struct dkm_kernel_api { const void *mmio; void *(*mm_map_mmio)(u64 phys, u64 size); void (*mm_unmap_mmio)(void *virt, u64 size); + /* B7 阶段2 尾部追加 (旧内核无此字段 -> 判空使用) */ + void (*register_apic_eoi)(void (*eoi_fn)(void)); + /* B7 阶段3 尾部追加 (本驱动不使用, 保持布局与 UTSM 一致) */ + int (*irq_vector_alloc)(void); + void (*irq_vector_free)(int vector); }; struct dkm_driver_handle; @@ -95,7 +134,7 @@ const struct dkm_driver_desc driver_desc = { .abi_version = DKM_ABI_VERSION, .desc_size = sizeof(struct dkm_driver_desc), .name = "apic", - .version = "0.1.0", + .version = "0.2.0", .vendor = "Deshab", .driver_class = 3, /* DKM_CLASS_INTERRUPT */ .stage = 0, @@ -149,6 +188,45 @@ typedef struct __attribute__((packed)) { u32 flags; } acpi_madt; +/* ACPI HPET 表 (signature "HPET") — apic_diag 绝对时基对拍用 */ +typedef struct __attribute__((packed)) { + acpi_sdt_header hdr; + u32 event_timer_block_id; + /* ACPI Generic Address Structure (GAS) */ + u8 addr_space_id; /* 0 = system memory */ + u8 register_bit_width; + u8 register_bit_offset; + u8 access_size; + u64 base_addr; /* HPET MMIO 物理基址 (QEMU: 0xFED00000) */ + u8 hpet_number; + u16 min_clock_tick; + u8 page_protection; +} acpi_hpet; + +/* Limine boot module 最小类型 (与 bootfs.drv / UTSM limine.h 布局一致) */ +struct limine_file_min { + u64 revision; + void *address; + u64 size; + char *path; + char *cmdline; + u32 media_type; + u32 unused; + u32 tftp_ip; + u32 tftp_port; + u32 partition_index; + u32 mbr_disk_id; + u8 gpt_disk_uuid[16]; + u8 gpt_part_uuid[16]; + u8 part_uuid[16]; +}; + +struct limine_module_response_min { + u64 revision; + u64 module_count; + struct limine_file_min **modules; +}; + /* ---- IOAPIC 信息存储(真机多 IOAPIC 支持) ---- */ #define MAX_IOAPIC 4 @@ -186,7 +264,22 @@ struct madt_info { struct lapic_entry lapics[MAX_LAPIC]; }; +/* ---- B7 路由接管配置 (FUCK [drivers]) ---- */ +struct apic_config { + int lapic_enable; /* lapic_enable, 默认 1 */ + int ioapic_init; /* ioapic_init, 默认 1 */ + int apic_route; /* apic_route, 默认 0 (PIC 路由, 现状) */ + int apic_route_legacy; /* apic_route_legacy, 默认 0 */ + int apic_diag; /* apic_diag, 默认 0 (HPET 校准/投递计数/鼠标流诊断) */ +}; + static const struct dkm_log_api *g_log; +static struct apic_config g_cfg = { 1, 1, 0, 0, 0 }; + +static volatile u32 *g_lapic; /* LAPIC MMIO 映射基址, 0=未映射 */ +static u32 g_lapic_id; /* BSP Local APIC ID (IOAPIC 目的 ID) */ +static volatile u32 g_tick_count; /* PIT tick 计数 (路由验证) */ +static int g_route_active; /* 1=IOAPIC 路由已接管 */ static __inline__ void apic_mb(void) { __asm__ volatile("" ::: "memory"); @@ -199,6 +292,24 @@ static int memeq(const char *a, const char *b, u32 n) { return 1; } +static u32 str_len(const char *s) { + u32 n = 0; + while (s[n]) n++; + return n; +} + +/* 在 NUL 结尾字符串中查找子串 (cmdline 匹配用) */ +static int str_contains(const char *hay, const char *needle) { + u32 nlen = str_len(needle); + if (!nlen) return 1; + for (u32 i = 0; hay[i]; i++) { + u32 j = 0; + while (j < nlen && hay[i + j] && hay[i + j] == needle[j]) j++; + if (j == nlen) return 1; + } + return 0; +} + static u8 checksum8(const u8 *data, u32 len) { u8 sum = 0; for (u32 i = 0; i < len; i++) sum += data[i]; @@ -219,6 +330,37 @@ static void log_hex(const char *prefix, u64 val) { g_log->info(buf); } +static void log_dec(const char *prefix, u64 val) { + char buf[24]; + u32 pos = 0; + if (!val) { + buf[pos++] = '0'; + } else { + char tmp[20]; + u32 n = 0; + while (val) { tmp[n++] = (char)('0' + (val % 10)); val /= 10; } + while (n) buf[pos++] = tmp[--n]; + } + buf[pos] = 0; + g_log->info(prefix); + g_log->info(buf); +} + +/* ---- I/O 端口 (PIC / PIT) ---- */ +static __inline__ void outb(u16 port, u8 value) { + __asm__ volatile ("outb %0, %1" :: "a"(value), "Nd"(port)); +} + +static __inline__ u8 inb(u16 port) { + u8 value; + __asm__ volatile ("inb %1, %0" : "=a"(value) : "Nd"(port)); + return value; +} + +static __inline__ void io_wait(void) { + outb(0x80, 0); +} + static void cpuid(u32 leaf, u32 *a, u32 *b, u32 *c, u32 *d) { __asm__ volatile ("cpuid" : "=a"(*a), "=b"(*b), "=c"(*c), "=d"(*d) @@ -232,6 +374,12 @@ static u64 rdmsr(u32 msr) { return ((u64)hi << 32) | lo; } +static void wrmsr(u32 msr, u64 val) { + u32 lo = (u32)(val & 0xffffffffu); + u32 hi = (u32)(val >> 32); + __asm__ volatile("wrmsr" :: "a"(lo), "d"(hi), "c"(msr)); +} + static int cpu_has_lapic(void) { u32 a, b, c, d; cpuid(1, &a, &b, &c, &d); @@ -241,6 +389,131 @@ static int cpu_has_lapic(void) { return (d & (1u << 9)) != 0; } +/* ---- FUCK 配置自解析 (DKM 侧, 不依赖 UTSM ini_parser) ---- * + * UTSM 通过 Limine boot module (cmdline="fuck:config") 预加载 FUCK; + * kernel_api.boot_modules_response 对所有 DKM 驱动可见。 + * 此处实现最小分区感知 INI 扫描: 只找 [drivers] 分区下的目标键。 + * 缺失文件/缺失键一律回退默认值, 行为与无配置时一致。 + */ + +/* 解析值 token: 支持 0/1/true/false/on/off/yes/no 及十进制整数 */ +static int parse_scalar(const char *line, u64 start, u64 line_len, int defval) { + u64 i = start; + while (i < line_len && (line[i] == ' ' || line[i] == '\t')) i++; + u64 tok_start = i; + while (i < line_len && line[i] != ' ' && line[i] != '\t' && + line[i] != '#' && line[i] != ';' && line[i] != '\r') i++; + u64 tok_len = i - tok_start; + const char *tok = line + tok_start; + if (!tok_len) return defval; + if (tok_len == 1 && tok[0] == '1') return 1; + if (tok_len == 1 && tok[0] == '0') return 0; + if (tok_len == 4 && memeq(tok, "true", 4)) return 1; + if (tok_len == 5 && memeq(tok, "false", 5)) return 0; + if (tok_len == 2 && memeq(tok, "on", 2)) return 1; + if (tok_len == 3 && memeq(tok, "off", 3)) return 0; + if (tok_len == 3 && memeq(tok, "yes", 3)) return 1; + if (tok_len == 2 && memeq(tok, "no", 2)) return 0; + /* 十进制整数 (允许前导 '-') */ + int neg = 0; + u64 j = 0; + if (tok[0] == '-') { neg = 1; j = 1; } + int val = 0; + int any = 0; + for (; j < tok_len; j++) { + if (tok[j] < '0' || tok[j] > '9') return defval; + val = val * 10 + (tok[j] - '0'); + any = 1; + } + if (!any) return defval; + return neg ? -val : val; +} + +static int fuck_get_int(const char *text, u64 size, + const char *section, const char *key, int defval) { + u64 pos = 0; + int in_section = 0; + u32 slen = str_len(section); + u32 klen = str_len(key); + + while (pos < size) { + u64 end = pos; + while (end < size && text[end] != '\n') end++; + u64 line_len = end - pos; + if (line_len > 0 && text[pos + line_len - 1] == '\r') line_len--; + const char *line = text + pos; + + u64 i = 0; + while (i < line_len && (line[i] == ' ' || line[i] == '\t')) i++; + if (i < line_len) { + char c = line[i]; + if (c == '#' || c == ';') { + /* 注释行, 跳过 */ + } else if (c == '[') { + in_section = 0; + if (line_len >= (u64)(i + 1 + slen + 1) && + memeq(line + i + 1, section, slen) && + line[i + 1 + slen] == ']') { + in_section = 1; + } + } else if (in_section) { + if (line_len >= (u64)(i + klen) && + memeq(line + i, key, klen)) { + u64 j = i + klen; + while (j < line_len && (line[j] == ' ' || line[j] == '\t')) j++; + if (j < line_len && line[j] == '=') { + return parse_scalar(line, j + 1, line_len, defval); + } + } + } + } + pos = end + 1; + } + return defval; +} + +static void load_fuck_config(const struct dkm_kernel_api *api) { + /* 编译期默认: 与 FUCK 注释承诺一致 (无配置时行为不变) */ + g_cfg.lapic_enable = 1; + g_cfg.ioapic_init = 1; + g_cfg.apic_route = 0; + g_cfg.apic_route_legacy = 0; + g_cfg.apic_diag = 0; + + const struct limine_module_response_min *resp = + (const struct limine_module_response_min *)api->boot_modules_response; + if (!resp || !resp->modules) { + g_log->warn("[apic] no boot modules; FUCK defaults in effect"); + return; + } + + for (u64 i = 0; i < resp->module_count; i++) { + struct limine_file_min *f = resp->modules[i]; + if (!f || !f->cmdline || !f->address || !f->size) continue; + if (!str_contains(f->cmdline, "fuck:config")) continue; + + const char *text = (const char *)f->address; + u64 size = f->size; + g_log->info("[apic] FUCK config module found"); + log_hex("[apic] FUCK size=", size); + + g_cfg.lapic_enable = fuck_get_int(text, size, "drivers", "lapic_enable", 1); + g_cfg.ioapic_init = fuck_get_int(text, size, "drivers", "ioapic_init", 1); + g_cfg.apic_route = fuck_get_int(text, size, "drivers", "apic_route", 0); + g_cfg.apic_route_legacy = fuck_get_int(text, size, "drivers", "apic_route_legacy", 0); + g_cfg.apic_diag = fuck_get_int(text, size, "drivers", "apic_diag", 0); + + log_dec("[apic] cfg lapic_enable=", (u64)g_cfg.lapic_enable); + log_dec("[apic] cfg ioapic_init=", (u64)g_cfg.ioapic_init); + log_dec("[apic] cfg apic_route=", (u64)g_cfg.apic_route); + log_dec("[apic] cfg apic_route_legacy=", (u64)g_cfg.apic_route_legacy); + log_dec("[apic] cfg apic_diag=", (u64)g_cfg.apic_diag); + return; + } + + g_log->warn("[apic] FUCK module (fuck:config) not found; defaults in effect"); +} + static const acpi_sdt_header *find_table_from_rsdt(const acpi_sdt_header *rsdt, const char sig[4], u32 entry_size) { if (!rsdt || rsdt->length < sizeof(acpi_sdt_header)) return NULL; if (checksum8((const u8 *)rsdt, rsdt->length) != 0) return NULL; @@ -288,6 +561,27 @@ static const acpi_madt *find_madt(const void *rsdp_addr) { return NULL; } +/* 查找 HPET 表 (apic_diag 用; 与 find_madt 同构, 仅签名不同) */ +static const acpi_hpet *find_hpet(const void *rsdp_addr) { + if (!rsdp_addr) return NULL; + const acpi_rsdp_v2 *rsdp = (const acpi_rsdp_v2 *)rsdp_addr; + if (!memeq(rsdp->signature, "RSD PTR ", 8)) return NULL; + if (checksum8((const u8 *)rsdp, 20) != 0) return NULL; + if (rsdp->revision >= 2 && rsdp->length >= sizeof(acpi_rsdp_v2) && rsdp->xsdt_addr) { + if (checksum8((const u8 *)rsdp, rsdp->length) == 0) { + const acpi_sdt_header *xsdt = (const acpi_sdt_header *)(uintptr_t)rsdp->xsdt_addr; + const acpi_sdt_header *hpet = find_table_from_rsdt(xsdt, "HPET", 8); + if (hpet) return (const acpi_hpet *)hpet; + } + } + if (rsdp->rsdt_addr) { + const acpi_sdt_header *rsdt = (const acpi_sdt_header *)(uintptr_t)(u64)rsdp->rsdt_addr; + const acpi_sdt_header *hpet = find_table_from_rsdt(rsdt, "HPET", 4); + if (hpet) return (const acpi_hpet *)hpet; + } + return NULL; +} + static void parse_madt_entries(const acpi_madt *madt, struct madt_info *info) { const u8 *base = (const u8 *)madt; u32 off = (u32)sizeof(acpi_madt); @@ -376,18 +670,19 @@ static void ioapic_write(volatile u32 *base, u8 reg, u32 val) { base[IOAPIC_IOWIN / 4] = val; } -/* IOAPIC 重定向表项 (64-bit, 分两次 32-bit 读写) +/* IOAPIC 重定向表项 (64-bit, 分两次 32-bit 读写, 82093AA 规范布局) * bits 0-7: interrupt vector (0x20-0xFF) - * bit 8: delivery mode: 0=fixed, 1=lowest, 2=SMI, 4=NMI, 5=INIT, 7=ExtINT - * bit 9: destination mode: 0=physical, 1=logical - * bit 10: delivery status (RO) - * bit 11: polarity: 0=active high, 1=active low - * bit 12: remote IRR (RO, level-triggered only) - * bit 13: trigger mode: 0=edge, 1=level - * bit 14: mask: 0=enabled, 1=masked - * bit 15: reserved - * bits 16-55: reserved + * bits 8-10: delivery mode: 0=fixed, 1=lowest, 2=SMI, 4=NMI, 5=INIT, 7=ExtINT + * bit 11: destination mode: 0=physical, 1=logical + * bit 12: delivery status (RO) + * bit 13: polarity: 0=active high, 1=active low + * bit 14: remote IRR (RO, level-triggered only) + * bit 15: trigger mode: 0=edge, 1=level + * bit 16: mask: 0=enabled, 1=masked + * bits 17-55: reserved * bits 56-63: destination APIC ID (physical mode) or MDA (logical mode) + * 注: 历史版本曾将 polarity/trigger/mask 错置于 bit 11/13/14, + * 导致 mask 写入只读 remote IRR 位而从未生效 —— 勿回退。 */ static u64 ioapic_read_redir(volatile u32 *base, u8 entry) { u32 lo = ioapic_read(base, (u8)(0x10 + entry * 2)); @@ -400,13 +695,96 @@ static void ioapic_write_redir(volatile u32 *base, u8 entry, u64 val) { ioapic_write(base, (u8)(0x10 + entry * 2 + 1), (u32)(val >> 32)); } +/* ---- B7: GSI 路由辅助 ---- */ + +/* 合成重定向表项: vector + ISO 极性/触发 + 物理目的 APIC ID + mask 位 */ +static u64 redir_compose(u32 vector, u16 iso_flags, u32 dest_apic_id, int masked) { + u64 entry = (u64)(vector & 0xFFu); + /* delivery mode = fixed (0), destination mode = physical (0) */ + + /* 极性: bit 13. ISA 默认 active-high (0). + * ISO flags bit 0-1: 0=conforms, 1=active-high, 2=reserved, 3=active-low */ + if ((iso_flags & 0x03u) == 0x03u) entry |= (1ULL << 13); + + /* 触发: bit 15. ISA 默认 edge (0). + * ISO flags bit 2-3: 0=conforms, 1=edge, 2=reserved, 3=level */ + if ((iso_flags & 0x0Cu) == 0x0Cu) entry |= (1ULL << 15); + + entry |= ((u64)dest_apic_id << 56); + if (masked) entry |= (1ULL << 16); + return entry; +} + +/* 找覆盖指定 GSI 的 IOAPIC (多 IOAPIC 按 GSI 范围划分) */ +static struct ioapic_info *ioapic_for_gsi(struct madt_info *info, u32 gsi) { + for (u32 i = 0; i < info->ioapic_count; i++) { + struct ioapic_info *io = &info->ioapics[i]; + if (gsi >= io->gsi_base && gsi < io->gsi_base + io->gsi_count) return io; + } + return NULL; +} + +/* ISA IRQ → GSI 解析: 优先 MADT ISO override, 缺省恒等 (QEMU ISA 惯例) */ +static u32 isa_irq_to_gsi(u8 isa_irq, u16 *flags_out, + const struct iso_info *isos, u32 iso_count) { + for (u32 i = 0; i < iso_count; i++) { + if (isos[i].bus_source == isa_irq) { + *flags_out = isos[i].flags; + return isos[i].gsi; + } + } + *flags_out = 0; + return (u32)isa_irq; +} + +/* 编程单个 GSI 的重定向项 (unmask=1 放开投递, 0 保持屏蔽) */ +static int ioapic_program_gsi(struct madt_info *info, u32 gsi, + u32 vector, u16 iso_flags, int unmask) { + struct ioapic_info *io = ioapic_for_gsi(info, gsi); + if (!io || !io->mmio) return -1; + u8 pin = (u8)(gsi - io->gsi_base); + ioapic_write_redir(io->mmio, pin, + redir_compose(vector, iso_flags, g_lapic_id, !unmask)); + return 0; +} + +/* 全 IOAPIC 全条目屏蔽 (降级路径) — 保留 vector 信息, 仅置 mask 位 */ +static void ioapic_mask_all(struct madt_info *info) { + for (u32 i = 0; i < info->ioapic_count; i++) { + struct ioapic_info *io = &info->ioapics[i]; + if (!io->mmio) continue; + for (u32 pin = 0; pin < io->gsi_count && pin < 64; pin++) { + u64 entry = ioapic_read_redir(io->mmio, (u8)pin); + entry |= (1ULL << 16); + ioapic_write_redir(io->mmio, (u8)pin, entry); + } + } +} + +/* 重定向表回读 dump (验证证据) */ +static void ioapic_dump_table(struct madt_info *info) { + for (u32 i = 0; i < info->ioapic_count; i++) { + struct ioapic_info *io = &info->ioapics[i]; + if (!io->mmio) continue; + g_log->info("[apic] IOREDTBL readback:"); + for (u32 pin = 0; pin < io->gsi_count && pin < 24; pin++) { + u64 entry = ioapic_read_redir(io->mmio, (u8)pin); + u32 vec = (u32)(entry & 0xFFu); + u32 masked = (u32)((entry >> 16) & 1u); + log_dec("[apic] pin=", pin); + log_hex("[apic] vec=", vec); + log_dec("[apic] mask=", masked); + } + } +} + /* ---- IOAPIC 初始化 ---- * * 逐条目编程重定向表: ISA IRQ→vector 映射 + mask all。 * 真机关键: 处理 MADT ISO (IRQ0→GSI2 是最常见的 override)。 * PIC 兼容模式: vector = 0x20 + IRQ (与 PIC remap 布局一致)。 * * **QEMU 无法测试但真机必需的代码路径**: - * 1. ISO 处理: 真机几乎总有 IRQ0→GSI2 override, QEMU 默认无 override + * 1. ISO 处理: 真机几乎总有 IRQ0→GSI2 override, QEMU 默认有 1 条 * 2. 极性标志: 真机 ISA IRQ0 (timer) 必须设 active-high, * 但 ISO 可能指定 active-low (PCIe SCI 等) * 3. 多 IOAPIC: 真机可能有 2+ IOAPIC, 每个 GSI 范围不同 @@ -480,29 +858,12 @@ static void ioapic_init_one(struct ioapic_info *io, u64 hhdm_offset, u32 vec = gsi_to_vector[gsi]; u16 flags = gsi_flags[gsi]; - entry = (u64)vec; /* vector */ - /* delivery mode = fixed (0) */ - /* destination mode = physical (0) */ - - /* 极性: bit 11. ISA 默认 active-high (0). - * ISO flags bit 1: 0=conforms (active-high for ISA), 1=active-low */ - if (flags & 0x03) { - if (flags & 0x02) entry |= (1ULL << 11); /* active low */ - } - - /* 触发模式: bit 13. ISA 默认 edge (0). - * ISO flags bit 3: 0=conforms (edge for ISA), 1=level */ - if (flags & 0x08) { - if (flags & 0x04) entry |= (1ULL << 13); /* level triggered */ - } - - /* destination: BSP APIC ID (读自 LAPIC) */ - /* 暂时硬编码 destination = 0 (BSP), 后续从 LAPIC ID 寄存器获取 */ - entry |= (0ULL << 56); /* destination APIC ID = 0 */ + /* vector + ISO 极性/触发 + 真实 BSP LAPIC ID, 保持 mask */ + entry = redir_compose(vec, flags, g_lapic_id, 1); + } else { + entry = (1ULL << 16); /* 无映射条目: 纯 mask (bit16, vector=0 无害) */ } - entry |= (1ULL << 14); /* mask = 1, 所有条目默认屏蔽 */ - ioapic_write_redir(io->mmio, (u8)pin, entry); } @@ -519,14 +880,8 @@ static void ioapic_init_one(struct ioapic_info *io, u64 hhdm_offset, * 但实机 BIOS 可能留 LAPIC 为 software-disable 状态。 */ -static void wrmsr(u32 msr, u64 val) { - u32 lo = (u32)(val & 0xffffffffu); - u32 hi = (u32)(val >> 32); - __asm__ volatile("wrmsr" :: "a"(lo), "d"(hi), "c"(msr)); -} - -static void lapic_enable(u64 hhdm_offset, u64 lapic_phys) { - volatile u32 *lapic = (volatile u32 *)(uintptr_t)(hhdm_offset + lapic_phys); +static void lapic_enable(void) { + volatile u32 *lapic = g_lapic; /* 确保 IA32_APIC_BASE 的 global enable 和 BSP 位已设置 */ u64 apic_base = rdmsr(0x1B); @@ -559,6 +914,532 @@ static void lapic_enable(u64 hhdm_offset, u64 lapic_phys) { log_hex("[apic] LAPIC version=", lapic[0x30 / 4]); } +/* LAPIC EOI — 写 0 到 EOI 寄存器 (offset 0xB0)。 + * B7 阶段2: 本函数作为 EOI 钩子经 kernel_api.register_apic_eoi 注册给 UTSM, + * idt_handler 在两个 IRQ 分支的分发末尾调用 (钩子签名 void(*)(void) 匹配)。 + * 调用点: IRQ 上下文, 单核 BSP interrupt gate (IF=0), 无嵌套, 无需锁。 */ +static __inline__ void lapic_eoi(void) { + if (g_lapic) { + g_lapic[0xB0 / 4] = 0; + apic_mb(); + } +} + +/* ---- B7 diag: HPET 绝对时基 (tick 2x 定论实验) ---- * + * 选型理由: TSC 校准 (PIT mode0 回绕法) 与被测对象 (PIT mode3 投递频率) + * 共享 PIT 硬件, 循环论证无法定论 2x 根因。HPET 主计数器是独立时基 + * (QEMU q35: 100MHz, period=10000fs), 32 位回绕周期 ~42.9s, 对 ms 级 + * 窗口充裕。只设 GEN_CONF bit0 (overall enable); 绝不置 bit1 + * (legacy replacement route 会抢 PIT IRQ0/RTC IRQ8 路由, 破坏被测链路)。 + */ + +static volatile u32 *g_hpet; /* HPET MMIO 基址 (HHDM), 0=不可用 */ +static u32 g_hpet_period_fs; /* 主计数器周期 (飞秒), GCAP_ID[63:32] */ + +#define HPET_GCAP_ID 0x00 +#define HPET_GEN_CONF 0x10 +#define HPET_MAIN_CNT 0xF0 + +static u64 hpet_read64(u32 off) { + volatile u32 *p = (volatile u32 *)((volatile u8 *)g_hpet + off); + u32 lo = p[0]; + u32 hi = p[1]; + return ((u64)hi << 32) | lo; +} + +static void hpet_write64(u32 off, u64 v) { + volatile u32 *p = (volatile u32 *)((volatile u8 *)g_hpet + off); + p[0] = (u32)(v & 0xffffffffu); + p[1] = (u32)(v >> 32); +} + +static __inline__ u32 hpet_main32(void) { + return *(volatile u32 *)((volatile u8 *)g_hpet + HPET_MAIN_CNT); +} + +/* 初始化 HPET 时基; 成功返回 0 且 g_hpet/g_hpet_period_fs 有效 */ +static int hpet_diag_init(const struct dkm_kernel_api *api) { + const acpi_hpet *hpet = find_hpet(api->rsdp_address); + if (!hpet) { + g_log->warn("[apic] diag: HPET table not found"); + return -1; + } + if (hpet->addr_space_id != 0 || !hpet->base_addr) { + g_log->warn("[apic] diag: HPET not memory-mapped"); + return -1; + } + g_hpet = (volatile u32 *)(uintptr_t)(api->hhdm_offset + hpet->base_addr); + u64 gcap = hpet_read64(HPET_GCAP_ID); + g_hpet_period_fs = (u32)(gcap >> 32); + if (!g_hpet_period_fs || g_hpet_period_fs > 100000000u) { + g_log->warn("[apic] diag: HPET period implausible"); + g_hpet = 0; + return -1; + } + log_hex("[apic] diag: HPET base=", hpet->base_addr); + log_dec("[apic] diag: HPET period(fs)=", g_hpet_period_fs); + + /* overall enable (bit0); legacy replacement route (bit1) 保持 0 */ + u64 conf = hpet_read64(HPET_GEN_CONF); + if (!(conf & 1u)) { + hpet_write64(HPET_GEN_CONF, conf | 1u); + apic_mb(); + } + g_log->info("[apic] diag: HPET main counter enabled"); + return 0; +} + +/* HPET 主计数器差值 → 微秒 (全程 u64 整数, -msoft-float 环境无浮点) */ +static u64 hpet_delta_us(u32 h0, u32 h1) { + /* 无符号回绕安全: u32 减法自动回绕 (42.9s 周期, 窗口 << 此值) */ + u64 ticks = (u64)(u32)(h1 - h0); + /* ticks * period_fs / 1e9 = us; period_fs 典型 10000, 500ms 窗口 + * ticks≈5e7, 乘积 5e11 << 2^64, 溢出安全 */ + return (ticks * (u64)g_hpet_period_fs) / 1000000000ull; +} + +/* ---- B7 diag: PS/2 鼠标流 (IRQ12 持续投递验证) ---- * + * UTSM 阶段 IRQ12 无属主 (mouseInit 是 DSK 阶段模块), diag handler 独占; + * DSK mouseInit 之后经 irq_register(12) 覆盖本 handler 并完整复位鼠标, + * 无状态冲突。所有单步带超时, 失败只记日志不阻塞。 */ + +static volatile u32 g_mouse_pkt_count; + +static int ps2_wait_ibf(void) { + u32 t = 100000; + while (t--) { if (!(inb(0x64) & 0x02)) return 0; } + return -1; +} + +static int ps2_wait_obf(void) { + u32 t = 100000; + while (t--) { if (inb(0x64) & 0x01) return 0; } + return -1; +} + +static void ps2_drain(void) { + while (inb(0x64) & 0x01) { (void)inb(0x60); } +} + +/* 经 0xD4 通道写鼠标命令并等 ACK(0xFA); 失败返回 -1 */ +static int ps2_aux_cmd(u8 cmd) { + if (ps2_wait_ibf() != 0) return -1; + outb(0x64, 0xD4); + if (ps2_wait_ibf() != 0) return -1; + outb(0x60, cmd); + if (ps2_wait_obf() != 0) return -1; + return inb(0x60) == 0xFA ? 0 : -1; +} + +/* IRQ12 诊断 handler: 只读数据计数, EOI 交给 idt_handler 分发路径 + * (PIC 模式: 8259 EOI; route 模式: 钩子 LAPIC EOI + 无害 8259 EOI)。 + * 返回 0 — 不自负 EOI, 与 ps2kbd/e1000 handler 同一约定。 */ +static int apic_mouse_diag_handler(u8 vector) { + (void)vector; + u8 st = inb(0x64); + if ((st & 0x01) && (st & 0x20)) { /* OBF 且 AUX 位置位 = 鼠标数据 */ + u8 data = inb(0x60); /* 必须读, 否则 OBF 保持阻塞后续包 */ + g_mouse_pkt_count++; + if (g_mouse_pkt_count <= 6) { + g_log->info("[apic] diag: mouse packet byte"); + log_hex("[apic] diag: data=", data); + } + } + return 0; +} + +/* IRQ1 诊断 handler (键盘流计数): ps2kbd demo 窗口只有 ~10ms 数量级, + * 自动化无法抓住; diag 在 stage0 自建 3s 观察窗验证 IRQ1 持续投递。 + * stage3 ps2kbd 注册时覆盖本 handler, 无冲突。 */ +static volatile u32 g_kbd_pkt_count; + +static int apic_kbd_diag_handler(u8 vector) { + (void)vector; + u8 st = inb(0x64); + if ((st & 0x01) && !(st & 0x20)) { /* OBF 且 AUX 位清零 = 键盘数据 */ + u8 data = inb(0x60); /* 读走扫描码 (窗内按键, 无消费者) */ + g_kbd_pkt_count++; + if (g_kbd_pkt_count <= 6) { + g_log->info("[apic] diag: kbd scancode byte"); + log_hex("[apic] diag: sc=", data); + } + } + return 0; +} + +/* AUX 端口初始化: 启用 AUX 时钟 + 采样率 100 + 数据报告, 最后才开 AUX IRQ。 + * 顺序要点: AUX IRQ (cfg bit1) 必须在全部命令/ACK 交互完成后才使能 — + * 否则 ACK 字节 (0xFA) 到达即触发 IRQ12 被 diag handler 读走, 轮询侧 + * 永远等不到 ACK (实测 F3 nack 即此竞态: handler 日志 data=0xfa)。 + * 单步失败只记日志继续 (与 mouseInit 同一安全准则)。 */ +static void ps2_aux_enable_stream(void) { + ps2_drain(); + + if (ps2_wait_ibf() != 0) { g_log->warn("[apic] diag: PS2 ibf timeout"); return; } + outb(0x64, 0x20); /* 读配置字节 */ + if (ps2_wait_obf() != 0) { g_log->warn("[apic] diag: PS2 cfg read timeout"); return; } + u8 cfg = inb(0x60); + cfg &= (u8)~0x02; /* bit1=0: AUX IRQ 先关闭 (轮询期) */ + cfg &= (u8)~0x20; /* bit5=0: AUX clock enable */ + cfg |= 0x01; /* bit0=1: KBD IRQ 保持使能 (键盘流用) */ + + if (ps2_wait_ibf() != 0) return; + outb(0x64, 0x60); /* 写配置字节 */ + if (ps2_wait_ibf() != 0) return; + outb(0x60, cfg); + log_hex("[apic] diag: PS2 cfg byte=", cfg); + + if (ps2_wait_ibf() != 0) return; + outb(0x64, 0xA8); /* 启用 AUX 端口 */ + + if (ps2_aux_cmd(0xF3) != 0) { g_log->warn("[apic] diag: mouse F3 nack"); } + else if (ps2_aux_cmd(100) != 0) { g_log->warn("[apic] diag: mouse rate nack"); } + if (ps2_aux_cmd(0xF4) != 0) { g_log->warn("[apic] diag: mouse F4 nack"); return; } + g_log->info("[apic] diag: PS/2 mouse data reporting enabled"); + + ps2_drain(); /* 清 F4 ACK 后可能挂起的首个流字节 */ + + /* 全部交互完成, 现在才开 AUX IRQ — 之后鼠标字节全走 IRQ12 handler */ + if (ps2_wait_ibf() != 0) return; + outb(0x64, 0x20); + if (ps2_wait_obf() != 0) return; + cfg = inb(0x60); + cfg |= 0x02; /* bit1=1: AUX IRQ enable */ + if (ps2_wait_ibf() != 0) return; + outb(0x64, 0x60); + if (ps2_wait_ibf() != 0) return; + outb(0x60, cfg); + log_hex("[apic] diag: PS2 cfg byte(final)=", cfg); +} + +/* ---- B7 阶段2: PIT tick 路由验证 ---- * + * vector 0x20 落在 idt_handler 的 PIC 分支 (vector 32-47) — 与键盘 0x21/ + * 鼠标 0x2C/网卡 0x2B 同一代码路径。分发顺序: handler(本函数) → 钩子 + * LAPIC EOI → 8259 EOI (PIC 全屏蔽下为无害 no-op)。 + * 本 handler 返回 0 不自负 EOI: 由 UTSM 钩子写 LAPIC EOI 寄存器清除 + * ISR 位; 重复投递 (计数>=2) 即证明"legacy 向量分支 + 钩子 EOI"可持续, + * 这正是键盘/鼠标/网卡将要走的路径 (阶段1 的 0xE0+自EOI 无法证明此路径)。 + * handler 是普通 C 函数 (SysV ABI), isr stub 已完成现场保存/iret。 + */ +static int apic_tick_handler(u8 vector) { + (void)vector; + g_tick_count++; + if (g_tick_count <= 8) { + g_log->info("[apic] ioapic tick (PIT GSI -> LAPIC -> IDT vec 0x20, hook EOI)"); + } + return 0; +} + +static u64 rdtsc(void) { + u32 lo, hi; + __asm__ volatile("rdtsc" : "=a"(lo), "=d"(hi)); + return ((u64)hi << 32) | lo; +} + +static u64 g_tsc_per_ms; + +/* PIT ch0 mode 0 单发 (~10ms, 11932 counts) 校准 TSC。 + * 必须在把 PIT 改为 mode 3 周期模式之前调用。 */ +static void tsc_calibrate(void) { + outb(0x43, 0x30); /* ch0, lo+hi, mode 0, binary */ + outb(0x40, 0x9C); /* 11932 low = ~10ms */ + outb(0x40, 0x2E); /* 11932 high */ + u64 tsc_start = rdtsc(); + u16 prev = 0; + u64 loops = 0; + for (;;) { + outb(0x43, 0x00); + u16 cur = (u16)inb(0x40) | ((u16)inb(0x40) << 8); + if (cur > prev && loops > 10) break; /* 回绕 = 10ms 到 */ + prev = cur; + loops++; + } + u64 tsc_end = rdtsc(); + g_tsc_per_ms = (tsc_end - tsc_start) / 10; +} + +static void tsc_delay_ms(u32 ms) { + if (!g_tsc_per_ms) { + for (volatile u32 i = 0; i < 100000 * ms; i++) __asm__ volatile("pause"); + return; + } + u64 target = g_tsc_per_ms * ms; + u64 start = rdtsc(); + while (rdtsc() - start < target) __asm__ volatile("pause"); +} + +/* ---- B7 阶段2: 路由切换主流程 ---- * + * 成功: PIC IMR 全 1, IOAPIC 拥有路由, EOI 钩子保持注册, tick 验证后 + * tick 线重新 mask (保留路由表内容), legacy 线按 apic_route_legacy。 + * 失败: IOAPIC 全 mask + 恢复原 PIC IMR + 注销 tick handler + 注销 EOI + * 钩子, 与切换前状态完全一致 (现状不回归)。 + */ +static void apic_route_switch(const struct dkm_kernel_api *api, + struct madt_info *info) { + g_log->info("[apic] apic_route=1: attempting IOAPIC routing takeover"); + + if (!g_lapic) { + g_log->warn("[apic] route abort: LAPIC MMIO unavailable; PIC retained"); + return; + } + if (info->ioapic_count == 0) { + g_log->warn("[apic] route abort: no IOAPIC; PIC retained"); + return; + } + if (!api->irq_register) { + g_log->warn("[apic] route abort: irq_register missing; PIC retained"); + return; + } + + /* 0) B7 阶段2 前提: EOI 钩子接口必须存在 (旧内核无此字段 → 判空中止)。 + * 无钩子时 legacy 向量 (0x20-0x2F) 在 IOAPIC 路由下只能 one-shot, + * 路由接管不成立 — 宁可保留 PIC 也不进入半残状态。 */ + if (!api->register_apic_eoi) { + g_log->warn("[apic] route abort: register_apic_eoi missing (old kernel); PIC retained"); + return; + } + api->register_apic_eoi(lapic_eoi); + g_log->info("[apic] LAPIC EOI hook registered to UTSM"); + + /* 1) 保存 PIC IMR (降级恢复用) */ + u8 imr1 = inb(0x21); + u8 imr2 = inb(0xA1); + log_hex("[apic] PIC IMR saved master=", imr1); + log_hex("[apic] PIC IMR saved slave=", imr2); + + /* 2) TSC 校准 (PIT 尚在 firmware/driver 遗留状态, 先校准再改模式)。 + * diag: HPET 绝对时基对拍本窗口 — 校准窗口标称 10ms, HPET 实测 + * 若 ~20000us 则 PIT mode0 实际跑了 20ms (TSC 测半), ~10000us + * 则校准正确, tick 2x 根因另在他处。 */ + int hpet_ok = 0; + if (g_cfg.apic_diag) { + hpet_ok = (hpet_diag_init(api) == 0); + } + u32 hc0 = 0; + if (hpet_ok) hc0 = hpet_main32(); + tsc_calibrate(); + if (hpet_ok) { + u32 hc1 = hpet_main32(); + u64 calib_us = hpet_delta_us(hc0, hc1); + log_dec("[apic] diag: calib window(us)=", calib_us); + g_log->info("[apic] diag: (nominal 10000us; ~20000 => TSC half-measured)"); + } + log_hex("[apic] tsc_per_ms=", g_tsc_per_ms); + + /* 3) 注册 tick handler @ vector 0x20 — PIC 分支 (legacy 向量路径), + * handler 返回 0, LAPIC EOI 由 UTSM 钩子完成 */ + if (api->irq_register(0x20, (void *)(uintptr_t)apic_tick_handler) != 0) { + g_log->warn("[apic] route abort: irq_register(0x20) failed"); + api->register_apic_eoi(NULL); + return; + } + g_log->info("[apic] tick handler registered at vector 0x20 (PIC branch, hook EOI)"); + + /* 4) PIT tick GSI 解析: MADT ISO source 0 (QEMU q35: GSI2), 缺省恒等 */ + u16 tick_flags = 0; + u32 tick_gsi = isa_irq_to_gsi(0, &tick_flags, info->isos, info->iso_count); + log_hex("[apic] tick GSI=", tick_gsi); + log_hex("[apic] tick GSI flags=", tick_flags); + + /* 5) 编程 tick 重定向项 (vector 0x20, 放开) */ + if (ioapic_program_gsi(info, tick_gsi, 0x20, tick_flags, 1) != 0) { + g_log->warn("[apic] route abort: tick GSI not covered by any IOAPIC"); + api->irq_register(0x20, (void *)0); + api->register_apic_eoi(NULL); + return; + } + g_log->info("[apic] tick redirection unmasked (vec 0x20)"); + + /* 6) legacy 线 (apic_route_legacy): EOI 钩子已就位, 持续投递成立。 + * ISA 线 (IRQ1/12) 按 ISO, 缺省 edge/active-high; + * PCI 线 (IRQ11 e1000) 无 ISO 覆盖时强制 level/active-low + * (PCI INTx 规范; flags=0xF = pol bit0-1=3 active-low + trig bit2-3=3 + * level)。e1000 handler 读 ICR 清源 → 电平 deassert → 钩子 EOI。 */ + if (g_cfg.apic_route_legacy) { + u16 f1 = 0, f12 = 0, f11 = 0; + u32 gsi1 = isa_irq_to_gsi(1, &f1, info->isos, info->iso_count); + u32 gsi12 = isa_irq_to_gsi(12, &f12, info->isos, info->iso_count); + u32 gsi11 = isa_irq_to_gsi(11, &f11, info->isos, info->iso_count); + if (!f11) f11 = 0x0F; + ioapic_program_gsi(info, gsi1, 0x21, f1, 1); /* 键盘 */ + ioapic_program_gsi(info, gsi12, 0x2C, f12, 1); /* 鼠标 */ + ioapic_program_gsi(info, gsi11, 0x2B, f11, 1); /* e1000 (QEMU 默认 IRQ11) */ + g_log->info("[apic] legacy IRQ1/12/11 unmasked (sustained delivery via EOI hook)"); + log_hex("[apic] kbd gsi=", gsi1); + log_hex("[apic] mouse gsi=", gsi12); + log_hex("[apic] e1000 gsi=", gsi11); + log_hex("[apic] e1000 flags=", f11); + } + + /* 7) LAPIC TPR=0 全放行 */ + g_lapic[0x80 / 4] = 0; + apic_mb(); + log_hex("[apic] LAPIC TPR=", g_lapic[0x80 / 4]); + + /* 8) PIC 全屏蔽 (先 slave 后 master, 避免 cascade 窗口; QEMU 无功能 IMCR) */ + outb(0xA1, 0xFF); + io_wait(); + outb(0x21, 0xFF); + io_wait(); + g_log->info("[apic] PIC fully masked; IOAPIC owns IRQ routing"); + + /* 9) PIT ch0 mode 3 周期 ~100Hz (divisor 11932), 产生真实 tick 源 */ + outb(0x43, 0x36); + outb(0x40, 0x9C); + outb(0x40, 0x2E); + g_log->info("[apic] PIT ch0 mode3 100Hz started"); + + /* 10) 500ms 验证窗口 (+ diag: HPET 实测窗口与 tick 频率定论) */ + u32 hv0 = 0; + if (hpet_ok) hv0 = hpet_main32(); + u32 before = g_tick_count; + tsc_delay_ms(500); + u32 ticks = g_tick_count - before; + if (hpet_ok) { + u32 hv1 = hpet_main32(); + u64 win_us = hpet_delta_us(hv0, hv1); + log_dec("[apic] diag: verify window(us)=", win_us); + if (win_us) { + u64 hz_x10 = (u64)ticks * 10000000ull / win_us; + log_dec("[apic] diag: tick rate(Hz x10)=", hz_x10); + } + /* tick 2x 定论分支: + * win≈500ms 且 ticks≈100 → PIT mode3 在 IOAPIC edge 下双触发 (200Hz 真实) + * win≈1000ms 且 ticks≈100 → TSC 校准窗口翻倍 (tick 频率真实 100Hz) + * win≈500ms 且 ticks≈50 → 一切正常 */ + if (win_us > 750000) { + g_log->info("[apic] diag: TICK2X VERDICT: TSC calib window doubled (PIT mode0 ran ~20ms); tick rate nominal"); + } else if (ticks > 75) { + g_log->info("[apic] diag: TICK2X VERDICT: PIT mode3 double-clocked on IOAPIC edge (~200Hz real)"); + } else { + g_log->info("[apic] diag: TICK2X VERDICT: nominal (~100Hz, no 2x)"); + } + } + log_dec("[apic] route verify ticks=", ticks); + + if (ticks >= 2) { + /* 成功: 重复投递证明钩子 EOI 正确清除 ISR 位, 路由链路完整 */ + g_log->info("[apic] ROUTE VERIFY OK — IOAPIC->LAPIC->IDT(vec 0x20)->handler->hook EOI chain proven"); + + /* diag: 第二窗口持续投递证据 (排除窗口内偶发两次的弱证明) */ + if (g_cfg.apic_diag) { + u32 before2 = g_tick_count; + tsc_delay_ms(500); + u32 ticks2 = g_tick_count - before2; + log_dec("[apic] diag: sustain window ticks=", ticks2); + if (ticks2 >= 2) { + g_log->info("[apic] diag: SUSTAINED delivery confirmed (2nd window)"); + } + } + + /* tick 线重新 mask (保留路由表, vector 保持 0x20), 避免后台周期中断; + * PIT 恢复 mode 3 @ 18.2Hz (BIOS 惯例值, 已被 mask 不会投递)。 + * EOI 钩子保持注册 — legacy 线的持续投递依赖它。 */ + ioapic_program_gsi(info, tick_gsi, 0x20, tick_flags, 0); + outb(0x43, 0x36); + outb(0x40, 0x00); + outb(0x40, 0x00); + g_route_active = 1; + g_log->info("[apic] APIC ROUTING ACTIVE (PIC masked; EOI hook live; tick line re-masked)"); + } else { + /* 失败: 完整降级回 PIC (含注销 EOI 钩子) */ + g_log->error("[apic] ROUTE VERIFY FAILED — degrading to PIC routing"); + ioapic_mask_all(info); + api->irq_register(0x20, (void *)0); + api->register_apic_eoi(NULL); + outb(0x43, 0x36); + outb(0x40, 0x00); + outb(0x40, 0x00); + outb(0x21, imr1); + io_wait(); + outb(0xA1, imr2); + io_wait(); + g_log->info("[apic] PIC routing restored (IMR restored, IOAPIC masked, hook cleared)"); + } +} + +/* ---- B7 diag: IRQ1/IRQ12 键鼠流观察窗 ---- * + * 在 route switch 之后调用 (route 成功走 IOAPIC; 否则走 PIC IMR)。 + * 打锚点日志供测试脚本/人工在窗口内用 QEMU monitor sendkey/mouse_move + * 注入输入; 窗口结束分别报告收包计数。计数 >1 即证明该线在当前路由 + * 模式下可持续投递 (一次按键=make+break 两包, 一次移动=多包)。 */ +static void apic_diag_mouse_window(const struct dkm_kernel_api *api, + struct madt_info *info) { + g_log->info("[apic] diag: IRQ1/IRQ12 input stream check begin"); + + /* 确保 TSC 延时可用 (route=0 时未校准: 现场校准并恢复 PIT 惯例状态) */ + if (!g_tsc_per_ms) { + tsc_calibrate(); + outb(0x43, 0x36); + outb(0x40, 0x00); + outb(0x40, 0x00); + } + + if (!api->irq_register) return; + + if (g_route_active) { + /* IOAPIC 路由: 确保 IRQ12/IRQ1 重定向项放开 (legacy=1 时幂等重编) */ + u16 f12 = 0, f1 = 0; + u32 gsi12 = isa_irq_to_gsi(12, &f12, info->isos, info->iso_count); + u32 gsi1 = isa_irq_to_gsi(1, &f1, info->isos, info->iso_count); + ioapic_program_gsi(info, gsi12, 0x2C, f12, 1); + ioapic_program_gsi(info, gsi1, 0x21, f1, 1); + g_log->info("[apic] diag: IRQ12+IRQ1 via IOAPIC redirection"); + } else { + /* PIC 路由: 解 slave IMR bit4 (IRQ12); master IMR bit1 (IRQ1) + * UTSM pic_remap 已留 0xFD (IRQ1 enable), 幂等确认。 + * diag 属实验开关, 显式改变 PIC 状态并打日志; UTSM PIC 分支 + * 会做 8259 EOI, diag handler 返回 0 即可。 */ + outb(0xA1, (u8)(inb(0xA1) & (u8)~0x10u)); + outb(0x21, (u8)(inb(0x21) & (u8)~0x02u)); + g_log->info("[apic] diag: IRQ12+IRQ1 via PIC (IMR bits cleared)"); + } + + /* 控制器交互必须全轮询完成: 期间到达的 IRQ (KBD bit0 一直开着) + * 因 handler 未注册只会被 EOI 丢弃, 不会偷走命令响应字节。 + * (实测教训: 先注册 handler 时, 0x20 读 cfg 的响应被 kbd handler + * 当扫描码读走, 轮询侧 cfg read timeout) */ + ps2_aux_enable_stream(); + + /* 交互全部完成, 现在才注册诊断 handler */ + if (api->irq_register(0x2C, (void *)(uintptr_t)apic_mouse_diag_handler) != 0) { + g_log->warn("[apic] diag: irq_register(0x2C) failed"); + return; + } + if (api->irq_register(0x21, (void *)(uintptr_t)apic_kbd_diag_handler) != 0) { + g_log->warn("[apic] diag: irq_register(0x21) failed"); + return; + } + + /* 6000ms 窗口: 注入方需经 "串口日志落盘(块缓冲) → 测试脚本轮询" 的 + * 可见性延迟 (~1s 量级), 3s 窗口实测会被注入延迟吃光 */ + g_log->info("[apic] diag: INPUT OBSERVE WINDOW 6000ms — sendkey/mouse_move now"); + u32 m_before = g_mouse_pkt_count; + u32 k_before = g_kbd_pkt_count; + tsc_delay_ms(6000); + u32 mpkts = g_mouse_pkt_count - m_before; + u32 kpkts = g_kbd_pkt_count - k_before; + log_dec("[apic] diag: mouse packets observed=", mpkts); + log_dec("[apic] diag: kbd packets observed=", kpkts); + if (mpkts > 1) { + g_log->info("[apic] diag: IRQ12 SUSTAINED delivery confirmed"); + } else if (mpkts == 1) { + g_log->warn("[apic] diag: IRQ12 single packet only (check EOI path)"); + } else { + g_log->info("[apic] diag: no mouse packets (inject via monitor mouse_move)"); + } + if (kpkts > 1) { + g_log->info("[apic] diag: IRQ1 SUSTAINED delivery confirmed"); + } else if (kpkts == 1) { + g_log->warn("[apic] diag: IRQ1 single packet only (check EOI path)"); + } else { + g_log->info("[apic] diag: no kbd packets (inject via monitor sendkey)"); + } + /* handler 保持注册: stage3 ps2kbd 经 irq_register(1) 覆盖键盘 handler, + * DSK mouseInit 经 irq_register(12) 覆盖鼠标 handler, 均无冲突; + * 重定向项/IMR 保持现状 (ps2kbd/mouseInit 有完整初始化序列)。 */ +} + __attribute__((visibility("default"))) int driver_init(const struct dkm_kernel_api *api, struct dkm_driver_handle *handle) { @@ -575,6 +1456,9 @@ int driver_init(const struct dkm_kernel_api *api, } g_log->info("[apic] kernel IRQ backend present: PIC"); + /* B7: FUCK 配置自解析 (lapic_enable/ioapic_init/apic_route/apic_route_legacy/apic_diag) */ + load_fuck_config(api); + if (!cpu_has_lapic()) { g_log->warn("[apic] CPUID local APIC not present; PIC fallback active"); g_log->info("[apic] driver ready"); @@ -612,29 +1496,60 @@ int driver_init(const struct dkm_kernel_api *api, } if (api->hhdm_offset && info.lapic_phys) { - volatile u32 *lapic = (volatile u32 *)(uintptr_t)(api->hhdm_offset + info.lapic_phys); - log_hex("[apic] LAPIC ID reg=", lapic[0x20 / 4]); - log_hex("[apic] LAPIC version reg=", lapic[0x30 / 4]); - log_hex("[apic] LAPIC SVR reg=", lapic[0xF0 / 4]); + g_lapic = (volatile u32 *)(uintptr_t)(api->hhdm_offset + info.lapic_phys); + g_lapic_id = g_lapic[0x20 / 4] >> 24; + log_hex("[apic] LAPIC ID reg=", g_lapic[0x20 / 4]); + log_hex("[apic] LAPIC version reg=", g_lapic[0x30 / 4]); + log_hex("[apic] LAPIC SVR reg=", g_lapic[0xF0 / 4]); + log_hex("[apic] LAPIC TPR reg=", g_lapic[0x80 / 4]); + log_hex("[apic] BSP LAPIC ID=", g_lapic_id); } else { g_log->warn("[apic] HHDM unavailable; skip LAPIC MMIO read"); + g_lapic = (volatile u32 *)0; } /* ---- LAPIC 使能(真机必需,QEMU 也可安全执行) ---- */ - if (api->hhdm_offset && info.lapic_phys) { - lapic_enable(api->hhdm_offset, info.lapic_phys); + if (g_lapic && g_cfg.lapic_enable) { + lapic_enable(); + } else if (!g_cfg.lapic_enable) { + g_log->info("[apic] lapic_enable=0: skip LAPIC software enable"); } /* ---- IOAPIC 初始化(真机必需,QEMU 也可安全执行) ---- */ - for (u32 i = 0; i < info.ioapic_count; i++) { - g_log->info("[apic] initializing IOAPIC"); - log_hex("[apic] index=", i); - ioapic_init_one(&info.ioapics[i], api->hhdm_offset, - info.isos, info.iso_count, - api->mm_map_mmio); + if (g_cfg.ioapic_init) { + for (u32 i = 0; i < info.ioapic_count; i++) { + g_log->info("[apic] initializing IOAPIC"); + log_hex("[apic] index=", i); + ioapic_init_one(&info.ioapics[i], api->hhdm_offset, + info.isos, info.iso_count, + api->mm_map_mmio); + } + } else { + g_log->info("[apic] ioapic_init=0: skip IOAPIC programming (PIC mode)"); } - g_log->info("[apic] discovery + init complete; PIC routing retained as fallback"); + /* ---- B7: 路由接管 (默认关, FUCK apic_route=1 启用) ---- */ + if (g_cfg.apic_route && g_cfg.ioapic_init) { + apic_route_switch(api, &info); + } else { + g_log->info("[apic] apic_route=0: PIC routing retained (default)"); + } + + /* ---- B7 diag: IRQ12 鼠标流观察窗 (apic_diag=1, 两种路由模式皆可测) ---- */ + if (g_cfg.apic_diag) { + apic_diag_mouse_window(api, &info); + } + + /* 重定向表回读 (验证证据; 无 IOAPIC 时无输出) */ + if (g_cfg.ioapic_init) { + ioapic_dump_table(&info); + } + + if (g_route_active) { + g_log->info("[apic] discovery + init complete; APIC routing active"); + } else { + g_log->info("[apic] discovery + init complete; PIC routing retained as fallback"); + } g_log->info("[apic] driver ready"); return 0; } diff --git a/CODE/DKM/dkm_shared.h b/CODE/DKM/dkm_shared.h index 1d5b79b..47c5515 100644 --- a/CODE/DKM/dkm_shared.h +++ b/CODE/DKM/dkm_shared.h @@ -193,6 +193,11 @@ struct dkm_kernel_api { /* —— 新增字段只允许追加在末尾(DSK 以 api+0xA8 硬偏移读取 block) —— */ void *(*mm_map_mmio)(u64 phys, u64 size); /* MMIO 独立窗口映射(PCD|PWT),返回虚拟地址,失败返回 0 */ void (*mm_unmap_mmio)(void *virt, u64 size); /* 解除 MMIO 独立窗口映射 */ + /* B7 阶段2 尾部追加(旧内核无此字段 → 判空使用) */ + void (*register_apic_eoi)(void (*eoi_fn)(void)); + /* B7 阶段3 尾部追加:动态 IDT 向量分配器(MSI/MSI-X),池 0x40-0xDF */ + int (*irq_vector_alloc)(void); + void (*irq_vector_free)(int vector); }; struct dkm_driver_handle; diff --git a/CODE/DKM/nvme/nvme.c b/CODE/DKM/nvme/nvme.c index 22fc271..d0557c1 100644 --- a/CODE/DKM/nvme/nvme.c +++ b/CODE/DKM/nvme/nvme.c @@ -6,8 +6,17 @@ * 1. 64-bit BAR 常位于 4G 以上(QEMU/OVMF 把 NVMe BAR0 放到 ~48GiB), * HHDM 直映射不保证覆盖。访问前先用 kernel_api.mmio->is_mapped 做 * 四级页表 walk 核实;未映射则 map_mmio 原地补建 4KiB UC 页。 - * 2. 同步轮询模型: 单 outstanding 命令 + CQE phase bit 轮询 + TSC 超时, - * 不依赖 IRQ/MSI(当前 PIC/APIC 后端未完工)。 + * 2. 完成模型: 单 outstanding 命令 + CQE phase bit。B7 阶段3 起 IO 队列 + * 默认尝试 MSI/MSI-X 中断驱动(FUCK [drivers] nvme_msi, 默认 1 自动): + * 先 MSI cap 编程, 缺则 MSI-X (QEMU nvme 只暴露 MSI-X) — 表项 + * addr=0xFEE00000|dest<<12, data=动态向量, entry unmask + cap Enable; + * Create IOCQ IEN=1/IV=0 + INTMC 解除 IV0; handler 摘 CQE 进完成槽、 + * 推进 head、写 CQ doorbell, 自写 LAPIC EOI 并返回 1 (MSI 直投 LAPIC, + * 与 apic_route 开关无关, 不依赖阶段2 的 EOI 钩子注册状态)。 + * 提交侧统一等待: 先查完成槽(中断路径), 再在 irqsave 临界区内手动 + * 摘取(自适应轮询路径) — IF=0 (DSK 阶段) 或 MSI 未投递时零感知降级, + * 行为与旧版纯轮询一致。计数: g_msi_irq_count / g_msi_slot_hits / + * g_msi_poll_harvests / g_msi_timeouts, init 末尾与压测后打点。 * 3. 数据面: 32 页 (128KiB) 物理连续 DMA bounce buffer + 单页 PRP list。 * PRP 布局按传输页数分档: 1 页单 PRP1; 2 页 PRP2 直连第二页; * >=3 页 PRP2 指向 PRP list 页 (512 项 u64, 上限 513 页 = 2052KiB)。 @@ -32,7 +41,7 @@ const struct dkm_driver_desc driver_desc = { .abi_version = DKM_ABI_VERSION, .desc_size = sizeof(struct dkm_driver_desc), .name = "nvme", - .version = "0.3.0", + .version = "0.4.0", .vendor = "Deshab", .driver_class = 6, /* DKM_CLASS_STORAGE */ .stage = 1, @@ -110,6 +119,24 @@ static u32 g_chunk_max_sectors; /* 单命令扇区上限 (buffer/MDTS/NL static int g_io_ready; static int g_test_disk; /* LBA0 带 DESHABNVME0 签名的测试盘 */ +/* ---- B7 阶段3: MSI 状态 ---- */ +static int g_cfg_nvme_msi = 1; /* FUCK [drivers] nvme_msi, 默认 1 自动尝试 */ +static int g_msi_enabled; /* MSI/MSI-X 编程成功且 handler 已注册 */ +static int g_msix_mode; /* 1=MSI-X 表项模式, 0=MSI cap 模式 */ +static int g_msi_vector = -1; /* 动态分配的 IDT 向量 (0x40-0xDF 池) */ +/* 完成槽: 单 outstanding 前提下, handler 是唯一写者、提交侧是唯一读者, + * 且下一命令提交前不可能再有新 CQE, 故读-清无需互斥; valid 最后写/最先读, + * x86 TSO store-store / load-load 天然有序, nvme_mb 只挡编译器重排。 */ +static volatile u32 g_slot_valid; +static volatile u32 g_slot_status; /* sc | (sct << 8), 0 = 成功 */ +static volatile u32 g_slot_dw0; /* CQE dw0 (命令特定返回) */ +static volatile u32 g_slot_cid; /* CQE dw3 低 16 位 CID, 提交侧比对防错位 */ +static u64 g_msi_irq_count; /* MSI handler 进入次数 (投递证明) */ +static u64 g_msi_slot_hits; /* 提交侧经完成槽完成的命令数 (中断路径) */ +static u64 g_msi_poll_harvests; /* 提交侧 irqsave 手动摘取数 (轮询路径) */ +static u64 g_msi_timeouts; /* 完成超时计数 (真故障, 非降级) */ +static u64 g_msi_cid_mismatch; /* 摘到 CID 与提交 cid 不符的 CQE 计数 (队列错位警报) */ + /* ---- 基础工具 ---- */ static void log_hex(const char *prefix, u64 value) { @@ -149,6 +176,66 @@ static __inline__ void nvme_mb(void) { __asm__ volatile("" ::: "memory"); } +/* 十进制打印 (MSI 计数器打点用, 比 hex 直观) */ +static void log_dec(const char *prefix, u64 val) { + char buf[24]; + u32 pos = 0; + if (!val) { + buf[pos++] = '0'; + } else { + char tmp[20]; + u32 n = 0; + while (val) { tmp[n++] = (char)('0' + (val % 10)); val /= 10; } + while (n) buf[pos++] = tmp[--n]; + } + buf[pos] = 0; + g_log->info(prefix); + g_log->info(buf); +} + +static int memeq(const char *a, const char *b, u32 n) { + for (u32 i = 0; i < n; i++) { + if (a[i] != b[i]) return 0; + } + return 1; +} + +static u32 str_len(const char *s) { + u32 n = 0; + while (s[n]) n++; + return n; +} + +/* 在 NUL 结尾字符串中查找子串 (boot module cmdline 匹配用) */ +static int str_contains(const char *hay, const char *needle) { + u32 nlen = str_len(needle); + if (!nlen) return 1; + for (u32 i = 0; hay[i]; i++) { + u32 j = 0; + while (j < nlen && hay[i + j] && hay[i + j] == needle[j]) j++; + if (j == nlen) return 1; + } + return 0; +} + +static void nvme_cpuid(u32 leaf, u32 *a, u32 *b, u32 *c, u32 *d) { + __asm__ volatile ("cpuid" + : "=a"(*a), "=b"(*b), "=c"(*c), "=d"(*d) + : "a"(leaf), "c"(0)); +} + +/* irqsave 临界区: 提交侧手动摘取 CQE 时屏蔽中断, 与 MSI handler 互斥。 + * pushf/popf 配对恢复 IF — DSK 阶段 IF=0, 绝不能无条件 sti。 */ +static __inline__ u64 nvme_irqsave(void) { + u64 flags; + __asm__ volatile("pushfq; popq %0; cli" : "=r"(flags) :: "memory"); + return flags; +} + +static __inline__ void nvme_irqrestore(u64 flags) { + __asm__ volatile("pushq %0; popfq" :: "r"(flags) : "memory", "cc"); +} + static u32 mmio_read32(u32 off) { return *(volatile u32 *)(void *)(g_regs + off); } @@ -204,6 +291,138 @@ static int nvme_find(u8 *out_bus, u8 *out_dev, u8 *out_func) { return -1; } +/* ---- FUCK 配置自解析 (与 apic.c 同一范式, 不依赖 UTSM ini_parser) ---- + * UTSM 经 Limine boot module (cmdline="fuck:config") 预加载 FUCK; + * kernel_api.boot_modules_response 对所有 DKM 驱动可见。 + * 此处只找 [drivers] 分区下的 nvme_msi 键; 缺失文件/键一律默认 1 + * (自动尝试 MSI, 失败回轮询), 与无配置时行为一致。 + */ + +/* Limine boot module 最小类型 (与 apic.c / bootfs.c / UTSM limine.h 布局一致) */ +struct limine_file_min { + u64 revision; + void *address; + u64 size; + char *path; + char *cmdline; + u32 media_type; + u32 unused; + u32 tftp_ip; + u32 tftp_port; + u32 partition_index; + u32 mbr_disk_id; + u8 gpt_disk_uuid[16]; + u8 gpt_part_uuid[16]; + u8 part_uuid[16]; +}; + +struct limine_module_response_min { + u64 revision; + u64 module_count; + struct limine_file_min **modules; +}; + +/* 解析值 token: 支持 0/1/true/false/on/off/yes/no 及十进制整数 */ +static int parse_scalar(const char *line, u64 start, u64 line_len, int defval) { + u64 i = start; + while (i < line_len && (line[i] == ' ' || line[i] == '\t')) i++; + u64 tok_start = i; + while (i < line_len && line[i] != ' ' && line[i] != '\t' && + line[i] != '#' && line[i] != ';' && line[i] != '\r') i++; + u64 tok_len = i - tok_start; + const char *tok = line + tok_start; + if (!tok_len) return defval; + if (tok_len == 1 && tok[0] == '1') return 1; + if (tok_len == 1 && tok[0] == '0') return 0; + if (tok_len == 4 && memeq(tok, "true", 4)) return 1; + if (tok_len == 5 && memeq(tok, "false", 5)) return 0; + if (tok_len == 2 && memeq(tok, "on", 2)) return 1; + if (tok_len == 3 && memeq(tok, "off", 3)) return 0; + if (tok_len == 3 && memeq(tok, "yes", 3)) return 1; + if (tok_len == 2 && memeq(tok, "no", 2)) return 0; + int neg = 0; + u64 j = 0; + if (tok[0] == '-') { neg = 1; j = 1; } + int val = 0; + int any = 0; + for (; j < tok_len; j++) { + if (tok[j] < '0' || tok[j] > '9') return defval; + val = val * 10 + (tok[j] - '0'); + any = 1; + } + if (!any) return defval; + return neg ? -val : val; +} + +static int fuck_get_int(const char *text, u64 size, + const char *section, const char *key, int defval) { + u64 pos = 0; + int in_section = 0; + u32 slen = str_len(section); + u32 klen = str_len(key); + + while (pos < size) { + u64 end = pos; + while (end < size && text[end] != '\n') end++; + u64 line_len = end - pos; + if (line_len > 0 && text[pos + line_len - 1] == '\r') line_len--; + const char *line = text + pos; + + u64 i = 0; + while (i < line_len && (line[i] == ' ' || line[i] == '\t')) i++; + if (i < line_len) { + char c = line[i]; + if (c == '#' || c == ';') { + /* 注释行, 跳过 */ + } else if (c == '[') { + in_section = 0; + if (line_len >= (u64)(i + 1 + slen + 1) && + memeq(line + i + 1, section, slen) && + line[i + 1 + slen] == ']') { + in_section = 1; + } + } else if (in_section) { + if (line_len >= (u64)(i + klen) && + memeq(line + i, key, klen)) { + u64 j = i + klen; + while (j < line_len && (line[j] == ' ' || line[j] == '\t')) j++; + if (j < line_len && line[j] == '=') { + return parse_scalar(line, j + 1, line_len, defval); + } + } + } + } + pos = end + 1; + } + return defval; +} + +static void nvme_load_config(const struct dkm_kernel_api *api) { + /* 编译期默认: 自动尝试 MSI (任何前提缺失都会回退纯轮询, 零回归) */ + g_cfg_nvme_msi = 1; + + const struct limine_module_response_min *resp = + (const struct limine_module_response_min *)api->boot_modules_response; + if (!resp || !resp->modules) { + g_log->warn("[nvme] no boot modules; FUCK defaults in effect"); + return; + } + + for (u64 i = 0; i < resp->module_count; i++) { + struct limine_file_min *f = resp->modules[i]; + if (!f || !f->cmdline || !f->address || !f->size) continue; + if (!str_contains(f->cmdline, "fuck:config")) continue; + + g_cfg_nvme_msi = fuck_get_int((const char *)f->address, f->size, + "drivers", "nvme_msi", 1); + g_log->info("[nvme] FUCK config module found"); + log_dec("[nvme] cfg nvme_msi=", (u64)g_cfg_nvme_msi); + return; + } + + g_log->warn("[nvme] FUCK module (fuck:config) not found; defaults in effect"); +} + /* ---- CSTS 等待 ---- */ static int nvme_wait_csts(u32 mask, u32 expect, u64 timeout_ms) { @@ -229,6 +448,142 @@ static int nvme_wait_csts(u32 mask, u32 expect, u64 timeout_ms) { /* 前向声明: nvme_dump_regs 在 nvme_submit 中使用 */ static void nvme_dump_regs(const char *ctx); +/* ---- B7 阶段3: IO CQ 完成摘取 (MSI handler 与提交侧轮询共用) ---- + * 返回 1 = 摘到一枚 CQE; 0 = 无新完成。 + * out_cid 返回 CQE dw3 低 16 位的命令 ID, 供调用方与提交 cid 比对 — + * 单 outstanding 模型下 CQ 任何时刻只应有当前命令的一枚 CQE, CID 不匹配 + * 即队列状态错位的硬证据, 必须显式报错而非静默当作当前命令完成 + * (route=0 曾观测 stress readback MISMATCH @0x9000, 疑 CQE 提前消费)。 + * 并发模型: 单生产者(控制器 DMA 写 CQ) + 单逻辑消费者, 消费者有两个入口 —— + * 1. MSI handler (IRQ 上下文): 同一 vector 在 EOI 前不会被 LAPIC 再次 + * 投递, handler 对同向量天然不可重入, 无需自锁; + * 2. 提交侧统一等待 (主上下文): 必须在 nvme_irqsave 临界区内调用, + * 屏蔽中断期间 handler 不可能插入, 消除 "phase 判定后、head 推进前 + * 被 handler 抢摘同一 CQE" 的双消费竞态。 + * head 推进 + CQ doorbell 只在此函数内发生, 是两个入口的唯一汇合点。 + */ +static int nvme_iocq_harvest(u32 *out_status, u32 *out_dw0, u32 *out_cid) { + volatile u32 *cq = (volatile u32 *)g_iocq.virt; + u32 dw3 = cq[g_iocq_head * 4 + 3]; + if (((dw3 >> 16) & 1u) != g_iocq_phase) return 0; + + if (out_dw0) *out_dw0 = cq[g_iocq_head * 4 + 0]; + if (out_cid) *out_cid = dw3 & 0xffffu; + u32 sc = (dw3 >> 17) & 0xffu; + u32 sct = (dw3 >> 25) & 0x7u; + /* SC 与 SCT 联合编码 (与 nvme_submit 经典路径同一约定): + * SCT!=0 且 SC==0 时只返回 sc 会被误判成功。 */ + *out_status = sc | (sct << 8); + + g_iocq_head = (g_iocq_head + 1) % IO_QSIZE; + if (g_iocq_head == 0) g_iocq_phase ^= 1u; + nvme_mb(); /* head 先于 doorbell 可见 */ + mmio_write32(cq_head_db(1), g_iocq_head); + return 1; +} + +/* MSI 中断 handler: 摘 CQE 进完成槽。 + * 返回 1 = 自负 LAPIC EOI (抑制 idt_handler 的钩子补 EOI)。 + * 为何自写 EOI 而不依赖钩子: MSI 是设备向 LAPIC 直投的消息中断, + * 投递路径不经过 IOAPIC/PIC, 与 apic_route 开关无关; 但 EOI 钩子只在 + * route=1 时注册, route=0 下钩子为 NULL 会漏写 EOI — LAPIC ISR 位 + * 残留将永久阻塞同优先级后续中断, MSI 静默退化为 one-shot。 + * handler 内直写 LAPIC EOI 寄存器后返回 1, 两种路由模式下行为一致。 + * LAPIC 未软件使能时该写为无害 no-op (此时 MSI 本就不会投递)。 + * 地址取 x86 默认 LAPIC base 0xFEE00000 + EOI 偏移 0xB0 (QEMU/MADT + * 一致; SAS-R0 恒等映射覆盖 4G 以下, apic.drv 已实证可直访)。 + * 时序说明: 即使提交侧轮询抢先摘走 CQE, 控制器中断信号早已发出, + * 本次 handler 仍会执行并计入 g_msi_irq_count — 投递证据不受竞态影响。 */ +static int nvme_msi_handler(u8 vector) { + (void)vector; + g_msi_irq_count++; + u32 st = 0, dw0 = 0, cid = 0; + if (nvme_iocq_harvest(&st, &dw0, &cid)) { + g_slot_status = st; + g_slot_dw0 = dw0; + g_slot_cid = cid; + nvme_mb(); /* payload 先于 valid 对提交侧可见 */ + g_slot_valid = 1; + } + *(volatile u32 *)(uintptr_t)0xFEE000B0u = 0; /* LAPIC EOI */ + return 1; +} + +/* IO 命令统一完成等待 (MSI 模式): 完成槽(中断路径)优先, irqsave 手动摘取 + * (自适应轮询路径)兜底。MSI 健康时 handler 在 CQE 落盘瞬间填槽; MSI 未投递 + * (LAPIC 未启用/IF=0/信号丢失) 时手动摘取沿原 phase 语义直接完成, 调用方 + * 零感知。超时语义与经典路径一致 (CAP.TO*500ms*10, 下限 5s)。 */ +static int nvme_io_wait_completion(u16 cid, u32 opcode, u32 *out_cqe_dw0) { + u64 submit_timeout_ms = g_timeout_ms * 10; + if (submit_timeout_ms < 5000) submit_timeout_ms = 5000; + u64 deadline = dkm_rdtsc() + dkm_tsc_per_ms * submit_timeout_ms; + + for (;;) { + /* 中断路径: handler 已摘取 CQE 进完成槽 */ + if (g_slot_valid) { + u32 st = g_slot_status; + u32 dw0 = g_slot_dw0; + u32 scid = g_slot_cid; + nvme_mb(); + g_slot_valid = 0; + g_msi_slot_hits++; + if (scid != (u32)cid) { + /* CID 错位: 槽内 CQE 不属于本命令 — 队列状态已损, + * 显式报错并继续等真 CQE (超时兜底), 绝不静默当成功 */ + g_msi_cid_mismatch++; + g_log->error("[nvme] CID mismatch (slot)"); + log_hex("[nvme] expect cid=", cid); + log_hex("[nvme] got cid=", scid); + continue; + } + if (out_cqe_dw0) *out_cqe_dw0 = dw0; + if (st) { + g_log->error("[nvme] command failed (msi slot)"); + log_hex("[nvme] opcode=", opcode); + log_hex("[nvme] cid=", cid); + log_hex("[nvme] status=", st); + return (int)st; + } + return 0; + } + + /* 自适应轮询路径: irqsave 临界区内手动摘取 (与 handler 互斥) */ + u32 st = 0, dw0 = 0, hcid = 0; + u64 flags = nvme_irqsave(); + int got = nvme_iocq_harvest(&st, &dw0, &hcid); + nvme_irqrestore(flags); + if (got) { + g_msi_poll_harvests++; + if (hcid != (u32)cid) { + g_msi_cid_mismatch++; + g_log->error("[nvme] CID mismatch (poll)"); + log_hex("[nvme] expect cid=", cid); + log_hex("[nvme] got cid=", hcid); + continue; + } + if (out_cqe_dw0) *out_cqe_dw0 = dw0; + if (st) { + g_log->error("[nvme] command failed"); + log_hex("[nvme] opcode=", opcode); + log_hex("[nvme] cid=", cid); + log_hex("[nvme] status=", st); + return (int)st; + } + return 0; + } + + if (dkm_rdtsc() > deadline) { + g_msi_timeouts++; + g_log->error("[nvme] completion timeout (msi mode)"); + log_hex("[nvme] opcode=", opcode); + log_hex("[nvme] cid=", cid); + nvme_dump_regs("completion timeout"); + return -100; + } + __asm__ volatile("pause"); + } +} + /* ---- 命令提交(同步轮询, 单 outstanding) ---- * dw[16] 为完整 SQE(dw0 低 16 位不含 CID, 由本函数填入)。 * 返回 0 成功;>0 = NVMe status code;<0 超时/内部错误。 @@ -239,6 +594,10 @@ static int nvme_submit(volatile u32 *sq, u32 *sq_tail, u16 sq_qid, dw[0] = (dw[0] & 0xffffu) | ((u32)g_cid << 16); u32 cid = g_cid; g_cid++; + /* CID=0 保留不用: QEMU 在 MSI-X unmask 边界会向 IO CQ 补投一枚 + * CID=0/phase 正确/status 成功的幽灵 CQE (实测稳定复现), g_cid 回绕 + * 跳过 0 可保证该幽灵 CQE 永远被 CID 校验拒绝, 绝无误判窗口。 */ + if (g_cid == 0) g_cid = 1; /* 逐 dword 写入 SQE(64B),避免 freestanding 下编译器生成 memcpy 调用 */ u32 pos = *sq_tail; @@ -248,6 +607,13 @@ static int nvme_submit(volatile u32 *sq, u32 *sq_tail, u16 sq_qid, nvme_mb(); /* SQ 内容先于 doorbell 可见 */ mmio_write32(sq_tail_db(sq_qid), *sq_tail); + /* B7 阶段3: IO 队列 MSI 模式 → 完成槽 + irqsave 摘取统一等待。 + * admin 队列保持纯轮询 (仅 init 期使用, 且 INTMC 解除前 IV0 处于 + * 屏蔽态, admin 完成不会触发 MSI)。 */ + if (cq_qid == 1 && g_msi_enabled) { + return nvme_io_wait_completion(cid, dw[0] & 0xffu, out_cqe_dw0); + } + /* 真机超时: 使用 CAP.TO * 500ms * 10 (留 10 倍余量), 下限 5s。 * 真机 NVMe 控制器在重负载下可能需要更长的完成时间。 */ u64 submit_timeout_ms = g_timeout_ms * 10; @@ -371,19 +737,277 @@ static int nvme_identify_namespace(u32 nsid, u64 *out_nsze, u32 *out_lbads) { return 0; } +/* ---- B7 阶段3: PCI MSI 能力编程 ---- + * MSI cap (ID 0x05) 布局: + * +0x00 u8 Cap ID (=0x05) +0x01 u8 Next Ptr + * +0x02 u16 Message Control: bit0 Enable, bit[3:1] MMC(可申请向量数-1), + * bit[6:4] MME(已分配向量数-1), bit7 64-bit Capable, bit8 PVM + * +0x04 u32 Message Address + * 64-bit 时: +0x08 u32 Message Upper Address, +0x0C u16 Message Data + * 32-bit 时: +0x08 u16 Message Data + * PVM(bit8)=1 时另有 Mask/Pending 寄存器, 复位值 0 (全部 unmask)。 + * x86 LAPIC 投递公式 (physical destination, fixed delivery): + * Address = 0xFEE00000 | (dest_apic_id << 12) (RH=0, DM=0) + * Data = vector | (0<<8 delivery=fixed) (level/trigger 对 MSI 无效) + */ +#define PCI_CAP_ID_MSI 0x05 +#define PCI_CAP_ID_MSIX 0x11 +#define PCI_STATUS_CAPLIST 0x10u + +static int nvme_find_pci_cap(u8 bus, u8 dev, u8 func, u8 cap_id) { + u32 reg04 = dkm_pci_read(bus, dev, func, PCI_COMMAND); + if (!((reg04 >> 16) & PCI_STATUS_CAPLIST)) return -1; /* Status.CapList=0 */ + u8 ptr = (u8)(dkm_pci_read(bus, dev, func, PCI_CAP_PTR) & 0xFC); + int guard = 0; + while (ptr && guard++ < 64) { /* guard 防配置空间坏链死循环 */ + u32 dw = dkm_pci_read(bus, dev, func, ptr); + u8 id = (u8)(dw & 0xff); + u8 next = (u8)((dw >> 8) & 0xff); + log_hex("[nvme] pci cap id=", id); + log_hex("[nvme] pci cap at=", ptr); + if (id == cap_id) return (int)ptr; + ptr = next & 0xFC; + } + return -1; +} + +/* MSI cap (0x05) 编程路径: 成功返回 1, cap 缺失或失败返回 0。 + * 失败路径完整回滚 (恢复 msg_ctl + 释放向量), 不留半编程状态。 */ +static int nvme_msi_program(const struct dkm_kernel_api *api, + u8 bus, u8 dev, u8 func) { + int cap = nvme_find_pci_cap(bus, dev, func, PCI_CAP_ID_MSI); + if (cap < 0) { + g_log->info("[nvme] MSI capability not present"); + return 0; + } + + u32 cdw = dkm_pci_read(bus, dev, func, (u8)cap); + u32 msg_ctl = (cdw >> 16) & 0xffffu; + int cap64 = (int)((msg_ctl >> 7) & 1u); + int pvm = (int)((msg_ctl >> 8) & 1u); + log_hex("[nvme] MSI msg_ctl=", msg_ctl); + log_hex("[nvme] MSI 64bit=", (u64)cap64); + log_hex("[nvme] MSI pvm=", (u64)pvm); + + /* 动态向量: UTSM 位图池 0x40-0xDF, 与静态向量 (tick 0xE0/spurious 0xFF) + * 及 legacy PIC 段 (0x20-0x2F) 无交叠。 */ + int vector = api->irq_vector_alloc(); + if (vector < 0 || vector > 255) { + g_log->warn("[nvme] MSI: vector alloc failed; polling"); + return 0; + } + log_dec("[nvme] MSI vector=", (u64)vector); + + /* 目的 APIC ID: CPUID.1:EBX[31:24] = BSP initial APIC ID (QEMU=0) */ + u32 ca, cb, cc_, cd; + nvme_cpuid(1, &ca, &cb, &cc_, &cd); + (void)ca; (void)cc_; (void)cd; + u32 dest = (cb >> 24) & 0xffu; + log_hex("[nvme] MSI dest apic id=", dest); + + u32 addr = 0xFEE00000u | (dest << 12); + u32 data = (u32)vector & 0xffu; /* fixed delivery, edge */ + + /* 先写 Address/Data, 最后置 Enable — 避免半成品消息被发出 */ + dkm_pci_write(bus, dev, func, (u8)(cap + 4), addr); + if (cap64) { + dkm_pci_write(bus, dev, func, (u8)(cap + 8), 0); /* upper addr */ + u32 dw = dkm_pci_read(bus, dev, func, (u8)(cap + 12)); + dw = (dw & 0xffff0000u) | data; /* data 为低 u16 */ + dkm_pci_write(bus, dev, func, (u8)(cap + 12), dw); + } else { + u32 dw = dkm_pci_read(bus, dev, func, (u8)(cap + 8)); + dw = (dw & 0xffff0000u) | data; + dkm_pci_write(bus, dev, func, (u8)(cap + 8), dw); + } + + /* Message Control: MME=0 (单向量), Enable=1; 低 u16 (id/next) 回写原值 */ + u32 mc = msg_ctl; + mc &= ~(0x7u << 4); /* MME=0 → 1 vector */ + mc |= 1u; /* MSI Enable */ + cdw = (cdw & 0x0000ffffu) | (mc << 16); + dkm_pci_write(bus, dev, func, (u8)cap, cdw); + + /* PVM: 显式清 mask bit0 (复位值即为 0, 此处防御性 RMW) */ + if (pvm) { + int mask_off = cap64 ? cap + 16 : cap + 12; + u32 mask = dkm_pci_read(bus, dev, func, (u8)mask_off); + mask &= ~1u; + dkm_pci_write(bus, dev, func, (u8)mask_off, mask); + log_hex("[nvme] MSI mask cleared=", mask); + } + + /* 回读验证: Enable 置位 + Address/Data 与编程值一致 */ + u32 rb_ctl = (dkm_pci_read(bus, dev, func, (u8)cap) >> 16) & 0xffffu; + u32 rb_addr = dkm_pci_read(bus, dev, func, (u8)(cap + 4)); + u32 rb_data = dkm_pci_read(bus, dev, func, + (u8)(cap64 ? cap + 12 : cap + 8)) & 0xffffu; + log_hex("[nvme] MSI readback ctl=", rb_ctl); + log_hex("[nvme] MSI readback addr=", rb_addr); + log_hex("[nvme] MSI readback data=", rb_data); + if (!(rb_ctl & 1u) || rb_addr != addr || (rb_data & 0xffu) != data) { + g_log->warn("[nvme] MSI: readback mismatch; rolling back"); + u32 rb = dkm_pci_read(bus, dev, func, (u8)cap); + rb = (rb & 0x0000ffffu) | (msg_ctl << 16); /* 恢复原 msg_ctl */ + dkm_pci_write(bus, dev, func, (u8)cap, rb); + api->irq_vector_free(vector); + return 0; + } + + /* 注册 handler (irq_register 已接受向量命名空间, >0x2F 直挂分发表) */ + api->irq_register((u8)vector, (void *)(uintptr_t)nvme_msi_handler); + + g_msi_enabled = 1; + g_msi_vector = vector; + g_log->info("[nvme] MSI enabled (IO CQ IEN=1, IV=0)"); + return 1; +} + +/* 按 BIR 读取 BAR 物理基址 (处理 64-bit BAR; IO BAR 返回 0) */ +static u64 nvme_bar_phys(u8 bus, u8 dev, u8 func, u32 bir) { + u8 reg = (u8)(PCI_BAR0 + bir * 4u); + u32 lo = dkm_pci_read(bus, dev, func, reg); + if (lo & 1u) return 0; /* IO BAR */ + if ((lo & 0x6u) == 0x4u) { /* 64-bit MMIO BAR */ + u32 hi = dkm_pci_read(bus, dev, func, (u8)(reg + 4)); + return ((u64)hi << 32) | (u64)(lo & 0xFFFFFFF0u); + } + return (u64)(lo & 0xFFFFFFF0u); +} + +/* MSI-X cap (0x11) 编程路径: 成功返回 1, cap 缺失或失败返回 0。 + * QEMU nvme 只暴露 MSI-X (无 0x05), 本路径是 QEMU 下中断驱动化的唯一通道。 + * 与 MSI 的差异仅在消息存储位置: MSI-X 的 addr/data 存放在 BAR 内的 + * 表项 (16B/项), 需先把表区 MMIO 映射进 CPU 地址空间再编程 entry 0。 + * 失败路径完整回滚 (清 Enable + 表项重新 mask + 释放向量)。 */ +static int nvme_msix_program(const struct dkm_kernel_api *api, + u8 bus, u8 dev, u8 func) { + int cap = nvme_find_pci_cap(bus, dev, func, PCI_CAP_ID_MSIX); + if (cap < 0) { + g_log->warn("[nvme] MSI-X capability not present; polling"); + return 0; + } + if (!api->mm_map_mmio) { + g_log->warn("[nvme] MSI-X: mm_map_mmio unavailable; polling"); + return 0; + } + + u32 cdw = dkm_pci_read(bus, dev, func, (u8)cap); + u32 msg_ctl = (cdw >> 16) & 0xffffu; + u32 table_size = (msg_ctl & 0x7ffu) + 1u; /* N-1 encoded */ + u32 tbl_dw = dkm_pci_read(bus, dev, func, (u8)(cap + 4)); + u32 bir = tbl_dw & 7u; + u32 tbl_off = tbl_dw & ~7u; + log_hex("[nvme] MSI-X msg_ctl=", msg_ctl); + log_dec("[nvme] MSI-X table size=", table_size); + log_hex("[nvme] MSI-X table BIR=", bir); + log_hex("[nvme] MSI-X table offset=", tbl_off); + if (bir > 5u) { + g_log->warn("[nvme] MSI-X: invalid BIR; polling"); + return 0; + } + + u64 bar_phys = nvme_bar_phys(bus, dev, func, bir); + if (!bar_phys) { + g_log->warn("[nvme] MSI-X: table BAR invalid; polling"); + return 0; + } + u64 tbl_phys = bar_phys + tbl_off; + log_hex("[nvme] MSI-X table BAR phys=", bar_phys); + log_hex("[nvme] MSI-X table phys=", tbl_phys); + + /* 映射表区首页 (含 entry0 的 16B)。mm_map_mmio 返回含页内偏移的指针, + * 页属性 PCD|PWT (UC), MSI-X 表访问不要求 cache 一致性操作。 */ + volatile u32 *tbl = (volatile u32 *)api->mm_map_mmio(tbl_phys, 4096); + if (!tbl) { + g_log->warn("[nvme] MSI-X: table MMIO map failed; polling"); + return 0; + } + + int vector = api->irq_vector_alloc(); + if (vector < 0 || vector > 255) { + g_log->warn("[nvme] MSI-X: vector alloc failed; polling"); + return 0; + } + log_dec("[nvme] MSI-X vector=", (u64)vector); + + u32 ca, cb, cc_, cd; + nvme_cpuid(1, &ca, &cb, &cc_, &cd); + (void)ca; (void)cc_; (void)cd; + u32 dest = (cb >> 24) & 0xffu; + u32 addr = 0xFEE00000u | (dest << 12); + + /* 先写表项 (addr/data/mask=0), 再置 cap Enable — 避免半成品消息 */ + volatile u32 *e = tbl; /* entry 0 */ + e[0] = addr; /* Msg Addr Lo */ + e[1] = 0; /* Msg Addr Hi */ + e[2] = (u32)vector & 0xffu; /* Msg Data = 向量 */ + e[3] = 0; /* Vector Control: unmask */ + nvme_mb(); + + u32 nc = (msg_ctl | 0x8000u) & ~(1u << 14); /* Enable=1, FunctionMask=0 */ + cdw = (cdw & 0x0000ffffu) | (nc << 16); + dkm_pci_write(bus, dev, func, (u8)cap, cdw); + + /* 回读验证: cap Enable + 表项 addr/data/mask 与编程值一致 */ + u32 rb_ctl = (dkm_pci_read(bus, dev, func, (u8)cap) >> 16) & 0xffffu; + u32 rb_a0 = e[0], rb_a1 = e[1], rb_d = e[2], rb_vc = e[3]; + log_hex("[nvme] MSI-X readback ctl=", rb_ctl); + log_hex("[nvme] MSI-X readback entry addr=", ((u64)rb_a1 << 32) | rb_a0); + log_hex("[nvme] MSI-X readback entry data=", rb_d); + log_hex("[nvme] MSI-X readback entry vctl=", rb_vc); + if (!(rb_ctl & 0x8000u) || rb_a0 != addr || rb_a1 != 0 || + (rb_d & 0xffu) != ((u32)vector & 0xffu) || (rb_vc & 1u)) { + g_log->warn("[nvme] MSI-X: readback mismatch; rolling back"); + u32 rb = dkm_pci_read(bus, dev, func, (u8)cap); + rb = (rb & 0x0000ffffu) | (msg_ctl << 16); /* 恢复原 msg_ctl */ + dkm_pci_write(bus, dev, func, (u8)cap, rb); + e[3] = 1; /* 表项重新 mask */ + api->irq_vector_free(vector); + return 0; + } + + api->irq_register((u8)vector, (void *)(uintptr_t)nvme_msi_handler); + + g_msi_enabled = 1; + g_msix_mode = 1; + g_msi_vector = vector; + g_log->info("[nvme] MSI-X enabled (table entry0, IO CQ IEN=1, IV=0)"); + return 1; +} + +/* 中断驱动化入口: 先 MSI (0x05), 缺则 MSI-X (0x11), 皆不可则纯轮询。 + * 前提检查集中在此; 两条编程路径各自保证失败完整回滚。 */ +static int nvme_msi_setup(const struct dkm_kernel_api *api, + u8 bus, u8 dev, u8 func) { + if (!g_cfg_nvme_msi) { + g_log->info("[nvme] MSI disabled by FUCK (nvme_msi=0); polling mode"); + return 0; + } + if (!api->irq_vector_alloc || !api->irq_vector_free || !api->irq_register) { + g_log->warn("[nvme] MSI: kernel vector allocator unavailable; polling"); + return 0; + } + if (nvme_msi_program(api, bus, dev, func)) return 1; + return nvme_msix_program(api, bus, dev, func); +} + /* ---- IO queue 创建 ---- */ static int nvme_create_io_queues(void) { u32 dw[16]; - /* Create IO CQ (qid=1): PC=1, IEN=0 (轮询模式) - * CDW10 = QSIZE[31:16](0's based) | QID[15:0] —— QID 必须在低半字 */ + /* Create IO CQ (qid=1): PC=1; MSI 就绪时 IEN=1 + IV=0 + * CDW10 = QSIZE[31:16](0's based) | QID[15:0] —— QID 必须在低半字 + * CDW11 = IV[31:16] | IEN[1] | PC[0] —— IV=0 与 INTMS/INTMC bit0 对应 */ nvme_zero(dw, sizeof(dw)); dw[0] = NVME_ADM_CREATE_IOCQ; dw[6] = (u32)(g_iocq.phys & 0xffffffffu); dw[7] = (u32)(g_iocq.phys >> 32); dw[10] = ((u32)(IO_QSIZE - 1u) << 16) | 1u; - dw[11] = 1; + dw[11] = 1u; + if (g_msi_enabled) dw[11] = 1u | (1u << 1); /* PC=1, IEN=1, IV=0 */ + log_hex("[nvme] create IOCQ CDW11=", dw[11]); int rc = nvme_admin_submit(dw, 0); if (rc != 0) { g_log->error("[nvme] create IO CQ failed"); @@ -586,6 +1210,61 @@ static void nvme_selftest_rw(u64 lba, u32 sectors) { g_log->info("[nvme] write+flush+readback verify OK"); } +/* B7 阶段3 压测: 128 扇区 (64KiB, PRP list 路径) × 50 次 写+读。 + * 统计总耗时与 MSI 计数器增量, 供中断/轮询两模式粗粒度对比。 + * 仅签名测试盘运行 (写 LBA3000 起的 50 个块, 不碰 LBA0 签名区)。 */ +static void nvme_selftest_stress(void) { + static u8 wbuf[65536]; + static u8 rbuf[65536]; + for (u32 i = 0; i < sizeof(wbuf); i++) wbuf[i] = (u8)(i * 17u + 3u); + + u64 irq0 = g_msi_irq_count, slot0 = g_msi_slot_hits, poll0 = g_msi_poll_harvests; + u64 t0 = dkm_rdtsc(); + u32 fails = 0; + for (u32 i = 0; i < 50; i++) { + u64 lba = 3000 + (u64)i * 128; + if (nvme_block_write(0, lba, 128, wbuf) != 0) { fails++; break; } + if (nvme_block_read(0, lba, 128, rbuf) != 0) { fails++; break; } + } + u64 elapsed_ms = dkm_tsc_per_ms ? (dkm_rdtsc() - t0) / dkm_tsc_per_ms : 0; + + /* 末次读回全量校验 (数据通路在 MSI 模式下完整性证明) */ + if (!fails) { + for (u32 i = 0; i < sizeof(wbuf); i++) { + if (rbuf[i] != wbuf[i]) { + g_log->error("[nvme] stress readback MISMATCH"); + log_hex("[nvme] first bad idx=", i); + log_hex("[nvme] expect=", wbuf[i]); + log_hex("[nvme] actual=", rbuf[i]); + log_hex("[nvme] cq head=", g_iocq_head); + log_hex("[nvme] cq phase=", g_iocq_phase); + log_hex("[nvme] slot valid=", g_slot_valid); + fails++; + break; + } + } + } + + log_dec("[nvme] stress 50x(128sec W+R) ms=", elapsed_ms); + log_dec("[nvme] stress fails=", fails); + log_dec("[nvme] stress irq delta=", g_msi_irq_count - irq0); + log_dec("[nvme] stress slot delta=", g_msi_slot_hits - slot0); + log_dec("[nvme] stress poll delta=", g_msi_poll_harvests - poll0); + if (!fails) g_log->info("[nvme] stress verify OK"); +} + +static void nvme_msi_stats_dump(const char *tag) { + g_log->info(tag); + log_dec("[nvme] msi enabled=", (u64)g_msi_enabled); + log_dec("[nvme] msix mode=", (u64)g_msix_mode); + log_dec("[nvme] msi vector=", (u64)(g_msi_vector < 0 ? 0 : g_msi_vector)); + log_dec("[nvme] msi irq count=", g_msi_irq_count); + log_dec("[nvme] msi slot hits=", g_msi_slot_hits); + log_dec("[nvme] msi poll harvests=", g_msi_poll_harvests); + log_dec("[nvme] msi timeouts=", g_msi_timeouts); + log_dec("[nvme] msi cid mismatch=", g_msi_cid_mismatch); +} + /* ---- 真机诊断: dump 关键寄存器 ---- */ static void nvme_dump_regs(const char *ctx) { @@ -673,6 +1352,7 @@ int driver_init(const struct dkm_kernel_api *api, dkm_instr_init(api); g_log->info("[nvme] init begin"); + nvme_load_config(api); u8 bus = 0, dev = 0, func = 0; if (nvme_find(&bus, &dev, &func) != 0) { @@ -867,6 +1547,9 @@ dma_ok: log_hex("[nvme] chunk max sectors=", g_chunk_max_sectors); } + /* ---- B7 阶段3: IO queue 创建前尝试 MSI (任何前提缺失回退纯轮询) ---- */ + nvme_msi_setup(api, bus, dev, func); + /* ---- IO queue ---- */ if (nvme_create_io_queues() != 0) { g_log->error("[nvme] IO queue creation failed"); @@ -876,6 +1559,13 @@ dma_ok: g_io_ready = 1; g_log->info("[nvme] IO queue live"); + /* MSI 就绪: INTMC 解除 IV0 屏蔽 (此前 IV0 保持屏蔽, admin 完成不产生 + * MSI; 此后 IO CQ 每次完成经 MSI 投递到 g_msi_vector) */ + if (g_msi_enabled) { + mmio_write32(NVME_INTMC, 1u); + log_hex("[nvme] INTMC unmask IV0, INTMS=", mmio_read32(NVME_INTMS)); + } + /* ---- LBA0 读取验证 ---- */ { int rc = nvme_io_rw(NVME_IO_READ, 0, 1, g_data.phys); @@ -921,6 +1611,7 @@ dma_ok: nvme_selftest_read_multipage(); /* 32 扇区 16KiB: PRP list 读 */ nvme_selftest_rw(1000, 16); /* 8KiB: PRP2 直连 (2 页) 写+读回 */ nvme_selftest_rw(2000, 32); /* 16KiB: PRP list (4 页) 写+读回 */ + nvme_selftest_stress(); /* B7: 128 扇区×50 压测 + MSI 计数 */ if (nvme_lba0_sig_ok()) { g_log->info("[nvme] LBA0 sig intact after write tests"); } else { @@ -930,6 +1621,8 @@ dma_ok: g_log->info("[nvme] not a test disk; write self-test skipped"); } + nvme_msi_stats_dump("[nvme] --- MSI stats ---"); + g_log->info("[nvme] driver ready"); return 0; } diff --git a/CODE/Deaicup Software/pe/src/raster.rs b/CODE/Deaicup Software/pe/src/raster.rs index 6d2690a..ff547fc 100644 --- a/CODE/Deaicup Software/pe/src/raster.rs +++ b/CODE/Deaicup Software/pe/src/raster.rs @@ -141,9 +141,15 @@ fn raster_tri( let mut w0 = edge(x1, y1, x2, y2, px as f32 + 0.5, fy) * inv_area; let mut w1 = edge(x2, y2, x0, y0, px as f32 + 0.5, fy) * inv_area; let mut w2 = 1.0 - w0 - w1; - // Per-pixel increments (dx = +1) - let dw0 = (y1 - y2) * inv_area; - let dw1 = (y2 - y0) * inv_area; + // Per-pixel increments (dx = +1). + // d/dpx edge(a, b, P) = (b.y - a.y): + // w0 = edge(p1, p2, P) -> (y2 - y1) + // w1 = edge(p2, p0, P) -> (y0 - y2) + // 符号写反会让 u/v/顶点色沿 +x 反向插值:文字逐行起始像素正确、 + // 向右逐渐反向采样图集(字形水平镜像/竖条碎裂),纯色面板因 + // 采样恒白区域不可见而掩盖了该 bug。 + let dw0 = (y2 - y1) * inv_area; + let dw1 = (y0 - y2) * inv_area; while px < max_x { if w0 >= 0.0 && w1 >= 0.0 && w2 >= 0.0 { diff --git a/CODE/UTSM/arch/x86_64/idt.c b/CODE/UTSM/arch/x86_64/idt.c index cbbd74b..331e28e 100644 --- a/CODE/UTSM/arch/x86_64/idt.c +++ b/CODE/UTSM/arch/x86_64/idt.c @@ -405,10 +405,21 @@ static void pic_remap(void) { outb(PIC2_DATA, 0xFF); /* mask all slave */ } +/* 运行时读取当前内核 CS — 必须与实际生效的 GDT 一致。 + * 注意:UTSM 运行在 Limine 提供的 GDT 下(CS=0x28),不能假设 + * OSDev 教程布局(0x08=code)。若 IDT gate selector 指向无效/非64位 + * 代码段,异常派发时加载 CS 会 #GP(selector error),异常链升级为 + * #DF -> triple fault,且全程无任何日志输出。 */ +static u16 idt_kernel_cs(void) { + u16 cs; + __asm__ volatile("mov %%cs, %0" : "=r"(cs)); + return cs; +} + static void idt_set_entry(u8 vector, void *handler, u8 ist, u8 type) { u64 addr = (u64)handler; g_idt[vector].offset_low = (u16)(addr & 0xffff); - g_idt[vector].selector = 0x08; /* kernel code segment (from GDT) */ + g_idt[vector].selector = idt_kernel_cs(); g_idt[vector].ist = ist; g_idt[vector].type_attr = type; g_idt[vector].offset_mid = (u16)((addr >> 16) & 0xffff); @@ -459,15 +470,122 @@ void idt_halt_all(void) { __asm__ volatile("lidt %0" :: "m"(idtr)); } +void idt_install_dsk_diag(void) { + /* BUG-20260801-005: DSK 交接诊断 IDT(替代原 idt_halt_all 静默化)。 + * 原 halt_all 将 256 项全部换成静默 halt stub——防御正确,但 DSK + * 阶段任何 CPU 异常(PF/GP/UD)被无声吞掉,零日志无法诊断 + * (实测 pe HELLO64.EXE 早期 #PF 直接表现为"卡死/三重故障")。 + * 本函数在保留 IRQ 防御的前提下恢复异常可见性: + * vector 0-31 -> 原 isr_stub(走 idt_handler 异常路径,打完整 + * vector/err/rip/CR2/寄存器日志后安全停机) + * vector 32-255 -> 保持 halt stub(PIC 已全 mask 且 IF=0,IRQ 本 + * 不该到达;spurious IRQ 维持原防御行为) + * 不做 pic_remap / sti——DSK 上下文 IF=0 不变式保持不变。 + * isr_stub / idt_handler 是 UTSM .text 代码,SAS-R0 下常驻内存 + * 有效(BUG-20260729-013 登记行为:halt_all 引入前 DSK 阶段异常 + * 本就能送达 idt_handler);异常路径只用 log/serial,不依赖已过期 + * 的 IRQ handler 状态。 */ + for (u16 i = 0; i < 32; i++) { + idt_set_entry((u8)i, g_isr_table[i], 0, 0x8E); + } + for (u16 i = 32; i < IDT_ENTRIES; i++) { + idt_set_entry((u8)i, (void *)isr_halt_stub, 0, 0x8E); + } + idt_ptr idtr; + idtr.limit = (u16)(sizeof(g_idt) - 1); + idtr.base = (u64)&g_idt; + __asm__ volatile("lidt %0" :: "m"(idtr)); + log_info("[IDT] DSK diag IDT installed (exc 0-31 logged, IRQ halted)"); +} + +void idt_install_pe_diag(void) { + /* 语义与 idt_install_dsk_diag 相同:BUG-20260801-005 起 DSK 交接 + * 默认装备诊断 IDT,PE 原生执行前重装一次保持幂等(防御 PE 运行 + * 期间 IDT 被第三方改写的场景)。 */ + idt_install_dsk_diag(); +} + /* IRQ handler table — 扩展到 256 个 vector (真机 APIC/IOAPIC/MSI 需要) */ static irq_handler_t g_irq_handlers[256]; +/* B7 阶段2: LAPIC EOI 钩子 (由 apic.drv 经 kernel_api.register_apic_eoi 注册)。 + * 指向写 LAPIC EOI 寄存器 (0xFEE000B0) 的函数; NULL = PIC 路由模式, 不调用。 + * 单核 BSP 场景: 注册发生在 apic.drv init (普通上下文), 调用发生在 IRQ + * 上下文; 函数指针赋值对齐原子, 无需锁。 */ +static void (*g_apic_eoi_hook)(void) = 0; + +void idt_register_apic_eoi(void (*eoi_fn)(void)) { + g_apic_eoi_hook = eoi_fn; + if (eoi_fn) { + log_info("[IDT] APIC EOI hook registered (LAPIC EOI on IRQ dispatch)"); + } else { + log_info("[IDT] APIC EOI hook cleared"); + } +} + /* ---- 插桩: IRQ 统计计数器 ---- */ INSTR_STAT_DECL(pic_irq_total); INSTR_STAT_DECL(apic_irq_total); +/* ---- B7 阶段3: 动态 IDT 向量分配器 (MSI/MSI-X 用) ---- + * 分配范围 0x40-0xDF (160 个向量): + * 0x00-0x1F CPU 异常 + * 0x20-0x2F PIC legacy IRQ (保留不动) + * 0x30-0x3F 保留给 IOAPIC GSI 16-31 未来扩展 (本阶段不动态分配) + * 0x40-0xDF 动态分配池 (MSI/MSI-X) + * 0xE0 APIC tick 自测预留 (阶段1 惯例) + * 0xFF LAPIC spurious vector + * 数据结构: 3 x u64 位图 (192 bit, 用后 160), __builtin_ctzll 找首个 + * 零位, 分配/释放均 O(1)。热路径无锁: SAS-R0 单核 BSP, alloc/free 只在 + * driver_init 主上下文调用 (MSI 编程期间对应设备尚未解除中断屏蔽, + * IRQ 上下文不可能并发分配), 位图字为对齐 u64 单写原子。 + * 动态入口无需新增 stub: idt_stubs.S 已为 0-255 全部向量预生成入口, + * 分配后 irq_register(vector, handler) 直接挂上既有分发表。 */ +#define IRQ_VEC_POOL_BASE 0x40 +#define IRQ_VEC_POOL_COUNT 160 +static u64 g_vec_bitmap[3]; + +int irq_vector_alloc(void) { + for (u32 w = 0; w < 3; w++) { + u64 free_bits = ~g_vec_bitmap[w]; + if (!free_bits) continue; + u32 bit = (u32)__builtin_ctzll(free_bits); + u32 idx = w * 64u + bit; + if (idx >= IRQ_VEC_POOL_COUNT) break; + g_vec_bitmap[w] |= (1ULL << bit); + int vec = IRQ_VEC_POOL_BASE + (int)idx; + log_info("[IDT] vector alloc"); + log_hex64("[IDT] vector=", (u64)vec); + return vec; + } + log_warn("[IDT] vector alloc failed: pool exhausted"); + return -1; +} + +void irq_vector_free(int vector) { + if (vector < IRQ_VEC_POOL_BASE || + vector >= IRQ_VEC_POOL_BASE + (int)IRQ_VEC_POOL_COUNT) { + return; /* 越界输入幂等忽略 (静态向量不在池内) */ + } + u32 idx = (u32)(vector - IRQ_VEC_POOL_BASE); + g_vec_bitmap[idx / 64u] &= ~(1ULL << (idx % 64u)); + log_info("[IDT] vector free"); + log_hex64("[IDT] vector=", (u64)vector); +} + int irq_register(u8 vector, irq_handler_t handler) { - if ((unsigned)vector >= 256) return -1; + /* IRQ号/向量兼容 shim: 0-15 视为 ISA IRQ 号, 换算 vector=0x20+IRQ。 + * 依据: vector 0-15 是 CPU 异常向量 (#DE/#DB/...), idt_handler 的 IRQ + * 分发只覆盖 32-255, 故 0-15 作为注册目标永不可达、无歧义。 + * 现状调用方两命名空间并存: ps2kbd(1)/e1000(11)/mouseInit(12) 传 IRQ 号, + * apic(0xE0)/virtio_net(0x20+n) 传向量 — 统一在此归一到向量空间。 */ + if (vector < 16) { + u8 v = (u8)(0x20u + vector); + log_info("[IDT] irq_register: ISA IRQ number translated to vector"); + log_hex64("[IDT] irq=", vector); + log_hex64("[IDT] vector=", v); + vector = v; + } g_irq_handlers[vector] = handler; INSTR_LOG(INSTR_LOG_DEBUG, INSTR_F_UTSM_TRACE, "[IDT] IRQ handler registered"); return 0; @@ -483,25 +601,55 @@ typedef struct { void idt_handler(isr_frame *frame) { if (frame->vector < 32) { + /* CR2: #PF 的 fault 线性地址;CR3: 当前页表基址。 + * 寄存器 dump 用于定位 fault 指令的操作数来源。 */ + u64 cr2, cr3; + __asm__ volatile("mov %%cr2, %0" : "=r"(cr2)); + __asm__ volatile("mov %%cr3, %0" : "=r"(cr3)); INSTR_PROBE(EXCP, frame->vector, frame->error_code, frame->rip, 0); log_error("[IDT] exception"); log_hex64("[IDT] vector=", frame->vector); log_hex64("[IDT] err=", frame->error_code); log_hex64("[IDT] rip=", frame->rip); + log_hex64("[IDT] cr2=", cr2); + log_hex64("[IDT] cr3=", cr3); + log_hex64("[IDT] rax=", frame->rax); + log_hex64("[IDT] rcx=", frame->rcx); + log_hex64("[IDT] rdx=", frame->rdx); + log_hex64("[IDT] rsi=", frame->rsi); + log_hex64("[IDT] rdi=", frame->rdi); + log_hex64("[IDT] r8=", frame->r8); + log_hex64("[IDT] r9=", frame->r9); + log_hex64("[IDT] r10=", frame->r10); + log_hex64("[IDT] r11=", frame->r11); + log_hex64("[IDT] cs=", frame->cs); + log_hex64("[IDT] rflags=", frame->rflags); for (;;) { __asm__ volatile ("cli; hlt"); } } - /* PIC IRQ: vectors 0x20-0x2F (32-47) */ + /* PIC IRQ: vectors 0x20-0x2F (32-47) + * B7 阶段2: 本分支同时服务两种路由来源: + * - 8259 PIC 路由 (默认): handler 后只需 8259 EOI, 钩子为 NULL 跳过; + * - IOAPIC->LAPIC 路由 (apic_route=1, legacy 线重定向到 0x20-0x2F): + * 中断由 LAPIC 递交, ISR 位置位, 必须写 LAPIC EOI (钩子) 清除, + * 否则同级 (vector[7:4] 相同) 中断被永久阻塞 — 每条线只投递一次。 + * LAPIC EOI 先于 8259 EOI; PIC 全屏蔽时 8259 EOI 为无害 no-op + * (8259 无 in-service 位, 非特异性 EOI 不写任何状态)。 */ if (frame->vector >= 32 && frame->vector <= 47) { INSTR_STAT_INC(pic_irq_total); u8 irq = (u8)(frame->vector - 0x20); - /* call registered handler */ + /* call registered handler; 返回 1 = 自负 EOI 全责 (两者皆抑制) */ if (g_irq_handlers[frame->vector]) { int suppress_eoi = g_irq_handlers[frame->vector](irq); if (suppress_eoi) return; } + /* LAPIC EOI (IOAPIC 路由时必需; PIC 路由时钩子为 NULL) */ + if (g_apic_eoi_hook) { + g_apic_eoi_hook(); + } + /* send PIC EOI */ if (frame->vector >= 0x28) { outb(PIC2_CMD, 0x20); @@ -511,19 +659,23 @@ void idt_handler(isr_frame *frame) { } /* APIC/IOAPIC/MSI IRQ: vectors 0x30-0xFF (48-255) - * **QEMU 无法测试但真机必需的代码路径**: - * - APIC EOI 写入 LAPIC EOI 寄存器 (0xFEE000B0) - * - 当前 PIC 仍激活, 不需要 APIC EOI - * - 迁移到 IOAPIC 后需要此处补 APIC EOI */ + * handler 约定: 返回 0 -> 由钩子补 LAPIC EOI (安全网); + * 返回 1 -> handler 已自行 LAPIC EOI, 跳过钩子。 + * 钩子未注册 (纯 PIC 路由) 时本分支不做任何 EOI — 与旧版行为一致, + * 此时 handler 必须自行 LAPIC EOI (参见 apic.drv tick handler)。 */ if (frame->vector >= 48 && frame->vector <= 255) { + /* LAPIC spurious (SVR 低 8 位 = 0xFF): 规范上不置 ISR 位, 永不需 EOI。 + * 若在此调钩子写 EOI, 会误清嵌套场景下真正 in-service 的最高位, + * 故特判直接返回 (EOI 空 ISR 虽无害, 但语义上 spurious 不该有 EOI)。 */ + if (frame->vector == 0xFF) return; INSTR_STAT_INC(apic_irq_total); + int suppress_eoi = 0; if (g_irq_handlers[frame->vector]) { - g_irq_handlers[frame->vector]((u8)frame->vector); + suppress_eoi = g_irq_handlers[frame->vector]((u8)frame->vector); + } + if (!suppress_eoi && g_apic_eoi_hook) { + g_apic_eoi_hook(); } - /* TODO: LAPIC EOI — 写 0 到 LAPIC EOI 寄存器 (mmio offset 0xB0) - * 当从 PIC 迁移到 APIC 中断路由时, 必须在此处添加: - * *(volatile u32 *)(hhdm_offset + lapic_phys + 0xB0) = 0; - */ return; } } diff --git a/CODE/UTSM/arch/x86_64/idt.o b/CODE/UTSM/arch/x86_64/idt.o index 1cc13d7..c635dcc 100644 Binary files a/CODE/UTSM/arch/x86_64/idt.o and b/CODE/UTSM/arch/x86_64/idt.o differ diff --git a/CODE/UTSM/dkm/kernel_api.c b/CODE/UTSM/dkm/kernel_api.c index 35660f8..c40eef8 100644 --- a/CODE/UTSM/dkm/kernel_api.c +++ b/CODE/UTSM/dkm/kernel_api.c @@ -56,7 +56,8 @@ static dkm_kernel_api g_kernel_api = { .block = 0, .mmio = 0, .mm_map_mmio = 0, - .mm_unmap_mmio = 0 + .mm_unmap_mmio = 0, + .register_apic_eoi = 0 }; const dkm_kernel_api *dkm_get_kernel_api(void) { @@ -94,4 +95,9 @@ void dkm_fill_platform_info(void) { } g_kernel_api.mm_map_mmio = mm_map_mmio; g_kernel_api.mm_unmap_mmio = mm_unmap_mmio; + /* B7 阶段2: 暴露 LAPIC EOI 钩子注册入口(apic.drv LAPIC 接管时调用) */ + g_kernel_api.register_apic_eoi = idt_register_apic_eoi; + /* B7 阶段3: 暴露动态向量分配器(MSI/MSI-X 设备驱动使用) */ + g_kernel_api.irq_vector_alloc = irq_vector_alloc; + g_kernel_api.irq_vector_free = irq_vector_free; } diff --git a/CODE/UTSM/dkm/kernel_api.o b/CODE/UTSM/dkm/kernel_api.o index 7293da0..b56edd3 100644 Binary files a/CODE/UTSM/dkm/kernel_api.o and b/CODE/UTSM/dkm/kernel_api.o differ diff --git a/CODE/UTSM/include/utsm/dkm.h b/CODE/UTSM/include/utsm/dkm.h index 489c68a..2acf173 100644 --- a/CODE/UTSM/include/utsm/dkm.h +++ b/CODE/UTSM/include/utsm/dkm.h @@ -100,6 +100,17 @@ typedef struct dkm_kernel_api { /* —— 新增字段只允许追加在末尾(DSK 以 api+0xA8 硬偏移读取 block) —— */ void *(*mm_map_mmio)(u64 phys, u64 size); /* MMIO 独立窗口映射(PCD|PWT),返回虚拟地址,失败返回 0 */ void (*mm_unmap_mmio)(void *virt, u64 size); /* 解除 MMIO 独立窗口映射 */ + /* B7 阶段2: 驱动向 UTSM 注册 LAPIC EOI 钩子(写 LAPIC EOI 寄存器的函数指针)。 + * apic.drv 在 LAPIC 接管时注册;idt_handler 的 IRQ 分发末尾调用该钩子, + * 使 vector 0x20-0x2F 的 legacy 线在 IOAPIC 路由下可持续投递。 + * 未注册(PIC 路由)时钩子为 NULL,idt_handler 跳过,行为与旧版一致。 */ + void (*register_apic_eoi)(void (*eoi_fn)(void)); + /* B7 阶段3: 动态 IDT 向量分配器(MSI/MSI-X 用),尾部追加保持 ABI。 + * 池范围 0x40-0xDF;alloc 返回向量号、失败 -1;free 幂等越界忽略。 + * 驱动拿到向量后经 irq_register(vector, handler) 直接注册(现有 + * 接口已接受向量命名空间,无需新注册 ABI)。 */ + int (*irq_vector_alloc)(void); + void (*irq_vector_free)(int vector); } dkm_kernel_api; typedef enum dkm_driver_state { diff --git a/CODE/UTSM/include/utsm/idt.h b/CODE/UTSM/include/utsm/idt.h index e69651f..e088866 100644 --- a/CODE/UTSM/include/utsm/idt.h +++ b/CODE/UTSM/include/utsm/idt.h @@ -10,11 +10,39 @@ void idt_init(void); * 被 spurious IRQ / 异常触发导致三重故障重启。 */ void idt_halt_all(void); -/* IRQ handler: return 1 to suppress default ACK (PIC), 0 for normal EOI. - * 参数为 IDT vector 号 (0x20-0xFF)。 - * PIC IRQ (0x20-0x2F): 返回 1 抑制 PIC EOI; 返回 0 自动发送 PIC EOI。 - * APIC/IOAPIC/MSI (0x30-0xFF): 返回值当前忽略, EOI 需手动处理 (TODO)。 */ +/* DSK 诊断 IDT:vector 0-31 恢复原 isr_stub(异常走 idt_handler + * 打完整日志后停机),vector 32-255 保持 halt stub。 + * BUG-20260801-005:UTSM→DSK 交接处以本函数替代 idt_halt_all, + * 恢复 DSK 阶段 CPU 异常可见性(原静默 halt 零诊断)。 + * 不做 pic_remap / sti,DSK 上下文 IF=0 不变式保持不变。 */ +void idt_install_dsk_diag(void); + +/* PE 诊断 IDT:idt_install_dsk_diag 的别名(幂等重装)。 + * 用于 PE 兼容层原生执行 PE32+ 前恢复异常可见性。 */ +void idt_install_pe_diag(void); + +/* IRQ handler: return 1 to suppress default ACK (EOI), 0 for normal EOI. + * 注册参数兼容两种命名空间(二者不重叠,自动判别): + * - ISA IRQ 号 0-15 -> 自动换算为 vector 0x20+IRQ (兼容 ps2kbd/e1000/mouseInit) + * - IDT vector 0x20+ -> 直接按 vector 注册 (apic 0xE0 / virtio_net 0x20+n 现状) + * vector 0-15 是 CPU 异常向量,idt_handler 从不作为 IRQ 分发目标,故无歧义。 + * EOI 语义: + * - PIC IRQ (0x20-0x2F): 返回 0 -> 先调 APIC EOI 钩子(若已注册)再发 8259 EOI; + * 返回 1 -> 两者都抑制, handler 自负 EOI 全责。 + * - APIC/IOAPIC/MSI (0x30-0xFF): 返回 0 -> 调 APIC EOI 钩子(若已注册); + * 返回 1 -> 抑制(如 handler 内已自行 LAPIC EOI)。未注册钩子时不做任何 EOI。 */ typedef int (*irq_handler_t)(u8 vector); int irq_register(u8 vector, irq_handler_t handler); +/* B7 阶段2: 注册 LAPIC EOI 钩子(由 apic.drv 在 LAPIC 接管后调用)。 + * 钩子语义: 向 LAPIC EOI 寄存器 (MMIO base+0xB0) 写 0,清除当前 in-service + * 最高优先级 ISR 位。重复注册以最后一次为准;传 NULL 可注销。 */ +void idt_register_apic_eoi(void (*eoi_fn)(void)); + +/* B7 阶段3: 动态 IDT 向量分配器 (MSI/MSI-X 用)。 + * 池范围 0x40-0xDF (160 个), 位图 O(1); 分配返回向量号, 池耗尽返回 -1。 + * free 幂等, 越界/静态向量输入忽略。主上下文调用, 无锁。 */ +int irq_vector_alloc(void); +void irq_vector_free(int vector); + #endif diff --git a/CODE/UTSM/kernel/dsk_loader.c b/CODE/UTSM/kernel/dsk_loader.c index ecfeba0..cb76481 100644 --- a/CODE/UTSM/kernel/dsk_loader.c +++ b/CODE/UTSM/kernel/dsk_loader.c @@ -731,12 +731,14 @@ int dsk_load_and_jump(void) { } } - /* 4) 将 IDT 所有 256 个条目替换为安全 halt stub。 - * 实机会产生 spurious IRQ7/15(即使 PIC 已 mask), - * 且 SMI/NMI 可能触发异常。如果 UTSM 的 idt_handler - * 所调用的 log 函数或串口驱动被覆盖,会导致双重异常→三重故障。 */ - extern void idt_halt_all(void); - idt_halt_all(); + /* 4) BUG-20260801-005: 换装 DSK 诊断 IDT(替代原静默 halt_all)。 + * vector 0-31 → UTSM isr_stub(idt_handler 打完整异常现场后 + * 安全停机),vector 32-255 保持 halt stub 防御 spurious IRQ7/15。 + * SAS-R0 下 UTSM .text 常驻内存,DSK 阶段异常投递路径已验证 + * 可用(BUG-20260729-013 登记行为);原 halt_all 把 #PF/#GP + * 无声吞掉,DSK 阶段零诊断。 */ + extern void idt_install_dsk_diag(void); + idt_install_dsk_diag(); /* 5) NMI off */ outb(0x70, inb(0x70) | 0x80); diff --git a/CODE/UTSM/kernel/dsk_loader.o b/CODE/UTSM/kernel/dsk_loader.o index b7b888e..70050cc 100644 Binary files a/CODE/UTSM/kernel/dsk_loader.o and b/CODE/UTSM/kernel/dsk_loader.o differ diff --git a/CODE/UTSM/pe/pe_loader.c b/CODE/UTSM/pe/pe_loader.c index faa3d07..6994d46 100644 --- a/CODE/UTSM/pe/pe_loader.c +++ b/CODE/UTSM/pe/pe_loader.c @@ -48,7 +48,7 @@ typedef struct { const image_file_header *file_hdr; const void *optional_hdr; int is_pe32_plus; - u32 image_base; /* 32位截断的加载基址(PE32+ 也截断用于 RVA 计算) */ + u64 image_base; /* 链接时首选基址(PE32+ 为 64 位,如 0x140000000,必须全宽保留) */ u32 size_of_image; u32 size_of_headers; u32 entry_rva; @@ -75,7 +75,7 @@ static int parse_nt_headers(const void *pe_data, u64 size, pe_nt_info *out) { if (magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) { const image_optional_header64 *oh = (const image_optional_header64 *)opt; out->is_pe32_plus = 1; - out->image_base = (u32)oh->image_base; + out->image_base = oh->image_base; /* PE32+: u64 全宽,禁止截断(重定位 delta 依赖高位) */ out->size_of_image = oh->size_of_image; out->size_of_headers = oh->size_of_headers; out->entry_rva = oh->address_of_entry_point; @@ -98,7 +98,7 @@ static int parse_nt_headers(const void *pe_data, u64 size, pe_nt_info *out) { } /* ---- 应用基址重定位 ---- */ -static void apply_relocations(u8 *image, u32 image_size, u64 image_base_actual, u32 image_base_preferred, +static void apply_relocations(u8 *image, u32 image_size, u64 image_base_actual, u64 image_base_preferred, const image_data_directory *reloc_dir, const image_section_header *sections, int nsec, const void *pe_data, u64 pe_size, int is_pe32_plus) { @@ -313,7 +313,7 @@ int pe_load_image(const void *pe_data, u64 size, pe_image_info *out) { /* 基址重定位 */ u64 actual_base = (u64)image; - u32 preferred_base = nt.image_base; + u64 preferred_base = nt.image_base; /* image 缓冲区按 RVA 排列(image[0]=RVA 0),重定位写入 image 内 */ /* 注意:apply_relocations 直接操作 image 缓冲区,delta = actual_base - preferred_base */ { diff --git a/CODE/UTSM/pe/pe_service.c b/CODE/UTSM/pe/pe_service.c index 85ddbf1..457c9c6 100644 --- a/CODE/UTSM/pe/pe_service.c +++ b/CODE/UTSM/pe/pe_service.c @@ -17,6 +17,7 @@ #include "x86emu32.h" #include "../include/utsm/pe.h" #include +#include #include #include @@ -275,6 +276,12 @@ int pe_service_run(const void *pe_data, u64 size, const char *cmdline, u64 *exit log_info("[PE] running PE32+ natively"); pe_enable_sse(); + /* 恢复异常可见性:UTSM 交权 DSK 时 idt_halt_all() 将 IDT 全部 + * 替换为静默 halt stub,PE 原生执行期间的 CPU 异常(PF/GP/UD) + * 会被无声吞掉。安装 PE 诊断 IDT 后异常走 idt_handler 打完整 + * vector/err/rip/CR2/寄存器日志再安全停机。 */ + idt_install_pe_diag(); + /* 独立 1MB 栈(内核栈仅 16KB,egui 渲染会溢出) */ u8 *pe_stack = (u8 *)kmem_alloc_aligned(PE_STACK_BYTES, 4096); if (!pe_stack) { @@ -283,9 +290,13 @@ int pe_service_run(const void *pe_data, u64 size, const char *cmdline, u64 *exit } u64 stack_top = (u64)pe_stack + PE_STACK_BYTES; /* 4096 对齐 -> 16 对齐 */ + /* BUG-20260801-006: __builtin_setjmp 必须直接出现在 + * pe_service_run 本体内——该帧在 PE 执行全程存活,longjmp + * 恢复合法.原 pe_shim_setup_exit() 包装函数 setjmp 后即返回, + * longjmp 跨已销毁帧 = UB(实测从脏栈 pop 出 log_hex64 缓冲 + * 残片充当 R12-R15/RBP → 野指针 call → 三重故障). */ u64 code = 0; - int jumped = pe_shim_setup_exit(); - if (jumped == 0) { + if (__builtin_setjmp(pe_shim_exit_jmpbuf()) == 0) { pe_entry_fn entry = (pe_entry_fn)info.entry_point; log_hex64("[PE] entry=", (u64)entry); log_hex64("[PE] stack_top=", stack_top); @@ -323,22 +334,30 @@ int pe_service_run(const void *pe_data, u64 size, const char *cmdline, u64 *exit } emu.eip = img.entry_point; + /* BUG-20260801-007: fn64 回退 shim 收到的指针参数是 32 位 + * guest VA,设置解释器内存基址使 shim 内 gp() 完成 guest→host + * 转换;运行结束立即复位(原生 PE32+ 路径恒为 0,gp 恒等映射). */ + pe_shim_set_emu_base((u64)img.mem); + + /* BUG-20260801-006: 同 PE32+ 路径,setjmp 直接出现在本体内. + * PE32 解释器路径同一 longjmp 机制,同样暴露在 UB 下. */ u64 code = 0; - int jumped = pe_shim_setup_exit(); - if (jumped == 0) { + if (__builtin_setjmp(pe_shim_exit_jmpbuf()) == 0) { int rrc = x86emu32_run(&emu, &code); if (rrc != 0) { log_error("[PE32] emulator error"); code = 0xFFFFFFFFULL; } + /* emu 字段只在未发生 longjmp 的正常返回路径读取 + * (setjmp 语义:longjmp 后被修改的自动变量值不确定) */ + if (emu.error && !emu.exited) { + log_warn("[PE32] emulator stopped with error"); + } } else { code = pe_shim_get_exit_code(); log_hex64("[PE32] ExitProcess code=", code); } - - if (emu.error && !emu.exited) { - log_warn("[PE32] emulator stopped with error"); - } + pe_shim_set_emu_base(0); if (exit_code) *exit_code = code; return 0; diff --git a/CODE/UTSM/pe/pe_service.d b/CODE/UTSM/pe/pe_service.d index 1154eb3..dba56d3 100644 --- a/CODE/UTSM/pe/pe_service.d +++ b/CODE/UTSM/pe/pe_service.d @@ -1,6 +1,6 @@ pe/pe_service.o: pe\pe_service.c pe\pe_loader.h include\utsm\types.h \ include\utsm\pe.h pe\pe_shim.h pe\x86emu32.h pe\..\include\utsm\pe.h \ - include\utsm\arena.h include\utsm\log.h + include\utsm\arena.h include\utsm\idt.h include\utsm\log.h pe\pe_loader.h: include\utsm\types.h: include\utsm\pe.h: @@ -8,4 +8,5 @@ pe\pe_shim.h: pe\x86emu32.h: pe\..\include\utsm\pe.h: include\utsm\arena.h: +include\utsm\idt.h: include\utsm\log.h: diff --git a/CODE/UTSM/pe/pe_shim.c b/CODE/UTSM/pe/pe_shim.c index 362eb21..d91ae1b 100644 --- a/CODE/UTSM/pe/pe_shim.c +++ b/CODE/UTSM/pe/pe_shim.c @@ -9,14 +9,19 @@ * ntdll.dll NtWriteFile、RtlNtStatusToDosError * bcryptprimitives.dll ProcessPrng (rdtsc xorshift) * api-ms-win-core-synch-l1-2-0.dll WaitOnAddress、WakeByAddress (futex 桩) - * user32.dll 窗口类、窗口、消息泵 (PS/2 键盘轮询)、DefWindowProc + * user32.dll 窗口类、窗口、消息泵 (PS/2 键盘+AUX 鼠标轮询)、DefWindowProc * gdi32.dll 内存 DC、DIBSection、BitBlt 到 Limine 帧缓冲 * * 窗口模型:单顶层窗口 (固定伪句柄),GetDC 返回窗口 DC, * BitBlt(窗口 DC, ...) 把 DIB 后台缓冲按窗口原点拷入帧缓冲. - * 消息泵:PeekMessageW 轮询 PS/2 键盘 (0x64/0x60),生成 - * WM_KEYDOWN、WM_KEYUP、WM_CHAR;Esc 额外合成 WM_CLOSE (无鼠标环境的 - * 唯一退出途径).鼠标字节 (状态位 0x20) 直接丢弃. + * 消息泵:PeekMessageW 轮询 PS/2 (0x64/0x60),按状态位 0x20 分流键盘/鼠标: + * 键盘 -> WM_KEYDOWN、WM_KEYUP、WM_CHAR;Esc 额外合成 WM_CLOSE; + * 鼠标 -> 3 字节包解码(同步位/溢出防御),坐标钳位到帧缓冲, + * 边沿生成 WM_MOUSEMOVE 与 WM_xBUTTONDOWN/WM_xBUTTONUP 系列. + * 软件光标:BitBlt 上屏后在光标坐标叠画 12x12 箭头(黑边白心), + * 下一帧 BitBlt 自动覆盖旧光标,无需保存/恢复背景. + * 鼠标初始化:on-demand(首次轮询时),AUX 启用 + 0xF4 数据报告, + * 全程超时保护,单步失败只记日志并禁用鼠标,绝不阻塞. */ #include "pe_shim.h" @@ -45,16 +50,39 @@ static int g_exit_jmp_set; static u64 g_exit_code; static const char *g_cmdline = ""; +/* ===== BUG-20260801-007: PE32 guest→host 指针转换 ===== + * PE32 解释器路径下,fn64 回退 shim 收到的指针参数是 32 位 guest VA + * (相对解释器 mem 基址的偏移),直接当 host 指针解引用 → 低地址 #PF + * → 三重故障(定案证据:#PF RIP 落在 shim_printf 体内解引用 0x401017)。 + * x86emu32 执行前经 pe_shim_set_emu_base() 设置 mem 基址,各 shim 在 + * **指针解引用点**经 gp() 转换(不在参数入口盲目转换——退出码/长度等 + * 小整数参数会被误转)。阈值 0x10000000 与 pe_service.c 解释器地址 + * 空间上限一致;shim 伪句柄(0x10000001+)与负值句柄天然豁免。 + * 原生 PE32+ 路径 g_emu_mem_base 恒为 0,gp() 恒等映射,零影响。 */ +static u64 g_emu_mem_base = 0; + +void pe_shim_set_emu_base(u64 base) { g_emu_mem_base = base; } + +static inline u64 gp(u64 va) { + if (g_emu_mem_base && va && va < 0x10000000ULL) + return g_emu_mem_base + va; + return va; +} + void pe_shim_init(const char *cmdline) { g_cmdline = cmdline ? cmdline : ""; g_exit_jmp_set = 0; g_exit_code = 0; + g_emu_mem_base = 0; /* BUG-007: 默认非解释器上下文(原生路径恒等映射) */ } -/* pe_run 调用:保存上下文,返回 0 = 首次(继续执行),1 = ExitProcess 触发 */ -int pe_shim_setup_exit(void) { +/* 返回 ExitProcess longjmp 目标 jmpbuf(见 pe_shim.h 的 BUG-006 说明). + * 本函数只提供缓冲区并重置退出状态;__builtin_setjmp 由调用方 + * (pe_service_run 本体)直接执行,确保 setjmp 帧在 PE 执行全程存活. */ +void **pe_shim_exit_jmpbuf(void) { g_exit_jmp_set = 1; - return __builtin_setjmp(g_exit_jmp); + g_exit_code = 0; + return g_exit_jmp; } void pe_shim_exit_process(u64 exit_code) { @@ -81,7 +109,18 @@ u64 pe_shim_unimpl_stub(void) { } /* ===== 串口/帧缓冲输出(shim 内部复用) ===== */ +/* 单次输出长度上限:防御 PE 侧传野 (buf,len)(如 panic 递归中损坏的 + * String)把半个地址空间倒进串口并踩穿未映射页(#PF).正常日志远远 + * 低于此值;截断时留锚点便于定位. */ +#define SHIM_OUTPUT_MAX (1ULL * 1024ULL * 1024ULL) + static void shim_output(const char *s, u64 len) { + if (len > SHIM_OUTPUT_MAX) { + log_error("[shim] output len clamped"); + log_hex64("[shim] output buf=", (u64)s); + log_hex64("[shim] output len=", len); + len = SHIM_OUTPUT_MAX; + } for (u64 i = 0; i < len; i++) { serial_putc(s[i]); } @@ -115,10 +154,11 @@ static u64 __attribute__((ms_abi)) shim_WriteFile(u64 handle, u64 buffer, u64 le u64 bytes_written_ptr, u64 overlapped) { (void)overlapped; if (handle == SHIM_STDOUT_HANDLE || handle == SHIM_STDERR_HANDLE) { - const char *buf = (const char *)buffer; + const char *buf = (const char *)gp(buffer); shim_output(buf, length); /* lpNumberOfBytesWritten 是 DWORD*(4 字节),不能按 u64 写 */ - if (bytes_written_ptr) *(u32 *)bytes_written_ptr = (u32)length; + u64 bwp = gp(bytes_written_ptr); + if (bwp) *(u32 *)bwp = (u32)length; return 1; /* TRUE */ } return 0; @@ -127,8 +167,9 @@ static u64 __attribute__((ms_abi)) shim_WriteFile(u64 handle, u64 buffer, u64 le static u64 __attribute__((ms_abi)) shim_ReadFile(u64 handle, u64 buffer, u64 max_length, u64 bytes_read_ptr, u64 overlapped) { (void)handle; (void)buffer; (void)max_length; (void)overlapped; - /* 暂不支持输入读取 */ - if (bytes_read_ptr) *(u64 *)bytes_read_ptr = 0; + /* 暂不支持输入读取;lpNumberOfBytesRead 同为 DWORD*(4 字节) */ + u64 brp = gp(bytes_read_ptr); + if (brp) *(u32 *)brp = 0; return 1; } @@ -163,12 +204,13 @@ static u64 __attribute__((ms_abi)) shim_HeapAlloc(u64 heap, u64 flags, u64 size) } static u64 __attribute__((ms_abi)) shim_HeapFree(u64 heap, u64 flags, u64 ptr) { (void)heap; (void)flags; - shim_heap_free((void *)ptr); + /* gp():PE32 野指针收束进 guest 沙盒,避免低地址 host 解引用 */ + shim_heap_free((void *)gp(ptr)); return 1; } static u64 __attribute__((ms_abi)) shim_HeapReAlloc(u64 heap, u64 flags, u64 ptr, u64 size) { (void)heap; (void)flags; - return (u64)shim_heap_realloc((void *)ptr, size); + return (u64)shim_heap_realloc((void *)gp(ptr), size); } static u64 __attribute__((ms_abi)) shim_GetProcessHeap(void) { return 0x10000010; @@ -216,7 +258,7 @@ static void shim_msvcrt_output(const char *s) { static u64 __attribute__((ms_abi)) shim_printf(u64 fmt, u64 a1, u64 a2, u64 a3) { /* 可变参数:MS x64 ABI 下 a1-a3 在 RDX/R8/R9,更多参数在栈上. * 简化处理:只处理前 3 个参数. */ - const char *f = (const char *)fmt; + const char *f = (const char *)gp(fmt); u64 args[3] = {a1, a2, a3}; int argi = 0; char outbuf[512]; @@ -226,7 +268,7 @@ static u64 __attribute__((ms_abi)) shim_printf(u64 fmt, u64 a1, u64 a2, u64 a3) i++; switch (f[i]) { case 's': { - const char *s = (const char *)(args[argi++ & 3]); + const char *s = (const char *)gp(args[argi++ & 3]); if (s) while (*s && oi < 510) outbuf[oi++] = *s++; break; } @@ -272,7 +314,7 @@ static u64 __attribute__((ms_abi)) shim_printf(u64 fmt, u64 a1, u64 a2, u64 a3) } static u64 __attribute__((ms_abi)) shim_puts(u64 str) { - const char *s = (const char *)str; + const char *s = (const char *)gp(str); shim_msvcrt_output(s); shim_msvcrt_output("\n"); return 0; @@ -292,9 +334,9 @@ static u64 __attribute__((ms_abi)) shim_calloc(u64 count, u64 size) { if (p) for (u64 i = 0; i < total; i++) p[i] = 0; return (u64)p; } -static u64 __attribute__((ms_abi)) shim_free(u64 ptr) { shim_heap_free((void *)ptr); return 0; } +static u64 __attribute__((ms_abi)) shim_free(u64 ptr) { shim_heap_free((void *)gp(ptr)); return 0; } static u64 __attribute__((ms_abi)) shim_realloc(u64 ptr, u64 size) { - return (u64)shim_heap_realloc((void *)ptr, size); + return (u64)shim_heap_realloc((void *)gp(ptr), size); } static u64 __attribute__((ms_abi)) shim_exit(u64 code) { pe_shim_exit_process(code); @@ -302,38 +344,38 @@ static u64 __attribute__((ms_abi)) shim_exit(u64 code) { } static u64 __attribute__((ms_abi)) shim_memcpy(u64 dst, u64 src, u64 n) { - u8 *d = (u8 *)dst; const u8 *s = (const u8 *)src; + u8 *d = (u8 *)gp(dst); const u8 *s = (const u8 *)gp(src); for (u64 i = 0; i < n; i++) d[i] = s[i]; return dst; } static u64 __attribute__((ms_abi)) shim_memset(u64 dst, u64 val, u64 n) { - u8 *d = (u8 *)dst; u8 v = (u8)(val & 0xFF); + u8 *d = (u8 *)gp(dst); u8 v = (u8)(val & 0xFF); for (u64 i = 0; i < n; i++) d[i] = v; return dst; } static u64 __attribute__((ms_abi)) shim_memmove(u64 dst, u64 src, u64 n) { - u8 *d = (u8 *)dst; const u8 *s = (const u8 *)src; + u8 *d = (u8 *)gp(dst); const u8 *s = (const u8 *)gp(src); if (d < s) { for (u64 i = 0; i < n; i++) d[i] = s[i]; } else { for (u64 i = n; i > 0; i--) d[i-1] = s[i-1]; } return dst; } static u64 __attribute__((ms_abi)) shim_strlen(u64 str) { - const char *s = (const char *)str; + const char *s = (const char *)gp(str); u64 n = 0; while (s[n]) n++; return n; } static u64 __attribute__((ms_abi)) shim_strcmp(u64 a, u64 b) { - const char *pa = (const char *)a; const char *pb = (const char *)b; + const char *pa = (const char *)gp(a); const char *pb = (const char *)gp(b); while (*pa && *pa == *pb) { pa++; pb++; } return (u64)(i64)(*pa - *pb); } static u64 __attribute__((ms_abi)) shim_strcpy(u64 dst, u64 src) { - char *d = (char *)dst; const char *s = (const char *)src; + char *d = (char *)gp(dst); const char *s = (const char *)gp(src); while (*s) *d++ = *s++; *d = 0; return dst; } static u64 __attribute__((ms_abi)) shim_strncmp(u64 a, u64 b, u64 n) { - const char *pa = (const char *)a; const char *pb = (const char *)b; + const char *pa = (const char *)gp(a); const char *pb = (const char *)gp(b); for (u64 i = 0; i < n; i++) { if (pa[i] != pb[i]) return (u64)(i64)(pa[i] - pb[i]); if (pa[i] == 0) return 0; @@ -343,12 +385,12 @@ static u64 __attribute__((ms_abi)) shim_strncmp(u64 a, u64 b, u64 n) { static u64 __attribute__((ms_abi)) shim_fwrite(u64 ptr, u64 size, u64 nmemb, u64 stream) { (void)stream; u64 total = size * nmemb; - shim_output((const char *)ptr, total); + shim_output((const char *)gp(ptr), total); return nmemb; } static u64 __attribute__((ms_abi)) shim_fputs(u64 str, u64 stream) { (void)stream; - shim_output_str((const char *)str); + shim_output_str((const char *)gp(str)); return 0; } static u64 __attribute__((ms_abi)) shim_fputc(u64 ch, u64 stream) { @@ -372,11 +414,23 @@ static u64 __attribute__((ms_abi)) shim_initterm_e(u64 start, u64 end) { * * Rust std 的 GlobalAlloc 走 GetProcessHeap/HeapAlloc/HeapReAlloc/ * HeapFree.arena 只增不减,GUI 主循环每帧的 Vec 分配会耗尽内存, - * 因此 shim 从 arena carve 一块 8MB 区域自行管理: - * 32B 块头 {size,used,next_free,pad},首次适应 + 分割 + 前向合并. + * 因此 shim 从 arena carve 一块 32MB 区域自行管理: + * 32B 块头 {size,used,next_free,pad},首次适应 + 分割. + * + * 合并策略(2026-08-02 修复):空闲链表按地址升序维护,free 时做 + * 前向 + 后向双向合并.旧实现是 LIFO 头插 + 仅前向合并,egui 每帧 + * 内部存在非 LIFO 的 alloc/free 序列,物理相邻的空闲块永远无法 + * 后向合并,堆逐渐碎成小片,实测 ~27KB/帧的速度劣化,第 ~1200 帧 + * (BitBlt frames=0x4b0)连 0x2a80 的连续块都分不出而 OOM,随后 + * Rust panic 递归损坏 String,NtWriteFile 野性转储 55MB 踩穿 + * 未映射页 #PF(rip=shim_NtWriteFile+0x30).地址有序空闲链表使 + * "物理相邻"等价于"链相邻",前后向合并即可完备回收. + * + * (egui 字体 + 每帧 tessellation 缓冲实测 8MB 会在第 3 帧 OOM,扩到 32MB; + * arena 总 64MB,PE 镜像~3MB + 栈 1MB + DIB ~6MB,余量充足.) * ========================================================== */ -#define SHIM_HEAP_BYTES (8ULL * 1024ULL * 1024ULL) +#define SHIM_HEAP_BYTES (32ULL * 1024ULL * 1024ULL) #define HEAP_HDR 32 typedef struct heap_blk { @@ -388,6 +442,7 @@ typedef struct heap_blk { static u8 *g_heap_region; static heap_blk *g_heap_free_head; +static u64 g_heap_live; /* 已分配字节合计(诊断:区分真泄漏与碎片化) */ static void shim_heap_init_once(void) { if (g_heap_region) return; @@ -404,57 +459,88 @@ static void shim_heap_init_once(void) { g_heap_free_head = b; } +/* 大/小块阈值:egui 长寿命缓存块多为几十字节,每帧 tessellation 等 + * 瞬态缓冲为 10-30KB.大块专取地址最高的适配块,使顶部连续区域免于 + * 被小长寿块钉碎(实测无此策略时 2400 帧后 29.9MB 空闲碎成 6 万个 + * 平均 500B 的片,free_max 只剩 10.2KB,10.8KB 请求即 OOM). */ +#define SHIM_HEAP_LARGE 2048ULL + static void *shim_heap_alloc(u64 size) { shim_heap_init_once(); if (!g_heap_region) return 0; if (size == 0) size = 16; size = (size + 15) & ~15ULL; heap_blk *prev = 0, *b = g_heap_free_head; - while (b) { - if (b->size >= size) { - if (prev) prev->next_free = b->next_free; - else g_heap_free_head = (heap_blk *)b->next_free; - /* 剩余足够再分一块则分割 */ - if (b->size >= size + HEAP_HDR + 16) { - heap_blk *nb = (heap_blk *)((u8 *)b + HEAP_HDR + size); - nb->size = b->size - size - HEAP_HDR; - nb->used = 0; - nb->pad = 0; - nb->next_free = (u64)g_heap_free_head; - g_heap_free_head = nb; - b->size = size; - } - b->used = 1; - b->next_free = 0; - return (u8 *)b + HEAP_HDR; + heap_blk *cand = 0, *cand_prev = 0; + if (size < SHIM_HEAP_LARGE) { + /* 小块:首次适应(低地址优先)填碎洞,保护顶部大区域 */ + while (b) { + if (b->size >= size) { cand = b; cand_prev = prev; break; } + prev = b; b = (heap_blk *)b->next_free; } - prev = b; - b = (heap_blk *)b->next_free; + } else { + /* 大块:取地址最高的适配块(地址有序链,最后一个适配者) */ + while (b) { + if (b->size >= size) { cand = b; cand_prev = prev; } + prev = b; b = (heap_blk *)b->next_free; + } + } + b = cand; prev = cand_prev; + if (b) { + /* 摘除 b;分割时余量 nb 占据 b 原链位即可保持地址有序 + * (nb=b+HEAP_HDR+size < 原后继:后继不与 b 重叠 => 后继>=b 块尾 > nb) */ + if (prev) prev->next_free = b->next_free; + else g_heap_free_head = (heap_blk *)b->next_free; + if (b->size >= size + HEAP_HDR + 16) { + heap_blk *nb = (heap_blk *)((u8 *)b + HEAP_HDR + size); + nb->size = b->size - size - HEAP_HDR; + nb->used = 0; + nb->pad = 0; + nb->next_free = b->next_free; + if (prev) prev->next_free = (u64)nb; + else g_heap_free_head = nb; + b->size = size; + } + b->used = 1; + b->next_free = 0; + g_heap_live += b->size; + return (u8 *)b + HEAP_HDR; + } + /* OOM 诊断:live 持续增长=真泄漏;live 平稳而 free_max 萎缩=碎片化 */ + u64 ft = 0, fm = 0, fc = 0; + for (heap_blk *f = g_heap_free_head; f; f = (heap_blk *)f->next_free) { + ft += f->size; if (f->size > fm) fm = f->size; fc++; } log_error("[shim] heap OOM"); log_hex64("[shim] heap req=", size); + log_hex64("[shim] heap live=", g_heap_live); + log_hex64("[shim] heap free_total=", ft); + log_hex64("[shim] heap free_max=", fm); + log_hex64("[shim] heap free_blocks=", fc); return 0; } static void shim_heap_free(void *ptr) { if (!ptr) return; heap_blk *b = (heap_blk *)((u8 *)ptr - HEAP_HDR); + g_heap_live -= b->size; b->used = 0; - /* 前向合并:物理下一块若空闲则吸收 */ - heap_blk *nb = (heap_blk *)((u8 *)b + HEAP_HDR + b->size); - if ((u8 *)nb >= g_heap_region && (u8 *)nb < g_heap_region + SHIM_HEAP_BYTES - && !nb->used && nb->size) { - /* 从空闲链摘除 nb */ - heap_blk *prev = 0, *x = g_heap_free_head; - while (x && x != nb) { prev = x; x = (heap_blk *)x->next_free; } - if (x == nb) { - if (prev) prev->next_free = nb->next_free; - else g_heap_free_head = (heap_blk *)nb->next_free; - b->size += HEAP_HDR + nb->size; - } + /* 地址升序插入空闲链表:prev < b < x(x 可为 0 表尾) */ + heap_blk *prev = 0, *x = g_heap_free_head; + while (x && x < b) { prev = x; x = (heap_blk *)x->next_free; } + b->next_free = (u64)x; + if (prev) prev->next_free = (u64)b; + else g_heap_free_head = b; + /* 前向合并:x 与 b 物理相邻则吸收(地址有序 => 物理相邻即链相邻) */ + if (x && (u8 *)b + HEAP_HDR + b->size == (u8 *)x) { + b->size += HEAP_HDR + x->size; + b->next_free = x->next_free; + } + /* 后向合并:prev 与 b 物理相邻则把 b 并入 prev */ + if (prev && (u8 *)prev + HEAP_HDR + prev->size == (u8 *)b) { + prev->size += HEAP_HDR + b->size; + prev->next_free = b->next_free; } - b->next_free = (u64)g_heap_free_head; - g_heap_free_head = b; } static void *shim_heap_realloc(void *ptr, u64 size) { @@ -503,11 +589,13 @@ static void shim_tsc_init(void) { static u64 __attribute__((ms_abi)) shim_QueryPerformanceFrequency(u64 freq_ptr) { shim_tsc_init(); + freq_ptr = gp(freq_ptr); if (freq_ptr) *(u64 *)freq_ptr = g_tsc_per_ms * 1000; return 1; } static u64 __attribute__((ms_abi)) shim_QueryPerformanceCounter(u64 count_ptr) { shim_tsc_init(); + count_ptr = gp(count_ptr); if (count_ptr) *(u64 *)count_ptr = shim_rdtsc(); return 1; } @@ -524,6 +612,7 @@ static u64 __attribute__((ms_abi)) shim_Sleep(u64 ms) { /* SYSTEM_INFO(x64,48 字节) */ static u64 __attribute__((ms_abi)) shim_GetSystemInfo(u64 si_p) { + si_p = gp(si_p); if (!si_p) return 0; u8 *p = (u8 *)si_p; for (int i = 0; i < 48; i++) p[i] = 0; @@ -548,7 +637,7 @@ static u64 __attribute__((ms_abi)) shim_WriteConsoleW(u64 handle, u64 buf, u64 n u64 written_ptr, u64 reserved) { (void)reserved; if (handle != SHIM_STDOUT_HANDLE && handle != SHIM_STDERR_HANDLE) return 0; - const u16 *s = (const u16 *)buf; + const u16 *s = (const u16 *)gp(buf); for (u64 i = 0; i < nchars; i++) { u32 c = s[i]; if (c < 0x80) { @@ -562,12 +651,14 @@ static u64 __attribute__((ms_abi)) shim_WriteConsoleW(u64 handle, u64 buf, u64 n serial_putc((char)(0x80 | (c & 0x3F))); } } + written_ptr = gp(written_ptr); if (written_ptr) *(u32 *)written_ptr = (u32)nchars; return 1; } static u64 __attribute__((ms_abi)) shim_GetCurrentDirectoryW(u64 buflen, u64 buf) { static const u16 cwd[] = { 'C', ':', '\\', 0 }; + buf = gp(buf); if (!buf || buflen < 4) return 4; /* 需要 4 个字符(含 NUL) */ u16 *out = (u16 *)buf; for (int i = 0; i < 4; i++) out[i] = cwd[i]; @@ -603,6 +694,7 @@ static u64 __attribute__((ms_abi)) shim_FormatMessageW(u64 flags, u64 src, u64 m } static u64 __attribute__((ms_abi)) shim_LoadLibraryA(u64 name_p) { + name_p = gp(name_p); if (!name_p) return 0; const char *n = (const char *)name_p; /* 已知 DLL 返回伪句柄(大小写不敏感子串匹配) */ @@ -625,7 +717,7 @@ static u64 __attribute__((ms_abi)) shim_LoadLibraryA(u64 name_p) { } static u64 __attribute__((ms_abi)) shim_lstrlenW(u64 str_p) { - const u16 *s = (const u16 *)str_p; + const u16 *s = (const u16 *)gp(str_p); u64 n = 0; if (s) while (s[n]) n++; return n; @@ -636,6 +728,7 @@ static u64 __attribute__((ms_abi)) shim_lstrlenW(u64 str_p) { * Rust std 用它动态探测 Rtl 系列/dbghelp API,命中返回真实 shim 地址,miss 返回 0。 */ static u64 __attribute__((ms_abi)) shim_GetProcAddress(u64 module, u64 name_p) { (void)module; + name_p = gp(name_p); if (!name_p) return 0; if ((name_p >> 16) == 0) { /* ordinal 导入 */ g_last_error = 127; /* ERROR_PROC_NOT_FOUND */ @@ -660,12 +753,13 @@ static u64 __attribute__((ms_abi)) shim_GetProcAddress(u64 module, u64 name_p) { /* Rtl* — Rust panic/unwind 机制引用(panic=abort 下不会真正走到) */ static u64 __attribute__((ms_abi)) shim_RtlCaptureContext(u64 ctx_p) { - u8 *p = (u8 *)ctx_p; + u8 *p = (u8 *)gp(ctx_p); if (p) for (int i = 0; i < 1232; i++) p[i] = 0; /* x64 CONTEXT ≈ 1232B */ return 0; } static u64 __attribute__((ms_abi)) shim_RtlLookupFunctionEntry(u64 rip, u64 base_p, u64 table_p) { (void)rip; + base_p = gp(base_p); table_p = gp(table_p); if (base_p) *(u64 *)base_p = 0; if (table_p) *(u64 *)table_p = 0; return 0; @@ -680,8 +774,8 @@ static u64 __attribute__((ms_abi)) shim_RtlVirtualUnwind(u64 a, u64 b, u64 c, u6 static u64 __attribute__((ms_abi)) shim_MultiByteToWideChar(u64 cp, u64 flags, u64 mbs_p, u64 cbmb, u64 wbuf_p, u64 cchw) { (void)flags; - const u8 *s = (const u8 *)mbs_p; - u16 *out = (u16 *)wbuf_p; + const u8 *s = (const u8 *)gp(mbs_p); + u16 *out = (u16 *)gp(wbuf_p); i64 remain = (i64)cbmb; /* -1 = NUL 结尾 */ u64 need = 0; if (!s) return 0; @@ -730,10 +824,11 @@ static u64 __attribute__((ms_abi)) shim_WideCharToMultiByte(u64 cp, u64 flags, u u64 cchw, u64 mbs_p, u64 cbmb, u64 defc, u64 used_p) { (void)flags; (void)defc; - const u16 *s = (const u16 *)ws_p; - u8 *out = (u8 *)mbs_p; + const u16 *s = (const u16 *)gp(ws_p); + u8 *out = (u8 *)gp(mbs_p); i64 remain = (i64)cchw; /* -1 = NUL 结尾 */ u64 need = 0; + used_p = gp(used_p); if (used_p) *(u32 *)used_p = 0; if (!s) return 0; for (;;) { @@ -785,8 +880,9 @@ static u64 __attribute__((ms_abi)) shim_NtWriteFile(u64 fh, u64 ev, u64 apc_r, u u64 off, u64 key) { (void)ev; (void)apc_r; (void)apc_c; (void)off; (void)key; if (fh == SHIM_STDOUT_HANDLE || fh == SHIM_STDERR_HANDLE) { - shim_output((const char *)buf, len); + shim_output((const char *)gp(buf), len); } + iosb = gp(iosb); if (iosb) { /* IO_STATUS_BLOCK: {NTSTATUS Status; ULONG_PTR Information} */ ((u64 *)iosb)[0] = 0; ((u64 *)iosb)[1] = len; @@ -799,7 +895,7 @@ static u64 __attribute__((ms_abi)) shim_RtlNtStatusToDosError(u64 status) { static u64 __attribute__((ms_abi)) shim_ProcessPrng(u64 buf, u64 len) { u64 s = shim_rdtsc() ^ 0x9E3779B97F4A7C15ULL; - u8 *p = (u8 *)buf; + u8 *p = (u8 *)gp(buf); for (u64 i = 0; i < len; i++) { s ^= s << 13; s ^= s >> 7; s ^= s << 17; /* xorshift64 */ p[i] = (u8)(s & 0xFF); @@ -815,7 +911,7 @@ static u64 __attribute__((ms_abi)) shim_WaitOnAddress(u64 addr, u64 expected, u6 static u64 __attribute__((ms_abi)) shim_WakeByAddress(u64 addr) { (void)addr; return 0; } /* ============================================================ - * user32.dll — 单窗口 + 消息泵(PS/2 键盘轮询) + * user32.dll — 单窗口 + 消息泵(PS/2 键盘 + AUX 鼠标轮询) * ========================================================== */ #define SHIM_HWND 0x20000001ULL @@ -827,6 +923,19 @@ static u64 __attribute__((ms_abi)) shim_WakeByAddress(u64 addr) { (void)addr; re #define SHIM_WM_KEYDOWN 0x0100u #define SHIM_WM_KEYUP 0x0101u #define SHIM_WM_CHAR 0x0102u +#define SHIM_WM_MOUSEMOVE 0x0200u +#define SHIM_WM_LBUTTONDOWN 0x0201u +#define SHIM_WM_LBUTTONUP 0x0202u +#define SHIM_WM_RBUTTONDOWN 0x0204u +#define SHIM_WM_RBUTTONUP 0x0205u +#define SHIM_WM_MBUTTONDOWN 0x0207u +#define SHIM_WM_MBUTTONUP 0x0208u +/* wparam MK_* 修饰位 */ +#define SHIM_MK_LBUTTON 0x0001u +#define SHIM_MK_RBUTTON 0x0002u +#define SHIM_MK_SHIFT 0x0004u +#define SHIM_MK_CONTROL 0x0008u +#define SHIM_MK_MBUTTON 0x0010u static u64 g_wnd_proc = 0; static int g_wnd_created = 0; @@ -846,8 +955,13 @@ typedef struct { static shim_msg_t g_msgq[SHIM_MSGQ_CAP]; static int g_mq_head = 0, g_mq_count = 0; -static void shim_msg_push(u32 msg, u64 w, u64 l) { - if (g_mq_count >= SHIM_MSGQ_CAP) return; /* 满则丢弃 */ +static u32 g_mq_drops = 0; /* 队列满丢弃计数(诊断) */ + +static void shim_msg_push_pt(u32 msg, u64 w, u64 l, i32 ptx, i32 pty) { + if (g_mq_count >= SHIM_MSGQ_CAP) { + if (g_mq_drops < 4) { g_mq_drops++; log_hex64("[shim] msgq drop msg=", msg); } + return; + } int idx = (g_mq_head + g_mq_count) % SHIM_MSGQ_CAP; g_msgq[idx].hwnd = SHIM_HWND; g_msgq[idx].message = msg; @@ -856,11 +970,15 @@ static void shim_msg_push(u32 msg, u64 w, u64 l) { g_msgq[idx].lparam = l; g_msgq[idx].time = 0; g_msgq[idx].pad1 = 0; - g_msgq[idx].ptx = 0; - g_msgq[idx].pty = 0; + g_msgq[idx].ptx = ptx; + g_msgq[idx].pty = pty; g_mq_count++; } +static void shim_msg_push(u32 msg, u64 w, u64 l) { + shim_msg_push_pt(msg, w, l, 0, 0); +} + /* PS/2 键盘状态 */ static int s_kbd_shift = 0, s_kbd_ctrl = 0, s_kbd_alt = 0, s_kbd_e0 = 0; @@ -895,9 +1013,15 @@ static u32 shim_scan_vk(u8 sc, int e0) { default: return 0; } } - if (sc >= 0x10 && sc <= 0x19) return 'Q' + (sc - 0x10); - if (sc >= 0x1E && sc <= 0x26) return 'A' + (sc - 0x1E); - if (sc >= 0x2C && sc <= 0x32) return 'Z' + (sc - 0x2C); + /* 字母三行:scancode 顺序与 ASCII 序不一致(QWERTY 布局),且 'Z'+1='[' + * 在 ASCII 中不连续,必须用显式表,不能算术偏移(此前 shift-b 出 0x5E). */ + static const u8 vk_row_q[10] = { 'Q','W','E','R','T','Y','U','I','O','P' }; + static const u8 vk_row_a[9] = { 'A','S','D','F','G','H','J','K','L' }; + static const u8 vk_row_z[7] = { 'Z','X','C','V','B','N','M' }; + if (sc >= 0x10 && sc <= 0x19) return vk_row_q[sc - 0x10]; + if (sc >= 0x1E && sc <= 0x26) return vk_row_a[sc - 0x1E]; + if (sc >= 0x2C && sc <= 0x32) return vk_row_z[sc - 0x2C]; + /* 数字行 scancode 与 ASCII 均连续,算术偏移正确 */ if (sc >= 0x02 && sc <= 0x0A) return '1' + (sc - 0x02); if (sc == 0x0B) return '0'; switch (sc) { @@ -913,13 +1037,134 @@ static u32 shim_scan_vk(u8 sc, int e0) { } } +/* 前向声明(shim_fb 定义在下方 RegisterClassExW 之后) */ +static const struct limine_framebuffer *shim_fb(void); + +/* ============================================================ + * PS/2 AUX 鼠标(轮询模式,无 IRQ12) + * + * 初始化:on-demand(首次消息泵轮询时执行),启用 AUX 端口 + + * 清配置字节 bit5(启用 AUX 时钟) + 发 0xF4 启用数据报告. + * 全程超时保护:任何单步失败只记日志并永久禁用鼠标,绝不阻塞 + * GUI 主循环.包解码:3 字节(同步位 bit3 校验,9 位有符号位移, + * 溢出位丢弃位移),坐标钳位到帧缓冲,边沿合成鼠标消息. + * ========================================================== */ + +static int s_mouse_inited = 0; +static int s_mouse_ok = 0; +static i32 s_mouse_x = 512, s_mouse_y = 384; /* 屏幕坐标 */ +static u8 s_mouse_btn = 0; /* bit0=L bit1=R bit2=M */ +static u8 s_mpkt[3]; +static int s_mpkt_n = 0; +static int s_dbg_aux = 0; /* 调试:前 16 个 AUX 字节计数 */ +static int s_dbg_pkt = 0; /* 调试:前 8 个完整包计数 */ +static int s_dbg_key = 0; /* 调试:前 16 个键盘消息计数(KEYDOWN/KEYUP/CHAR) */ + +static int ps2_wait_ibf_clear(u32 spins) { + while (spins--) { if (!(inb(0x64) & 2)) return 0; } + return -1; +} +static int ps2_wait_obf_set(u32 spins) { + while (spins--) { if (inb(0x64) & 1) return 0; } + return -1; +} +/* 经 0xD4 转发命令到鼠标并等 ACK(0xFA) */ +static int ps2_aux_cmd(u8 v) { + if (ps2_wait_ibf_clear(100000) < 0) return -1; + outb(0x64, 0xD4); + if (ps2_wait_ibf_clear(100000) < 0) return -1; + outb(0x60, v); + if (ps2_wait_obf_set(100000) < 0) return -1; + return inb(0x60) == 0xFA ? 0 : -1; +} + +static void shim_mouse_init_once(void) { + if (s_mouse_inited) return; + s_mouse_inited = 1; + /* 排空控制器残留输出字节 */ + for (int i = 0; i < 16 && (inb(0x64) & 1); i++) (void)inb(0x60); + /* 启用 AUX 端口 */ + if (ps2_wait_ibf_clear(100000) < 0) { log_warn("[shim] mouse: ctrl busy"); return; } + outb(0x64, 0xA8); + /* 读配置字节:清 bit5(启用 AUX 时钟),置 bit1(IRQ12 允许,轮询下无害) */ + if (ps2_wait_ibf_clear(100000) < 0) { log_warn("[shim] mouse: ctrl busy2"); return; } + outb(0x64, 0x20); + if (ps2_wait_obf_set(100000) < 0) { log_warn("[shim] mouse: no cfg byte"); return; } + u8 cfg = inb(0x60); + cfg = (u8)((cfg & ~0x20u) | 0x02u); + if (ps2_wait_ibf_clear(100000) < 0) { log_warn("[shim] mouse: cfg wbusy"); return; } + outb(0x64, 0x60); + if (ps2_wait_ibf_clear(100000) < 0) { log_warn("[shim] mouse: cfg wbusy2"); return; } + outb(0x60, cfg); + /* 启用流模式数据报告 */ + if (ps2_aux_cmd(0xF4) < 0) { log_warn("[shim] mouse: enable-report nack"); return; } + s_mouse_ok = 1; + log_info("[shim] mouse init ok (AUX streaming, polled)"); +} + +/* 鼠标 1 字节 -> 3 字节包 -> Win32 消息 */ +static void shim_mouse_byte(u8 b) { + if (s_mpkt_n == 0 && !(b & 0x08)) return; /* byte0 同步位(bit3)必须为 1,否则丢弃重同步 */ + s_mpkt[s_mpkt_n++] = b; + if (s_mpkt_n < 3) return; + s_mpkt_n = 0; + + u8 f = s_mpkt[0]; + i32 dx = (i32)s_mpkt[1] - ((f & 0x10) ? 0x100 : 0); /* 9 位有符号位移 */ + i32 dy = (i32)s_mpkt[2] - ((f & 0x20) ? 0x100 : 0); + if (f & 0x40) dx = 0; /* X 溢出:位移不可信,只取按钮 */ + if (f & 0x80) dy = 0; /* Y 溢出 */ + if (s_dbg_pkt < 8) { + s_dbg_pkt++; + log_hex64("[shim] mpkt f=", f); + log_hex64("[shim] mpkt dx=", (u64)(i64)dx); + log_hex64("[shim] mpkt dy=", (u64)(i64)dy); + } + + const struct limine_framebuffer *fb = shim_fb(); + i32 fw = fb ? (i32)fb->width : 1024; + i32 fh = fb ? (i32)fb->height : 768; + i32 nx = s_mouse_x + dx; + i32 ny = s_mouse_y - dy; /* PS/2 Y 向前(屏幕上方向)为正 -> 屏幕坐标取反 */ + if (nx < 0) nx = 0; else if (nx >= fw) nx = fw - 1; + if (ny < 0) ny = 0; else if (ny >= fh) ny = fh - 1; + + u8 nbtn = (u8)(f & 0x07); + i32 cx = nx - g_wnd_x, cy = ny - g_wnd_y; /* 客户区坐标(消息 lparam) */ + u64 lp = (u64)(u32)(((u32)(u16)cy << 16) | (u32)(u16)cx); + u64 mk = 0; + if (nbtn & 1) mk |= SHIM_MK_LBUTTON; + if (nbtn & 2) mk |= SHIM_MK_RBUTTON; + if (nbtn & 4) mk |= SHIM_MK_MBUTTON; + if (s_kbd_shift) mk |= SHIM_MK_SHIFT; + if (s_kbd_ctrl) mk |= SHIM_MK_CONTROL; + + if (nx != s_mouse_x || ny != s_mouse_y) { + s_mouse_x = nx; s_mouse_y = ny; + shim_msg_push_pt(SHIM_WM_MOUSEMOVE, mk, lp, nx, ny); + } + /* 按钮边沿(按钮事件罕见,逐一打点:msg/客户区坐标) */ + if ((nbtn & 1) && !(s_mouse_btn & 1)) { shim_msg_push_pt(SHIM_WM_LBUTTONDOWN, mk, lp, nx, ny); log_hex64("[shim] LDOWN lp=", lp); } + if (!(nbtn & 1) && (s_mouse_btn & 1)) { shim_msg_push_pt(SHIM_WM_LBUTTONUP, mk, lp, nx, ny); log_hex64("[shim] LUP lp=", lp); } + if ((nbtn & 2) && !(s_mouse_btn & 2)) shim_msg_push_pt(SHIM_WM_RBUTTONDOWN, mk, lp, nx, ny); + if (!(nbtn & 2) && (s_mouse_btn & 2)) shim_msg_push_pt(SHIM_WM_RBUTTONUP, mk, lp, nx, ny); + if ((nbtn & 4) && !(s_mouse_btn & 4)) shim_msg_push_pt(SHIM_WM_MBUTTONDOWN, mk, lp, nx, ny); + if (!(nbtn & 4) && (s_mouse_btn & 4)) shim_msg_push_pt(SHIM_WM_MBUTTONUP, mk, lp, nx, ny); + s_mouse_btn = nbtn; +} + /* 排空 PS/2 输出缓冲,转成 Win32 消息入队 */ static void shim_kbd_poll(void) { + shim_mouse_init_once(); for (;;) { u8 st = inb(0x64); if (!(st & 1)) break; u8 sc = inb(0x60); - if (st & 0x20) continue; /* 鼠标字节丢弃 */ + if (st & 0x20) { + if (s_dbg_aux < 16) { s_dbg_aux++; log_hex64("[shim] aux byte=", sc); } + if (s_mouse_ok) shim_mouse_byte(sc); + continue; + } if (sc == 0xE0) { s_kbd_e0 = 1; continue; } int e0 = s_kbd_e0; if (!e0) { @@ -933,18 +1178,25 @@ static void shim_kbd_poll(void) { s_kbd_e0 = 0; if (sc & 0x80) { u32 vk = shim_scan_vk(sc & 0x7F, e0); - if (vk) shim_msg_push(SHIM_WM_KEYUP, vk, sc); + if (vk) { + shim_msg_push(SHIM_WM_KEYUP, vk, sc); + if (s_dbg_key < 16) { s_dbg_key++; log_hex64("[shim] KEYUP vk=", vk); } + } continue; } u32 vk = shim_scan_vk(sc, e0); if (vk) { shim_msg_push(SHIM_WM_KEYDOWN, vk, sc); + if (s_dbg_key < 16) { s_dbg_key++; log_hex64("[shim] KEYDOWN vk=", vk); } /* Esc -> 额外合成 WM_CLOSE:无鼠标环境下关闭窗口的唯一途径 */ if (vk == 0x1B && !e0) shim_msg_push(SHIM_WM_CLOSE, 0, 0); } if (!e0) { char c = shim_scan_ascii(sc, s_kbd_shift); - if (c >= ' ' && c < 0x7F) shim_msg_push(SHIM_WM_CHAR, (u64)(u8)c, sc); + if (c >= ' ' && c < 0x7F) { + shim_msg_push(SHIM_WM_CHAR, (u64)(u8)c, sc); + if (s_dbg_key < 16) { s_dbg_key++; log_hex64("[shim] CHAR ch=", (u64)(u8)c); } + } } } } @@ -1005,7 +1257,7 @@ static u64 __attribute__((ms_abi)) shim_SetCursor(u64 cur) { (void)cur; return SHIM_HCURSOR; } static u64 __attribute__((ms_abi)) shim_GetCursorPos(u64 pt_p) { - if (pt_p) { ((i32 *)pt_p)[0] = g_wnd_w / 2; ((i32 *)pt_p)[1] = g_wnd_h / 2; } + if (pt_p) { ((i32 *)pt_p)[0] = s_mouse_x; ((i32 *)pt_p)[1] = s_mouse_y; } return 1; } static u64 __attribute__((ms_abi)) shim_ScreenToClient(u64 hwnd, u64 pt_p) { @@ -1163,6 +1415,50 @@ static u64 __attribute__((ms_abi)) shim_GdiFlush(void) { return 1; } static u64 g_blit_count = 0; +/* 12x12 箭头软件光标:'X'=黑边 '#'=白心 '.'=透明. + * 设计:BitBlt 上屏后叠画;egui 每帧全量重绘,下一帧 BitBlt 自动覆盖旧光标, + * 无需保存/恢复背景.只在窗口矩形内绘制(越界裁剪),避免屏外残影. */ +static const char *const g_cursor_bmp[12] = { + "X...........", + "XX..........", + "X#X.........", + "X##X........", + "X###X.......", + "X####X......", + "X#####X.....", + "X######X....", + "X#######X...", + "X########X..", + "X####XXXX...", + "XXXX........", +}; + +static void shim_draw_cursor(const struct limine_framebuffer *fb) { + if (!s_mouse_ok || !g_wnd_created) return; + i32 wx1 = g_wnd_x + g_wnd_w, wy1 = g_wnd_y + g_wnd_h; + if (s_mouse_x < g_wnd_x || s_mouse_y < g_wnd_y) return; + if (s_mouse_x >= wx1 || s_mouse_y >= wy1) return; + u8 *fbb = (u8 *)fb->address; + for (i32 r = 0; r < 12; r++) { + i32 fy = s_mouse_y + r; + if (fy >= wy1 || fy >= (i32)fb->height) break; + u8 *rowb = fbb + (u64)fy * fb->pitch; + for (i32 c = 0; c < 12; c++) { + char ch = g_cursor_bmp[r][c]; + if (ch != 'X' && ch != '#') continue; + i32 fx = s_mouse_x + c; + if (fx >= wx1 || fx >= (i32)fb->width) break; + if (fb->bpp == 32) { + ((u32 *)rowb)[fx] = (ch == '#') ? 0xFFFFFFFFu : 0xFF000000u; + } else { + u8 v = (ch == '#') ? 0xFF : 0x00; + u8 *p = rowb + (u64)fx * 3; + p[0] = v; p[1] = v; p[2] = v; + } + } + } +} + static u64 __attribute__((ms_abi)) shim_BitBlt(u64 dstdc, u64 x, u64 y, u64 cx, u64 cy, u64 srcdc, u64 x1, u64 y1, u64 rop) { (void)rop; @@ -1216,9 +1512,19 @@ static u64 __attribute__((ms_abi)) shim_BitBlt(u64 dstdc, u64 x, u64 y, u64 cx, } } } + shim_draw_cursor(fb); g_blit_count++; if (g_blit_count == 1) log_info("[shim] BitBlt first frame on screen"); - if ((g_blit_count % 600) == 0) log_hex64("[shim] BitBlt frames=", g_blit_count); + if ((g_blit_count % 600) == 0) { + log_hex64("[shim] BitBlt frames=", g_blit_count); + /* 堆健康度:live 逐周期线性增长=真泄漏;free_max 持续萎缩=碎片化 */ + u64 fm = 0; + for (heap_blk *f = g_heap_free_head; f; f = (heap_blk *)f->next_free) { + if (f->size > fm) fm = f->size; + } + log_hex64("[shim] heap live=", g_heap_live); + log_hex64("[shim] heap free_max=", fm); + } return 1; } diff --git a/CODE/UTSM/pe/pe_shim.h b/CODE/UTSM/pe/pe_shim.h index 3c5ae9b..d54280d 100644 --- a/CODE/UTSM/pe/pe_shim.h +++ b/CODE/UTSM/pe/pe_shim.h @@ -49,9 +49,22 @@ void pe_shim_init(const char *cmdline); * 64位和32位共用。exit_code = 进程退出码。 */ void pe_shim_exit_process(u64 exit_code); -/* pe_run 调用:保存上下文(__builtin_setjmp)。 - * 返回 0 = 首次(继续执行 PE),1 = ExitProcess 触发(longjmp 回来)。 */ -int pe_shim_setup_exit(void); +/* 获取 ExitProcess longjmp 目标 jmpbuf,并重置退出状态。 + * + * BUG-20260801-006:__builtin_setjmp 必须出现在 PE 执行全程存活的 + * 栈帧内(pe_service_run 本体)。此前由 pe_shim_setup_exit() 包装 + * 调用——该函数调用 setjmp 后即返回,C 标准明确规定"含 setjmp 的 + * 函数已返回后再 longjmp"为未定义行为(实测 longjmp 尾声从脏栈 + * pop 出 log_hex64 缓冲区 ASCII 残片充当 R12-R15/RBP → 野指针 + * call → 三重故障)。 + * + * 调用方标准写法(setjmp 直接出现在调用方函数本体内): + * if (__builtin_setjmp(pe_shim_exit_jmpbuf()) == 0) { + * ... 执行 PE ... + * } else { + * code = pe_shim_get_exit_code(); + * } */ +void **pe_shim_exit_jmpbuf(void); /* 获取上次 ExitProcess 的退出码 */ u64 pe_shim_get_exit_code(void); @@ -62,4 +75,11 @@ int pe_shim_is_exit_call(u64 target); /* 获取 cmdline(shim 内部用) */ const char *pe_shim_get_cmdline(void); +/* BUG-20260801-007:设置 PE32 解释器内存基址(guest→host 指针转换)。 + * PE32 解释器路径下,fn64 回退 shim 收到的指针参数是 32 位 guest VA + * (相对解释器 mem 基址的偏移),直接当 host 指针解引用 → 低地址 #PF + * → 三重故障。执行 PE32 前设置 base=mem,shim 内部 gp() 在指针解引用点 + * 完成转换;base=0 表示非解释器上下文(原生 PE32+ 路径),gp() 恒等映射。 */ +void pe_shim_set_emu_base(u64 base); + #endif /* PE_SHIM_H */ diff --git a/CODE/UTSM/pe/x86emu32.c b/CODE/UTSM/pe/x86emu32.c index 2e7ec93..38ea5f5 100644 --- a/CODE/UTSM/pe/x86emu32.c +++ b/CODE/UTSM/pe/x86emu32.c @@ -310,8 +310,15 @@ static int try_iat_call(x86emu_state *emu, u32 target) { ret = se->fn32(esp_ptr); } else if (se->fn64) { /* 回退:调用 64位 shim(最多 4 个参数)。 - * MS x64 ABI 由 C 编译器处理寄存器分配。 */ - typedef u64 (*fn4_t)(u64, u64, u64, u64); + * BUG-20260801-007 后续修正:fn4_t 必须显式 ms_abi。 + * UTSM 以 clang x86_64-unknown-none 编译,C 默认 ABI 是 SysV + * (arg1=RDI/RSI/RDX/RCX),而 shim 全部 __attribute__((ms_abi)) + * (arg1=RCX/RDX/R8/R9)。typedef 不带 ms_abi 时编译器生成 SysV + * 调用序列 → shim 在 RCX 读到的是 SysV 第4参数 a3(=0) → gp(0)=0 + * → shim_printf 解引用 NULL → #PF cr2=0(实测寄存器现场: + * rdi=0x401017 即 SysV arg1=fmt, rcx=0, cr2=0)。 + * 原生 PE32+ 路径 guest 是 Windows 程序天生 MS ABI,不受影响。 */ + typedef u64 (__attribute__((ms_abi)) *fn4_t)(u64, u64, u64, u64); fn4_t f = (fn4_t)se->fn64; ret = f(a0, a1, a2, a3); diff --git a/CODE/desktop/main.c b/CODE/desktop/main.c index 3e99305..f451b35 100644 --- a/CODE/desktop/main.c +++ b/CODE/desktop/main.c @@ -265,6 +265,10 @@ static int g_drag_off_x, g_drag_off_y; static app_descriptor g_apps[MAX_APPS]; static int g_app_count = 0; +/* Linux 兼容层服务运行态(register_linux_apps 设置):0 时 Linux 图标置灰, + * 双击仍开终端窗口并提示不可用原因(lnx_on_create 运行时复查)。 */ +static int g_lnx_svc_up = 0; + static desktop_icon g_icons[MAX_ICONS]; static int g_icon_count = 0; @@ -545,24 +549,25 @@ static void draw_icon_browser(du_context *ctx, int cx, int cy) { du_pixel(ctx, ccx, ccy, KS_ACCENT2); } -/* Linux 应用图标:暗底终端 + 霓虹绿 >_ 提示符(与 bash 终端同色板) */ -static void draw_icon_linux(du_context *ctx, int cx, int cy) { +/* Linux 应用图标:暗底终端 + 霓虹绿 >_ 提示符(与 bash 终端同色板)。 + * 服务不可用时 accent 传 KS_TEXT_DIM(置灰,点击仍给原因提示)。 */ +static void draw_icon_linux(du_context *ctx, int cx, int cy, u32 accent) { du_fill_rect(ctx, cx, cy, 40, 40, KS_BG_PRIMARY); - du_rect_outline(ctx, cx, cy, 40, 40, KS_ACCENT2, 2); + du_rect_outline(ctx, cx, cy, 40, 40, accent, 2); /* '>' 折线 */ - du_fill_rect(ctx, cx + 8, cy + 12, 2, 2, KS_ACCENT2); - du_fill_rect(ctx, cx + 10, cy + 14, 2, 2, KS_ACCENT2); - du_fill_rect(ctx, cx + 12, cy + 16, 2, 2, KS_ACCENT2); - du_fill_rect(ctx, cx + 10, cy + 18, 2, 2, KS_ACCENT2); - du_fill_rect(ctx, cx + 8, cy + 20, 2, 2, KS_ACCENT2); + du_fill_rect(ctx, cx + 8, cy + 12, 2, 2, accent); + du_fill_rect(ctx, cx + 10, cy + 14, 2, 2, accent); + du_fill_rect(ctx, cx + 12, cy + 16, 2, 2, accent); + du_fill_rect(ctx, cx + 10, cy + 18, 2, 2, accent); + du_fill_rect(ctx, cx + 8, cy + 20, 2, 2, accent); /* '_' 光标块 */ - du_fill_rect(ctx, cx + 16, cy + 22, 8, 2, KS_ACCENT2); + du_fill_rect(ctx, cx + 16, cy + 22, 8, 2, accent); } static void draw_app_icon(du_context *ctx, int app_id, int cx, int cy) { /* Linux 应用走专属图标(暗底绿提示符),其余保持原有映射 */ if (app_id >= 0 && app_id < g_app_count && g_apps[app_id].linux_cmd) { - draw_icon_linux(ctx, cx, cy); + draw_icon_linux(ctx, cx, cy, g_lnx_svc_up ? KS_ACCENT2 : KS_TEXT_DIM); return; } switch (app_id) { @@ -1355,19 +1360,16 @@ static void lnx_on_destroy(void *state) { } /* ---- Linux 应用注册 ---- - * 服务不可用(reserved[5] 为空或 magic 不匹配或 guest 未就绪)时直接返回, - * 不注册任何图标,desktop 其余功能不受影响。 + * 无论服务是否可用都注册图标:可用时双击即同步 exec;不可用(VMX 缺失/ + * guest 未驻留)时图标置灰(g_lnx_svc_up=0),双击由 lnx_on_create 运行时 + * 复查并显示 "linux: compat service unavailable [exit -1]",绝不 panic。 * 配置:FAT32 根目录 LINUXAPP.CNF(8.3 名 LINUXAPPCNF),每行 显示名|linux命令; - * 文件缺失或无有效行时回退内置默认列表(neofetch / htop)。 */ + * 文件缺失或无有效行时回退内置默认列表(echo hello / uname -a / ls -l /, + * 三条均为 Arch rootfs 与 initramfs busybox 环境共有的可执行命令)。 */ static void register_linux_apps(void) { - if (!g_lxc_svc) { - slog("linux apps: service absent"); - return; - } - if (!g_lxc_svc->is_available()) { - slog("linux apps: service unavailable"); - return; - } + g_lnx_svc_up = (g_lxc_svc && g_lxc_svc->is_available()) ? 1 : 0; + if (!g_lxc_svc) slog("linux apps: service absent (icons greyed)"); + else if (!g_lnx_svc_up) slog("linux apps: service unavailable (icons greyed)"); da_linux_app ents[DA_LINUXAPP_MAX]; int n = 0; @@ -1379,12 +1381,14 @@ static void register_linux_apps(void) { } if (n <= 0) { - /* 内置默认保底 */ - kstrcpy(g_lnx_name_pool[0], "NEOFETCH", 12); - kstrcpy(g_lnx_cmd_pool[0], "/usr/bin/neofetch", 64); - kstrcpy(g_lnx_name_pool[1], "HTOP", 12); - kstrcpy(g_lnx_cmd_pool[1], "/usr/bin/htop", 64); - n = 2; + /* 内置默认保底(guest 中确定存在的命令) */ + kstrcpy(g_lnx_name_pool[0], "HELLO", 12); + kstrcpy(g_lnx_cmd_pool[0], "/bin/echo hello from deshab linux", 64); + kstrcpy(g_lnx_name_pool[1], "UNAME", 12); + kstrcpy(g_lnx_cmd_pool[1], "/bin/uname -a", 64); + kstrcpy(g_lnx_name_pool[2], "LS", 12); + kstrcpy(g_lnx_cmd_pool[2], "/bin/ls -l /", 64); + n = 3; slog("linux apps: builtin defaults"); } else { for (int i = 0; i < n; i++) { @@ -1403,6 +1407,12 @@ static void register_linux_apps(void) { 0, g_lnx_cmd_pool[i] }; g_app_count++; + { + char msg[64]; + kstrcpy(msg, "linux app registered: ", 64); + kstrcat(msg, g_lnx_name_pool[i], 64); + slog(msg); + } } slog("linux apps registered"); } @@ -1759,7 +1769,8 @@ static void register_apps(void) { }; g_app_count++; - /* Linux 兼容层应用(服务不可用时内部直接返回,不注册图标) */ + /* Linux 兼容层应用:无论服务是否可用都注册图标(不可用时置灰, + * 双击仍开终端窗口提示原因,见 register_linux_apps / lnx_on_create) */ register_linux_apps(); } @@ -1794,6 +1805,24 @@ static void setup_desktop_icons(void) { g_icon_count++; x += KATE_ICON_W + KATE_ICON_GAP; } + + /* 再追加 Linux 应用图标(register_linux_apps 已登记进 g_apps,带 linux_cmd)。 + * 服务不可用时图标仍上架但置灰(draw_app_icon 查 g_lnx_svc_up), + * 双击开终端窗口提示原因,与可用时同一入口。 */ + for (int a = 0; a < g_app_count && g_icon_count < MAX_ICONS; a++) { + if (!g_apps[a].linux_cmd) continue; + if (x > pad && x + KATE_ICON_W > max_x) { + x = pad; + y += KATE_ICON_H + KATE_ICON_GAP; + } + g_icons[g_icon_count].app_id = a; + g_icons[g_icon_count].x = x; + g_icons[g_icon_count].y = y; + g_icons[g_icon_count].label = g_apps[a].display_name; + g_icons[g_icon_count].selected = 0; + g_icon_count++; + x += KATE_ICON_W + KATE_ICON_GAP; + } } /* ============================================================ diff --git a/CODE/shell/main.c b/CODE/shell/main.c index 506a804..e665dc6 100644 --- a/CODE/shell/main.c +++ b/CODE/shell/main.c @@ -1846,8 +1846,9 @@ typedef struct { u32 type,flags; u64 offset,vaddr,paddr,filesz,memsz,align; } sh typedef struct { i64 tag; u64 val; } sh_elf64_dyn; typedef struct { u64 offset; u64 info; i64 addend; } sh_elf64_rela; -/* run 加载镜像缓冲(容纳工具 ELF 展开后的 memsz) */ -static u8 g_run_image[262144]; +/* run 加载镜像缓冲(容纳工具 ELF 展开后的 memsz;与 desktop 宿主 g_elf_image 对齐。 + * editor.elf 文件 43KB 但 BSS NOBITS 内存镜像 ~496KB,256KB 上限曾导致 sh_load_elf -5。) */ +static u8 g_run_image[1048576]; static int sh_load_elf(u8 *data, void **entry_out) { const sh_elf64_ehdr *eh = (const sh_elf64_ehdr *)data; @@ -3314,6 +3315,7 @@ void dsk_entry(const dsk_boot_context *ctx) { /* 主循环 */ int shift = 0; int e0 = 0; + int ctrl = 0; /* Ctrl 修饰键状态:0x1D 按下 / 0x9D 释放(右 Ctrl 为 E0 前缀同名扫描码) */ u64 blink_start = rdtsc_shell(); for (;;) { draw_prompt(); @@ -3350,7 +3352,11 @@ void dsk_entry(const dsk_boot_context *ctx) { if (sc == 0xE0) { e0 = 1; continue; } if (sc == 0x2A || sc == 0x36) { shift = 1; continue; } if (sc == 0xAA || sc == 0xB6) { shift = 0; continue; } + /* Ctrl 修饰键跟踪:左 Ctrl 按下 0x1D;右 Ctrl 为 E0 1D(e0 已置位,此处一并清除) */ + if (sc == 0x1D) { ctrl = 1; e0 = 0; continue; } if (sc & 0x80) { + /* 左 Ctrl 释放 0x9D;右 Ctrl 释放 E0 9D —— 两者 sc 均为 0x9D */ + if (sc == 0x9D) { ctrl = 0; } if (e0 && sc == 0x9C) { e0 = 0; } else if (e0 && sc == 0xCB) { e0 = 0; } else if (e0 && sc == 0xCD) { e0 = 0; } @@ -3396,9 +3402,10 @@ void dsk_entry(const dsk_boot_context *ctx) { if (sc == 0x0E) { delete_char_back(); continue; } if (sc == 0x0F) { handle_tab_complete(); continue; } if (sc == 0x01) { logl("[shell] esc -> return to DSK"); return; } - if (sc == 0x15 && !shift) { kill_line(); continue; } - if (sc == 0x17 && !shift) { while (input_cursor > 0) delete_char_back(); continue; } - if (sc == 0x0B && !shift) { kill_to_end(); continue; } + /* Ctrl 组合键:仅 Ctrl 按下时生效,普通键入 y/i/0 走下方 ASCII 插入(BUG-20260801-001) */ + if (sc == 0x15 && ctrl) { kill_line(); continue; } /* Ctrl+U: 清空整行 */ + if (sc == 0x17 && ctrl) { while (input_cursor > 0) delete_char_back(); continue; } /* Ctrl+W: 删至行首 */ + if (sc == 0x0B && ctrl) { kill_to_end(); continue; } /* Ctrl+K: 删至行尾 */ char c = scan_to_ascii(sc, shift); if (c && c >= 32 && c <= 126) { insert_char(c); diff --git a/CODE/shell/main.o b/CODE/shell/main.o index bdc7f0f..1451bb8 100644 Binary files a/CODE/shell/main.o and b/CODE/shell/main.o differ diff --git a/CODE/tools/desktop_app.h b/CODE/tools/desktop_app.h index 498700c..f0f7f59 100644 --- a/CODE/tools/desktop_app.h +++ b/CODE/tools/desktop_app.h @@ -258,14 +258,30 @@ typedef struct { int has_pkt; } da_mouse; +/* 冲刷 PS/2 控制器输出缓冲(最多 16 字节) */ +static inline void da_ps2_drain(void) { + for (int i = 0; i < 16; i++) { + if (!(inb(0x64) & 1)) break; + inb(0x60); + } +} + static inline void da_mouse_init(void) { + /* 必须先冲刷输出缓冲再发 0x20 读配置字:shell 启动本应用时最后键入的 + * 回车 break code(0x9C) 或鼠标流字节若残留在输出缓冲,会被误读为"配置字" + * 并写回控制器 —— 残留字节 bit4(0x10)=1 会置位 KBD 时钟禁用位,键盘被 + * 控制器级禁用且应用退出后仍不恢复(BUG-20260801-008)。 + * 复位/使能后的响应字节也必须全部读完(0xFF 返回 FA AA 00 共 3 字节), + * 否则残留字节同样会污染后续配置字读写。 + * 序列与 desktop/main.c:ps2_mouse_init 保持一致。 */ + da_ps2_drain(); for (int t = 0; t < 100000; t++) { if (!(inb(0x64) & 2)) break; } outb(0x64, 0xA8); for (int t = 0; t < 100000; t++) { if (!(inb(0x64) & 2)) break; } outb(0x64, 0x20); for (int t = 0; t < 100000; t++) { if (inb(0x64) & 1) break; } u8 cfg = inb(0x60); - cfg |= 0x02; cfg &= ~0x20; + cfg &= ~0x20; cfg |= 0x02; cfg |= 0x40; /* AUX 时钟启用 + AUX IRQ + 键盘翻译(集1, 与 desktop 一致) */ for (int t = 0; t < 100000; t++) { if (!(inb(0x64) & 2)) break; } outb(0x64, 0x60); for (int t = 0; t < 100000; t++) { if (!(inb(0x64) & 2)) break; } @@ -274,21 +290,33 @@ static inline void da_mouse_init(void) { outb(0x64, 0xD4); for (int t = 0; t < 100000; t++) { if (!(inb(0x64) & 2)) break; } outb(0x60, 0xFF); - for (int t = 0; t < 100000; t++) { if (inb(0x64) & 1) break; } - inb(0x60); + for (int i = 0; i < 8; i++) { /* 读完复位全部响应(FA AA 00) */ + int got = 0; + for (int t = 0; t < 100000; t++) { if (inb(0x64) & 1) { got = 1; break; } } + if (!got) break; + inb(0x60); + } for (int t = 0; t < 100000; t++) { if (!(inb(0x64) & 2)) break; } outb(0x64, 0xD4); for (int t = 0; t < 100000; t++) { if (!(inb(0x64) & 2)) break; } outb(0x60, 0xF4); - for (int t = 0; t < 100000; t++) { if (inb(0x64) & 1) break; } - inb(0x60); + for (int i = 0; i < 4; i++) { /* 读完使能 ACK */ + int got = 0; + for (int t = 0; t < 100000; t++) { if (inb(0x64) & 1) { got = 1; break; } } + if (!got) break; + inb(0x60); + } + da_ps2_drain(); } static inline int da_mouse_poll(da_mouse *m, da_cursor *c, i64 fb_w, i64 fb_h) { u8 st = inb(0x64); if (!(st & 1)) return 0; - u8 data = inb(0x60); + /* 必须先查 AUX 位(bit5)再读数据端口:键盘字节(st&0x20)==0 属于键盘路径, + * 此处若先 inb(0x60) 会把键盘扫描码吞掉(BUG-20260801-003)。 + * 与 desktop/main.c:ps2_mouse_poll 顺序保持一致。 */ if (!(st & 0x20)) return 0; + u8 data = inb(0x60); m->buf[m->idx++] = data; if (m->idx < 3) return 0; m->idx = 0; diff --git a/CODE/tools/pe-samples/cmd.exe b/CODE/tools/pe-samples/cmd.exe index b3ac238..8db14a2 100644 Binary files a/CODE/tools/pe-samples/cmd.exe and b/CODE/tools/pe-samples/cmd.exe differ diff --git a/CODE/tools/pe-samples/fault64.exe b/CODE/tools/pe-samples/fault64.exe new file mode 100644 index 0000000..128a799 Binary files /dev/null and b/CODE/tools/pe-samples/fault64.exe differ diff --git a/CODE/tools/pe-samples/gen_pe.py b/CODE/tools/pe-samples/gen_pe.py index f572201..bd560f4 100644 --- a/CODE/tools/pe-samples/gen_pe.py +++ b/CODE/tools/pe-samples/gen_pe.py @@ -128,7 +128,11 @@ def build_pe32_plus(): code_rva = 0x1000 # .text RVA main_rva = code_rva printf_iat_rva = code_rva + 0x200 # IAT 在 .text 后(同一 section 内,简化) - exitproc_iat_rva = printf_iat_rva + 8 + # 注意:每个 import descriptor 的 thunk 数组必须 NULL 终止。 + # printf thunk 占 0x200..0x207,0x208..0x20F 为 NULL 终止符, + # ExitProcess thunk 放 0x210(此前放 0x208 导致 desc0 的 thunk 数组 + # 无 NULL 终止、加载器把 ExitProcess 误配到 msvcrt.dll)。 + exitproc_iat_rva = printf_iat_rva + 16 # 修正 lea rcx, [rip + msg]: msg 在 code[19] 之后(lea 7字节 + call 6 + xor 2 + call 6 + ret 1 = 22) msg_offset_in_code = 22 @@ -140,30 +144,41 @@ def build_pe32_plus(): struct.pack_into(' + mov dword [rax], 1 ; C7 00 01 00 00 00 + jmp $ ; EB FE (兜底:万一未 fault 原地死循环) + 0x400000000000 是低位规范地址(bit47=0)且远超任何已映射区, + 写入必然 #PF(err=0x2, supervisor write non-present, cr2=0x400000000000)。 + 预期串口: "[IDT] exception" + vector=0xe + cr2=0x0000400000000000。 + 无导入表(pe_loader 对空 import directory 直接跳过)。 + """ + image_base = 0x140000000 + section_align = 0x1000 + file_align = 0x200 + + dos_header = bytearray(64) + struct.pack_into('= ' ' && ch <= 'z'`(0x7a)截断,导致 `{` `|` `}` `~` 显示为空格;改为 `ch > '~'`(0x7e)后覆盖全部 95 个可打印字符。 3. **布局偏移要跟随字高调整**: ASCII_H 从 16 变为 18 后,硬编码的 `y+(h-16)/2` 需改为 `y+(h-ASCII_H)/2`,wifi 行的 `y+17`(选中标记居中)需改为 `y+16`((50-18)/2=16)。 +## 2026-08-03 — B7 DKM 侧 LAPIC + IOAPIC 接管中断路由(PIC→APIC 迁移第一步) + +### 经验 + +1. **tick 链路自验是路由切换的最强证据**: 切换后用 PIT ch0 mode3 100Hz 作真实中断源,统计 handler 调用次数(阈值 >=2)。重复投递证明 LAPIC EOI 正确(无 EOI 时 edge/level 都只投递一次),比"读寄存器确认"可信得多。验证窗口用 TSC deadline 实现,不依赖任何中断。 +2. **先保存再切换,失败完整降级**: 切换前保存 PIC IMR(0x21/0xA1),验证失败时 IOAPIC 全 mask + 恢复 IMR + 注销 handler + PIT 恢复 18.2Hz,系统无感回到 PIC 路由。stage0 时机早,此时所有 stage1-3 驱动尚未加载,降级零成本。 +3. **FUCK 配置自解析 + 双开关**: `apic_route`(主开关,默认 0 保持 PIC)与 `apic_route_legacy`(legacy 线实验开关)分离,默认行为与旧版本完全一致,回归风险为零。 +4. **tick vector 选 0xE0 避开了 LAPIC ISR 卡位陷阱**: LAPIC 规则要求新中断优先级 (vec>>4) 严格高于当前 ISR 才投递。若 tick 选 0x20-0x2F(优先级 2),一旦有无 EOI 的 0x2x 中断先卡位,tick 将永远静默,验证必失败。0xE0(优先级 14)无此问题。 +5. **MADT ISO 必须查表,不能假设恒等**: QEMU q35 的 PIT IRQ0→GSI2(非恒等),键盘 IRQ1→GSI1(恒等)。`isa_irq_to_gsi` 先查 ISO 再回退恒等,两种拓扑都正确。 +6. **验证脚本参数化**: w3c_route0/1.bat 固定 monitor 端口(45701/45702)+ TMP/TEMP 指向项目内目录(解决 edk2 pflash overlay 在中文用户名 Temp 下创建失败)+ `-snapshot` 保护镜像。 + +### 教训 + +1. **IOREDTBL 位布局抄错规范是隐蔽杀手**: 初版把 polarity/trigger/mask 错置于 bit 11/13/14(正确为 13/15/16)。mask 写入只读 remote IRR 位从未生效,"all masked" 名不副实;readback 读 remote IRR 恒 0,给出虚假证据。QEMU(全 active-high edge ISA)下功能侥幸正常,但真机 level/active-low PCI 线必炸。**硬件寄存器位定义必须对照原始 datasheet(82093AA),注释与代码同步修正,readback 证据必须与 claim 交叉验证。** +2. **build.ps1 漏编 apic.drv 导致修改不生效**: 原 `driver/platform/apic.drv` 是预置二进制,改 apic.c 后镜像不变。任何"驱动源码修改后行为无变化"都应先检查构建脚本是否真的编译了该驱动。 +3. **QEMU pflash overlay 依赖 %TEMP%**: 中文用户名路径下临时 overlay 创建失败(`vl.XXXXXX: No such file or directory`),必须显式设置 TMP/TEMP 到纯 ASCII 路径。 +4. **PIT mode3 计数器每 CLK 减 2**: 用 PIT 校准 TSC 时若按"减 1"假设,tsc_per_ms 偏小 2 倍,所有派生 delay 翻倍。校准代码必须考虑 mode2/mode3 差异(本轮 tick 计数 100@500ms 即为 2x 观察,功能不受影响但时序类代码需注意)。 + +## 2026-08-03 — B7 阶段2 UTSM 侧 LAPIC EOI 钩子落地 + legacy IRQ 持续投递 + tick 2x HPET 定论 + +### 经验 + +1. **EOI 钩子经 kernel_api 尾部追加是 ABI 安全的最小改动**: `register_apic_eoi` 追加在 `dkm_kernel_api` 末尾(不动既有字段偏移),UTSM 侧 `g_apic_eoi_hook` 为 NULL 时跳过调用,PIC 模式行为与旧版逐字节一致。新内核+旧驱动(钩子 NULL)与旧内核+新驱动(驱动判空跳过)双向兼容。 +2. **handler 返回码区分"自 EOI"与"钩子兜底"**: idt_handler 两个 IRQ 分支(PIC 0x20-0x2F / APIC 0x30-0xFF)约定 handler 返回 1 = 已自行 LAPIC EOI(如阶段1 的 0xE0 tick handler),返回 0 = 由钩子补 EOI 安全网。分层职责让 handler 写法自由,链路永不漏 EOI。 +3. **tick 验证 vector 从 0xE0 迁回 0x20 才能证明 legacy 分支**: 阶段1 用 0xE0 自 EOI 只证明了 APIC 分支(0x30+);阶段2 把 tick handler 迁到 0x20(PIC 分支),与键盘 0x21/鼠标 0x2C 同一代码路径,500ms 内重复投递 101 次即证明 "legacy vector 分支 + 钩子 EOI" 全链路可持续。 +4. **spurious 向量 0xFF 必须特判跳过 EOI 钩子**: LAPIC spurious(SVR 低 8 位)规范上不置 ISR 位、永不需 EOI;若对 0xFF 也调钩子写 EOI,嵌套场景下会误清真正 in-service 的最高优先级位。 +5. **HPET 主计数器是 QEMU 下最干净的绝对时基**: 解析 ACPI HPET 表 → MMIO 主计数器(100MHz,周期从 GCAP_ID[63:32] 读飞秒值)→ 直接换算 us,不依赖 PIT/TSC 任何先验。tick 2x 的两个候选假设(TSC 校准测半 vs PIT mode3 双触发)用一次对拍即定案:calib 窗口 10132us 准确 + verify 窗口 504883us 准确 + 实测 200Hz → PIT mode3 方波全周期双沿触发定论(详见 BUG-20260803-009)。 +6. **观察窗长度必须覆盖注入链路延迟**: "内核打锚点 → 串口落盘(块缓冲)→ 测试脚本轮询可见 → monitor 注入" 链路延迟 ~1s 量级,3s 窗口实测被吃光(注入到达时窗口已关闭,计数=0),6000ms 才稳定拿到注入数据(鼠标 27 包 / 键盘 8 包)。 + +### 教训 + +1. **PS/2 控制器交互期间绝不能有 IRQ handler 偷读 0x60**: diag handler 先于 `ps2_aux_enable_stream()` 注册时,控制器命令响应字节(0xFA ACK 等)被 handler 当数据包读走,轮询侧 wait_output 超时(表现为 F3 nack 假象,实为字节被偷)。铁律:控制器命令序列全轮询完成后再注册 handler;交互期间到达的 IRQ 因 handler 未注册只会被 EOI 丢弃,无副作用。 +2. **真机级别的寄存器语义要按设备类强制**: PCI INTx(e1000 IRQ11)无 MADT ISO 覆盖时必须强制 level/active-low(PCI 规范 + QEMU pci irq 电平语义 + Linux/Windows 同配置),ISA 线才按 ISO 表缺省 edge/high;一概套 ISO 缺省会让 PCI 设备中断一次性投递后卡死。 +3. **验证脚本数组参数经 -File 传递会被合并**: `powershell -File xxx.ps1 -Scenarios @('B1','B2')` 中数组变单字符串 "B1 B2"(场景被跳过且报"未知场景"),需用 `powershell -Command "& 'xxx.ps1' -Scenarios 'B1','B2'"`。 + diff --git a/SYSTEM/LINUXAPP.CNF b/SYSTEM/LINUXAPP.CNF new file mode 100644 index 0000000..64ad1bb --- /dev/null +++ b/SYSTEM/LINUXAPP.CNF @@ -0,0 +1,8 @@ +# Deshab Linux desktop apps (LINUXAPP.CNF) +# Format per line: DISPLAY_NAME|linux command +# - '#' starts a comment, blank lines are ignored +# - display name <= 11 chars, command <= 63 chars, args split on spaces +# - command runs in the Linux guest via the compat layer (see dsl command) +HELLO|/bin/echo hello from deshab linux +UNAME|/bin/uname -a +LS|/bin/ls -l / diff --git a/SYSTEM/bin/AUTOEXEC.BAT b/SYSTEM/bin/AUTOEXEC.BAT index 573a207..4a336b6 100644 --- a/SYSTEM/bin/AUTOEXEC.BAT +++ b/SYSTEM/bin/AUTOEXEC.BAT @@ -1,2 +1,2 @@ -:: Deshab CMD 启动批处理 — 自动运行 Deaicup Software PE64 GUI -deaicup +ver +exit diff --git a/SYSTEM/bin/cmd.exe b/SYSTEM/bin/cmd.exe index b3ac238..8db14a2 100644 Binary files a/SYSTEM/bin/cmd.exe and b/SYSTEM/bin/cmd.exe differ diff --git a/SYSTEM/bin/deaicup.exe b/SYSTEM/bin/deaicup.exe index 59cb1eb..80a9f51 100644 Binary files a/SYSTEM/bin/deaicup.exe and b/SYSTEM/bin/deaicup.exe differ diff --git a/SYSTEM/bin/fault64.exe b/SYSTEM/bin/fault64.exe new file mode 100644 index 0000000..128a799 Binary files /dev/null and b/SYSTEM/bin/fault64.exe differ diff --git a/SYSTEM/bin/hello32.exe b/SYSTEM/bin/hello32.exe index 9cd27ea..7f08b68 100644 Binary files a/SYSTEM/bin/hello32.exe and b/SYSTEM/bin/hello32.exe differ diff --git a/SYSTEM/bin/hello64.exe b/SYSTEM/bin/hello64.exe index cfb10a8..6ef4cf4 100644 Binary files a/SYSTEM/bin/hello64.exe and b/SYSTEM/bin/hello64.exe differ diff --git a/SYSTEM/boot/utsm.elf b/SYSTEM/boot/utsm.elf index 9a1d425..d86f80e 100644 Binary files a/SYSTEM/boot/utsm.elf and b/SYSTEM/boot/utsm.elf differ diff --git a/SYSTEM/driver/block/nvme.drv b/SYSTEM/driver/block/nvme.drv index 7f26570..2413d9e 100644 Binary files a/SYSTEM/driver/block/nvme.drv and b/SYSTEM/driver/block/nvme.drv differ diff --git a/SYSTEM/driver/platform/apic.drv b/SYSTEM/driver/platform/apic.drv index 961620e..a485043 100644 Binary files a/SYSTEM/driver/platform/apic.drv and b/SYSTEM/driver/platform/apic.drv differ diff --git a/SYSTEM/system/deshab64/FUCK b/SYSTEM/system/deshab64/FUCK index d0c5264..65a46e5 100644 --- a/SYSTEM/system/deshab64/FUCK +++ b/SYSTEM/system/deshab64/FUCK @@ -40,6 +40,28 @@ ps2kbd=1 ioapic_init=1 # lapic_enable=0 可禁用 LAPIC software enable (留 firmware 状态) lapic_enable=1 +# B7: APIC 路由接管开关 (apic.drv 自解析, 缺失=0) +# 0=保持 8259 PIC 路由 (默认, 现状不回归) +# 1=IOAPIC->LAPIC 接管: 注册 LAPIC EOI 钩子 + PIC IMR 全屏蔽 + TPR=0 +# + tick 链路自验 (vector 0x20, 失败自动完整降级回 PIC 并注销钩子) +apic_route=0 +# B7 阶段2: 随 apic_route=1 额外放开 legacy IRQ1(键盘)/12(鼠标)/11(e1000) +# UTSM EOI 钩子已落地, 这些线可持续投递 (ISA 线 edge/high, e1000 强制 +# level/active-low 按 PCI INTx 规范), 默认 0 关闭 +# QEMU 全链路已验证 (B1 向导按键/B2 启动/e1000 IRQ11), 真机冒烟前可回 0 +apic_route_legacy=1 +# B7 阶段3: NVMe MSI/MSI-X 中断驱动开关 (nvme.drv 自解析, 缺失=1) +# 1=自动尝试: 先 MSI cap, 缺则 MSI-X (QEMU nvme 只有 MSI-X) — 表项编程 +# + 动态向量 (0x40-0xDF 池) + Create IOCQ IEN=1/IV=0 + INTMC 解除; +# 任何前提缺失 (无 MSI/MSI-X cap/旧内核无向量分配器/回读不符) +# 自动回退纯轮询, 行为与旧版一致 +# 0=强制纯轮询 (CQE phase bit), 与 B7 阶段3 之前完全一致 +nvme_msi=1 +# B7 诊断 (apic.drv): 1=启用 HPET 绝对时基对拍 (tick 2x 定论实验) + +# IRQ12 鼠标流观察窗 (配合 QEMU monitor mouse_move), 只增观测不改路由语义 +# 每次启动 +10s (6s 观察窗), 定论已完成 (PIT mode3 IOAPIC edge 双触发 +# ~200Hz), 常规构建保持 0 +apic_diag=0 [utsm] max_segments=1024 diff --git a/SYSTEM/system/deshab64/desktop.elf b/SYSTEM/system/deshab64/desktop.elf index ef63007..1f2dcc7 100644 Binary files a/SYSTEM/system/deshab64/desktop.elf and b/SYSTEM/system/deshab64/desktop.elf differ diff --git a/SYSTEM/system/deshab64/shell.elf b/SYSTEM/system/deshab64/shell.elf index 753c059..5e650b8 100644 Binary files a/SYSTEM/system/deshab64/shell.elf and b/SYSTEM/system/deshab64/shell.elf differ diff --git a/SYSTEM/system/deshab64/tools/browser.elf b/SYSTEM/system/deshab64/tools/browser.elf index ad04cba..17b23ee 100644 Binary files a/SYSTEM/system/deshab64/tools/browser.elf and b/SYSTEM/system/deshab64/tools/browser.elf differ diff --git a/SYSTEM/system/deshab64/tools/fileman.elf b/SYSTEM/system/deshab64/tools/fileman.elf index 00de5c5..b20c1d1 100644 Binary files a/SYSTEM/system/deshab64/tools/fileman.elf and b/SYSTEM/system/deshab64/tools/fileman.elf differ diff --git a/SYSTEM/system/user/use/firstInit.txt b/SYSTEM/system/user/use/firstInit.txt index 56a6051..0bc999a 100644 --- a/SYSTEM/system/user/use/firstInit.txt +++ b/SYSTEM/system/user/use/firstInit.txt @@ -1 +1,2 @@ +1 1 \ No newline at end of file diff --git a/build.ps1 b/build.ps1 index be54972..dfee511 100644 --- a/build.ps1 +++ b/build.ps1 @@ -672,6 +672,10 @@ foreach ($tool in $toolApps) { Write-Host '[build] Building DKM framebuffer driver...' Build-DkmDriver (Join-Path $Root 'CODE\DKM\console_fb\console_fb.c') (Join-Path $SystemDir 'driver\console\console_fb.drv') +# B7: apic.drv 纳入构建 (原 platform/apic.drv 为预置二进制, 不随 apic.c 更新) +Write-Host '[build] Building DKM platform apic driver...' +Build-DkmDriver (Join-Path $Root 'CODE\DKM\apic\apic.c') (Join-Path $SystemDir 'driver\platform\apic.drv') + Write-Host '[build] Building DKM storage drivers...' Build-DkmDriver (Join-Path $Root 'CODE\DKM\nvme\nvme.c') (Join-Path $SystemDir 'driver\block\nvme.drv') Build-DkmDriver (Join-Path $Root 'CODE\DKM\xhci\xhci.c') (Join-Path $SystemDir 'driver\block\xhci.drv') diff --git a/debug-pe-compat-p1.md b/debug-pe-compat-p1.md new file mode 100644 index 0000000..5bc9cc5 --- /dev/null +++ b/debug-pe-compat-p1.md @@ -0,0 +1,48 @@ +# Debug Session: pe-compat-p1 + +- Session ID: `pe-compat-p1` +- 创建: 2026-08-03 +- 状态: [OPEN] +- 范围: BUG-20260801-005 / 006 / 007(PE 兼容层三条 P1)修复 + QEMU 端到端验证 + +## 前置证据状态 + +三条缺陷**根因已定案**(非猜测),证据链完整登记于 `docs/RE/Bug记录.txt`: + +| Bug | 定案证据 | 根因一句话 | +|-----|---------|-----------| +| 006 | QEMU `-d int` v=0e e=0003 + utsm.elf 反汇编(`pe_shim_setup_exit` 尾声从脏栈 pop 7 槽)+ R12/R13= log_hex64 缓冲区 ASCII 残片 | `pe_shim_setup_exit()` 内 `__builtin_setjmp` 后函数即返回,longjmp 跨已返回帧 = C 标准 UB | +| 007 | `-d int` v=0e RIP 落在 shim_printf 体内(反汇编确认);串口止于 "[PE] running PE32 via x86emu32" | shim 表 fn32 列全 NULL,fn64 回退把 32 位 guest VA(0x401017)当 host 指针解引用 | +| 005 | `-d int`: `old:0xe new:0xd`(投递 #PF 即 #GP)→ `old:0x8 new:0xd` → Triple | `idt_halt_all()` 静默 halt 门在 DSK 阶段异常无法投递,零诊断 | + +本会话不再重复根因调查,直接进入**修复实施 → 修复后对比验证**阶段。 +可证伪假设(针对修复设计的验证点,非根因猜测): + +- **H1 (006)**: `__builtin_setjmp` 下沉到 `pe_service_run` 本体(PE 执行全程存活的帧)后,longjmp 恢复的 RSP/RBP/R12-15 来自存活帧,HELLO64 能干净走完 `[PE] ExitProcess code=` → `[cmd] PE run end`。 + 证伪条件: 若仍三重故障 → pe_call_on_stack 切栈与 longjmp 存在第二处交互问题。 +- **H2 (007)**: `g_emu_mem_base` + 各 shim 解引用点 `gp()` 转换(guest VA < 0x10000000 时加 emu->mem 基址)可让 hello32 `printf(0x401017)` 输出 "Hello from PE32!"。 + 证伪条件: 输出乱码=gp 遗漏解引用点;仍 #PF=base 设置时机/阈值有误。 +- **H3 (005)**: DSK 交接以诊断 IDT(0-31→isr_stub 走 idt_handler 打现场;32-255→halt stub 保持 IRQ 防御)替换全静默 halt 后,故意 #PF 串口可见 `[IDT] exception vector=0xe cr2=...`。 + 证伪条件: 仍三重故障 → DSK 阶段投递机制存在更深层问题(GDT/TSS),需 `-d int` 再定位。 + 旁证: BUG-20260729-013 登记行为表明 halt_all 引入**之前** DSK 阶段异常能送达 idt_handler(isr_stub 门投递在 DSK 阶段是已验证可用路径)。 +- **H4 (回归)**: `gp()` 在 `g_emu_mem_base=0`(原生路径)为恒等映射,HELLO64/Deaicup/cmd.exe 行为不变。 + 证伪条件: Deaicup 首帧 BitBlt 消失或输出异常。 + +## 修复方案(与 Bug 记录方向一致) + +1. **006**: pe_shim.c 删除 `pe_shim_setup_exit()` 包装;新增 `pe_shim_exit_jmpbuf()` 返回 jmpbuf 指针;pe_service.c 两条路径(PE32+/PE32)在 `pe_service_run` 本体内直接 `__builtin_setjmp(pe_shim_exit_jmpbuf())`。 +2. **007**: pe_shim.c 新增 `g_emu_mem_base` + `pe_shim_set_emu_base()` + `gp()` 内联;x86emu32.c `try_iat_call` 调 shim 前设 base;各 shim 在**指针解引用点**(非参数入口盲目转换——避免把退出码/长度等小整数误转)套 gp()。user32/gdi32 结构体布局 32/64 位不同,PE32 GUI 明确不在本轮范围。 +3. **005**: idt.c 新增 `idt_install_dsk_diag()`(0-31 diag + 32-255 halt + lidt),`idt_install_pe_diag` 改为其别名;dsk_loader.c 交接处以 `idt_install_dsk_diag()` 替换 `idt_halt_all()`;gen_pe.py 新增 `fault64.exe`(`mov [0x10],imm` 故意 #PF)作验收样本,pe-compat.ps1 加会话 D 断言 `[IDT] exception`。 + +## 修复日志 + +(实施中填写) + +## 验证记录 + +(QEMU 实测后填写 pre/post 对比) + +## 清理清单 + +- [ ] debug 文件收尾(用户确认后归档状态) +- [ ] Bug记录.txt 三条状态 OPEN→FIXED,填修复提交与验证结果 diff --git a/docs/RE/Bug记录.txt b/docs/RE/Bug记录.txt index 2c98925..b36056e 100644 --- a/docs/RE/Bug记录.txt +++ b/docs/RE/Bug记录.txt @@ -33,6 +33,107 @@ +### BUG-20260803-009 [P3] [DKM/apic] PIT mode3 方波在 IOAPIC edge 触发下每周期双投递:100Hz 设定实测 200Hz(tick 2x 定论) +- 现象: apic_route=1 接管验证时,500ms 窗口数到 ~100 ticks(PIT ch0 mode3 设定 100Hz,预期 50);tick handler 计数恰为 2 倍,且持续投递窗口(sustain)同样双倍。 +- 复现步骤: FUCK [drivers] apic_route=1 启动 → ROUTE VERIFY 阶段计数复现;apic_diag=1 时 HPET 对拍实验自动输出定论。 +- 串口/日志证据: `.build_tmp/tests/apicroute-b7p2-071006.log`:`calib window(us)=10132` / `verify window(us)=504883` / `tick rate(Hz x10)=2000` / `route verify ticks=101` / `sustain window ticks=100` / `TICK2X VERDICT: PIT mode3 double-clocked on IOAPIC edge (~200Hz real)`。 +- 根因分析: HPET 绝对时基(ACPI HPET 表 → MMIO 主计数器,QEMU 100MHz)对拍逐一排除候选假设:① "TSC 校准测半"——calib 窗口实测 10132us ≈ 10.13ms,准确,排除;② "验证窗口偏长"——实测 504883us ≈ 504.9ms,准确,排除。窗口与校时皆正确而计数双倍,定论为 PIT mode3(方波发生器)OUT 引脚每个全周期含上升沿+下降沿各一次翻转,QEMU IOAPIC edge 触发重定向项的输入采样等效双沿触发 → 每 PIT 周期产生 2 次中断,真实投递频率 ~200Hz。与 LESSONS_LEARNED 2026-08-03 B7 阶段1 记录的 "PIT mode3 计数器每 CLK 减 2"(同一硬件特性:mode3 半周期翻转)互证。 +- 修复方案: 无需修复(观测性定论,非功能缺陷;EOI 钩子链路与窗口校时均验证正确)。后续约束:① route 模式下凡按 PIT 中断次数计时的代码须按 200Hz 折算,或改用 PIT mode2(rate generator,每周期单脉冲)根除双触发;② 时延派生一律走 HPET/TSC 绝对时基,禁止用 tick 计数当时间源;③ apic_diag 开关保留(FUCK 默认 0),真机冒烟时可复开对拍。 +- 状态: [CLOSED] +- 修复提交: 工作区(CODE/DKM/apic/apic.c:HPET 表解析 + 主计数器时基 + TICK2X VERDICT 对拍实验) +- 验证: apic-route-verify.ps1 18 项全 PASS(route=1+legacy=1+diag=1);boot-regression.ps1 B1-B5 全 PASS(route=1+legacy=1+diag=0,B1 向导按键全程走 IOAPIC→LAPIC→IDT→ps2kbd 链路);route=0 回归 B1+B2 PASS(PIC 锚点在、钩子未注册、行为与旧版一致)。 + +### BUG-20260801-008 [P1] [tools/desktop_app.h] da_mouse_init 读配置字前未冲刷输出缓冲:残留字节写回控制器配置 → KBD 时钟禁用位被置位 → 键盘控制器级全灭且跨应用持续 +- 现象: dev_mode shell 中 `run FILEMAN.ELF`(或 BROWSER.ELF)后应用内键盘全灭:Esc/方向键/回车均无响应;鼠标移动/点击正常。退出应用返回 shell 后键盘仍不恢复(会话级持续,直至重启)。editor(不调用 da_mouse_init)与启动初期 shell 键盘正常。 +- 复现步骤: dev_mode shell → 键入 `run FILEMAN.ELF` 回车 → fileman 内按 Esc/方向键 → 无响应、串口零输出。 +- 串口/日志证据: `.build_tmp/w3b_s2_tools.log`:`[fileman] boot` 后 monitor sendkey esc/down/ret 串口零增量、screendump 选区不动;mouse_move 光标正常移动、mouse_button 点击可选中条目(FIRSTINT.XT 高亮)并可点关闭按钮退出(`[fileman] close button clicked` → `[shell] run: tool returned`);退回 shell 后 sendkey i/d/ret 仍零输出(键盘未恢复)。运行中 fileman.elf 反汇编证实 da_mouse_poll 与键盘轮询指令顺序均正确(`test al,0x21` 后才 `in 0x60`),排除 BUG-20260801-003 残留。 +- 根因分析: da_mouse_init 序列中 `outb(0x64,0x20)`(读控制器配置字)之前未冲刷输出缓冲。shell 启动应用时最后键入的回车 break code(0x9C)或鼠标流字节残留于输出缓冲 → OBF 等待立即返回、`inb(0x60)` 读到残留字节而非配置字 → `cfg|=0x02; cfg&=~0x20` 后写回 → 残留字节 bit4(0x10)=1(0x9C 即满足)置位配置字 KBD 时钟禁用位 → i8042 控制器级禁用键盘,且应用退出后无人恢复该配置 → 会话级键盘全灭。另:鼠标复位(0xFF)实际返回 FA AA 00 共 3 字节,原实现仅读 1 字节,残留响应同样可污染后续配置字读写。desktop/main.c:ps2_mouse_init(键盘鼠标长期共存的参照实现)首尾均有 ps2_drain 且读完全部响应,故桌面不受影响。 +- 修复方案: da_mouse_init 对齐 desktop/main.c:ps2_mouse_init:①序列开头新增 da_ps2_drain() 冲刷残留;②配置字 `cfg &= ~0x20; cfg |= 0x02; cfg |= 0x40`(补键盘翻译位,与 desktop/mouseInit 0x47 一致);③复位后有界读完 FA AA 00 全部响应、使能数据报告后读完 ACK;④结尾再次 da_ps2_drain()。 +- 状态: [FIXED] +- 修复提交: 工作区已修复(CODE/tools/desktop_app.h da_mouse_init + 新增 da_ps2_drain),2026-08-04 全量构建闭环(BUILD.lock 互斥):fileman.elf 46848→46888B、browser.elf 48896→48928B、editor.elf 同步重编(MAKEFILE 无头文件依赖,已删 .o 强制重编),ISO/deshab.img 768MB 重打包 + SATA 镜像重建,构建零警告。 +- 验证: 2026-08-04 QEMU 验收 12/12 ALL PASS(`.build_tmp/w4a/w4a_bug008.log`,-snapshot,monitor 45601,WHPX):dev_mode shell `run FILEMAN.ELF` → `[fileman] boot`(L4794)→ sendkey 方向键选区移动(screendump w4a_fm1/fm2.ppm 像素差异采样 1831)→ ret 打开条目(w4a_fm3.ppm 差异 2170)→ esc → `[fileman] exit`(L4803)→ `[shell] run: tool returned`(L4804)→ 键入 `id` → `uid=0(root) gid=0(root)`(L4806,键盘跨应用存活闭环);`run BROWSER.ELF` → `[browser] boot`(L5197)→ esc → `[browser] exit`(L5198)→ tool returned(L5199)→ `id` → `uid=0(root)`(L5201)同链路通过;渲染 screendump w4a_fm1/fm2/fm3/br1/shell.ppm 均正常。回归:boot-regression.ps1 PASS(257.8s,B1 首启向导/B2 正常启动/B3 dev_mode/B4 无盘降级/B5 无 NIC 全过,无 [PANIC]);userapps-matrix.ps1 系统行为全 PASS(dev_tests/help/pci 存活/Esc→cmd→desktop 链路/fileman boot/browser boot),唯一 FAIL 为 editor 断言陈旧——脚本仍期待 BUG-002 修复前的 "run: ELF 加载失败",实测 `[editor] boot`→`[editor] exit` 正常(userapps-matrix-125211.log L5066-5067),该断言升级属测试脚本维护范畴,非系统回归。 + +### BUG-20260801-007 [P1] [UTSM/pe] PE32 解释器 shim fn64 回退把 guest 指针当 host 指针:shim_printf 解引用 0x401017 → #PF → 三重故障 +- 现象: `pe HELLO32.EXE` 解释执行时串口止于 "[PE] running PE32 via x86emu32",无 "Hello from PE32!",guest 三重故障,QEMU(-no-reboot)静默退出。TCG 与 WHPX 表现一致。 +- 复现步骤: x86emu32-coverage.ps1(AUTOEXEC `pe HELLO32.EXE`)→ 会话在 "[PE] running PE32 via x86emu32" 后立即退出。 +- 串口/日志证据: `.build_tmp/tests/x86emu32-tcg-134351.log` 尾部止于 "[PE] running PE32 via x86emu32";此前 QEMU `-d int` 捕获 v=0e(#PF)RIP 落在 shim_printf 函数体内(utsm.elf 反汇编确认),无 "[PE32] emulator error"——**解释器本身无差错,故障在 host 侧 shim**。 +- 根因分析(静态全链定案): + 1. pe_shim.c shim 表条目第 4 初始化元全为 0(如 `{"msvcrt.dll","printf",(void*)shim_printf,0}` 只设 fn64)→ **全表 fn32=NULL,pe_shim.h 设计的 fn32 列("从模拟栈读参数调用 fn32")整体缺失**。 + 2. x86emu32.c try_iat_call() 拦截 CALL IAT(合成地址 0x0001xxxx)后 fn32 为空 → 走 fn64 回退 `f(a0,a1,a2,a3)`,a0..a3 是从模拟栈读出的**原始 32 位 guest 值**:hello32 的 `push 0x401017`(msg guest VA = image_base 0x400000 + 0x1017)→ a0=0x401017。 + 3. shim_printf(pe_shim.c L219)`const char *f = (const char *)fmt;` 把 guest VA 直接当 host 指针解引用,**无 guest→host 转换**(解释器映像驻留在 emu->mem 大缓冲区,正确地址应为 emu->mem + 0x401017)→ host 低地址 0x401017 未映射 → #PF → BUG-20260801-005 放大为三重故障。 + 拦截前链路均正确:0x68 push imm32、FF /2 `call [disp32]` ModRM 解码、read_rm32 读 IAT 合成地址、try_iat_call 参数读取——解释器覆盖率不记倒退。 +- 修复方案(系统侧,本轮测试任务约束不改系统代码,仅登记): ①为每个含指针参数的 shim 实现 fn32(接收 esp_ptr=&emu->mem[esp],guest VA 经 emu->mem 基址转换为 host 指针);②或 fn64 回退前设置全局 g_emu_mem_base,shim 内对 <0x10000000 的指针统一加基址转换;③kernel32 WriteFile/ReadFile 等指针参数函数同理。 +- 状态: [OPEN] +- 修复提交: 待提交 +- 验证: 修复后 x86emu32-coverage.ps1 应见 "Hello from PE32!" → "[cmd] PE run end" → "[DSK] cmd.elf returned";pe-compat.ps1 会话 B 同步验证(两处均已留自动升级断言:日志出现即升 PASS)。 + +### BUG-20260801-006 [P1] [UTSM/pe] pe_shim setjmp/longjmp 跨已返回函数 UB:longjmp 恢复脏栈寄存器 → 野指针 call → 三重故障 +- 现象: `pe HELLO64.EXE` 原生执行成功打印 "Hello from PE32+!"(BUG-004 修复后),随后在 ExitProcess 阶段 guest 三重故障,QEMU(-no-reboot)静默退出;后续 AUTOEXEC 命令(HELLO32/NONEXIST/exit)全部不再执行。TCG 与 WHPX 表现一致。 +- 复现步骤: pe-compat.ps1(AUTOEXEC `pe HELLO64.EXE`)→ 串口止于 "Hello from PE32+!"。 +- 串口/日志证据: QEMU `-d int` 追踪(`.build_tmp/tests/pe-int-dbg.log`): + `v=0e e=0003 IP=0028:ffffffff80014796 SP=0030:ffffffff80003030 CR2=ffffffff80003028`(**写保护违例**,非缺页)→ 投递 #PF 即 #GP(BUG-005 放大)→ v=08 → Triple fault。 + 现场寄存器:R12=0x6666666666667830("0xfffffff")、R13=0x3033326530386666("ff80e230")——正是 log_hex64("[PE] stack_top=",0xffffffff80e23000) 格式化缓冲区 ASCII 残片;R14/R15/RBP 均为脏值。 + 反汇编定案(utsm.elf):fault RIP `pe_service_run+0x226` = `call *%r15`(pe_call_on_stack 内联汇编调 PE 入口那条);但 R15=0xffffffff8055b260 并非 PE 入口 0xffffffff80d22000、RSP=0xffffffff80003030 并非 PE 栈顶 0xffffffff80e23000——**这是 longjmp 用脏栈"恢复"出来的假现场**,不是正常调用路径。 + pe_shim_setup_exit(0xffffffff80014d60)反汇编:clang 把 __builtin_setjmp 实现为 `g_exit_jmp={RBP,RSP,resume=0x80014d94}`,恢复后从 0x80014d94 起执行函数尾声 `pop rbx;pop r12;pop r13;pop r14;pop r15;pop rbp;ret`——**从恢复的栈上弹 7 个槽**。 +- 根因分析: pe_shim_setup_exit() 内调 __builtin_setjmp 后**即返回**(C 标准:含 setjmp 的函数已返回时再 longjmp 为未定义行为)。setjmp→longjmp 之间 pe_service_run 又调用 log_hex64×2("[PE] entry="/"[PE] stack_top="),其栈帧**精确复用** pe_shim_setup_exit 尾声要弹的那片栈区 → longjmp 尾声 pop 出的 rbx/r12-r15/rbp 全是 log_hex64 帧残片(R12/R13 即十六进制字符串残片),最后 `ret` 弹到 [X-8] 槽位的残留返回地址 0x8001478c(log_hex64 调用点返回地址)→ 落到 pe_call_on_stack 内联汇编正中间(`mov %rsp,g_pe_saved_rsp; mov %r14,%rsp; call *%r15`)→ 以脏 R14 为栈、脏 R15 为入口野调用 → 写返回地址到只读页 0xffffffff80003028 → #PF(保护违例) → BUG-005 放大为三重故障。PE32 解释器路径同一 setjmp/longjmp 机制,同样暴露在 UB 下(只是中间无 log 调用、脏数据不同,崩溃形态可能不同)。 +- 修复方案(系统侧,本轮测试任务约束不改系统代码,仅登记): ①把 setjmp 下沉到 pe_service_run 本体内(含 PE 调用全程存活的作用域),不要用"setjmp 包装函数";②或弃用 __builtin_setjmp/longjmp,改用显式上下文保存结构(保存 callee-saved + RSP/RIP 到 pe_service_run 帧内,ExitProcess 经受控 trampoline 返航);③PE32 路径同理。 +- 状态: [OPEN] +- 修复提交: 待提交 +- 验证: 修复后 pe-compat.ps1 应见 "[PE] ExitProcess code=" → "[cmd] PE run end" → "[DSK] cmd.elf returned" 完整链路;x86emu32-coverage.ps1 动态链路同步验证。 + +### BUG-20260801-004 [P1] [tools/pe-samples] gen_pe.py PE32+ 二进制双重构造缺陷 → 原生执行三重故障 +- 现象: `pe HELLO64.EXE` 原生执行时 guest 直接三重故障,QEMU(-no-reboot)进程退出;串口最后一行为 `[PE] stack_top=...`,无 "Hello from PE32+!"。WHPX 下表现为 "Unexpected VP exit code 4",TCG 下同样退出(此前误判为 WHPX 平台限制)。 +- 复现步骤: pe-compat.ps1(AUTOEXEC `pe HELLO64.EXE`)→ 会话在 `[PE] running PE32+ natively` 后退出。 +- 串口/日志证据: QEMU `-d int,cpu_reset` 追踪(`.build_tmp/pe-dbg-qemu.log` 尾部):`v=0e e=0010 IP=0028:...0300 CR2=0x300`(在 0x300 取指缺页)→ 投递 #PF 时 #GP → `v=08` → Triple fault。RIP=0x300 正是未被 shim 修补的 IAT[0] 原始值(printf 名称 RVA)。 +- 根因分析(双重缺陷,均属 gen_pe.py 构造错误): + 1. **call disp32 写错偏移**:call 指令位于 code[7..12](FF 15 + disp32),disp32 应写偏移 9,实际写偏移 10 → disp 左移一字节变为 0x1F300,同时覆盖下一条指令首字节。 + 2. **import 区 RVA 全部缺 +0x1000(code_rva)偏置**(修复①后仍失败的真正主因):build_pe32_plus/build_cmd_exe 中 printf_name/exitproc_name/msvcrt/kernel32/import_desc 写成裸缓冲区偏移 0x300/0x320/0x350/0x360/0x400 → 数据目录 Import RVA=0x400 落在 headers 区 → rva_to_offset 返回 0xFFFFFFFF → resolve_imports 静默返回 → IAT 永不被 shim 修补 → `call [rip+0x1F3]` 跳到 0x300 → #PF → 三重故障。build_pe32 一直是正确的(0x1300 等带偏置),PE32+ 两个 builder 抄错。 + 3. 放大器(系统侧,另立 BUG-20260801-005):DSK 阶段任何 CPU 异常都无法投递,直接三重故障重启,串口零诊断。 +- 修复方案: ①两处 disp32 pack_into 偏移 10→9;②build_pe32_plus/build_cmd_exe 的 name/DLL/import_desc RVA 全部加 code_rva 偏置(0x1300/0x1320/0x1350/0x1360/0x1400),IAT thunk 同步修正;③重新生成 hello64.exe/hello32.exe/cmd.exe 并同步 SYSTEM/bin/。 +- 状态: [FIXED] +- 修复提交: 工作区已修复(CODE/tools/pe-samples/gen_pe.py build_pe32_plus/build_cmd_exe) +- 验证: hello64.exe 静态校验全绿——.text `call [rip+0x1F3]`→IAT 0x1200、ImportDir RVA=0x1400、desc0/desc1 OFT/FT=0x1200/0x1210、Name=0x1350/0x1360、IAT thunk=0x1300/0x1320;pe-compat.ps1 复测。 + +### BUG-20260801-005 [P1] [UTSM/IDT] DSK 阶段 CPU 异常无法投递:#PF 投递即 #GP → 三重故障重启,串口零诊断 +- 现象: DSK/cmd/PE 阶段一旦发生 #PF/#GP 等异常,guest 立即三重故障,QEMU(-no-reboot)静默退出,串口无任何异常信息(无向量号/RIP/CR2)。 +- 复现步骤: 运行含缺陷的 HELLO64.EXE(BUG-004,call 到未映射地址 0x300)→ QEMU 直接退出;或任何 DSK 阶段空指针解引用。 +- 串口/日志证据: QEMU `-d int` 追踪(`.build_tmp/pe-dbg-qemu.log`):`check_exception old: 0xffffffff new 0xe`(#PF at RIP=0x300, CR2=0x300, RSP=0xffffffff80e22ff0 栈有效)→ `old: 0xe new: 0xd`(**投递 #PF 时即 #GP**)→ `old: 0x8 new 0xd`(投递 DF 时再 #GP)→ Triple fault。现场:IDT=ffffffff8002b000 lim=0xfff,GDT=ffff80007fe3d000 lim=0x47,CS=0028/SS=0030,TR=0038 且 TSS base=0。 +- 根因分析: BUG-20260729-013 的 `idt_halt_all()` 把 256 个 IDT 条目换成 halt stub,意图 cli;hlt 安全停机;但实测异常**投递本身**就 #GP——halt 门描述符的选择子/IST 与 DSK 阶段 GDT/TSS(TSS base=0)不匹配,异常帧无法压栈。净效果:DSK 阶段是无异常容错的裸奔环境,任何 guest bug 都表现为"QEMU 静默退出",测试侧只能开 `-d int` 才能定位。 +- 修复方案(系统侧,本轮测试任务约束不改系统代码,仅登记): ① halt IDT 门用 DSK 阶段 GDT 内有效选择子重建、IST=0;② 或 DSK 入口自建最小 IDT(#PF/#GP/#DF 打印向量+CR2+RIP 后 cli;hlt);③ 恢复 TSS 有效基址。 +- 状态: [OPEN] +- 修复提交: 待提交 +- 验证: 修复后重跑含缺陷 PE:串口应输出异常诊断而非 QEMU 静默退出。 + +### BUG-20260801-003 [P1] [tools/desktop_app.h] da_mouse_poll 吞键盘扫描码导致应用键盘全灭 +- 现象: fileman/browser/editor(所有 #include desktop_app.h 的全屏应用)内键盘输入完全无效:Esc 无法退出、方向键/回车无响应;应用只能由宿主动作(关机/会话终止)结束。 +- 复现步骤: dev_mode shell → `run FILEMAN.ELF` → 启动后按 Esc → 无 `[fileman] exit`,应用永久停留。 +- 串口/日志证据: `.build_tmp/tests/userapps-matrix-021449.log`:`[fileman] boot` 后 3 次 Esc(每次等 20s)均无 `[fileman] exit`。 +- 根因分析: `da_mouse_poll()`(desktop_app.h L287-291)先 `inb(0x64)` 判有数据即 `inb(0x60)` 读走字节,再判 `st & 0x20` 发现是键盘字节直接 return 丢弃 → 键盘扫描码被鼠标轮询消耗。应用主循环每轮先调 da_mouse_poll 再轮询键盘 → 键盘字节永远到不了键盘分支。desktop/main.c 的 `ps2_mouse_poll()` 顺序正确(先判 0x20 再读 0x60),桌面不受影响;desktop_app.h 注释自称"与 desktop 保持一致"但实际不一致。 +- 修复方案: `da_mouse_poll()` 调整为:`(st & 1) && (st & 0x20)` 时才 `inb(0x60)`,与 desktop/main.c ps2_mouse_poll 对齐。 +- 状态: [FIXED] +- 修复提交: 工作区已修复(CODE/tools/desktop_app.h da_mouse_poll:先判 AUX 位 st&0x20 再读 0x60) +- 验证: QEMU 实测(`.build_tmp/w3b_s2_tools.log`):editor 键盘输入与 Esc 退出正常(`[editor] boot`→`[editor] exit`→`[shell] run: tool returned`);fileman 鼠标移动/点击选中/关闭按钮退出正常;运行中 fileman.elf 反汇编确认键盘字节(st&0x20==0)不再被鼠标轮询消耗。注:fileman/browser 键盘在修复后仍全灭,深挖定位为另一独立根因——da_mouse_init 配置字污染(BUG-20260801-008),该条目修复后 `[fileman] exit`/`[browser] exit` 断言方可完全恢复。 + +### BUG-20260801-002 [P1] [shell] g_run_image 仅 256KB,editor.elf(496KB 内存镜像)run 拒绝加载 +- 现象: dev_mode shell 中 `run EDITOR.ELF` 输出 "run: ELF 加载失败(无效格式或过大)",编辑器无法从 shell 启动(桌面宿主 g_elf_image=1MB,可正常启动)。 +- 复现步骤: dev_mode shell → `run EDITOR.ELF`。 +- 串口/日志证据: `.build_tmp/tests/userapps-matrix-021449.log` L4821/5167/5513 "run: ELF 加载失败(无效格式或过大)" ×3。 +- 根因分析: shell `g_run_image[262144]`(256KB)上限;editor.elf 文件 43KB 但 BSS NOBITS 内存镜像 496KB(编辑器大文本缓冲)→ `sh_load_elf` 返回 -5。其余工具内存镜像均在限内:fileman 208KB / browser 116KB / ping 128KB / curl 200KB。 +- 修复方案: `g_run_image` 扩至 1MB(与 desktop g_elf_image 对齐),或改为按 ELF memsz 动态划分。 +- 状态: [FIXED] +- 修复提交: 工作区已修复(CODE/shell/main.c g_run_image 262144→1048576,与 desktop 宿主 g_elf_image 对齐) +- 验证: QEMU dev_mode shell 实测(`.build_tmp/w3b_s2_tools.log` L4732-4735):`run EDITOR.ELF` → `[shell] run: jumping to tool` → `[editor] boot` → `[editor] exit` → `[shell] run: tool returned`,editor.elf 496KB 内存镜像成功载入 1MB 缓冲并运行、退出;userapps-matrix.ps1 的 `[editor] boot` 断言应可升级。 + +### BUG-20260801-001 [P1] [shell] 扫描码 0x15/0x17/0x0B 被误当 Ctrl+U/W/K,字母 y/i/0 无法输入 +- 现象: shell 交互输入中字母 i、y、数字 0 被吞:`id` 变 "d"、`ping` 变 "ng"、`whoami`/`exit`/`editor` 等含 i 命令均无法键入;串口回显 "未知命令: d" / "未知命令: ng"。 +- 复现步骤: dev_mode shell 提示符下键入 `id` 回车(QEMU monitor sendkey 或真实键盘均可复现)。 +- 串口/日志证据: `.build_tmp/tests/userapps-matrix-021449.log` L4431/4438/4445 "未知命令: d" ×3,L4460/4467/4474 "未知命令: ng" ×3。 +- 根因分析: shell 主循环(CODE/shell/main.c L3399-3401)将扫描码 0x15(y)/0x17(i)/0x0B(0) 直接映射为 Ctrl+U(kill_line)/Ctrl+W(删整行)/Ctrl+K(kill_to_end) 快捷键,但 shell 未跟踪 Ctrl 修饰键状态,也无 Ctrl 按下判定 → 普通按键被当作快捷键吞掉。`ping` 的 p 插入后 i 触发"删整行"→ 剩 "ng"。 +- 修复方案: 增加 Ctrl 修饰键跟踪(0x1D/0x9D 左右 Ctrl),仅在 Ctrl 按下时启用 0x15/0x17/0x0B 快捷键分支。 +- 状态: [FIXED] +- 修复提交: 工作区已修复(CODE/shell/main.c:新增 ctrl 变量跟踪 0x1D 按下/0x9D 释放,0x15/0x17/0x0B 快捷键分支加 && ctrl 条件) +- 验证: QEMU dev_mode shell monitor sendkey 实测(`.build_tmp/w3b_s1_shell.log`):`id` → 输出 `uid=0(root) gid=0(root)`(L4405,i 不再被吞);吞键对照测试 `y0i ok` 完整回显(L4407,y/0/i 三键全通,修复前分别触发 Ctrl+U/W/K 被吞);`ping` → `[ping] boot`(L5214,i 不再被吞)。Ctrl+U/W/K 快捷键逻辑保留,仅在按住 Ctrl 时生效。userapps-matrix.ps1 的 id/whoami/ping MANUAL 断言可升级。 + ### BUG-20260729-021 [P2] [DSK/kernel_api] 硬偏移读取 kernel_api.block 脆弱性 - 现象: DSK 通过 `*(u64*)(api + 0xA8)` 硬偏移读取 `dkm_kernel_api.block`,若结构体在 `block` 字段前插入新字段,DSK 将读取到错误指针 → 解引用 → 立即崩溃,无日志。 - 复现步骤: 修改 dkm.h 中 `dkm_kernel_api` 在 `block` 字段前增加任意字段 → 编译 DSK(不重新编译)→ 启动。 diff --git a/docs/RE/test-cases/README.md b/docs/RE/test-cases/README.md index 80b5a8e..01bb2e2 100644 --- a/docs/RE/test-cases/README.md +++ b/docs/RE/test-cases/README.md @@ -25,14 +25,57 @@ ## 自动化执行 -```powershell -# 运行全部自动化测试场景 -.\ISO\run_tests.bat +PowerShell 5.1 测试套件位于 [tests/qemu/](../../../tests/qemu/),每份矩阵文档的"自动化脚本"小节 +列出对应用例脚本与参数。总运行器顺序执行全部 9 个用例并汇总: -# 输出 -# ISO/logs/test-report-.md +```powershell +# 全量(先构建镜像再跑 9 用例) +.\tests\qemu\Run-All.ps1 -Build + +# 复用现有镜像全量跑 +.\tests\qemu\Run-All.ps1 + +# 只跑指定用例(逗号分隔,大小写不敏感) +.\tests\qemu\Run-All.ps1 -Only network-e2e,x86emu32-coverage ``` +| 用例脚本 | 对应文档 | 说明 | +|---|---|---| +| boot-regression.ps1 | [boot-regression.md](boot-regression.md) | B1~B5 启动场景(-Scenarios 过滤) | +| driver-matrix.ps1 | [driver-matrix.md](driver-matrix.md) | 16 清单驱动加载矩阵 + NVMe 写路径 | +| linux-compat.ps1 | [linux-compat.md](linux-compat.md) | VMM 分流(-Cpu max 走路径 A) | +| pe-compat.ps1 | [pe-compat.md](pe-compat.md) | PE32+ 原生 + PE32 x86emu32 | +| deaicup-e2e.ps1 | [pe-compat.md](pe-compat.md) | Deaicup GUI PE 端到端(首帧像素/键鼠消息/Esc 退出链) | +| userapps-matrix.ps1 | [userapps-matrix.md](userapps-matrix.md) | shell dev_tests + id/ping/editor/fileman/browser 全交互(sendkey 驱动 + Esc 退出链) | +| network-e2e.ps1 | [network-e2e.md](network-e2e.md) | N-A netman / N-B curl(-Scenarios NA,NB) | +| performance-baseline.ps1 | [performance-baseline.md](performance-baseline.md) | 启动阶段墙上计时(-Runs N) | +| x86emu32-coverage.ps1 | [x86emu32-coverage.md](x86emu32-coverage.md) | 静态枚举 + hello32 动态执行 + shim/返回链硬断言(末锚点被 cmd AUTOEXEC 缓冲别名系统 bug 阻塞,见文档) | + +公共库 [tests/qemu/lib/QemuTest.ps1](../../../tests/qemu/lib/QemuTest.ps1) 提供 QEMU 会话管理、 +串口日志断言(必须/禁止/顺序/计数)、monitor sendkey/screendump、firstInit/AUTOEXEC 场景 +备份-替换-恢复、极简 HTTP 测试服务器。所有 QEMU 启动带 `-snapshot`(镜像只读,避免写锁与并发冲突; +可用 `-NoSnapshot` 关闭),monitor 端口按用例固定分配(boot-regression=45501、 +driver-matrix=45502、linux-compat=45503、pe-compat=45504、deaicup-e2e=45454、 +userapps-matrix=45505、network-e2e=45506、performance-baseline=45507、x86emu32-coverage=45508; +被占用时段内自动退让,兜底 45509-45599),避开人工调试 4444 与其他套件 456xx/457xx。清理策略只杀本套件 `-name deshabtest-` 标记的会话, +绝不全局杀 QEMU(并发代理/人工调试不受影响)。 + +并发隔离(套件可与其他代理/人工的 QEMU+build 并发运行): + +- QEMU 只挂私有镜像副本:deshab/nvme/usb 镜像在 `Initialize-QemuTest` 时复制到 + `.build_tmp\tests\img\`(share-tolerant 读取 + 60s 重试;mtime+size 未变则跳过); +- 场景翻转(firstInit.txt / AUTOEXEC.BAT)只写 `.build_tmp\tests\SYSTEM\` 镜像树 + (robocopy /MIR 自共享 SYSTEM 同步),SATA 镜像由 `.build_tmp\tests\fat32gen\` 下的 + mkfat32.ps1 副本生成到私有路径——共享 SYSTEM 树与 `.build_tmp\sata_fat32_dsk.img` + 全程不被套件读写; +- 会话早夭自愈:WHPX 分区强杀后偶发重建异常(guest 启动即三重故障,日志仅一行 + `Ignoring request for interrupt vector 0`),启动 3s 内死亡且日志 <4KB 时自动重开一次。 + +临时文件与日志输出到 `.build_tmp\tests\`(Run-All 汇总日志在 `.build_tmp\tests\runall\`, +性能基线 JSON 在 `.build_tmp\perf_baseline.json`)。每用例独立子进程 + 场景恢复,幂等可重复执行。 + +退出码约定:用例全部 PASS → 0;任一 FAIL → 1。MANUAL/SKIP 项不影响退出码。 + ## Bug 跟踪 所有测试发现的 bug 登记到 [../Bug记录.txt](../Bug记录.txt),并在对应测试文档中引用 bug 编号。 diff --git a/docs/RE/test-cases/driver-matrix.md b/docs/RE/test-cases/driver-matrix.md index c50c1c5..250ae8b 100644 --- a/docs/RE/test-cases/driver-matrix.md +++ b/docs/RE/test-cases/driver-matrix.md @@ -63,6 +63,26 @@ stage 3 (optional): e1000 → virtio_net → ath9k → ps2kbd -device virtio-net-pci,netdev=net0 ^ ``` +## 自动化脚本 + +[tests/qemu/driver-matrix.ps1](../../../tests/qemu/driver-matrix.ps1) 单次 QEMU 会话挂全设备 +(SATA 测试盘 + NVMe 测试盘 + xHCI USB 盘 + e1000),断言 4 个 stage 共 16 个清单驱动的 +加载顺序与功能锚点: + +```powershell +.\tests\qemu\driver-matrix.ps1 # 复用现有镜像 +.\tests\qemu\driver-matrix.ps1 -Build # 先全量构建再测 +``` + +自动断言内容: + +- 16 个 `driver ready` 锚点按 manifest 顺序(ath9k 走 FUCK 禁用路径,断言 `[DSM] driver disabled by FUCK`) +- 功能锚点:ahci/nvme block provider 注册、fat32 经块设备解析镜像、e1000 netdev+link、ps2kbd IRQ1、virtio_net 无设备降级 +- NVMe 写路径回归:LBA0 签名识别 → write+flush+readback 逐字节校验(PRP2 + PRP list 两趟)→ 签名完好,且 ahci0=0 → nvme0=1 注册顺序 +- `[DKM] external driver active` ≥ 15、`[DSM] stage end` × 4 + +已知限制(与上方一致):virtio_net 在 e1000-only 拓扑断言降级锚点;xhci USB MSC 枚举仅断言驱动就绪。 + ## 已知限制 - NVMe BAR0 在 QEMU 中常位于 4G 以上,当前 HHDM 不能安全访问,需 Phase 0 高位 MMIO 映射解决 diff --git a/docs/RE/test-cases/linux-compat.md b/docs/RE/test-cases/linux-compat.md index 3b38ab7..67b8e3d 100644 --- a/docs/RE/test-cases/linux-compat.md +++ b/docs/RE/test-cases/linux-compat.md @@ -38,6 +38,23 @@ | exec daemon 先发 EXEC_READY 再 HLT | daemon 源码 [utsm_exec_daemon.c](../../CODE/linux/initramfs/bin/utsm_exec_daemon.c) | ⬜ | | UTSM hypercall ABI: RAX=magic\|op | 检查 [hypercall.c](../../CODE/UTSM/vmm/hypercall.c) 处理 | ⬜ | +## 自动化脚本 + +[tests/qemu/linux-compat.ps1](../../../tests/qemu/linux-compat.ps1) 动态检测 VMM 可用性,自动分流: + +```powershell +.\tests\qemu\linux-compat.ps1 # QEMU WHPX:走路径 B(VMM unavailable 优雅降级) +.\tests\qemu\linux-compat.ps1 -Cpu max # 真机/KVM nested VMX:走路径 A(完整双内核流程) +``` + +- **路径 A**(guest VMX 可用):按序断言 A5.1~A5.7 启动链锚点 + (VMM init → self-test → bzImage → IPC shm → vmlaunch → `Linux version` → daemon park → + `[LNXC] service ready` → DSK),随后经 monitor 按键执行 `dsl` / `dsl ls` / `dsl uname -a` / + `dsl nosuchprog` 覆盖 A5.8~A5.12。 +- **路径 B**(QEMU WHPX/TCG 无 nested VMX,本环境预期):断言 `[UTSM] VMM unavailable` 后 + DSK 正常接管、dev_tests 完成、`dsl` 报告运行态不可用、Esc 到 desktop; + A5.1~A5.12 标记 SKIP(环境限制,非产品缺陷)。 + ## 已知限制 - QEMU TCG 不支持 VMX,必须 WHPX 或真机 diff --git a/docs/RE/test-cases/network-e2e.md b/docs/RE/test-cases/network-e2e.md index 2a4824d..8162aa1 100644 --- a/docs/RE/test-cases/network-e2e.md +++ b/docs/RE/test-cases/network-e2e.md @@ -55,6 +55,28 @@ - curl 64KB 页面接收无明显停顿 - DHCP 总耗时 < 5s +## 自动化脚本 + +[tests/qemu/network-e2e.ps1](../../../tests/qemu/network-e2e.ps1) 分两个场景: + +```powershell +.\tests\qemu\network-e2e.ps1 # N-A + N-B 全跑 +.\tests\qemu\network-e2e.ps1 -Scenarios NA # 只跑 netman 全链路 +.\tests\qemu\network-e2e.ps1 -Scenarios NB # 只跑 curl 主机侧端到端 +``` + +- **场景 N-A**(netman 全链路,首启调度,覆盖 N1~N5):firstInit=`0\n0`,挂 e1000(net0) + + virtio-net(net1) 双网卡。断言 virtio_net selftest(真实 slirp DHCP OFFER)→ + netman 双设备枚举 → DHCP DISCOVER/OFFER/REQUEST/ACK → ARP 网关解析 → DNS example.com → + `network final rc=0x0000000000000000`,并禁止 `no OFFER / ARP timeout / DNS timeout / [PANIC]`。 +- **场景 N-B**(curl.elf 主机侧端到端,覆盖 N8/N10):dev_mode shell → 交互 `curl` → + 输入 `http://10.0.2.2:8000/`。脚本在主机 127.0.0.1:8000 起极简 HTTP 服务器 + (QemuTest.ps1 `Start-TestHttpServer`),断言主机侧收到 `GET / HTTP/1.0` + (TCP+IP+ETH 全链路最强证据),guest 侧断言 `[curl] boot/exit` 与 shell 回收。 + HTTP 响应体渲染仅帧缓冲,标 MANUAL。 +- N6/N11(ping 链路)由 [userapps-matrix.ps1](../../../tests/qemu/userapps-matrix.ps1) 交互阶段覆盖; + N7/N9(外网)依赖主机联网,不在自动化断言范围。 + ## 已知限制 - HTTPS/TLS 不支持(curl 仅 http) diff --git a/docs/RE/test-cases/pe-compat.md b/docs/RE/test-cases/pe-compat.md index ad25a95..51697d7 100644 --- a/docs/RE/test-cases/pe-compat.md +++ b/docs/RE/test-cases/pe-compat.md @@ -65,6 +65,58 @@ int main(void) { 若无 MinGW/MSVC,使用 [CODE/tools/pe-samples/gen_pe.py](../../CODE/tools/pe-samples/gen_pe.py) 生成最小 PE 程序。 +## 自动化脚本 + +[tests/qemu/pe-compat.ps1](../../../tests/qemu/pe-compat.ps1) 正常启动到 cmd.elf, +AUTOEXEC.BAT 自动执行 PE 命令序列: + +```powershell +.\tests\qemu\pe-compat.ps1 # 复用现有镜像 +.\tests\qemu\pe-compat.ps1 -Build # 先全量构建再测 +``` + +因两个已登记系统 bug([Bug记录.txt](../Bug记录.txt) BUG-20260801-006/007),脚本拆三个独立会话: + +- **会话 A (pe64)**:B2.1 `pe HELLO64.EXE` → `[PE] PE32+ (64-bit)` → + `[PE] running PE32+ natively` → `Hello from PE32+!`(自动断言 PASS)。 + 随后的 ExitProcess 触发 BUG-006(setjmp/longjmp UB → 三重故障),会话终止; + `[cmd] PE run end` 返回链标 MANUAL,日志出现即自动升级 PASS。 +- **会话 B (pe32)**:B2.2 `pe HELLO32.EXE` → `[PE] running PE32 via x86emu32` + 解释器接管(自动断言 PASS,且无 emulator error / unimplemented)。 + 首个 shim 调用 printf 触发 BUG-007(fn64 回退 guest 指针当 host 指针), + 会话终止;`Hello from PE32!` 标 MANUAL,出现即自动升级 PASS。 +- **会话 C (peerr)**:B2.4 `pe NONEXIST.EXE` 错误路径(不产生 `[cmd] PE run begin`) + + `exit` 正常返回 DSK(`[DSK] cmd.elf returned`,自动断言 PASS)。 + +WHPX 下任一会话未命中锚点自动以 `-Accel tcg` 重跑(可 `-Accel tcg` 直接指定)。 +禁止锚点:`[PANIC]` / `[PE32] emulator error` / `[PE] unimplemented import`。 + +B2.3/B2.7(peinfo)与 B2.5(无参用法提示)仅写帧缓冲(cmd 非 dev_mode 无串口镜像),标 MANUAL。 +PE32 解释器指令级覆盖率另见 [x86emu32-coverage.ps1](../../../tests/qemu/x86emu32-coverage.ps1)。 + +### GUI 应用端到端:deaicup-e2e.ps1 + +[tests/qemu/deaicup-e2e.ps1](../../../tests/qemu/deaicup-e2e.ps1) 验证 Deaicup +(Rust/egui 移植的原始 Win32 API PE64 GUI 应用)端到端:AUTOEXEC `pe DEAICUP.EXE` 启动后 +经 monitor 驱动键鼠并断言串口锚点 + screendump 像素: + +- **9 锚点顺序链**:`[cmd] cmdline: DEAICUP.EXE` → `[PE] running PE32+ natively` → + `[deaicup] mainCRTStartup` → `[shim] RegisterClassExW ok` → `[shim] CreateWindowExW ok` → + `[shim] CreateDIBSection ok` → `[dc] Context::default ok` → + `[shim] BitBlt first frame on screen` → `[dc] bitblt ok` +- **首帧像素分析**:非全黑(nonBlack≥100000)、颜色种数 ≥32、GUI 窗口矩形检测 + (窗口主体色 #1B1B1B 行扫描实心段 → 800x508 包围盒、填充率 ≥75%) +- **鼠标**:AUX 初始化 + mouse_move → `[shim] mpkt`(WM_MOUSEMOVE)、 + mouse_button → `[shim] LDOWN/LUP` 边沿(WM_LBUTTONDOWN/UP)、 + 两次 screendump 光标区像素差异(8..60000 采样点) +- **键盘**:sendkey → `[shim] KEYDOWN/CHAR/KEYUP`(WM_KEYDOWN/WM_CHAR/WM_KEYUP) +- **退出链**:Esc → WM_CLOSE → `[deaicup] quit, ExitProcess(0)` → + `[PE] ExitProcess code=` → `[cmd] PE run end`(回归 BUG-006 长运行返回链、BUG-005 异常噪声) +- 禁止锚点:`[PANIC]` / `[IDT] exception` / `Unimplemented handler` + +WHPX 未命中首帧锚点自动以 `-Accel tcg` 重跑(可 `-Accel tcg` 直接指定);monitor 固定 45454。 +证据帧输出 `.build_tmp/tests/deaicup-frame.png`。 + ## 已知限制 - pe_shim 实现的 Win32 API 子集有限,测试程序仅使用 WriteFile + ExitProcess diff --git a/docs/RE/test-cases/performance-baseline.md b/docs/RE/test-cases/performance-baseline.md index 15bccfc..abff305 100644 --- a/docs/RE/test-cases/performance-baseline.md +++ b/docs/RE/test-cases/performance-baseline.md @@ -81,6 +81,32 @@ foreach ($m in $curr.metrics) { } ``` +## 自动化脚本 + +[tests/qemu/performance-baseline.ps1](../../../tests/qemu/performance-baseline.ps1) 以主机墙上时钟 +轮询串口锚点,测量启动阶段耗时(正常启动 firstInit=`1\n0`,AUTOEXEC=ver/exit): + +```powershell +.\tests\qemu\performance-baseline.ps1 # 默认 3 轮取中位数 +.\tests\qemu\performance-baseline.ps1 -Runs 5 # 自定义轮数 +``` + +测量锚点与阶段换算: + +| 阶段 | 起 → 止 | 对应指标 | +|---|---|---| +| ovmf_bootloader_ms | QEMU 进程启动 → `[UTSM] boot` | OVMF + Limine | +| utsm_drivers_ms | `[UTSM] boot` → `[UTSM] SELFTEST PASS` | P1(UTSM+16 驱动装载) | +| selftest_to_dsk_ms | SELFTEST → `[DSK] boot` | DSK 加载(FAT32 read + PIE load) | +| dsk_to_desktop_ms | `[DSK] boot` → `desktop ready` | DSK → 桌面就绪 | +| total_boot_ms | QEMU 启动 → `desktop ready` | 端到端总启动 | + +结果写入 `.build_tmp\perf_baseline.json`(锚点绝对时刻 + 各轮数据 + 中位数 + 环境信息)。 +本脚本只测量展示、不做阈值断言;轮询精度 ±200ms,QEMU 数值仅作回归参考。 + +未自动化项(需内核 TSC 打点日志,当前串口无锚点,保持 PENDING):P2/P3/P6(单 ELF 加载计时)、 +P4 ping RTT、P5 linux RTT、P7 桌面帧率、P8~P11(PE/VMM/Linux)、P12 DHCP 耗时。 + ## 已知限制 - QEMU 性能数据不等于真机,仅作回归参考 diff --git a/docs/RE/test-cases/userapps-matrix.md b/docs/RE/test-cases/userapps-matrix.md index aef6fc9..88bb6c8 100644 --- a/docs/RE/test-cases/userapps-matrix.md +++ b/docs/RE/test-cases/userapps-matrix.md @@ -70,6 +70,40 @@ shell → run CURL.ELF → curl.elf | 窗口拖动 | 流畅,无撕裂 | ⬜ | | 任务栏点击 | 窗口切换 | ⬜ | +## 自动化脚本 + +[tests/qemu/userapps-matrix.ps1](../../../tests/qemu/userapps-matrix.ps1) dev_mode 单会话覆盖 +shell 内建命令与工具程序(dev_mode 串口镜像开启,全程可断言): + +```powershell +.\tests\qemu\userapps-matrix.ps1 # 复用现有镜像 +.\tests\qemu\userapps-matrix.ps1 -Build # 先全量构建再测 +``` + +自动断言(两阶段,单会话;2026-08-04 BUG-20260801-001/002/003/008 全部修复后, +原 MANUAL 降级项已回收为自动化): + +1. **dev_tests 硬编码序列**:`[DSK:SCHED] decision: dev_mode -> shell.elf` → + `[1] ls` → `[2] echo>` → `[3] cat` → `[4] cp` → `[6] mv` → `[8] rm` → `[10] ls` + → `=== 自动测试完成 ===`(SATA 镜像无 test.bas 时走 fallback 分支)。 +2. **交互阶段**(monitor sendkey 驱动):`help` → `id`(断言 `uid=0(root) gid=0(root)`, + BUG-001 修复回归守护)→ `pci` 存活验证 → + `ping`(PATH 查找 `/bin/PING.ELF` → `[ping] boot`,输入 10.0.2.2 发四报文, + Esc → `[ping] exit` → `[shell] path: tool returned`)→ + `run EDITOR.ELF`(`[editor] boot` → 按键插入 → Esc → `[editor] exit`, + BUG-002 修复后行为,原"超限优雅拒绝"断言已升级)→ + `run FILEMAN.ELF`(`[fileman] boot` → 方向键选区移动 → Esc → `[fileman] exit`, + BUG-003/008 修复后键盘路径)→ + `run BROWSER.ELF`(`[browser] boot` → Esc → `[browser] exit`)→ + Esc 回 DSK → cmd(AUTOEXEC=ver/exit) → `desktop ready`。 + 锚点:`[shell] path: running /bin/ tool` / `[shell] path: tool returned` / + `[shell] run: jumping to tool` / `[shell] run: tool returned` / + `[ping|editor|fileman|browser] boot|exit`。 + +标 MANUAL 项(帧缓冲限定,无串口锚点):ping ICMP reply/RTT 与四报文统计、 +editor 编辑内容渲染与保存、fileman 列表渲染/进入子目录; +FirstInit/login/desktop UI(双击/拖动/任务栏,boot-regression B1 已覆盖向导按键)。 + ## 已知问题 - fileman.elf 之前触发过 QEMU 退出(访问未支持 boot context 字段),需观察是否复现 diff --git a/docs/RE/test-cases/x86emu32-coverage.md b/docs/RE/test-cases/x86emu32-coverage.md index d89bb77..85587c0 100644 --- a/docs/RE/test-cases/x86emu32-coverage.md +++ b/docs/RE/test-cases/x86emu32-coverage.md @@ -84,3 +84,36 @@ ``` 目标:核心指令(MOV/ADD/SUB/CMP/JMP/CALL/RET/PUSH/POP)覆盖率 100%。 + +## 自动化脚本 + +[tests/qemu/x86emu32-coverage.ps1](../../../tests/qemu/x86emu32-coverage.ps1) 两层验证: + +```powershell +.\tests\qemu\x86emu32-coverage.ps1 # 复用现有镜像 +.\tests\qemu\x86emu32-coverage.ps1 -Build # 先全量构建再测 +``` + +1. **静态枚举**(无需 QEMU):解析 [x86emu32.c](../../CODE/UTSM/pe/x86emu32.c) 统计 + `case 0xNN` 操作码分支数(覆盖率分母,含 0F 扩展页),并校验 hello32.exe 头 + (machine=0x14C / optional magic=0x10B,确为 PE32 样本)。 +2. **动态执行**:正常启动 → cmd.elf,AUTOEXEC.BAT 执行 `pe HELLO32.EXE` + `exit`。 + 自动断言 `[PE] running PE32 via x86emu32` 解释器接管;禁止 `[PE32] emulator error` / + `[PE] unimplemented import` / `[PE] unimplemented API called` / `[PANIC]`。 + 注意 `[PE] PE32 (32-bit)` 仅 PE32+ 原生路径(pe_load_image)输出,解释器路径 + (pe32_load_into)不打印该行,不可作锚点。 + + BUG-20260801-006/007 已修复([Bug记录.txt](../Bug记录.txt)),原软升级项已转 + 硬断言(回归守护):`Hello from PE32!`(shim IAT 拦截输出)与 + `[cmd] PE run end` → `[DSK] cmd.elf returned`(完整返回链)。 + + 已知系统侧阻塞(2026-08-04 monitor 取证定案):末锚点 `[DSK] cmd.elf returned` + 当前必缺失。根因不在 PE 层——cmd `run_autoexec` 的 `data`/`size` 直指共享静态 + 缓冲 `f32_data`(fat32_io.h),首行 `pe HELLO32.EXE` 加载 PE 文件即覆盖剩余 + 脚本,`exit` 行丢失,cmd 停留交互主循环轮询键盘(guest 存活、无异常投递、 + 无三重故障)。反证:pe-compat 会话 C(`pe NONEXIST.EXE` + `exit`,文件未找到 + 不覆盖缓冲)返回链正常。待 cmd 侧修复(脚本先拷入私有缓冲再逐行执行)后 + 末锚点自动转绿。 + +指令级逐条覆盖(上方矩阵 test_arith/test_logic/test_control/test_string)依赖 +CODE/tools/pe-samples/ 下尚未构建的分类测试 PE,标 MANUAL。 diff --git a/docs/上下文/25072901/Deshab Linux_EXE 兼容层开发.md b/docs/上下文/25072901/Deshab Linux_EXE 兼容层开发.md index dbe4ae6..8c57051 100644 --- a/docs/上下文/25072901/Deshab Linux_EXE 兼容层开发.md +++ b/docs/上下文/25072901/Deshab Linux_EXE 兼容层开发.md @@ -253,7 +253,7 @@ typedef struct { **统计**:rx_filled / rx_drop_nobuf / rx_badbuf / tx_sent / tx_drop,每 512 事件串口打一行 -### Phase 4: desktop 集成 Linux 命令为应用图标 +### Phase 4: desktop 集成 Linux 命令为应用图标 ✅ 已完成(2026-08-03,桌面 Phase 5) **文件**:`CODE/tools/desktop_app.h` + `CODE/desktop/main.c` @@ -264,6 +264,16 @@ typedef struct { - `lnx_on_create`:bash_reset → 标题 "NAME . Linux" → `$ ` 回显 → 同步 `exec()` 阻塞 → 输出经 ANSI CSI 剥离/`^M` 忽略/`\t` 展开后一次性追加 → `[exit N]` - `draw_icon_linux`:暗底 + 霓虹绿 `>_` +**补齐与验证(桌面 Phase 5)**: +- Linux 应用图标追加进桌面图标网格(4 个 .lnk 快捷方式之后),`setup_desktop_icons()` 统一上架 +- **无论服务是否可用都注册图标**:`g_lnx_svc_up = is_available()`,不可用时 `draw_icon_linux` 用 `KS_TEXT_DIM` 置灰;双击仍开终端窗口,`lnx_on_create` 运行时复查服务,不可用显示红色 `linux: compat service unavailable` + `[exit -1]`,绝不 panic +- `SYSTEM/LINUXAPP.CNF` 随镜像分发(mkfat32 8.3 名 LINUXAPPCNF),默认登记 HELLO/UNAME/LS 三条(guest Arch rootfs 与 initramfs busybox 共有命令);CNF 缺失回退内置同款默认 +- QEMU 验证(`.build_tmp/w5c_desktop_verify.ps1`,服务状态感知断言,TCG/WHPX 各 14 项全 PASS): + - 串口锚点:`linux apps: loaded LINUXAPP.CNF`、`linux app registered: HELLO/UNAME/LS`、(降级时)`linux apps: service unavailable (icons greyed)` + - 像素佐证:图标网格 7 图标(4 快捷方式 + 3 Linux 应用);降级时 HELLO 图标 grey=46 green=0 + - 双击 HELLO:`[desktop] app launched`,窗口显示 `$ /bin/echo hello from deshab linux` + 红色不可用提示 + `[exit -1]`(red=149),无 PANIC +- **注意**:本机 TCG(`-cpu max`)/WHPX 均无 VMX(TCG 不模拟 VMX 指令),只能验证降级路径;服务可用路径(绿图标 + `linux exec` + guest 输出回显)需真机 / Linux KVM / 嵌套 Hyper-V,验证脚本同一套断言自动切换 + ### 构建问题记录 1. **GCC 15 C23 与 Linux 6.6 不兼容**:`bool`/`false` 变成关键字 → 安装 gcc-12 + `update-alternatives` diff --git a/docs/桌面设计.md b/docs/桌面设计.md index 9158244..5049ada 100644 --- a/docs/桌面设计.md +++ b/docs/桌面设计.md @@ -127,9 +127,21 @@ MAX_DIRTY_RECTS=16,mark_dirty记录变化区域,溢出降级全屏重绘。 --- -## 8. Linux应用集成 +## 8. 桌面图标来源(.lnk快捷方式 + LINUXAPP.CNF Linux应用) -Linux兼容层服务可用时读FAT32根目录LINUXAPP.CNF(8.3名LINUXAPPCNF),每行注册图标:`显示名|linux命令`。规则:空行/#注释忽略;显示名≤11字符,命令≤63字符;按空格分词(首token为程序路径,裸命令补/bin/);DA_LINUXAPP_MAX=8。点击图标→内置终端窗口承载→linux_compat_service.exec()委托执行。 +### 8.1 .lnk快捷方式 + +`setup_desktop_icons()`先上架4个用户快捷方式(编辑器/文件管理/浏览器/终端),`shortcuts_refresh()`当前为硬编码列表(SYSTEM/user/desktop/*.lnk已随镜像分发,解析留待后续);快捷方式图标`app_id=-1`,双击经`launch_external_elf()`加载对应工具ELF全屏宿主。 + +### 8.2 LINUXAPP.CNF Linux应用 + +快捷方式之后追加Linux应用图标。**无论Linux兼容层服务是否可用都注册**:桌面启动时读FAT32根目录LINUXAPP.CNF(8.3名LINUXAPPCNF),每行一条:`显示名|linux命令`。规则:空行/#注释忽略;显示名≤11字符(DA_LINUXAPP_NAME_CAP=12),命令≤63字符(DA_LINUXAPP_CMD_CAP=64);按空格分词(首token为程序路径,绝对路径直接用,裸命令补/bin/前缀);DA_LINUXAPP_MAX=8。CNF缺失/无有效行时回退内置默认3条(HELLO=/bin/echo、UNAME=/bin/uname -a、LS=/bin/ls -l /,均为Arch rootfs与initramfs busybox共有命令)。 + +- 图标:`draw_icon_linux`暗底终端+霓虹绿`>_`;服务不可用(VMX缺失/guest未驻留)时改用KS_TEXT_DIM置灰(`g_lnx_svc_up=0`) +- 双击:内置终端窗口承载(720×480,标题`NAME . Linux`)→回显`$ `→`lnx_on_create`运行时复查服务: + - 可用:`linux_compat_service.exec()`同步委托执行(UI冻结),输出经ANSI CSI剥离/`^M`忽略/`\t`展开一次性追加,`[exit N]`按退出码绿/琥珀 + - 不可用:红色提示`linux: compat service unavailable`+`[exit -1]`,绝不panic +- 串口打点:`linux apps: loaded LINUXAPP.CNF`/`builtin defaults`/`service unavailable (icons greyed)`、`linux app registered: <名>`、`linux exec`、`app launched` --- diff --git a/docs/系统开发策划.md b/docs/系统开发策划.md index f1620be..0e5fe9d 100644 --- a/docs/系统开发策划.md +++ b/docs/系统开发策划.md @@ -38,10 +38,10 @@ docs/上下文/ 开发对话记录(待提炼为正式设计文 | DMA分配器 | ✅ | — | | UTSM封缄内存 | 🔶 | 真实加密原语未选型 | | AHCI块设备 | ✅ | 单扇区偶发超时 | -| NVMe | 🔶 | BAR0>4G,待高位MMIO | +| NVMe | ✅ | 写路径+PRP list+读回校验已完成 | | FAT32 | 🔶 | DKM只读;DSK读写(根目录),子目录写待补 | | e1000网络 | ✅ | 中断驱动收发(仍polling) | -| virtio_net | 🔶 | 无feature negotiation/virtqueue | +| virtio_net | ✅ | virtqueue RX/TX+netdev注册+DHCP自测已完成 | | 字体系统 | ✅ | — | | FirstInit | 🔶 | B6按键崩溃修复中;鼠标仍轮询 | | 正常启动路径 | ✅ | — | @@ -73,12 +73,12 @@ docs/上下文/ 开发对话记录(待提炼为正式设计文 | # | 阻塞点 | 严重度 | 缓解 | |---|--------|--------|------| | B1 | ~~block write~~ ✅已解决 | — | AHCI WRITE DMA+block_api.write已落地 | -| **B2** | 高位PCI MMIO映射缺失(4G以上BAR) | P1 | Phase 0页表映射接口 | +| B2 | ~~高位PCI MMIO映射缺失~~ ✅已解决 | — | NVMe BAR0可读,写路径+PRP list+读回校验通过 | | B3 | ~~WSL构建阻塞~~ ✅已解决 | — | bzImage/initrd/rootfs已产出 | | **B4** | QEMU TCG不支持VMX | P0 | 真机VT-x/KVM/云VM验证;WHPX检测已落地 | | B5 | ~~fileman QEMU exit~~ ✅已解决 | — | linker.ld BSS NOBITS分离+g_elf_image扩至1MB | | **B6** | FirstInit按键崩溃 | P1 | fb_text边界检查+编译优化降级调试中 | -| **B7** | IDT仅0–47,无vector allocator/APIC EOI | P2 | Phase 2 | +| **B7** | APIC/IOAPIC stage1 已落地(LAPIC接管/MADT/IOREDTBL/apic_route开关/tick链路验证) | P2 | UTSM LAPIC EOI、IDT 256+vector allocator、MSI-X 进行中(Phase 2) | ### 3.2 风险登记 @@ -122,18 +122,19 @@ M5 软件生态 (依赖M3) Pacman→AUR→desktop集成 ### 4.4 M2 — 存储升级+中断后端 -| 任务 | 验收标准 | -|------|---------| -| 页表映射接口 | mmio_map/unmap通用接口 | -| 高位PCI MMIO映射(B2) | NVMe BAR0可读register | -| 替换arena为真实页分配 | 移除16MB arena上限 | -| NVMe admin queue/identify+block provider | 注册nvme0 | -| IDT扩展256 vectors+vector allocator | 动态分配中断向量 | -| APIC EOI→IOAPIC redirection→LAPIC timer | 替代PIC | -| MSI/MSI-X | NVMe/e1000使用MSI-X | -| e1000中断驱动收发 | 替代polling | -| virtio-net virtqueue RX/TX | 收发包成功 | -| DKM工程化收尾 | 统一ABI头、零warning | +| 任务 | 验收标准 | 进度 | +|------|---------|------| +| 页表映射接口 | mmio_map/unmap通用接口 | ✅ | +| 高位PCI MMIO映射(B2) | NVMe BAR0可读register | ✅(NVMe 数据路径已验证) | +| 替换arena为真实页分配 | 移除16MB arena上限 | | +| NVMe admin queue/identify+block provider | 注册nvme0 | ✅ 写路径+PRP list+读回校验完成 | +| IDT扩展256 vectors+vector allocator | 动态分配中断向量 | | +| APIC EOI→IOAPIC redirection→LAPIC timer | 替代PIC | 🔶 stage1 完成(LAPIC接管/MADT/IOREDTBL/apic_route开关/tick链路验证),UTSM LAPIC EOI 进行中 | +| MSI/MSI-X | NVMe/e1000使用MSI-X | | +| e1000中断驱动收发 | 替代polling | | +| virtio-net virtqueue RX/TX | 收发包成功 | ✅ virtqueue RX/TX+netdev注册+DHCP自测完成 | +| DKM工程化收尾 | 统一ABI头、零warning | | +| QEMU测试套件脚本化 | Run-All 全用例自动化 | ✅ 9 用例(8+deaicup-e2e)全自动化 PASS | ### 4.5 M3 — 兼容层验证 diff --git a/tests/qemu/Run-All.ps1 b/tests/qemu/Run-All.ps1 new file mode 100644 index 0000000..524c6e5 --- /dev/null +++ b/tests/qemu/Run-All.ps1 @@ -0,0 +1,115 @@ +#requires -Version 5.1 +<# +.SYNOPSIS + Run-All — Deshab QEMU 自动化测试套件总运行器 + +.DESCRIPTION + 顺序执行 tests/qemu 下全部 9 个用例脚本,每用例独立 powershell 子进程 + (独立 QEMU 会话 + firstInit/AUTOEXEC 场景隔离),汇总 PASS/FAIL/耗时。 + + 用例顺序(依赖与耗时综合考虑,快用例靠后): + 1. boot-regression.ps1 B1 首启向导 / B2 正常 / B3 dev_mode / B4 无盘 / B5 无 NIC + 2. driver-matrix.ps1 16 清单驱动加载矩阵 + NVMe 写路径回归 + 3. linux-compat.ps1 VMM 分流(QEMU 走 VMM unavailable 优雅降级路径) + 4. pe-compat.ps1 PE32+ 原生 + PE32 x86emu32 + 错误路径 + 5. deaicup-e2e.ps1 Deaicup GUI PE 端到端(首帧像素/键鼠消息/Esc 退出链) + 6. userapps-matrix.ps1 shell dev_tests + ping/editor/fileman/browser 工具链 + 7. network-e2e.ps1 N-A netman 全链路 + N-B curl 主机侧端到端 + 8. performance-baseline.ps1 + 启动阶段墙上计时 → .build_tmp/perf_baseline.json + 9. x86emu32-coverage.ps1 x86emu32 静态枚举 + hello32 动态解释执行 + +.PARAMETER Only + 只跑指定用例(逗号分隔的脚本基名,大小写不敏感): + .\Run-All.ps1 -Only network-e2e,x86emu32-coverage + +.PARAMETER Build + 先执行一次 build.ps1(全量编译+打包镜像)再跑用例。 + +.NOTES + 退出码:全部用例 PASS → 0;任一 FAIL → 1。 + 每用例完整输出存档 .build_tmp\tests\runall\.log。 +#> +param( + [string[]]$Only = @(), + [switch]$Build +) +$ErrorActionPreference = 'Stop' +$suiteRoot = $PSScriptRoot +$repoRoot = [System.IO.Path]::GetFullPath((Join-Path $suiteRoot '..\..')) +$outDir = Join-Path $repoRoot '.build_tmp\tests\runall' +New-Item -ItemType Directory -Force -Path $outDir | Out-Null + +# ---- 跑前清理:只杀本套件历史会话残留的 QEMU(-name deshabtest- 标记)。 +# 绝不全局清理——可能有其他代理/人工在并发跑 QEMU(如 monitor 45454 的调试会话)。 +# 本套件所有会话默认 -snapshot(镜像只读),无需靠杀进程解决镜像锁。 ---- +. (Join-Path $suiteRoot 'lib\QemuTest.ps1') +Stop-ResidualQemu + +# ---- 可选:先构建一次 ---- +if ($Build) { + Write-Host '[runall] running build.ps1 ...' + & powershell -ExecutionPolicy Bypass -File (Join-Path $repoRoot 'build.ps1') + if ($LASTEXITCODE -ne 0) { Write-Host "[runall] build.ps1 FAILED (exit $LASTEXITCODE)"; exit 1 } +} + +$cases = @( + 'boot-regression', + 'driver-matrix', + 'linux-compat', + 'pe-compat', + 'deaicup-e2e', + 'userapps-matrix', + 'network-e2e', + 'performance-baseline', + 'x86emu32-coverage' +) +if ($Only.Count -gt 0) { + # -File 调用下逗号列表常作为单个字符串传入,统一按逗号再切一次 + $want = @($Only | ForEach-Object { $_ -split ',' } | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ }) + $cases = @($cases | Where-Object { $want -contains $_.ToLower() }) + if ($cases.Count -eq 0) { Write-Host "[runall] -Only 过滤后无用例可跑: $($Only -join ',')"; exit 1 } +} + +$rows = New-Object System.Collections.ArrayList +$suiteStart = Get-Date +foreach ($case in $cases) { + $script = Join-Path $suiteRoot "$case.ps1" + $log = Join-Path $outDir "$case.log" + Write-Host '' + Write-Host ("================ [runall] {0} ================" -f $case) + $t0 = Get-Date + & powershell -ExecutionPolicy Bypass -File $script 2>&1 | Tee-Object -FilePath $log | Out-Host + $rc = $LASTEXITCODE + $dur = [math]::Round(((Get-Date) - $t0).TotalSeconds, 1) + + $outText = '' + if (Test-Path $log) { $outText = Get-Content $log -Raw } + $manualCount = ([regex]::Matches($outText, '\[MANUAL\]')).Count + $skipCount = ([regex]::Matches($outText, '\[SKIP\]')).Count + $status = 'PASS' + if ($rc -ne 0) { $status = 'FAIL' } + + [void]$rows.Add([pscustomobject]@{ + Case = $case + Status = $status + Duration = "${dur}s" + Manual = $manualCount + Skip = $skipCount + ExitCode = $rc + Log = $log + }) +} + +# ---- 汇总表 ---- +$totalDur = [math]::Round(((Get-Date) - $suiteStart).TotalMinutes, 1) +Write-Host '' +Write-Host ("================ 测试套件汇总 ({0} min) ================" -f $totalDur) +$rows | Format-Table Case, Status, Duration, Manual, Skip, ExitCode -AutoSize | Out-Host + +$failCount = @($rows | Where-Object { $_.Status -eq 'FAIL' }).Count +$passCount = @($rows | Where-Object { $_.Status -eq 'PASS' }).Count +Write-Host ("PASS: {0} FAIL: {1} TOTAL: {2}" -f $passCount, $failCount, $rows.Count) +Write-Host ("日志目录: {0}" -f $outDir) + +if ($failCount -gt 0) { exit 1 } else { exit 0 } diff --git a/tests/qemu/boot-regression.ps1 b/tests/qemu/boot-regression.ps1 index 38256f4..e6b450d 100644 --- a/tests/qemu/boot-regression.ps1 +++ b/tests/qemu/boot-regression.ps1 @@ -1,4 +1,4 @@ -#requires -Version 5.1 +#requires -Version 5.1 <# .SYNOPSIS boot-regression — 启动链路回归测试(docs/RE/test-cases/boot-regression.md) @@ -67,7 +67,7 @@ function Complete-FirstInitWizard { function Invoke-ScenarioB1 { Write-Host "`n--- B1 首次启动 (firstInit=0) ---" Set-DeshabFirstInit -First 0 -DevMode 0 - $s = Start-QemuSession -Name 'bootreg-b1' + $s = Start-QemuSession -Name 'bootreg-b1' -MonitorPort 45501 try { if (-not (Complete-FirstInitWizard -Session $s)) { return } $hit = Wait-QemuLog -Session $s -Patterns @('desktop ready') -TimeoutSeconds 120 @@ -100,7 +100,7 @@ function Invoke-ScenarioB2 { Write-Host "`n--- B2 正常启动 (firstInit=1, AUTOEXEC=ver/exit) ---" Set-DeshabFirstInit -First 1 -DevMode 0 Set-DeshabAutoexec -Lines @('ver', 'exit') - $s = Start-QemuSession -Name 'bootreg-b2' + $s = Start-QemuSession -Name 'bootreg-b2' -MonitorPort 45501 try { $hit = Wait-QemuLog -Session $s -Patterns @('desktop ready') -TimeoutSeconds 150 $log = Read-QemuLogText $s.LogPath @@ -131,7 +131,7 @@ function Invoke-ScenarioB3 { Write-Host "`n--- B3 dev_mode (firstInit=1\1) ---" Set-DeshabFirstInit -First 1 -DevMode 1 Set-DeshabAutoexec -Lines @('ver', 'exit') - $s = Start-QemuSession -Name 'bootreg-b3' + $s = Start-QemuSession -Name 'bootreg-b3' -MonitorPort 45501 try { $hit = Wait-QemuLog -Session $s -Patterns @('[8] rm TEST.TXT') -TimeoutSeconds 150 if (-not $hit) { @@ -165,7 +165,7 @@ function Invoke-ScenarioB3 { function Invoke-ScenarioB4 { Write-Host "`n--- B4 无 SATA 盘 (DSK 回退 Limine module) ---" - $s = Start-QemuSession -Name 'bootreg-b4' -NoSata + $s = Start-QemuSession -Name 'bootreg-b4' -NoSata -MonitorPort 45501 try { $hit = Wait-QemuLog -Session $s -Patterns @('[DSK] firstInit.txt not found', '[DSK] desktop load failed', '[PANIC]') -TimeoutSeconds 120 Start-Sleep -Seconds 3 @@ -190,7 +190,7 @@ function Invoke-ScenarioB5 { Write-Host "`n--- B5 无 NIC (e1000/virtio_net not found, 继续启动) ---" Set-DeshabFirstInit -First 1 -DevMode 0 Set-DeshabAutoexec -Lines @('ver', 'exit') - $s = Start-QemuSession -Name 'bootreg-b5' -NoNet + $s = Start-QemuSession -Name 'bootreg-b5' -NoNet -MonitorPort 45501 try { $hit = Wait-QemuLog -Session $s -Patterns @('desktop ready') -TimeoutSeconds 150 $log = Read-QemuLogText $s.LogPath diff --git a/tests/qemu/deaicup-e2e.ps1 b/tests/qemu/deaicup-e2e.ps1 new file mode 100644 index 0000000..8e44fbe --- /dev/null +++ b/tests/qemu/deaicup-e2e.ps1 @@ -0,0 +1,369 @@ +#requires -Version 5.1 +<# +.SYNOPSIS + deaicup-e2e - Deaicup GUI PE end-to-end test (docs/RE/test-cases/pe-compat.md) + +.DESCRIPTION + AUTOEXEC runs `pe DEAICUP.EXE` (Rust/egui original Win32 API PE64 GUI app), + then the script drives the app through the QEMU monitor and asserts on the + serial log plus screendump pixel analysis: + + 1. boot chain: 9-anchor ordered chain + (cmdline -> native run -> mainCRTStartup -> RegisterClassExW -> + CreateWindowExW -> CreateDIBSection -> Context::default -> + BitBlt first frame -> bitblt ok) + 2. screendump pixel analysis of the rendered GUI frame: + not-all-black, distinct color threshold, GUI window rectangle + detection (window body color / count / bounding box / fill ratio) + 3. mouse: AUX init + mouse_move -> mpkt (WM_MOUSEMOVE), + mouse_button -> LDOWN/LUP edges (WM_LBUTTONDOWN/WM_LBUTTONUP), + cursor-region pixel diff between two screendumps + 4. keyboard: sendkey -> KEYDOWN/CHAR/KEYUP (WM_KEYDOWN/WM_CHAR/WM_KEYUP) + 5. Esc -> WM_CLOSE -> ExitProcess(0) -> [cmd] PE run end clean exit chain + (also regresses BUG-006 long-run return chain and BUG-005 noise) + + Forbidden patterns over the whole session: + [PANIC] / [IDT] exception / Unimplemented handler + + WHPX note: native PE32+ execution enables SSE via CR4 and WHPX may kill + QEMU with "Unexpected VP exit code 4" (platform limitation, not a guest + crash). If the first-frame anchor is missed under the requested + accelerator the session is rerun once with -Accel tcg + (pass -Accel tcg to skip WHPX directly). + + NOTE: keep this file ASCII-only. PS 5.1 reads BOM-less UTF-8 as GBK; + CJK comment bytes can swallow newlines and merge code into comments. + + Exit code: 0 = all checks PASS, 1 = any FAIL. +#> +param( + [switch]$Build, + [string]$Accel = 'whpx', + [int]$MonitorPort = 45454 +) +$ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'lib\QemuTest.ps1') + +Initialize-QemuTest -Build:$Build +$result = New-TestCaseResult 'deaicup-e2e' + +# ================================================================ +# PPM helpers. WARNING: [byte] -shl N truncates back to byte in +# PS 5.1, always cast to [int] before shifting (the scratch +# analyze script lost R/G channels that way). +# ================================================================ +function Read-PpmFile { + param([Parameter(Mandatory)][string]$Path) + $b = [System.IO.File]::ReadAllBytes($Path) + $p = 0 + $tok = @() + while ($tok.Count -lt 4) { + while ($p -lt $b.Length) { + $c = $b[$p] + if ($c -eq 0x23) { while ($p -lt $b.Length -and $b[$p] -ne 0x0A) { $p++ } } + elseif ($c -le 0x20) { $p++ } + else { break } + } + $s = $p + while ($p -lt $b.Length -and $b[$p] -gt 0x20) { $p++ } + $tok += [Text.Encoding]::ASCII.GetString($b, $s, $p - $s) + } + $p++ + if ($tok[0] -ne 'P6') { throw "not a P6 ppm: $($tok[0])" } + return @{ data = $b; off = $p; w = [int]$tok[1]; h = [int]$tok[2] } +} + +# Pass 1: histogram + non-black count. Pass 2: window rectangle. +# Window body color = most common color with mid-dark luminance +# (excludes black console background and bright text/cursor/highlight). +function Get-PpmStats { + param([Parameter(Mandatory)]$F) + $npix = $F.w * $F.h + $colors = @{} + $nonBlack = 0 + for ($i = 0; $i -lt $npix; $i++) { + $o = $F.off + $i * 3 + $r = [int]$F.data[$o]; $g = [int]$F.data[$o + 1]; $b = [int]$F.data[$o + 2] + $key = ($r -shl 16) -bor ($g -shl 8) -bor $b + if ($colors.ContainsKey($key)) { $colors[$key]++ } else { $colors[$key] = 1 } + if ($r -ge 8 -or $g -ge 8 -or $b -ge 8) { $nonBlack++ } + } + $winKey = -1; $winCount = 0 + foreach ($e in ($colors.GetEnumerator() | Sort-Object Value -Descending)) { + $k = [int]$e.Key + $r = ($k -shr 16) -band 0xFF; $g = ($k -shr 8) -band 0xFF; $b = $k -band 0xFF + $lum = 0.299 * $r + 0.587 * $g + 0.114 * $b + if ($lum -ge 15 -and $lum -le 90) { $winKey = $k; $winCount = [int]$e.Value; break } + } + # Window rectangle: the GUI window body is nearly solid window-color, while + # anti-aliased console text rows only contain a sparse sprinkle of it. + # So: per-row window-color count -> longest contiguous run of "solid" rows + # (>= 50% of the max row count) -> x extent + fill ratio inside that run. + $box = $null + if ($winKey -ge 0) { + $rowCnt = New-Object int[] $F.h + $rowFirst = New-Object int[] $F.h + $rowLast = New-Object int[] $F.h + for ($y = 0; $y -lt $F.h; $y++) { + $rowOff = $F.off + $y * $F.w * 3 + $cnt = 0; $fx = -1; $lx = -1 + for ($x = 0; $x -lt $F.w; $x++) { + $o = $rowOff + $x * 3 + $key = ([int]$F.data[$o] -shl 16) -bor ([int]$F.data[$o + 1] -shl 8) -bor [int]$F.data[$o + 2] + if ($key -eq $winKey) { + $cnt++ + if ($fx -lt 0) { $fx = $x } + $lx = $x + } + } + $rowCnt[$y] = $cnt; $rowFirst[$y] = $fx; $rowLast[$y] = $lx + } + $maxRow = 0 + for ($y = 0; $y -lt $F.h; $y++) { if ($rowCnt[$y] -gt $maxRow) { $maxRow = $rowCnt[$y] } } + $rowThresh = [int]($maxRow * 0.5) + $bestY0 = -1; $bestY1 = -1; $curY0 = -1 + for ($y = 0; $y -le $F.h; $y++) { + $inRun = ($y -lt $F.h) -and ($rowCnt[$y] -ge $rowThresh) + if ($inRun -and $curY0 -lt 0) { $curY0 = $y } + if (-not $inRun -and $curY0 -ge 0) { + if (($y - 1 - $curY0) -gt ($bestY1 - $bestY0)) { $bestY0 = $curY0; $bestY1 = $y - 1 } + $curY0 = -1 + } + } + if ($bestY0 -ge 0) { + $minX = $F.w; $maxX = -1; $inside = 0 + for ($y = $bestY0; $y -le $bestY1; $y++) { + if ($rowCnt[$y] -gt 0) { + if ($rowFirst[$y] -lt $minX) { $minX = $rowFirst[$y] } + if ($rowLast[$y] -gt $maxX) { $maxX = $rowLast[$y] } + $inside += $rowCnt[$y] + } + } + $bw = $maxX - $minX + 1; $bh = $bestY1 - $bestY0 + 1 + $box = @{ X0 = $minX; Y0 = $bestY0; X1 = $maxX; Y1 = $bestY1; W = $bw; H = $bh + Fill = [math]::Round($inside / [double]($bw * $bh), 4) } + } + } + return @{ Colors = $colors.Count; NonBlack = $nonBlack; Pixels = $npix + WinKey = $winKey; WinCount = $winCount; WinBox = $box } +} + +# Sampled diff (every 2nd pixel) between two frames over a region. +# A pixel counts as changed when |dR|+|dG|+|dB| > 30. +function Compare-PpmRegion { + param([Parameter(Mandatory)]$A, [Parameter(Mandatory)]$B, + [int]$X0, [int]$Y0, [int]$X1, [int]$Y1) + $changed = 0 + for ($y = $Y0; $y -le $Y1; $y += 2) { + $rowA = $A.off + $y * $A.w * 3 + $rowB = $B.off + $y * $B.w * 3 + for ($x = $X0; $x -le $X1; $x += 2) { + $oa = $rowA + $x * 3 + $ob = $rowB + $x * 3 + $d = [Math]::Abs([int]$A.data[$oa] - [int]$B.data[$ob]) + + [Math]::Abs([int]$A.data[$oa + 1] - [int]$B.data[$ob + 1]) + + [Math]::Abs([int]$A.data[$oa + 2] - [int]$B.data[$ob + 2]) + if ($d -gt 30) { $changed++ } + } + } + return $changed +} + +function Convert-PpmToPng { + param([Parameter(Mandatory)][string]$PpmPath, [Parameter(Mandatory)][string]$PngPath) + Add-Type -AssemblyName System.Drawing + $bytes = [System.IO.File]::ReadAllBytes($PpmPath) + # ASCII decode: 1 byte == 1 char, so string index == byte offset. + # QEMU screendump header is "P6\n \n255\n" + single whitespace byte. + $hdr = [System.Text.Encoding]::ASCII.GetString($bytes, 0, [Math]::Min(64, $bytes.Length)) + $m = [regex]::Match($hdr, '^P6\s+(\d+)\s+(\d+)\s+(\d+)\s') + if (-not $m.Success) { throw 'bad ppm header' } + $w = [int]$m.Groups[1].Value; $h = [int]$m.Groups[2].Value; $max = [int]$m.Groups[3].Value + if ($max -ne 255) { throw "unsupported maxval $max" } + $pos = $m.Length + $bmp = New-Object System.Drawing.Bitmap $w, $h, ([System.Drawing.Imaging.PixelFormat]::Format24bppRgb) + $rect = New-Object System.Drawing.Rectangle 0, 0, $w, $h + $data = $bmp.LockBits($rect, [System.Drawing.Imaging.ImageLockMode]::WriteOnly, $bmp.PixelFormat) + try { + $rowBytes = $w * 3 + $rgb = New-Object byte[] ($rowBytes) + for ($y = 0; $y -lt $h; $y++) { + $srcOff = $pos + $y * $rowBytes + for ($x = 0; $x -lt $w; $x++) { + $rgb[$x*3+0] = $bytes[$srcOff + $x*3 + 2] # B + $rgb[$x*3+1] = $bytes[$srcOff + $x*3 + 1] # G + $rgb[$x*3+2] = $bytes[$srcOff + $x*3 + 0] # R + } + [System.Runtime.InteropServices.Marshal]::Copy($rgb, 0, [IntPtr]($data.Scan0.ToInt64() + $y * $data.Stride), $rowBytes) + } + } finally { $bmp.UnlockBits($data) } + $bmp.Save($PngPath, [System.Drawing.Imaging.ImageFormat]::Png) + $bmp.Dispose() +} + +$script:UsedAccel = $Accel +$s = $null +try { + Set-DeshabFirstInit -First 1 -DevMode 0 + Set-DeshabAutoexec -Lines @('pe DEAICUP.EXE') + + $s = Start-QemuSession -Name "deaicup-$Accel" -Accel $Accel -MonitorPort $MonitorPort + $result.LogPath = $s.LogPath + + # ---- 1. boot chain + first frame (WHPX miss -> TCG rerun once) ---- + $waitFirst = 150 + if ($Accel -eq 'tcg') { $waitFirst = 360 } # TCG full emulation is much slower + $hit = Wait-QemuLog -Session $s -Patterns @('[shim] BitBlt first frame on screen', '[PANIC]', '[IDT] exception') -TimeoutSeconds $waitFirst + + if ($hit -ne '[shim] BitBlt first frame on screen' -and $Accel -eq 'whpx') { + $why = 'first-frame anchor missed under WHPX' + $early = Read-QemuLogText $s.LogPath + if ($early.Contains('WHPX: Unexpected VP exit')) { $why = 'WHPX terminated QEMU at native PE execution (Unexpected VP exit, platform limitation)' } + Add-TestNote $result "$why; rerunning once with TCG" + Stop-QemuSession $s + $s = $null + $script:UsedAccel = 'tcg' + $s = Start-QemuSession -Name 'deaicup-tcg' -Accel 'tcg' -MonitorPort $MonitorPort + $result.LogPath = $s.LogPath + $hit = Wait-QemuLog -Session $s -Patterns @('[shim] BitBlt first frame on screen', '[PANIC]', '[IDT] exception') -TimeoutSeconds 360 + } + + $log = Read-QemuLogText $s.LogPath + if ($hit -ne '[shim] BitBlt first frame on screen') { + Add-TestCheck $result 'first frame: BitBlt first frame on screen' 'FAIL' "hit=$hit" + throw 'first-frame anchor missing' + } + Add-TestCheck $result 'first frame: BitBlt first frame on screen' 'PASS' + Assert-QemuLog $result $log -Ordered @( + '[cmd] cmdline: DEAICUP.EXE', + '[PE] running PE32+ natively', + '[deaicup] mainCRTStartup', + '[shim] RegisterClassExW ok', + '[shim] CreateWindowExW ok', + '[shim] CreateDIBSection ok', + '[dc] Context::default ok', + '[shim] BitBlt first frame on screen', + '[dc] bitblt ok' + ) -MustNotContain @('[PANIC]', '[IDT] exception', 'Unimplemented handler') + + # ---- 2. screendump A: pixel analysis of the rendered GUI frame ---- + # QEMU human monitor parses backslashes as escapes inside quoted strings + # (\C etc. illegal -> screendump silently fails); OutPath must use '/'. + Start-Sleep -Seconds 2 + $ppmA = ($script:TestWorkDir -replace '\\', '/') + '/deaicup-frame-a.ppm' + $ppmB = ($script:TestWorkDir -replace '\\', '/') + '/deaicup-frame-b.ppm' + $png = Join-Path $script:TestWorkDir 'deaicup-frame.png' + $fa = $null + $statsA = $null + if (Get-QemuScreenshot -Session $s -OutPath $ppmA) { + try { + Convert-PpmToPng -PpmPath $ppmA -PngPath $png + Add-TestCheck $result "screenshot: evidence frame -> $png" 'PASS' + } catch { + Add-TestCheck $result 'screenshot: PPM->PNG convert' 'FAIL' $_.Exception.Message + } + $fa = Read-PpmFile $ppmA + $statsA = Get-PpmStats $fa + $boxDesc = 'none' + if ($statsA.WinBox) { + $boxDesc = ("({0},{1})-({2},{3}) {4}x{5} fill={6:P1}" -f $statsA.WinBox.X0, $statsA.WinBox.Y0, + $statsA.WinBox.X1, $statsA.WinBox.Y1, $statsA.WinBox.W, $statsA.WinBox.H, $statsA.WinBox.Fill) + } + Add-TestNote $result ("frame {0}x{1}: distinctColors={2} nonBlack={3}/{4} winColor=#{5:X6} winCount={6} winBox={7}" -f + $fa.w, $fa.h, $statsA.Colors, $statsA.NonBlack, $statsA.Pixels, $statsA.WinKey, $statsA.WinCount, $boxDesc) + + if ($statsA.NonBlack -ge 100000) { + Add-TestCheck $result "pixels: not all black (nonBlack=$($statsA.NonBlack) >= 100000)" 'PASS' + } else { + Add-TestCheck $result 'pixels: not all black' 'FAIL' "nonBlack=$($statsA.NonBlack) (< 100000)" + } + if ($statsA.Colors -ge 32) { + Add-TestCheck $result "pixels: distinct colors >= 32 ($($statsA.Colors))" 'PASS' + } else { + Add-TestCheck $result 'pixels: distinct colors >= 32' 'FAIL' "only $($statsA.Colors)" + } + $wb = $statsA.WinBox + if ($statsA.WinKey -ge 0 -and $statsA.WinCount -ge 300000 -and $wb -and + $wb.W -ge 600 -and $wb.W -le 900 -and $wb.H -ge 450 -and $wb.H -le 700 -and $wb.Fill -ge 0.75) { + Add-TestCheck $result "pixels: GUI window rect ($($wb.W)x$($wb.H) fill=$($wb.Fill.ToString('P1')))" 'PASS' + } else { + Add-TestCheck $result 'pixels: GUI window rect' 'FAIL' ("winKey=#{0:X6} count={1} box={2}" -f $statsA.WinKey, $statsA.WinCount, $boxDesc) + } + } else { + Add-TestCheck $result 'screenshot: screendump A' 'FAIL' 'ppm not produced' + } + + # ---- 3. mouse: mouse_move -> WM_MOUSEMOVE (mpkt) + cursor pixel diff ---- + $mouseInitOk = $log.Contains('[shim] mouse init ok (AUX streaming, polled)') + Send-QemuMouseMove -Session $s -Dx 120 -Dy 60 + Start-Sleep -Milliseconds 700 + Send-QemuMouseMove -Session $s -Dx 40 -Dy 20 + $hitMv = Wait-QemuLog -Session $s -Patterns @('[shim] mpkt dx=', '[PANIC]', '[IDT] exception') -TimeoutSeconds 60 + if ($mouseInitOk -and $hitMv -eq '[shim] mpkt dx=') { + Add-TestCheck $result 'mouse: AUX init + mouse_move -> WM_MOUSEMOVE (mpkt)' 'PASS' + } else { + Add-TestCheck $result 'mouse: AUX init + mouse_move -> WM_MOUSEMOVE (mpkt)' 'FAIL' "init=$mouseInitOk hit=$hitMv" + } + + Start-Sleep -Seconds 2 + $cursorDiff = -1 + if ($fa -and (Get-QemuScreenshot -Session $s -OutPath $ppmB)) { + $fb = Read-PpmFile $ppmB + $x0 = 4; $y0 = 4; $x1 = $fa.w - 5; $y1 = $fa.h - 5 + if ($statsA.WinBox) { + $x0 = $statsA.WinBox.X0 + 4; $y0 = $statsA.WinBox.Y0 + 4 + $x1 = $statsA.WinBox.X1 - 4; $y1 = $statsA.WinBox.Y1 - 4 + } + $cursorDiff = Compare-PpmRegion -A $fa -B $fb -X0 $x0 -Y0 $y0 -X1 $x1 -Y1 $y1 + Add-TestNote $result "cursor-region diff (sampled, region ($x0,$y0)-($x1,$y1)): $cursorDiff px" + } + if ($cursorDiff -ge 8 -and $cursorDiff -le 60000) { + Add-TestCheck $result "mouse: cursor-region pixel diff after move ($cursorDiff px)" 'PASS' + } else { + Add-TestCheck $result 'mouse: cursor-region pixel diff after move' 'FAIL' "diff=$cursorDiff (expect 8..60000)" + } + + # ---- 4. mouse buttons: mouse_button -> WM_LBUTTONDOWN/UP edges ---- + [void](Invoke-QemuMonitor -Session $s -Command 'mouse_button 1' -SettleMs 500) + [void](Invoke-QemuMonitor -Session $s -Command 'mouse_button 0' -SettleMs 500) + $hitUp = Wait-QemuLog -Session $s -Patterns @('[shim] LUP', '[PANIC]', '[IDT] exception') -TimeoutSeconds 60 + $log = Read-QemuLogText $s.LogPath + if ($hitUp -eq '[shim] LUP' -and $log.Contains('[shim] LDOWN lp=')) { + Add-TestCheck $result 'mouse: mouse_button -> WM_LBUTTONDOWN/UP edges' 'PASS' + } else { + Add-TestCheck $result 'mouse: mouse_button -> WM_LBUTTONDOWN/UP edges' 'FAIL' "hit=$hitUp ldown=$($log.Contains('[shim] LDOWN lp='))" + } + + # ---- 5. keyboard: sendkey -> WM_KEYDOWN / WM_CHAR / WM_KEYUP ---- + Send-QemuKeys -Session $s -Keys @('a') + $hitK = Wait-QemuLog -Session $s -Patterns @('[shim] CHAR ch=', '[PANIC]', '[IDT] exception') -TimeoutSeconds 60 + $log = Read-QemuLogText $s.LogPath + if ($hitK -eq '[shim] CHAR ch=' -and $log.Contains('[shim] KEYDOWN vk=') -and $log.Contains('[shim] KEYUP vk=')) { + Add-TestCheck $result 'keyboard: sendkey -> WM_KEYDOWN/WM_CHAR/WM_KEYUP' 'PASS' + } else { + Add-TestCheck $result 'keyboard: sendkey -> WM_KEYDOWN/WM_CHAR/WM_KEYUP' 'FAIL' "hit=$hitK" + } + + # ---- 6. Esc -> WM_CLOSE -> ExitProcess(0) -> cmd continues ---- + Send-QemuKeys -Session $s -Keys @('esc') + $hitQ = Wait-QemuLog -Session $s -Patterns @('[cmd] PE run end', '[PANIC]', '[IDT] exception') -TimeoutSeconds 90 + $log = Read-QemuLogText $s.LogPath + if ($hitQ -eq '[cmd] PE run end') { + Add-TestCheck $result 'exit chain: Esc -> WM_CLOSE -> ExitProcess(0) -> cmd continues' 'PASS' + } else { + Add-TestCheck $result 'exit chain: Esc -> WM_CLOSE -> ExitProcess(0) -> cmd continues' 'FAIL' "hit=$hitQ" + } + Assert-QemuLog $result $log -Ordered @( + '[deaicup] quit, ExitProcess(0)', + '[PE] ExitProcess code=', + '[cmd] PE run end' + ) -MustNotContain @('[PANIC]', '[IDT] exception', 'Unimplemented handler') + + Add-TestNote $result "accelerator: $script:UsedAccel" +} finally { + if ($s) { Stop-QemuSession $s } + Restore-DeshabScenario +} + +Write-TestCaseResult $result +if ($result.Status -eq 'FAIL') { exit 1 } else { exit 0 } diff --git a/tests/qemu/driver-matrix.ps1 b/tests/qemu/driver-matrix.ps1 index 9e2cb44..23ca606 100644 --- a/tests/qemu/driver-matrix.ps1 +++ b/tests/qemu/driver-matrix.ps1 @@ -1,4 +1,4 @@ -#requires -Version 5.1 +#requires -Version 5.1 <# .SYNOPSIS driver-matrix — DKM 驱动功能矩阵(docs/RE/test-cases/driver-matrix.md) @@ -27,7 +27,7 @@ Initialize-QemuTest -Build:$Build $result = New-TestCaseResult 'driver-matrix' Write-Host "`n--- 全设备单会话驱动矩阵 ---" -$s = Start-QemuSession -Name 'driver-matrix' +$s = Start-QemuSession -Name 'driver-matrix' -MonitorPort 45502 try { $hit = Wait-QemuLog -Session $s -Patterns @('[DSK] boot') -TimeoutSeconds 150 Start-Sleep -Seconds 2 @@ -66,6 +66,21 @@ try { '[DSM] driver disabled by FUCK' # ath9k: FUCK 禁用路径 ) + # ---- NVMe 数据面回归(写路径):签名盘自检 + 写/刷/读回 + provider 序号 ---- + Assert-QemuLog $result $log -MustContain @( + '[nvme] LBA0 signature OK', # LBA0 DESHABNVME0 签名识别(测试盘) + '[nvme] write+flush+readback verify OK', # 写→FLUSH→读回逐字节校验(PRP2 + PRP list 两趟) + '[nvme] LBA0 sig intact after write tests' # 写测试后 LBA0 签名未损坏 + ) + # ahci0=0 → nvme0=1 注册顺序([BLOCK] device registered → 名称 → index 三联行) + Assert-QemuLog $result $log -Ordered @( + '[INFO] ahci0', + '[BLOCK] index=0x0000000000000000', + '[INFO] nvme0', + '[BLOCK] index=0x0000000000000001' + ) + Assert-QemuLog $result $log -CountPattern '[nvme] write+flush+readback verify OK' -MinCount 2 + # ---- DSM 装载统计:16 个 manifest 驱动中 15 个 active(ath9k 禁用) ---- Assert-QemuLog $result $log -CountPattern '[DKM] external driver active' -MinCount 15 diff --git a/tests/qemu/lib/QemuTest.ps1 b/tests/qemu/lib/QemuTest.ps1 index 9c1b019..806e827 100644 --- a/tests/qemu/lib/QemuTest.ps1 +++ b/tests/qemu/lib/QemuTest.ps1 @@ -1,4 +1,4 @@ -#requires -Version 5.1 +#requires -Version 5.1 <# .SYNOPSIS Deshab QEMU 自动化测试公共库(tests/qemu/lib/QemuTest.ps1) @@ -20,6 +20,14 @@ $script:BuildTmp = Join-Path $script:RepoRoot '.build_tmp' $script:TestWorkDir = Join-Path $script:BuildTmp 'tests' $script:IsoDir = Join-Path $script:RepoRoot 'ISO' $script:SystemDir = Join-Path $script:RepoRoot 'SYSTEM' +# 并发隔离布局(另一代理并发跑 QEMU/build,共享路径一律只读,写入全进私有区): +# .build_tmp\tests\img\ 私有镜像副本(deshab/nvme/usb,QEMU 只挂副本) +# .build_tmp\tests\SYSTEM\ SYSTEM 镜像树(robocopy /MIR;场景文件只翻这里) +# .build_tmp\tests\CODE\...\deaicup.exe mkfat32 引用的单文件副本 +# .build_tmp\tests\fat32gen\ mkfat32.ps1 副本 + 私有 SATA 镜像输出 +$script:SuiteImgDir = Join-Path $script:TestWorkDir 'img' +$script:MkFat32Dir = Join-Path $script:TestWorkDir 'fat32gen' +$script:MirrorSystem = Join-Path $script:TestWorkDir 'SYSTEM' $script:Config = $null # 场景备份(模块级,保证只备份一次、恢复幂等) @@ -56,60 +64,147 @@ function Initialize-QemuTest { if (-not $ovmf) { throw 'OVMF UEFI firmware not found under QEMU install directory' } New-Item -ItemType Directory -Force -Path $script:TestWorkDir | Out-Null + New-Item -ItemType Directory -Force -Path $script:SuiteImgDir | Out-Null + New-Item -ItemType Directory -Force -Path $script:MkFat32Dir | Out-Null - $img = Join-Path $script:IsoDir 'deshab.img' - $sata = Join-Path $script:BuildTmp 'sata_fat32_dsk.img' - $nvme = Join-Path $script:BuildTmp 'nvme_test.img' - $usb = Join-Path $script:BuildTmp 'usb_test.img' + $imgSrc = Join-Path $script:IsoDir 'deshab.img' + $nvmeSrc = Join-Path $script:BuildTmp 'nvme_test.img' + $usbSrc = Join-Path $script:BuildTmp 'usb_test.img' - if ($Build -or -not (Test-Path $img)) { + if ($Build -or -not (Test-Path $imgSrc)) { Write-Host '[init] building images via build.ps1 ...' Stop-ResidualQemu & powershell -ExecutionPolicy Bypass -File (Join-Path $script:RepoRoot 'build.ps1') | Out-Null if ($LASTEXITCODE -ne 0) { throw "build.ps1 failed with exit code $LASTEXITCODE" } } - if (-not (Test-Path $img)) { throw "image missing: $img (run with -Build)" } - if (-not (Test-Path $sata)) { - Write-Host '[init] sata_fat32_dsk.img missing, regenerating via mkfat32.ps1 ...' - Invoke-MkFat32 - } - if (-not (Test-Path $sata)) { throw "sata image missing: $sata" } + if (-not (Test-Path $imgSrc)) { throw "image missing: $imgSrc (run with -Build)" } - $nvmeOk = $null; if (Test-Path $nvme) { $nvmeOk = $nvme } - $usbOk = $null; if (Test-Path $usb) { $usbOk = $usb } + # --- 并发隔离:SYSTEM 镜像树 + 私有 SATA 生成器 + 私有镜像副本 --- + Sync-SuiteTree + Invoke-MkFat32 + $sata = Join-Path $script:MkFat32Dir 'sata_fat32_dsk.img' + if (-not (Test-Path $sata)) { throw "private sata image not generated: $sata" } + + $img = Copy-SuiteImage -Source $imgSrc -FileName 'deshab.img' -Mandatory + $nvme = Copy-SuiteImage -Source $nvmeSrc -FileName 'nvme_test.img' + $usb = Copy-SuiteImage -Source $usbSrc -FileName 'usb_test.img' $script:Config = [pscustomobject]@{ QemuExe = $qemu Ovmf = $ovmf Img = $img SataImg = $sata - NvmeImg = $nvmeOk - UsbImg = $usbOk + NvmeImg = $nvme + UsbImg = $usb } Write-Host "[init] QEMU: $qemu" Write-Host "[init] OVMF: $ovmf" - Write-Host "[init] IMG: $img" + Write-Host "[init] IMG: $img (private copy)" return $script:Config } +# ================================================================ +# 并发隔离:共享文件只读复制到套件私有区 +# ================================================================ + +# share-tolerant 复制:源可能被并发代理的 QEMU/打包器短暂占用,重试至多 60s。 +# 目标不变(mtime+size 相同)时跳过,避免每用例重复拷 768MB。 +function Copy-SuiteImage { + param( + [Parameter(Mandatory)][string]$Source, + [Parameter(Mandatory)][string]$FileName, + [switch]$Mandatory + ) + if (-not (Test-Path $Source)) { + if ($Mandatory) { throw "image missing: $Source (run with -Build)" } + return $null + } + $dest = Join-Path $script:SuiteImgDir $FileName + $srcItem = Get-Item $Source + if ((Test-Path $dest)) { + $dstItem = Get-Item $dest + if ($dstItem.Length -eq $srcItem.Length -and $dstItem.LastWriteTimeUtc -eq $srcItem.LastWriteTimeUtc) { + return $dest + } + } + $deadline = (Get-Date).AddSeconds(60) + while ($true) { + $in = $null; $out = $null + try { + $in = [System.IO.File]::Open($Source, 'Open', 'Read', [System.IO.FileShare]::ReadWrite -bor [System.IO.FileShare]::Delete) + $out = [System.IO.File]::Open($dest, 'Create', 'Write', [System.IO.FileShare]::None) + $in.CopyTo($out, 1048576) + $out.Close(); $in.Close() + [System.IO.File]::SetLastWriteTimeUtc($dest, $srcItem.LastWriteTimeUtc) + Write-Host "[init] private copy: $FileName ($([math]::Round($srcItem.Length/1MB))MB)" + return $dest + } catch [System.IO.IOException] { + if ($out) { try { $out.Close() } catch {} } + if ($in) { try { $in.Close() } catch {} } + if ((Get-Date) -gt $deadline) { + throw "cannot copy $Source (locked by concurrent process for >60s)" + } + Start-Sleep -Seconds 2 + } catch { + if ($out) { try { $out.Close() } catch {} } + if ($in) { try { $in.Close() } catch {} } + throw + } + } +} + +# SYSTEM 镜像树 + deaicup.exe 单文件 + mkfat32.ps1 生成器副本。 +# 场景翻转(firstInit/AUTOEXEC)只作用于镜像树,共享工作树绝不写入。 +function Sync-SuiteTree { + & robocopy $script:SystemDir $script:MirrorSystem /MIR /R:2 /W:1 /NFL /NDL /NJH /NJS /NP | Out-Null + if ($LASTEXITCODE -ge 8) { throw "robocopy SYSTEM mirror failed (rc=$LASTEXITCODE)" } + + $deaSrc = Join-Path $script:RepoRoot 'CODE\Deaicup Software\pe\target\x86_64-pc-windows-msvc\release\deaicup.exe' + if (Test-Path $deaSrc) { + $deaDst = Join-Path $script:TestWorkDir 'CODE\Deaicup Software\pe\target\x86_64-pc-windows-msvc\release\deaicup.exe' + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $deaDst) | Out-Null + $srcItem = Get-Item $deaSrc + $need = $true + if (Test-Path $deaDst) { + $dstItem = Get-Item $deaDst + if ($dstItem.Length -eq $srcItem.Length -and $dstItem.LastWriteTimeUtc -eq $srcItem.LastWriteTimeUtc) { $need = $false } + } + if ($need) { Copy-Item $deaSrc $deaDst -Force } + } + + $mkSrc = Join-Path $script:BuildTmp 'mkfat32.ps1' + if (-not (Test-Path $mkSrc)) { throw "mkfat32.ps1 not found: $mkSrc (run build.ps1 once)" } + Copy-Item $mkSrc (Join-Path $script:MkFat32Dir 'mkfat32.ps1') -Force +} + # ================================================================ # 进程管理 # ================================================================ -# 清理持有本项目镜像的残留 QEMU(deshab.img 被锁会导致 build/启动失败)。 -# 只杀命令行中引用本仓库路径的 qemu 进程,避免误伤他人虚拟机。 +# 清理本套件历史会话残留的 QEMU(Start-QemuSession 以 -name deshabtest-... 标记)。 +# 只按 deshabtest- 标记精确匹配,绝不动其他代理/人工启动的 QEMU(并发隔离要求: +# 另一代理可能正挂着本仓库镜像跑调试,全局或按路径匹配都会误杀)。 function Stop-ResidualQemu { $procs = Get-CimInstance Win32_Process -Filter "Name='qemu-system-x86_64.exe'" -ErrorAction SilentlyContinue foreach ($p in $procs) { - if ($p.CommandLine -and $p.CommandLine.ToLower().Contains('deshab')) { - Write-Host "[qemu] killing residual qemu pid=$($p.ProcessId)" + if ($p.CommandLine -and $p.CommandLine.Contains('deshabtest-')) { + Write-Host "[qemu] killing residual suite qemu pid=$($p.ProcessId)" Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue } } } +# monitor 端口:45501-45508 按用例分配,deaicup-e2e 固定 45454 +# (避开人工调试 4444 与其他套件 456xx/457xx)。 +# -Preferred 传入用例固定端口;被占用(如 TIME_WAIT)时扫描段内其余端口, +# 最后兜底 45509-45599。段内端口被他人占用时只换端口,绝不杀占用进程。 function Get-FreeMonitorPort { - for ($port = 4444; $port -lt 4544; $port++) { + param([int]$Preferred = 0) + $candidates = New-Object System.Collections.ArrayList + if (($Preferred -ge 45501 -and $Preferred -le 45508) -or $Preferred -eq 45454) { [void]$candidates.Add($Preferred) } + foreach ($p in 45501..45508) { if ($p -ne $Preferred) { [void]$candidates.Add($p) } } + foreach ($p in 45509..45599) { [void]$candidates.Add($p) } + foreach ($port in $candidates) { $listener = $null try { $listener = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Loopback, $port) @@ -137,6 +232,8 @@ function Start-QemuSession { [switch]$NoNet, [string]$NetModel = 'e1000', [switch]$NoMonitor, + [int]$MonitorPort = 0, + [switch]$NoSnapshot, [string]$Cpu = '', [string[]]$ExtraArgs = @() ) @@ -167,9 +264,12 @@ function Start-QemuSession { '-drive', ('format=raw,file="{0}",if=virtio' -f $cfg.Img) ) + # -snapshot 默认开:镜像只读 + 写入落临时 overlay,避免镜像写锁与并发代理冲突。 + if (-not $NoSnapshot) { $qargs += '-snapshot' } + $port = 0 if (-not $NoMonitor) { - $port = Get-FreeMonitorPort + $port = Get-FreeMonitorPort -Preferred $MonitorPort $qargs += @('-monitor', "tcp:127.0.0.1:$port,server,nowait") } if (-not $NoSata) { @@ -192,13 +292,21 @@ function Start-QemuSession { } elseif ($NetModel -eq 'virtio-net-pci') { $qargs += @('-device', 'virtio-net-pci,netdev=net0') } + } else { + # 已知坑3:不显式禁网时 QEMU 会自动补一块默认 e1000 网卡(q35 默认型号), + # -NoNet 场景必须用 -nic none 把默认网络整个关掉。 + $qargs += @('-nic', 'none') } foreach ($x in $ExtraArgs) { $qargs += $x } $qargs += @('-boot', 'menu=on') - # 已知坑:Start-Process 下 -serial file: 不生效;用 cmd 批处理重定向 stdio。 + # 已知坑1:Start-Process 下 -serial file: 不生效;用 cmd 批处理重定向 stdio。 + # 已知坑2:-snapshot 会在 %TEMP% 创建临时 overlay,Windows 用户名含非 ASCII + # (如中文)时 QEMU 创建失败;把 TEMP/TMP 重定向到仓库内 ASCII 目录。 + $qemuTmp = Join-Path $script:BuildTmp 'qemu-tmp' + New-Item -ItemType Directory -Force -Path $qemuTmp | Out-Null $cmdline = '"' + $cfg.QemuExe + '" ' + ($qargs -join ' ') - $batContent = "@echo off`r`n$cmdline > `"$log`" 2>&1`r`n" + $batContent = "@echo off`r`nset `"TEMP=$qemuTmp`"`r`nset `"TMP=$qemuTmp`"`r`n$cmdline > `"$log`" 2>&1`r`n" [System.IO.File]::WriteAllText($bat, $batContent) Stop-ResidualQemu @@ -213,6 +321,24 @@ function Start-QemuSession { Accel = $Accel StartTime = Get-Date } + + # 早夭自愈:WHPX 分区被强杀后偶发重建异常,guest 启动即三重故障 + # (实测日志仅一行 "Ignoring request for interrupt vector 0")。 + # 启动后观察 3s:进程已死且日志近乎为空 → 同一 bat 重开一次 + # (tag/monitor 端口不变,旧进程已死无冲突;bat 重定向会重建日志)。 + Start-Sleep -Seconds 3 + if (Test-QemuSessionExited $session) { + $logSize = 0 + if (Test-Path $log) { $logSize = (Get-Item $log).Length } + if ($logSize -lt 4096) { + Write-Host "[qemu] session '$Name' died at launch (log ${logSize}B, WHPX startup flake), relaunching once" + Start-Sleep -Milliseconds 800 + Start-Process -FilePath $bat -WindowStyle Hidden + } else { + Write-Host "[qemu] session '$Name' exited early (log ${logSize}B)" + } + } + Write-Host "[qemu] session '$Name' started (monitor=$port, log=$log)" return $session } @@ -426,20 +552,24 @@ function Get-QemuScreenshot { # 场景准备(firstInit.txt / AUTOEXEC.BAT + SATA 镜像重建) # ================================================================ +# 私有 SATA 镜像生成:运行 fat32gen\ 下的 mkfat32.ps1 副本。 +# 副本的 PSScriptRoot=fat32gen → 输出 fat32gen\sata_fat32_dsk.img(私有), +# 输入 tests\SYSTEM 镜像树 + tests\CODE\...\deaicup.exe,不读写任何共享路径。 function Invoke-MkFat32 { - $mk = Join-Path $script:BuildTmp 'mkfat32.ps1' - if (-not (Test-Path $mk)) { throw "mkfat32.ps1 not found: $mk (run build.ps1 once)" } + $mk = Join-Path $script:MkFat32Dir 'mkfat32.ps1' + if (-not (Test-Path $mk)) { throw "mkfat32.ps1 copy not found: $mk (Initialize-QemuTest runs Sync-SuiteTree)" } & powershell -ExecutionPolicy Bypass -File $mk | Out-Null if ($LASTEXITCODE -ne 0) { throw "mkfat32.ps1 failed with exit code $LASTEXITCODE" } } -# 写 firstInit.txt(格式 "`n",LF)并重建 SATA 镜像;原文件自动备份 +# 写镜像树 firstInit.txt(格式 "`n",LF)并重建私有 SATA 镜像;原内容自动备份。 +# 只写 .build_tmp\tests\SYSTEM 镜像树,共享 SYSTEM 工作树保持不变(并发代理可能正在翻转它)。 function Set-DeshabFirstInit { param( [Parameter(Mandatory)][int]$First, [Parameter(Mandatory)][int]$DevMode ) - $path = Join-Path $script:SystemDir 'system\user\use\firstInit.txt' + $path = Join-Path $script:MirrorSystem 'system\user\use\firstInit.txt' if ($null -eq $script:FirstInitBackup) { $script:FirstInitBackup = [System.IO.File]::ReadAllBytes($path) } @@ -448,7 +578,7 @@ function Set-DeshabFirstInit { } function Restore-DeshabFirstInit { - $path = Join-Path $script:SystemDir 'system\user\use\firstInit.txt' + $path = Join-Path $script:MirrorSystem 'system\user\use\firstInit.txt' if ($null -ne $script:FirstInitBackup) { [System.IO.File]::WriteAllBytes($path, $script:FirstInitBackup) $script:FirstInitBackup = $null @@ -456,10 +586,10 @@ function Restore-DeshabFirstInit { } } -# 替换 AUTOEXEC.BAT 内容(cmd.elf 启动时逐行自动执行)并重建 SATA 镜像;原文件自动备份 +# 替换镜像树 AUTOEXEC.BAT 内容(cmd.elf 启动时逐行自动执行)并重建私有 SATA 镜像;原内容自动备份 function Set-DeshabAutoexec { param([Parameter(Mandatory)][string[]]$Lines) - $path = Join-Path $script:SystemDir 'bin\AUTOEXEC.BAT' + $path = Join-Path $script:MirrorSystem 'bin\AUTOEXEC.BAT' if ($null -eq $script:AutoexecBackup) { $script:AutoexecBackup = [System.IO.File]::ReadAllBytes($path) } @@ -468,7 +598,7 @@ function Set-DeshabAutoexec { } function Restore-DeshabAutoexec { - $path = Join-Path $script:SystemDir 'bin\AUTOEXEC.BAT' + $path = Join-Path $script:MirrorSystem 'bin\AUTOEXEC.BAT' if ($null -ne $script:AutoexecBackup) { [System.IO.File]::WriteAllBytes($path, $script:AutoexecBackup) $script:AutoexecBackup = $null diff --git a/tests/qemu/linux-compat.ps1 b/tests/qemu/linux-compat.ps1 index f556803..c62451c 100644 --- a/tests/qemu/linux-compat.ps1 +++ b/tests/qemu/linux-compat.ps1 @@ -1,4 +1,4 @@ -#requires -Version 5.1 +#requires -Version 5.1 <# .SYNOPSIS linux-compat — UTSM+Linux 双内核兼容层端到端(docs/RE/test-cases/linux-compat.md) @@ -35,7 +35,7 @@ $result = New-TestCaseResult 'linux-compat' Set-DeshabFirstInit -First 1 -DevMode 1 Set-DeshabAutoexec -Lines @('ver', 'exit') -$s = Start-QemuSession -Name 'linux-compat' -Cpu $Cpu +$s = Start-QemuSession -Name 'linux-compat' -Cpu $Cpu -MonitorPort 45503 try { # ---- 分流点:VMM 自检通过 还是 VMM 不可用 ---- $fork = Wait-QemuLog -Session $s -Patterns @( diff --git a/tests/qemu/network-e2e.ps1 b/tests/qemu/network-e2e.ps1 new file mode 100644 index 0000000..d5b0dd2 --- /dev/null +++ b/tests/qemu/network-e2e.ps1 @@ -0,0 +1,189 @@ +#requires -Version 5.1 +<# +.SYNOPSIS + network-e2e — 网络端到端测试(docs/RE/test-cases/network-e2e.md) + +.DESCRIPTION + 场景 N-A(netman 全链路,首启调度): + firstInit="0\n0" 首启,挂 e1000(net0) + virtio-net(net1) 双网卡。 + DSK 首启链 mouseInit → netman → FirstInit,netman 读取 conf.conf(mode=dhcp), + 在 index=0 的 e1000 上执行 DHCP DISCOVER/OFFER/REQUEST/ACK → ARP 网关 → DNS example.com。 + 断言锚点(netman log_hex 为 16 位定宽小写十六进制,同行输出): + [virtio_net] selftest PASS: RX path ok (stage3 驱动自测,真实 slirp 后端 DHCP OFFER) + [netman] netdev_count=0x0000000000000002 (e1000 + virtio_net 双设备) + [netman] DHCP OFFER ip= / DHCP ACK, leased ip= / DHCP final rc=0x0...0 + [netman] ARP: gateway resolved / ARP final rc=0x0...0 + [netman] DNS example.com= / DNS final rc=0x0...0 + [netman] network final rc=0x0000000000000000 + netman 之后停在 FirstInit 向导即杀会话(向导自动化由 boot-regression B1 覆盖)。 + + 场景 N-B(curl.elf 主机侧端到端,dev_mode shell): + firstInit="1\n1" → shell dev_tests → 交互 `curl`(PATH → /bin/CURL.ELF)。 + 主机侧 Start-TestHttpServer 监听 127.0.0.1:8000,guest 输入 + `http://10.0.2.2:8000/` 经 slirp 网关访问主机服务; + 断言主机侧 HTTP 服务器日志收到 "GET / HTTP/1.0"(端到端最强证据), + guest 侧断言 [curl] boot / [curl] exit / [shell] path: tool returned。 + 响应体只写帧缓冲(无串口),标 MANUAL。 + + 退出码:全部通过 0,任一 FAIL 1。 +#> +param( + [switch]$Build, + [string[]]$Scenarios = @('NA', 'NB') +) +$ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'lib\QemuTest.ps1') + +Initialize-QemuTest -Build:$Build +$result = New-TestCaseResult 'network-e2e' + +# ---------------------------------------------------------------- +function Invoke-ScenarioNA { + Write-Host "`n--- N-A netman 全链路 (首启, e1000 + virtio-net 双网卡) ---" + Set-DeshabFirstInit -First 0 -DevMode 0 + # run_qemu.bat 默认仅 e1000;追加第二网卡 virtio-net-pci 挂独立 slirp 后端 net1。 + $s = Start-QemuSession -Name 'nete2e-na' -MonitorPort 45506 -ExtraArgs @( + '-netdev', 'user,id=net1', + '-device', 'virtio-net-pci,netdev=net1' + ) + try { + $hit = Wait-QemuLog -Session $s -Patterns @( + '[netman] network final rc=', + '[netman] DHCP: no OFFER received', + '[netman] ARP: timeout', + '[netman] DNS: timeout', + '[PANIC]' + ) -TimeoutSeconds 240 + Start-Sleep -Seconds 2 + $log = Read-QemuLogText $s.LogPath + $result.LogPath = $s.LogPath + + # ---- N1/N2: 双网卡驱动与设备注册 ---- + Assert-QemuLog $result $log -Ordered @( + '[e1000] netdev registered, index=0x0000000000000000', + '[virtio_net] netdev registered, index=', + '[virtio_net] selftest: TX path ok', + '[virtio_net] selftest: DHCP OFFER yiaddr=', + '[virtio_net] selftest PASS: RX path ok' + ) + # ---- netman 全链路(N3 DHCP / N4 ARP / N5 DNS) ---- + Assert-QemuLog $result $log -Ordered @( + '[DSK] loading netman', + '[netman] boot', + '[netman] netdev_count=0x0000000000000002', + '[netman] DHCP: sending DISCOVER', + '[netman] DHCP OFFER ip=', + '[netman] DHCP: sending REQUEST', + '[netman] DHCP ACK, leased ip=', + '[netman] DHCP final rc=0x0000000000000000', + '[netman] ARP: gateway resolved', + '[netman] ARP final rc=0x0000000000000000', + '[netman] DNS example.com=', + '[netman] DNS final rc=0x0000000000000000', + '[netman] network final rc=0x0000000000000000' + ) -MustNotContain @( + '[PANIC]', + '[netman] DHCP: no OFFER received', + '[netman] ARP: timeout', + '[netman] DNS: timeout' + ) + + if ($hit -eq '[netman] network final rc=') { + Add-TestCheck $result 'N-A: netman DHCP+ARP+DNS 全链路 rc=0' 'PASS' + } else { + Add-TestCheck $result 'N-A: netman DHCP+ARP+DNS 全链路 rc=0' 'FAIL' "hit=$hit" + } + Add-TestNote $result 'N-A: netman 仅首启调度;后续 FirstInit 向导自动化由 boot-regression.ps1 B1 覆盖,本场景断言后即结束会话' + } finally { + Stop-QemuSession $s + } +} + +# ---------------------------------------------------------------- +function Invoke-ScenarioNB { + Write-Host "`n--- N-B curl.elf 主机侧端到端 (dev_mode shell → http://10.0.2.2:8000/) ---" + Set-DeshabFirstInit -First 1 -DevMode 1 + Set-DeshabAutoexec -Lines @('ver', 'exit') + + $httpLog = Join-Path $script:TestWorkDir 'nete2e-http-requests.log' + $httpJob = Start-TestHttpServer -Port 8000 -LogPath $httpLog + $s = Start-QemuSession -Name 'nete2e-nb' -MonitorPort 45506 + try { + $hit = Wait-QemuLog -Session $s -Patterns @('=== 自动测试完成 ===', '[PANIC]') -TimeoutSeconds 240 + if ($hit -ne '=== 自动测试完成 ===') { + $result.LogPath = $s.LogPath + Add-TestCheck $result 'N-B: shell dev_tests 完成' 'FAIL' "hit=$hit" + return + } + Add-TestCheck $result 'N-B: shell dev_tests 完成' 'PASS' + + # --- 启动 curl 工具(PATH → /bin/CURL.ELF) --- + Start-Sleep -Seconds 1 + Send-QemuText -Session $s -Text 'curl' -Enter + $h = Wait-QemuLog -Session $s -Patterns @('[curl] boot') -TimeoutSeconds 60 + if (-not $h) { Add-TestCheck $result 'N-B: curl.elf PATH 加载启动' 'FAIL' 'missing [curl] boot'; return } + Add-TestCheck $result 'N-B: curl.elf PATH 加载启动' 'PASS' + + # --- 输入 URL:经 slirp 网关 10.0.2.2 访问主机 127.0.0.1:8000 --- + Start-Sleep -Milliseconds 800 + Send-QemuText -Session $s -Text 'http://10.0.2.2:8000/' -Enter -DelayMs 110 + + # 主机侧端到端证据:HTTP 服务器收到 guest 的 GET 请求 + $deadline = (Get-Date).AddSeconds(45) + $hostGot = $false + while ((Get-Date) -lt $deadline) { + if ((Test-Path $httpLog) -and ((Get-Content $httpLog -Raw -ErrorAction SilentlyContinue) -match 'GET / HTTP/1\.0')) { + $hostGot = $true; break + } + Start-Sleep -Milliseconds 800 + } + if ($hostGot) { + Add-TestCheck $result 'N-B: 主机 HTTP 服务器收到 guest GET /(TCP+IP+ETH 全链路)' 'PASS' + } else { + Add-TestCheck $result 'N-B: 主机 HTTP 服务器收到 guest GET /(TCP+IP+ETH 全链路)' 'FAIL' 'no request logged by host http server within 45s' + } + + # --- Esc 退出 curl 返回 shell --- + Start-Sleep -Seconds 2 + Send-QemuKeys -Session $s -Keys @('esc') + $h2 = Wait-QemuLog -Session $s -Patterns @('[curl] exit') -TimeoutSeconds 30 + $logc = Read-QemuLogText $s.LogPath + if ($h2 -and $logc.Contains('[shell] path: tool returned')) { + Add-TestCheck $result 'N-B: curl Esc 退出返回 shell' 'PASS' + } else { + Add-TestCheck $result 'N-B: curl Esc 退出返回 shell' 'FAIL' 'missing [curl] exit / tool returned' + } + + # --- Esc 回 DSK → cmd → desktop --- + Send-QemuKeys -Session $s -Keys @('esc') + $h3 = Wait-QemuLog -Session $s -Patterns @('desktop ready') -TimeoutSeconds 150 + $log = Read-QemuLogText $s.LogPath + $result.LogPath = $s.LogPath + Assert-QemuLog $result $log -MustNotContain @('[PANIC]') + if ($h3) { Add-TestCheck $result 'N-B: Esc → cmd → desktop ready' 'PASS' } + else { Add-TestCheck $result 'N-B: Esc → cmd → desktop ready' 'FAIL' 'no desktop ready' } + + Add-TestCheck $result 'N-B: HTTP 200 响应体与 DESHAB-NET-E2E-MARKER 渲染' 'MANUAL' 'curl 响应输出仅帧缓冲(无串口锚点);主机服务器固定回复 marker 页面,可 screendump 人工核对' + Add-TestNote $result 'N6/N11 ping 链路:由 userapps-matrix.ps1 交互阶段覆盖(ping 工具 10.0.2.2 四报文)' + Add-TestNote $result 'N7/N9 外网访问:依赖主机联网,不在自动化断言范围(文档标 🔶)' + } finally { + Stop-QemuSession $s + Stop-TestHttpServer $httpJob + } +} + +# ---------------------------------------------------------------- +try { + foreach ($sc in $Scenarios) { + switch ($sc.ToUpper()) { + 'NA' { Invoke-ScenarioNA } + 'NB' { Invoke-ScenarioNB } + default { Add-TestNote $result "未知场景 $sc,跳过" } + } + } +} finally { + Restore-DeshabScenario +} + +Write-TestCaseResult $result +if ($result.Status -eq 'FAIL') { exit 1 } else { exit 0 } diff --git a/tests/qemu/pe-compat.ps1 b/tests/qemu/pe-compat.ps1 index b939587..1e07055 100644 --- a/tests/qemu/pe-compat.ps1 +++ b/tests/qemu/pe-compat.ps1 @@ -1,15 +1,23 @@ -#requires -Version 5.1 +#requires -Version 5.1 <# .SYNOPSIS pe-compat — PE/EXE 兼容层测试(docs/RE/test-cases/pe-compat.md) .DESCRIPTION - 正常启动到 cmd.elf,用 AUTOEXEC.BAT 自动执行 PE 命令序列: + 正常启动到 cmd.elf,用 AUTOEXEC.BAT 自动执行 PE 命令序列。 + 因两个已登记系统 bug(详见 docs/RE/Bug记录.txt),拆三个独立会话: - pe HELLO64.EXE → PE32+ 原生 Ring0 执行,shim_WriteFile 输出串口 - pe HELLO32.EXE → PE32 x86emu32 解释执行,shim32 输出串口 - pe NONEXIST.EXE → 错误路径:不产生 [cmd] PE run begin - exit → 返回 DSK → desktop + 会话 A (pe64):pe HELLO64.EXE → PE32+ 原生 Ring0 执行,shim_WriteFile 输出串口。 + BUG-20260801-006 已修复(setjmp 下沉 pe_service_run 本体): + ExitProcess longjmp 干净返回,"[cmd] PE run end" 稳定出现。 + 会话 B (pe32):pe HELLO32.EXE → PE32 x86emu32 解释执行。 + BUG-20260801-007 已修复(gp() guest→host 指针转换 + + fn64 调用 typedef 显式 ms_abi):"Hello from PE32!" 稳定输出。 + 会话 D (fault64):pe FAULT64.EXE → BUG-20260801-005 验收:DSK 阶段故意 + #PF 必须打出 [IDT] exception 完整现场(vector=0xe, err=0x2, + cr2=0x40000000000)。修复前 halt_all 静默吞异常零日志。 + 会话 C (peerr):pe NONEXIST.EXE + exit → 错误路径:不产生 [cmd] PE run begin; + exit 正常返回 DSK("[DSK] cmd.elf returned")。 串口可断言锚点: [cmd] PE run begin / [cmd] cmdline: ... / [cmd] PE run end (logl/swrite, 始终上串口) @@ -17,66 +25,183 @@ [PE] running PE32 via x86emu32 (pe_service) Hello from PE32+! / Hello from PE32! (shim WriteFile → 串口) + 注意:cmd_pe 传 full_cmdline=args(不含 "pe " 前缀), + 所以串口回显为 "[cmd] cmdline: HELLO64.EXE" 而不是 "pe HELLO64.EXE"。 + + WHPX 已知问题:pe_service 为原生 PE32+ 执行写 CR4 启用 SSE 后, + WHPX 可能以 "Unexpected VP exit code 4" 提前终止 QEMU(WHPX 平台限制, + 非 guest 崩溃)。任一会话在 WHPX 下未命中锚点时自动以 -Accel tcg 重跑 + (可用 -Accel tcg 直接指定跳过 WHPX)。 + peinfo / 无参用法提示只写帧缓冲(cmd 非 dev_mode 无串口镜像),标 MANUAL。 退出码:全部通过 0,任一 FAIL 1。 #> -param([switch]$Build) +param( + [switch]$Build, + [string]$Accel = 'whpx' +) $ErrorActionPreference = 'Stop' . (Join-Path $PSScriptRoot 'lib\QemuTest.ps1') Initialize-QemuTest -Build:$Build $result = New-TestCaseResult 'pe-compat' -Set-DeshabFirstInit -First 1 -DevMode 0 -Set-DeshabAutoexec -Lines @('pe HELLO64.EXE', 'pe HELLO32.EXE', 'pe NONEXIST.EXE', 'exit') +# ---------------------------------------------------------------- +# 单会话执行一轮 AUTOEXEC:正常启动 → cmd → 命令序列 → 等锚点/会话结束 +# 填充 $script:RunLog / RunLogPath;返回命中的锚点或 $null。 +# ---------------------------------------------------------------- +$script:RunLog = '' +$script:RunLogPath = '' +function Invoke-PeSession { + param( + [Parameter(Mandatory)][string]$Name, + [Parameter(Mandatory)][string]$AccelMode, + [Parameter(Mandatory)][string[]]$Autoexec, + [Parameter(Mandatory)][string[]]$Anchors + ) + Set-DeshabFirstInit -First 1 -DevMode 0 + Set-DeshabAutoexec -Lines $Autoexec -$s = Start-QemuSession -Name 'pe-compat' + $timeout = 150 + if ($AccelMode -eq 'tcg') { $timeout = 360 } # TCG 全模拟显著更慢 + $s = Start-QemuSession -Name "$Name-$AccelMode" -Accel $AccelMode -MonitorPort 45504 + try { + $hit = Wait-QemuLog -Session $s -Patterns $Anchors -TimeoutSeconds $timeout + Start-Sleep -Seconds 2 + $script:RunLog = Read-QemuLogText $s.LogPath + $script:RunLogPath = $s.LogPath + return $hit + } finally { + Stop-QemuSession $s + } +} + +# WHPX 未命中锚点 → TCG 兜底重跑一轮 +function Invoke-PeSessionWithFallback { + param( + [Parameter(Mandatory)][string]$Name, + [Parameter(Mandatory)][string[]]$Autoexec, + [Parameter(Mandatory)][string[]]$Anchors + ) + $hit = Invoke-PeSession -Name $Name -AccelMode $Accel -Autoexec $Autoexec -Anchors $Anchors + if (-not $hit -and $Accel -eq 'whpx') { + $why = 'WHPX 下未命中锚点' + if ($script:RunLog.Contains('WHPX: Unexpected VP exit')) { $why = 'WHPX 在 PE 执行处终止 QEMU(Unexpected VP exit,WHPX 平台限制)' } + Add-TestNote $result "${Name}: $why,自动以 TCG 重跑" + $script:UsedAccel = 'tcg' + $hit = Invoke-PeSession -Name $Name -AccelMode 'tcg' -Autoexec $Autoexec -Anchors $Anchors + } + return $hit +} + +$script:UsedAccel = $Accel try { - $hit = Wait-QemuLog -Session $s -Patterns @('[DSK] cmd.elf returned') -TimeoutSeconds 180 - Start-Sleep -Seconds 2 - $log = Read-QemuLogText $s.LogPath - $result.LogPath = $s.LogPath + # ================================================================ + # 会话 A:B2.1 PE32+ 原生加载执行(BUG-006 已修复,ExitProcess 干净返航) + # ================================================================ + $hitA = Invoke-PeSessionWithFallback -Name 'pe64' -Autoexec @('pe HELLO64.EXE') -Anchors @('Hello from PE32+!', '[PANIC]') + $logA = $script:RunLog + $result.LogPath = $script:RunLogPath - # ---- B2.1 PE32+ 加载执行 ---- - Assert-QemuLog $result $log -Ordered @( + Assert-QemuLog $result $logA -Ordered @( '[cmd] running AUTOEXEC.BAT', '[cmd] PE run begin', - '[cmd] cmdline: pe HELLO64.EXE', + '[cmd] cmdline: HELLO64.EXE', + '[PE] PE32+ (64-bit)', '[PE] running PE32+ natively', - 'Hello from PE32+!', - '[cmd] PE run end' + 'Hello from PE32+!' + ) -MustNotContain @('[PANIC]') + + # ---- ABI 验证:cmd 启动时 ctx->reserved[4] + PE_SERVICE_MAGIC 校验 ---- + Assert-QemuLog $result $logA -MustContain @('[cmd] PE service ok') + + # ---- B2.6 ExitProcess 返回链:BUG-006 已修复,转硬断言(回归守护) ---- + if ($logA.Contains('[cmd] PE run end')) { + Add-TestCheck $result 'B2.6: ExitProcess 后 cmd 继续(PE32+ 返回链)' 'PASS' + } else { + Add-TestCheck $result 'B2.6: ExitProcess 后 cmd 继续(PE32+ 返回链)' 'FAIL' 'missing [cmd] PE run end(BUG-20260801-006 回归?)' + } + + # ================================================================ + # 会话 B:B2.2 PE32 x86emu32 解释执行(BUG-007 已修复,shim 指针转换) + # ================================================================ + $hitB = Invoke-PeSessionWithFallback -Name 'pe32' -Autoexec @('pe HELLO32.EXE') -Anchors @('Hello from PE32!', '[PANIC]', '[PE32] emulator error') + $logB = $script:RunLog + + Assert-QemuLog $result $logB -Ordered @( + '[cmd] PE run begin', + '[cmd] cmdline: HELLO32.EXE', + '[PE] running PE32 via x86emu32' + ) -MustNotContain @( + '[PANIC]', + '[PE32] emulator error', + '[PE32] emu init failed', + '[PE] unimplemented import', + '[PE32] unimplemented import', + '[PE] unimplemented API called' ) - # ---- B2.2 PE32 解释器执行 ---- - Assert-QemuLog $result $log -Ordered @( - 'Hello from PE32+!', - '[cmd] cmdline: pe HELLO32.EXE', - '[PE] running PE32 via x86emu32', - 'Hello from PE32!', - '[cmd] PE run end' + # ---- B2.2 shim 输出:BUG-007 已修复,转硬断言(回归守护) ---- + if ($logB.Contains('Hello from PE32!')) { + Add-TestCheck $result 'B2.2: PE32 解释执行 shim 输出' 'PASS' + } else { + Add-TestCheck $result 'B2.2: PE32 解释执行 shim 输出' 'FAIL' 'missing Hello from PE32!(BUG-20260801-007 回归?)' + } + + # ================================================================ + # 会话 D:BUG-20260801-005 验收 — DSK 阶段故意 #PF 必须打出完整异常现场 + # FAULT64.EXE: movabs rax,0x400000000000 ; mov [rax],1 ; jmp $ + # 预期 vector=0xe / err=0x2(supervisor write non-present) / + # cr2=0x0000400000000000;修复前 halt_all 静默吞掉零日志。 + # 异常后 idt_handler 安全停机(hlt 循环),QEMU 存活 → 锚点命中即返回。 + # 若异常投递失效则三重故障 → -no-reboot QEMU 退出 → hit=$null → FAIL。 + # ================================================================ + $hitD = Invoke-PeSessionWithFallback -Name 'fault64' -Autoexec @('pe FAULT64.EXE') -Anchors @('[IDT] exception', '[PANIC]') + $logD = $script:RunLog + + if (-not $hitD) { + Add-TestCheck $result 'BUG-005: DSK 阶段 #PF 异常投递' 'FAIL' 'session exited without [IDT] exception(异常被吞/三重故障)' + } else { + Add-TestCheck $result 'BUG-005: DSK 阶段 #PF 异常投递' 'PASS' + } + Assert-QemuLog $result $logD -Ordered @( + '[cmd] cmdline: FAULT64.EXE', + '[PE] running PE32+ natively', + '[IDT] exception', + '[IDT] vector=0x000000000000000e', + '[IDT] err=0x0000000000000002', + '[IDT] cr2=0x0000400000000000' ) - # ---- B2.4 文件不存在错误路径:NONEXIST 不得产生 PE run begin ---- - Assert-QemuLog $result $log -MustNotContain @('[cmd] cmdline: pe NONEXIST.EXE') - # 恰好两次 PE 执行(HELLO64 + HELLO32) - Assert-QemuLog $result $log -CountPattern '[cmd] PE run begin' -MinCount 2 + # ================================================================ + # 会话 C:B2.4 文件不存在错误路径 + cmd exit 正常返回 DSK + # ================================================================ + $hitC = Invoke-PeSessionWithFallback -Name 'peerr' -Autoexec @('pe NONEXIST.EXE', 'exit') -Anchors @('[DSK] cmd.elf returned', '[PANIC]') + $logC = $script:RunLog - # ---- B2.6 ExitProcess 返回:cmd 接管继续执行后续命令并最终返回 DSK ---- - if ($hit) { Add-TestCheck $result 'B2.6: ExitProcess 后 cmd 返回 DSK' 'PASS' } - else { Add-TestCheck $result 'B2.6: ExitProcess 后 cmd 返回 DSK' 'FAIL' 'missing [DSK] cmd.elf returned' } + # NONEXIST 不得进入 PE 执行路径 + if ($logC.Contains('[cmd] cmdline: NONEXIST.EXE') -or $logC.Contains('[cmd] PE run begin')) { + Add-TestCheck $result 'B2.4: NONEXIST.EXE 不进入 PE 执行路径' 'FAIL' 'unexpected PE run begin / cmdline echo' + } else { + Add-TestCheck $result 'B2.4: NONEXIST.EXE 不进入 PE 执行路径' 'PASS' + } + Assert-QemuLog $result $logC -MustNotContain @('[PANIC]') - # ---- ABI 验证(源码级,日志侧证) ---- - Assert-QemuLog $result $log -MustContain @('[cmd] PE service ok') # ctx->reserved[4] + PE_SERVICE_MAGIC 校验通过 + if ($hitC -eq '[DSK] cmd.elf returned') { + Add-TestCheck $result '链路: cmd exit 正常返回 DSK' 'PASS' + } else { + Add-TestCheck $result '链路: cmd exit 正常返回 DSK' 'FAIL' "hit=$hitC(missing [DSK] cmd.elf returned)" + } + + Add-TestNote $result "加速方案: $script:UsedAccel" + Add-TestNote $result 'BUG-006/007 已修复,返回链断言已转硬断言;会话仍按故障域拆分(单会话全序列合并留作后续优化)' # ---- 帧缓冲限定项(无串口输出,标 MANUAL) ---- Add-TestCheck $result 'B2.3: peinfo HELLO64.EXE 头解析' 'MANUAL' 'peinfo 输出仅帧缓冲(cmd 非 dev_mode 无串口镜像),手动验证 Machine: 0x8664' Add-TestCheck $result 'B2.7: peinfo HELLO32.EXE 头解析' 'MANUAL' '同上,预期 Machine: 0x14c / image_base=0x400000' Add-TestCheck $result 'B2.5: pe 无参用法提示' 'MANUAL' '用法提示仅帧缓冲输出' - - Assert-QemuLog $result $log -MustNotContain @('[PANIC]') } finally { - Stop-QemuSession $s Restore-DeshabScenario } diff --git a/tests/qemu/performance-baseline.ps1 b/tests/qemu/performance-baseline.ps1 new file mode 100644 index 0000000..06e34e4 --- /dev/null +++ b/tests/qemu/performance-baseline.ps1 @@ -0,0 +1,139 @@ +#requires -Version 5.1 +<# +.SYNOPSIS + performance-baseline — 启动性能基线(docs/RE/test-cases/performance-baseline.md) + +.DESCRIPTION + 正常启动场景(firstInit="1\n0", AUTOEXEC=ver/exit),按墙上时钟记录 + 关键阶段锚点在串口日志中首次出现的时刻,计算阶段时间差: + + t0 QEMU 进程启动(含 OVMF + Limine bootloader) + [UTSM] boot UTSM 内核入口 + [UTSM] SELFTEST PASS + UTSM 自检完成(位于 4 stage 共 16 驱动装载之后, + 即"驱动加载完"时刻) + [DSK] boot DSK 主内核接管 + desktop ready 桌面就绪(login skipped → cmd(AUTOEXEC) → desktop) + + 输出 .build_tmp\perf_baseline.json(锚点绝对时刻 + 阶段毫秒数 + 环境信息)。 + 本用例只测量展示、不做阈值断言(文档标 P2,QEMU 数值仅作回归参考)。 + 多次测量取中位数:默认 3 轮,可用 -Runs 调整。 + + 退出码:测量完成 0;会话/锚点异常 1。 +#> +param( + [switch]$Build, + [int]$Runs = 3 +) +$ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'lib\QemuTest.ps1') + +Initialize-QemuTest -Build:$Build +$result = New-TestCaseResult 'performance-baseline' + +# 阶段锚点定义(顺序即启动顺序) +$anchorDefs = @( + @{ Key = 'utsm_boot'; Pattern = '[UTSM] boot' }, + @{ Key = 'utsm_selftest'; Pattern = '[UTSM] SELFTEST PASS' }, + @{ Key = 'dsk_boot'; Pattern = '[DSK] boot' }, + @{ Key = 'desktop_ready'; Pattern = 'desktop ready' } +) + +# 轮询串口日志,记录每个锚点首次出现的墙上时刻;返回 hashtable 或 $null +function Measure-BootPhases { + param([Parameter(Mandatory)]$Session, [int]$TimeoutSeconds = 240) + $marks = @{} + foreach ($a in $anchorDefs) { $marks[$a.Key] = $null } + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + $remaining = $anchorDefs.Count + while ((Get-Date) -lt $deadline -and $remaining -gt 0) { + $text = Read-QemuLogText $Session.LogPath + foreach ($a in $anchorDefs) { + if ($null -eq $marks[$a.Key] -and $text.Contains($a.Pattern)) { + $marks[$a.Key] = Get-Date + $remaining-- + } + } + if (Test-QemuSessionExited $Session) { break } + Start-Sleep -Milliseconds 200 + } + return $marks +} + +$allRuns = New-Object System.Collections.ArrayList +try { + for ($run = 1; $run -le $Runs; $run++) { + Write-Host "`n--- perf run $run/$Runs (正常启动 → desktop ready) ---" + Set-DeshabFirstInit -First 1 -DevMode 0 + Set-DeshabAutoexec -Lines @('ver', 'exit') + + $t0 = Get-Date + $s = Start-QemuSession -Name "perf-run$run" -MonitorPort 45507 + try { + $marks = Measure-BootPhases -Session $s -TimeoutSeconds 240 + $result.LogPath = $s.LogPath + } finally { + Stop-QemuSession $s + } + + if ($null -eq $marks['desktop_ready']) { + Add-TestCheck $result "run${run}: 锚点采集完整" 'FAIL' 'desktop ready not observed within timeout' + continue + } + Add-TestCheck $result "run${run}: 锚点采集完整" 'PASS' + + $ms = [ordered]@{ + ovmf_bootloader_ms = [int]($marks['utsm_boot'] - $t0).TotalMilliseconds + utsm_drivers_ms = [int]($marks['utsm_selftest'] - $marks['utsm_boot']).TotalMilliseconds + selftest_to_dsk_ms = [int]($marks['dsk_boot'] - $marks['utsm_selftest']).TotalMilliseconds + dsk_to_desktop_ms = [int]($marks['desktop_ready'] - $marks['dsk_boot']).TotalMilliseconds + total_boot_ms = [int]($marks['desktop_ready'] - $t0).TotalMilliseconds + } + [void]$allRuns.Add([pscustomobject]@{ + run = $run + started_at = $t0.ToString('yyyy-MM-dd HH:mm:ss') + metrics = $ms + }) + Write-Host (" ovmf={0}ms utsm+drivers={1}ms dsk_load={2}ms dsk->desktop={3}ms total={4}ms" -f ` + $ms.ovmf_bootloader_ms, $ms.utsm_drivers_ms, $ms.selftest_to_dsk_ms, $ms.dsk_to_desktop_ms, $ms.total_boot_ms) + } +} finally { + Restore-DeshabScenario +} + +# ---- 中位数汇总 + JSON 输出 ---- +if ($allRuns.Count -gt 0) { + $median = [ordered]@{} + foreach ($k in @('ovmf_bootloader_ms', 'utsm_drivers_ms', 'selftest_to_dsk_ms', 'dsk_to_desktop_ms', 'total_boot_ms')) { + $vals = @($allRuns | ForEach-Object { [int]$_.metrics[$k] } | Sort-Object) + $median[$k] = $vals[[int]($vals.Count / 2)] + } + + $out = [ordered]@{ + generated_at = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') + method = 'host wall-clock polling of serial log anchors (200ms poll, +/-200ms 精度)' + environment = [ordered]@{ + accel = 'whpx' + cpu = 'qemu64,-vmx' + machine = 'q35' + memory = '2G' + scenario = 'firstInit=1 dev_mode=0 AUTOEXEC=ver,exit -> desktop ready' + snapshot = $true + } + median_ms = $median + runs = $allRuns + } + $jsonPath = Join-Path $script:BuildTmp 'perf_baseline.json' + ($out | ConvertTo-Json -Depth 6) | Out-File -Encoding utf8 $jsonPath + Add-TestNote $result "基线 JSON: $jsonPath" + + Add-TestCheck $result ("P1: UTSM 启动+驱动装载 (utsm_boot→SELFTEST) = {0} ms (目标<500ms QEMU, 仅展示)" -f $median.utsm_drivers_ms) 'PASS' + Add-TestCheck $result ("DSK 加载 (SELFTEST→[DSK] boot) = {0} ms" -f $median.selftest_to_dsk_ms) 'PASS' + Add-TestCheck $result ("DSK→desktop ready = {0} ms" -f $median.dsk_to_desktop_ms) 'PASS' + Add-TestCheck $result ("总启动 (QEMU 启动→desktop ready) = {0} ms" -f $median.total_boot_ms) 'PASS' + Add-TestNote $result '本用例不做阈值断言;QEMU WHPX 数值仅作回归参考(文档:QEMU≠真机,固定加速方案)' + Add-TestNote $result 'P2/P3/P6(单 ELF 加载计时)、P4 ping RTT、P7 桌面帧率、P8-P11(PE/VMM/Linux)需内核 TSC 打点日志,当前串口无对应锚点,保持 PENDING(见建议锚点清单)' +} + +Write-TestCaseResult $result +if ($result.Status -eq 'FAIL') { exit 1 } else { exit 0 } diff --git a/tests/qemu/userapps-matrix.ps1 b/tests/qemu/userapps-matrix.ps1 index 7b0e0db..79a5e84 100644 --- a/tests/qemu/userapps-matrix.ps1 +++ b/tests/qemu/userapps-matrix.ps1 @@ -1,25 +1,31 @@ -#requires -Version 5.1 +#requires -Version 5.1 <# .SYNOPSIS userapps-matrix — 用户态程序功能矩阵(docs/RE/test-cases/userapps-matrix.md) .DESCRIPTION - dev_mode 单会话覆盖 shell 内建命令与工具程序(串口镜像开启,全程可断言): + dev_mode 单会话覆盖 shell 内建命令与工具程序(串口镜像开启,全程可断言)。 - 1) dev_tests 硬编码序列自动执行(ls/echo/cat/cp/mv/rm,SATA 镜像无 test.bas) - 2) 交互阶段经 monitor 按键驱动: - help / id / pci(存活验证)→ - ping (PATH 查找 /bin/PING.ELF,输入 10.0.2.2,Esc 退出) → - run EDITOR.ELF / FILEMAN.ELF / BROWSER.ELF(root 目录加载,Esc 退出)→ - Esc 回 DSK → cmd(AUTOEXEC) → desktop + 会话 A(唯一):dev_tests 硬编码序列 → help → id → pci 存活 → + ping(PATH 加载 → 10.0.2.2 四报文 → Esc 退出)→ + run EDITOR.ELF / FILEMAN.ELF / BROWSER.ELF(加载 + 按键 + Esc 退出)→ + Esc 回 DSK → cmd(AUTOEXEC) → desktop - 串口可断言锚点: - [shell] path: running /bin/ tool / [shell] path: tool returned - [shell] run: jumping to tool / [shell] run: tool returned - [ping] boot|exit [editor] boot|exit [fileman] boot|exit [browser] boot|exit + 历史背景(详见 docs/RE/Bug记录.txt,均已 FIXED,相关降级已全部回收): - 工具内部 UI 输出(编辑器内容、fileman 列表、ping RTT)仅帧缓冲,标 MANUAL。 - desktop 双击/拖动/任务栏无串口锚点,标 MANUAL。 + BUG-20260801-001 [shell] y/i/0 吞键(未跟踪 Ctrl 修饰键): + 已修复(0x1D/0x9D 跟踪 Ctrl,快捷键仅在 Ctrl 按下时生效) + → id/ping 等含 i 命令恢复自动化断言。 + + BUG-20260801-002 [shell] g_run_image 仅 256KB,editor(496KB) 拒绝加载: + 已修复(g_run_image 扩至 1MB,与 desktop 宿主对齐) + → run EDITOR.ELF 断言升级为 [editor] boot + [editor] exit。 + + BUG-20260801-003 [desktop_app.h] da_mouse_poll 吞键盘扫描码 + + BUG-20260801-008 [desktop_app.h] da_mouse_init 配置字污染: + 均已修复(先判 AUX 位再读 0x60;init 首尾 da_ps2_drain + 读全响应) + → fileman/editor/browser 键盘路径恢复,Esc 退出断言全部自动化; + 工具不再占住会话,三会合并为单会话顺序执行。 退出码:全部通过 0,任一 FAIL 1。 #> @@ -30,136 +36,191 @@ $ErrorActionPreference = 'Stop' Initialize-QemuTest -Build:$Build $result = New-TestCaseResult 'userapps-matrix' -Set-DeshabFirstInit -First 1 -DevMode 1 -Set-DeshabAutoexec -Lines @('ver', 'exit') - -$s = Start-QemuSession -Name 'userapps-matrix' -try { - # ================= 阶段 1: dev_tests 自动序列 ================= - $hit = Wait-QemuLog -Session $s -Patterns @('=== 自动测试完成 ===', '[PANIC]') -TimeoutSeconds 240 - $log = Read-QemuLogText $s.LogPath - $result.LogPath = $s.LogPath - - Assert-QemuLog $result $log -Ordered @( - '[DSK:SCHED] decision: dev_mode -> shell.elf', - '[shell] boot', - '[shell] test.bas not found, fallback to hardcoded', - '[1] ls', - '[2] echo "Deshab dev test" > TEST.TXT', - '[3] cat TEST.TXT', - '[4] cp TEST.TXT COPY.TXT', - '[6] mv COPY.TXT MOVED.TXT', - '[8] rm TEST.TXT', - '[10] ls' - ) -MustNotContain @('[PANIC]') - if ($hit -eq '=== 自动测试完成 ===') { - Add-TestCheck $result 'shell: dev_tests 完成(ls/echo/cat/cp/mv/rm)' 'PASS' - } else { - Add-TestCheck $result 'shell: dev_tests 完成(ls/echo/cat/cp/mv/rm)' 'FAIL' "hit=$hit" +# ---------------------------------------------------------------- +# monitor sendkey 在 guest 重负载(AHCI 读盘刷屏、帧缓冲重绘)下会丢扫描码。 +# 对策:每键 150ms + "发送→等锚点→重试 ≤3 次",重试前先 backspace 清行。 +# 注意:backspace(0x0E) 不在 shell Ctrl+U/W/K 快捷键扫描码列表内,清行可靠。 +# ---------------------------------------------------------------- +function Invoke-ShellCmdRetry { + param( + [Parameter(Mandatory)]$Session, + [Parameter(Mandatory)][string]$Text, + [Parameter(Mandatory)][string[]]$Anchors, + [int]$TimeoutSeconds = 20, + [int]$MaxAttempts = 3 + ) + for ($a = 1; $a -le $MaxAttempts; $a++) { + if ($a -gt 1) { + $bk = @(); for ($i = 0; $i -lt 24; $i++) { $bk += 'backspace' } + Send-QemuKeys -Session $Session -Keys $bk -DelayMs 50 + Start-Sleep -Milliseconds 400 + Add-TestNote $result "输入 '$Text' 第 $($a-1) 次未命中锚点,清行重试" + } + Send-QemuText -Session $Session -Text $Text -Enter -DelayMs 150 + $h = Wait-QemuLog -Session $Session -Patterns $Anchors -TimeoutSeconds $TimeoutSeconds + if ($h) { return $h } } + return $null +} - # ================= 阶段 2: 交互命令 ================= - if ($hit -eq '=== 自动测试完成 ===') { - # --- help --- - Start-Sleep -Seconds 1 - Send-QemuText -Session $s -Text 'help' -Enter - $h = Wait-QemuLog -Session $s -Patterns @('dsl push <') -TimeoutSeconds 30 - if ($h) { Add-TestCheck $result 'shell: help 命令列表' 'PASS' } - else { Add-TestCheck $result 'shell: help 命令列表' 'FAIL' 'missing help output' } +# Esc 退出重试:只在锚点未出现时补发,锚点出现即停(避免多余 Esc 漏进 shell/cmd) +function Send-EscRetry { + param( + [Parameter(Mandatory)]$Session, + [Parameter(Mandatory)][string[]]$Anchors, + [int]$TimeoutSeconds = 15, + [int]$MaxAttempts = 2 + ) + for ($a = 1; $a -le $MaxAttempts; $a++) { + Send-QemuKeys -Session $Session -Keys @('esc') + $h = Wait-QemuLog -Session $Session -Patterns $Anchors -TimeoutSeconds $TimeoutSeconds + if ($h) { return $h } + if ($a -lt $MaxAttempts) { Add-TestNote $result "Esc 第 $a 次未命中($($Anchors -join ' | ')),补发" } + } + return $null +} - # --- id(uid/gid 输出为 ASCII 强锚点) --- - Send-QemuText -Session $s -Text 'id' -Enter - $h = Wait-QemuLog -Session $s -Patterns @('uid=0(root) gid=0(root)') -TimeoutSeconds 30 - if ($h) { Add-TestCheck $result 'shell: id/whoami 用户查询' 'PASS' } - else { Add-TestCheck $result 'shell: id/whoami 用户查询' 'FAIL' 'missing uid=0(root)' } +try { + # ================================================================ + # 会话 A(唯一):shell 内建命令 + ping/editor/fileman/browser + # 工具链全交互 + Esc→cmd→desktop 链路 + # ================================================================ + Set-DeshabFirstInit -First 1 -DevMode 1 + Set-DeshabAutoexec -Lines @('ver', 'exit') - # --- pci(输出含中文,验证存活:pci 后 ver 仍有响应,锚点取 ver 独有行) --- - Send-QemuText -Session $s -Text 'pci' -Enter - Start-Sleep -Seconds 3 - Send-QemuText -Session $s -Text 'ver' -Enter - $h = Wait-QemuLog -Session $s -Patterns @('Architecture: x86_64') -TimeoutSeconds 30 - if ($h) { Add-TestCheck $result 'shell: pci 枚举后 shell 存活' 'PASS' } - else { Add-TestCheck $result 'shell: pci 枚举后 shell 存活' 'FAIL' 'no response after pci' } + $s = Start-QemuSession -Name 'userapps-matrix' -MonitorPort 45505 + try { + $hit = Wait-QemuLog -Session $s -Patterns @('=== 自动测试完成 ===', '[PANIC]') -TimeoutSeconds 240 + $log = Read-QemuLogText $s.LogPath + $result.LogPath = $s.LogPath - # --- ping 工具(PATH → /bin/PING.ELF) --- - Send-QemuText -Session $s -Text 'ping' -Enter - $h = Wait-QemuLog -Session $s -Patterns @('[ping] boot') -TimeoutSeconds 40 - if ($h) { - Add-TestCheck $result 'ping.elf: PATH 加载启动' 'PASS' - Start-Sleep -Milliseconds 800 - Send-QemuText -Session $s -Text '10.0.2.2' -Enter - Start-Sleep -Seconds 12 # 4 次 ICMP echo(每次超时上限 2s) - Send-QemuKeys -Session $s -Keys @('esc') - $h2 = Wait-QemuLog -Session $s -Patterns @('[ping] exit', '[shell] path: tool returned') -TimeoutSeconds 30 - $logp = Read-QemuLogText $s.LogPath - if ($h2 -and $logp.Contains('[ping] exit') -and $logp.Contains('[shell] path: tool returned')) { - Add-TestCheck $result 'ping.elf: Esc 退出返回 shell' 'PASS' - } else { - Add-TestCheck $result 'ping.elf: Esc 退出返回 shell' 'FAIL' 'missing [ping] exit / tool returned' - } - } else { - Add-TestCheck $result 'ping.elf: PATH 加载启动' 'FAIL' 'missing [ping] boot' - } - - # --- run EDITOR.ELF --- - Send-QemuText -Session $s -Text 'run EDITOR.ELF' -Enter - $h = Wait-QemuLog -Session $s -Patterns @('[editor] boot') -TimeoutSeconds 40 - if ($h) { - Add-TestCheck $result 'editor.elf: run 加载启动' 'PASS' - Start-Sleep -Seconds 2 - Send-QemuKeys -Session $s -Keys @('esc') - $h2 = Wait-QemuLog -Session $s -Patterns @('[editor] exit') -TimeoutSeconds 30 - $loge = Read-QemuLogText $s.LogPath - if ($h2 -and $loge.Contains('[shell] run: tool returned')) { - Add-TestCheck $result 'editor.elf: Esc 退出返回 shell' 'PASS' - } else { - Add-TestCheck $result 'editor.elf: Esc 退出返回 shell' 'FAIL' 'missing [editor] exit / run returned' - } - } else { - Add-TestCheck $result 'editor.elf: run 加载启动' 'FAIL' 'missing [editor] boot' - } - - # --- run FILEMAN.ELF(已知问题观察项:曾触发 QEMU 退出) --- - Send-QemuText -Session $s -Text 'run FILEMAN.ELF' -Enter - $h = Wait-QemuLog -Session $s -Patterns @('[fileman] boot') -TimeoutSeconds 40 - if ($h) { - Add-TestCheck $result 'fileman.elf: run 加载启动' 'PASS' - Start-Sleep -Seconds 2 - Send-QemuKeys -Session $s -Keys @('esc') - $h2 = Wait-QemuLog -Session $s -Patterns @('[fileman] exit') -TimeoutSeconds 30 - if ($h2) { Add-TestCheck $result 'fileman.elf: Esc 退出返回 shell(未复现 QEMU 退出)' 'PASS' } - else { Add-TestCheck $result 'fileman.elf: Esc 退出返回 shell(未复现 QEMU 退出)' 'FAIL' 'missing [fileman] exit(疑似已知问题复现)' } - } else { - Add-TestCheck $result 'fileman.elf: run 加载启动' 'FAIL' 'missing [fileman] boot(疑似已知问题复现:QEMU 退出)' - } - - # --- run BROWSER.ELF --- - Send-QemuText -Session $s -Text 'run BROWSER.ELF' -Enter - $h = Wait-QemuLog -Session $s -Patterns @('[browser] boot') -TimeoutSeconds 40 - if ($h) { - Add-TestCheck $result 'browser.elf: run 加载启动' 'PASS' - Start-Sleep -Seconds 2 - Send-QemuKeys -Session $s -Keys @('esc') - $h2 = Wait-QemuLog -Session $s -Patterns @('[browser] exit') -TimeoutSeconds 30 - if ($h2) { Add-TestCheck $result 'browser.elf: Esc 退出返回 shell' 'PASS' } - else { Add-TestCheck $result 'browser.elf: Esc 退出返回 shell' 'FAIL' 'missing [browser] exit' } - } else { - Add-TestCheck $result 'browser.elf: run 加载启动' 'FAIL' 'missing [browser] boot' - } - - # --- Esc 回 DSK → cmd → desktop --- - Send-QemuKeys -Session $s -Keys @('esc') - $h = Wait-QemuLog -Session $s -Patterns @('desktop ready') -TimeoutSeconds 150 - $logf = Read-QemuLogText $s.LogPath - Assert-QemuLog $result $logf -Ordered @( - '[shell] esc -> return to DSK', - '[DSK] shell auto-test returned' + Assert-QemuLog $result $log -Ordered @( + '[DSK:SCHED] decision: dev_mode -> shell.elf', + '[shell] boot', + '[shell] test.bas not found, fallback to hardcoded', + '[1] ls', + '[2] echo "Deshab dev test" > TEST.TXT', + '[3] cat TEST.TXT', + '[4] cp TEST.TXT COPY.TXT', + '[6] mv COPY.TXT MOVED.TXT', + '[8] rm TEST.TXT', + '[10] ls' ) -MustNotContain @('[PANIC]') - if ($h) { Add-TestCheck $result '链路: Esc → cmd → desktop ready' 'PASS' } - else { Add-TestCheck $result '链路: Esc → cmd → desktop ready' 'FAIL' 'no desktop ready' } + if ($hit -eq '=== 自动测试完成 ===') { + Add-TestCheck $result 'shell: dev_tests 完成(ls/echo/cat/cp/mv/rm)' 'PASS' + } else { + Add-TestCheck $result 'shell: dev_tests 完成(ls/echo/cat/cp/mv/rm)' 'FAIL' "hit=$hit" + } + + if ($hit -eq '=== 自动测试完成 ===') { + # --- help --- + Start-Sleep -Seconds 1 + $h = Invoke-ShellCmdRetry -Session $s -Text 'help' -Anchors @('dsl push <') -TimeoutSeconds 20 + if ($h) { Add-TestCheck $result 'shell: help 命令列表' 'PASS' } + else { Add-TestCheck $result 'shell: help 命令列表' 'FAIL' 'missing help output' } + + # --- id(BUG-001 修复回归守护:i 不再被吞,输出上串口) --- + $h = Invoke-ShellCmdRetry -Session $s -Text 'id' -Anchors @('uid=0(root) gid=0(root)') -TimeoutSeconds 20 + if ($h) { Add-TestCheck $result 'shell: id 用户查询(BUG-001 修复后 i 可键入)' 'PASS' } + else { Add-TestCheck $result 'shell: id 用户查询(BUG-001 修复后 i 可键入)' 'FAIL' 'missing uid=0(root) output' } + + # --- pci(验证存活:pci 后 ver 仍有响应,锚点取 ver 独有行) --- + Send-QemuText -Session $s -Text 'pci' -Enter -DelayMs 150 + Start-Sleep -Seconds 3 + $h = Invoke-ShellCmdRetry -Session $s -Text 'ver' -Anchors @('Architecture: x86_64') -TimeoutSeconds 20 + if ($h) { Add-TestCheck $result 'shell: pci 枚举后 shell 存活' 'PASS' } + else { Add-TestCheck $result 'shell: pci 枚举后 shell 存活' 'FAIL' 'no response after pci' } + + # --- ping:BUG-001 修复后可键入;PATH → /bin/PING.ELF → 10.0.2.2 四报文 → Esc 退出 --- + $h = Invoke-ShellCmdRetry -Session $s -Text 'ping' -Anchors @('[ping] boot') -TimeoutSeconds 30 + if ($h) { + Add-TestCheck $result 'ping.elf: PATH 加载启动([ping] boot)' 'PASS' + Start-Sleep -Milliseconds 800 + # 4 个 ICMP Echo(每报文超时上限 2s);reply/统计仅帧缓冲,串口无锚点 + Send-QemuText -Session $s -Text '10.0.2.2' -Enter -DelayMs 150 + Start-Sleep -Seconds 10 + $h2 = Send-EscRetry -Session $s -Anchors @('[ping] exit') -TimeoutSeconds 15 -MaxAttempts 2 + $logp = Read-QemuLogText $s.LogPath + if ($h2 -and $logp.Contains('[shell] path: tool returned')) { + Add-TestCheck $result 'ping.elf: Esc 退出返回 shell' 'PASS' + } else { + Add-TestCheck $result 'ping.elf: Esc 退出返回 shell' 'FAIL' 'missing [ping] exit / path: tool returned' + } + } else { + Add-TestCheck $result 'ping.elf: PATH 加载启动([ping] boot)' 'FAIL' 'missing [ping] boot' + } + + # --- editor:BUG-002 已修复(g_run_image 扩至 1MB),run 加载 → 按键插入 → Esc 退出 --- + $h = Invoke-ShellCmdRetry -Session $s -Text 'run EDITOR.ELF' -Anchors @('[editor] boot') -TimeoutSeconds 30 + if ($h) { + Add-TestCheck $result 'editor.elf: run 加载启动([editor] boot,BUG-002 修复后行为)' 'PASS' + Start-Sleep -Seconds 1 + Send-QemuText -Session $s -Text 'abc' -DelayMs 150 # 编辑模式按键插入(驱动键盘路径,无串口锚点) + $h2 = Send-EscRetry -Session $s -Anchors @('[editor] exit') -TimeoutSeconds 15 -MaxAttempts 2 + $loge = Read-QemuLogText $s.LogPath + if ($h2 -and $loge.Contains('[shell] run: tool returned')) { + Add-TestCheck $result 'editor.elf: Esc 退出返回 shell([editor] exit)' 'PASS' + } else { + Add-TestCheck $result 'editor.elf: Esc 退出返回 shell([editor] exit)' 'FAIL' 'missing [editor] exit / run: tool returned' + } + } else { + Add-TestCheck $result 'editor.elf: run 加载启动([editor] boot,BUG-002 修复后行为)' 'FAIL' 'missing [editor] boot' + } + + # --- fileman:BUG-003/008 修复后键盘可用;方向键移动选区 + Esc 退出 --- + $h = Invoke-ShellCmdRetry -Session $s -Text 'run FILEMAN.ELF' -Anchors @('[fileman] boot') -TimeoutSeconds 30 + if ($h) { + Add-TestCheck $result 'fileman.elf: run 加载启动(未复现 QEMU 退出)' 'PASS' + Start-Sleep -Seconds 2 # 等 da_mouse_init + 目录加载 + 首帧渲染 + Send-QemuKeys -Session $s -Keys @('down', 'down', 'up') -DelayMs 200 # 选区移动(仅帧缓冲,驱动键盘路径) + $h2 = Send-EscRetry -Session $s -Anchors @('[fileman] exit') -TimeoutSeconds 15 -MaxAttempts 2 + $logfm = Read-QemuLogText $s.LogPath + if ($h2 -and $logfm.Contains('[shell] run: tool returned')) { + Add-TestCheck $result 'fileman.elf: 方向键 + Esc 退出返回 shell(BUG-003/008 修复后键盘路径)' 'PASS' + } else { + Add-TestCheck $result 'fileman.elf: 方向键 + Esc 退出返回 shell(BUG-003/008 修复后键盘路径)' 'FAIL' 'missing [fileman] exit / run: tool returned' + } + } else { + Add-TestCheck $result 'fileman.elf: run 加载启动(未复现 QEMU 退出)' 'FAIL' 'missing [fileman] boot' + } + + # --- browser:启动 + Esc 退出 --- + $h = Invoke-ShellCmdRetry -Session $s -Text 'run BROWSER.ELF' -Anchors @('[browser] boot') -TimeoutSeconds 30 + if ($h) { + Add-TestCheck $result 'browser.elf: run 加载启动' 'PASS' + Start-Sleep -Seconds 2 + $h2 = Send-EscRetry -Session $s -Anchors @('[browser] exit') -TimeoutSeconds 15 -MaxAttempts 2 + $logbr = Read-QemuLogText $s.LogPath + if ($h2 -and $logbr.Contains('[shell] run: tool returned')) { + Add-TestCheck $result 'browser.elf: Esc 退出返回 shell([browser] exit)' 'PASS' + } else { + Add-TestCheck $result 'browser.elf: Esc 退出返回 shell([browser] exit)' 'FAIL' 'missing [browser] exit / run: tool returned' + } + } else { + Add-TestCheck $result 'browser.elf: run 加载启动' 'FAIL' 'missing [browser] boot' + } + + # --- Esc 回 DSK → cmd(AUTOEXEC) → desktop --- + # 只重试到中间锚点 '[shell] esc -> return to DSK' 出现即停, + # 避免多余 Esc 漏进 cmd/desktop;desktop ready 纯被动等待。 + $h = Send-EscRetry -Session $s -Anchors @('[shell] esc -> return to DSK') -TimeoutSeconds 20 -MaxAttempts 2 + $hitDesk = Wait-QemuLog -Session $s -Patterns @('desktop ready') -TimeoutSeconds 150 + $logf = Read-QemuLogText $s.LogPath + Assert-QemuLog $result $logf -Ordered @( + '[shell] esc -> return to DSK', + '[DSK] shell auto-test returned' + ) -MustNotContain @('[PANIC]') + if ($hitDesk) { Add-TestCheck $result '链路: Esc → cmd → desktop ready' 'PASS' } + else { Add-TestCheck $result '链路: Esc → cmd → desktop ready' 'FAIL' 'no desktop ready' } + } + } finally { + Stop-QemuSession $s } # ================= MANUAL / 引用项 ================= + Add-TestCheck $result 'ping.elf: ICMP reply/RTT 与四报文统计' 'MANUAL' 'reply/统计输出仅帧缓冲(无串口锚点);10.0.2.2 输入与 [ping] boot/exit 链已自动化' + Add-TestCheck $result 'editor.elf: 编辑内容渲染与保存' 'MANUAL' '编辑区/保存结果仅帧缓冲与磁盘(无串口锚点);boot/按键插入/exit 链已自动化' + Add-TestCheck $result 'fileman 浏览内容(列表渲染/进入子目录)' 'MANUAL' '列表渲染仅帧缓冲(无串口锚点);方向键/Esc 键盘路径已自动化' Add-TestCheck $result 'FirstInit 向导(程序 1)' 'MANUAL' '由 boot-regression.ps1 B1 自动化覆盖(monitor 按键填表)' Add-TestCheck $result 'login(程序 2)' 'SKIP' 'FUCK [dsk] skip_login=1 跳过;USER.CONF 解密路径需人工验证' Add-TestCheck $result 'desktop 双击/窗口拖动/任务栏(程序 3)' 'MANUAL' '无串口锚点,需人工或 screendump 验证' @@ -167,9 +228,7 @@ try { Add-TestCheck $result 'netman(程序 6)' 'MANUAL' '由 network-e2e.ps1 场景 N-A 自动化覆盖(netman 仅首启调度)' Add-TestCheck $result 'mouseInit(程序 7)' 'MANUAL' '由 boot-regression.ps1 B1 首启链路覆盖' Add-TestCheck $result 'curl.elf HTTP(程序 11)' 'MANUAL' '由 network-e2e.ps1 场景 N-B 自动化覆盖(主机侧 HTTP 服务器断言)' - Add-TestCheck $result 'editor 保存/fileman 浏览/ping RTT' 'MANUAL' '工具内部输出仅帧缓冲,无串口锚点' } finally { - Stop-QemuSession $s Restore-DeshabScenario } diff --git a/tests/qemu/x86emu32-coverage.ps1 b/tests/qemu/x86emu32-coverage.ps1 new file mode 100644 index 0000000..f79bebb --- /dev/null +++ b/tests/qemu/x86emu32-coverage.ps1 @@ -0,0 +1,169 @@ +#requires -Version 5.1 +<# +.SYNOPSIS + x86emu32-coverage — x86emu32 解释器覆盖率(docs/RE/test-cases/x86emu32-coverage.md) + +.DESCRIPTION + 两层验证: + + 1) 静态枚举(源码侧): + 解析 CODE/UTSM/pe/x86emu32.c,统计 switch case 0xNN 标签数 + (= 已实现操作码处理分支),并校验 hello32.exe 头 + (machine=0x14C, optional magic=0x10B, 确为 PE32 目标样本)。 + + 2) 动态执行(QEMU 侧): + 正常启动 firstInit="1\n0" → cmd.elf,AUTOEXEC.BAT 自动执行: + pe HELLO32.EXE → PE32 走 x86emu32 解释器 + exit → 返回 DSK → desktop + 断言锚点(BUG-20260801-007 已修复,全链路硬断言): + [cmd] PE run begin / [cmd] cmdline: HELLO32.EXE + [PE] running PE32 via x86emu32 (解释器接管证据) + Hello from PE32! (解释执行 + shim IAT 拦截证据) + [cmd] PE run end / [DSK] cmd.elf returned + 已知系统侧阻塞(2026-08-04 monitor 取证定案):末锚点当前必缺失—— + cmd run_autoexec 共享 f32_data 缓冲被首行 pe 文件加载覆盖,exit 行 + 丢失,cmd 停留交互主循环(无异常、无三重故障,guest 存活轮询键盘)。 + 解释器与 shim 链路本身无恙;待 cmd 侧修复后末锚点自动转绿。 + 注意:[PE] PE32 (32-bit) 仅 pe_load_image(PE32+ 原生路径)输出, + 解释器路径(pe32_load_into)不打印该行,不可作锚点。 + 禁止锚点: + [PE32] emulator error / [PE] unimplemented import / + [PE] unimplemented API called / [PANIC] + (出现 unimplemented 即说明 hello32 触到未实现指令/导入,覆盖率倒退) + + 历史:BUG-20260801-007(shim fn32 全缺,fn64 回退把 guest 指针 + 0x401017 当 host 指针解引用 → #PF → 三重故障)曾截断于首个 shim + 调用;修复后全链路可断言。故障在 host 侧 shim 而非解释器: + 0x68 push / FF /2 call [disp32] / IAT 合成地址拦截链路均正确, + 故"无 emulator error"仍为有效覆盖证据。 + + 指令级逐条覆盖(文档矩阵 test_arith/test_logic/test_control/test_string) + 依赖尚未存在的分类测试 PE,标 MANUAL。 + + WHPX 已知问题:guest 在 PE 兼容层执行阶段可能触发 + "Unexpected VP exit code 4" 直接终止 QEMU(WHPX 平台限制)。 + 检测到该特征时自动以 -Accel tcg 重跑一轮(可用 -Accel tcg 直接指定)。 + + 退出码:全部通过 0,任一 FAIL 1。 +#> +param( + [switch]$Build, + [string]$Accel = 'whpx' +) +$ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'lib\QemuTest.ps1') + +Initialize-QemuTest -Build:$Build +$result = New-TestCaseResult 'x86emu32-coverage' + +# ================= 静态枚举 ================= +$emuSrc = Join-Path $script:RepoRoot 'CODE\UTSM\pe\x86emu32.c' +$src = Get-Content $emuSrc -Raw +$caseCount = ([regex]::Matches($src, 'case\s+0x[0-9A-Fa-f]+')).Count +Add-TestCheck $result "静态: x86emu32.c 已实现操作码分支 case 0xNN × $caseCount" 'PASS' '仅枚举计数,供覆盖率分母参考' +Add-TestNote $result "覆盖率分母: x86emu32.c 共 $caseCount 个 case 0xNN 操作码分支(含 0F 扩展页)" + +$h32 = [System.IO.File]::ReadAllBytes((Join-Path $script:SystemDir 'bin\hello32.exe')) +$peOff = [BitConverter]::ToInt32($h32, 0x3C) +$mach = [BitConverter]::ToUInt16($h32, $peOff + 4) +$optm = [BitConverter]::ToUInt16($h32, $peOff + 24) +if ($mach -eq 0x14C -and $optm -eq 0x10B) { + Add-TestCheck $result '静态: hello32.exe 为 PE32 (machine=0x14C, magic=0x10B)' 'PASS' +} else { + Add-TestCheck $result '静态: hello32.exe 为 PE32 (machine=0x14C, magic=0x10B)' 'FAIL' ("actual machine=0x{0:X} magic=0x{1:X}" -f $mach, $optm) +} + +# ================= 动态执行 ================= +# 单轮执行:正常启动 → cmd → AUTOEXEC(pe HELLO32.EXE / exit) → 返回 DSK +# 返回 $true=完整跑完;$false=会话提前退出(WHPX 终止等) +function Invoke-EmuRun { + param([Parameter(Mandatory)][string]$AccelMode) + Set-DeshabFirstInit -First 1 -DevMode 0 + Set-DeshabAutoexec -Lines @('pe HELLO32.EXE', 'exit') + + $timeout = 180 + if ($AccelMode -eq 'tcg') { $timeout = 420 } # TCG 全模拟显著更慢 + $s = Start-QemuSession -Name "x86emu32-$AccelMode" -Accel $AccelMode -MonitorPort 45508 + try { + $script:RunHit = Wait-QemuLog -Session $s -Patterns @('[DSK] cmd.elf returned', '[PANIC]') -TimeoutSeconds $timeout + Start-Sleep -Seconds 2 + $script:RunLog = Read-QemuLogText $s.LogPath + $script:RunLogPath = $s.LogPath + if (-not $script:RunHit) { return $false } + return $true + } finally { + Stop-QemuSession $s + } +} + +$script:RunLog = '' +$script:RunLogPath = '' +$script:RunHit = $null + +try { + $usedAccel = $Accel + $ok = Invoke-EmuRun -AccelMode $Accel + if (-not $ok -and $Accel -eq 'whpx' -and $script:RunLog.Contains('WHPX: Unexpected VP exit')) { + Add-TestNote $result "WHPX 在 PE 兼容层执行处终止 QEMU(Unexpected VP exit,WHPX 平台限制),自动以 TCG 重跑" + $usedAccel = 'tcg' + $ok = Invoke-EmuRun -AccelMode 'tcg' + } + $log = $script:RunLog + $result.LogPath = $script:RunLogPath + Add-TestNote $result "加速方案: $usedAccel" + $hit = $script:RunHit + + # ---- 解释器接管链(BUG-007 已修复,全链路可稳定断言) ---- + Assert-QemuLog $result $log -Ordered @( + '[cmd] running AUTOEXEC.BAT', + '[cmd] PE run begin', + '[cmd] cmdline: HELLO32.EXE', + '[PE] running PE32 via x86emu32' + ) -MustNotContain @( + '[PANIC]', + '[PE32] emulator error', + '[PE32] emu init failed', + '[PE] unimplemented import', + '[PE] unimplemented API called' + ) + + # ---- shim 输出与完整返回链:BUG-007/006 已修复,转硬断言(回归守护) ---- + if ($log.Contains('Hello from PE32!')) { + Add-TestCheck $result '动态: 解释执行 + shim IAT 拦截输出(Hello from PE32!)' 'PASS' + } else { + Add-TestCheck $result '动态: 解释执行 + shim IAT 拦截输出(Hello from PE32!)' 'FAIL' 'missing Hello from PE32!(BUG-20260801-007 回归?)' + } + if ($log.Contains('[cmd] PE run end') -and $log.Contains('[DSK] cmd.elf returned')) { + Add-TestCheck $result '动态: ExitProcess 回 cmd,AUTOEXEC 跑完返回 DSK' 'PASS' + } else { + # 已知系统侧阻塞(2026-08-04 诊断定案,非 BUG-006/007 回归): + # cmd run_autoexec 的 data/size 直指共享静态缓冲 f32_data(fat32_io.h), + # 首行 `pe HELLO32.EXE` 加载 PE 即覆盖剩余脚本,exit 行丢失 → cmd 停留 + # 交互主循环(monitor 取证 RIP=cmd.elf 内 inb 0x60 轮询点,无异常投递)。 + # pe-compat 会话 C(pe NONEXIST.EXE + exit)通过反证:文件未找到不覆盖 + # 缓冲时 exit 返回链正常。待 cmd 侧修复(脚本行先拷入私有缓冲再执行)。 + Add-TestCheck $result '动态: ExitProcess 回 cmd,AUTOEXEC 跑完返回 DSK' 'FAIL' 'missing [DSK] cmd.elf returned(已知系统 bug:cmd run_autoexec 共享 f32_data 被 pe 文件加载覆盖,exit 行丢失)' + } + + # 解释器侧覆盖判定:hello32 全指令流(含 shim IAT 拦截与参数读取) + # 均正确执行——无 emulator error / unimplemented 日志即解释器覆盖率 + # 不记倒退。 + if (-not $log.Contains('[PE] unimplemented') -and -not $log.Contains('[PE32] emulator') -and $log.Contains('[PE] running PE32 via x86emu32')) { + Add-TestCheck $result '动态: hello32 已执行指令全部命中已实现集(无 unimplemented/emulator error)' 'PASS' + } else { + Add-TestCheck $result '动态: hello32 已执行指令全部命中已实现集(无 unimplemented/emulator error)' 'FAIL' 'unimplemented/emulator error anchor present or interpreter not reached' + } + + # ---- 指令级分类覆盖(依赖尚不存在的分类测试 PE) ---- + foreach ($t in @('数据传送簇 (MOV/PUSH/POP 逐操作数形态)', '算术簇 (ADD/SUB/INC/DEC/MUL/DIV 逐标志验证)', + '逻辑簇 (AND/OR/XOR/NOT)', '控制流簇 (JMP/CALL/RET/Jcc)', + '字符串簇 (MOVSB/STOSB/REP 前缀)', '堆栈帧 (ENTER/LEAVE) + INT3 断点路径')) { + Add-TestCheck $result "指令级: $t" 'MANUAL' '需 CODE/tools/pe-samples/ 下分类测试 PE(test_arith.exe 等),当前未构建;hello32 仅综合冒烟' + } + Add-TestNote $result "pe-compat.ps1 已覆盖 PE32+ 原生对照组(HELLO64.EXE)与 PE 服务 ABI 锚点" +} finally { + Restore-DeshabScenario +} + +Write-TestCaseResult $result +if ($result.Status -eq 'FAIL') { exit 1 } else { exit 0 }