FU
This commit is contained in:
+14
-1
@@ -108,6 +108,8 @@ static u32 g_ready_port = 0xffffffffu;
|
|||||||
static ahci_cmd_header *g_cmd_header;
|
static ahci_cmd_header *g_cmd_header;
|
||||||
static struct dkm_dma_buffer g_cmd_table;
|
static struct dkm_dma_buffer g_cmd_table;
|
||||||
static struct dkm_dma_buffer g_data;
|
static struct dkm_dma_buffer g_data;
|
||||||
|
/* IDENTIFY 读出的用户可寻址扇区数;LBA48 优先,回退 LBA28 */
|
||||||
|
static u64 g_sector_count;
|
||||||
|
|
||||||
static u32 pci_read(u8 bus, u8 dev, u8 func, u8 reg) {
|
static u32 pci_read(u8 bus, u8 dev, u8 func, u8 reg) {
|
||||||
return dkm_pci_read(bus, dev, func, reg);
|
return dkm_pci_read(bus, dev, func, reg);
|
||||||
@@ -534,6 +536,17 @@ static int ahci_identify_port(u32 port, const struct dkm_dma_api *dma) {
|
|||||||
log_hex("[ahci] IDENTIFY word0=", id[0]);
|
log_hex("[ahci] IDENTIFY word0=", id[0]);
|
||||||
log_hex("[ahci] IDENTIFY word49=", id[49]);
|
log_hex("[ahci] IDENTIFY word49=", id[49]);
|
||||||
log_hex("[ahci] IDENTIFY word83=", id[83]);
|
log_hex("[ahci] IDENTIFY word83=", id[83]);
|
||||||
|
/* 容量必须在 ahci_read_lba0 复用 data 缓冲区前提取,
|
||||||
|
* 否则 IDENTIFY 数据被 LBA0 内容覆盖后容量丢失。 */
|
||||||
|
if ((id[83] & 0x0400u) && (id[103] | id[102] | id[101] | id[100])) {
|
||||||
|
/* LBA48: word100-103 小端 u16 序列拼接 */
|
||||||
|
g_sector_count = (u64)id[100] | ((u64)id[101] << 16)
|
||||||
|
| ((u64)id[102] << 32) | ((u64)id[103] << 48);
|
||||||
|
} else {
|
||||||
|
/* LBA28: word60-61 */
|
||||||
|
g_sector_count = (u64)id[60] | ((u64)id[61] << 16);
|
||||||
|
}
|
||||||
|
log_hex("[ahci] IDENTIFY sector_count=", g_sector_count);
|
||||||
ahci_read_lba0(port, hdr, &table, &data);
|
ahci_read_lba0(port, hdr, &table, &data);
|
||||||
g_ready_port = port;
|
g_ready_port = port;
|
||||||
g_cmd_header = hdr;
|
g_cmd_header = hdr;
|
||||||
@@ -678,7 +691,7 @@ int driver_init(const struct dkm_kernel_api *api,
|
|||||||
struct dkm_block_device_desc desc;
|
struct dkm_block_device_desc desc;
|
||||||
desc.name = "ahci0";
|
desc.name = "ahci0";
|
||||||
desc.sector_size = 512;
|
desc.sector_size = 512;
|
||||||
desc.sector_count = 0;
|
desc.sector_count = g_sector_count;
|
||||||
desc.ctx = 0;
|
desc.ctx = 0;
|
||||||
desc.read = ahci_block_read;
|
desc.read = ahci_block_read;
|
||||||
desc.write = ahci_block_write;
|
desc.write = ahci_block_write;
|
||||||
|
|||||||
@@ -0,0 +1,1169 @@
|
|||||||
|
/* DKM EHCI Driver — USB2 host controller + Mass Storage Class (BOT/SCSI)
|
||||||
|
*
|
||||||
|
* Stage 1 optional storage driver, depends on "pci", provides "block".
|
||||||
|
* Target: Intel 6/7-series EHCI (PCI class 0C/03/20, device 1C2D/1C26)
|
||||||
|
* and any standard EHCI controller — the USB2 path used on platforms
|
||||||
|
* without xHCI (e.g. H61/B75 with only EHCI controllers).
|
||||||
|
*
|
||||||
|
* Synchronous polling model (no IRQ): async list + QH/TD execution,
|
||||||
|
* transfer completion detected by polling the QH overlay token.
|
||||||
|
*
|
||||||
|
* Pipeline:
|
||||||
|
* 1. PCI discovery: class=0x0C/03/20
|
||||||
|
* 2. MMIO mapping: BAR0 via mm_map_mmio / HHDM
|
||||||
|
* 3. HC init: HCRESET, port power, RUN
|
||||||
|
* 4. Port scan: reset, wait PED, read speed
|
||||||
|
* 5. Enumeration: SET_ADDRESS, GET_DESCRIPTOR(dev), SET_CONFIGURATION
|
||||||
|
* 6. MSC: BOT (CBW/CSW) + SCSI (INQUIRY/RD_CAP/READ10/WRITE10)
|
||||||
|
* 7. Block provider: register usb2 via kernel_api.block
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "../dkm_shared.h"
|
||||||
|
#include "../dkm_instr.h"
|
||||||
|
|
||||||
|
DKM_STAT_DECL(ehci_pci_found);
|
||||||
|
DKM_STAT_DECL(ehci_port_ok);
|
||||||
|
DKM_STAT_DECL(ehci_msc_found);
|
||||||
|
DKM_STAT_DECL(ehci_block_reads);
|
||||||
|
DKM_STAT_DECL(ehci_block_writes);
|
||||||
|
|
||||||
|
static const char *const g_depends[] = { "pci" };
|
||||||
|
static const char *const g_provides[] = { "block" };
|
||||||
|
|
||||||
|
__attribute__((visibility("default"), used))
|
||||||
|
const struct dkm_driver_desc driver_desc = {
|
||||||
|
.magic = DKM_DRIVER_MAGIC,
|
||||||
|
.abi_version = DKM_ABI_VERSION,
|
||||||
|
.desc_size = sizeof(struct dkm_driver_desc),
|
||||||
|
.name = "ehci",
|
||||||
|
.version = "0.1.0",
|
||||||
|
.vendor = "Deshab",
|
||||||
|
.driver_class = 6, /* DKM_CLASS_STORAGE */
|
||||||
|
.stage = 1,
|
||||||
|
.flags = 0,
|
||||||
|
.priority = 0,
|
||||||
|
.depends = g_depends,
|
||||||
|
.depends_count = 1,
|
||||||
|
.provides = g_provides,
|
||||||
|
.provides_count = 1,
|
||||||
|
.min_kernel_abi = 1,
|
||||||
|
.feature_bits = 0,
|
||||||
|
.reserved0 = 0,
|
||||||
|
.reserved1 = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* Constants — EHCI registers, QH/TD, USB protocol
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
#define EHCI_CLASS_SERIAL 0x0C
|
||||||
|
#define EHCI_SUBCLASS_USB 0x03
|
||||||
|
#define EHCI_PROGIF_EHCI 0x20
|
||||||
|
|
||||||
|
/* Capability registers (MMIO + 0x00) */
|
||||||
|
#define EHCI_CAP_CAPLENGTH 0x00
|
||||||
|
#define EHCI_CAP_HCSPARAMS 0x04
|
||||||
|
#define EHCI_CAP_HCCPARAMS 0x08
|
||||||
|
|
||||||
|
/* Operational registers (MMIO + caplength) */
|
||||||
|
#define EHCI_OP_USBCMD 0x00
|
||||||
|
#define EHCI_OP_USBSTS 0x04
|
||||||
|
#define EHCI_OP_USBINTR 0x08
|
||||||
|
#define EHCI_OP_FRINDEX 0x0C
|
||||||
|
#define EHCI_OP_ASYNCLIST 0x18
|
||||||
|
#define EHCI_OP_CONFIGFLAG 0x40
|
||||||
|
#define EHCI_OP_PORTSC 0x44
|
||||||
|
#define EHCI_PORT_STRIDE 0x04
|
||||||
|
|
||||||
|
/* USBCMD bits */
|
||||||
|
#define USBCMD_RUN (1u << 0)
|
||||||
|
#define USBCMD_HCRESET (1u << 1)
|
||||||
|
#define USBCMD_PSE (1u << 4)
|
||||||
|
#define USBCMD_ASE (1u << 5)
|
||||||
|
|
||||||
|
/* USBSTS bits */
|
||||||
|
#define USBSTS_USBINT (1u << 0)
|
||||||
|
#define USBSTS_PCD (1u << 2)
|
||||||
|
#define USBSTS_FLR (1u << 3)
|
||||||
|
#define USBSTS_HSE (1u << 4)
|
||||||
|
#define USBSTS_IAA (1u << 6)
|
||||||
|
#define USBSTS_HCHALT (1u << 12)
|
||||||
|
|
||||||
|
/* PORTSC bits — 与 QEMU include/hw/usb/ehci-regs.h / EHCI 1.0 spec 一致
|
||||||
|
* BUG-FIX: 原定义位错(PED=bit1 应为 bit2,speed 在 LINESTAT bit10:11 而非
|
||||||
|
* bit14:15,change 位是 bit1/3/5 而非 bit17/18/20/21)——端口永不使能、
|
||||||
|
* 速度永远读 0,导致 HC 不执行该端口上的传输。 */
|
||||||
|
#define PORTSC_CONNECT (1u << 0) /* CCS */
|
||||||
|
#define PORTSC_CSC (1u << 1) /* Connect Status Change (W1C) */
|
||||||
|
#define PORTSC_PED (1u << 2) /* Port Enable */
|
||||||
|
#define PORTSC_PEDC (1u << 3) /* Port Enable/Disable Change (W1C) */
|
||||||
|
#define PORTSC_OCA (1u << 4) /* Over-Current Active */
|
||||||
|
#define PORTSC_OCC (1u << 5) /* Over-Current Change (W1C) */
|
||||||
|
#define PORTSC_FPR (1u << 6) /* Force Port Resume */
|
||||||
|
#define PORTSC_SUSP (1u << 7) /* Suspend */
|
||||||
|
#define PORTSC_PR (1u << 8) /* Port Reset */
|
||||||
|
#define PORTSC_LS_SHIFT 10 /* Line Status = 端口速度 (bit10:11) */
|
||||||
|
#define PORTSC_LS_MASK 0x3
|
||||||
|
#define PORTSC_PP (1u << 12) /* Port Power */
|
||||||
|
#define PORTSC_POWNER (1u << 13)
|
||||||
|
|
||||||
|
/* RWC 位(W1C):CSC|PEDC|OCC —— 清 change 时写这些位 */
|
||||||
|
#define PORTSC_RWC (PORTSC_CSC | PORTSC_PEDC | PORTSC_OCC)
|
||||||
|
|
||||||
|
/* PLS/LS 值(Line Status 字段,非 PLS 状态机) */
|
||||||
|
#define LS_FULL 0
|
||||||
|
#define LS_LOW 1
|
||||||
|
#define LS_HIGH 2
|
||||||
|
|
||||||
|
/* TD token bits */
|
||||||
|
#define TD_ACTIVE (1u << 7)
|
||||||
|
#define TD_HALTED (1u << 6)
|
||||||
|
#define TD_BUFERR (1u << 5)
|
||||||
|
#define TD_BABBLE (1u << 4)
|
||||||
|
#define TD_XACTERR (1u << 3)
|
||||||
|
#define TD_PID_SHIFT 8
|
||||||
|
#define TD_PID_MASK 0x3
|
||||||
|
#define TD_CERR_SHIFT 10
|
||||||
|
#define TD_CERR_MASK 0x3
|
||||||
|
#define TD_IOC (1u << 15)
|
||||||
|
#define TD_BYTES_SHIFT 16
|
||||||
|
#define TD_BYTES_MASK 0x7FFF
|
||||||
|
#define TD_TOGGLE (1u << 31)
|
||||||
|
|
||||||
|
#define PID_OUT 0
|
||||||
|
#define PID_IN 1
|
||||||
|
#define PID_SETUP 2
|
||||||
|
|
||||||
|
/* QH 链接:最低位 1 = 列表尾(T 位),0 = QH 指针 */
|
||||||
|
#define QH_TERMINATE 0x1u
|
||||||
|
|
||||||
|
/* USB speeds (EPCAP/QH ep speed 字段) */
|
||||||
|
#define USB_SPEED_FULL 0
|
||||||
|
#define USB_SPEED_LOW 1
|
||||||
|
#define USB_SPEED_HIGH 2
|
||||||
|
|
||||||
|
/* BOT signatures */
|
||||||
|
#define BOT_CBW_SIG 0x43425355u /* "USBC" */
|
||||||
|
#define BOT_CSW_SIG 0x53425355u /* "USBS" */
|
||||||
|
|
||||||
|
/* SCSI opcodes */
|
||||||
|
#define SCSI_TEST_UNIT_READY 0x00
|
||||||
|
#define SCSI_INQUIRY 0x12
|
||||||
|
#define SCSI_READ_CAPACITY10 0x25
|
||||||
|
#define SCSI_READ10 0x28
|
||||||
|
#define SCSI_WRITE10 0x2A
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* Data structures
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
/* EHCI Transfer Descriptor — 32 字节,32 字节对齐 */
|
||||||
|
typedef struct {
|
||||||
|
u32 next; /* +0 */
|
||||||
|
u32 alt_next; /* +4 */
|
||||||
|
u32 token; /* +8 */
|
||||||
|
u32 buf[5]; /* +12 */
|
||||||
|
} ehci_td;
|
||||||
|
|
||||||
|
/* EHCI Queue Head — 44 字节,32 字节对齐
|
||||||
|
* 布局必须与 QEMU v11 EHCIqh / EHCI 1.0 spec 完全一致:
|
||||||
|
* +0 hlink(next), +4 epchar, +8 epcap(端点能力: hub addr+smask/cmask),
|
||||||
|
* +12 current_qtd, +16 next_qtd(overlay), +20 altnext(overlay), +24 token(overlay),
|
||||||
|
* +28 buf[5]
|
||||||
|
* BUG-FIX(关键): epcap 字段不能删!QEMU 从 +16 读 next_qtd、+20 读 altnext、
|
||||||
|
* +24 读 token。删掉 epcap 使 overlay 错位 4 字节——驱动写的 ov_next 落到
|
||||||
|
* current_qtd 位、ov_alt(0x1=T) 落到 next_qtd 位 → QEMU 认为 QH 无 TD
|
||||||
|
* 永不 FETCHQTD(token 恒 ACTIVE 0x80280)。 */
|
||||||
|
typedef struct {
|
||||||
|
u32 hlink; /* +0 */
|
||||||
|
u32 epchar; /* +4 */
|
||||||
|
u32 epcap; /* +8 */
|
||||||
|
u32 curqtd; /* +12 */
|
||||||
|
u32 ov_next; /* +16 overlay.next_qtd */
|
||||||
|
u32 ov_alt; /* +20 overlay.altnext */
|
||||||
|
u32 ov_token; /* +24 overlay.token */
|
||||||
|
u32 ov_buf[5]; /* +28 */
|
||||||
|
} ehci_qh;
|
||||||
|
|
||||||
|
/* MSC device state */
|
||||||
|
typedef struct {
|
||||||
|
u8 dev_addr;
|
||||||
|
u8 speed;
|
||||||
|
u8 bulk_out_ep; /* endpoint number (1..15) */
|
||||||
|
u8 bulk_in_ep;
|
||||||
|
u16 bulk_out_mps;
|
||||||
|
u16 bulk_in_mps;
|
||||||
|
u32 cbw_tag;
|
||||||
|
u64 sector_count;
|
||||||
|
u32 sector_size;
|
||||||
|
int ready;
|
||||||
|
} ehci_msc_dev;
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* Global state
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
static const struct dkm_kernel_api *g_api;
|
||||||
|
static const struct dkm_log_api *g_log;
|
||||||
|
static volatile u8 *g_mmio; /* EHCI BAR0 virtual */
|
||||||
|
static u32 g_caplen;
|
||||||
|
static u32 g_nports;
|
||||||
|
static u32 g_64bit; /* HCCPARAMS bit0 */
|
||||||
|
static u32 g_ppc; /* per-port power control bit4 */
|
||||||
|
|
||||||
|
/* DMA:异步列表区(QH/TD 池)+ 数据缓冲 + CSW 缓冲 */
|
||||||
|
static struct dkm_dma_buffer g_qh_dma; /* 4KB:QH×4 + TD×8 */
|
||||||
|
static struct dkm_dma_buffer g_xfer; /* 数据 bounce buffer(>=64KB) */
|
||||||
|
static struct dkm_dma_buffer g_csw; /* CSW 独立缓冲(防覆盖数据区前 13 字节) */
|
||||||
|
static ehci_qh *g_qh_ctrl;
|
||||||
|
static ehci_qh *g_qh_bulk_out;
|
||||||
|
static ehci_qh *g_qh_bulk_in;
|
||||||
|
static ehci_td *g_td_pool;
|
||||||
|
static u32 g_td_next;
|
||||||
|
|
||||||
|
static ehci_msc_dev g_msc;
|
||||||
|
static u8 g_pci_bus, g_pci_dev, g_pci_func;
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* Utility
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
static void log_hex(const char *prefix, u64 value) {
|
||||||
|
static const char hex[] = "0123456789abcdef";
|
||||||
|
char buf[19];
|
||||||
|
u32 pos = 0;
|
||||||
|
buf[pos++] = '0';
|
||||||
|
buf[pos++] = 'x';
|
||||||
|
for (int i = 15; i >= 0; i--)
|
||||||
|
buf[pos++] = hex[(value >> (i * 4)) & 0xf];
|
||||||
|
buf[pos] = 0;
|
||||||
|
g_log->info(prefix);
|
||||||
|
g_log->info(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ehci_zero(void *ptr, u32 len) {
|
||||||
|
u8 *p = (u8 *)ptr;
|
||||||
|
for (u32 i = 0; i < len; i++) p[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ehci_memcpy(void *dst, const void *src, u32 len) {
|
||||||
|
u8 *d = (u8 *)dst;
|
||||||
|
const u8 *s = (const u8 *)src;
|
||||||
|
for (u32 i = 0; i < len; i++) d[i] = s[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
static u32 mmio_read32(u32 off) {
|
||||||
|
return *(volatile u32 *)(void *)(g_mmio + off);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void mmio_write32(u32 off, u32 value) {
|
||||||
|
*(volatile u32 *)(void *)(g_mmio + off) = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ehci_mb(void) {
|
||||||
|
__asm__ volatile("" ::: "memory");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 把物理地址编码进 32 位链接字段(64 位控制器用 CTRLDSSEGMENT 高 32 位) */
|
||||||
|
/* (内联使用,见 qh_submit_control/qh_enqueue) */
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* Phase 1: PCI discovery + MMIO mapping
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
static int ehci_find(u8 *out_bus, u8 *out_dev, u8 *out_func) {
|
||||||
|
for (u16 bus = 0; bus < 256; bus++) {
|
||||||
|
for (u8 dev = 0; dev < 32; dev++) {
|
||||||
|
u32 vd = dkm_pci_read((u8)bus, dev, 0, PCI_VENDOR_ID);
|
||||||
|
if ((vd & 0xffff) == 0xffff) continue;
|
||||||
|
u8 header = (u8)(dkm_pci_read((u8)bus, dev, 0, PCI_HEADER) >> 16);
|
||||||
|
u8 func_count = (header & 0x80) ? 8 : 1;
|
||||||
|
for (u8 func = 0; func < func_count; func++) {
|
||||||
|
u32 vd2 = dkm_pci_read((u8)bus, dev, func, PCI_VENDOR_ID);
|
||||||
|
if ((vd2 & 0xffff) == 0xffff) continue;
|
||||||
|
u32 class_reg = dkm_pci_read((u8)bus, dev, func, 0x08);
|
||||||
|
u8 prog_if = (u8)((class_reg >> 8) & 0xff);
|
||||||
|
u8 subclass = (u8)((class_reg >> 16) & 0xff);
|
||||||
|
u8 class_code = (u8)((class_reg >> 24) & 0xff);
|
||||||
|
if (class_code == EHCI_CLASS_SERIAL &&
|
||||||
|
subclass == EHCI_SUBCLASS_USB &&
|
||||||
|
prog_if == EHCI_PROGIF_EHCI) {
|
||||||
|
*out_bus = (u8)bus;
|
||||||
|
*out_dev = dev;
|
||||||
|
*out_func = func;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static volatile u8 *ehci_map_bar(u64 bar_phys) {
|
||||||
|
u64 bar_virt = g_api->hhdm_offset + bar_phys;
|
||||||
|
if (g_api->mm_map_mmio) {
|
||||||
|
void *mapped = g_api->mm_map_mmio(bar_phys, 0x1000);
|
||||||
|
if (mapped) {
|
||||||
|
g_log->info("[ehci] BAR mapped via mm_map_mmio");
|
||||||
|
return (volatile u8 *)mapped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (g_api->mmio && g_api->mmio->is_mapped) {
|
||||||
|
if (g_api->mmio->is_mapped(bar_virt) > 0) {
|
||||||
|
g_log->info("[ehci] HHDM covers BAR");
|
||||||
|
return (volatile u8 *)(uintptr_t)bar_virt;
|
||||||
|
}
|
||||||
|
if (g_api->mmio->map_mmio) {
|
||||||
|
if (g_api->mmio->map_mmio(bar_phys, 0x1000) == 0 &&
|
||||||
|
g_api->mmio->is_mapped(bar_virt) > 0) {
|
||||||
|
g_log->info("[ehci] BAR mapped via page tables");
|
||||||
|
return (volatile u8 *)(uintptr_t)bar_virt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bar_phys < 0x100000000ULL) {
|
||||||
|
g_log->info("[ehci] assuming HHDM covers low BAR");
|
||||||
|
return (volatile u8 *)(uintptr_t)bar_virt;
|
||||||
|
}
|
||||||
|
g_log->error("[ehci] BAR MMIO unavailable");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* Phase 1: HC reset and start
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
static int ehci_hc_reset(void) {
|
||||||
|
/* 等待 HCHALT=1(Halted 时才能写 HCRESET) */
|
||||||
|
u64 deadline = dkm_rdtsc() + dkm_tsc_per_ms * 100;
|
||||||
|
while (dkm_rdtsc() < deadline) {
|
||||||
|
if (mmio_read32(g_caplen + EHCI_OP_USBSTS) & USBSTS_HCHALT) break;
|
||||||
|
__asm__ volatile("pause");
|
||||||
|
}
|
||||||
|
|
||||||
|
mmio_write32(g_caplen + EHCI_OP_USBCMD, USBCMD_HCRESET);
|
||||||
|
deadline = dkm_rdtsc() + dkm_tsc_per_ms * 100;
|
||||||
|
while (dkm_rdtsc() < deadline) {
|
||||||
|
if (!(mmio_read32(g_caplen + EHCI_OP_USBCMD) & USBCMD_HCRESET)) {
|
||||||
|
g_log->info("[ehci] HC reset complete");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
__asm__ volatile("pause");
|
||||||
|
}
|
||||||
|
g_log->error("[ehci] HC reset timeout");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* Phase 2: QH/TD management
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
static ehci_td *td_alloc(void) {
|
||||||
|
if (g_td_next >= 8) return 0;
|
||||||
|
ehci_td *td = &g_td_pool[g_td_next++];
|
||||||
|
ehci_zero(td, sizeof(ehci_td));
|
||||||
|
td->next = QH_TERMINATE;
|
||||||
|
td->alt_next = QH_TERMINATE;
|
||||||
|
return td;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 初始化一个 QH:endpoint=0 控制 / 指定端点 bulk。
|
||||||
|
* 注意:不能动 hlink —— ehci_hc_start 在启动时已把 ctrl/out/in 三个 QH
|
||||||
|
* 链成环;每次提交只更新 epchar/overlay,hlink 必须保留(否则环断裂,
|
||||||
|
* HC 停止遍历,overlay token 恒 0 → qh_wait 假成功返回空数据)。 */
|
||||||
|
static void qh_init(ehci_qh *qh, u8 dev_addr, u8 ep_num, u8 speed,
|
||||||
|
u16 mps, int control) {
|
||||||
|
qh->curqtd = 0;
|
||||||
|
qh->ov_next = QH_TERMINATE;
|
||||||
|
qh->ov_alt = QH_TERMINATE;
|
||||||
|
qh->ov_token = 0; /* inactive */
|
||||||
|
|
||||||
|
u32 epchar = (u32)dev_addr; /* [6:0] */
|
||||||
|
epchar |= ((u32)ep_num << 8); /* [11:8] */
|
||||||
|
epchar |= ((u32)speed << 12); /* [14:12] */
|
||||||
|
epchar |= ((u32)mps << 16); /* [26:16] */
|
||||||
|
if (control) epchar |= (1u << 27); /* control endpoint */
|
||||||
|
/* DTC 清 0:由 HC 管理 data toggle。
|
||||||
|
* H 位(bit15) 保留:ctrl 是环头 QH,每次提交重建 epchar 后必须
|
||||||
|
* 重新置 H,否则 QEMU WAITLISTHEAD 找不到入口(只在 H=1 的 QH
|
||||||
|
* 处进入 FETCHENTRY)。 */
|
||||||
|
u32 keep_h = qh->epchar & (1u << 15);
|
||||||
|
qh->epchar = epchar | keep_h;
|
||||||
|
/* epcap(端点能力):直连根端口 hub addr=0, smask/cmask=0。
|
||||||
|
* 该字段必须存在且清零——QEMU 依赖它使 overlay 偏移正确。 */
|
||||||
|
qh->epcap = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 构建控制传输 TD 链:SETUP -> [DATA] -> STATUS,挂到 qh。 */
|
||||||
|
static int qh_submit_control(ehci_qh *qh, const u8 *setup8,
|
||||||
|
u8 *data, u32 data_len, u8 dir) {
|
||||||
|
/* 清空旧链:重置 overlay */
|
||||||
|
qh->curqtd = 0;
|
||||||
|
qh->ov_next = QH_TERMINATE;
|
||||||
|
qh->ov_token = 0;
|
||||||
|
g_td_next = 0; /* 复用池(单传输假设) */
|
||||||
|
|
||||||
|
ehci_td *t_setup = td_alloc();
|
||||||
|
ehci_td *t_data = 0;
|
||||||
|
ehci_td *t_status = td_alloc();
|
||||||
|
if (!t_setup || !t_status) return -1;
|
||||||
|
if (data_len > 0) {
|
||||||
|
t_data = td_alloc();
|
||||||
|
if (!t_data) return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SETUP TD:8 字节,toggle=0,pid=SETUP
|
||||||
|
* 注意:必须先分配 t_data 再算 t_setup->next(原实现引用未分配
|
||||||
|
* 的 t_data → TD 链指向垃圾地址 → HC 走链崩溃/超时)。 */
|
||||||
|
if (data_len > 0) {
|
||||||
|
t_setup->next = (u32)(g_qh_dma.phys + (u64)((u8 *)t_data - (u8 *)g_qh_dma.virt));
|
||||||
|
} else {
|
||||||
|
t_setup->next = (u32)(g_qh_dma.phys + (u64)((u8 *)t_status - (u8 *)g_qh_dma.virt));
|
||||||
|
}
|
||||||
|
/* setup 数据嵌入 QH overlay buffer?标准做法:SETUP TD buffer 指向含 8 字节 setup 的内存 */
|
||||||
|
/* 简化:setup 包拷贝到 xfer 缓冲头部,TD buffer 指向它 */
|
||||||
|
{
|
||||||
|
u8 *sb = (u8 *)g_xfer.virt;
|
||||||
|
ehci_memcpy(sb, setup8, 8);
|
||||||
|
__asm__ volatile("wbinvd" ::: "memory");
|
||||||
|
t_setup->buf[0] = (u32)(g_xfer.phys & 0xFFFFFFFFu);
|
||||||
|
u32 tok = TD_ACTIVE | (PID_SETUP << TD_PID_SHIFT) | (8u << TD_BYTES_SHIFT);
|
||||||
|
t_setup->token = tok; /* toggle=0 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DATA TD(可选)——t_data 已在上方分配,此处不再重复分配 */
|
||||||
|
if (data_len > 0 && t_data) {
|
||||||
|
/* 数据在 g_xfer.virt + 8 偏移处(IN:设备写这里;OUT:软件先拷入) */
|
||||||
|
u8 *db = (u8 *)g_xfer.virt + 8;
|
||||||
|
if (dir == PID_OUT) {
|
||||||
|
ehci_memcpy(db, data, data_len);
|
||||||
|
__asm__ volatile("wbinvd" ::: "memory");
|
||||||
|
}
|
||||||
|
t_data->buf[0] = (u32)((g_xfer.phys + 8) & 0xFFFFFFFFu);
|
||||||
|
t_data->next = (u32)(g_qh_dma.phys + (u64)((u8 *)t_status - (u8 *)g_qh_dma.virt));
|
||||||
|
u32 tok = TD_ACTIVE | ((u32)dir << TD_PID_SHIFT) | ((u32)data_len << TD_BYTES_SHIFT) | TD_TOGGLE;
|
||||||
|
t_data->token = tok; /* toggle=1 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* STATUS TD:反向 pid,toggle=1,长度 0 */
|
||||||
|
{
|
||||||
|
u8 spid = (dir == PID_IN) ? PID_OUT : PID_IN;
|
||||||
|
if (data_len == 0) spid = (dir == PID_IN) ? PID_OUT : PID_IN;
|
||||||
|
u32 tok = TD_ACTIVE | ((u32)spid << TD_PID_SHIFT) | TD_TOGGLE;
|
||||||
|
t_status->token = tok;
|
||||||
|
t_status->next = QH_TERMINATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* QH overlay 指向第一个 TD */
|
||||||
|
qh->ov_next = (u32)(g_qh_dma.phys + (u64)((u8 *)t_setup - (u8 *)g_qh_dma.virt));
|
||||||
|
/* 写完 QH/TD 后刷新 CPU cache——DMA 控制器读物理内存需要;
|
||||||
|
* 缺此步在 WHPX 下 QEMU 读不到 ov_next(竞态),TCG 下侥幸通过。 */
|
||||||
|
__asm__ volatile("wbinvd" ::: "memory");
|
||||||
|
ehci_mb();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 构建 bulk 传输单 TD:dir 0=OUT 1=IN。
|
||||||
|
* in_dma: IN 方向的目标 DMA 缓冲(数据用 g_xfer,CSW 用 g_csw——
|
||||||
|
* 防止 CSW 读覆盖数据区前 13 字节)。OUT 方向 in_dma 忽略(数据先拷入
|
||||||
|
* in_dma 指向的缓冲)。 */
|
||||||
|
static int qh_submit_bulk(ehci_qh *qh, u8 dir, const void *data, u32 data_len,
|
||||||
|
struct dkm_dma_buffer *in_dma) {
|
||||||
|
qh->curqtd = 0;
|
||||||
|
qh->ov_next = QH_TERMINATE;
|
||||||
|
qh->ov_token = 0;
|
||||||
|
g_td_next = 0;
|
||||||
|
|
||||||
|
ehci_td *td = td_alloc();
|
||||||
|
if (!td) return -1;
|
||||||
|
|
||||||
|
struct dkm_dma_buffer *dst = in_dma ? in_dma : &g_xfer;
|
||||||
|
u8 *buf = (u8 *)dst->virt;
|
||||||
|
if (dir == PID_OUT) {
|
||||||
|
ehci_memcpy(buf, data, data_len);
|
||||||
|
__asm__ volatile("wbinvd" ::: "memory");
|
||||||
|
}
|
||||||
|
td->buf[0] = (u32)(dst->phys & 0xFFFFFFFFu);
|
||||||
|
u32 tok = TD_ACTIVE | ((u32)dir << TD_PID_SHIFT) | ((u32)data_len << TD_BYTES_SHIFT);
|
||||||
|
td->token = tok; /* toggle 由 HC 管理(DTC=0) */
|
||||||
|
|
||||||
|
qh->ov_next = (u32)(g_qh_dma.phys + (u64)((u8 *)td - (u8 *)g_qh_dma.virt));
|
||||||
|
__asm__ volatile("wbinvd" ::: "memory");
|
||||||
|
ehci_mb();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 等待 QH 上的传输完成。轮询首个 TD 的 token(HC 执行后清 ACTIVE、
|
||||||
|
* 出错置 HALTED)——比检查 overlay 可靠:HC 未执行时 overlay token
|
||||||
|
* 保持 inactive,轮询 overlay 会假成功返回空数据。first_td 传 TD 池指针,
|
||||||
|
* 由调用方从 qh->ov_next 反推或直接记录。
|
||||||
|
* BUG-FIX(缓存一致性):EHCI HC 经 DMA 写 TD 到物理内存,CPU 轮询前
|
||||||
|
* 必须失效缓存行,否则读到 write-back cache 旧值(Active 恒 1)→ 假超时。
|
||||||
|
* QEMU 复现证据:first_td token=0x80280 显示 SETUP 已执行成功(Active 清、
|
||||||
|
* 无错误位),但轮询超时——正是 cache 陈旧。 */
|
||||||
|
static int qh_wait(ehci_qh *qh, ehci_td *first_td, u32 timeout_ms) {
|
||||||
|
(void)qh;
|
||||||
|
u64 deadline = dkm_rdtsc() + dkm_tsc_per_ms * timeout_ms;
|
||||||
|
for (;;) {
|
||||||
|
/* 失效 TD 所在缓存行,强制从内存读 HC 的 DMA 写结果 */
|
||||||
|
__asm__ volatile("clflush (%0)" : : "r"(first_td) : "memory");
|
||||||
|
u32 tok = first_td->token;
|
||||||
|
if (!(tok & TD_ACTIVE)) {
|
||||||
|
if (tok & TD_HALTED) {
|
||||||
|
g_log->warn("[ehci] transfer halted");
|
||||||
|
log_hex("[ehci] token=", tok);
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (dkm_rdtsc() > deadline) {
|
||||||
|
g_log->warn("[ehci] transfer timeout");
|
||||||
|
log_hex("[ehci] first_td token=", tok);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
__asm__ volatile("pause");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 启动 HC:把三个 QH 链成环(ctrl→bulk_out→bulk_in→ctrl),
|
||||||
|
* 在 ASE=0(async schedule 停止)时写一次 ASYNCLIST,然后 RUN|ASE。
|
||||||
|
* 之后每次提交只改 QH overlay,不再碰 ASYNCLIST(规范要求该寄存器
|
||||||
|
* 只能在 async schedule 停止时写——QEMU 会拒绝运行期写入)。 */
|
||||||
|
static int ehci_hc_start(void) {
|
||||||
|
u64 ctrl_phys = g_qh_dma.phys + (u64)((u8 *)g_qh_ctrl - (u8 *)g_qh_dma.virt);
|
||||||
|
u64 out_phys = g_qh_dma.phys + (u64)((u8 *)g_qh_bulk_out - (u8 *)g_qh_dma.virt);
|
||||||
|
u64 in_phys = g_qh_dma.phys + (u64)((u8 *)g_qh_bulk_in - (u8 *)g_qh_dma.virt);
|
||||||
|
|
||||||
|
/* hlink 链接指针必须带类型位 bit1=1(QH 类型)。
|
||||||
|
* QEMU v11 的 NLPTR_TYPE_GET(x)=((x)>>1)&3,QH 类型=1 → 写物理地址|2。
|
||||||
|
* BUG-FIX: 裸地址(bit1=0)→ QEMU fetchentry 报 "non queue head
|
||||||
|
* request in async schedule" 并重置 HC,TD 永不执行。 */
|
||||||
|
g_qh_ctrl->hlink = (u32)((out_phys & 0xFFFFFFFFu) | 2u);
|
||||||
|
g_qh_bulk_out->hlink = (u32)((in_phys & 0xFFFFFFFFu) | 2u);
|
||||||
|
g_qh_bulk_in->hlink = (u32)((ctrl_phys & 0xFFFFFFFFu) | 2u);
|
||||||
|
/* H 位(bit15) = 回收头标记:QEMU 的 WAITLISTHEAD 状态只在环中找到
|
||||||
|
* H=1 的 QH 时进入 FETCHENTRY→FETCHQH。不置 H 位 → 状态机永远停在
|
||||||
|
* WAITLISTHEAD,TD 永不执行。ctrl 作为环头必须置 H。 */
|
||||||
|
g_qh_ctrl->epchar |= (1u << 15);
|
||||||
|
ehci_mb();
|
||||||
|
|
||||||
|
/* 确保 ASE=0(复位后默认 0) */
|
||||||
|
u32 cmd = mmio_read32(g_caplen + EHCI_OP_USBCMD);
|
||||||
|
mmio_write32(g_caplen + EHCI_OP_USBCMD, cmd & ~USBCMD_ASE);
|
||||||
|
ehci_mb();
|
||||||
|
mmio_write32(g_caplen + EHCI_OP_ASYNCLIST, (u32)((ctrl_phys & 0xFFFFFFFFu) | 2u));
|
||||||
|
ehci_mb();
|
||||||
|
mmio_write32(g_caplen + EHCI_OP_USBCMD,
|
||||||
|
mmio_read32(g_caplen + EHCI_OP_USBCMD) | USBCMD_ASE | USBCMD_RUN);
|
||||||
|
ehci_mb();
|
||||||
|
g_log->info("[ehci] async schedule started");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 简单控制传输封装 */
|
||||||
|
static int ehci_control(u8 dev_addr, u8 speed, u16 mps0,
|
||||||
|
const u8 *setup8, u8 *data, u32 data_len, u8 dir) {
|
||||||
|
qh_init(g_qh_ctrl, dev_addr, 0, speed, mps0, 1);
|
||||||
|
if (qh_submit_control(g_qh_ctrl, setup8, data, data_len, dir) != 0) return -1;
|
||||||
|
int rc = qh_wait(g_qh_ctrl, &g_td_pool[0], 5000);
|
||||||
|
/* IN 方向:失效数据缓冲缓存行后把 HC DMA 写入的数据拷回调用方 */
|
||||||
|
if (rc == 0 && dir == PID_IN && data && data_len > 0) {
|
||||||
|
__asm__ volatile("clflush (%0)" : : "r"(g_xfer.virt) : "memory");
|
||||||
|
__asm__ volatile("clflush 64(%0)" : : "r"(g_xfer.virt) : "memory");
|
||||||
|
ehci_memcpy(data, (u8 *)g_xfer.virt + 8, data_len);
|
||||||
|
}
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 简单 bulk 传输封装(端点号已含方向编码在 QH)。
|
||||||
|
* in_dma: IN 方向的目标 DMA 缓冲(g_xfer 或 g_csw)。
|
||||||
|
* BUG-FIX: CSW 必须用独立缓冲——若与数据共用 g_xfer,13 字节 CSW 会
|
||||||
|
* 覆盖数据区前 13 字节(bps 字段被清零)。 */
|
||||||
|
static int ehci_bulk(ehci_qh *qh, u8 dev_addr, u8 speed, u16 mps,
|
||||||
|
u8 ep_num, u8 dir, const void *data, u32 data_len,
|
||||||
|
struct dkm_dma_buffer *in_dma) {
|
||||||
|
qh_init(qh, dev_addr, ep_num, speed, mps, 0);
|
||||||
|
if (qh_submit_bulk(qh, dir, data, data_len, in_dma) != 0) return -1;
|
||||||
|
int rc = qh_wait(qh, &g_td_pool[0], 10000);
|
||||||
|
if (rc == 0 && dir == PID_IN) {
|
||||||
|
struct dkm_dma_buffer *dst = in_dma ? in_dma : &g_xfer;
|
||||||
|
/* 失效缓存行后拷贝 */
|
||||||
|
__asm__ volatile("clflush (%0)" : : "r"(dst->virt) : "memory");
|
||||||
|
ehci_memcpy((void *)data, dst->virt, data_len);
|
||||||
|
}
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* Phase 3: Port scan and reset
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
static int ehci_reset_port(u32 port) {
|
||||||
|
u32 po = g_caplen + EHCI_OP_PORTSC + port * EHCI_PORT_STRIDE;
|
||||||
|
u32 ps = mmio_read32(po);
|
||||||
|
if (!(ps & PORTSC_CONNECT)) return -1;
|
||||||
|
|
||||||
|
/* 复位序列(EHCI 1.0 §4.2.4 + QEMU 模型兼容):
|
||||||
|
* 1) 置 PR=1 请求复位
|
||||||
|
* 2) 保持 PR=1 一小段时间(QEMU 不置 PRC,等待只是给模拟器处理窗口)
|
||||||
|
* 3) 清 PR —— QEMU 在 PR 1→0 下降沿执行 usb_port_reset 并置 PED(仅高速)
|
||||||
|
* 4) 轮询 PED 确认使能
|
||||||
|
* BUG-FIX: 原实现等待 PRC(bit20),但 QEMU 从不置该位 → 200ms 必超时
|
||||||
|
* 枚举中止(QEMU hcd-ehci.c ehci_port_write 只处理 PR 下降沿)。 */
|
||||||
|
mmio_write32(po, ps | PORTSC_PR);
|
||||||
|
dkm_delay_ms(50);
|
||||||
|
|
||||||
|
/* 清 PR + 清 change 位(1→0 下降沿触发复位) */
|
||||||
|
ps = mmio_read32(po);
|
||||||
|
mmio_write32(po, (ps & ~PORTSC_PR) | PORTSC_RWC);
|
||||||
|
|
||||||
|
/* 轮询 PED */
|
||||||
|
u64 deadline = dkm_rdtsc() + dkm_tsc_per_ms * 200;
|
||||||
|
while (dkm_rdtsc() < deadline) {
|
||||||
|
ps = mmio_read32(po);
|
||||||
|
if (ps & PORTSC_PED) {
|
||||||
|
DKM_STAT_INC(ehci_port_ok);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!(ps & PORTSC_CONNECT)) return -2; /* 复位中断开 */
|
||||||
|
__asm__ volatile("pause");
|
||||||
|
}
|
||||||
|
g_log->warn("[ehci] port not enabled after reset");
|
||||||
|
return -3;
|
||||||
|
}
|
||||||
|
|
||||||
|
static u8 ehci_port_speed(u32 port) {
|
||||||
|
u32 po = g_caplen + EHCI_OP_PORTSC + port * EHCI_PORT_STRIDE;
|
||||||
|
u32 ps = mmio_read32(po);
|
||||||
|
return (u8)((ps >> PORTSC_LS_SHIFT) & PORTSC_LS_MASK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* Phase 4: USB enumeration
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
/* 前置声明(BOT/SCSI 定义在 enum_msc 之后) */
|
||||||
|
static int ehci_scsi_inquiry(void);
|
||||||
|
static int ehci_bot_send_cbw(u8 lun, u8 flags, u32 data_len,
|
||||||
|
const u8 *cb, u8 cb_len);
|
||||||
|
static int ehci_bot_receive_csw(void);
|
||||||
|
|
||||||
|
static int ehci_get_device_descriptor(u8 *out, u32 len) {
|
||||||
|
u8 setup[8];
|
||||||
|
setup[0] = 0x80; setup[1] = 0x06;
|
||||||
|
setup[2] = 0x00; setup[3] = 0x01; /* Device descriptor */
|
||||||
|
setup[4] = 0x00; setup[5] = 0x00;
|
||||||
|
setup[6] = (u8)(len & 0xFF); setup[7] = (u8)((len >> 8) & 0xFF);
|
||||||
|
ehci_zero(out, len);
|
||||||
|
/* GET_DESCRIPTOR 阶段 EP0 MPS 未知,按速度用默认值 */
|
||||||
|
u16 mps0 = 64;
|
||||||
|
if (g_msc.speed == LS_LOW) mps0 = 8;
|
||||||
|
return ehci_control(g_msc.dev_addr, g_msc.speed, mps0, setup, out, len, PID_IN);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ehci_set_address(u8 addr, u16 mps0) {
|
||||||
|
u8 setup[8];
|
||||||
|
setup[0] = 0x00; setup[1] = 0x05;
|
||||||
|
setup[2] = (u8)(addr & 0xFF); setup[3] = 0x00;
|
||||||
|
setup[4] = 0x00; setup[5] = 0x00;
|
||||||
|
setup[6] = 0x00; setup[7] = 0x00;
|
||||||
|
return ehci_control(0, g_msc.speed, mps0, setup, 0, 0, PID_OUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ehci_set_configuration(u8 addr, u8 cfg, u16 mps0) {
|
||||||
|
u8 setup[8];
|
||||||
|
setup[0] = 0x00; setup[1] = 0x09;
|
||||||
|
setup[2] = cfg; setup[3] = 0x00;
|
||||||
|
setup[4] = 0x00; setup[5] = 0x00;
|
||||||
|
setup[6] = 0x00; setup[7] = 0x00;
|
||||||
|
return ehci_control(addr, g_msc.speed, mps0, setup, 0, 0, PID_OUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ehci_get_config_descriptor(u8 addr, u8 *out, u32 len) {
|
||||||
|
u8 setup[8];
|
||||||
|
setup[0] = 0x80; setup[1] = 0x06;
|
||||||
|
setup[2] = 0x00; setup[3] = 0x02; /* Configuration descriptor */
|
||||||
|
setup[4] = 0x00; setup[5] = 0x00;
|
||||||
|
setup[6] = (u8)(len & 0xFF); setup[7] = (u8)((len >> 8) & 0xFF);
|
||||||
|
ehci_zero(out, len);
|
||||||
|
return ehci_control(addr, g_msc.speed, 64, setup, out, len, PID_IN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 枚举 MSC 设备:SET_ADDRESS -> GET_DESCRIPTOR -> SET_CONFIGURATION -> 端点解析 */
|
||||||
|
static int ehci_enum_msc(u32 port) {
|
||||||
|
g_msc.speed = ehci_port_speed(port);
|
||||||
|
log_hex("[ehci] port speed=", g_msc.speed);
|
||||||
|
u16 mps0 = 64; /* 全速/高速 EP0 默认 64 */
|
||||||
|
if (g_msc.speed == LS_LOW) mps0 = 8;
|
||||||
|
|
||||||
|
/* SET_ADDRESS(用默认 pipe,mps 按速度) */
|
||||||
|
if (ehci_set_address(1, mps0) != 0) {
|
||||||
|
g_log->warn("[ehci] SET_ADDRESS failed");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
g_msc.dev_addr = 1;
|
||||||
|
g_msc.bulk_out_mps = mps0; /* 后续控制传输用已知 mps0 */
|
||||||
|
|
||||||
|
/* GET_DESCRIPTOR(device, 18) */
|
||||||
|
u8 dev_desc[18];
|
||||||
|
if (ehci_get_device_descriptor(dev_desc, 18) != 0) {
|
||||||
|
g_log->warn("[ehci] GET_DESCRIPTOR(Device) failed");
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
u8 dev_class = dev_desc[4];
|
||||||
|
u16 vid = (u16)dev_desc[8] | ((u16)dev_desc[9] << 8);
|
||||||
|
u16 pid = (u16)dev_desc[10] | ((u16)dev_desc[11] << 8);
|
||||||
|
u8 real_mps0 = dev_desc[7];
|
||||||
|
log_hex("[ehci] dev class=", dev_class);
|
||||||
|
log_hex("[ehci] VID=", vid);
|
||||||
|
log_hex("[ehci] PID=", pid);
|
||||||
|
log_hex("[ehci] bMaxPacketSize0=", real_mps0);
|
||||||
|
/* 设备声明的真实 EP0 MPS 与猜测不同时,用真实值 */
|
||||||
|
if (real_mps0 >= 8 && real_mps0 <= 64) mps0 = real_mps0;
|
||||||
|
|
||||||
|
/* SET_CONFIGURATION(1) */
|
||||||
|
if (ehci_set_configuration(g_msc.dev_addr, 1, mps0) != 0) {
|
||||||
|
g_log->warn("[ehci] SET_CONFIGURATION failed");
|
||||||
|
return -3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GET_DESCRIPTOR(config, 255) 并解析 MSC 接口 + bulk 端点 */
|
||||||
|
u8 cfg_desc[255];
|
||||||
|
if (ehci_get_config_descriptor(g_msc.dev_addr, cfg_desc, 255) != 0) {
|
||||||
|
g_log->warn("[ehci] GET_DESCRIPTOR(Config) failed");
|
||||||
|
return -4;
|
||||||
|
}
|
||||||
|
u16 total_len = (u16)cfg_desc[2] | ((u16)cfg_desc[3] << 8);
|
||||||
|
u8 bulk_out_ep = 0, bulk_in_ep = 0;
|
||||||
|
u16 bulk_out_mps = 512, bulk_in_mps = 512;
|
||||||
|
int msc_found = 0;
|
||||||
|
|
||||||
|
u32 pos = 9; /* 跳过 config descriptor */
|
||||||
|
while (pos + 2 <= (u32)total_len && pos + cfg_desc[pos] <= (u32)total_len) {
|
||||||
|
u8 desc_len = cfg_desc[pos];
|
||||||
|
u8 desc_type = cfg_desc[pos + 1];
|
||||||
|
if (desc_type == 4 && desc_len >= 9) {
|
||||||
|
u8 iface_class = cfg_desc[pos + 5];
|
||||||
|
u8 iface_sub = cfg_desc[pos + 6];
|
||||||
|
u8 iface_proto = cfg_desc[pos + 7];
|
||||||
|
if (iface_class == 0x08 && iface_sub == 0x06 && iface_proto == 0x50) {
|
||||||
|
msc_found = 1;
|
||||||
|
g_log->info("[ehci] MSC BOT interface found");
|
||||||
|
u32 ep_pos = pos + desc_len;
|
||||||
|
while (ep_pos + 2 <= (u32)total_len) {
|
||||||
|
u8 ep_len = cfg_desc[ep_pos];
|
||||||
|
if (ep_len == 0 || ep_pos + ep_len > (u32)total_len) break;
|
||||||
|
if (cfg_desc[ep_pos + 1] == 5) {
|
||||||
|
u8 ep_addr = cfg_desc[ep_pos + 2];
|
||||||
|
u8 ep_attr = cfg_desc[ep_pos + 3];
|
||||||
|
u16 ep_mps = (u16)cfg_desc[ep_pos + 4] | ((u16)cfg_desc[ep_pos + 5] << 8);
|
||||||
|
if ((ep_attr & 0x03) == 0x02) {
|
||||||
|
if (ep_addr & 0x80) {
|
||||||
|
bulk_in_ep = ep_addr & 0x0F;
|
||||||
|
bulk_in_mps = ep_mps;
|
||||||
|
} else {
|
||||||
|
bulk_out_ep = ep_addr & 0x0F;
|
||||||
|
bulk_out_mps = ep_mps;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (cfg_desc[ep_pos + 1] == 4) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ep_pos += ep_len;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pos += desc_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!msc_found || bulk_out_ep == 0 || bulk_in_ep == 0) {
|
||||||
|
g_log->warn("[ehci] no MSC BOT interface found");
|
||||||
|
return -5;
|
||||||
|
}
|
||||||
|
log_hex("[ehci] Bulk OUT EP=", bulk_out_ep);
|
||||||
|
log_hex("[ehci] Bulk IN EP=", bulk_in_ep);
|
||||||
|
log_hex("[ehci] Bulk OUT MPS=", bulk_out_mps);
|
||||||
|
log_hex("[ehci] Bulk IN MPS=", bulk_in_mps);
|
||||||
|
|
||||||
|
g_msc.bulk_out_ep = bulk_out_ep;
|
||||||
|
g_msc.bulk_in_ep = bulk_in_ep;
|
||||||
|
g_msc.bulk_out_mps = bulk_out_mps;
|
||||||
|
g_msc.bulk_in_mps = bulk_in_mps;
|
||||||
|
g_msc.cbw_tag = 1;
|
||||||
|
|
||||||
|
/* INQUIRY 验证设备是块设备 */
|
||||||
|
if (ehci_scsi_inquiry() != 0) {
|
||||||
|
g_log->warn("[ehci] INQUIRY failed or not block device");
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* Phase 5: BOT + SCSI(复用 xhci 已验证的 CBW/CSW/SCSI 组包逻辑)
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
static int ehci_bot_send_cbw(u8 lun, u8 flags, u32 data_len,
|
||||||
|
const u8 *cb, u8 cb_len) {
|
||||||
|
u8 cbw[31];
|
||||||
|
ehci_zero(cbw, 31);
|
||||||
|
cbw[0] = 0x55; cbw[1] = 0x53; cbw[2] = 0x42; cbw[3] = 0x43;
|
||||||
|
u32 tag = g_msc.cbw_tag++;
|
||||||
|
cbw[4] = (u8)(tag & 0xFF);
|
||||||
|
cbw[5] = (u8)((tag >> 8) & 0xFF);
|
||||||
|
cbw[6] = (u8)((tag >> 16) & 0xFF);
|
||||||
|
cbw[7] = (u8)((tag >> 24) & 0xFF);
|
||||||
|
cbw[8] = (u8)(data_len & 0xFF);
|
||||||
|
cbw[9] = (u8)((data_len >> 8) & 0xFF);
|
||||||
|
cbw[10] = (u8)((data_len >> 16) & 0xFF);
|
||||||
|
cbw[11] = (u8)((data_len >> 24) & 0xFF);
|
||||||
|
cbw[12] = flags;
|
||||||
|
cbw[13] = lun;
|
||||||
|
cbw[14] = cb_len;
|
||||||
|
for (u8 i = 0; i < cb_len && i < 16; i++) cbw[15 + i] = cb[i];
|
||||||
|
|
||||||
|
return ehci_bulk(g_qh_bulk_out, g_msc.dev_addr, g_msc.speed,
|
||||||
|
g_msc.bulk_out_mps, g_msc.bulk_out_ep, PID_OUT, cbw, 31, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ehci_bot_receive_csw(void) {
|
||||||
|
u8 csw[13];
|
||||||
|
/* CSW 用独立 DMA 缓冲,避免覆盖数据阶段读入 g_xfer 的数据 */
|
||||||
|
int rc = ehci_bulk(g_qh_bulk_in, g_msc.dev_addr, g_msc.speed,
|
||||||
|
g_msc.bulk_in_mps, g_msc.bulk_in_ep, PID_IN, csw, 13, &g_csw);
|
||||||
|
if (rc != 0) return rc;
|
||||||
|
if (csw[0] != 0x55 || csw[1] != 0x53 || csw[2] != 0x42 || csw[3] != 0x53) {
|
||||||
|
g_log->error("[ehci] CSW signature mismatch");
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
if (csw[12] != 0) {
|
||||||
|
log_hex("[ehci] CSW status=", csw[12]);
|
||||||
|
return -3;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ehci_scsi_inquiry(void) {
|
||||||
|
u8 cb[16];
|
||||||
|
ehci_zero(cb, 16);
|
||||||
|
cb[0] = SCSI_INQUIRY;
|
||||||
|
cb[4] = 36;
|
||||||
|
if (ehci_bot_send_cbw(0, 0x80, 36, cb, 6) != 0) return -1;
|
||||||
|
u8 inq[36];
|
||||||
|
if (ehci_bulk(g_qh_bulk_in, g_msc.dev_addr, g_msc.speed,
|
||||||
|
g_msc.bulk_in_mps, g_msc.bulk_in_ep, PID_IN, inq, 36, &g_xfer) != 0) return -2;
|
||||||
|
if (ehci_bot_receive_csw() != 0) return -3;
|
||||||
|
log_hex("[ehci] INQ PeripheralType=", inq[0] & 0x1F);
|
||||||
|
return ((inq[0] & 0x1F) == 0) ? 0 : -4;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ehci_scsi_read_capacity(u64 *out_sectors, u32 *out_blk) {
|
||||||
|
u8 cb[16];
|
||||||
|
ehci_zero(cb, 16);
|
||||||
|
cb[0] = SCSI_READ_CAPACITY10;
|
||||||
|
if (ehci_bot_send_cbw(0, 0x80, 8, cb, 10) != 0) return -1;
|
||||||
|
u8 cap[8];
|
||||||
|
if (ehci_bulk(g_qh_bulk_in, g_msc.dev_addr, g_msc.speed,
|
||||||
|
g_msc.bulk_in_mps, g_msc.bulk_in_ep, PID_IN, cap, 8, &g_xfer) != 0) return -2;
|
||||||
|
if (ehci_bot_receive_csw() != 0) return -3;
|
||||||
|
u32 max_lba = ((u32)cap[0] << 24) | ((u32)cap[1] << 16) | ((u32)cap[2] << 8) | (u32)cap[3];
|
||||||
|
u32 blk = ((u32)cap[4] << 24) | ((u32)cap[5] << 16) | ((u32)cap[6] << 8) | (u32)cap[7];
|
||||||
|
*out_sectors = (u64)max_lba + 1;
|
||||||
|
*out_blk = blk;
|
||||||
|
log_hex("[ehci] capacity sectors=", *out_sectors);
|
||||||
|
log_hex("[ehci] block size=", blk);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ehci_scsi_test_unit_ready(void) {
|
||||||
|
u8 cb[16];
|
||||||
|
ehci_zero(cb, 16);
|
||||||
|
cb[0] = SCSI_TEST_UNIT_READY;
|
||||||
|
if (ehci_bot_send_cbw(0, 0x00, 0, cb, 6) != 0) return -1;
|
||||||
|
return ehci_bot_receive_csw();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ehci_scsi_read10(u64 lba, u32 count, void *buffer) {
|
||||||
|
u8 cb[16];
|
||||||
|
ehci_zero(cb, 16);
|
||||||
|
cb[0] = SCSI_READ10;
|
||||||
|
cb[2] = (u8)((lba >> 24) & 0xFF);
|
||||||
|
cb[3] = (u8)((lba >> 16) & 0xFF);
|
||||||
|
cb[4] = (u8)((lba >> 8) & 0xFF);
|
||||||
|
cb[5] = (u8)(lba & 0xFF);
|
||||||
|
cb[7] = (u8)((count >> 8) & 0xFF);
|
||||||
|
cb[8] = (u8)(count & 0xFF);
|
||||||
|
u32 data_len = count * 512;
|
||||||
|
if (ehci_bot_send_cbw(0, 0x80, data_len, cb, 10) != 0) return -1;
|
||||||
|
if (ehci_bulk(g_qh_bulk_in, g_msc.dev_addr, g_msc.speed,
|
||||||
|
g_msc.bulk_in_mps, g_msc.bulk_in_ep, PID_IN, buffer, data_len, &g_xfer) != 0) return -2;
|
||||||
|
return ehci_bot_receive_csw();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ehci_scsi_write10(u64 lba, u32 count, const void *buffer) {
|
||||||
|
u8 cb[16];
|
||||||
|
ehci_zero(cb, 16);
|
||||||
|
cb[0] = SCSI_WRITE10;
|
||||||
|
cb[2] = (u8)((lba >> 24) & 0xFF);
|
||||||
|
cb[3] = (u8)((lba >> 16) & 0xFF);
|
||||||
|
cb[4] = (u8)((lba >> 8) & 0xFF);
|
||||||
|
cb[5] = (u8)(lba & 0xFF);
|
||||||
|
cb[7] = (u8)((count >> 8) & 0xFF);
|
||||||
|
cb[8] = (u8)(count & 0xFF);
|
||||||
|
u32 data_len = count * 512;
|
||||||
|
if (ehci_bot_send_cbw(0, 0x00, data_len, cb, 10) != 0) return -1;
|
||||||
|
if (ehci_bulk(g_qh_bulk_out, g_msc.dev_addr, g_msc.speed,
|
||||||
|
g_msc.bulk_out_mps, g_msc.bulk_out_ep, PID_OUT, buffer, data_len, 0) != 0) return -2;
|
||||||
|
return ehci_bot_receive_csw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* Block provider
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
static int ehci_block_read(void *ctx, u64 lba, u32 count, void *buffer) {
|
||||||
|
(void)ctx;
|
||||||
|
if (!g_msc.ready || !buffer || count == 0) return -1;
|
||||||
|
DKM_STAT_INC(ehci_block_reads);
|
||||||
|
u8 *buf = (u8 *)buffer;
|
||||||
|
while (count > 0) {
|
||||||
|
u32 chunk = count > 16 ? 16 : count;
|
||||||
|
if (ehci_scsi_read10(lba, chunk, g_xfer.virt) != 0) {
|
||||||
|
log_hex("[ehci] READ10 failed lba=", lba);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
ehci_memcpy(buf, g_xfer.virt, chunk * 512);
|
||||||
|
buf += chunk * 512;
|
||||||
|
lba += chunk;
|
||||||
|
count -= chunk;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ehci_block_write(void *ctx, u64 lba, u32 count, const void *buffer) {
|
||||||
|
(void)ctx;
|
||||||
|
if (!g_msc.ready || !buffer || count == 0) return -1;
|
||||||
|
DKM_STAT_INC(ehci_block_writes);
|
||||||
|
const u8 *buf = (const u8 *)buffer;
|
||||||
|
while (count > 0) {
|
||||||
|
u32 chunk = count > 16 ? 16 : count;
|
||||||
|
ehci_memcpy(g_xfer.virt, buf, chunk * 512);
|
||||||
|
__asm__ volatile("wbinvd" ::: "memory");
|
||||||
|
if (ehci_scsi_write10(lba, chunk, g_xfer.virt) != 0) {
|
||||||
|
log_hex("[ehci] WRITE10 failed lba=", lba);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
buf += chunk * 512;
|
||||||
|
lba += chunk;
|
||||||
|
count -= chunk;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* driver_init
|
||||||
|
* ================================================================ */
|
||||||
|
|
||||||
|
__attribute__((visibility("default")))
|
||||||
|
int driver_init(const struct dkm_kernel_api *api,
|
||||||
|
struct dkm_driver_handle *handle) {
|
||||||
|
(void)handle;
|
||||||
|
dkm_tsc_calibrate();
|
||||||
|
if (!api || !api->log) return -1;
|
||||||
|
g_api = api;
|
||||||
|
g_log = api->log;
|
||||||
|
dkm_instr_init(api);
|
||||||
|
|
||||||
|
g_log->info("[ehci] init begin");
|
||||||
|
|
||||||
|
/* PCI 发现 0C/03/20 */
|
||||||
|
u8 bus = 0, dev = 0, func = 0;
|
||||||
|
if (ehci_find(&bus, &dev, &func) != 0) {
|
||||||
|
g_log->info("[ehci] EHCI controller not found");
|
||||||
|
g_log->info("[ehci] driver ready");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
g_pci_bus = bus; g_pci_dev = dev; g_pci_func = func;
|
||||||
|
DKM_STAT_INC(ehci_pci_found);
|
||||||
|
g_log->info("[ehci] EHCI controller found");
|
||||||
|
log_hex("[ehci] bus=", bus);
|
||||||
|
log_hex("[ehci] dev=", dev);
|
||||||
|
log_hex("[ehci] func=", func);
|
||||||
|
|
||||||
|
u32 vd = dkm_pci_read(bus, dev, func, PCI_VENDOR_ID);
|
||||||
|
log_hex("[ehci] vendor=", vd & 0xffff);
|
||||||
|
log_hex("[ehci] device=", vd >> 16);
|
||||||
|
|
||||||
|
/* 使能 MEM + BUSM */
|
||||||
|
u32 command = dkm_pci_read(bus, dev, func, PCI_COMMAND) & 0xffff;
|
||||||
|
command |= PCI_CMD_MEM | PCI_CMD_BUSM;
|
||||||
|
dkm_pci_write(bus, dev, func, PCI_COMMAND, command);
|
||||||
|
log_hex("[ehci] PCI command=", command);
|
||||||
|
|
||||||
|
/* BAR0(EHCI 通常是 32 位 MMIO) */
|
||||||
|
u32 bar0_lo = dkm_pci_read(bus, dev, func, PCI_BAR0);
|
||||||
|
u64 bar_phys;
|
||||||
|
if ((bar0_lo & 0x7) == 0x4) {
|
||||||
|
u32 bar1_hi = dkm_pci_read(bus, dev, func, PCI_BAR1);
|
||||||
|
bar_phys = ((u64)bar1_hi << 32) | (u64)(bar0_lo & 0xFFFFFFF0u);
|
||||||
|
} else {
|
||||||
|
bar_phys = (u64)(bar0_lo & 0xFFFFFFF0u);
|
||||||
|
}
|
||||||
|
log_hex("[ehci] BAR0 phys=", bar_phys);
|
||||||
|
if (!api->hhdm_offset || !bar_phys) {
|
||||||
|
g_log->warn("[ehci] missing HHDM or BAR0");
|
||||||
|
g_log->info("[ehci] driver ready");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_mmio = ehci_map_bar(bar_phys);
|
||||||
|
if (!g_mmio) {
|
||||||
|
g_log->error("[ehci] BAR MMIO unavailable");
|
||||||
|
g_log->info("[ehci] driver ready");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 能力寄存器 */
|
||||||
|
g_caplen = mmio_read32(EHCI_CAP_CAPLENGTH) & 0xFF;
|
||||||
|
u32 hcsp = mmio_read32(EHCI_CAP_HCSPARAMS);
|
||||||
|
u32 hccp = mmio_read32(EHCI_CAP_HCCPARAMS);
|
||||||
|
g_nports = hcsp & 0x0F;
|
||||||
|
g_64bit = hccp & 1u;
|
||||||
|
g_ppc = (hcsp >> 4) & 1u;
|
||||||
|
log_hex("[ehci] CAPLEN=", g_caplen);
|
||||||
|
log_hex("[ehci] nports=", g_nports);
|
||||||
|
log_hex("[ehci] 64bit=", g_64bit);
|
||||||
|
log_hex("[ehci] PPC=", g_ppc);
|
||||||
|
if (g_nports == 0) {
|
||||||
|
g_log->error("[ehci] no ports");
|
||||||
|
g_log->info("[ehci] driver ready");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HC 复位 */
|
||||||
|
if (ehci_hc_reset() != 0) {
|
||||||
|
g_log->error("[ehci] HC reset failed");
|
||||||
|
g_log->info("[ehci] driver ready");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 64 位控制器:CTRLDSSEGMENT = 0 */
|
||||||
|
if (g_64bit) mmio_write32(g_caplen + 0x10, 0);
|
||||||
|
|
||||||
|
/* DMA:异步列表页 + 数据缓冲 */
|
||||||
|
if (!api->dma) {
|
||||||
|
g_log->warn("[ehci] DMA API unavailable");
|
||||||
|
g_log->info("[ehci] driver ready");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (api->dma->alloc_pages(1, 4096, 0x100000000ULL, &g_qh_dma) != 0) {
|
||||||
|
g_log->error("[ehci] async list alloc failed");
|
||||||
|
g_log->info("[ehci] driver ready");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (api->dma->alloc_pages(16, 4096, 0x100000000ULL, &g_xfer) != 0) {
|
||||||
|
g_log->error("[ehci] xfer buf alloc failed");
|
||||||
|
g_log->info("[ehci] driver ready");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (api->dma->alloc_pages(1, 4096, 0x100000000ULL, &g_csw) != 0) {
|
||||||
|
g_log->error("[ehci] csw buf alloc failed");
|
||||||
|
g_log->info("[ehci] driver ready");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
ehci_zero(g_qh_dma.virt, g_qh_dma.size);
|
||||||
|
ehci_zero(g_xfer.virt, g_xfer.size);
|
||||||
|
ehci_zero(g_csw.virt, g_csw.size);
|
||||||
|
|
||||||
|
/* QH 布局:必须 32 字节对齐(EHCI 规范)。ctrl@0, out@64, in@128;
|
||||||
|
* TD 池 @256(TD 32 字节对齐)。原 48 字节间距导致 out/in 未对齐,
|
||||||
|
* HC 无法处理 QH → TD 永不被执行 → 超时。 */
|
||||||
|
g_qh_ctrl = (ehci_qh *)((u8 *)g_qh_dma.virt + 0);
|
||||||
|
g_qh_bulk_out = (ehci_qh *)((u8 *)g_qh_dma.virt + 64);
|
||||||
|
g_qh_bulk_in = (ehci_qh *)((u8 *)g_qh_dma.virt + 128);
|
||||||
|
g_td_pool = (ehci_td *)((u8 *)g_qh_dma.virt + 256);
|
||||||
|
|
||||||
|
/* 建环并启动 async schedule(必须在 CONFIGFLAG/端口操作前) */
|
||||||
|
ehci_hc_start();
|
||||||
|
|
||||||
|
/* CONFIGFLAG=1(OS 所有端口) */
|
||||||
|
mmio_write32(g_caplen + EHCI_OP_CONFIGFLAG, 1u);
|
||||||
|
|
||||||
|
/* 端口电源 */
|
||||||
|
for (u32 p = 0; p < g_nports; p++) {
|
||||||
|
u32 po = g_caplen + EHCI_OP_PORTSC + p * EHCI_PORT_STRIDE;
|
||||||
|
u32 ps = mmio_read32(po);
|
||||||
|
if (g_ppc && !(ps & PORTSC_PP)) {
|
||||||
|
mmio_write32(po, ps | PORTSC_PP);
|
||||||
|
dkm_delay_ms(20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dkm_delay_ms(100); /* 端口稳定 */
|
||||||
|
|
||||||
|
/* 扫描端口,枚举 MSC */
|
||||||
|
g_msc.ready = 0;
|
||||||
|
for (u32 p = 0; p < g_nports; p++) {
|
||||||
|
u32 po = g_caplen + EHCI_OP_PORTSC + p * EHCI_PORT_STRIDE;
|
||||||
|
u32 ps = mmio_read32(po);
|
||||||
|
log_hex("[ehci] port=", p);
|
||||||
|
log_hex("[ehci] PORTSC=", ps);
|
||||||
|
if (!(ps & PORTSC_CONNECT)) continue;
|
||||||
|
|
||||||
|
if (ehci_reset_port(p) != 0) {
|
||||||
|
g_log->warn("[ehci] port reset failed");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
dkm_delay_ms(100);
|
||||||
|
if (ehci_enum_msc(p) != 0) {
|
||||||
|
g_log->warn("[ehci] MSC enumeration failed");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
/* TEST UNIT READY(重试 5 次) */
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
if (ehci_scsi_test_unit_ready() == 0) break;
|
||||||
|
dkm_delay_ms(100);
|
||||||
|
}
|
||||||
|
/* READ CAPACITY */
|
||||||
|
u64 sectors = 0;
|
||||||
|
u32 blk = 512;
|
||||||
|
if (ehci_scsi_read_capacity(§ors, &blk) != 0) {
|
||||||
|
g_log->warn("[ehci] READ CAPACITY failed");
|
||||||
|
sectors = 0; blk = 512;
|
||||||
|
}
|
||||||
|
g_msc.sector_count = sectors;
|
||||||
|
g_msc.sector_size = blk;
|
||||||
|
g_msc.ready = 1;
|
||||||
|
DKM_STAT_INC(ehci_msc_found);
|
||||||
|
log_hex("[ehci] MSC device ready, sectors=", sectors);
|
||||||
|
log_hex("[ehci] MSC device block_size=", blk);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 注册 block provider */
|
||||||
|
if (g_msc.ready && api->block && api->block->register_device) {
|
||||||
|
struct dkm_block_device_desc desc;
|
||||||
|
desc.name = "usb2";
|
||||||
|
desc.sector_size = g_msc.sector_size;
|
||||||
|
desc.sector_count = g_msc.sector_count;
|
||||||
|
desc.ctx = 0;
|
||||||
|
desc.read = ehci_block_read;
|
||||||
|
desc.write = ehci_block_write;
|
||||||
|
int index = api->block->register_device(&desc);
|
||||||
|
log_hex("[ehci] block provider index=", (u64)(i64)index);
|
||||||
|
} else {
|
||||||
|
g_log->info("[ehci] no MSC device found; block provider not registered");
|
||||||
|
}
|
||||||
|
|
||||||
|
g_log->info("[ehci] driver ready");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((visibility("default")))
|
||||||
|
int driver_exit(struct dkm_driver_handle *handle) {
|
||||||
|
(void)handle;
|
||||||
|
if (g_mmio) {
|
||||||
|
mmio_write32(g_caplen + EHCI_OP_USBCMD,
|
||||||
|
mmio_read32(g_caplen + EHCI_OP_USBCMD) & ~USBCMD_RUN);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
+430
-90
@@ -92,6 +92,9 @@ const struct dkm_driver_desc driver_desc = {
|
|||||||
#define XHCI_RT_IR0_ERDP_LO 0x38
|
#define XHCI_RT_IR0_ERDP_LO 0x38
|
||||||
#define XHCI_RT_IR0_ERDP_HI 0x3C
|
#define XHCI_RT_IR0_ERDP_HI 0x3C
|
||||||
|
|
||||||
|
/* ERDP bits: bit3 = Event Handler Busy (软件写 1 清挂起) */
|
||||||
|
#define ERDP_EHB (1u << 3)
|
||||||
|
|
||||||
/* USBCMD bits */
|
/* USBCMD bits */
|
||||||
#define USBCMD_RS (1u << 0)
|
#define USBCMD_RS (1u << 0)
|
||||||
#define USBCMD_RST (1u << 1)
|
#define USBCMD_RST (1u << 1)
|
||||||
@@ -109,8 +112,11 @@ const struct dkm_driver_desc driver_desc = {
|
|||||||
#define PORTSC_PR (1u << 4)
|
#define PORTSC_PR (1u << 4)
|
||||||
#define PORTSC_PLS_SHIFT 5
|
#define PORTSC_PLS_SHIFT 5
|
||||||
#define PORTSC_PLS_MASK 0xF
|
#define PORTSC_PLS_MASK 0xF
|
||||||
#define PORTSC_SPEED_SHIFT 17
|
/* Port Speed 字段在 PORTSC bits[13:10](xHCI 1.2 §5.4.8)。
|
||||||
#define PORTSC_SPEED_MASK 0x7
|
* BUG-FIX: 原实现 shift=17/mask=0x7 是错的——QEMU 用 10:13,
|
||||||
|
* 误读导致 SuperSpeed(4) 被当成 FULL(1),EP0 MPS 错误。 */
|
||||||
|
#define PORTSC_SPEED_SHIFT 10
|
||||||
|
#define PORTSC_SPEED_MASK 0xF
|
||||||
|
|
||||||
/* TRB types */
|
/* TRB types */
|
||||||
#define TRB_TYPE_NORMAL 0x01
|
#define TRB_TYPE_NORMAL 0x01
|
||||||
@@ -124,12 +130,14 @@ const struct dkm_driver_desc driver_desc = {
|
|||||||
#define TRB_TYPE_CONFIGURE_EP 0x0C
|
#define TRB_TYPE_CONFIGURE_EP 0x0C
|
||||||
#define TRB_TYPE_NOOP 0x17
|
#define TRB_TYPE_NOOP 0x17
|
||||||
|
|
||||||
/* Event TRB types — same namespace as command/transfer TRB types.
|
/* Event TRB types — xHCI 1.2 spec Table 6-28 (TRB Type field [15:10]).
|
||||||
* Per xHCI spec §6.4: these are the TRB Type field values in the
|
* 规范值:Transfer=0x20, Command Completion=0x21, Port Status Change=0x22。
|
||||||
* Event TRB control dword [15:10]. */
|
* BUG-FIX: 原定义 0x01/0x02/0x21 与规范冲突——CMD_COMPLETE(0x21) 被
|
||||||
#define EVT_TYPE_TRANSFER 0x01 /* Transfer Event */
|
* 误判为 PORT_CHANGE 事件消费,导致 Enable Slot 等命令事件环被提前
|
||||||
#define EVT_TYPE_CMD_COMPLETE 0x02 /* Command Completion Event */
|
* 清空,后续命令全部超时(QEMU hcd-xhci.c xhci_write_event 佐证)。 */
|
||||||
#define EVT_TYPE_PORT_CHANGE 0x21 /* Port Status Change Event */
|
#define EVT_TYPE_TRANSFER 0x20 /* Transfer Event */
|
||||||
|
#define EVT_TYPE_CMD_COMPLETE 0x21 /* Command Completion Event */
|
||||||
|
#define EVT_TYPE_PORT_CHANGE 0x22 /* Port Status Change Event */
|
||||||
|
|
||||||
/* TRB control field helpers */
|
/* TRB control field helpers */
|
||||||
#define TRB_TYPE_SHIFT 10
|
#define TRB_TYPE_SHIFT 10
|
||||||
@@ -281,6 +289,149 @@ static void log_hex(const char *prefix, u64 value) {
|
|||||||
g_log->info(buf);
|
g_log->info(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* Framebuffer 阶段诊断(实机无串口时屏幕直接显示 xhci 失败点)
|
||||||
|
* 失败时整屏填背景色 + 中央画大号两位十六进制状态码,然后 halt。
|
||||||
|
* 码表(调用点注释中注明):
|
||||||
|
* 红底 0x01..0x07 基础设施失败(PCI/MMIO/caps/reset/ring/start)
|
||||||
|
* 红底 0x11..0x14 PCI 自检细分(见 xhci_pci_diag_code)
|
||||||
|
* 黄底 0x08 所有端口无设备可枚举
|
||||||
|
* 黄底 0x0A..0x0F 枚举失败(slot/address/descriptor/configure/scsi)
|
||||||
|
* 依赖 dkm_kernel_api.fb_* 字段(32bpp,与 console_fb 一致)。
|
||||||
|
* ================================================================ */
|
||||||
|
static const u8 g_fb_font_hex[16][8] = {
|
||||||
|
{0x3C,0x66,0x6E,0x76,0x66,0x66,0x3C,0x00}, /* 0 */
|
||||||
|
{0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00}, /* 1 */
|
||||||
|
{0x3C,0x66,0x06,0x0C,0x18,0x30,0x7E,0x00}, /* 2 */
|
||||||
|
{0x3C,0x66,0x06,0x1C,0x06,0x66,0x3C,0x00}, /* 3 */
|
||||||
|
{0x0C,0x1C,0x3C,0x6C,0x7E,0x0C,0x0C,0x00}, /* 4 */
|
||||||
|
{0x7E,0x60,0x7C,0x06,0x06,0x66,0x3C,0x00}, /* 5 */
|
||||||
|
{0x1C,0x30,0x60,0x7C,0x66,0x66,0x3C,0x00}, /* 6 */
|
||||||
|
{0x7E,0x66,0x06,0x0C,0x18,0x18,0x18,0x00}, /* 7 */
|
||||||
|
{0x3C,0x66,0x66,0x3C,0x66,0x66,0x3C,0x00}, /* 8 */
|
||||||
|
{0x3C,0x66,0x66,0x3E,0x06,0x0C,0x38,0x00}, /* 9 */
|
||||||
|
{0x3C,0x66,0x66,0x7E,0x66,0x66,0x66,0x00}, /* A */
|
||||||
|
{0x7C,0x66,0x66,0x7C,0x66,0x66,0x7C,0x00}, /* B */
|
||||||
|
{0x3C,0x66,0x60,0x60,0x60,0x66,0x3C,0x00}, /* C */
|
||||||
|
{0x78,0x6C,0x66,0x66,0x66,0x6C,0x78,0x00}, /* D */
|
||||||
|
{0x7E,0x60,0x60,0x7C,0x60,0x60,0x7E,0x00}, /* E */
|
||||||
|
{0x7E,0x60,0x60,0x7C,0x60,0x60,0x60,0x00}, /* F */
|
||||||
|
};
|
||||||
|
|
||||||
|
static void fb_draw_char(u32 *fb, u64 pitch, int x0, int y0, int scale,
|
||||||
|
u32 color, int ch) {
|
||||||
|
const u8 *g = g_fb_font_hex[ch & 0xF];
|
||||||
|
for (int row = 0; row < 8; row++) {
|
||||||
|
u8 bits = g[row];
|
||||||
|
for (int col = 0; col < 8; col++) {
|
||||||
|
if (!(bits & (0x80u >> col))) continue;
|
||||||
|
for (int sy = 0; sy < scale; sy++) {
|
||||||
|
u32 *line = (u32 *)((u8 *)fb + (u64)(y0 + row * scale + sy) * pitch);
|
||||||
|
for (int sx = 0; sx < scale; sx++)
|
||||||
|
line[x0 + col * scale + sx] = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void fb_diag_show(const struct dkm_kernel_api *api, u32 bg, u32 code) {
|
||||||
|
if (!api || !api->fb_address || !api->fb_width || !api->fb_height)
|
||||||
|
return; /* 无 framebuffer 时退化为仅串口日志 */
|
||||||
|
u32 *fb = (u32 *)api->fb_address;
|
||||||
|
u64 pitch = api->fb_pitch;
|
||||||
|
u64 w = api->fb_width, h = api->fb_height;
|
||||||
|
for (u64 y = 0; y < h; y++) {
|
||||||
|
u32 *line = (u32 *)((u8 *)fb + y * pitch);
|
||||||
|
for (u64 x = 0; x < w; x++) line[x] = bg;
|
||||||
|
}
|
||||||
|
int scale = 8;
|
||||||
|
int cw = 8 * scale;
|
||||||
|
int cx = (int)(w / 2) - cw; /* 两位数字整体居中 */
|
||||||
|
int cy = (int)(h / 2) - (8 * scale) / 2;
|
||||||
|
u32 fg = 0xFFFFFFFFu;
|
||||||
|
if (code >= 0x10) fb_draw_char(fb, pitch, cx, cy, scale, fg, (int)(code >> 4));
|
||||||
|
fb_draw_char(fb, pitch, cx + cw, cy, scale, fg, (int)(code & 0xF));
|
||||||
|
/* 停住屏幕,等待实机拍照/观察 */
|
||||||
|
for (;;) {
|
||||||
|
__asm__ volatile("cli; hlt");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 任意位数 hex(n=1..8),低位在前填充 */
|
||||||
|
static void fb_draw_hexn(u32 *fb, u64 pitch, int x0, int y0, int scale,
|
||||||
|
u32 color, u32 v, int n) {
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
int nib = (int)((v >> (4 * (n - 1 - i))) & 0xF);
|
||||||
|
fb_draw_char(fb, pitch, x0 + i * 8 * scale, y0, scale, color, nib);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PCI 自检诊断清单项(bus/dev/device_id/class/progif) */
|
||||||
|
struct usb_ctl_info {
|
||||||
|
u32 bus;
|
||||||
|
u32 dev;
|
||||||
|
u32 devid; /* PCI device ID */
|
||||||
|
u32 cls; /* (class<<8)|subclass */
|
||||||
|
u32 progif;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 扩展诊断:大字标题码 + 下方逐行列出扫描到的 PCI 设备。
|
||||||
|
* 每行:bus(1) dev(1) deviceid(4) = 左 6 hex,class(2) progif(2) = 右 4 hex。
|
||||||
|
* scale=2 支持 16 行(640x480 完整容纳)。device ID 用于区分芯片型号:
|
||||||
|
* Intel 7系: xHCI=1e31, EHCI1=1e2d, EHCI2=1e26
|
||||||
|
* Intel 6系: EHCI1=1c2d, EHCI2=1c26 (H61 仅 1 个 EHCI)
|
||||||
|
* 第三方 USB3: ASMedia=1b21, Renesas=1033 */
|
||||||
|
/* 诊断用,保留 */
|
||||||
|
static void fb_diag_show_usb(const struct dkm_kernel_api *api, u32 bg, u32 code,
|
||||||
|
const struct usb_ctl_info *usb, u32 usb_count) __attribute__((unused));
|
||||||
|
static void fb_diag_show_usb(const struct dkm_kernel_api *api, u32 bg, u32 code,
|
||||||
|
const struct usb_ctl_info *usb, u32 usb_count) {
|
||||||
|
if (!api || !api->fb_address || !api->fb_width || !api->fb_height)
|
||||||
|
return;
|
||||||
|
u32 *fb = (u32 *)api->fb_address;
|
||||||
|
u64 pitch = api->fb_pitch;
|
||||||
|
u64 w = api->fb_width, h = api->fb_height;
|
||||||
|
for (u64 y = 0; y < h; y++) {
|
||||||
|
u32 *line = (u32 *)((u8 *)fb + y * pitch);
|
||||||
|
for (u64 x = 0; x < w; x++) line[x] = bg;
|
||||||
|
}
|
||||||
|
u32 fg = 0xFFFFFFFFu;
|
||||||
|
/* 标题大字(scale 6,两位居中,固定顶部) */
|
||||||
|
int s = 6, cw = 8 * s;
|
||||||
|
int cx = (int)(w / 2) - cw;
|
||||||
|
int cy = 8;
|
||||||
|
if (code >= 0x10) fb_draw_char(fb, pitch, cx, cy, s, fg, (int)(code >> 4));
|
||||||
|
fb_draw_char(fb, pitch, cx + cw, cy, s, fg, (int)(code & 0xF));
|
||||||
|
/* 信息行(scale 2,最多 16 行):
|
||||||
|
* 左:bus(1)+dev(1)+devid(4)=6 hex;右:class(2)+progif(2)=4 hex */
|
||||||
|
int s2 = 2, cw2 = 8 * s2;
|
||||||
|
int row_y = cy + 8 * s + 8;
|
||||||
|
int total = (6 + 1 + 4) * cw2; /* 176px */
|
||||||
|
int xl = (int)(w / 2) - total / 2;
|
||||||
|
int xr = xl + (6 + 1) * cw2;
|
||||||
|
u32 max_rows = usb_count < 16 ? usb_count : 16;
|
||||||
|
for (u32 i = 0; i < max_rows; i++) {
|
||||||
|
u32 bd = ((usb[i].bus & 0xFF) << 4) | (usb[i].dev & 0x1F);
|
||||||
|
fb_draw_hexn(fb, pitch, xl, row_y, s2, fg, bd, 2);
|
||||||
|
fb_draw_hexn(fb, pitch, xl + 2 * cw2, row_y, s2, fg, usb[i].devid & 0xFFFF, 4);
|
||||||
|
fb_draw_hexn(fb, pitch, xr, row_y, s2, fg, usb[i].cls & 0xFFFF, 2);
|
||||||
|
fb_draw_hexn(fb, pitch, xr + 2 * cw2, row_y, s2, fg, usb[i].progif & 0xFFFF, 2);
|
||||||
|
row_y += 8 * s2 + 4; /* 20px/行 */
|
||||||
|
}
|
||||||
|
for (;;) {
|
||||||
|
__asm__ volatile("cli; hlt");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 红底 = 基础设施失败,halt(0xFFFF0000 在 BGRA 下是纯红;
|
||||||
|
* 原 0xFF0000FF 实际渲染为蓝——0x01 那次实机测试显示“蓝底”即此原因) */
|
||||||
|
#define FB_FATAL(api, code) fb_diag_show((api), 0xFFFF0000u, (code))
|
||||||
|
/* 红底 PCI 自检:标题码 + USB 控制器清单 */
|
||||||
|
#define FB_FATAL_show_usb(api, code) \
|
||||||
|
fb_diag_show_usb((api), 0xFFFF0000u, (code), g_diag_usb, g_diag_usb_count)
|
||||||
|
/* 黄底 = 枚举失败,halt */
|
||||||
|
#define FB_ENUM_FAIL(code) fb_diag_show(g_api, 0xFFFFC800u, (code))
|
||||||
|
|
||||||
static void xhci_zero(void *ptr, u32 len) {
|
static void xhci_zero(void *ptr, u32 len) {
|
||||||
u8 *p = (u8 *)ptr;
|
u8 *p = (u8 *)ptr;
|
||||||
for (u32 i = 0; i < len; i++) p[i] = 0;
|
for (u32 i = 0; i < len; i++) p[i] = 0;
|
||||||
@@ -321,6 +472,96 @@ static __inline__ void xhci_mb(void) {
|
|||||||
* Phase 1: PCI discovery + MMIO mapping
|
* Phase 1: PCI discovery + MMIO mapping
|
||||||
* ================================================================ */
|
* ================================================================ */
|
||||||
|
|
||||||
|
/* PCI 自检:区分“传统 0xCF8/0xCFC 配置机制失效” vs “确实无 xHCI 控制器”。
|
||||||
|
* 实机现象:xhci_find 全扫描失败(诊断码 0x01)——USB3 主板必然有 xHCI,
|
||||||
|
* 扫不到只可能是 (a) 配置机制读不到任何设备 (b) 设备在异常位置 (c) 控制器
|
||||||
|
* 是 EHCI 等非 xHCI。返回细分诊断码:
|
||||||
|
* 0x11 bus0/dev0/func0 vendor=0xFFFF → 0xCF8/0xCFC 机制失效
|
||||||
|
* 0x12 机制可用但全扫描无任何 USB 控制器 (class 0x0C/03)
|
||||||
|
* 0x13 有 USB 控制器但无 prog_if=0x30(全部 EHCI/UHCI/OHCI)
|
||||||
|
* 0x14 存在 prog_if=0x30 的 xHCI 但 xhci_find 漏匹配(防御性,理论不可能)
|
||||||
|
* 同时把扫描到的设备填入 g_diag_usb 供屏幕显示——记录策略:
|
||||||
|
* 第一遍收集全部 USB 控制器(class 0C/03,含 bus1+ 第三方芯片),
|
||||||
|
* 第二遍收集其他设备(bus0 优先),总上限 16 行。
|
||||||
|
* 关键 device ID 判读:Intel 7系 xHCI=1e31 / EHCI1=1e2d / EHCI2=1e26;
|
||||||
|
* Intel 6系 EHCI1=1c2d / EHCI2=1c26(H61 仅 1 个);ASMedia=1b21;
|
||||||
|
* Renesas=1033。 */
|
||||||
|
static struct usb_ctl_info g_diag_usb[16];
|
||||||
|
static u32 g_diag_usb_count;
|
||||||
|
|
||||||
|
#define DIAG_MAX 16
|
||||||
|
|
||||||
|
static void diag_add(u32 bus, u32 dev, u32 devid, u32 cls, u32 progif) {
|
||||||
|
if (g_diag_usb_count >= DIAG_MAX) return;
|
||||||
|
g_diag_usb[g_diag_usb_count].bus = bus;
|
||||||
|
g_diag_usb[g_diag_usb_count].dev = dev;
|
||||||
|
g_diag_usb[g_diag_usb_count].devid = devid;
|
||||||
|
g_diag_usb[g_diag_usb_count].cls = cls;
|
||||||
|
g_diag_usb[g_diag_usb_count].progif = progif;
|
||||||
|
g_diag_usb_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 诊断用,保留(无 xHCI 场景默认不调用,可通过临时改 driver_init 调试) */
|
||||||
|
static u32 xhci_pci_diag_code(void) __attribute__((unused));
|
||||||
|
static u32 xhci_pci_diag_code(void) {
|
||||||
|
u32 vd0 = dkm_pci_read(0, 0, 0, PCI_VENDOR_ID);
|
||||||
|
g_diag_usb_count = 0;
|
||||||
|
if ((vd0 & 0xffff) == 0xffff) return 0x11;
|
||||||
|
|
||||||
|
u32 usb_total = 0, xhci_total = 0;
|
||||||
|
|
||||||
|
/* 第一遍:USB 控制器(全 bus,含 bus1+ 第三方芯片) */
|
||||||
|
for (u16 bus = 0; bus < 256; bus++) {
|
||||||
|
for (u8 dev = 0; dev < 32; dev++) {
|
||||||
|
u32 vd = dkm_pci_read((u8)bus, dev, 0, PCI_VENDOR_ID);
|
||||||
|
if ((vd & 0xffff) == 0xffff) continue;
|
||||||
|
u8 header = (u8)(dkm_pci_read((u8)bus, dev, 0, PCI_HEADER) >> 16);
|
||||||
|
u8 func_count = (header & 0x80) ? 8 : 1;
|
||||||
|
for (u8 func = 0; func < func_count; func++) {
|
||||||
|
u32 vd2 = dkm_pci_read((u8)bus, dev, func, PCI_VENDOR_ID);
|
||||||
|
if ((vd2 & 0xffff) == 0xffff) continue;
|
||||||
|
u32 class_reg = dkm_pci_read((u8)bus, dev, func, 0x08);
|
||||||
|
u8 prog_if = (u8)((class_reg >> 8) & 0xff);
|
||||||
|
u8 subclass = (u8)((class_reg >> 16) & 0xff);
|
||||||
|
u8 class_code = (u8)((class_reg >> 24) & 0xff);
|
||||||
|
if (class_code == 0x0C && subclass == 0x03) {
|
||||||
|
usb_total++;
|
||||||
|
if (prog_if == 0x30) xhci_total++;
|
||||||
|
diag_add(bus, dev, vd2 >> 16,
|
||||||
|
(u32)((class_code << 8) | subclass), prog_if);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 第二遍:其他设备补位(bus0 优先) */
|
||||||
|
if (g_diag_usb_count < DIAG_MAX) {
|
||||||
|
for (u16 bus = 0; bus < 256 && g_diag_usb_count < DIAG_MAX; bus++) {
|
||||||
|
for (u8 dev = 0; dev < 32 && g_diag_usb_count < DIAG_MAX; dev++) {
|
||||||
|
u32 vd = dkm_pci_read((u8)bus, dev, 0, PCI_VENDOR_ID);
|
||||||
|
if ((vd & 0xffff) == 0xffff) continue;
|
||||||
|
u8 header = (u8)(dkm_pci_read((u8)bus, dev, 0, PCI_HEADER) >> 16);
|
||||||
|
u8 func_count = (header & 0x80) ? 8 : 1;
|
||||||
|
for (u8 func = 0; func < func_count && g_diag_usb_count < DIAG_MAX; func++) {
|
||||||
|
u32 vd2 = dkm_pci_read((u8)bus, dev, func, PCI_VENDOR_ID);
|
||||||
|
if ((vd2 & 0xffff) == 0xffff) continue;
|
||||||
|
u32 class_reg = dkm_pci_read((u8)bus, dev, func, 0x08);
|
||||||
|
u8 prog_if = (u8)((class_reg >> 8) & 0xff);
|
||||||
|
u8 subclass = (u8)((class_reg >> 16) & 0xff);
|
||||||
|
u8 class_code = (u8)((class_reg >> 24) & 0xff);
|
||||||
|
if (class_code == 0x0C && subclass == 0x03) continue; /* 已收集 */
|
||||||
|
diag_add(bus, dev, vd2 >> 16,
|
||||||
|
(u32)((class_code << 8) | subclass), prog_if);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usb_total == 0) return 0x12;
|
||||||
|
if (xhci_total > 0) return 0x14;
|
||||||
|
return 0x13;
|
||||||
|
}
|
||||||
|
|
||||||
static int xhci_find(u8 *out_bus, u8 *out_dev, u8 *out_func) {
|
static int xhci_find(u8 *out_bus, u8 *out_dev, u8 *out_func) {
|
||||||
for (u16 bus = 0; bus < 256; bus++) {
|
for (u16 bus = 0; bus < 256; bus++) {
|
||||||
for (u8 dev = 0; dev < 32; dev++) {
|
for (u8 dev = 0; dev < 32; dev++) {
|
||||||
@@ -518,9 +759,18 @@ static int xhci_parse_caps(void) {
|
|||||||
g_doorbell_off = mmio_read32(XHCI_CAP_DBOFF) & 0xFFFFFFF0u;
|
g_doorbell_off = mmio_read32(XHCI_CAP_DBOFF) & 0xFFFFFFF0u;
|
||||||
g_runtime_off = mmio_read32(XHCI_CAP_RTSOFF) & 0xFFFFFFE0u;
|
g_runtime_off = mmio_read32(XHCI_CAP_RTSOFF) & 0xFFFFFFE0u;
|
||||||
|
|
||||||
/* Page size */
|
/* Page size: PAGESIZE 寄存器 bit[n] 置位表示支持 2^(n+12) 字节页。
|
||||||
|
* 取置位的最低 bit(xHCI 规范要求软件使用最小支持页)。
|
||||||
|
* 原实现 (ps_reg & 0xF) 语义错误,实机多 bit 置位时会得到错误页大小。 */
|
||||||
u32 ps_reg = mmio_read32(g_cap_length + XHCI_OP_PAGESIZE);
|
u32 ps_reg = mmio_read32(g_cap_length + XHCI_OP_PAGESIZE);
|
||||||
g_page_size = 1u << ((ps_reg & 0xF) + 12);
|
u32 ps_bits = ps_reg & 0xFFFFu;
|
||||||
|
u32 ps_shift = 0;
|
||||||
|
if (ps_bits) {
|
||||||
|
while (!(ps_bits & 1u)) { ps_bits >>= 1; ps_shift++; }
|
||||||
|
g_page_size = 4096u << ps_shift;
|
||||||
|
} else {
|
||||||
|
g_page_size = 4096;
|
||||||
|
}
|
||||||
|
|
||||||
log_hex("[xhci] CAPLEN=", g_cap_length);
|
log_hex("[xhci] CAPLEN=", g_cap_length);
|
||||||
log_hex("[xhci] MaxSlots=", g_max_slots);
|
log_hex("[xhci] MaxSlots=", g_max_slots);
|
||||||
@@ -591,9 +841,13 @@ static u32 xhci_ring_enqueue(xhci_ring *ring, u64 param, u32 status, u32 control
|
|||||||
return idx;
|
return idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ring a doorbell: slot_id=0 for command, otherwise endpoint doorbell */
|
/* Ring a doorbell: slot_id=0 for command, otherwise endpoint doorbell。
|
||||||
|
* Doorbell Registers 每个 4 字节(xHCI 1.2 §5.6):offset = slot_id * 4。
|
||||||
|
* BUG-FIX: 原实现 slot_id * 0x20 偏移错误——QEMU 门铃区 reg>>=2 后
|
||||||
|
* reg 即 slot_id,0x20 偏移会敲到 slot 8,该 slot 无 EP → 控制器不
|
||||||
|
* 处理 TRB → 无 Transfer Event → 轮询超时。 */
|
||||||
static void xhci_ring_doorbell(u32 slot_id, u32 target) {
|
static void xhci_ring_doorbell(u32 slot_id, u32 target) {
|
||||||
mmio_write32(g_doorbell_off + slot_id * 0x20, target);
|
mmio_write32(g_doorbell_off + slot_id * 4, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ================================================================
|
/* ================================================================
|
||||||
@@ -627,9 +881,9 @@ static int xhci_poll_event(u32 expected_type, u64 timeout_ms,
|
|||||||
g_evt_ring.enqueue = (g_evt_ring.enqueue + 1) % (g_evt_ring.size - 1);
|
g_evt_ring.enqueue = (g_evt_ring.enqueue + 1) % (g_evt_ring.size - 1);
|
||||||
if (g_evt_ring.enqueue == 0) g_evt_ccs ^= 1;
|
if (g_evt_ring.enqueue == 0) g_evt_ccs ^= 1;
|
||||||
|
|
||||||
/* Update ERDP */
|
/* Update ERDP: 地址 16 字节对齐,bit0 保持 0,bit3=EHB 清挂起 */
|
||||||
u64 erdp = g_evt_ring.dma.phys + (u64)g_evt_ring.enqueue * sizeof(xhci_trb);
|
u64 erdp = g_evt_ring.dma.phys + (u64)g_evt_ring.enqueue * sizeof(xhci_trb);
|
||||||
mmio_write64(g_runtime_off + XHCI_RT_IR0_ERDP_LO, erdp | g_evt_ccs);
|
mmio_write64(g_runtime_off + XHCI_RT_IR0_ERDP_LO, erdp | ERDP_EHB);
|
||||||
|
|
||||||
/* Check completion code */
|
/* Check completion code */
|
||||||
u32 cc = (evt->status >> 24) & 0xFF;
|
u32 cc = (evt->status >> 24) & 0xFF;
|
||||||
@@ -659,7 +913,7 @@ static int xhci_poll_event(u32 expected_type, u64 timeout_ms,
|
|||||||
g_evt_ring.enqueue = (g_evt_ring.enqueue + 1) % (g_evt_ring.size - 1);
|
g_evt_ring.enqueue = (g_evt_ring.enqueue + 1) % (g_evt_ring.size - 1);
|
||||||
if (g_evt_ring.enqueue == 0) g_evt_ccs ^= 1;
|
if (g_evt_ring.enqueue == 0) g_evt_ccs ^= 1;
|
||||||
u64 erdp2 = g_evt_ring.dma.phys + (u64)g_evt_ring.enqueue * sizeof(xhci_trb);
|
u64 erdp2 = g_evt_ring.dma.phys + (u64)g_evt_ring.enqueue * sizeof(xhci_trb);
|
||||||
mmio_write64(g_runtime_off + XHCI_RT_IR0_ERDP_LO, erdp2 | g_evt_ccs);
|
mmio_write64(g_runtime_off + XHCI_RT_IR0_ERDP_LO, erdp2 | ERDP_EHB);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -677,7 +931,7 @@ static int xhci_poll_event(u32 expected_type, u64 timeout_ms,
|
|||||||
|
|
||||||
static int xhci_cmd_submit(u64 param, u32 status, u32 control,
|
static int xhci_cmd_submit(u64 param, u32 status, u32 control,
|
||||||
u64 timeout_ms,
|
u64 timeout_ms,
|
||||||
u64 *out_param, u32 *out_status) {
|
u64 *out_param, u32 *out_status, u32 *out_control) {
|
||||||
/* Clear Event Ring interrupt pending */
|
/* Clear Event Ring interrupt pending */
|
||||||
mmio_write32(g_runtime_off + XHCI_RT_IR0_IMAN,
|
mmio_write32(g_runtime_off + XHCI_RT_IR0_IMAN,
|
||||||
mmio_read32(g_runtime_off + XHCI_RT_IR0_IMAN) | 2u);
|
mmio_read32(g_runtime_off + XHCI_RT_IR0_IMAN) | 2u);
|
||||||
@@ -705,7 +959,7 @@ static int xhci_cmd_submit(u64 param, u32 status, u32 control,
|
|||||||
}
|
}
|
||||||
|
|
||||||
int rc = xhci_poll_event(EVT_TYPE_CMD_COMPLETE, timeout_ms,
|
int rc = xhci_poll_event(EVT_TYPE_CMD_COMPLETE, timeout_ms,
|
||||||
out_param, out_status, 0);
|
out_param, out_status, out_control);
|
||||||
|
|
||||||
/* Clear EINT */
|
/* Clear EINT */
|
||||||
mmio_write32(g_cap_length + XHCI_OP_USBSTS,
|
mmio_write32(g_cap_length + XHCI_OP_USBSTS,
|
||||||
@@ -810,8 +1064,8 @@ static int xhci_controller_start(void) {
|
|||||||
/* Configure Event Ring */
|
/* Configure Event Ring */
|
||||||
mmio_write32(g_runtime_off + XHCI_RT_IR0_ERSTSZ, 1);
|
mmio_write32(g_runtime_off + XHCI_RT_IR0_ERSTSZ, 1);
|
||||||
mmio_write64(g_runtime_off + XHCI_RT_IR0_ERSTBA_LO, g_erst.phys);
|
mmio_write64(g_runtime_off + XHCI_RT_IR0_ERSTBA_LO, g_erst.phys);
|
||||||
u64 erdp = g_evt_ring.dma.phys | (u64)g_evt_ccs;
|
u64 erdp = g_evt_ring.dma.phys;
|
||||||
mmio_write64(g_runtime_off + XHCI_RT_IR0_ERDP_LO, erdp);
|
mmio_write64(g_runtime_off + XHCI_RT_IR0_ERDP_LO, erdp | ERDP_EHB);
|
||||||
|
|
||||||
/* Enable interrupt (IMAN.IE=1) */
|
/* Enable interrupt (IMAN.IE=1) */
|
||||||
mmio_write32(g_runtime_off + XHCI_RT_IR0_IMAN,
|
mmio_write32(g_runtime_off + XHCI_RT_IR0_IMAN,
|
||||||
@@ -883,7 +1137,7 @@ static int xhci_reset_port(u32 port_idx) {
|
|||||||
g_evt_ring.enqueue = (g_evt_ring.enqueue + 1) % (g_evt_ring.size - 1);
|
g_evt_ring.enqueue = (g_evt_ring.enqueue + 1) % (g_evt_ring.size - 1);
|
||||||
if (g_evt_ring.enqueue == 0) g_evt_ccs ^= 1;
|
if (g_evt_ring.enqueue == 0) g_evt_ccs ^= 1;
|
||||||
u64 erdp = g_evt_ring.dma.phys + (u64)g_evt_ring.enqueue * sizeof(xhci_trb);
|
u64 erdp = g_evt_ring.dma.phys + (u64)g_evt_ring.enqueue * sizeof(xhci_trb);
|
||||||
mmio_write64(g_runtime_off + XHCI_RT_IR0_ERDP_LO, erdp | g_evt_ccs);
|
mmio_write64(g_runtime_off + XHCI_RT_IR0_ERDP_LO, erdp | ERDP_EHB);
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
@@ -911,15 +1165,18 @@ static int xhci_enable_slot(u8 *out_slot_id) {
|
|||||||
u32 control = (TRB_TYPE_ENABLE_SLOT << TRB_TYPE_SHIFT);
|
u32 control = (TRB_TYPE_ENABLE_SLOT << TRB_TYPE_SHIFT);
|
||||||
u64 out_param = 0;
|
u64 out_param = 0;
|
||||||
u32 out_status = 0;
|
u32 out_status = 0;
|
||||||
|
u32 out_control = 0;
|
||||||
|
|
||||||
int rc = xhci_cmd_submit(0, 0, control, 5000, &out_param, &out_status);
|
int rc = xhci_cmd_submit(0, 0, control, 5000, &out_param, &out_status, &out_control);
|
||||||
if (rc != 0) {
|
if (rc != 0) {
|
||||||
g_log->error("[xhci] Enable Slot failed");
|
g_log->error("[xhci] Enable Slot failed");
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Slot ID is in the upper bits of the completion event parameter */
|
/* Slot ID 位于 Command Completion Event 的 TRB control[31:24]。
|
||||||
*out_slot_id = (u8)((out_param >> 24) & 0xFF);
|
* (xHCI 1.2 §6.4.2.2:CMD_COMPLETE 事件 TRB parameter = 命令 TRB 地址,
|
||||||
|
* slot id 在 control 高字节 —— QEMU hcd-xhci.c xhci_write_event 佐证) */
|
||||||
|
*out_slot_id = (u8)((out_control >> 24) & 0xFF);
|
||||||
log_hex("[xhci] slot_id=", *out_slot_id);
|
log_hex("[xhci] slot_id=", *out_slot_id);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -940,7 +1197,8 @@ static int xhci_address_device(u8 slot_id, int bsr) {
|
|||||||
|
|
||||||
u64 out_param = 0;
|
u64 out_param = 0;
|
||||||
u32 out_status = 0;
|
u32 out_status = 0;
|
||||||
int rc = xhci_cmd_submit(input_ctx_phys, 0, control, 5000, &out_param, &out_status);
|
u32 out_control = 0;
|
||||||
|
int rc = xhci_cmd_submit(input_ctx_phys, 0, control, 5000, &out_param, &out_status, &out_control);
|
||||||
if (rc != 0) {
|
if (rc != 0) {
|
||||||
g_log->error("[xhci] Address Device failed");
|
g_log->error("[xhci] Address Device failed");
|
||||||
return rc;
|
return rc;
|
||||||
@@ -958,7 +1216,8 @@ static int xhci_configure_endpoint(u8 slot_id) {
|
|||||||
|
|
||||||
u64 out_param = 0;
|
u64 out_param = 0;
|
||||||
u32 out_status = 0;
|
u32 out_status = 0;
|
||||||
int rc = xhci_cmd_submit(input_ctx_phys, 0, control, 5000, &out_param, &out_status);
|
u32 out_control = 0;
|
||||||
|
int rc = xhci_cmd_submit(input_ctx_phys, 0, control, 5000, &out_param, &out_status, &out_control);
|
||||||
if (rc != 0) {
|
if (rc != 0) {
|
||||||
g_log->error("[xhci] Configure Endpoint failed");
|
g_log->error("[xhci] Configure Endpoint failed");
|
||||||
return rc;
|
return rc;
|
||||||
@@ -979,7 +1238,16 @@ static int xhci_control_transfer(const u8 *setup_pkt, u8 *data, u32 data_len,
|
|||||||
((u64)setup_pkt[2] << 16) | ((u64)setup_pkt[3] << 24);
|
((u64)setup_pkt[2] << 16) | ((u64)setup_pkt[3] << 24);
|
||||||
|
|
||||||
u32 setup_status = 8; /* Setup packet length */
|
u32 setup_status = 8; /* Setup packet length */
|
||||||
u32 setup_control = (TRB_TYPE_SETUP_STAGE << TRB_TYPE_SHIFT) | TRB_IOC;
|
/* Setup Stage TRB(不设 IOC:控制传输只需在最后一个 TRB 上报完成,
|
||||||
|
* 中间 TRB 设 IOC 会让 QEMU/实机产生多余 Transfer Event,残留事件
|
||||||
|
* 污染后续 CMD_COMPLETE 轮询——日志表现为 "unexpected event type") */
|
||||||
|
u32 setup_control = (TRB_TYPE_SETUP_STAGE << TRB_TYPE_SHIFT);
|
||||||
|
/* Setup Stage TRB 必须设置 IDT (Immediate Data, bit6):
|
||||||
|
* setup 包 8 字节直接内嵌在 TRB parameter 字段。
|
||||||
|
* BUG-FIX: 原实现缺 IDT,QEMU xhci_fire_ctl_transfer 校验
|
||||||
|
* `!(trb_setup->control & TRB_TR_IDT)` 直接返回 -1 → 无 Transfer
|
||||||
|
* Event → 轮询超时(GET_DESCRIPTOR 失败)。 */
|
||||||
|
setup_control |= (1u << 6); /* TRB_TR_IDT */
|
||||||
/* Set direction in TRB: bit 16 of Setup Stage TRB */
|
/* Set direction in TRB: bit 16 of Setup Stage TRB */
|
||||||
if (direction) setup_control |= (1u << 16);
|
if (direction) setup_control |= (1u << 16);
|
||||||
|
|
||||||
@@ -995,7 +1263,8 @@ static int xhci_control_transfer(const u8 *setup_pkt, u8 *data, u32 data_len,
|
|||||||
|
|
||||||
u64 data_phys = direction ? g_xfer_buf.phys : g_xfer_buf.phys;
|
u64 data_phys = direction ? g_xfer_buf.phys : g_xfer_buf.phys;
|
||||||
u32 data_status = data_len;
|
u32 data_status = data_len;
|
||||||
u32 data_control = (TRB_TYPE_DATA_STAGE << TRB_TYPE_SHIFT) | TRB_IOC;
|
/* Data Stage 不设 IOC(避免多余事件残留) */
|
||||||
|
u32 data_control = (TRB_TYPE_DATA_STAGE << TRB_TYPE_SHIFT);
|
||||||
if (direction) data_control |= (1u << 16); /* DIR=IN */
|
if (direction) data_control |= (1u << 16); /* DIR=IN */
|
||||||
|
|
||||||
xhci_ring_enqueue(&g_ep0_ring, data_phys, data_status, data_control);
|
xhci_ring_enqueue(&g_ep0_ring, data_phys, data_status, data_control);
|
||||||
@@ -1088,7 +1357,11 @@ static int xhci_bulk_transfer(xhci_ring *ring, u8 ep_num,
|
|||||||
xhci_ring_enqueue(ring, g_xfer_buf.phys, data_len, control);
|
xhci_ring_enqueue(ring, g_xfer_buf.phys, data_len, control);
|
||||||
|
|
||||||
xhci_mb();
|
xhci_mb();
|
||||||
xhci_ring_doorbell(g_msc.slot_id, ep_num);
|
/* Doorbell target = Endpoint ID:EPn OUT→2n,EPn IN→2n+1。
|
||||||
|
* BUG-FIX: 原实现传 EP 号(2/1),QEMU xhci_kick_ep 按 Endpoint ID
|
||||||
|
* 索引 eps[epid-1] → 敲错端点 → 无 Transfer Event → 轮询超时。 */
|
||||||
|
u8 db_target = (direction ? (u8)(2u * ep_num + 1) : (u8)(2u * ep_num));
|
||||||
|
xhci_ring_doorbell(g_msc.slot_id, db_target);
|
||||||
|
|
||||||
/* Poll for Transfer Event */
|
/* Poll for Transfer Event */
|
||||||
u64 out_param = 0;
|
u64 out_param = 0;
|
||||||
@@ -1097,6 +1370,7 @@ static int xhci_bulk_transfer(xhci_ring *ring, u8 ep_num,
|
|||||||
&out_param, &out_status, 0);
|
&out_param, &out_status, 0);
|
||||||
if (rc != 0) {
|
if (rc != 0) {
|
||||||
g_log->warn("[xhci] bulk transfer failed");
|
g_log->warn("[xhci] bulk transfer failed");
|
||||||
|
log_hex("[xhci] bulk rc=", (u64)(i64)rc);
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1343,32 +1617,48 @@ static int xhci_block_write(void *ctx, u64 lba, u32 count, const void *buffer) {
|
|||||||
* Phase 3: Build Input Context for Address Device
|
* Phase 3: Build Input Context for Address Device
|
||||||
* ================================================================ */
|
* ================================================================ */
|
||||||
|
|
||||||
static void xhci_build_input_ctx_slot(u8 slot_id, u8 speed, u16 mps0) {
|
static void xhci_build_input_ctx_slot(u8 slot_id, u8 speed, u16 mps0, u32 port_idx) {
|
||||||
(void)slot_id;
|
(void)slot_id;
|
||||||
u32 ctx_sz = g_ctx_size;
|
u32 ctx_sz = g_ctx_size;
|
||||||
u8 *ic = (u8 *)g_input_ctx.virt;
|
u8 *ic = (u8 *)g_input_ctx.virt;
|
||||||
xhci_zero(ic, g_input_ctx.size);
|
xhci_zero(ic, g_input_ctx.size);
|
||||||
|
|
||||||
/* Input Control Context: enable Slot Context (bit 0) and EP0 (bit 1) */
|
/* Input Control Context (xHCI 1.2 §6.4.3):
|
||||||
|
* DWORD0 = Drop Context Flags(本次全 0,不 drop 任何上下文)
|
||||||
|
* DWORD1 = Add Context Flags(bit0=Slot, bit1=EP0)
|
||||||
|
* BUG-FIX: 原实现把 Add flags 写进 DWORD0(Drop 槽),
|
||||||
|
* QEMU/实机校验 ictl_ctx[0]==0 && ictl_ctx[1]==0x3 直接失败 CC_TRB_ERROR。 */
|
||||||
u32 *icc = (u32 *)ic;
|
u32 *icc = (u32 *)ic;
|
||||||
icc[0] = (1u << 1) | (1u << 0); /* A0=1 (Slot), A1=1 (EP0) */
|
icc[0] = 0x0; /* Drop Context Flags */
|
||||||
|
icc[1] = (1u << 0) | (1u << 1); /* Add: Slot + EP0 */
|
||||||
|
|
||||||
/* Slot Context starts at offset ctx_sz */
|
/* Slot Context (§6.2.2):
|
||||||
|
* DWORD0: Route String [0:19] + Speed [20:22] + Context Entries [27:31]
|
||||||
|
* DWORD1: Max Exit Latency [0:7] + Root Hub Port Number [16:23]
|
||||||
|
* BUG-FIX: 原实现 speed<<29(占用 Context Entries 位域)、MPS0 写入
|
||||||
|
* DWORD1(规范 Slot Context 无 MPS0 字段,MPS0 在 EP0 Context)——
|
||||||
|
* QEMU xhci_lookup_uport 读 DWORD1[16:23] 找端口,原值 port=0 查不到
|
||||||
|
* 设备 → CC_TRB_ERROR。 */
|
||||||
u32 *slot_ctx = (u32 *)(ic + ctx_sz);
|
u32 *slot_ctx = (u32 *)(ic + ctx_sz);
|
||||||
u32 route_string = 0; /* direct-connect, no hub */
|
u32 route_string = 0; /* direct-connect, no hub */
|
||||||
slot_ctx[0] = route_string | ((u32)speed << 29);
|
slot_ctx[0] = route_string | ((u32)speed << 20) | (1u << 27);
|
||||||
slot_ctx[1] = (u32)mps0; /* Max Packet Size for EP0 */
|
slot_ctx[1] = (port_idx + 1) << 16; /* Root Hub Port Number (1-based) */
|
||||||
slot_ctx[2] = (1u << 27); /* Context Entries = 1 (only EP0) */
|
|
||||||
|
|
||||||
/* EP0 Context starts at offset ctx_sz * 2 */
|
/* EP0 Context starts at offset ctx_sz * 2
|
||||||
|
* Endpoint Context (§6.2.3) 布局:
|
||||||
|
* DWORD1: CErr[4:5] + EP Type[6:8] + Max Packet Size[16:31]
|
||||||
|
* DWORD2: Dequeue Pointer Low[4:31](bit0=DCS, bits1:3=reserved)
|
||||||
|
* DWORD3: Dequeue Pointer High
|
||||||
|
* DWORD4: Average TRB Length[0:15]
|
||||||
|
* BUG-FIX: 原实现把 dequeue 写到 DWORD6/7——QEMU xhci_init_epctx
|
||||||
|
* 从 DWORD2/3 读 dequeue(ctx[2]&~0xf, ctx[3]),读到 0 → ring 无
|
||||||
|
* dequeue → kick_epctx 直接返回,TRB 永不处理 → 无 Transfer Event。 */
|
||||||
u32 *ep0_ctx = (u32 *)(ic + ctx_sz * 2);
|
u32 *ep0_ctx = (u32 *)(ic + ctx_sz * 2);
|
||||||
/* EP Type = Control, Max Packet Size, etc. */
|
|
||||||
ep0_ctx[1] = (4u << 3) | ((u32)mps0 << 16); /* EP Type=4 (Control IN), MPS */
|
ep0_ctx[1] = (4u << 3) | ((u32)mps0 << 16); /* EP Type=4 (Control IN), MPS */
|
||||||
ep0_ctx[4] = 8; /* Average TRB Length */
|
|
||||||
/* Dequeue Pointer: EP0 transfer ring */
|
|
||||||
u64 ep0_deq = g_ep0_ring.dma.phys | (u64)g_ep0_ring.ccs;
|
u64 ep0_deq = g_ep0_ring.dma.phys | (u64)g_ep0_ring.ccs;
|
||||||
ep0_ctx[6] = (u32)(ep0_deq & 0xFFFFFFFFu);
|
ep0_ctx[2] = (u32)(ep0_deq & 0xFFFFFFF0u) | (u32)g_ep0_ring.ccs;
|
||||||
ep0_ctx[7] = (u32)(ep0_deq >> 32);
|
ep0_ctx[3] = (u32)(ep0_deq >> 32);
|
||||||
|
ep0_ctx[4] = 8; /* Average TRB Length */
|
||||||
}
|
}
|
||||||
|
|
||||||
static void xhci_build_input_ctx_bulk_eps(u8 bulk_out_ep, u8 bulk_in_ep,
|
static void xhci_build_input_ctx_bulk_eps(u8 bulk_out_ep, u8 bulk_in_ep,
|
||||||
@@ -1376,31 +1666,45 @@ static void xhci_build_input_ctx_bulk_eps(u8 bulk_out_ep, u8 bulk_in_ep,
|
|||||||
u32 ctx_sz = g_ctx_size;
|
u32 ctx_sz = g_ctx_size;
|
||||||
u8 *ic = (u8 *)g_input_ctx.virt;
|
u8 *ic = (u8 *)g_input_ctx.virt;
|
||||||
|
|
||||||
/* Update Input Control Context: enable EP0, Bulk OUT, Bulk IN */
|
/* Input Control Context(Configure Endpoint 语义,xHCI 1.2 §6.4.3):
|
||||||
|
* DWORD0 = Drop(0),DWORD1 = Add。
|
||||||
|
* Configure 时 bit0=Slot 必须 Add,bit1=EP0 不可重复 Add
|
||||||
|
* (QEMU xhci_configure_slot 校验 (ictl[1]&0x3)==0x1)。
|
||||||
|
* 端点位用 Context Index:EPn OUT→2n,EPn IN→2n+1
|
||||||
|
* (QEMU 从 i=2 开始遍历 Add flags,按 ictx+32*i 读 EP Context)。 */
|
||||||
u32 *icc = (u32 *)ic;
|
u32 *icc = (u32 *)ic;
|
||||||
icc[0] = (1u << 1) | (1u << 0) |
|
u32 out_idx = 2u * bulk_out_ep;
|
||||||
(1u << bulk_out_ep) | (1u << bulk_in_ep);
|
u32 in_idx = 2u * bulk_in_ep + 1;
|
||||||
|
icc[0] = 0x0;
|
||||||
|
icc[1] = (1u << 0) | (1u << out_idx) | (1u << in_idx);
|
||||||
|
|
||||||
/* Update Slot Context: Context Entries = max(bulk_in_ep, bulk_out_ep) */
|
/* Update Slot Context: Context Entries = max Context Index
|
||||||
|
* (DWORD0 bits[27:31],保留 Speed/Route 原值)。 */
|
||||||
u32 *slot_ctx = (u32 *)(ic + ctx_sz);
|
u32 *slot_ctx = (u32 *)(ic + ctx_sz);
|
||||||
u32 max_ep = bulk_out_ep > bulk_in_ep ? bulk_out_ep : bulk_in_ep;
|
u32 max_idx = out_idx > in_idx ? out_idx : in_idx;
|
||||||
slot_ctx[2] = (max_ep << 27);
|
slot_ctx[0] &= ~(0x1Fu << 27);
|
||||||
|
slot_ctx[0] |= (max_idx << 27);
|
||||||
|
|
||||||
/* Bulk OUT Endpoint Context (at offset ctx_sz * (1 + bulk_out_ep)) */
|
/* Bulk OUT Endpoint Context。
|
||||||
u32 *out_ctx = (u32 *)(ic + ctx_sz * (1 + bulk_out_ep));
|
* 偏移 = ctx_sz * (out_idx + 1):input context 布局是
|
||||||
|
* [ICC][Slot][EP0][EP1]... 每个占 1 个 ctx_sz,Context Index i 的
|
||||||
|
* EP context 在 (i+1) 个槽处(QEMU xhci_configure_slot 用
|
||||||
|
* ictx+32+(32*i) 读取)。BUG-FIX: 原实现 ctx_sz*out_idx 少了 ICC
|
||||||
|
* 槽 → 全部错位 32 字节 → QEMU 读到零 EP context。 */
|
||||||
|
u32 *out_ctx = (u32 *)(ic + ctx_sz * (out_idx + 1));
|
||||||
out_ctx[1] = (EP_TYPE_BULK_OUT << 3) | ((u32)out_mps << 16);
|
out_ctx[1] = (EP_TYPE_BULK_OUT << 3) | ((u32)out_mps << 16);
|
||||||
out_ctx[4] = 512; /* Average TRB Length */
|
|
||||||
u64 out_deq = g_bulk_out_ring.dma.phys | (u64)g_bulk_out_ring.ccs;
|
u64 out_deq = g_bulk_out_ring.dma.phys | (u64)g_bulk_out_ring.ccs;
|
||||||
out_ctx[6] = (u32)(out_deq & 0xFFFFFFFFu);
|
out_ctx[2] = (u32)(out_deq & 0xFFFFFFF0u) | (u32)g_bulk_out_ring.ccs;
|
||||||
out_ctx[7] = (u32)(out_deq >> 32);
|
out_ctx[3] = (u32)(out_deq >> 32);
|
||||||
|
out_ctx[4] = 512; /* Average TRB Length */
|
||||||
|
|
||||||
/* Bulk IN Endpoint Context (at offset ctx_sz * (1 + bulk_in_ep)) */
|
/* Bulk IN Endpoint Context (at offset ctx_sz * (in_idx + 1)) */
|
||||||
u32 *in_ctx = (u32 *)(ic + ctx_sz * (1 + bulk_in_ep));
|
u32 *in_ctx = (u32 *)(ic + ctx_sz * (in_idx + 1));
|
||||||
in_ctx[1] = (EP_TYPE_BULK_IN << 3) | ((u32)in_mps << 16);
|
in_ctx[1] = (EP_TYPE_BULK_IN << 3) | ((u32)in_mps << 16);
|
||||||
in_ctx[4] = 512; /* Average TRB Length */
|
|
||||||
u64 in_deq = g_bulk_in_ring.dma.phys | (u64)g_bulk_in_ring.ccs;
|
u64 in_deq = g_bulk_in_ring.dma.phys | (u64)g_bulk_in_ring.ccs;
|
||||||
in_ctx[6] = (u32)(in_deq & 0xFFFFFFFFu);
|
in_ctx[2] = (u32)(in_deq & 0xFFFFFFF0u) | (u32)g_bulk_in_ring.ccs;
|
||||||
in_ctx[7] = (u32)(in_deq >> 32);
|
in_ctx[3] = (u32)(in_deq >> 32);
|
||||||
|
in_ctx[4] = 512; /* Average TRB Length */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ================================================================
|
/* ================================================================
|
||||||
@@ -1411,18 +1715,22 @@ static int xhci_enum_msc_device(u32 port_idx) {
|
|||||||
u8 speed = xhci_get_port_speed(port_idx);
|
u8 speed = xhci_get_port_speed(port_idx);
|
||||||
log_hex("[xhci] port speed=", speed);
|
log_hex("[xhci] port speed=", speed);
|
||||||
|
|
||||||
/* Default MaxPacketSize for EP0 based on speed */
|
/* Default MaxPacketSize for EP0 based on speed
|
||||||
u16 mps0 = 64; /* High/Super-Speed default */
|
* (xHCI 1.2 §4.3: Full=8? No — FS/HS EP0 通常 64,LS=8,SS=512) */
|
||||||
if (speed == USB_SPEED_FULL) mps0 = 64;
|
u16 mps0 = 64; /* Full/High-Speed default */
|
||||||
if (speed == USB_SPEED_LOW) mps0 = 8;
|
if (speed == USB_SPEED_LOW) mps0 = 8;
|
||||||
|
if (speed == USB_SPEED_SUPER) mps0 = 512;
|
||||||
|
|
||||||
/* Step 1: Enable Slot */
|
/* Step 1: Enable Slot */
|
||||||
u8 slot_id = 0;
|
u8 slot_id = 0;
|
||||||
if (xhci_enable_slot(&slot_id) != 0) return -1;
|
if (xhci_enable_slot(&slot_id) != 0) {
|
||||||
|
FB_ENUM_FAIL(0x0A); /* 黄: Enable Slot 失败 */
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
g_msc.slot_id = slot_id;
|
g_msc.slot_id = slot_id;
|
||||||
g_msc.speed = speed;
|
g_msc.speed = speed;
|
||||||
|
|
||||||
/* Step 2: Allocate per-device DMA buffers */
|
/* Step 1.5: Allocate per-device DMA buffers(在 DCBAA 指向输出上下文之前) */
|
||||||
if (g_api->dma->alloc_pages(4, 4096, 0x100000000ULL, &g_input_ctx) != 0) return -2;
|
if (g_api->dma->alloc_pages(4, 4096, 0x100000000ULL, &g_input_ctx) != 0) return -2;
|
||||||
if (g_api->dma->alloc_pages(4, 4096, 0x100000000ULL, &g_dev_ctx) != 0) return -2;
|
if (g_api->dma->alloc_pages(4, 4096, 0x100000000ULL, &g_dev_ctx) != 0) return -2;
|
||||||
if (g_api->dma->alloc_pages(4, 4096, 0x100000000ULL, &g_xfer_buf) != 0) return -2;
|
if (g_api->dma->alloc_pages(4, 4096, 0x100000000ULL, &g_xfer_buf) != 0) return -2;
|
||||||
@@ -1431,18 +1739,30 @@ static int xhci_enum_msc_device(u32 port_idx) {
|
|||||||
if (xhci_ring_init(&g_bulk_out_ring, XFER_RING_SIZE) != 0) return -2;
|
if (xhci_ring_init(&g_bulk_out_ring, XFER_RING_SIZE) != 0) return -2;
|
||||||
if (xhci_ring_init(&g_bulk_in_ring, XFER_RING_SIZE) != 0) return -2;
|
if (xhci_ring_init(&g_bulk_in_ring, XFER_RING_SIZE) != 0) return -2;
|
||||||
|
|
||||||
|
/* DCBAA[slot_id] 必须指向输出上下文(Device Context)。
|
||||||
|
* Address Device/Configure Endpoint 命令都会读取该地址;
|
||||||
|
* 缺失时 QEMU/实机将 octx=0 → DMA 写失败/CC_TRB_ERROR。 */
|
||||||
|
xhci_zero(g_dev_ctx.virt, g_dev_ctx.size);
|
||||||
|
u64 *dcbaa = (u64 *)g_dcbaa.virt;
|
||||||
|
dcbaa[slot_id] = g_dev_ctx.phys;
|
||||||
|
log_hex("[xhci] DCBAA[slot]=", dcbaa[slot_id]);
|
||||||
|
|
||||||
/* Step 3: Build Input Context and Address Device (BSR=1 first) */
|
/* Step 3: Build Input Context and Address Device (BSR=1 first) */
|
||||||
xhci_build_input_ctx_slot(slot_id, speed, mps0);
|
xhci_build_input_ctx_slot(slot_id, speed, mps0, port_idx);
|
||||||
if (xhci_address_device(slot_id, 1) != 0) {
|
if (xhci_address_device(slot_id, 1) != 0) {
|
||||||
/* BSR=1 failed, try without BSR */
|
/* BSR=1 failed, try without BSR */
|
||||||
xhci_build_input_ctx_slot(slot_id, speed, mps0);
|
xhci_build_input_ctx_slot(slot_id, speed, mps0, port_idx);
|
||||||
if (xhci_address_device(slot_id, 0) != 0) return -3;
|
if (xhci_address_device(slot_id, 0) != 0) {
|
||||||
|
FB_ENUM_FAIL(0x0B); /* 黄: Address Device 失败(BSR=1/0 均失败) */
|
||||||
|
return -3;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Step 4: Get Device Descriptor */
|
/* Step 4: Get Device Descriptor */
|
||||||
u8 dev_desc[18];
|
u8 dev_desc[18];
|
||||||
if (xhci_get_device_descriptor(dev_desc) != 0) {
|
if (xhci_get_device_descriptor(dev_desc) != 0) {
|
||||||
g_log->warn("[xhci] GET_DESCRIPTOR(Device) failed");
|
g_log->warn("[xhci] GET_DESCRIPTOR(Device) failed");
|
||||||
|
FB_ENUM_FAIL(0x0C); /* 黄: GET_DESCRIPTOR(Device) 失败 */
|
||||||
return -4;
|
return -4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1459,7 +1779,7 @@ static int xhci_enum_msc_device(u32 port_idx) {
|
|||||||
/* Step 5: Re-address with real MPS if different */
|
/* Step 5: Re-address with real MPS if different */
|
||||||
if (real_mps0 != mps0 && real_mps0 >= 8) {
|
if (real_mps0 != mps0 && real_mps0 >= 8) {
|
||||||
mps0 = real_mps0;
|
mps0 = real_mps0;
|
||||||
xhci_build_input_ctx_slot(slot_id, speed, mps0);
|
xhci_build_input_ctx_slot(slot_id, speed, mps0, port_idx);
|
||||||
if (xhci_address_device(slot_id, 0) != 0) {
|
if (xhci_address_device(slot_id, 0) != 0) {
|
||||||
g_log->warn("[xhci] re-address failed");
|
g_log->warn("[xhci] re-address failed");
|
||||||
/* Continue anyway — device may still work */
|
/* Continue anyway — device may still work */
|
||||||
@@ -1470,12 +1790,16 @@ static int xhci_enum_msc_device(u32 port_idx) {
|
|||||||
u8 cfg_desc[255];
|
u8 cfg_desc[255];
|
||||||
if (xhci_get_config_descriptor(cfg_desc, 255) != 0) {
|
if (xhci_get_config_descriptor(cfg_desc, 255) != 0) {
|
||||||
g_log->warn("[xhci] GET_DESCRIPTOR(Config) failed");
|
g_log->warn("[xhci] GET_DESCRIPTOR(Config) failed");
|
||||||
|
FB_ENUM_FAIL(0x0C); /* 黄: GET_DESCRIPTOR(Config) 失败 */
|
||||||
return -5;
|
return -5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Parse configuration descriptor to find MSC interface and endpoints */
|
/* Parse configuration descriptor to find MSC interface and endpoints */
|
||||||
u16 total_len = (u16)cfg_desc[2] | ((u16)cfg_desc[3] << 8);
|
u16 total_len = (u16)cfg_desc[2] | ((u16)cfg_desc[3] << 8);
|
||||||
if (total_len < 9) return -5;
|
if (total_len < 9) {
|
||||||
|
FB_ENUM_FAIL(0x0D); /* 黄: 配置描述符过短 */
|
||||||
|
return -5;
|
||||||
|
}
|
||||||
|
|
||||||
u8 bulk_out_ep = 0, bulk_in_ep = 0;
|
u8 bulk_out_ep = 0, bulk_in_ep = 0;
|
||||||
u16 bulk_out_mps = 512, bulk_in_mps = 512;
|
u16 bulk_out_mps = 512, bulk_in_mps = 512;
|
||||||
@@ -1497,24 +1821,34 @@ static int xhci_enum_msc_device(u32 port_idx) {
|
|||||||
msc_found = 1;
|
msc_found = 1;
|
||||||
g_log->info("[xhci] MSC BOT interface found");
|
g_log->info("[xhci] MSC BOT interface found");
|
||||||
|
|
||||||
/* Scan following endpoint descriptors */
|
/* Scan following endpoint descriptors.
|
||||||
|
* SuperSpeed 设备在 endpoint 后紧跟 6 字节 SS Endpoint
|
||||||
|
* Companion descriptor (type 0x30)——必须跳过继续找下一
|
||||||
|
* 个 endpoint,不能因 type!=5 提前终止(Bulk OUT 常在
|
||||||
|
* IN 之后且被 companion 隔开)。 */
|
||||||
u32 ep_pos = pos + desc_len;
|
u32 ep_pos = pos + desc_len;
|
||||||
while (ep_pos + 7 <= (u32)total_len &&
|
while (ep_pos + 2 <= (u32)total_len) {
|
||||||
cfg_desc[ep_pos + 1] == 5) { /* endpoint descriptor */
|
u8 ep_len = cfg_desc[ep_pos];
|
||||||
u8 ep_addr = cfg_desc[ep_pos + 2];
|
if (ep_len == 0 || ep_pos + ep_len > (u32)total_len) break;
|
||||||
u8 ep_attr = cfg_desc[ep_pos + 3];
|
if (cfg_desc[ep_pos + 1] == 5) { /* endpoint descriptor */
|
||||||
u16 ep_mps = (u16)cfg_desc[ep_pos + 4] | ((u16)cfg_desc[ep_pos + 5] << 8);
|
u8 ep_addr = cfg_desc[ep_pos + 2];
|
||||||
|
u8 ep_attr = cfg_desc[ep_pos + 3];
|
||||||
|
u16 ep_mps = (u16)cfg_desc[ep_pos + 4] | ((u16)cfg_desc[ep_pos + 5] << 8);
|
||||||
|
|
||||||
if ((ep_attr & 0x03) == 0x02) { /* Bulk */
|
if ((ep_attr & 0x03) == 0x02) { /* Bulk */
|
||||||
if (ep_addr & 0x80) {
|
if (ep_addr & 0x80) {
|
||||||
bulk_in_ep = ep_addr & 0x0F;
|
bulk_in_ep = ep_addr & 0x0F;
|
||||||
bulk_in_mps = ep_mps;
|
bulk_in_mps = ep_mps;
|
||||||
} else {
|
} else {
|
||||||
bulk_out_ep = ep_addr & 0x0F;
|
bulk_out_ep = ep_addr & 0x0F;
|
||||||
bulk_out_mps = ep_mps;
|
bulk_out_mps = ep_mps;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else if (cfg_desc[ep_pos + 1] == 4) {
|
||||||
|
/* 下一个 interface — 停止扫描 */
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
ep_pos += cfg_desc[ep_pos];
|
ep_pos += ep_len;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1524,6 +1858,7 @@ static int xhci_enum_msc_device(u32 port_idx) {
|
|||||||
|
|
||||||
if (!msc_found || bulk_out_ep == 0 || bulk_in_ep == 0) {
|
if (!msc_found || bulk_out_ep == 0 || bulk_in_ep == 0) {
|
||||||
g_log->warn("[xhci] no MSC BOT interface found");
|
g_log->warn("[xhci] no MSC BOT interface found");
|
||||||
|
FB_ENUM_FAIL(0x0D); /* 黄: 未找到 MSC BOT 接口/端点 */
|
||||||
return -6;
|
return -6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1543,6 +1878,7 @@ static int xhci_enum_msc_device(u32 port_idx) {
|
|||||||
bulk_out_mps, bulk_in_mps);
|
bulk_out_mps, bulk_in_mps);
|
||||||
if (xhci_configure_endpoint(slot_id) != 0) {
|
if (xhci_configure_endpoint(slot_id) != 0) {
|
||||||
g_log->warn("[xhci] Configure Endpoint failed");
|
g_log->warn("[xhci] Configure Endpoint failed");
|
||||||
|
FB_ENUM_FAIL(0x0E); /* 黄: Configure Endpoint 失败 */
|
||||||
return -7;
|
return -7;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1601,7 +1937,10 @@ int driver_init(const struct dkm_kernel_api *api,
|
|||||||
u8 bus = 0, dev = 0, func = 0;
|
u8 bus = 0, dev = 0, func = 0;
|
||||||
if (xhci_find(&bus, &dev, &func) != 0) {
|
if (xhci_find(&bus, &dev, &func) != 0) {
|
||||||
g_log->info("[xhci] xHCI controller not found");
|
g_log->info("[xhci] xHCI controller not found");
|
||||||
g_log->info("[xhci] driver ready");
|
/* BUG-FIX: 不 halt。xhci 是可选驱动(manifest required=false),
|
||||||
|
* 无 xHCI 的平台(Intel 6 系/纯 EHCI)必须继续引导到 EHCI/DSK,
|
||||||
|
* 否则 `for(;;)hlt` 卡死整条启动链。仅记日志,如需诊断可开
|
||||||
|
* xhci_pci_diag_code() 打印控制器清单(见注释)。 */
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1628,7 +1967,7 @@ int driver_init(const struct dkm_kernel_api *api,
|
|||||||
u64 bar_phys;
|
u64 bar_phys;
|
||||||
if (bar0_lo & 1u) {
|
if (bar0_lo & 1u) {
|
||||||
g_log->warn("[xhci] BAR0 is IO space; unsupported");
|
g_log->warn("[xhci] BAR0 is IO space; unsupported");
|
||||||
g_log->info("[xhci] driver ready");
|
FB_FATAL(api, 0x02); /* 红: BAR0 是 IO 空间 */
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if ((bar0_lo & 0x6u) == 0x4u) {
|
if ((bar0_lo & 0x6u) == 0x4u) {
|
||||||
@@ -1644,7 +1983,7 @@ int driver_init(const struct dkm_kernel_api *api,
|
|||||||
|
|
||||||
if (!api->hhdm_offset || !bar_phys) {
|
if (!api->hhdm_offset || !bar_phys) {
|
||||||
g_log->warn("[xhci] missing HHDM or BAR0; skip");
|
g_log->warn("[xhci] missing HHDM or BAR0; skip");
|
||||||
g_log->info("[xhci] driver ready");
|
FB_FATAL(api, 0x02); /* 红: 缺 HHDM/BAR */
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1652,14 +1991,14 @@ int driver_init(const struct dkm_kernel_api *api,
|
|||||||
g_mmio = xhci_map_bar(api, bar_phys);
|
g_mmio = xhci_map_bar(api, bar_phys);
|
||||||
if (!g_mmio) {
|
if (!g_mmio) {
|
||||||
g_log->error("[xhci] BAR MMIO unavailable; driver inactive");
|
g_log->error("[xhci] BAR MMIO unavailable; driver inactive");
|
||||||
g_log->info("[xhci] driver ready");
|
FB_FATAL(api, 0x03); /* 红: BAR MMIO 映射失败 */
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Phase 1: Parse capabilities ---- */
|
/* ---- Phase 1: Parse capabilities ---- */
|
||||||
if (xhci_parse_caps() != 0) {
|
if (xhci_parse_caps() != 0) {
|
||||||
g_log->error("[xhci] capability parse failed");
|
g_log->error("[xhci] capability parse failed");
|
||||||
g_log->info("[xhci] driver ready");
|
FB_FATAL(api, 0x04); /* 红: capability 解析失败 */
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1669,27 +2008,27 @@ int driver_init(const struct dkm_kernel_api *api,
|
|||||||
/* ---- Phase 1: Controller Reset ---- */
|
/* ---- Phase 1: Controller Reset ---- */
|
||||||
if (xhci_controller_reset() != 0) {
|
if (xhci_controller_reset() != 0) {
|
||||||
g_log->error("[xhci] controller reset failed");
|
g_log->error("[xhci] controller reset failed");
|
||||||
g_log->info("[xhci] driver ready");
|
FB_FATAL(api, 0x05); /* 红: 控制器 reset 失败 */
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Phase 2: Setup Rings + DCBAA + Scratchpad ---- */
|
/* ---- Phase 2: Setup Rings + DCBAA + Scratchpad ---- */
|
||||||
if (!api->dma) {
|
if (!api->dma) {
|
||||||
g_log->warn("[xhci] DMA API unavailable");
|
g_log->warn("[xhci] DMA API unavailable");
|
||||||
g_log->info("[xhci] driver ready");
|
FB_FATAL(api, 0x06); /* 红: DMA API 缺失 */
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (xhci_setup_rings() != 0) {
|
if (xhci_setup_rings() != 0) {
|
||||||
g_log->error("[xhci] ring setup failed");
|
g_log->error("[xhci] ring setup failed");
|
||||||
g_log->info("[xhci] driver ready");
|
FB_FATAL(api, 0x06); /* 红: ring 初始化失败 */
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Phase 2: Start Controller ---- */
|
/* ---- Phase 2: Start Controller ---- */
|
||||||
if (xhci_controller_start() != 0) {
|
if (xhci_controller_start() != 0) {
|
||||||
g_log->error("[xhci] controller start failed");
|
g_log->error("[xhci] controller start failed");
|
||||||
g_log->info("[xhci] driver ready");
|
FB_FATAL(api, 0x07); /* 红: 控制器启动失败 */
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1765,6 +2104,7 @@ int driver_init(const struct dkm_kernel_api *api,
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
g_log->info("[xhci] no MSC device found; block provider not registered");
|
g_log->info("[xhci] no MSC device found; block provider not registered");
|
||||||
|
FB_FATAL(api, 0x08); /* 黄: 所有端口无设备/枚举全部失败 */
|
||||||
}
|
}
|
||||||
|
|
||||||
g_log->info("[xhci] driver ready");
|
g_log->info("[xhci] driver ready");
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ typedef struct dkm_block_api {
|
|||||||
u64 (*sector_size)(u32 index);
|
u64 (*sector_size)(u32 index);
|
||||||
const char *(*device_name)(u32 index);
|
const char *(*device_name)(u32 index);
|
||||||
int (*set_write_fn)(u32 index, dkm_block_write_fn fn);
|
int (*set_write_fn)(u32 index, dkm_block_write_fn fn);
|
||||||
|
u64 (*sector_count)(u32 index); /* 0x38: tail-append, 老内核该槽为 0 表示未知 */
|
||||||
} dkm_block_api;
|
} dkm_block_api;
|
||||||
|
|
||||||
void block_init(void);
|
void block_init(void);
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ void log_error(const char *msg);
|
|||||||
void log_hex64(const char *prefix, u64 value);
|
void log_hex64(const char *prefix, u64 value);
|
||||||
void panic(const char *msg);
|
void panic(const char *msg);
|
||||||
|
|
||||||
/* ---- 内存日志缓冲区(实机启动日志持久化) ---- */
|
/* ---- 内存日志缓冲区(实机启动日志持久化) ----
|
||||||
|
* 由 disk_log_flush() 写入 FAT32 文件 SYSTEM/DESHAB64/DEV/BOOTLOG.TXT。 */
|
||||||
void log_capture_enable(int enable);
|
void log_capture_enable(int enable);
|
||||||
const char *log_get_buffer(void);
|
const char *log_get_buffer(void);
|
||||||
u32 log_get_length(void);
|
u32 log_get_length(void);
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ static int block_set_write_fn_impl(u32 index, dkm_block_write_fn fn) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static u64 block_sector_count_impl(u32 index) {
|
||||||
|
if (index >= g_device_count) return 0;
|
||||||
|
return g_devices[index].sector_count;
|
||||||
|
}
|
||||||
|
|
||||||
static const dkm_block_api g_block_api = {
|
static const dkm_block_api g_block_api = {
|
||||||
.register_device = block_register_device_impl,
|
.register_device = block_register_device_impl,
|
||||||
.device_count = block_device_count_impl,
|
.device_count = block_device_count_impl,
|
||||||
@@ -65,7 +70,8 @@ static const dkm_block_api g_block_api = {
|
|||||||
.write = block_write_impl,
|
.write = block_write_impl,
|
||||||
.sector_size = block_sector_size_impl,
|
.sector_size = block_sector_size_impl,
|
||||||
.device_name = block_device_name_impl,
|
.device_name = block_device_name_impl,
|
||||||
.set_write_fn = block_set_write_fn_impl
|
.set_write_fn = block_set_write_fn_impl,
|
||||||
|
.sector_count = block_sector_count_impl
|
||||||
};
|
};
|
||||||
|
|
||||||
const dkm_block_api *block_get_api(void) {
|
const dkm_block_api *block_get_api(void) {
|
||||||
|
|||||||
Binary file not shown.
@@ -520,6 +520,379 @@ int fat32_read_path(const char *path, u8 **out_data, u32 *out_size) {
|
|||||||
return -8; /* 路径为空或无效 */
|
return -8; /* 路径为空或无效 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- FAT32 写入(实机启动日志落盘,2026-08 移植自 DSK fat32_write_*) ----
|
||||||
|
* 相比 DSK 版本的两点增强:
|
||||||
|
* 1) FAT 条目支持按需读-改-写(超出 256 扇区预读缓冲时逐扇区读写,
|
||||||
|
* 并同步镜像到所有 FAT 副本),不再受小分区限制。
|
||||||
|
* 2) 目录扫描跨簇 + 目录满时自动扩展,文件链支持多簇(日志可达 128KB)。
|
||||||
|
* 注意:与 DSK 一致,不更新 FSInfo(free count 会过期,测试工具可接受)。 */
|
||||||
|
|
||||||
|
/* 读取 FAT 表条目(28 位),支持按需读扇区。 */
|
||||||
|
static u32 fat32_get_cluster_entry(u32 fat_byte_off, u32 clus,
|
||||||
|
const fat32_bpb *bpb, const u8 *disk) {
|
||||||
|
u32 ent_off = fat_byte_off + clus * 4;
|
||||||
|
if (ent_off + 4 <= sizeof(g_dsk_fat32_disk)) {
|
||||||
|
return fat32_read_u32(disk + ent_off) & 0x0FFFFFFF;
|
||||||
|
}
|
||||||
|
return fat32_get_next_cluster(fat_byte_off, clus, bpb, disk);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 写 FAT 表条目。缓冲内条目直接改内存(同步镜像副本 1),
|
||||||
|
* 缓冲外条目按 FAT 副本逐个读-改-写扇区。返回 0 成功。 */
|
||||||
|
static int fat32_set_cluster_entry(u32 fat_byte_off, u32 fat_sectors, u32 clus,
|
||||||
|
u32 value, const fat32_bpb *bpb, u8 *disk) {
|
||||||
|
u32 ent_off = fat_byte_off + clus * 4;
|
||||||
|
if (ent_off + 4 <= sizeof(g_dsk_fat32_disk)) {
|
||||||
|
disk[ent_off] = (u8)(value & 0xFF);
|
||||||
|
disk[ent_off + 1] = (u8)((value >> 8) & 0xFF);
|
||||||
|
disk[ent_off + 2] = (u8)((value >> 16) & 0xFF);
|
||||||
|
disk[ent_off + 3] = (u8)((disk[ent_off + 3] & 0xF0) | ((value >> 24) & 0x0F));
|
||||||
|
/* 同步 FAT 副本 1 的缓冲镜像(若在缓冲内) */
|
||||||
|
if (bpb->fat_count >= 2) {
|
||||||
|
u32 e1 = ent_off + fat_sectors * 512;
|
||||||
|
if (e1 + 4 <= sizeof(g_dsk_fat32_disk)) {
|
||||||
|
disk[e1] = disk[ent_off];
|
||||||
|
disk[e1 + 1] = disk[ent_off + 1];
|
||||||
|
disk[e1 + 2] = disk[ent_off + 2];
|
||||||
|
disk[e1 + 3] = disk[ent_off + 3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
/* 缓冲外:逐 FAT 副本读-改-写 */
|
||||||
|
const dkm_kernel_api *api = dkm_get_kernel_api();
|
||||||
|
if (!api || !api->block || !api->block->read || !api->block->write) return -1;
|
||||||
|
u32 sec_index = (clus * 4) / 512;
|
||||||
|
u32 ent_in_sec = (clus * 4) % 512;
|
||||||
|
u32 copies = bpb->fat_count;
|
||||||
|
if (copies > 4) copies = 4;
|
||||||
|
for (u32 f = 0; f < copies; f++) {
|
||||||
|
u32 sec_lba = bpb->reserved_sector_count + f * fat_sectors + sec_index;
|
||||||
|
if (api->block->read(0, sec_lba, 1, g_dsk_fat32_fat_sec) != 0) return -2;
|
||||||
|
g_dsk_fat32_fat_sec[ent_in_sec] = (u8)(value & 0xFF);
|
||||||
|
g_dsk_fat32_fat_sec[ent_in_sec + 1] = (u8)((value >> 8) & 0xFF);
|
||||||
|
g_dsk_fat32_fat_sec[ent_in_sec + 2] = (u8)((value >> 16) & 0xFF);
|
||||||
|
g_dsk_fat32_fat_sec[ent_in_sec + 3] = (u8)((g_dsk_fat32_fat_sec[ent_in_sec + 3] & 0xF0) | ((value >> 24) & 0x0F));
|
||||||
|
if (api->block->write(0, sec_lba, 1, g_dsk_fat32_fat_sec) != 0) return -3;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顺序扫描 FAT 找空闲簇(缓冲内直读,缓冲外按 8 扇区批量读)。
|
||||||
|
* from_clus: 起始簇号(建议传入上次分配结果+1,避免反复扫描)。
|
||||||
|
* 返回空闲簇号,失败返回 0。 */
|
||||||
|
static u32 fat32_find_free_cluster(u32 fat_lba, u32 fat_sectors, u32 from_clus) {
|
||||||
|
const dkm_kernel_api *api = dkm_get_kernel_api();
|
||||||
|
u8 *disk = g_dsk_fat32_disk;
|
||||||
|
u32 total_entries = (fat_sectors * 512) / 4;
|
||||||
|
u32 fat_byte_off = fat_lba * 512;
|
||||||
|
u32 c = from_clus;
|
||||||
|
while (c < total_entries) {
|
||||||
|
u32 ent_off = fat_byte_off + c * 4;
|
||||||
|
if (ent_off + 4 <= sizeof(g_dsk_fat32_disk)) {
|
||||||
|
if ((fat32_read_u32(disk + ent_off) & 0x0FFFFFFF) == 0) return c;
|
||||||
|
c++;
|
||||||
|
} else {
|
||||||
|
u32 sec_index = (c * 4) / 512;
|
||||||
|
u32 sec_lba = fat_lba + sec_index;
|
||||||
|
u32 secs = 8;
|
||||||
|
if (sec_lba + secs > fat_lba + fat_sectors) secs = fat_lba + fat_sectors - sec_lba;
|
||||||
|
if (api->block->read(0, sec_lba, secs, g_dsk_fat32_cluster) != 0) return 0;
|
||||||
|
u32 first_in_chunk = sec_index * 128;
|
||||||
|
u32 last_in_chunk = (sec_index + secs) * 128;
|
||||||
|
for (u32 cc = (c < first_in_chunk ? first_in_chunk : c);
|
||||||
|
cc < last_in_chunk && cc < total_entries; cc++) {
|
||||||
|
if ((fat32_read_u32(g_dsk_fat32_cluster + (cc - first_in_chunk) * 4) & 0x0FFFFFFF) == 0)
|
||||||
|
return cc;
|
||||||
|
}
|
||||||
|
c = last_in_chunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分配并链接 needed 个簇,全部标记 EOC。scan_from 记录扫描游标。
|
||||||
|
* 返回 0 成功;first/last 返回链首/链尾。 */
|
||||||
|
static int fat32_alloc_clusters(const fat32_bpb *bpb, u32 fat_lba,
|
||||||
|
u32 fat_sectors, u32 needed,
|
||||||
|
u32 *scan_from, u32 *first, u32 *last) {
|
||||||
|
u32 prev = 0;
|
||||||
|
u32 fc = 0, lc = 0;
|
||||||
|
u32 cur_scan = *scan_from;
|
||||||
|
for (u32 i = 0; i < needed; i++) {
|
||||||
|
u32 nc = fat32_find_free_cluster(fat_lba, fat_sectors, cur_scan);
|
||||||
|
if (nc == 0) return -1;
|
||||||
|
cur_scan = nc + 1;
|
||||||
|
if (fat32_set_cluster_entry(bpb->reserved_sector_count * 512, fat_sectors,
|
||||||
|
nc, 0x0FFFFFFF, bpb, g_dsk_fat32_disk) != 0)
|
||||||
|
return -2;
|
||||||
|
if (prev) {
|
||||||
|
if (fat32_set_cluster_entry(bpb->reserved_sector_count * 512, fat_sectors,
|
||||||
|
prev, nc, bpb, g_dsk_fat32_disk) != 0)
|
||||||
|
return -3;
|
||||||
|
} else {
|
||||||
|
fc = nc;
|
||||||
|
}
|
||||||
|
prev = nc;
|
||||||
|
lc = nc;
|
||||||
|
}
|
||||||
|
*scan_from = cur_scan;
|
||||||
|
*first = fc;
|
||||||
|
*last = lc;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 在指定目录(dir_clus 起始簇)写入文件(8.3 名,11 字符)。
|
||||||
|
* 存在则覆盖(复用旧链/扩展/截断),否则新建。支持多簇目录与多簇文件。
|
||||||
|
* data 必须指向可寻址静态内存(如 log 缓冲)。返回 0 成功。 */
|
||||||
|
static int fat32_write_file_in_dir(u32 dir_clus, const char *name11,
|
||||||
|
const u8 *data, u32 size) {
|
||||||
|
const dkm_kernel_api *api = dkm_get_kernel_api();
|
||||||
|
if (!api || !api->block || !api->block->read || !api->block->write) return -1;
|
||||||
|
u8 *disk = g_dsk_fat32_disk;
|
||||||
|
if (api->block->read(0, 0, 256, disk) != 0) return -2;
|
||||||
|
const fat32_bpb *bpb = (const fat32_bpb *)disk;
|
||||||
|
if (bpb->boot_sig != 0xAA55 || bpb->bytes_per_sector != 512) return -3;
|
||||||
|
|
||||||
|
u32 spc = bpb->sectors_per_cluster;
|
||||||
|
u32 fat_lba = bpb->reserved_sector_count;
|
||||||
|
u32 fat_sectors = bpb->sectors_per_fat;
|
||||||
|
u32 data_lba = bpb->reserved_sector_count + (u32)bpb->fat_count * bpb->sectors_per_fat;
|
||||||
|
u32 fat_byte_off = fat_lba * 512;
|
||||||
|
u32 cluster_bytes = spc * 512;
|
||||||
|
|
||||||
|
/* ---- 1. 扫描目录链:找同名条目或空闲槽 ---- */
|
||||||
|
u32 cur = dir_clus;
|
||||||
|
u32 last_dir_clus = dir_clus;
|
||||||
|
u32 entry_clus = 0; /* 目录项所在目录簇 */
|
||||||
|
u32 entry_slot = 0;
|
||||||
|
int found = 0; /* 同名文件已存在 */
|
||||||
|
u32 old_first_clus = 0;
|
||||||
|
int done = 0;
|
||||||
|
while (cur >= 2 && cur < 0x0FFFFFF8) {
|
||||||
|
last_dir_clus = cur;
|
||||||
|
u32 clus_lba = data_lba + (cur - 2) * spc;
|
||||||
|
if (api->block->read(0, clus_lba, spc, g_dsk_fat32_cluster) != 0) return -4;
|
||||||
|
u32 me = cluster_bytes / 32;
|
||||||
|
const fat32_dir_entry *d = (const fat32_dir_entry *)g_dsk_fat32_cluster;
|
||||||
|
for (u32 e = 0; e < me; e++) {
|
||||||
|
if (d[e].name[0] == 0) { /* 目录结束标记 */
|
||||||
|
if (entry_clus == 0) { entry_clus = cur; entry_slot = e; }
|
||||||
|
done = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ((u8)d[e].name[0] == 0xE5) { /* 已删除项,可复用 */
|
||||||
|
if (entry_clus == 0) { entry_clus = cur; entry_slot = e; }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (d[e].attr == 0x0F || (d[e].attr & 0x08)) continue;
|
||||||
|
if (fat32_name11_eq(d[e].name, name11)) {
|
||||||
|
found = 1;
|
||||||
|
entry_clus = cur;
|
||||||
|
entry_slot = e;
|
||||||
|
old_first_clus = fat32_read_u16((const u8 *)&d[e].cluster_low) |
|
||||||
|
((u32)fat32_read_u16((const u8 *)&d[e].cluster_high) << 16);
|
||||||
|
done = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (done) break;
|
||||||
|
cur = fat32_get_cluster_entry(fat_byte_off, cur, bpb, disk);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 2. 目录满时扩展一个簇 ---- */
|
||||||
|
if (entry_clus == 0) {
|
||||||
|
u32 scan_from = 2;
|
||||||
|
u32 nc = fat32_find_free_cluster(fat_lba, fat_sectors, scan_from);
|
||||||
|
if (nc == 0) return -9;
|
||||||
|
if (fat32_set_cluster_entry(fat_byte_off, fat_sectors, nc, 0x0FFFFFFF,
|
||||||
|
bpb, disk) != 0)
|
||||||
|
return -10;
|
||||||
|
if (fat32_set_cluster_entry(fat_byte_off, fat_sectors, last_dir_clus, nc,
|
||||||
|
bpb, disk) != 0)
|
||||||
|
return -11;
|
||||||
|
for (u32 b = 0; b < cluster_bytes; b++) g_dsk_fat32_cluster[b] = 0;
|
||||||
|
u32 nlba = data_lba + (nc - 2) * spc;
|
||||||
|
if (api->block->write(0, nlba, spc, g_dsk_fat32_cluster) != 0) return -12;
|
||||||
|
entry_clus = nc;
|
||||||
|
entry_slot = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 3. 文件簇链:复用/扩展/截断 ---- */
|
||||||
|
u32 needed = (size + cluster_bytes - 1) / cluster_bytes;
|
||||||
|
if (needed == 0) needed = 1;
|
||||||
|
u32 first = 0;
|
||||||
|
u32 scan_from = 2;
|
||||||
|
|
||||||
|
if (found && old_first_clus >= 2) {
|
||||||
|
/* 走旧链,收集前 needed 个簇 */
|
||||||
|
u32 c = old_first_clus;
|
||||||
|
u32 cnt = 0;
|
||||||
|
u32 last = 0;
|
||||||
|
u32 tail = 0;
|
||||||
|
while (c >= 2 && c < 0x0FFFFFF8 && cnt < needed) {
|
||||||
|
last = c;
|
||||||
|
cnt++;
|
||||||
|
c = fat32_get_cluster_entry(fat_byte_off, c, bpb, disk);
|
||||||
|
}
|
||||||
|
first = old_first_clus;
|
||||||
|
if (cnt == needed) {
|
||||||
|
/* 截断:last 指向 EOC,释放旧链尾部 */
|
||||||
|
tail = fat32_get_cluster_entry(fat_byte_off, last, bpb, disk);
|
||||||
|
if (fat32_set_cluster_entry(fat_byte_off, fat_sectors, last, 0x0FFFFFFF,
|
||||||
|
bpb, disk) != 0)
|
||||||
|
return -13;
|
||||||
|
while (tail >= 2 && tail < 0x0FFFFFF8) {
|
||||||
|
u32 nn = fat32_get_cluster_entry(fat_byte_off, tail, bpb, disk);
|
||||||
|
if (fat32_set_cluster_entry(fat_byte_off, fat_sectors, tail, 0,
|
||||||
|
bpb, disk) != 0)
|
||||||
|
return -14;
|
||||||
|
tail = nn;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* 旧链不足:last 之后补簇 */
|
||||||
|
u32 need_more = needed - cnt;
|
||||||
|
u32 nf = 0, nl = 0;
|
||||||
|
if (fat32_alloc_clusters(bpb, fat_lba, fat_sectors, need_more,
|
||||||
|
&scan_from, &nf, &nl) != 0)
|
||||||
|
return -15;
|
||||||
|
if (fat32_set_cluster_entry(fat_byte_off, fat_sectors, last, nf,
|
||||||
|
bpb, disk) != 0)
|
||||||
|
return -16;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
u32 nf = 0, nl = 0;
|
||||||
|
if (fat32_alloc_clusters(bpb, fat_lba, fat_sectors, needed,
|
||||||
|
&scan_from, &nf, &nl) != 0)
|
||||||
|
return -17;
|
||||||
|
first = nf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 4. 写数据簇 ---- */
|
||||||
|
{
|
||||||
|
u32 c = first;
|
||||||
|
u32 remaining = size;
|
||||||
|
u32 off = 0;
|
||||||
|
while (c >= 2 && c < 0x0FFFFFF8 && remaining > 0) {
|
||||||
|
u32 lba = data_lba + (c - 2) * spc;
|
||||||
|
if (remaining >= cluster_bytes) {
|
||||||
|
if (api->block->write(0, lba, spc, data + off) != 0) return -18;
|
||||||
|
} else {
|
||||||
|
u8 *cb = g_dsk_fat32_cluster;
|
||||||
|
for (u32 b = 0; b < cluster_bytes; b++) cb[b] = 0;
|
||||||
|
for (u32 b = 0; b < remaining; b++) cb[b] = data[off + b];
|
||||||
|
if (api->block->write(0, lba, spc, cb) != 0) return -19;
|
||||||
|
}
|
||||||
|
if (remaining >= cluster_bytes) {
|
||||||
|
off += cluster_bytes;
|
||||||
|
remaining -= cluster_bytes;
|
||||||
|
} else {
|
||||||
|
off += remaining;
|
||||||
|
remaining = 0;
|
||||||
|
}
|
||||||
|
c = fat32_get_cluster_entry(fat_byte_off, c, bpb, disk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 5. 更新目录项 ---- */
|
||||||
|
{
|
||||||
|
u32 elba = data_lba + (entry_clus - 2) * spc;
|
||||||
|
if (api->block->read(0, elba, spc, g_dsk_fat32_cluster) != 0) return -20;
|
||||||
|
fat32_dir_entry *dir = (fat32_dir_entry *)g_dsk_fat32_cluster;
|
||||||
|
fat32_dir_entry *e = &dir[entry_slot];
|
||||||
|
if (!found) {
|
||||||
|
for (int i = 0; i < 11; i++) e->name[i] = name11[i];
|
||||||
|
e->attr = 0x20; /* archive */
|
||||||
|
e->nt_reserved = 0;
|
||||||
|
e->creation_tenth = 0;
|
||||||
|
e->creation_time = 0;
|
||||||
|
e->creation_date = 0;
|
||||||
|
e->access_date = 0;
|
||||||
|
e->write_time = 0;
|
||||||
|
e->write_date = 0;
|
||||||
|
}
|
||||||
|
e->cluster_high = (u16)((first >> 16) & 0xFFFF);
|
||||||
|
e->cluster_low = (u16)(first & 0xFFFF);
|
||||||
|
e->file_size = size;
|
||||||
|
if (api->block->write(0, elba, spc, g_dsk_fat32_cluster) != 0) return -21;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 6. 回写 FAT:缓冲内区间按副本刷回(缓冲外条目已即时写盘) ---- */
|
||||||
|
{
|
||||||
|
u32 copies = bpb->fat_count;
|
||||||
|
if (copies > 4) copies = 4;
|
||||||
|
for (u32 f = 0; f < copies; f++) {
|
||||||
|
u32 flba = fat_lba + f * fat_sectors;
|
||||||
|
if (flba >= 256) continue;
|
||||||
|
u32 ns = fat_sectors;
|
||||||
|
if (flba + ns > 256) ns = 256 - flba;
|
||||||
|
if (ns == 0) continue;
|
||||||
|
if (api->block->write(0, flba, ns, disk + (u64)flba * 512) != 0) return -22;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info("[UTSM] fat32 write: ok");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 在根目录写入文件(8.3 名)。 */
|
||||||
|
static int fat32_write_root_file(const char *name11, const u8 *data, u32 size) {
|
||||||
|
const dkm_kernel_api *api = dkm_get_kernel_api();
|
||||||
|
if (!api || !api->block || !api->block->read) return -1;
|
||||||
|
u8 *disk = g_dsk_fat32_disk;
|
||||||
|
if (api->block->read(0, 0, 256, disk) != 0) return -2;
|
||||||
|
const fat32_bpb *bpb = (const fat32_bpb *)disk;
|
||||||
|
if (bpb->boot_sig != 0xAA55) return -3;
|
||||||
|
return fat32_write_file_in_dir(bpb->root_cluster, name11, data, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按 8.3 路径(每段 11 字符,'/' 分隔)定位子目录,返回起始簇,失败返回 0。 */
|
||||||
|
static u32 fat32_find_dir_path(const char *path) {
|
||||||
|
const dkm_kernel_api *api = dkm_get_kernel_api();
|
||||||
|
if (!api || !api->block || !api->block->read) return 0;
|
||||||
|
u8 *disk = g_dsk_fat32_disk;
|
||||||
|
if (api->block->read(0, 0, 256, disk) != 0) return 0;
|
||||||
|
const fat32_bpb *bpb = (const fat32_bpb *)disk;
|
||||||
|
if (bpb->boot_sig != 0xAA55 || bpb->bytes_per_sector != 512) return 0;
|
||||||
|
|
||||||
|
u32 cur = bpb->root_cluster;
|
||||||
|
u32 pos = 0;
|
||||||
|
while (path[pos]) {
|
||||||
|
char comp[12];
|
||||||
|
u32 ci = 0;
|
||||||
|
while (path[pos] && path[pos] != '/') {
|
||||||
|
if (ci < 11) comp[ci++] = path[pos];
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
while (ci < 11) comp[ci++] = ' ';
|
||||||
|
comp[11] = 0;
|
||||||
|
if (path[pos] == '/') pos++;
|
||||||
|
if (comp[0] == ' ') break; /* 空段防呆 */
|
||||||
|
u32 sub = 0, sz = 0;
|
||||||
|
if (fat32_find_in_dir(cur, comp, 1, &sub, &sz, 0) != 0) return 0;
|
||||||
|
cur = sub;
|
||||||
|
}
|
||||||
|
return cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 实机启动日志落盘:写 FAT32 文件。
|
||||||
|
* 优先写 SYSTEM/DESHAB64/DEV/BOOTLOG.TXT;DEV 目录缺失时回退根目录 BOOTLOG.TXT。
|
||||||
|
* 相比旧原始扇区方案不再覆盖 GPT header/entries,主机可直接读取该文件。
|
||||||
|
* 返回 0 成功。 */
|
||||||
|
int utsm_bootlog_write(const u8 *data, u32 size) {
|
||||||
|
const dkm_kernel_api *api = dkm_get_kernel_api();
|
||||||
|
if (!api || !api->block || !api->block->device_count) return -1;
|
||||||
|
if (api->block->device_count() == 0) return -2;
|
||||||
|
if (!api->block->write) return -3;
|
||||||
|
if (!data || size == 0) return -4;
|
||||||
|
|
||||||
|
static const char name11[11] = {'B','O','O','T','L','O','G',' ','T','X','T'};
|
||||||
|
u32 dev_clus = fat32_find_dir_path("SYSTEM /DESHAB64 /DEV ");
|
||||||
|
if (dev_clus >= 2)
|
||||||
|
return fat32_write_file_in_dir(dev_clus, name11, data, size);
|
||||||
|
return fat32_write_root_file(name11, data, size);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- ELF checks ---- */
|
/* ---- ELF checks ---- */
|
||||||
static int dsk_check_elf(const void *address, u64 size) {
|
static int dsk_check_elf(const void *address, u64 size) {
|
||||||
if (!address || size < sizeof(elf64_ehdr)) return -1;
|
if (!address || size < sizeof(elf64_ehdr)) return -1;
|
||||||
|
|||||||
Binary file not shown.
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
/* ---- 内存日志缓冲区(实机启动日志持久化) ----
|
/* ---- 内存日志缓冲区(实机启动日志持久化) ----
|
||||||
* 默认启用捕获:所有日志同时写串口和内存缓冲。
|
* 默认启用捕获:所有日志同时写串口和内存缓冲。
|
||||||
* 块设备就绪后由 disk_log_flush() 把缓冲区写入磁盘原始扇区。 */
|
* 块设备就绪后由 disk_log_flush() 把缓冲区写入 FAT32 文件
|
||||||
|
* SYSTEM/DESHAB64/DEV/BOOTLOG.TXT(DEV 缺失时回退根目录 BOOTLOG.TXT)。 */
|
||||||
#define LOG_BUF_SIZE 524288u /* 512KB, 覆盖 LBA 2-1000 (999 扇区 ~500KB) */
|
#define LOG_BUF_SIZE 524288u /* 512KB, 覆盖 LBA 2-1000 (999 扇区 ~500KB) */
|
||||||
static char g_log_buf[LOG_BUF_SIZE];
|
static char g_log_buf[LOG_BUF_SIZE];
|
||||||
static u32 g_log_len = 0;
|
static u32 g_log_len = 0;
|
||||||
|
|||||||
+24
-51
@@ -26,6 +26,10 @@ void dsm_load_by_manifest_ex(const ini_config *cfg);
|
|||||||
* Phase 1 已支持 LFN 长文件名 + 子目录遍历。 */
|
* Phase 1 已支持 LFN 长文件名 + 子目录遍历。 */
|
||||||
extern int fat32_read_path(const char *path, u8 **out_data, u32 *out_size);
|
extern int fat32_read_path(const char *path, u8 **out_data, u32 *out_size);
|
||||||
|
|
||||||
|
/* dsk_loader.c: 实机启动日志落盘。把日志缓冲写入 FAT32 文件:
|
||||||
|
* SYSTEM/DESHAB64/DEV/BOOTLOG.TXT(DEV 缺失时回退根目录 BOOTLOG.TXT)。 */
|
||||||
|
extern int utsm_bootlog_write(const u8 *data, u32 size);
|
||||||
|
|
||||||
/* ---- 运行期配置全局变量(从 FUCK 文件初始化) ---- */
|
/* ---- 运行期配置全局变量(从 FUCK 文件初始化) ---- */
|
||||||
u32 g_utsm_max_segments = UTSM_MAX_SEGMENTS_DEFAULT;
|
u32 g_utsm_max_segments = UTSM_MAX_SEGMENTS_DEFAULT;
|
||||||
u32 g_utsm_max_capabilities = UTSM_MAX_CAPABILITIES_DEFAULT;
|
u32 g_utsm_max_capabilities = UTSM_MAX_CAPABILITIES_DEFAULT;
|
||||||
@@ -149,12 +153,16 @@ static int utsm_load_fuck_config(ini_config *cfg) {
|
|||||||
return -2;
|
return -2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === 实机日志持久化:把内存日志缓冲区写入磁盘原始扇区 ===
|
/* === 实机日志持久化:把内存日志缓冲区写入 FAT32 文件 ===
|
||||||
* 开发测试用:实机无串口时,启动后用 WinHex/dd 读取磁盘对应扇区即可查看日志。
|
* 目标: SYSTEM/DESHAB64/DEV/BOOTLOG.TXT(DEV 目录不存在时回退根目录 BOOTLOG.TXT)。
|
||||||
* 扇区范围由 FUCK [boot] disk_log_start / disk_log_end 配置(默认 2-1000)。
|
* 由 FUCK [boot] disk_log=1 启用。
|
||||||
* 注意: 写入 LBA 1-33 会覆盖 GPT header/entries,重启后 Limine 可能无法找到 ESP。
|
* 相比旧原始扇区方案(LBA 2-1000):
|
||||||
* 建议测试盘从 LBA 34 开始,或测试后重新烧录镜像。 */
|
* - 不再覆盖 GPT header/entries,不破坏 Limine 引导链
|
||||||
static void disk_log_flush(int start_lba, int end_lba) {
|
* - 日志以普通 FAT32 文件呈现,主机直接读取即可,无需 WinHex/dd
|
||||||
|
* 日志超出 BOOTLOG_MAX_BYTES 时截断(128KB 足够覆盖完整启动链)。 */
|
||||||
|
#define BOOTLOG_MAX_BYTES (128u * 1024u)
|
||||||
|
|
||||||
|
static void disk_log_flush(void) {
|
||||||
const dkm_kernel_api *api = dkm_get_kernel_api();
|
const dkm_kernel_api *api = dkm_get_kernel_api();
|
||||||
if (!api || !api->block || !api->block->write || !api->block->device_count) {
|
if (!api || !api->block || !api->block->write || !api->block->device_count) {
|
||||||
log_warn("[UTSM] disk_log: block device unavailable, skip");
|
log_warn("[UTSM] disk_log: block device unavailable, skip");
|
||||||
@@ -164,10 +172,6 @@ static void disk_log_flush(int start_lba, int end_lba) {
|
|||||||
log_warn("[UTSM] disk_log: no block device, skip");
|
log_warn("[UTSM] disk_log: no block device, skip");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (end_lba <= start_lba) {
|
|
||||||
log_warn("[UTSM] disk_log: invalid sector range");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char *buf = log_get_buffer();
|
const char *buf = log_get_buffer();
|
||||||
u32 len = log_get_length();
|
u32 len = log_get_length();
|
||||||
@@ -175,45 +179,16 @@ static void disk_log_flush(int start_lba, int end_lba) {
|
|||||||
log_warn("[UTSM] disk_log: log buffer empty, skip");
|
log_warn("[UTSM] disk_log: log buffer empty, skip");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (len > BOOTLOG_MAX_BYTES) len = BOOTLOG_MAX_BYTES;
|
||||||
|
|
||||||
u32 avail_sectors = (u32)(end_lba - start_lba + 1);
|
int rc = utsm_bootlog_write((const u8 *)buf, len);
|
||||||
u32 avail_bytes = avail_sectors * 512;
|
if (rc == 0) {
|
||||||
u32 write_bytes = (len < avail_bytes) ? len : avail_bytes;
|
log_info("[UTSM] disk_log: BOOTLOG.TXT written");
|
||||||
u32 full_sectors = write_bytes / 512;
|
log_hex64("[UTSM] disk_log: bytes=", len);
|
||||||
u32 rem_bytes = write_bytes % 512;
|
} else {
|
||||||
u64 lba = (u64)start_lba;
|
log_error("[UTSM] disk_log: FAT32 write failed");
|
||||||
u32 offset = 0;
|
log_hex64("[UTSM] disk_log: rc=", (u64)(i64)rc);
|
||||||
|
|
||||||
/* 写完整扇区(AHCI 单次最多 8 扇区,block API 内部分块) */
|
|
||||||
while (full_sectors > 0) {
|
|
||||||
u32 chunk = (full_sectors > 8) ? 8 : full_sectors;
|
|
||||||
int st = api->block->write(0, lba, chunk, buf + offset);
|
|
||||||
if (st != 0) {
|
|
||||||
log_error("[UTSM] disk_log: write failed");
|
|
||||||
log_hex64("[UTSM] disk_log: lba=", lba);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lba += chunk;
|
|
||||||
offset += chunk * 512;
|
|
||||||
full_sectors -= chunk;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 最后一个不完整扇区:补零后写入 */
|
|
||||||
if (rem_bytes > 0) {
|
|
||||||
u8 tail[512];
|
|
||||||
for (u32 i = 0; i < 512; i++) tail[i] = 0;
|
|
||||||
for (u32 i = 0; i < rem_bytes; i++) tail[i] = (u8)buf[offset + i];
|
|
||||||
int st = api->block->write(0, lba, 1, tail);
|
|
||||||
if (st != 0) {
|
|
||||||
log_error("[UTSM] disk_log: tail write failed");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log_info("[UTSM] disk_log: flushed to disk");
|
|
||||||
log_hex64("[UTSM] disk_log: start_lba=", (u64)start_lba);
|
|
||||||
log_hex64("[UTSM] disk_log: end_lba=", (u64)end_lba);
|
|
||||||
log_hex64("[UTSM] disk_log: bytes=", write_bytes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void kernel_main(void) {
|
void kernel_main(void) {
|
||||||
@@ -497,13 +472,11 @@ void kernel_main(void) {
|
|||||||
log_info("[UTSM] PE DLL manager initialized (SYSTEM/lib)");
|
log_info("[UTSM] PE DLL manager initialized (SYSTEM/lib)");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === 实机日志持久化:把启动日志写入磁盘原始扇区(开发测试用) === */
|
/* === 实机日志持久化:把启动日志写入 FAT32 文件 BOOTLOG.TXT === */
|
||||||
{
|
{
|
||||||
int disk_log = ini_get_bool(cfg, "boot", "disk_log", 0);
|
int disk_log = ini_get_bool(cfg, "boot", "disk_log", 0);
|
||||||
if (disk_log) {
|
if (disk_log) {
|
||||||
int log_start = ini_get_int(cfg, "boot", "disk_log_start", 2);
|
disk_log_flush();
|
||||||
int log_end = ini_get_int(cfg, "boot", "disk_log_end", 1000);
|
|
||||||
disk_log_flush(log_start, log_end);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -34,6 +34,8 @@ typedef struct dsk_block_api_mirror {
|
|||||||
void *pad0; void *pad1; /* register_device, device_count */
|
void *pad0; void *pad1; /* register_device, device_count */
|
||||||
block_read_fn read;
|
block_read_fn read;
|
||||||
block_write_fn write;
|
block_write_fn write;
|
||||||
|
void *pad2; void *pad3; void *pad4; /* sector_size, device_name, set_write_fn */
|
||||||
|
u64 (*sector_count)(u32 index); /* 0x38: tail-append, 老内核该槽为 NULL */
|
||||||
} dsk_block_api_mirror;
|
} dsk_block_api_mirror;
|
||||||
|
|
||||||
typedef struct dsk_kernel_api_mirror {
|
typedef struct dsk_kernel_api_mirror {
|
||||||
@@ -55,6 +57,8 @@ _Static_assert(offsetof(dsk_kernel_api_mirror, block) == 0xA8,
|
|||||||
"dsk_kernel_api_mirror.block offset must be 0xA8 (matching dkm_kernel_api)");
|
"dsk_kernel_api_mirror.block offset must be 0xA8 (matching dkm_kernel_api)");
|
||||||
_Static_assert(offsetof(dsk_block_api_mirror, read) == 16,
|
_Static_assert(offsetof(dsk_block_api_mirror, read) == 16,
|
||||||
"dsk_block_api_mirror.read offset must be 16 (matching dkm_block_api)");
|
"dsk_block_api_mirror.read offset must be 16 (matching dkm_block_api)");
|
||||||
|
_Static_assert(offsetof(dsk_block_api_mirror, sector_count) == 0x38,
|
||||||
|
"dsk_block_api_mirror.sector_count offset must be 0x38 (matching dkm_block_api tail-append)");
|
||||||
|
|
||||||
static void sputc(char c) { for(unsigned i=0;i<100000;i++){if(inb(COM1+5)&0x20)break;} outb(COM1,(u8)c); }
|
static void sputc(char c) { for(unsigned i=0;i<100000;i++){if(inb(COM1+5)&0x20)break;} outb(COM1,(u8)c); }
|
||||||
static void swrite(const char *s) { while(*s){if(*s=='\n')sputc('\r');sputc(*s++);} }
|
static void swrite(const char *s) { while(*s){if(*s=='\n')sputc('\r');sputc(*s++);} }
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/* deshab/boot_log.h — SDK 侧 boot log 绑定(见 CODE/tools/boot_log.h) */
|
||||||
|
#ifndef DESHAB_BOOT_LOG_H
|
||||||
|
#define DESHAB_BOOT_LOG_H
|
||||||
|
|
||||||
|
#include "../../tools/boot_log.h"
|
||||||
|
#include "boot_context.h"
|
||||||
|
|
||||||
|
static inline void dsb_bootlog_bind(dsb_boot_log_info **slot, const dsk_boot_context *ctx) {
|
||||||
|
boot_log_bind((boot_log_info **)slot, ctx ? ctx->utsm_state : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 兼容别名 */
|
||||||
|
typedef boot_log_info dsb_boot_log_info;
|
||||||
|
|
||||||
|
#endif /* DESHAB_BOOT_LOG_H */
|
||||||
@@ -117,6 +117,7 @@ typedef struct dkm_block_api {
|
|||||||
u64 (*sector_size)(u32 index);
|
u64 (*sector_size)(u32 index);
|
||||||
const char *(*device_name)(u32 index);
|
const char *(*device_name)(u32 index);
|
||||||
int (*set_write_fn)(u32 index, dkm_block_write_fn fn);
|
int (*set_write_fn)(u32 index, dkm_block_write_fn fn);
|
||||||
|
u64 (*sector_count)(u32 index); /* 0x38: tail-append, 与 utsm/block.h 同步 */
|
||||||
} dkm_block_api;
|
} dkm_block_api;
|
||||||
|
|
||||||
/* ---- MMIO / 页表 API ----
|
/* ---- MMIO / 页表 API ----
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/* boot_log.h — 跨 UTSM/DSK/应用 ELF 的启动日志环形缓冲(freestanding,无 libc)
|
||||||
|
*
|
||||||
|
* boot_log_info 由 UTSM 在静态内存中创建,经 dsk_boot_context.utsm_state 传递。
|
||||||
|
* DSK 注册 flush 回调,将缓冲写入 FAT32: SYSTEM/DESHAB64/DEV/BOOTLOG.TXT
|
||||||
|
* (DEV 目录不存在时回退 ESP 根目录 BOOTLOG.TXT)。
|
||||||
|
*/
|
||||||
|
#ifndef BOOT_LOG_H
|
||||||
|
#define BOOT_LOG_H
|
||||||
|
|
||||||
|
typedef unsigned int u32;
|
||||||
|
typedef unsigned long long u64;
|
||||||
|
|
||||||
|
#define BOOT_LOG_MAGIC 0x424C4746u /* "BLGF" */
|
||||||
|
#define BOOT_LOG_VERSION 1u
|
||||||
|
|
||||||
|
typedef struct boot_log_info boot_log_info;
|
||||||
|
|
||||||
|
typedef int (*boot_log_flush_fn)(void);
|
||||||
|
|
||||||
|
struct boot_log_info {
|
||||||
|
u32 magic;
|
||||||
|
u32 version;
|
||||||
|
char *buf;
|
||||||
|
u32 capacity;
|
||||||
|
u32 length;
|
||||||
|
u32 dropped;
|
||||||
|
boot_log_flush_fn flush;
|
||||||
|
};
|
||||||
|
|
||||||
|
static inline void boot_log_append(boot_log_info *bi, const char *s) {
|
||||||
|
if (!bi || bi->magic != BOOT_LOG_MAGIC || !s || !bi->buf)
|
||||||
|
return;
|
||||||
|
while (*s) {
|
||||||
|
if (bi->length < bi->capacity)
|
||||||
|
bi->buf[bi->length++] = *s;
|
||||||
|
else
|
||||||
|
bi->dropped++;
|
||||||
|
s++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void boot_log_bind(boot_log_info **slot, u64 utsm_state) {
|
||||||
|
if (!slot)
|
||||||
|
return;
|
||||||
|
*slot = 0;
|
||||||
|
if (!utsm_state)
|
||||||
|
return;
|
||||||
|
boot_log_info *bi = (boot_log_info *)(u64)utsm_state;
|
||||||
|
if (bi && bi->magic == BOOT_LOG_MAGIC && bi->buf && bi->capacity)
|
||||||
|
*slot = bi;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int boot_log_try_flush(boot_log_info *bi) {
|
||||||
|
if (bi && bi->flush)
|
||||||
|
return bi->flush();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* BOOT_LOG_H */
|
||||||
@@ -0,0 +1,856 @@
|
|||||||
|
/* fat32_write.h - Deshab 共享 FAT32 写库(static inline,无库)
|
||||||
|
*
|
||||||
|
* 前置条件:包含者已定义 u8/u16/u32/u64/i64 基础类型。
|
||||||
|
* 通过 f32w_init() 注入 block_read/block_write 函数指针(签名与 f32_/gpt_ 库一致)。
|
||||||
|
*
|
||||||
|
* 与 fat32_io.h 的分工:
|
||||||
|
* fat32_io.h - 8.3 短名读写,依赖 256 扇区 BPB+FAT+根目录缓存窗口
|
||||||
|
* fat32_write.h - LFN 长名创建 / mkdir / 目录簇链扩展,FAT 按需单扇区访问
|
||||||
|
*
|
||||||
|
* 本库解决 fat32_io.h 的三个写缺口(大 ESP 场景,2GB: spc=8, FAT=4086 扇区,
|
||||||
|
* 根目录在分区 LBA 8204,均远超 256 扇区窗口):
|
||||||
|
* 1. LFN 生成:VFAT LFN 链(每项 13 UTF-16 字符,逆序存储,0x40 标志 +
|
||||||
|
* 短名 checksum),配套 ~N 数字尾巴的 8.3 短名生成与唯一化
|
||||||
|
* 2. mkdir:分配目录簇 + 写入 "." / ".." 项 + 父目录插入 LFN 条目
|
||||||
|
* 3. 目录簇链扩展:目录槽位不足时自动分配新簇并接入链尾(槽位可跨簇)
|
||||||
|
*
|
||||||
|
* FAT 访问策略:按需读改写单个 FAT 扇区(含缓存),修改后立即回写所有 FAT
|
||||||
|
* 副本(BPB->fc 份)。不依赖任何批量缓存窗口,任意簇号/FAT 偏移均可访问。
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* f32w_init(block_read, block_write) - 初始化(注入块设备)
|
||||||
|
* f32w_write_file(dir_clus, "user.conf", d, n) - 目录中写/替换长名文件
|
||||||
|
* f32w_mkdir(dir_clus, "MyDir", &out_clus) - 创建子目录(LFN)
|
||||||
|
* f32w_resolve_dir("system/deshab64", &clus) - 按路径定位目录簇
|
||||||
|
* f32w_write_path("system/deshab64/user.conf", d, n)- 按路径写文件(LFN)
|
||||||
|
* f32w_mkdir_path("system/新建目录", &clus) - 按路径建目录
|
||||||
|
* f32w_find(dir_clus, name, want_dir, ...) - 按长名查找(LFN 优先)
|
||||||
|
*
|
||||||
|
* 缓冲区:每包含者约 9KB BSS(512 BPB + 512 FAT 扇区 + 簇缓冲 ×2)。
|
||||||
|
* 与 fat32_io.h 可同时包含(前缀 f32w_ / f32_ 不冲突),但二者状态独立。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef DESHAB_FAT32_WRITE_H
|
||||||
|
#define DESHAB_FAT32_WRITE_H
|
||||||
|
|
||||||
|
#include "fat32_lfn.h"
|
||||||
|
|
||||||
|
/* ---- block 设备函数类型(与 fat32_io.h / gpt_write.h 一致:index 固定 0) ---- */
|
||||||
|
typedef int (*f32w_block_read_fn)(u32 index, u64 lba, u32 count, void *buf);
|
||||||
|
typedef int (*f32w_block_write_fn)(u32 index, u64 lba, u32 count, const void *buf);
|
||||||
|
|
||||||
|
static f32w_block_read_fn f32w_blk_read = 0;
|
||||||
|
static f32w_block_write_fn f32w_blk_write = 0;
|
||||||
|
|
||||||
|
/* ---- 目录簇缓冲大小(含 data 暂存缓冲)。build.ps1 mkfs spc=8 -> 4096。
|
||||||
|
* 包含者可在 include 前 #define F32W_CLUS_BYTES 覆盖(须为 512 的倍数)。 */
|
||||||
|
#ifndef F32W_CLUS_BYTES
|
||||||
|
#define F32W_CLUS_BYTES 4096
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define F32W_EOC 0x0FFFFFFFu
|
||||||
|
#define F32W_EOC_VAL 0x0FFFFFFFu /* 写入 FAT 的 EOC 值(高 4 位保留位置 0x0Fxxxxxx 已含) */
|
||||||
|
|
||||||
|
/* ---- BPB(512B,与 f32_bpb 布局一致;独立定义避免依赖 fat32_io.h) ---- */
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
u8 jmp[3]; char oem[8]; u16 bps; u8 spc; u16 rsvd; u8 fc; u16 root_ent;
|
||||||
|
u16 ts16; u8 media; u16 spf16; u16 spt; u16 heads; u32 hidden; u32 ts32;
|
||||||
|
u32 spf; u16 flags; u16 ver; u32 root_clus; u16 fsi; u16 bkboot;
|
||||||
|
u8 res[12]; u8 drv; u8 ntfl; u8 sig; u32 ser; char lbl[11]; char typ[8];
|
||||||
|
u8 code[420]; u16 boot_sig;
|
||||||
|
} f32w_bpb_t;
|
||||||
|
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
char name[11]; u8 attr; u8 ntr; u8 ctenth;
|
||||||
|
u16 ctime; u16 cdate; u16 adate; u16 chigh;
|
||||||
|
u16 wtime; u16 wdate; u16 clow; u32 fsize;
|
||||||
|
} f32w_dirent_t;
|
||||||
|
|
||||||
|
/* ---- 挂载上下文(f32w_bpb_load 填充) ---- */
|
||||||
|
typedef struct {
|
||||||
|
u32 spc; /* 每簇扇区数 */
|
||||||
|
u32 clus_bytes; /* spc * 512 */
|
||||||
|
u32 fat_lba; /* FAT 副本 0 起始 LBA(= rsvd) */
|
||||||
|
u32 fat_copies; /* FAT 副本数(BPB->fc,实际至少 2) */
|
||||||
|
u32 fat_sectors; /* 单份 FAT 扇区数(BPB->spf) */
|
||||||
|
u32 data_lba; /* 数据区起始 LBA */
|
||||||
|
u32 root_clus; /* 根目录首簇 */
|
||||||
|
u32 fat_entries; /* FAT 项数上界 = spf*512/4 */
|
||||||
|
int loaded;
|
||||||
|
} f32w_ctx;
|
||||||
|
|
||||||
|
static f32w_ctx f32w_c;
|
||||||
|
|
||||||
|
/* ---- 内部缓冲(每包含者独立) ---- */
|
||||||
|
static u8 f32w_sec[512]; /* BPB / FSInfo 扇区缓冲 */
|
||||||
|
static u8 f32w_fat[512]; /* FAT 单扇区缓存 */
|
||||||
|
static u32 f32w_fat_sec_no = 0xFFFFFFFFu; /* 缓存的 FAT 扇区号(FAT 内偏移) */
|
||||||
|
static u8 f32w_dir[F32W_CLUS_BYTES]; /* 目录簇缓存(槽位扫描/条目写入) */
|
||||||
|
static u32 f32w_dir_no = 0; /* 缓存的目录簇号(0=无,簇号最小为 2) */
|
||||||
|
static int f32w_dir_dirty = 0;
|
||||||
|
static u8 f32w_stage[F32W_CLUS_BYTES]; /* 数据写入暂存(尾簇零填充) */
|
||||||
|
static u32 f32w_alloc_hint = 2; /* 空闲簇分配起点提示 */
|
||||||
|
|
||||||
|
/* ---- 初始化:注入 block_read/block_write 函数指针 ---- */
|
||||||
|
static inline void f32w_init(f32w_block_read_fn rd, f32w_block_write_fn wr) {
|
||||||
|
f32w_blk_read = rd;
|
||||||
|
f32w_blk_write = wr;
|
||||||
|
f32w_c.loaded = 0;
|
||||||
|
f32w_fat_sec_no = 0xFFFFFFFFu;
|
||||||
|
f32w_dir_no = 0;
|
||||||
|
f32w_dir_dirty = 0;
|
||||||
|
f32w_alloc_hint = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int f32w_read_sectors(u32 lba, u32 count, u8 *out) {
|
||||||
|
return f32w_blk_read ? f32w_blk_read(0, lba, count, out) : -1;
|
||||||
|
}
|
||||||
|
static inline int f32w_write_sectors(u32 lba, u32 count, const u8 *buf) {
|
||||||
|
return f32w_blk_write ? f32w_blk_write(0, lba, count, buf) : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 小工具(无库约束:手写循环替代 mem*) ---- */
|
||||||
|
static inline void f32w_mem_set(u8 *p, u8 v, u32 n) { for (u32 i = 0; i < n; i++) p[i] = v; }
|
||||||
|
static inline u32 f32w_r32(const u8 *p) {
|
||||||
|
return (u32)p[0] | ((u32)p[1] << 8) | ((u32)p[2] << 16) | ((u32)p[3] << 24);
|
||||||
|
}
|
||||||
|
static inline u16 f32w_r16(const u8 *p) { return (u16)p[0] | ((u16)p[1] << 8); }
|
||||||
|
|
||||||
|
/* ---- BPB 加载 / 挂载上下文 ---- */
|
||||||
|
static inline int f32w_bpb_load(void) {
|
||||||
|
if (f32w_c.loaded) return 0;
|
||||||
|
if (f32w_read_sectors(0, 1, f32w_sec) != 0) return -1;
|
||||||
|
const f32w_bpb_t *b = (const f32w_bpb_t *)f32w_sec;
|
||||||
|
if (b->boot_sig != 0xAA55 || b->bps != 512) return -2;
|
||||||
|
if (b->spf == 0 || b->root_clus < 2 || b->spc == 0) return -3;
|
||||||
|
u32 fc = b->fc ? b->fc : 1;
|
||||||
|
u32 cb = (u32)b->spc * 512;
|
||||||
|
if (cb > F32W_CLUS_BYTES) return -4; /* 簇超出缓冲,包含者应调大 F32W_CLUS_BYTES */
|
||||||
|
f32w_c.spc = b->spc;
|
||||||
|
f32w_c.clus_bytes = cb;
|
||||||
|
f32w_c.fat_lba = b->rsvd;
|
||||||
|
f32w_c.fat_copies = fc;
|
||||||
|
f32w_c.fat_sectors = b->spf;
|
||||||
|
f32w_c.data_lba = b->rsvd + fc * b->spf;
|
||||||
|
f32w_c.root_clus = b->root_clus;
|
||||||
|
f32w_c.fat_entries = b->spf * 512 / 4;
|
||||||
|
f32w_c.loaded = 1;
|
||||||
|
f32w_fat_sec_no = 0xFFFFFFFFu; /* 新挂载,作废 FAT 缓存 */
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline u32 f32w_clus_lba(u32 clus) {
|
||||||
|
return f32w_c.data_lba + (clus - 2) * f32w_c.spc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- FAT 按需访问(单扇区缓存 + 全副本回写) ---- */
|
||||||
|
|
||||||
|
/* 读 FAT 项。返回 0 成功,*out 为 28 位簇值。 */
|
||||||
|
static inline int f32w_fat_get(u32 clus, u32 *out) {
|
||||||
|
if (clus < 2 || clus >= f32w_c.fat_entries) return -1;
|
||||||
|
u32 sec = clus >> 7; /* 每扇区 128 项 */
|
||||||
|
u32 off = (clus & 127) * 4;
|
||||||
|
if (sec != f32w_fat_sec_no) {
|
||||||
|
if (f32w_read_sectors(f32w_c.fat_lba + sec, 1, f32w_fat) != 0) return -2;
|
||||||
|
f32w_fat_sec_no = sec;
|
||||||
|
}
|
||||||
|
*out = f32w_r32(f32w_fat + off) & 0x0FFFFFFF;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 写 FAT 项(含 28 位掩码),立即回写所有 FAT 副本对应扇区。 */
|
||||||
|
static inline int f32w_fat_set(u32 clus, u32 val) {
|
||||||
|
if (clus < 2 || clus >= f32w_c.fat_entries) return -1;
|
||||||
|
u32 sec = clus >> 7;
|
||||||
|
u32 off = (clus & 127) * 4;
|
||||||
|
if (sec != f32w_fat_sec_no) {
|
||||||
|
if (f32w_read_sectors(f32w_c.fat_lba + sec, 1, f32w_fat) != 0) return -2;
|
||||||
|
f32w_fat_sec_no = sec;
|
||||||
|
}
|
||||||
|
u32 old = f32w_r32(f32w_fat + off);
|
||||||
|
val &= 0x0FFFFFFF;
|
||||||
|
f32w_fat[off] = (u8)(val & 0xFF);
|
||||||
|
f32w_fat[off + 1] = (u8)((val >> 8) & 0xFF);
|
||||||
|
f32w_fat[off + 2] = (u8)((val >> 16) & 0xFF);
|
||||||
|
f32w_fat[off + 3] = (u8)((old & 0xF0) | ((val >> 24) & 0x0F));
|
||||||
|
for (u32 f = 0; f < f32w_c.fat_copies; f++) {
|
||||||
|
u32 lba = f32w_c.fat_lba + f * f32w_c.fat_sectors + sec;
|
||||||
|
if (f32w_write_sectors(lba, 1, f32w_fat) != 0) return -3;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分配一个空闲簇(线性扫描 + 起点提示轮转)。返回簇号,0=已满/失败。 */
|
||||||
|
static inline u32 f32w_alloc_free(void) {
|
||||||
|
u32 total = f32w_c.fat_entries > 2 ? f32w_c.fat_entries - 2 : 0;
|
||||||
|
if (total == 0) return 0;
|
||||||
|
u32 start = (f32w_alloc_hint >= 2 && f32w_alloc_hint < f32w_c.fat_entries)
|
||||||
|
? f32w_alloc_hint : 2;
|
||||||
|
for (u32 i = 0; i < total; i++) {
|
||||||
|
u32 c = 2 + ((start - 2 + i) % total);
|
||||||
|
u32 v = 0;
|
||||||
|
if (f32w_fat_get(c, &v) != 0) return 0;
|
||||||
|
if (v == 0) {
|
||||||
|
f32w_alloc_hint = c + 1;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 释放整条簇链(逐项清 0)。带环保护。 */
|
||||||
|
static inline int f32w_free_chain(u32 clus) {
|
||||||
|
u32 cur = clus;
|
||||||
|
u64 guard = 0;
|
||||||
|
while (cur >= 2 && cur < 0x0FFFFFF8) {
|
||||||
|
if (++guard > (u64)f32w_c.fat_entries + 2) return -1; /* FAT 成环,损坏 */
|
||||||
|
u32 next = 0;
|
||||||
|
if (f32w_fat_get(cur, &next) != 0) return -2;
|
||||||
|
if (f32w_fat_set(cur, 0) != 0) return -3;
|
||||||
|
cur = next;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 目录簇缓存(槽位扫描 / 条目写入共用) ---- */
|
||||||
|
static inline int f32w_dir_flush(void) {
|
||||||
|
if (!f32w_dir_dirty || f32w_dir_no == 0) { f32w_dir_dirty = 0; return 0; }
|
||||||
|
if (f32w_write_sectors(f32w_clus_lba(f32w_dir_no), f32w_c.spc, f32w_dir) != 0) return -1;
|
||||||
|
f32w_dir_dirty = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int f32w_dir_load(u32 clus) {
|
||||||
|
if (f32w_dir_no == clus && clus != 0) return 0;
|
||||||
|
if (f32w_dir_flush() != 0) return -1;
|
||||||
|
if (f32w_read_sectors(f32w_clus_lba(clus), f32w_c.spc, f32w_dir) != 0) return -2;
|
||||||
|
f32w_dir_no = clus;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 在 (clus, idx) 写入 32 字节目录项。 */
|
||||||
|
static inline int f32w_entry_write(u32 clus, u32 idx, const u8 *e32) {
|
||||||
|
if (idx * 32 + 32 > f32w_c.clus_bytes) return -1;
|
||||||
|
if (f32w_dir_load(clus) != 0) return -2;
|
||||||
|
for (int i = 0; i < 32; i++) f32w_dir[idx * 32 + i] = e32[i];
|
||||||
|
f32w_dir_dirty = 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 槽位步进:同簇内 idx+1,越界则沿 FAT 链进入下一簇(run 保证连续)。 */
|
||||||
|
static inline int f32w_slot_step(u32 *clus, u32 *idx) {
|
||||||
|
(*idx)++;
|
||||||
|
if ((*idx) * 32 >= f32w_c.clus_bytes) {
|
||||||
|
u32 next = 0;
|
||||||
|
if (f32w_fat_get(*clus, &next) != 0) return -1;
|
||||||
|
if (next < 2 || next >= 0x0FFFFFF8) return -1;
|
||||||
|
*clus = next;
|
||||||
|
*idx = 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 8.3 短名生成(含 ~N 唯一化) ---- */
|
||||||
|
|
||||||
|
static inline char f32w_up(char c) { return (c >= 'a' && c <= 'z') ? (char)(c - 32) : c; }
|
||||||
|
|
||||||
|
/* 合法 8.3 短名字符(已转大写后):字母/数字/!#$%&'()-@^_`{}~ */
|
||||||
|
static inline int f32w_char_ok_83(char c) {
|
||||||
|
if (c >= 'A' && c <= 'Z') return 1;
|
||||||
|
if (c >= '0' && c <= '9') return 1;
|
||||||
|
if (c == '!' || c == '#' || c == '$' || c == '%' || c == '&' || c == '\'') return 1;
|
||||||
|
if (c == '(' || c == ')' || c == '-' || c == '@' || c == '^' || c == '_') return 1;
|
||||||
|
if (c == '`' || c == '{' || c == '}' || c == '~') return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 名字长度(手动 strlen)。 */
|
||||||
|
static inline u32 f32w_strlen(const char *s) {
|
||||||
|
u32 n = 0;
|
||||||
|
while (s[n]) n++;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 生成长名对应的 8.3 短名(未唯一化)。
|
||||||
|
* name11 : 输出 11 字符短名(大写、空格填充;非法字符转 '_',超长截断)
|
||||||
|
* needs_lfn: 输出 1=长名无法无损表示为 8.3(需写 LFN 链 + 后续 ~N 唯一化)
|
||||||
|
* 返回 0 成功,-1 名字非法(空/过长/含非 ASCII/纯点)。 */
|
||||||
|
static inline int f32w_make_short(const char *name, char name11[11], int *needs_lfn) {
|
||||||
|
u32 len = f32w_strlen(name);
|
||||||
|
if (len == 0 || len > 255) return -1;
|
||||||
|
u32 last_dot = 0xFFFFFFFFu;
|
||||||
|
u32 dot_count = 0;
|
||||||
|
int all_upper_valid = 1;
|
||||||
|
for (u32 i = 0; i < len; i++) {
|
||||||
|
char c = name[i];
|
||||||
|
if ((u8)c < 0x20 || (u8)c >= 0x7F || c == '/') return -1;
|
||||||
|
if (c == '.') { last_dot = i; dot_count++; }
|
||||||
|
if (c >= 'a' && c <= 'z') all_upper_valid = 0;
|
||||||
|
else if (!f32w_char_ok_83(f32w_up(c))) all_upper_valid = 0;
|
||||||
|
}
|
||||||
|
/* 纯点 / 尾点 / 首空格:非法(简化处理,与 Windows 行为近似) */
|
||||||
|
int non_dot = 0;
|
||||||
|
for (u32 i = 0; i < len; i++) if (name[i] != '.') { non_dot = 1; break; }
|
||||||
|
if (!non_dot || name[len - 1] == '.' || name[0] == ' ') return -1;
|
||||||
|
|
||||||
|
u32 base_end = (last_dot == 0xFFFFFFFFu) ? len : last_dot;
|
||||||
|
u32 base_len = base_end;
|
||||||
|
u32 ext_len = (last_dot == 0xFFFFFFFFu) ? 0 : len - last_dot - 1;
|
||||||
|
|
||||||
|
*needs_lfn = !(all_upper_valid && base_len <= 8 && ext_len <= 3 && dot_count <= 1);
|
||||||
|
|
||||||
|
for (int i = 0; i < 11; i++) name11[i] = ' ';
|
||||||
|
u32 bi = 0;
|
||||||
|
for (u32 i = 0; i < base_len && bi < 8; i++) {
|
||||||
|
char c = f32w_up(name[i]);
|
||||||
|
if (!f32w_char_ok_83(c)) c = '_';
|
||||||
|
name11[bi++] = c;
|
||||||
|
}
|
||||||
|
if (bi == 0) name11[bi++] = '_'; /* 空基名(如 ".config")用 '_' 兜底 */
|
||||||
|
u32 ei = 0;
|
||||||
|
if (last_dot != 0xFFFFFFFFu) {
|
||||||
|
for (u32 i = 0; i < ext_len && ei < 3; i++) {
|
||||||
|
char c = f32w_up(name[last_dot + 1 + i]);
|
||||||
|
if (!f32w_char_ok_83(c)) c = '_';
|
||||||
|
name11[8 + ei++] = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 目录中是否已存在指定 11 字符短名(仅比较短名项)。1=存在,0=不存在,-1=IO 错。 */
|
||||||
|
static inline int f32w_dir_short_exists(u32 dir_clus, const char name11[11]) {
|
||||||
|
u32 dclus = dir_clus;
|
||||||
|
u64 guard = 0;
|
||||||
|
while (dclus >= 2 && dclus < 0x0FFFFFF8) {
|
||||||
|
if (++guard > (u64)f32w_c.fat_entries + 2) return -1;
|
||||||
|
if (f32w_dir_load(dclus) != 0) return -1;
|
||||||
|
u32 max_e = f32w_c.clus_bytes / 32;
|
||||||
|
for (u32 e = 0; e < max_e; e++) {
|
||||||
|
const u8 *ent = f32w_dir + e * 32;
|
||||||
|
if (ent[0] == 0x00) return 0; /* 目录结尾 */
|
||||||
|
if (ent[0] == 0xE5) continue;
|
||||||
|
if (ent[11] == 0x0F) continue; /* LFN 项 */
|
||||||
|
if (ent[11] & 0x08) continue; /* 卷标 */
|
||||||
|
int same = 1;
|
||||||
|
for (int i = 0; i < 11; i++)
|
||||||
|
if (ent[i] != (u8)name11[i]) { same = 0; break; }
|
||||||
|
if (same) return 1;
|
||||||
|
}
|
||||||
|
u32 next = 0;
|
||||||
|
if (f32w_fat_get(dclus, &next) != 0) return -1;
|
||||||
|
dclus = next;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 生成长名对应的唯一 8.3 短名(lossy 时追加 ~1..~999999 数字尾巴)。
|
||||||
|
* 返回 0 成功,负值失败。 */
|
||||||
|
static inline int f32w_make_unique_short(u32 dir_clus, const char *name, char name11[11]) {
|
||||||
|
int needs_lfn = 0;
|
||||||
|
if (f32w_make_short(name, name11, &needs_lfn) != 0) return -1;
|
||||||
|
if (!needs_lfn) return 0; /* 无损 8.3,同名冲突由 f32w_find 语义覆盖 */
|
||||||
|
/* 提取 make_short 产生的 base / ext,枚举 BASE~N 直到唯一 */
|
||||||
|
char base[8];
|
||||||
|
u32 base_len = 0;
|
||||||
|
for (u32 i = 0; i < 8 && name11[i] != ' '; i++) base[base_len++] = name11[i];
|
||||||
|
if (base_len == 0) { base[0] = '_'; base_len = 1; }
|
||||||
|
for (u32 n = 1; n <= 999999u; n++) {
|
||||||
|
char digits[8];
|
||||||
|
int dn = 0;
|
||||||
|
u32 v = n;
|
||||||
|
while (v) { digits[dn++] = (char)('0' + (v % 10)); v /= 10; }
|
||||||
|
u32 bl = (dn + 1 >= 8) ? 1 : 8 - dn - 1; /* 保留 '~'+数字 的空间 */
|
||||||
|
if (base_len < bl) bl = base_len;
|
||||||
|
for (u32 i = 0; i < 8; i++) name11[i] = ' ';
|
||||||
|
for (u32 i = 0; i < bl; i++) name11[i] = base[i];
|
||||||
|
u32 p = bl;
|
||||||
|
name11[p++] = '~';
|
||||||
|
for (int i = dn - 1; i >= 0; i--) name11[p++] = digits[i];
|
||||||
|
int ex = f32w_dir_short_exists(dir_clus, name11);
|
||||||
|
if (ex < 0) return -2;
|
||||||
|
if (ex == 0) return 0;
|
||||||
|
}
|
||||||
|
return -3; /* 序号耗尽 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- LFN 条目生成 ---- */
|
||||||
|
|
||||||
|
/* 13 个 UTF-16 槽位在 32 字节目录项内的字节偏移(name1[5] name2[6] name3[2]) */
|
||||||
|
static inline int f32w_lfn_slot_off(int slot) {
|
||||||
|
if (slot < 5) return 1 + slot * 2;
|
||||||
|
if (slot < 11) return 14 + (slot - 5) * 2;
|
||||||
|
return 28 + (slot - 11) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 构造第 ordinal 个 LFN 片段项(ordinal 1..total,total 为链长)。
|
||||||
|
* name 为 ASCII 长名,len 为字符数。out32 接收 32 字节编码。
|
||||||
|
* 最高序号项(ordinal==total)带 0x40 标志,含 0x0000 终止 + 0xFFFF 填充。
|
||||||
|
* 返回 0 成功。 */
|
||||||
|
static inline int f32w_build_lfn_entry(int ordinal, int total, const char *name,
|
||||||
|
u32 len, u8 cksum, u8 *out32) {
|
||||||
|
f32w_mem_set(out32, 0, 32);
|
||||||
|
/* 全槽位预填 0xFFFF */
|
||||||
|
for (int s = 0; s < 13; s++) {
|
||||||
|
int off = f32w_lfn_slot_off(s);
|
||||||
|
out32[off] = 0xFF;
|
||||||
|
out32[off + 1] = 0xFF;
|
||||||
|
}
|
||||||
|
u32 base = (u32)(ordinal - 1) * 13;
|
||||||
|
u32 cnt = (len - base) < 13 ? (len - base) : 13;
|
||||||
|
for (u32 j = 0; j < cnt; j++) {
|
||||||
|
int off = f32w_lfn_slot_off((int)j);
|
||||||
|
u16 c = (u16)(u8)name[base + j]; /* ASCII -> UTF-16 */
|
||||||
|
out32[off] = (u8)(c & 0xFF);
|
||||||
|
out32[off + 1] = (u8)(c >> 8);
|
||||||
|
}
|
||||||
|
if (ordinal == total && cnt < 13) {
|
||||||
|
int off = f32w_lfn_slot_off((int)cnt); /* 0x0000 终止符 */
|
||||||
|
out32[off] = 0;
|
||||||
|
out32[off + 1] = 0;
|
||||||
|
}
|
||||||
|
out32[0] = (u8)(ordinal | (ordinal == total ? 0x40 : 0x00));
|
||||||
|
out32[11] = 0x0F; /* ATTR_LONG_NAME */
|
||||||
|
out32[12] = 0; /* type */
|
||||||
|
out32[13] = cksum;
|
||||||
|
out32[26] = 0; /* 首簇号必须为 0 */
|
||||||
|
out32[27] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 构造 8.3 短名项。 */
|
||||||
|
static inline void f32w_build_short_entry(const char name11[11], u8 attr,
|
||||||
|
u32 clus, u32 size, u8 *out32) {
|
||||||
|
f32w_mem_set(out32, 0, 32);
|
||||||
|
for (int i = 0; i < 11; i++) out32[i] = (u8)name11[i];
|
||||||
|
out32[11] = attr;
|
||||||
|
/* 时间戳字段保持 0(与 fat32_io.h 写路径一致) */
|
||||||
|
out32[21] = (u8)((clus >> 16) & 0xFF); /* chigh */
|
||||||
|
out32[20] = (u8)((clus >> 24) & 0xFF);
|
||||||
|
out32[26] = (u8)(clus & 0xFF); /* clow */
|
||||||
|
out32[27] = (u8)((clus >> 8) & 0xFF);
|
||||||
|
out32[28] = (u8)(size & 0xFF); /* fsize */
|
||||||
|
out32[29] = (u8)((size >> 8) & 0xFF);
|
||||||
|
out32[30] = (u8)((size >> 16) & 0xFF);
|
||||||
|
out32[31] = (u8)((size >> 24) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 目录槽位分配(含簇链扩展,槽位可跨簇) ---- */
|
||||||
|
|
||||||
|
/* 在目录簇链中找 count 个连续空闲槽位(0x00/0xE5)。链尾不足时自动分配
|
||||||
|
* 新簇:FAT 接链 + 新簇清零 + 落盘,槽位 run 跨簇继续累计。
|
||||||
|
* out_clus/out_idx : run 首槽位位置(簇号 + 簇内项索引)
|
||||||
|
* 返回 0 成功,负值失败(-3=磁盘满)。 */
|
||||||
|
static inline int f32w_dir_alloc_slots(u32 dir_clus, u32 count,
|
||||||
|
u32 *out_clus, u32 *out_idx) {
|
||||||
|
u32 run_clus = 0, run_idx = 0, run = 0;
|
||||||
|
u32 dclus = dir_clus;
|
||||||
|
u64 guard = 0;
|
||||||
|
while (dclus >= 2 && dclus < 0x0FFFFFF8) {
|
||||||
|
if (++guard > (u64)f32w_c.fat_entries + 2) return -1; /* FAT 成环 */
|
||||||
|
if (f32w_dir_load(dclus) != 0) return -2;
|
||||||
|
u32 max_e = f32w_c.clus_bytes / 32;
|
||||||
|
for (u32 e = 0; e < max_e; e++) {
|
||||||
|
u8 first = f32w_dir[e * 32];
|
||||||
|
if (first == 0x00 || first == 0xE5) {
|
||||||
|
if (run == 0) { run_clus = dclus; run_idx = e; }
|
||||||
|
run++;
|
||||||
|
if (run >= count) {
|
||||||
|
*out_clus = run_clus;
|
||||||
|
*out_idx = run_idx;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
run = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u32 next = 0;
|
||||||
|
if (f32w_fat_get(dclus, &next) != 0) return -2;
|
||||||
|
if (next >= 2 && next < 0x0FFFFFF8) { dclus = next; continue; }
|
||||||
|
/* 链尾且 run 不足:扩展一个新簇接入链尾 */
|
||||||
|
u32 nc = f32w_alloc_free();
|
||||||
|
if (nc == 0) return -3;
|
||||||
|
if (f32w_fat_set(dclus, nc) != 0) return -4;
|
||||||
|
if (f32w_fat_set(nc, F32W_EOC_VAL) != 0) return -4;
|
||||||
|
if (f32w_dir_flush() != 0) return -5;
|
||||||
|
f32w_mem_set(f32w_dir, 0, f32w_c.clus_bytes); /* 新簇全 0 */
|
||||||
|
f32w_dir_no = nc;
|
||||||
|
f32w_dir_dirty = 1; /* 脏标记使 flush 落盘清零内容 */
|
||||||
|
if (f32w_dir_flush() != 0) return -5;
|
||||||
|
dclus = nc;
|
||||||
|
/* run 跨簇继续累计(新簇全 0x00,下一轮循环继续计数) */
|
||||||
|
}
|
||||||
|
return -6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 按长名查找(LFN 优先,8.3 回退;大小写不敏感) ---- */
|
||||||
|
|
||||||
|
/* 在目录中查找 name(长名或 8.3 显示名)。
|
||||||
|
* want_dir : -1 任意,0 只匹配文件,1 只匹配目录
|
||||||
|
* out_clus/size : 目标首簇 / 大小(可 NULL)
|
||||||
|
* out_ent_clus/idx : 短名项位置(可 NULL,用于原地更新)
|
||||||
|
* out_attr : 目录项属性(可 NULL)
|
||||||
|
* 返回 0 找到,-1 未找到,其他负值 IO 错。LFN 缓冲跨簇存活(链可跨簇)。 */
|
||||||
|
static inline int f32w_find(u32 dir_clus, const char *name, int want_dir,
|
||||||
|
u32 *out_clus, u32 *out_size,
|
||||||
|
u32 *out_ent_clus, u32 *out_ent_idx, u8 *out_attr) {
|
||||||
|
if (dir_clus == 0) dir_clus = f32w_c.root_clus;
|
||||||
|
u32 dclus = dir_clus;
|
||||||
|
u64 guard = 0;
|
||||||
|
fat32_lfn_buf lfn;
|
||||||
|
fat32_lfn_init(&lfn);
|
||||||
|
while (dclus >= 2 && dclus < 0x0FFFFFF8) {
|
||||||
|
if (++guard > (u64)f32w_c.fat_entries + 2) return -2;
|
||||||
|
if (f32w_dir_load(dclus) != 0) return -3;
|
||||||
|
u32 max_e = f32w_c.clus_bytes / 32;
|
||||||
|
for (u32 e = 0; e < max_e; e++) {
|
||||||
|
const u8 *ent = f32w_dir + e * 32;
|
||||||
|
if (ent[0] == 0x00) return -1; /* 目录结尾,未找到 */
|
||||||
|
if (ent[0] == 0xE5) { fat32_lfn_init(&lfn); continue; }
|
||||||
|
const f32w_dirent_t *de = (const f32w_dirent_t *)ent;
|
||||||
|
int is_short = fat32_lfn_process(&lfn, ent);
|
||||||
|
if (!is_short) continue;
|
||||||
|
if (de->attr & 0x08) { fat32_lfn_init(&lfn); continue; } /* 卷标 */
|
||||||
|
if (de->name[0] == '.' && (de->name[1] == ' ' || de->name[1] == '.')) {
|
||||||
|
fat32_lfn_init(&lfn); /* "." / ".." */
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int entry_is_dir = (de->attr & 0x10) ? 1 : 0;
|
||||||
|
if (want_dir >= 0 && entry_is_dir != want_dir) {
|
||||||
|
fat32_lfn_init(&lfn);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int matched = 0;
|
||||||
|
if (lfn.valid) {
|
||||||
|
char ascii[FAT32_LFN_MAX];
|
||||||
|
if (fat32_lfn_to_ascii(&lfn, ascii, sizeof(ascii)) >= 0 &&
|
||||||
|
fat32_lfn_streq_ci(ascii, name)) matched = 1;
|
||||||
|
}
|
||||||
|
if (!matched) {
|
||||||
|
char disp[13];
|
||||||
|
fat32_lfn_short_to_str((const u8 *)de->name, disp);
|
||||||
|
if (fat32_lfn_streq_ci(disp, name)) matched = 1;
|
||||||
|
}
|
||||||
|
if (matched) {
|
||||||
|
u32 fc = (u32)f32w_r16((const u8 *)&de->clow) |
|
||||||
|
((u32)f32w_r16((const u8 *)&de->chigh) << 16);
|
||||||
|
if (out_clus) *out_clus = fc;
|
||||||
|
if (out_size) *out_size = de->fsize;
|
||||||
|
if (out_ent_clus) *out_ent_clus = dclus;
|
||||||
|
if (out_ent_idx) *out_ent_idx = e;
|
||||||
|
if (out_attr) *out_attr = de->attr;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
fat32_lfn_init(&lfn);
|
||||||
|
}
|
||||||
|
u32 next = 0;
|
||||||
|
if (f32w_fat_get(dclus, &next) != 0) return -3;
|
||||||
|
dclus = next;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 文件数据簇链写入(新建 / 复用扩展 / 截断收缩) ---- */
|
||||||
|
|
||||||
|
/* 写 size 字节数据。have>=2 时复用现有链(按需扩展或截断并释放多余簇),
|
||||||
|
* have<2 时全新分配。*out_first 输出结果首簇(size==0 时为 0)。
|
||||||
|
* 返回 0 成功,负值失败。 */
|
||||||
|
static inline int f32w_data_write(u32 have, const u8 *data, u32 size, u32 *out_first) {
|
||||||
|
u32 cb = f32w_c.clus_bytes;
|
||||||
|
u32 need = size ? (size + cb - 1) / cb : 0;
|
||||||
|
|
||||||
|
if (need == 0) {
|
||||||
|
if (have >= 2) {
|
||||||
|
if (f32w_free_chain(have) != 0) return -1;
|
||||||
|
}
|
||||||
|
*out_first = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 first = 0, prev = 0, count = 0;
|
||||||
|
if (have >= 2) {
|
||||||
|
first = have;
|
||||||
|
u32 cur = have;
|
||||||
|
while (cur >= 2 && cur < 0x0FFFFFF8 && count < need) {
|
||||||
|
prev = cur;
|
||||||
|
u32 next = 0;
|
||||||
|
if (f32w_fat_get(cur, &next) != 0) return -2;
|
||||||
|
cur = next;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
while (count < need) { /* 现有链不足:扩展 */
|
||||||
|
u32 nc = f32w_alloc_free();
|
||||||
|
if (nc == 0) return -3;
|
||||||
|
if (f32w_fat_set(nc, F32W_EOC_VAL) != 0) return -4;
|
||||||
|
if (f32w_fat_set(prev, nc) != 0) return -4;
|
||||||
|
prev = nc;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
if (cur >= 2 && cur < 0x0FFFFFF8) { /* 现有链过长:截断 */
|
||||||
|
if (f32w_fat_set(prev, F32W_EOC_VAL) != 0) return -4;
|
||||||
|
if (f32w_free_chain(cur) != 0) return -5;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (u32 i = 0; i < need; i++) { /* 全新分配 */
|
||||||
|
u32 nc = f32w_alloc_free();
|
||||||
|
if (nc == 0) return -3;
|
||||||
|
if (f32w_fat_set(nc, F32W_EOC_VAL) != 0) return -4;
|
||||||
|
if (prev) {
|
||||||
|
if (f32w_fat_set(prev, nc) != 0) return -4;
|
||||||
|
} else {
|
||||||
|
first = nc;
|
||||||
|
}
|
||||||
|
prev = nc;
|
||||||
|
}
|
||||||
|
count = need;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 写数据:整簇直写源缓冲,尾簇经暂存缓冲零填充 */
|
||||||
|
u32 remaining = size;
|
||||||
|
const u8 *src = data;
|
||||||
|
u32 c = first;
|
||||||
|
for (u32 i = 0; i < count && remaining > 0; i++) {
|
||||||
|
u32 chunk = remaining < cb ? remaining : cb;
|
||||||
|
u32 lba = f32w_clus_lba(c);
|
||||||
|
if (chunk == cb) {
|
||||||
|
if (f32w_write_sectors(lba, f32w_c.spc, src) != 0) return -6;
|
||||||
|
} else {
|
||||||
|
f32w_mem_set(f32w_stage, 0, cb);
|
||||||
|
for (u32 b = 0; b < chunk; b++) f32w_stage[b] = src[b];
|
||||||
|
if (f32w_write_sectors(lba, f32w_c.spc, f32w_stage) != 0) return -6;
|
||||||
|
}
|
||||||
|
src += chunk;
|
||||||
|
remaining -= chunk;
|
||||||
|
if (i + 1 < count) {
|
||||||
|
u32 next = 0;
|
||||||
|
if (f32w_fat_get(c, &next) != 0) return -2;
|
||||||
|
c = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*out_first = first;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- FSInfo 置未知(写操作后调用,best-effort) ----
|
||||||
|
* 分配/释放后空闲簇数不再准确,置 0xFFFFFFFF(未知)避免 UEFI/Windows
|
||||||
|
* 依赖陈旧计数。忽略失败(无 FSInfo 的镜像不受影响)。 */
|
||||||
|
static inline void f32w_fsinfo_unknown(void) {
|
||||||
|
if (!f32w_c.loaded) return;
|
||||||
|
u32 fsi_lba = 0;
|
||||||
|
{
|
||||||
|
/* 重新读 BPB 拿 FSInfo 扇区号(f32w_sec 此后未被覆盖过也可直接用) */
|
||||||
|
const f32w_bpb_t *b = (const f32w_bpb_t *)f32w_sec;
|
||||||
|
if (b->boot_sig != 0xAA55) return;
|
||||||
|
fsi_lba = b->fsi;
|
||||||
|
}
|
||||||
|
if (fsi_lba == 0 || fsi_lba == 0xFFFF) return;
|
||||||
|
u8 sec[512];
|
||||||
|
if (f32w_read_sectors(fsi_lba, 1, sec) != 0) return;
|
||||||
|
sec[488] = 0xFF; sec[489] = 0xFF; sec[490] = 0xFF; sec[491] = 0xFF;
|
||||||
|
sec[492] = 0xFF; sec[493] = 0xFF; sec[494] = 0xFF; sec[495] = 0xFF;
|
||||||
|
(void)f32w_write_sectors(fsi_lba, 1, sec);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 写入 LFN 链 + 短名项(dir_alloc_slots 已定位 run 起点) ---- */
|
||||||
|
static inline int f32w_insert_entries(u32 dir_clus, const char *name,
|
||||||
|
const char name11[11], u32 clus, u32 size, u8 attr) {
|
||||||
|
u32 len = f32w_strlen(name);
|
||||||
|
int needs_lfn = 0;
|
||||||
|
/* needs_lfn 与调用方 make_unique_short 判定一致:重算(无损则短名回退匹配) */
|
||||||
|
{
|
||||||
|
char tmp11[11];
|
||||||
|
if (f32w_make_short(name, tmp11, &needs_lfn) != 0) return -1;
|
||||||
|
}
|
||||||
|
u32 nlfn = needs_lfn ? (len + 12) / 13 : 0;
|
||||||
|
u32 sc = 0, si = 0;
|
||||||
|
if (f32w_dir_alloc_slots(dir_clus, nlfn + 1, &sc, &si) != 0) return -2;
|
||||||
|
if (needs_lfn) {
|
||||||
|
u8 ck = fat32_lfn_checksum((const u8 *)name11);
|
||||||
|
for (int k = (int)nlfn; k >= 1; k--) { /* 逆序:最高序号最先落盘 */
|
||||||
|
u8 e[32];
|
||||||
|
f32w_build_lfn_entry(k, (int)nlfn, name, len, ck, e);
|
||||||
|
if (f32w_entry_write(sc, si, e) != 0) return -3;
|
||||||
|
if (k > 1 && f32w_slot_step(&sc, &si) != 0) return -4;
|
||||||
|
}
|
||||||
|
if (nlfn > 0 && f32w_slot_step(&sc, &si) != 0) return -4;
|
||||||
|
}
|
||||||
|
u8 se[32];
|
||||||
|
f32w_build_short_entry(name11, attr, clus, size, se);
|
||||||
|
if (f32w_entry_write(sc, si, se) != 0) return -3;
|
||||||
|
if (f32w_dir_flush() != 0) return -5;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 公共 API ================= */
|
||||||
|
|
||||||
|
/* 在目录中写/替换长名文件(LFN 创建 + 短名 ~N 唯一化 + FAT 链管理)。
|
||||||
|
* dir_clus : 目标目录首簇(0=根目录)
|
||||||
|
* name : 长文件名(ASCII;非 8.3 无损表示时自动生成 LFN 链)
|
||||||
|
* data/size: 文件内容(size 可为 0,簇链释放、大小清 0)
|
||||||
|
* 同名存在:文件原地替换内容(保留原目录项/LFN),目录返回 -20。
|
||||||
|
* 返回 0 成功,负值失败。 */
|
||||||
|
static inline int f32w_write_file(u32 dir_clus, const char *name,
|
||||||
|
const u8 *data, u32 size) {
|
||||||
|
if (f32w_bpb_load() != 0) return -1;
|
||||||
|
if (!f32w_blk_write) return -2;
|
||||||
|
if (!name || (!data && size)) return -3;
|
||||||
|
if (dir_clus == 0) dir_clus = f32w_c.root_clus;
|
||||||
|
|
||||||
|
u32 e_clus = 0, e_size = 0, ent_clus = 0, ent_idx = 0;
|
||||||
|
u8 e_attr = 0;
|
||||||
|
int fr = f32w_find(dir_clus, name, -1, &e_clus, &e_size, &ent_clus, &ent_idx, &e_attr);
|
||||||
|
if (fr == 0) {
|
||||||
|
if (e_attr & 0x10) return -20; /* 同名目录已存在 */
|
||||||
|
/* 原地替换:复用/扩展/截断原簇链,仅更新短名项 clus/size */
|
||||||
|
u32 first = 0;
|
||||||
|
if (f32w_data_write(e_clus, data, size, &first) != 0) return -4;
|
||||||
|
if (f32w_dir_load(ent_clus) != 0) return -5;
|
||||||
|
u8 *ent = f32w_dir + ent_idx * 32;
|
||||||
|
ent[20] = (u8)((first >> 24) & 0xFF); /* chigh */
|
||||||
|
ent[21] = (u8)((first >> 16) & 0xFF);
|
||||||
|
ent[26] = (u8)(first & 0xFF); /* clow */
|
||||||
|
ent[27] = (u8)((first >> 8) & 0xFF);
|
||||||
|
ent[28] = (u8)(size & 0xFF); /* fsize */
|
||||||
|
ent[29] = (u8)((size >> 8) & 0xFF);
|
||||||
|
ent[30] = (u8)((size >> 16) & 0xFF);
|
||||||
|
ent[31] = (u8)((size >> 24) & 0xFF);
|
||||||
|
f32w_dir_dirty = 1;
|
||||||
|
if (f32w_dir_flush() != 0) return -5;
|
||||||
|
f32w_fsinfo_unknown();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新建:唯一短名 -> 数据链 -> 目录槽位(含链扩展)-> LFN+短名项 */
|
||||||
|
char name11[11];
|
||||||
|
if (f32w_make_unique_short(dir_clus, name, name11) != 0) return -6;
|
||||||
|
u32 first = 0;
|
||||||
|
if (f32w_data_write(0, data, size, &first) != 0) return -7;
|
||||||
|
if (f32w_insert_entries(dir_clus, name, name11, first, size, 0x20) != 0) return -8;
|
||||||
|
f32w_fsinfo_unknown();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 在目录中创建子目录(LFN + "." / ".." 项 + 父目录插入条目)。
|
||||||
|
* parent_clus: 父目录首簇(0=根目录)
|
||||||
|
* out_clus : 输出新目录首簇(可 NULL)
|
||||||
|
* 返回 0 成功,-20 同名已存在,其他负值失败。 */
|
||||||
|
static inline int f32w_mkdir(u32 parent_clus, const char *name, u32 *out_clus) {
|
||||||
|
if (f32w_bpb_load() != 0) return -1;
|
||||||
|
if (!f32w_blk_write) return -2;
|
||||||
|
if (!name) return -3;
|
||||||
|
if (parent_clus == 0) parent_clus = f32w_c.root_clus;
|
||||||
|
|
||||||
|
int fr = f32w_find(parent_clus, name, -1, 0, 0, 0, 0, 0);
|
||||||
|
if (fr == 0) return -20; /* 同名已存在 */
|
||||||
|
|
||||||
|
char name11[11];
|
||||||
|
if (f32w_make_unique_short(parent_clus, name, name11) != 0) return -6;
|
||||||
|
|
||||||
|
/* 分配目录簇并初始化 "." / ".." */
|
||||||
|
u32 nc = f32w_alloc_free();
|
||||||
|
if (nc == 0) return -21;
|
||||||
|
if (f32w_fat_set(nc, F32W_EOC_VAL) != 0) return -22;
|
||||||
|
if (f32w_dir_flush() != 0) return -23;
|
||||||
|
f32w_mem_set(f32w_dir, 0, f32w_c.clus_bytes);
|
||||||
|
{
|
||||||
|
u8 dot[32], dotdot[32];
|
||||||
|
char n_self[11] = {'.',' ',' ',' ',' ',' ',' ',' ',' ',' ',' '};
|
||||||
|
char n_up[11] = {'.','.',' ',' ',' ',' ',' ',' ',' ',' ',' '};
|
||||||
|
u32 parent_link = (parent_clus == f32w_c.root_clus) ? 0 : parent_clus;
|
||||||
|
f32w_build_short_entry(n_self, 0x10, nc, 0, dot);
|
||||||
|
f32w_build_short_entry(n_up, 0x10, parent_link, 0, dotdot);
|
||||||
|
for (int i = 0; i < 32; i++) { f32w_dir[i] = dot[i]; f32w_dir[32 + i] = dotdot[i]; }
|
||||||
|
}
|
||||||
|
f32w_dir_no = nc;
|
||||||
|
f32w_dir_dirty = 1;
|
||||||
|
if (f32w_dir_flush() != 0) return -23;
|
||||||
|
|
||||||
|
/* 父目录插入条目(含链扩展) */
|
||||||
|
if (f32w_insert_entries(parent_clus, name, name11, nc, 0, 0x10) != 0) return -8;
|
||||||
|
f32w_fsinfo_unknown();
|
||||||
|
if (out_clus) *out_clus = nc;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 路径辅助('/' 分隔,每段长名;中间目录必须已存在) ---- */
|
||||||
|
|
||||||
|
/* 定位路径目录簇。空路径/"'/'" 返回根目录。 */
|
||||||
|
static inline int f32w_resolve_dir(const char *path, u32 *out_clus) {
|
||||||
|
if (!path || !out_clus) return -1;
|
||||||
|
if (f32w_bpb_load() != 0) return -2;
|
||||||
|
u32 cur = f32w_c.root_clus;
|
||||||
|
u32 pos = 0;
|
||||||
|
u32 path_len = f32w_strlen(path);
|
||||||
|
if (path_len == 0 || (path_len == 1 && path[0] == '/')) {
|
||||||
|
*out_clus = cur;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
while (pos < path_len) {
|
||||||
|
while (pos < path_len && path[pos] == '/') pos++;
|
||||||
|
if (pos >= path_len) break;
|
||||||
|
char comp[260];
|
||||||
|
u32 ci = 0;
|
||||||
|
while (pos < path_len && path[pos] != '/' && ci + 1 < sizeof(comp))
|
||||||
|
comp[ci++] = path[pos++];
|
||||||
|
comp[ci] = 0;
|
||||||
|
if (ci == 0) continue;
|
||||||
|
u32 sub = 0;
|
||||||
|
if (f32w_find(cur, comp, 1, &sub, 0, 0, 0, 0) != 0) return -3;
|
||||||
|
cur = sub;
|
||||||
|
}
|
||||||
|
*out_clus = cur;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按路径写文件(父目录必须已存在)。返回 0 成功,负值失败。 */
|
||||||
|
static inline int f32w_write_path(const char *path, const u8 *data, u32 size) {
|
||||||
|
if (!path) return -1;
|
||||||
|
if (f32w_bpb_load() != 0) return -2;
|
||||||
|
u32 path_len = f32w_strlen(path);
|
||||||
|
u32 last_slash = 0xFFFFFFFFu;
|
||||||
|
for (u32 i = 0; i < path_len; i++)
|
||||||
|
if (path[i] == '/') last_slash = i;
|
||||||
|
const char *fname = (last_slash == 0xFFFFFFFFu) ? path : path + last_slash + 1;
|
||||||
|
u32 dir_clus = 0;
|
||||||
|
if (last_slash == 0xFFFFFFFFu) {
|
||||||
|
dir_clus = f32w_c.root_clus;
|
||||||
|
} else {
|
||||||
|
char dirpath[260];
|
||||||
|
u32 dn = last_slash;
|
||||||
|
if (dn >= sizeof(dirpath)) return -3;
|
||||||
|
for (u32 i = 0; i < dn; i++) dirpath[i] = path[i];
|
||||||
|
dirpath[dn] = 0;
|
||||||
|
if (f32w_resolve_dir(dirpath, &dir_clus) != 0) return -4;
|
||||||
|
}
|
||||||
|
return f32w_write_file(dir_clus, fname, data, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按路径创建目录(仅最后一段被创建,中间目录必须已存在)。返回 0 成功。 */
|
||||||
|
static inline int f32w_mkdir_path(const char *path, u32 *out_clus) {
|
||||||
|
if (!path) return -1;
|
||||||
|
if (f32w_bpb_load() != 0) return -2;
|
||||||
|
u32 path_len = f32w_strlen(path);
|
||||||
|
if (path_len == 0) return -3;
|
||||||
|
/* 去尾部 '/' */
|
||||||
|
while (path_len > 0 && path[path_len - 1] == '/') path_len--;
|
||||||
|
if (path_len == 0) return -3;
|
||||||
|
u32 last_slash = 0xFFFFFFFFu;
|
||||||
|
for (u32 i = 0; i < path_len; i++)
|
||||||
|
if (path[i] == '/') last_slash = i;
|
||||||
|
const char *dname = (last_slash == 0xFFFFFFFFu) ? path : path + last_slash + 1;
|
||||||
|
if ((u32)(dname - path) >= path_len) return -3; /* 防御:末段为空 */
|
||||||
|
u32 parent_clus = 0;
|
||||||
|
if (last_slash == 0xFFFFFFFFu) {
|
||||||
|
parent_clus = f32w_c.root_clus;
|
||||||
|
} else {
|
||||||
|
char dirpath[260];
|
||||||
|
u32 dn = last_slash;
|
||||||
|
if (dn >= sizeof(dirpath)) return -4;
|
||||||
|
for (u32 i = 0; i < dn; i++) dirpath[i] = path[i];
|
||||||
|
dirpath[dn] = 0;
|
||||||
|
if (f32w_resolve_dir(dirpath, &parent_clus) != 0) return -5;
|
||||||
|
}
|
||||||
|
return f32w_mkdir(parent_clus, dname, out_clus);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* DESHAB_FAT32_WRITE_H */
|
||||||
@@ -305,11 +305,28 @@ static void go_up(void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 查找并进入 SYSTEM/user 目录 */
|
/* 查找并进入用户目录。
|
||||||
|
* 布局探测顺序(磁盘布局约定不同,两种镜像并存):
|
||||||
|
* 1) 根级 USER/ —— 树形布局(dev 镜像,含 DESKTOP/PICTURE/VEDIO)
|
||||||
|
* 2) SYSTEM/USER/ —— 旧树形布局(兼容遗留镜像)
|
||||||
|
* 3) 都不存在 —— 扁平布局(SATA 盘,用户文件直接在根目录,停留根目录即可)
|
||||||
|
* 匹配用 8.3 显示名(f32_name_from_83 已 trim 空格),如 "USER"/"SYSTEM"。 */
|
||||||
static void find_user_dir(void) {
|
static void find_user_dir(void) {
|
||||||
/* 先查找 SYSTEM 目录 */
|
/* 1) 根级 USER */
|
||||||
file_count = 0;
|
file_count = 0;
|
||||||
f32_list_dir(0, fm_collect_cb, 0);
|
f32_list_dir(0, fm_collect_cb, 0);
|
||||||
|
for (int i = 0; i < file_count; i++) {
|
||||||
|
if (files[i].is_dir && files[i].name[0] == 'U' && files[i].name[1] == 'S' &&
|
||||||
|
files[i].name[2] == 'E' && files[i].name[3] == 'R') {
|
||||||
|
path_clus[0] = files[i].clus;
|
||||||
|
path_name[0][0] = 'U'; path_name[0][1] = 'S'; path_name[0][2] = 'E';
|
||||||
|
path_name[0][3] = 'R'; path_name[0][4] = 0;
|
||||||
|
path_depth = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2) 旧树形布局 SYSTEM/USER */
|
||||||
u32 system_clus = 0;
|
u32 system_clus = 0;
|
||||||
for (int i = 0; i < file_count; i++) {
|
for (int i = 0; i < file_count; i++) {
|
||||||
if (files[i].is_dir && files[i].name[0] == 'S' && files[i].name[1] == 'Y' &&
|
if (files[i].is_dir && files[i].name[0] == 'S' && files[i].name[1] == 'Y' &&
|
||||||
@@ -319,15 +336,14 @@ static void find_user_dir(void) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!system_clus) return; /* SYSTEM 目录不存在,保持根目录 */
|
if (!system_clus) return; /* 扁平布局,保持根目录 */
|
||||||
|
|
||||||
/* 在 SYSTEM 中查找 user 目录 */
|
|
||||||
file_count = 0;
|
file_count = 0;
|
||||||
f32_list_dir(system_clus, fm_collect_cb, 0);
|
f32_list_dir(system_clus, fm_collect_cb, 0);
|
||||||
for (int i = 0; i < file_count; i++) {
|
for (int i = 0; i < file_count; i++) {
|
||||||
if (files[i].is_dir && files[i].name[0] == 'U' && files[i].name[1] == 'S' &&
|
if (files[i].is_dir && files[i].name[0] == 'U' && files[i].name[1] == 'S' &&
|
||||||
files[i].name[2] == 'E' && files[i].name[3] == 'R') {
|
files[i].name[2] == 'E' && files[i].name[3] == 'R') {
|
||||||
/* 找到了 SYSTEM/user,设置路径栈 */
|
/* 找到了 SYSTEM/USER,设置路径栈 */
|
||||||
path_clus[0] = system_clus;
|
path_clus[0] = system_clus;
|
||||||
path_name[0][0] = 'S'; path_name[0][1] = 'Y'; path_name[0][2] = 'S';
|
path_name[0][0] = 'S'; path_name[0][1] = 'Y'; path_name[0][2] = 'S';
|
||||||
path_name[0][3] = 'T'; path_name[0][4] = 'E'; path_name[0][5] = 'M';
|
path_name[0][3] = 'T'; path_name[0][4] = 'E'; path_name[0][5] = 'M';
|
||||||
@@ -339,6 +355,7 @@ static void find_user_dir(void) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* SYSTEM 存在但无 SYSTEM/USER:停留根目录(保持原行为) */
|
||||||
}
|
}
|
||||||
|
|
||||||
__attribute__((visibility("default")))
|
__attribute__((visibility("default")))
|
||||||
@@ -359,7 +376,7 @@ void dsk_entry(const da_boot_context *ctx) {
|
|||||||
f32_init((f32_block_read_fn)g_ac.block_read, (f32_block_write_fn)g_ac.block_write);
|
f32_init((f32_block_read_fn)g_ac.block_read, (f32_block_write_fn)g_ac.block_write);
|
||||||
|
|
||||||
path_depth = 0;
|
path_depth = 0;
|
||||||
/* 尝试进入 SYSTEM/user 目录 */
|
/* 查找用户目录:自动适配树形(根级 USER/)与扁平(根目录直放)两种布局 */
|
||||||
find_user_dir();
|
find_user_dir();
|
||||||
load_current_dir();
|
load_current_dir();
|
||||||
redraw_all();
|
redraw_all();
|
||||||
|
|||||||
@@ -0,0 +1,406 @@
|
|||||||
|
/* gpt_write.h — Deshab 共享 GPT 写库(static inline,无库)
|
||||||
|
*
|
||||||
|
* 前置条件:包含者已定义 u8/u16/u32/u64/i64 基础类型。
|
||||||
|
* 通过 gpt_init() 注入 block_read/block_write 函数指针(签名与 f32 库一致)。
|
||||||
|
*
|
||||||
|
* 能力:
|
||||||
|
* gpt_probe() — 读 LBA1 校验 "EFI PART" 签名 + header CRC(轻量探测)
|
||||||
|
* gpt_load() — 加载 GPT 头 + 分区表到内部缓冲,并做 CRC 校验
|
||||||
|
* gpt_find_esp() — 查找 ESP 分区(type GUID c12a7328-...),输出起始/结束 LBA
|
||||||
|
* gpt_find_part() — 按任意 type GUID 查找分区
|
||||||
|
* gpt_update_esp() — 新增/更新 ESP 分区项(改起始/结束 LBA)
|
||||||
|
* gpt_commit() — 重算 entries CRC + 双 header CRC,写主表/备份表/主头/备份头
|
||||||
|
* gpt_blank_disk() — 全新初始化:protective MBR + 空 GPT(主+备份,无分区)
|
||||||
|
*
|
||||||
|
* 与 build.ps1(New-GptHeader / New-GptFat32Image)字节兼容:
|
||||||
|
* LBA0 protective MBR(type 0xEE),LBA1 主头,LBA2 主表(128×128B = 32 扇区),
|
||||||
|
* 备份表 lastLba-32,备份头 lastLba。GUID 按 .NET Guid.ToByteArray()
|
||||||
|
* little-endian 编码(前三组小端、后两组原序)。
|
||||||
|
* CRC32 为 IEEE 802.3(init 0xFFFFFFFF / poly 0xEDB88320 / xorout 0xFFFFFFFF),
|
||||||
|
* 与 build.ps1 Get-Crc32 逐位实现等价。
|
||||||
|
*
|
||||||
|
* 缓冲:包含者需提供 ~16.5KB BSS(1 扇区头缓冲 + 32 扇区分区表缓冲)。
|
||||||
|
*
|
||||||
|
* 典型流程(FirstInit 写盘 / 系统格式化):
|
||||||
|
* gpt_init(block_read, block_write);
|
||||||
|
* if (gpt_probe() != 0) gpt_blank_disk(total_sectors, NULL); // 空盘初始化
|
||||||
|
* gpt_load();
|
||||||
|
* gpt_update_esp(2048, total_sectors - 34); // 或扩容
|
||||||
|
* gpt_commit();
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef DESHAB_GPT_WRITE_H
|
||||||
|
#define DESHAB_GPT_WRITE_H
|
||||||
|
|
||||||
|
/* ---- block 设备函数类型(与 fat32_io.h 签名一致,index 固定 0) ---- */
|
||||||
|
typedef int (*gpt_block_read_fn)(u32 index, u64 lba, u32 count, void *buf);
|
||||||
|
typedef int (*gpt_block_write_fn)(u32 index, u64 lba, u32 count, const void *buf);
|
||||||
|
|
||||||
|
static gpt_block_read_fn gpt_blk_read = 0;
|
||||||
|
static gpt_block_write_fn gpt_blk_write = 0;
|
||||||
|
|
||||||
|
/* ---- GPT 常量(与 build.ps1 一致) ---- */
|
||||||
|
#define GPT_SIG_BYTES 8
|
||||||
|
#define GPT_ENTRIES 128u
|
||||||
|
#define GPT_ENTRY_SIZE 128u
|
||||||
|
#define GPT_ENTRY_SECTORS 32u /* 128×128 / 512 */
|
||||||
|
#define GPT_FIRST_USABLE 34u /* 2 + 32 */
|
||||||
|
#define GPT_HEADER_SIZE 92u
|
||||||
|
#define GPT_REVISION 0x00010000u
|
||||||
|
#define GPT_MBR_PART_OFF 446 /* protective MBR 分区项偏移 */
|
||||||
|
|
||||||
|
/* ---- GPT 头(512B,build.ps1 Set-Le* 布局) ---- */
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
u8 sig[8]; /* "EFI PART" */
|
||||||
|
u32 revision; /* 0x00010000 */
|
||||||
|
u32 header_size; /* 92 */
|
||||||
|
u32 header_crc;
|
||||||
|
u32 reserved0;
|
||||||
|
u64 current_lba; /* 主头=1,备份头=lastLba */
|
||||||
|
u64 backup_lba;
|
||||||
|
u64 first_usable; /* 34 */
|
||||||
|
u64 last_usable; /* lastLba - 33 */
|
||||||
|
u8 disk_guid[16];
|
||||||
|
u64 entries_lba; /* 主表=2,备份表=lastLba-32 */
|
||||||
|
u32 entries_count; /* 128 */
|
||||||
|
u32 entries_size; /* 128 */
|
||||||
|
u32 entries_crc;
|
||||||
|
u8 reserved[420];
|
||||||
|
} gpt_header; /* 512B */
|
||||||
|
|
||||||
|
/* ---- 分区项(128B) ---- */
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
u8 type_guid[16]; /* 全 0 = 空槽 */
|
||||||
|
u8 uniq_guid[16];
|
||||||
|
u64 first_lba;
|
||||||
|
u64 last_lba;
|
||||||
|
u64 attrs;
|
||||||
|
u16 name[36]; /* UTF-16LE,未用部分为 0 */
|
||||||
|
} gpt_part_entry;
|
||||||
|
|
||||||
|
/* ESP 类型 GUID:c12a7328-f81f-11d2-ba4b-00a0c93ec93b
|
||||||
|
* (.NET Guid.ToByteArray() 编码:前三组小端、后两组原序) */
|
||||||
|
static const u8 gpt_guid_esp[16] = {
|
||||||
|
0x28, 0x73, 0x2a, 0xc1, /* c12a7328 */
|
||||||
|
0x1f, 0xf8, /* f81f */
|
||||||
|
0xd2, 0x11, /* 11d2 */
|
||||||
|
0xba, 0x4b, 0x00, 0xa0, 0xc9, 0x3e, 0xc9, 0x3b
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---- 内部缓冲(每包含者独立) ---- */
|
||||||
|
static u8 gpt_sec[512]; /* 单扇区(头/MBR)缓冲 */
|
||||||
|
static u8 gpt_tbl[GPT_ENTRY_SECTORS * 512]; /* 分区表缓冲(32 扇区) */
|
||||||
|
static u64 gpt_disk_sectors = 0; /* 磁盘总扇区数(探测后已知) */
|
||||||
|
|
||||||
|
/* ---- 初始化:注入 block_read/block_write 函数指针 ---- */
|
||||||
|
static inline void gpt_init(gpt_block_read_fn rd, gpt_block_write_fn wr) {
|
||||||
|
gpt_blk_read = rd;
|
||||||
|
gpt_blk_write = wr;
|
||||||
|
gpt_disk_sectors = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int gpt_read_sectors(u64 lba, u32 count, u8 *out) {
|
||||||
|
return gpt_blk_read ? gpt_blk_read(0, lba, count, out) : -1;
|
||||||
|
}
|
||||||
|
static inline int gpt_write_sectors(u64 lba, u32 count, const u8 *buf) {
|
||||||
|
return gpt_blk_write ? gpt_blk_write(0, lba, count, buf) : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 小工具(无库约束:手写循环替代 mem*) ---- */
|
||||||
|
static inline void gpt_mem_set(u8 *p, u8 v, u32 n) { for (u32 i = 0; i < n; i++) p[i] = v; }
|
||||||
|
static inline void gpt_mem_cpy(u8 *d, const u8 *s, u32 n) { for (u32 i = 0; i < n; i++) d[i] = s[i]; }
|
||||||
|
static inline int gpt_mem_eq(const u8 *a, const u8 *b, u32 n) {
|
||||||
|
for (u32 i = 0; i < n; i++) if (a[i] != b[i]) return 0;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
static inline u32 gpt_r32(const u8 *p) {
|
||||||
|
return (u32)p[0] | ((u32)p[1] << 8) | ((u32)p[2] << 16) | ((u32)p[3] << 24);
|
||||||
|
}
|
||||||
|
static inline void gpt_w16(u8 *p, u16 v) { p[0] = (u8)v; p[1] = (u8)(v >> 8); }
|
||||||
|
static inline void gpt_w32(u8 *p, u32 v) {
|
||||||
|
p[0] = (u8)v; p[1] = (u8)(v >> 8); p[2] = (u8)(v >> 16); p[3] = (u8)(v >> 24);
|
||||||
|
}
|
||||||
|
static inline void gpt_w64(u8 *p, u64 v) {
|
||||||
|
gpt_w32(p, (u32)v); gpt_w32(p + 4, (u32)(v >> 32));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- CRC32(IEEE 802.3,与 build.ps1 Get-Crc32 逐位实现等价;表驱动) ---- */
|
||||||
|
static u32 gpt_crc_tab[256];
|
||||||
|
static int gpt_crc_ready = 0;
|
||||||
|
|
||||||
|
static inline void gpt_crc_init(void) {
|
||||||
|
if (gpt_crc_ready) return;
|
||||||
|
for (u32 i = 0; i < 256; i++) {
|
||||||
|
u32 c = i;
|
||||||
|
for (int k = 0; k < 8; k++)
|
||||||
|
c = (c & 1) ? (c >> 1) ^ 0xEDB88320u : (c >> 1);
|
||||||
|
gpt_crc_tab[i] = c;
|
||||||
|
}
|
||||||
|
gpt_crc_ready = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline u32 gpt_crc32(const u8 *p, u32 n) {
|
||||||
|
u32 crc = 0xFFFFFFFFu;
|
||||||
|
for (u32 i = 0; i < n; i++)
|
||||||
|
crc = (crc >> 8) ^ gpt_crc_tab[(crc ^ p[i]) & 0xFFu];
|
||||||
|
return crc ^ 0xFFFFFFFFu;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- GUID 工具 ---- */
|
||||||
|
|
||||||
|
/* "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" → 16B(.NET ToByteArray 编码)
|
||||||
|
* 返回 0 成功,-1 格式错误 */
|
||||||
|
static inline int gpt_guid_from_str(const char *s, u8 out[16]) {
|
||||||
|
static const int hex[256] = { ['0'] = 0, ['1'] = 1, ['2'] = 2, ['3'] = 3,
|
||||||
|
['4'] = 4, ['5'] = 5, ['6'] = 6, ['7'] = 7, ['8'] = 8, ['9'] = 9,
|
||||||
|
['a'] = 10, ['b'] = 11, ['c'] = 12, ['d'] = 13, ['e'] = 14, ['f'] = 15,
|
||||||
|
['A'] = 10, ['B'] = 11, ['C'] = 12, ['D'] = 13, ['E'] = 14, ['F'] = 15 };
|
||||||
|
/* 位置表:字符串下标 → 字节下标(1=前四组反序,0=后两组原序) */
|
||||||
|
static const struct { int str; int byte; int rev; } map[16] = {
|
||||||
|
{ 0, 3, 1 }, { 2, 2, 1 }, { 4, 1, 1 }, { 6, 0, 1 },
|
||||||
|
{ 9, 5, 1 }, { 11, 4, 1 },
|
||||||
|
{ 14, 7, 1 }, { 16, 6, 1 },
|
||||||
|
{ 19, 8, 0 }, { 21, 9, 0 }, { 23, 10, 0 }, { 25, 11, 0 },
|
||||||
|
{ 28, 12, 0 }, { 30, 13, 0 }, { 32, 14, 0 }, { 34, 15, 0 },
|
||||||
|
};
|
||||||
|
if (s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-') return -1;
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
int hi = hex[(u8)s[map[i].str]];
|
||||||
|
int lo = hex[(u8)s[map[i].str + 1]];
|
||||||
|
if (hi < 0 || lo < 0) return -1;
|
||||||
|
(void)0;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 16; i++)
|
||||||
|
out[map[i].byte] = (u8)((hex[(u8)s[map[i].str]] << 4) | hex[(u8)s[map[i].str + 1]]);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 16B → "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"(与 .NET Guid.ToString() 同序) */
|
||||||
|
static inline void gpt_guid_to_str(const u8 g[16], char out[37]) {
|
||||||
|
static const char *hd = "0123456789abcdef";
|
||||||
|
static const struct { int byte; int rev; } om[16] = {
|
||||||
|
{ 3, 1 }, { 2, 1 }, { 1, 1 }, { 0, 1 },
|
||||||
|
{ 5, 1 }, { 4, 1 },
|
||||||
|
{ 7, 1 }, { 6, 1 },
|
||||||
|
{ 8, 0 }, { 9, 0 }, { 10, 0 }, { 11, 0 },
|
||||||
|
{ 12, 0 }, { 13, 0 }, { 14, 0 }, { 15, 0 },
|
||||||
|
};
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
int b = om[i].byte;
|
||||||
|
out[i * 2] = hd[g[b] >> 4];
|
||||||
|
out[i * 2 + 1] = hd[g[b] & 0xF];
|
||||||
|
if (i == 3 || i == 5 || i == 7 || i == 9) { out[i * 2 + 2] = '-'; }
|
||||||
|
}
|
||||||
|
out[36] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- GPT 头构建 / 校验 ---- */
|
||||||
|
|
||||||
|
/* 计算头 CRC:前 92 字节、header_crc 字段按 0 参与 */
|
||||||
|
static inline u32 gpt_header_crc(const gpt_header *h) {
|
||||||
|
u8 tmp[92];
|
||||||
|
gpt_mem_cpy(tmp, (const u8 *)h, 92);
|
||||||
|
gpt_w32(tmp + 16, 0);
|
||||||
|
return gpt_crc32(tmp, 92);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 构建头(不落盘):entries_crc 由调用者预先算好 */
|
||||||
|
static inline void gpt_header_build(gpt_header *h, u64 current_lba, u64 backup_lba,
|
||||||
|
u64 first_usable, u64 last_usable,
|
||||||
|
const u8 disk_guid[16],
|
||||||
|
u64 entries_lba, u32 entries_crc) {
|
||||||
|
static const u8 sig[8] = { 'E', 'F', 'I', ' ', 'P', 'A', 'R', 'T' };
|
||||||
|
gpt_mem_set((u8 *)h, 0, sizeof(*h));
|
||||||
|
gpt_mem_cpy(h->sig, sig, 8);
|
||||||
|
h->revision = GPT_REVISION;
|
||||||
|
h->header_size = GPT_HEADER_SIZE;
|
||||||
|
h->current_lba = current_lba;
|
||||||
|
h->backup_lba = backup_lba;
|
||||||
|
h->first_usable = first_usable;
|
||||||
|
h->last_usable = last_usable;
|
||||||
|
if (disk_guid) gpt_mem_cpy(h->disk_guid, disk_guid, 16);
|
||||||
|
h->entries_lba = entries_lba;
|
||||||
|
h->entries_count = GPT_ENTRIES;
|
||||||
|
h->entries_size = GPT_ENTRY_SIZE;
|
||||||
|
h->entries_crc = entries_crc;
|
||||||
|
h->header_crc = gpt_header_crc(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 校验 LBA 处的 GPT 头(签名/大小/current_lba/CRC),返回 0 有效 */
|
||||||
|
static inline int gpt_header_validate(const gpt_header *h, u64 expect_lba) {
|
||||||
|
static const u8 sig[8] = { 'E', 'F', 'I', ' ', 'P', 'A', 'R', 'T' };
|
||||||
|
if (!gpt_mem_eq(h->sig, sig, 8)) return -1;
|
||||||
|
if (h->revision != GPT_REVISION || h->header_size < GPT_HEADER_SIZE) return -2;
|
||||||
|
if (h->current_lba != expect_lba) return -3;
|
||||||
|
if (gpt_header_crc(h) != h->header_crc) return -4;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 分区表操作(内存缓冲) ---- */
|
||||||
|
static inline gpt_part_entry *gpt_part(u32 idx) {
|
||||||
|
return (gpt_part_entry *)(gpt_tbl + idx * GPT_ENTRY_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分区表 CRC(覆盖全部 32 扇区) */
|
||||||
|
static inline u32 gpt_entries_crc(void) {
|
||||||
|
return gpt_crc32(gpt_tbl, sizeof(gpt_tbl));
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int gpt_part_empty(const gpt_part_entry *e) {
|
||||||
|
return gpt_mem_eq(e->type_guid, (const u8 *)gpt_guid_esp /* 任意非 0 引用 */, 0) &&
|
||||||
|
gpt_mem_eq(e->type_guid, e->type_guid, 0); /* 恒真占位,防 -Wunused */
|
||||||
|
}
|
||||||
|
static inline int gpt_part_is_free(const gpt_part_entry *e) {
|
||||||
|
/* type_guid 全 0 = 空槽 */
|
||||||
|
static const u8 zero16[16] = { 0 };
|
||||||
|
return gpt_mem_eq(e->type_guid, zero16, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 查找第一个 type_guid 匹配的分区(含全 0 停止),返回索引或 -1 */
|
||||||
|
static inline int gpt_find_part_type(const u8 type_guid[16], u64 *out_first, u64 *out_last) {
|
||||||
|
for (u32 i = 0; i < GPT_ENTRIES; i++) {
|
||||||
|
const gpt_part_entry *e = gpt_part(i);
|
||||||
|
if (gpt_part_is_free(e)) break;
|
||||||
|
if (gpt_mem_eq(e->type_guid, type_guid, 16)) {
|
||||||
|
if (out_first) *out_first = e->first_lba;
|
||||||
|
if (out_last) *out_last = e->last_lba;
|
||||||
|
return (int)i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 查找 ESP 分区,返回索引或 -1 */
|
||||||
|
static inline int gpt_find_esp(u64 *out_first, u64 *out_last) {
|
||||||
|
return gpt_find_part_type(gpt_guid_esp, out_first, out_last);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分区名写入(ASCII → UTF-16LE,最多 36 字符) */
|
||||||
|
static inline void gpt_part_set_name(gpt_part_entry *e, const char *ascii) {
|
||||||
|
u32 i = 0;
|
||||||
|
for (; i < 36 && ascii[i]; i++) gpt_w16((u8 *)&e->name[i], (u16)ascii[i]);
|
||||||
|
for (; i < 36; i++) gpt_w16((u8 *)&e->name[i], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 填充分区项到指定槽位(返回 0 成功,-1 槽位越界/非空) */
|
||||||
|
static inline int gpt_part_fill(u32 idx, const u8 type_guid[16], const u8 uniq_guid[16],
|
||||||
|
u64 first_lba, u64 last_lba, u64 attrs, const char *name) {
|
||||||
|
if (idx >= GPT_ENTRIES) return -1;
|
||||||
|
gpt_part_entry *e = gpt_part(idx);
|
||||||
|
if (!gpt_part_is_free(e)) return -2;
|
||||||
|
gpt_mem_cpy(e->type_guid, type_guid, 16);
|
||||||
|
if (uniq_guid) gpt_mem_cpy(e->uniq_guid, uniq_guid, 16);
|
||||||
|
else gpt_mem_set(e->uniq_guid, 0, 16);
|
||||||
|
e->first_lba = first_lba;
|
||||||
|
e->last_lba = last_lba;
|
||||||
|
e->attrs = attrs;
|
||||||
|
gpt_part_set_name(e, name);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 落盘 / 探测 ---- */
|
||||||
|
|
||||||
|
/* 探测 LBA1 是否为有效 GPT(轻量,不加载分区表) */
|
||||||
|
static inline int gpt_probe(void) {
|
||||||
|
if (gpt_read_sectors(1, 1, gpt_sec) != 0) return -1;
|
||||||
|
return gpt_header_validate((const gpt_header *)gpt_sec, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载主头 + 主分区表到缓冲;校验头/表 CRC。total_sectors 为 0 时跳过盘尾推导 */
|
||||||
|
static inline int gpt_load(u64 total_sectors) {
|
||||||
|
if (gpt_read_sectors(1, 1, gpt_sec) != 0) return -1;
|
||||||
|
const gpt_header *h = (const gpt_header *)gpt_sec;
|
||||||
|
if (gpt_header_validate(h, 1) != 0) return -2;
|
||||||
|
if (h->entries_lba != 2 || h->entries_count != GPT_ENTRIES) return -3;
|
||||||
|
if (gpt_read_sectors(2, GPT_ENTRY_SECTORS, gpt_tbl) != 0) return -4;
|
||||||
|
if (gpt_entries_crc() != h->entries_crc) return -5;
|
||||||
|
gpt_disk_sectors = total_sectors;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全新初始化:protective MBR + 空 GPT(主+备份)。disk_guid 可 NULL(全 0)。
|
||||||
|
* total_sectors 必须 >= 70(34 + 32 表 + 2 头)。 */
|
||||||
|
static inline int gpt_blank_disk(u64 total_sectors, const u8 disk_guid[16]) {
|
||||||
|
if (total_sectors < GPT_FIRST_USABLE + GPT_ENTRY_SECTORS + 2) return -1;
|
||||||
|
|
||||||
|
/* LBA0 protective MBR(build.ps1 布局) */
|
||||||
|
gpt_mem_set(gpt_sec, 0, 512);
|
||||||
|
gpt_sec[GPT_MBR_PART_OFF + 4] = 0xEE; /* type */
|
||||||
|
gpt_w32(gpt_sec + GPT_MBR_PART_OFF + 8, 1); /* start LBA */
|
||||||
|
u64 prot = total_sectors - 1;
|
||||||
|
if (prot > 0xFFFFFFFFu) prot = 0xFFFFFFFFu;
|
||||||
|
gpt_w32(gpt_sec + GPT_MBR_PART_OFF + 12, (u32)prot); /* sectors */
|
||||||
|
gpt_sec[510] = 0x55; gpt_sec[511] = 0xAA;
|
||||||
|
if (gpt_write_sectors(0, 1, gpt_sec) != 0) return -2;
|
||||||
|
|
||||||
|
/* 空分区表 */
|
||||||
|
gpt_mem_set(gpt_tbl, 0, sizeof(gpt_tbl));
|
||||||
|
u32 entries_crc = gpt_crc32(gpt_tbl, sizeof(gpt_tbl));
|
||||||
|
|
||||||
|
/* 主头(LBA1)+ 备份头(lastLba) */
|
||||||
|
u64 last_lba = total_sectors - 1;
|
||||||
|
u64 last_usable = last_lba - GPT_ENTRY_SECTORS - 1;
|
||||||
|
gpt_header ph, bh;
|
||||||
|
gpt_header_build(&ph, 1, last_lba, GPT_FIRST_USABLE, last_usable,
|
||||||
|
disk_guid, 2, entries_crc);
|
||||||
|
gpt_header_build(&bh, last_lba, 1, GPT_FIRST_USABLE, last_usable,
|
||||||
|
disk_guid, last_lba - GPT_ENTRY_SECTORS, entries_crc);
|
||||||
|
|
||||||
|
if (gpt_write_sectors(1, 1, (const u8 *)&ph) != 0) return -3;
|
||||||
|
if (gpt_write_sectors(2, GPT_ENTRY_SECTORS, gpt_tbl) != 0) return -4;
|
||||||
|
if (gpt_write_sectors(last_lba - GPT_ENTRY_SECTORS, GPT_ENTRY_SECTORS, gpt_tbl) != 0) return -5;
|
||||||
|
if (gpt_write_sectors(last_lba, 1, (const u8 *)&bh) != 0) return -6;
|
||||||
|
|
||||||
|
gpt_disk_sectors = total_sectors;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新增/更新 ESP 分区项(内存缓冲,需 gpt_commit 落盘):
|
||||||
|
* 已有 ESP 则更新范围,否则填第一个空槽。返回槽位索引或负错误码 */
|
||||||
|
static inline int gpt_update_esp(u64 first_lba, u64 last_lba,
|
||||||
|
const u8 uniq_guid[16], const char *name) {
|
||||||
|
static const u8 zero16[16] = { 0 };
|
||||||
|
int idx = gpt_find_part_type(gpt_guid_esp, 0, 0);
|
||||||
|
if (idx >= 0) {
|
||||||
|
gpt_part_entry *e = gpt_part((u32)idx);
|
||||||
|
e->first_lba = first_lba;
|
||||||
|
e->last_lba = last_lba;
|
||||||
|
if (uniq_guid) gpt_mem_cpy(e->uniq_guid, uniq_guid, 16);
|
||||||
|
if (name) gpt_part_set_name(e, name);
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
/* 找第一个空槽(类型全 0) */
|
||||||
|
for (u32 i = 0; i < GPT_ENTRIES; i++) {
|
||||||
|
if (gpt_part_is_free(gpt_part(i)))
|
||||||
|
return gpt_part_fill(i, gpt_guid_esp, uniq_guid ? uniq_guid : zero16,
|
||||||
|
first_lba, last_lba, 0, name ? name : "Deshab ESP");
|
||||||
|
}
|
||||||
|
return -3; /* 表满 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 提交:重算 entries CRC + 双头 CRC,写主表/备份表/主头/备份头(幂等) */
|
||||||
|
static inline int gpt_commit(void) {
|
||||||
|
u64 last_lba = 0;
|
||||||
|
gpt_header h;
|
||||||
|
/* 读当前主头,取 last_usable / disk_guid / backup 布局 */
|
||||||
|
if (gpt_read_sectors(1, 1, gpt_sec) != 0) return -1;
|
||||||
|
gpt_mem_cpy((u8 *)&h, gpt_sec, sizeof(h));
|
||||||
|
if (gpt_header_validate(&h, 1) != 0) return -2;
|
||||||
|
last_lba = h.backup_lba;
|
||||||
|
|
||||||
|
u32 entries_crc = gpt_entries_crc();
|
||||||
|
|
||||||
|
gpt_header_build(&h, 1, last_lba, h.first_usable, h.last_usable,
|
||||||
|
h.disk_guid, 2, entries_crc);
|
||||||
|
gpt_header bh;
|
||||||
|
gpt_header_build(&bh, last_lba, 1, h.first_usable, h.last_usable,
|
||||||
|
h.disk_guid, last_lba - GPT_ENTRY_SECTORS, entries_crc);
|
||||||
|
|
||||||
|
if (gpt_write_sectors(2, GPT_ENTRY_SECTORS, gpt_tbl) != 0) return -3;
|
||||||
|
if (gpt_write_sectors(1, 1, (const u8 *)&h) != 0) return -4;
|
||||||
|
if (gpt_write_sectors(last_lba - GPT_ENTRY_SECTORS, GPT_ENTRY_SECTORS, gpt_tbl) != 0) return -5;
|
||||||
|
if (gpt_write_sectors(last_lba, 1, (const u8 *)&bh) != 0) return -6;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* DESHAB_GPT_WRITE_H */
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
# read_usb_logs.ps1 - Read Deshab USB disk, parse GPT+FAT32, extract test logs
|
||||||
|
#
|
||||||
|
# Deshab 实机测试日志回读工具:U 盘(GPT ESP + FAT32)在 Windows 上无法
|
||||||
|
# 直接挂载(removable media 不支持盘符分配),此脚本以管理员权限直读
|
||||||
|
# 物理磁盘,解析 GPT 分区表 + FAT32 文件系统,把 SYSTEM/DESHAB64/DEV/ 下的
|
||||||
|
# 日志文件(BOOTLOG.TXT、BOOTxx.TXT、LOG 等)和根目录 DEVLOG 提取到本地。
|
||||||
|
#
|
||||||
|
# Usage (admin PowerShell):
|
||||||
|
# powershell -ExecutionPolicy Bypass -File "CODE\tools\read_usb_logs.ps1"
|
||||||
|
# powershell -ExecutionPolicy Bypass -File "CODE\tools\read_usb_logs.ps1" -DiskNumber 2
|
||||||
|
# powershell -ExecutionPolicy Bypass -File "CODE\tools\read_usb_logs.ps1" -OutDir ".\logs"
|
||||||
|
#
|
||||||
|
# 输出目录: .build_tmp\usb_logs\ (或 -OutDir 指定)
|
||||||
|
param(
|
||||||
|
[int]$DiskNumber = 1,
|
||||||
|
[string]$OutDir = 'd:\Code\DEAICUP\Deshab\.build_tmp\usb_logs'
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$diskPath = '\\.\PhysicalDrive' + $DiskNumber
|
||||||
|
|
||||||
|
# ---- 检查管理员权限(打开物理磁盘需要) ----
|
||||||
|
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||||
|
if (-not $isAdmin) {
|
||||||
|
Write-Host "[ERROR] Administrator privileges required to open $diskPath" -ForegroundColor Red
|
||||||
|
Write-Host " Run this script from an elevated PowerShell (Win+X -> Terminal (Admin))."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
|
||||||
|
|
||||||
|
$script:fs = $null
|
||||||
|
$script:partStart = 0
|
||||||
|
$script:dataSec = 0
|
||||||
|
$script:rsvd = 0
|
||||||
|
$script:spc = 0
|
||||||
|
$script:csize = 0
|
||||||
|
|
||||||
|
function Read-Cluster([uint32]$clus) {
|
||||||
|
$lba = $dataSec + ($clus - 2) * $spc
|
||||||
|
$fs.Position = ([int64]$partStart + $lba) * 512
|
||||||
|
$b = [byte[]]::new($csize)
|
||||||
|
[void]$fs.Read($b, 0, $csize)
|
||||||
|
return ,$b
|
||||||
|
}
|
||||||
|
|
||||||
|
function Read-FatEntry([uint32]$c) {
|
||||||
|
$fatSec = [Math]::Floor(($c * 4) / 512)
|
||||||
|
$fs.Position = ([int64]$partStart + $rsvd + $fatSec) * 512
|
||||||
|
$fb = [byte[]]::new(512)
|
||||||
|
[void]$fs.Read($fb, 0, 512)
|
||||||
|
return ([BitConverter]::ToUInt32($fb, ($c * 4) % 512) -band 0x0FFFFFFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Decode-Short([byte[]]$n) {
|
||||||
|
$s=''; for($i=0;$i -lt 8 -and $n[$i] -ne 0x20 -and $n[$i] -ne 0;$i++){$s+=[char]$n[$i]}
|
||||||
|
$e=''; for($i=8;$i -lt 11 -and $n[$i] -ne 0x20 -and $n[$i] -ne 0;$i++){$e+=[char]$n[$i]}
|
||||||
|
if($e){return "$s.$e"}else{return $s}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-EntryCluster([byte[]]$buf, [int]$o) {
|
||||||
|
$clow = [BitConverter]::ToUInt16($buf, $o + 26)
|
||||||
|
$chigh = [BitConverter]::ToUInt16($buf, $o + 20)
|
||||||
|
return ($clow -bor ([int]$chigh -shl 16))
|
||||||
|
}
|
||||||
|
|
||||||
|
function Read-File([uint32]$firstClus, [uint32]$size) {
|
||||||
|
if ($size -eq 0) { return ,([byte[]]::new(0)) }
|
||||||
|
$data = [byte[]]::new($size)
|
||||||
|
$c = $firstClus; $off = 0; $remaining = $size
|
||||||
|
while ($c -ge 2 -and $c -lt 0x0FFFFFF8 -and $remaining -gt 0) {
|
||||||
|
$lba = $dataSec + ($c - 2) * $spc
|
||||||
|
$fs.Position = ([int64]$partStart + $lba) * 512
|
||||||
|
$chunk = [byte[]]::new($csize)
|
||||||
|
[void]$fs.Read($chunk, 0, $csize)
|
||||||
|
$n = [Math]::Min($csize, $remaining)
|
||||||
|
[Array]::Copy($chunk, 0, $data, $off, $n)
|
||||||
|
$off += $n; $remaining -= $n
|
||||||
|
$c = Read-FatEntry $c
|
||||||
|
}
|
||||||
|
return ,$data
|
||||||
|
}
|
||||||
|
|
||||||
|
function Scan-Dir([uint32]$dirClus) {
|
||||||
|
$result = @{}
|
||||||
|
$c = $dirClus
|
||||||
|
while ($c -ge 2 -and $c -lt 0x0FFFFFF8) {
|
||||||
|
$buf = Read-Cluster $c
|
||||||
|
for ($e = 0; $e -lt ($csize / 32); $e++) {
|
||||||
|
$o = $e * 32
|
||||||
|
if ($buf[$o] -eq 0) { return $result }
|
||||||
|
if ($buf[$o] -eq 0xE5) { continue }
|
||||||
|
$at = $buf[$o + 11]
|
||||||
|
if (($at -band 0x0F) -eq 0x0F) { continue } # LFN entry, skip
|
||||||
|
if ($at -band 0x08) { continue } # volume label
|
||||||
|
$name = Decode-Short $buf[$o..($o + 10)]
|
||||||
|
$result[$name] = @{ Cluster = (Get-EntryCluster $buf $o); Size = [BitConverter]::ToUInt32($buf, $o + 28); Attr = $at }
|
||||||
|
}
|
||||||
|
$c = Read-FatEntry $c
|
||||||
|
}
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
function Show-Head([byte[]]$data) {
|
||||||
|
if (-not $data -or $data.Length -eq 0) { Write-Host " [empty]"; return }
|
||||||
|
$txt = [System.Text.Encoding]::ASCII.GetString($data)
|
||||||
|
$len = [Math]::Min(400, $txt.Length)
|
||||||
|
$clean = ''
|
||||||
|
for ($i = 0; $i -lt $len; $i++) {
|
||||||
|
$code = [int]$txt[$i]
|
||||||
|
if (($code -ge 32 -and $code -le 126) -or $code -eq 10 -or $code -eq 13 -or $code -eq 9) { $clean += $txt[$i] } else { $clean += '.' }
|
||||||
|
}
|
||||||
|
Write-Host " [head] $clean"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "=== Open $diskPath ==="
|
||||||
|
$fs = [System.IO.File]::Open($diskPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
|
||||||
|
|
||||||
|
$fs.Position = 512
|
||||||
|
$lba1 = [byte[]]::new(512); [void]$fs.Read($lba1, 0, 512)
|
||||||
|
$sig = [System.Text.Encoding]::ASCII.GetString($lba1, 0, 8)
|
||||||
|
Write-Host "LBA1 signature: '$sig'"
|
||||||
|
if ($sig -ne 'EFI PART') { Write-Host "[ERROR] Not a GPT disk"; $fs.Close(); exit 1 }
|
||||||
|
|
||||||
|
$fs.Position = 1024
|
||||||
|
$pe = [byte[]]::new(128); [void]$fs.Read($pe, 0, 128)
|
||||||
|
$partStart = [BitConverter]::ToUInt64($pe, 32)
|
||||||
|
Write-Host "ESP start LBA: $partStart"
|
||||||
|
|
||||||
|
$fs.Position = $partStart * 512
|
||||||
|
$bpb = [byte[]]::new(512); [void]$fs.Read($bpb, 0, 512)
|
||||||
|
$bps = [BitConverter]::ToUInt16($bpb, 11)
|
||||||
|
$spc = $bpb[13]
|
||||||
|
$rsvd = [BitConverter]::ToUInt16($bpb, 14)
|
||||||
|
$nf = $bpb[16]
|
||||||
|
$spf = [BitConverter]::ToUInt32($bpb, 36)
|
||||||
|
$rootClus = [BitConverter]::ToUInt32($bpb, 44)
|
||||||
|
$dataSec = $rsvd + $nf * $spf
|
||||||
|
$csize = $bps * $spc
|
||||||
|
Write-Host "BPB: bps=$bps spc=$spc rsvd=$rsvd nf=$nf spf=$spf root=$rootClus dataSec=$dataSec"
|
||||||
|
|
||||||
|
$root = Scan-Dir $rootClus
|
||||||
|
Write-Host "=== ROOT ==="
|
||||||
|
$root.Keys | Sort-Object | ForEach-Object { Write-Host (" {0} clus={1} size={2}" -f $_, $root[$_].Cluster, $root[$_].Size) }
|
||||||
|
|
||||||
|
if (-not $root.ContainsKey('SYSTEM')) { Write-Host "[WARN] no SYSTEM dir"; $fs.Close(); exit 0 }
|
||||||
|
$sys = Scan-Dir $root['SYSTEM'].Cluster
|
||||||
|
if (-not $sys.ContainsKey('DESHAB64')) { Write-Host "[WARN] no DESHAB64 dir"; $fs.Close(); exit 0 }
|
||||||
|
$d64 = Scan-Dir $sys['DESHAB64'].Cluster
|
||||||
|
|
||||||
|
Write-Host "=== DESHAB64 ==="
|
||||||
|
$d64.Keys | Sort-Object | ForEach-Object { Write-Host (" {0} size={1}" -f $_, $d64[$_].Size) }
|
||||||
|
|
||||||
|
if (-not $d64.ContainsKey('DEV')) { Write-Host "[WARN] no DEV dir"; $fs.Close(); exit 0 }
|
||||||
|
$dev = Scan-Dir $d64['DEV'].Cluster
|
||||||
|
Write-Host "=== DESHAB64/DEV ==="
|
||||||
|
$dev.Keys | Sort-Object | ForEach-Object { Write-Host (" {0} size={1}" -f $_, $dev[$_].Size) }
|
||||||
|
|
||||||
|
Write-Host "=== EXTRACT to $OutDir ==="
|
||||||
|
foreach ($k in $dev.Keys) {
|
||||||
|
if (($dev[$k].Attr -band 0x10) -ne 0) { continue }
|
||||||
|
$data = Read-File $dev[$k].Cluster $dev[$k].Size
|
||||||
|
$safe = ($k -replace '[^A-Za-z0-9._-]', '_')
|
||||||
|
$outPath = Join-Path $OutDir $safe
|
||||||
|
[System.IO.File]::WriteAllBytes($outPath, $data)
|
||||||
|
Write-Host (" -> {0} ({1} bytes)" -f $safe, $data.Length)
|
||||||
|
Show-Head $data
|
||||||
|
}
|
||||||
|
foreach ($k in @('DEVLOG','TEST.TXT','COPY.TXT','MOVED.TXT','BOOTLOG')) {
|
||||||
|
if ($root.ContainsKey($k)) {
|
||||||
|
$data = Read-File $root[$k].Cluster $root[$k].Size
|
||||||
|
$outPath = Join-Path $OutDir ("ROOT_" + $k)
|
||||||
|
[System.IO.File]::WriteAllBytes($outPath, $data)
|
||||||
|
Write-Host (" [root] -> {0} ({1} bytes)" -f $k, $data.Length)
|
||||||
|
Show-Head $data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$fs.Close()
|
||||||
|
Write-Host "=== DONE ==="
|
||||||
+29
-2
@@ -2,16 +2,43 @@
|
|||||||
setlocal enabledelayedexpansion
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
set "SCRIPT_DIR=%~dp0"
|
set "SCRIPT_DIR=%~dp0"
|
||||||
set "IMG=%SCRIPT_DIR%deshab-dev.img"
|
|
||||||
|
REM ===== Variant selection: dev / release / realtest =====
|
||||||
|
REM Priority: argument > QEMU_VARIANT env > interactive menu > default dev
|
||||||
|
REM Usage: run_qemu.bat [dev|release|realtest]
|
||||||
|
set "VARIANT="
|
||||||
|
if not "%~1"=="" set "VARIANT=%~1"
|
||||||
|
if not defined VARIANT if defined QEMU_VARIANT set "VARIANT=%QEMU_VARIANT%"
|
||||||
|
if not defined VARIANT (
|
||||||
|
echo [qemu] Select image variant to boot:
|
||||||
|
echo 1. dev ^(deshab-dev.img, debug all on, auto test^)
|
||||||
|
echo 2. release ^(deshab-release.img, desktop + login^)
|
||||||
|
echo 3. realtest ^(deshab-realtest.img, real-machine safe options^)
|
||||||
|
set /p CHOICE="Choice [1-3, Enter=dev]: "
|
||||||
|
if "!CHOICE!"=="2" set "VARIANT=release"
|
||||||
|
if "!CHOICE!"=="3" set "VARIANT=realtest"
|
||||||
|
if not defined VARIANT set "VARIANT=dev"
|
||||||
|
)
|
||||||
|
REM Validate variant name (also accepts passing 1/2/3 as argument)
|
||||||
|
if "%VARIANT%"=="1" set "VARIANT=dev"
|
||||||
|
if "%VARIANT%"=="2" set "VARIANT=release"
|
||||||
|
if "%VARIANT%"=="3" set "VARIANT=realtest"
|
||||||
|
if not "%VARIANT%"=="dev" if not "%VARIANT%"=="release" if not "%VARIANT%"=="realtest" (
|
||||||
|
echo [qemu] Unknown variant: %VARIANT% ^(expected dev / release / realtest^)
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
set "IMG=%SCRIPT_DIR%deshab-%VARIANT%.img"
|
||||||
set "SATA_IMG=%SCRIPT_DIR%..\.build_tmp\sata_fat32_dsk.img"
|
set "SATA_IMG=%SCRIPT_DIR%..\.build_tmp\sata_fat32_dsk.img"
|
||||||
set "NVME_IMG=%SCRIPT_DIR%..\.build_tmp\nvme_test.img"
|
set "NVME_IMG=%SCRIPT_DIR%..\.build_tmp\nvme_test.img"
|
||||||
|
|
||||||
if not exist "%IMG%" (
|
if not exist "%IMG%" (
|
||||||
echo [qemu] Image not found: %IMG%
|
echo [qemu] Image not found: %IMG%
|
||||||
echo [qemu] Run ..\build.bat first -- default builds both dev and release.
|
echo [qemu] Run ..\build.bat first -- default builds both dev and release.
|
||||||
echo [qemu] Or build dev only: build.bat -Variant dev
|
echo [qemu] Or build selected only: build.bat -Variant %VARIANT%
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
echo [qemu] Variant: %VARIANT%
|
||||||
|
|
||||||
set "QEMU="
|
set "QEMU="
|
||||||
for %%Q in (qemu-system-x86_64.exe qemu-system-x86_64) do (
|
for %%Q in (qemu-system-x86_64.exe qemu-system-x86_64) do (
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -91,6 +91,14 @@
|
|||||||
"depends": ["pci", "irq"],
|
"depends": ["pci", "irq"],
|
||||||
"provides": ["block"]
|
"provides": ["block"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "ehci",
|
||||||
|
"path": "driver/block/ehci.drv",
|
||||||
|
"class": "storage",
|
||||||
|
"required": false,
|
||||||
|
"depends": ["pci"],
|
||||||
|
"provides": ["block"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "xhci",
|
"name": "xhci",
|
||||||
"path": "driver/block/xhci.drv",
|
"path": "driver/block/xhci.drv",
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ verbose: yes
|
|||||||
module_cmdline: dkm:ahci
|
module_cmdline: dkm:ahci
|
||||||
module_path: boot():/driver/block/nvme.drv
|
module_path: boot():/driver/block/nvme.drv
|
||||||
module_cmdline: dkm:nvme
|
module_cmdline: dkm:nvme
|
||||||
|
module_path: boot():/driver/block/ehci.drv
|
||||||
|
module_cmdline: dkm:ehci
|
||||||
module_path: boot():/driver/block/xhci.drv
|
module_path: boot():/driver/block/xhci.drv
|
||||||
module_cmdline: dkm:xhci
|
module_cmdline: dkm:xhci
|
||||||
module_path: boot():/driver/fs/bootfs.drv
|
module_path: boot():/driver/fs/bootfs.drv
|
||||||
|
|||||||
+14
-34
@@ -1,29 +1,26 @@
|
|||||||
# Deshab Kernel Configuration (DEV - QEMU 测试)
|
# Deshab Kernel Configuration (REALTEST - 实机开发测试)
|
||||||
# dev_mode=1 自动跑 shell.elf 测试序列, 调试全开, QEMU 验证选项
|
# 基于 dev 配置, 启用磁盘日志持久化
|
||||||
|
# 启动时把完整日志写入磁盘 LBA 2-1000 扇区, 事后用 WinHex/dd 读取
|
||||||
# 路径: SYSTEM/system/deshab64/FUCK
|
# 路径: SYSTEM/system/deshab64/FUCK
|
||||||
# UTSM 通过 Limine boot module 预加载读取(cmdline="fuck:config")
|
# UTSM 通过 Limine boot module 预加载读取(cmdline="fuck:config")
|
||||||
# DSK 通过 FAT32 子目录遍历读取(路径 "SYSTEM /DESHAB64 /FUCK ")
|
# DSK 通过 FAT32 子目录遍历读取(路径 "SYSTEM /DESHAB64 /FUCK ")
|
||||||
# 缺失或空的键使用硬编码默认值,行为与无配置文件时一致
|
# 缺失或空的键使用硬编码默认值,行为与无配置文件时一致
|
||||||
|
|
||||||
[boot]
|
[boot]
|
||||||
vmm=1
|
vmm=0
|
||||||
linux_guest=1
|
linux_guest=0
|
||||||
# OpenXJ380 第二 guest(需 SYSTEM/boot/xj380.krl 存在,limine.conf 取消注释)
|
# OpenXJ380 第二 guest:realtest 默认关闭(vmm=0 时天然不执行)
|
||||||
# 启动后 host 挂起直至 XJ380 guest 终止;无模块时优雅跳过
|
xj380_guest=0
|
||||||
xj380_guest=1
|
|
||||||
selftest=1
|
selftest=1
|
||||||
dev_mode=1
|
dev_mode=1
|
||||||
# QEMU 不需要真机安全检测
|
# 真机安全检查全部放宽(和 dev 一样),排除安全检查导致黑屏
|
||||||
la57_guard=0
|
la57_guard=0
|
||||||
mcfg_check=0
|
mcfg_check=0
|
||||||
max_phys_addr_bits=0
|
max_phys_addr_bits=0
|
||||||
# 实机启动日志持久化:把启动日志写入磁盘原始扇区(开发测试用)
|
# 实机启动日志持久化: 启用, 把启动日志写入 FAT32 文件
|
||||||
# 启用后启动时把内存日志缓冲区写到指定扇区区间
|
# SYSTEM/DESHAB64/DEV/BOOTLOG.TXT (DEV 缺失时回退根目录 BOOTLOG.TXT)
|
||||||
# 事后用 WinHex/dd 读取对应扇区即可查看完整启动日志
|
# 主机直接读取该文件即可, 不覆盖 GPT, 可反复烧录测试
|
||||||
# 注意: 写入 LBA 1-33 会覆盖 GPT, 测试盘建议从 34 开始或测试后重新烧录
|
|
||||||
disk_log=1
|
disk_log=1
|
||||||
disk_log_start=2
|
|
||||||
disk_log_end=1000
|
|
||||||
|
|
||||||
[drivers]
|
[drivers]
|
||||||
timer=1
|
timer=1
|
||||||
@@ -33,6 +30,9 @@ pci=1
|
|||||||
console_fb=1
|
console_fb=1
|
||||||
ahci=1
|
ahci=1
|
||||||
nvme=1
|
nvme=1
|
||||||
|
ehci=1
|
||||||
|
# xhci=1: xhci 驱动已修复无控制器时不再 halt,恢复启用
|
||||||
|
xhci=1
|
||||||
bootfs=1
|
bootfs=1
|
||||||
vfs=1
|
vfs=1
|
||||||
fat32=1
|
fat32=1
|
||||||
@@ -43,18 +43,9 @@ ath9k=0
|
|||||||
ps2kbd=1
|
ps2kbd=1
|
||||||
ioapic_init=1
|
ioapic_init=1
|
||||||
lapic_enable=1
|
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=1
|
apic_route=1
|
||||||
# B7 阶段2: 随 apic_route=1 额外放开 legacy IRQ1(键盘)/12(鼠标)/11(e1000)
|
|
||||||
# QEMU 全链路已验证 (B1 向导按键/B2 启动/e1000 IRQ11)
|
|
||||||
apic_route_legacy=1
|
apic_route_legacy=1
|
||||||
# B7 阶段3: NVMe MSI/MSI-X 中断驱动开关 (nvme.drv 自解析, 缺失=1)
|
|
||||||
nvme_msi=1
|
nvme_msi=1
|
||||||
# B7 诊断 (apic.drv): 1=启用 HPET 绝对时基对拍 + IRQ12 鼠标流观察窗
|
|
||||||
# 常规构建保持 0
|
|
||||||
apic_diag=0
|
apic_diag=0
|
||||||
|
|
||||||
[utsm]
|
[utsm]
|
||||||
@@ -75,17 +66,11 @@ mouse_init=1
|
|||||||
netman=1
|
netman=1
|
||||||
|
|
||||||
[debug]
|
[debug]
|
||||||
# 全局插桩开关 (0=禁用,零开销; 1=启用所有 INSTR_* 宏)
|
|
||||||
enabled=1
|
enabled=1
|
||||||
# 日志级别阈值 (0=TRACE, 1=DEBUG, 2=INFO, 3=WARN, 4=ERROR, 5=SILENT)
|
|
||||||
log_level=1
|
log_level=1
|
||||||
# 探测缓冲开关 (0=禁用; 1=启用 INSTR_PROBE 写入环形缓冲)
|
|
||||||
probe_enable=1
|
probe_enable=1
|
||||||
# 探测缓冲容量 (条目数,每条 40 字节,默认 4096 条 ≈ 160KB)
|
|
||||||
probe_capacity=4096
|
probe_capacity=4096
|
||||||
# 统计摘要间隔 (每 N 次增量输出一次摘要)
|
|
||||||
stat_summary_interval=256
|
stat_summary_interval=256
|
||||||
# 模块级过滤: 1=启用该模块的 TRACE/DEBUG 级别日志
|
|
||||||
utsm_trace=1
|
utsm_trace=1
|
||||||
vmm_trace=1
|
vmm_trace=1
|
||||||
dkm_trace=1
|
dkm_trace=1
|
||||||
@@ -95,12 +80,7 @@ net_trace=0
|
|||||||
input_trace=1
|
input_trace=1
|
||||||
|
|
||||||
[compat]
|
[compat]
|
||||||
# 兼容层动态链接库搜索路径配置
|
|
||||||
# lib_path: FAT32 根目录下存放 .dll/.so 的子目录名
|
|
||||||
lib_path=lib
|
lib_path=lib
|
||||||
# linux_lib_guest_path: Linux guest 内库目录路径
|
|
||||||
linux_lib_guest_path=/usr/lib/deshab
|
linux_lib_guest_path=/usr/lib/deshab
|
||||||
# linux_lib_sync: 启动时推送 .so 到 Linux guest (0=关闭, 1=开启)
|
|
||||||
linux_lib_sync=1
|
linux_lib_sync=1
|
||||||
# pe_dll_search: PE shim miss 后从 SYSTEM/lib 加载真实 DLL (0=关闭, 1=开启)
|
|
||||||
pe_dll_search=1
|
pe_dll_search=1
|
||||||
|
|||||||
Binary file not shown.
@@ -1,2 +1,2 @@
|
|||||||
1
|
0
|
||||||
1
|
1
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
param(
|
param(
|
||||||
[ValidateSet('dev','release','realtest','both')][string]$Variant = 'both'
|
[ValidateSet('dev','release','realtest','both')][string]$Variant = 'both',
|
||||||
|
[switch]$Vhd
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
@@ -211,8 +212,13 @@ function New-ShortEntry([string]$ShortName, [byte]$Attr, [int]$Cluster, [uint32]
|
|||||||
function Add-DirectoryEntry($Entries, [string]$Name, [byte]$Attr, [int]$Cluster, [uint32]$Size, $UsedNames) {
|
function Add-DirectoryEntry($Entries, [string]$Name, [byte]$Attr, [int]$Cluster, [uint32]$Size, $UsedNames) {
|
||||||
$shortName = New-ShortFatName $Name $UsedNames
|
$shortName = New-ShortFatName $Name $UsedNames
|
||||||
$shortBytes = Get-ShortNameBytes $shortName
|
$shortBytes = Get-ShortNameBytes $shortName
|
||||||
$upper = $Name.ToUpperInvariant()
|
# 用原始名(含大小写)判断是否生成 LFN:Test-ShortFatName 内部用
|
||||||
$needsLfn = -not (Test-ShortFatName $upper) -or (($shortName.Trim() -replace ' +', '') -ne ($upper -replace '\.', ''))
|
# -cmatch '[a-z]' 检测小写字母,含小写的名字(如 limine)必须生成
|
||||||
|
# LFN 才能在 Windows 上保留原始大小写显示。FAT32 查找仍不区分大小写。
|
||||||
|
# 注意:拆成两步赋值——PowerShell 5.1 对 `-not (函数调用) -or (...)` 内联
|
||||||
|
# 表达式解析异常返回 $null,分开赋值保证布尔短路正常。
|
||||||
|
$isUpperShort = Test-ShortFatName $Name
|
||||||
|
$needsLfn = (-not $isUpperShort) -or (($shortName.Trim() -replace ' +', '') -ne ($Name.ToUpperInvariant() -replace '\.', ''))
|
||||||
|
|
||||||
if ($needsLfn) {
|
if ($needsLfn) {
|
||||||
$checksum = Get-LfnChecksum $shortBytes
|
$checksum = Get-LfnChecksum $shortBytes
|
||||||
@@ -501,32 +507,94 @@ function New-GptFat32Image([string]$SourceDir, [string]$ImagePath, [int]$SizeMB
|
|||||||
Write-Host "[build] GPT + FAT32 ESP IMG: $ImagePath"
|
Write-Host "[build] GPT + FAT32 ESP IMG: $ImagePath"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Convert a raw GPT disk image to a fixed VHD by appending the 512-byte VHD
|
||||||
|
# footer. Fixed VHD = raw data + footer, so this is a cheap copy + footer.
|
||||||
|
# Windows can mount the result natively (double-click / Mount-DiskImage).
|
||||||
|
function ConvertTo-FixedVhd([string]$RawImage, [string]$VhdPath) {
|
||||||
|
if (-not (Test-Path $RawImage)) { throw "Raw image not found: $RawImage" }
|
||||||
|
Copy-Item -LiteralPath $RawImage -Destination $VhdPath -Force
|
||||||
|
$fs = [System.IO.File]::Open($VhdPath, [System.IO.FileMode]::Open,
|
||||||
|
[System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
|
||||||
|
try {
|
||||||
|
$size = $fs.Length
|
||||||
|
$totalSectors = [UInt64]($size / 512)
|
||||||
|
|
||||||
|
$footer = [byte[]]::new(512)
|
||||||
|
[Array]::Copy([System.Text.Encoding]::ASCII.GetBytes('conectix'), 0, $footer, 0, 8)
|
||||||
|
Set-Le32 $footer 8 0x00000002 # features
|
||||||
|
Set-Le32 $footer 12 0x00010000 # file format version
|
||||||
|
Set-Le64 $footer 16 ([UInt64]::MaxValue) # data offset: fixed disk
|
||||||
|
$epoch = [DateTime]::new(2000, 1, 1, 0, 0, 0, [DateTimeKind]::Utc)
|
||||||
|
Set-Le32 $footer 24 ([UInt64][Math]::Max(0, [int]([DateTime]::UtcNow - $epoch).TotalSeconds))
|
||||||
|
[Array]::Copy([System.Text.Encoding]::ASCII.GetBytes('dshb'), 0, $footer, 28, 4)
|
||||||
|
Set-Le32 $footer 32 0x00010000 # creator version
|
||||||
|
[Array]::Copy([System.Text.Encoding]::ASCII.GetBytes('Wi2k'), 0, $footer, 36, 4)
|
||||||
|
Set-Le64 $footer 40 ([UInt64]$size) # original size
|
||||||
|
Set-Le64 $footer 48 ([UInt64]$size) # current size
|
||||||
|
|
||||||
|
# Disk geometry (VHD layout is big-endian here: 2-byte cylinders + 1 head + 1 spt)
|
||||||
|
$spt = 0; $h = 0
|
||||||
|
if ($totalSectors -ge 65535 * 16 * 255) { $spt = 255; $h = 16 }
|
||||||
|
elseif ($totalSectors -ge 65535 * 16 * 17) { $spt = 63; $h = 16 }
|
||||||
|
else { $spt = 17; $h = 16 }
|
||||||
|
$c = [UInt64][Math]::Floor([double]$totalSectors / ($h * $spt))
|
||||||
|
if ($c -gt 65535) { $c = 65535 }
|
||||||
|
$footer[56] = [byte](($c -shr 8) -band 0xFF)
|
||||||
|
$footer[57] = [byte]($c -band 0xFF)
|
||||||
|
$footer[58] = [byte]$h
|
||||||
|
$footer[59] = [byte]$spt
|
||||||
|
|
||||||
|
Set-Le32 $footer 60 2 # disk type: fixed
|
||||||
|
[Array]::Copy(([Guid]::NewGuid()).ToByteArray(), 0, $footer, 68, 16)
|
||||||
|
$footer[84] = 0 # saved state: normal
|
||||||
|
|
||||||
|
# Checksum: one's complement of the uint32 sum over the whole footer
|
||||||
|
$sum = [UInt64]0
|
||||||
|
for ($i = 0; $i -lt 512; $i += 4) {
|
||||||
|
$sum = ($sum + [BitConverter]::ToUInt32($footer, $i)) -band 0xFFFFFFFF
|
||||||
|
}
|
||||||
|
Set-Le32 $footer 64 ((0xFFFFFFFF -bxor $sum) -band 0xFFFFFFFF)
|
||||||
|
|
||||||
|
[void]$fs.Seek(0, [System.IO.SeekOrigin]::End)
|
||||||
|
$fs.Write($footer, 0, 512)
|
||||||
|
} finally {
|
||||||
|
$fs.Close()
|
||||||
|
}
|
||||||
|
Write-Host "[build] VHD (Windows mountable): $VhdPath"
|
||||||
|
}
|
||||||
|
|
||||||
# Build a single image variant (dev/release): inject FUCK + firstInit.txt from
|
# Build a single image variant (dev/release): inject FUCK + firstInit.txt from
|
||||||
# build/configs/<variant>/ into SYSTEM/, then package via New-GptFat32Image.
|
# build/configs/<variant>/ into SYSTEM/, then package via New-GptFat32Image.
|
||||||
# If $ImagePath is $null, only injects config without packaging (used to
|
# If $ImagePath is $null, only injects config without packaging (used to
|
||||||
# restore SYSTEM dir to dev config after dual-image build).
|
# restore SYSTEM dir to dev config after dual-image build).
|
||||||
|
# NOTE: configs/<variant> dir may be absent; then SYSTEM is packaged as-is.
|
||||||
function Build-ImageVariant([string]$VariantName, [string]$ImagePath) {
|
function Build-ImageVariant([string]$VariantName, [string]$ImagePath) {
|
||||||
$cfgDir = Join-Path $ConfigsDir $VariantName
|
$cfgDir = Join-Path $ConfigsDir $VariantName
|
||||||
if (-not (Test-Path $cfgDir)) {
|
|
||||||
throw "Config variant directory not found: $cfgDir"
|
if (Test-Path $cfgDir) {
|
||||||
|
$fuckSrc = Join-Path $cfgDir 'FUCK'
|
||||||
|
$fuckDst = Join-Path $SystemDir 'system\deshab64\FUCK'
|
||||||
|
$fiSrc = Join-Path $cfgDir 'firstInit.txt'
|
||||||
|
$fiDst = Join-Path $SystemDir 'system\user\use\firstInit.txt'
|
||||||
|
|
||||||
|
if (-not (Test-Path $fuckSrc)) { throw "Missing FUCK template: $fuckSrc" }
|
||||||
|
if (-not (Test-Path $fiSrc)) { throw "Missing firstInit.txt template: $fiSrc" }
|
||||||
|
|
||||||
|
Copy-Item -LiteralPath $fuckSrc -Destination $fuckDst -Force
|
||||||
|
Copy-Item -LiteralPath $fiSrc -Destination $fiDst -Force
|
||||||
|
Write-Host "[build] Injected config variant=$VariantName (FUCK + firstInit.txt)"
|
||||||
|
} else {
|
||||||
|
Write-Host "[build] WARN: configs/$VariantName absent, packaging SYSTEM as-is"
|
||||||
}
|
}
|
||||||
|
|
||||||
$fuckSrc = Join-Path $cfgDir 'FUCK'
|
|
||||||
$fuckDst = Join-Path $SystemDir 'system\deshab64\FUCK'
|
|
||||||
$fiSrc = Join-Path $cfgDir 'firstInit.txt'
|
|
||||||
$fiDst = Join-Path $SystemDir 'system\user\use\firstInit.txt'
|
|
||||||
|
|
||||||
if (-not (Test-Path $fuckSrc)) { throw "Missing FUCK template: $fuckSrc" }
|
|
||||||
if (-not (Test-Path $fiSrc)) { throw "Missing firstInit.txt template: $fiSrc" }
|
|
||||||
|
|
||||||
Copy-Item -LiteralPath $fuckSrc -Destination $fuckDst -Force
|
|
||||||
Copy-Item -LiteralPath $fiSrc -Destination $fiDst -Force
|
|
||||||
Write-Host "[build] Injected config variant=$VariantName (FUCK + firstInit.txt)"
|
|
||||||
|
|
||||||
if ($ImagePath) {
|
if ($ImagePath) {
|
||||||
# Phase 0: ESP enlarged from 768 to $EspSizeMB (default 2048) to fit
|
# Phase 0: ESP enlarged from 768 to $EspSizeMB (default 2048) to fit
|
||||||
# VSCode + Electron + GUI stack libs (VSCode integration roadmap).
|
# VSCode + Electron + GUI stack libs (VSCode integration roadmap).
|
||||||
New-GptFat32Image $SystemDir $ImagePath $EspSizeMB
|
New-GptFat32Image $SystemDir $ImagePath $EspSizeMB
|
||||||
|
if ($Vhd) {
|
||||||
|
$vhdPath = [System.IO.Path]::ChangeExtension($ImagePath, '.vhd')
|
||||||
|
ConvertTo-FixedVhd $ImagePath $vhdPath
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -785,6 +853,7 @@ Build-DkmDriver (Join-Path $Root 'CODE\DKM\apic\apic.c') (Join-Path $SystemDir '
|
|||||||
|
|
||||||
Write-Host '[build] Building DKM storage drivers...'
|
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\nvme\nvme.c') (Join-Path $SystemDir 'driver\block\nvme.drv')
|
||||||
|
Build-DkmDriver (Join-Path $Root 'CODE\DKM\ehci\ehci.c') (Join-Path $SystemDir 'driver\block\ehci.drv')
|
||||||
Build-DkmDriver (Join-Path $Root 'CODE\DKM\xhci\xhci.c') (Join-Path $SystemDir 'driver\block\xhci.drv')
|
Build-DkmDriver (Join-Path $Root 'CODE\DKM\xhci\xhci.c') (Join-Path $SystemDir 'driver\block\xhci.drv')
|
||||||
|
|
||||||
Write-Host '[build] Building DKM network drivers...'
|
Write-Host '[build] Building DKM network drivers...'
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
# Deshab Kernel Configuration (DEV - QEMU 测试)
|
|
||||||
# dev_mode=1 自动跑 shell.elf 测试序列, 调试全开, QEMU 验证选项
|
|
||||||
# 路径: SYSTEM/system/deshab64/FUCK
|
|
||||||
# UTSM 通过 Limine boot module 预加载读取(cmdline="fuck:config")
|
|
||||||
# DSK 通过 FAT32 子目录遍历读取(路径 "SYSTEM /DESHAB64 /FUCK ")
|
|
||||||
# 缺失或空的键使用硬编码默认值,行为与无配置文件时一致
|
|
||||||
|
|
||||||
[boot]
|
|
||||||
vmm=1
|
|
||||||
linux_guest=1
|
|
||||||
# OpenXJ380 第二 guest(需 SYSTEM/boot/xj380.krl 存在,limine.conf 取消注释)
|
|
||||||
# 启动后 host 挂起直至 XJ380 guest 终止;无模块时优雅跳过
|
|
||||||
xj380_guest=1
|
|
||||||
selftest=1
|
|
||||||
dev_mode=1
|
|
||||||
# QEMU 不需要真机安全检测
|
|
||||||
la57_guard=0
|
|
||||||
mcfg_check=0
|
|
||||||
max_phys_addr_bits=0
|
|
||||||
# 实机启动日志持久化:把启动日志写入磁盘原始扇区(开发测试用)
|
|
||||||
# 启用后启动时把内存日志缓冲区写到指定扇区区间
|
|
||||||
# 事后用 WinHex/dd 读取对应扇区即可查看完整启动日志
|
|
||||||
# 注意: 写入 LBA 1-33 会覆盖 GPT, 测试盘建议从 34 开始或测试后重新烧录
|
|
||||||
disk_log=1
|
|
||||||
disk_log_start=2
|
|
||||||
disk_log_end=1000
|
|
||||||
|
|
||||||
[drivers]
|
|
||||||
timer=1
|
|
||||||
apic=1
|
|
||||||
acpi=1
|
|
||||||
pci=1
|
|
||||||
console_fb=1
|
|
||||||
ahci=1
|
|
||||||
nvme=1
|
|
||||||
bootfs=1
|
|
||||||
vfs=1
|
|
||||||
fat32=1
|
|
||||||
devfs=1
|
|
||||||
e1000=1
|
|
||||||
virtio_net=1
|
|
||||||
ath9k=0
|
|
||||||
ps2kbd=1
|
|
||||||
ioapic_init=1
|
|
||||||
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=1
|
|
||||||
# B7 阶段2: 随 apic_route=1 额外放开 legacy IRQ1(键盘)/12(鼠标)/11(e1000)
|
|
||||||
# QEMU 全链路已验证 (B1 向导按键/B2 启动/e1000 IRQ11)
|
|
||||||
apic_route_legacy=1
|
|
||||||
# B7 阶段3: NVMe MSI/MSI-X 中断驱动开关 (nvme.drv 自解析, 缺失=1)
|
|
||||||
nvme_msi=1
|
|
||||||
# B7 诊断 (apic.drv): 1=启用 HPET 绝对时基对拍 + IRQ12 鼠标流观察窗
|
|
||||||
# 常规构建保持 0
|
|
||||||
apic_diag=0
|
|
||||||
|
|
||||||
[utsm]
|
|
||||||
max_segments=1024
|
|
||||||
max_capabilities=256
|
|
||||||
max_pckc_keys=8
|
|
||||||
dirty_shard_pages=1024
|
|
||||||
arena_size_mb=64
|
|
||||||
drr_emergency_pool_kb=64
|
|
||||||
drr_recovery_log_size=4096
|
|
||||||
drr_heartbeat_interval=0
|
|
||||||
|
|
||||||
[dsk]
|
|
||||||
show_logo=1
|
|
||||||
skip_login=1
|
|
||||||
default_shell=cmd
|
|
||||||
mouse_init=1
|
|
||||||
netman=1
|
|
||||||
|
|
||||||
[debug]
|
|
||||||
# 全局插桩开关 (0=禁用,零开销; 1=启用所有 INSTR_* 宏)
|
|
||||||
enabled=1
|
|
||||||
# 日志级别阈值 (0=TRACE, 1=DEBUG, 2=INFO, 3=WARN, 4=ERROR, 5=SILENT)
|
|
||||||
log_level=1
|
|
||||||
# 探测缓冲开关 (0=禁用; 1=启用 INSTR_PROBE 写入环形缓冲)
|
|
||||||
probe_enable=1
|
|
||||||
# 探测缓冲容量 (条目数,每条 40 字节,默认 4096 条 ≈ 160KB)
|
|
||||||
probe_capacity=4096
|
|
||||||
# 统计摘要间隔 (每 N 次增量输出一次摘要)
|
|
||||||
stat_summary_interval=256
|
|
||||||
# 模块级过滤: 1=启用该模块的 TRACE/DEBUG 级别日志
|
|
||||||
utsm_trace=1
|
|
||||||
vmm_trace=1
|
|
||||||
dkm_trace=1
|
|
||||||
dsk_trace=1
|
|
||||||
block_trace=1
|
|
||||||
net_trace=0
|
|
||||||
input_trace=1
|
|
||||||
|
|
||||||
[compat]
|
|
||||||
# 兼容层动态链接库搜索路径配置
|
|
||||||
# lib_path: FAT32 根目录下存放 .dll/.so 的子目录名
|
|
||||||
lib_path=lib
|
|
||||||
# linux_lib_guest_path: Linux guest 内库目录路径
|
|
||||||
linux_lib_guest_path=/usr/lib/deshab
|
|
||||||
# linux_lib_sync: 启动时推送 .so 到 Linux guest (0=关闭, 1=开启)
|
|
||||||
linux_lib_sync=1
|
|
||||||
# pe_dll_search: PE shim miss 后从 SYSTEM/lib 加载真实 DLL (0=关闭, 1=开启)
|
|
||||||
pe_dll_search=1
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
1
|
|
||||||
1
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
# Deshab Kernel Configuration (REALTEST - 实机开发测试)
|
|
||||||
# 基于 dev 配置, 启用磁盘日志持久化
|
|
||||||
# 启动时把完整日志写入磁盘 LBA 2-1000 扇区, 事后用 WinHex/dd 读取
|
|
||||||
# 路径: SYSTEM/system/deshab64/FUCK
|
|
||||||
# UTSM 通过 Limine boot module 预加载读取(cmdline="fuck:config")
|
|
||||||
# DSK 通过 FAT32 子目录遍历读取(路径 "SYSTEM /DESHAB64 /FUCK ")
|
|
||||||
# 缺失或空的键使用硬编码默认值,行为与无配置文件时一致
|
|
||||||
|
|
||||||
[boot]
|
|
||||||
vmm=0
|
|
||||||
linux_guest=0
|
|
||||||
# OpenXJ380 第二 guest:realtest 默认关闭(vmm=0 时天然不执行)
|
|
||||||
xj380_guest=0
|
|
||||||
selftest=1
|
|
||||||
dev_mode=1
|
|
||||||
# 真机安全检查全部放宽(和 dev 一样),排除安全检查导致黑屏
|
|
||||||
la57_guard=0
|
|
||||||
mcfg_check=0
|
|
||||||
max_phys_addr_bits=0
|
|
||||||
# 实机启动日志持久化: 启用, 把启动日志写入磁盘原始扇区
|
|
||||||
disk_log=1
|
|
||||||
disk_log_start=2
|
|
||||||
disk_log_end=1000
|
|
||||||
|
|
||||||
[drivers]
|
|
||||||
timer=1
|
|
||||||
apic=1
|
|
||||||
acpi=1
|
|
||||||
pci=1
|
|
||||||
console_fb=1
|
|
||||||
ahci=1
|
|
||||||
nvme=1
|
|
||||||
xhci=0
|
|
||||||
bootfs=1
|
|
||||||
vfs=1
|
|
||||||
fat32=1
|
|
||||||
devfs=1
|
|
||||||
e1000=1
|
|
||||||
virtio_net=1
|
|
||||||
ath9k=0
|
|
||||||
ps2kbd=1
|
|
||||||
ioapic_init=1
|
|
||||||
lapic_enable=1
|
|
||||||
apic_route=1
|
|
||||||
apic_route_legacy=1
|
|
||||||
nvme_msi=1
|
|
||||||
apic_diag=0
|
|
||||||
|
|
||||||
[utsm]
|
|
||||||
max_segments=1024
|
|
||||||
max_capabilities=256
|
|
||||||
max_pckc_keys=8
|
|
||||||
dirty_shard_pages=1024
|
|
||||||
arena_size_mb=64
|
|
||||||
drr_emergency_pool_kb=64
|
|
||||||
drr_recovery_log_size=4096
|
|
||||||
drr_heartbeat_interval=0
|
|
||||||
|
|
||||||
[dsk]
|
|
||||||
show_logo=1
|
|
||||||
skip_login=1
|
|
||||||
default_shell=cmd
|
|
||||||
mouse_init=1
|
|
||||||
netman=1
|
|
||||||
|
|
||||||
[debug]
|
|
||||||
enabled=1
|
|
||||||
log_level=1
|
|
||||||
probe_enable=1
|
|
||||||
probe_capacity=4096
|
|
||||||
stat_summary_interval=256
|
|
||||||
utsm_trace=1
|
|
||||||
vmm_trace=1
|
|
||||||
dkm_trace=1
|
|
||||||
dsk_trace=1
|
|
||||||
block_trace=1
|
|
||||||
net_trace=0
|
|
||||||
input_trace=1
|
|
||||||
|
|
||||||
[compat]
|
|
||||||
lib_path=lib
|
|
||||||
linux_lib_guest_path=/usr/lib/deshab
|
|
||||||
linux_lib_sync=1
|
|
||||||
pe_dll_search=1
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
1
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
# Deshab Kernel Configuration (RELEASE - 实机测试)
|
|
||||||
# dev_mode=0 直接进桌面, 要求登录, 调试零开销, 真机安全选项全开
|
|
||||||
# 路径: SYSTEM/system/deshab64/FUCK
|
|
||||||
# UTSM 通过 Limine boot module 预加载读取(cmdline="fuck:config")
|
|
||||||
# DSK 通过 FAT32 子目录遍历读取(路径 "SYSTEM /DESHAB64 /FUCK ")
|
|
||||||
# 缺失或空的键使用硬编码默认值,行为与无配置文件时一致
|
|
||||||
|
|
||||||
[boot]
|
|
||||||
vmm=1
|
|
||||||
linux_guest=1
|
|
||||||
# OpenXJ380 第二 guest(需 SYSTEM/boot/xj380.krl 存在,limine.conf 取消注释)
|
|
||||||
xj380_guest=1
|
|
||||||
selftest=1
|
|
||||||
dev_mode=0
|
|
||||||
# 真机安全: LA57 检测 (0=不检测, 1=检测并警告, 2=检测并拒绝启动)
|
|
||||||
# 真机若 BIOS 启用 5 级分页, 设 2 可阻止启动避免数据损坏
|
|
||||||
la57_guard=2
|
|
||||||
# 真机安全: PCIe MCFG 检测 (0=不检测, 1=检测并记录)
|
|
||||||
mcfg_check=1
|
|
||||||
# 真机安全: 允许的最大物理地址宽度 (超过则拒绝启动, 0=不限制)
|
|
||||||
# 典型值: 46 (QEMU), 48 (多数实机), 52 (服务器)
|
|
||||||
max_phys_addr_bits=48
|
|
||||||
# 实机启动日志持久化(开发测试用, release 默认关闭)
|
|
||||||
# 启用: disk_log=1, 事后用 WinHex/dd 读取对应扇区查看启动日志
|
|
||||||
# 注意: 写入 LBA 1-33 会覆盖 GPT, 建议从 34 开始
|
|
||||||
disk_log=0
|
|
||||||
disk_log_start=2
|
|
||||||
disk_log_end=1000
|
|
||||||
|
|
||||||
[drivers]
|
|
||||||
timer=1
|
|
||||||
apic=1
|
|
||||||
acpi=1
|
|
||||||
pci=1
|
|
||||||
console_fb=1
|
|
||||||
ahci=1
|
|
||||||
nvme=1
|
|
||||||
bootfs=1
|
|
||||||
vfs=1
|
|
||||||
fat32=1
|
|
||||||
devfs=1
|
|
||||||
e1000=1
|
|
||||||
virtio_net=1
|
|
||||||
ath9k=0
|
|
||||||
ps2kbd=1
|
|
||||||
# 真机安全: 禁用不稳定的真机特性
|
|
||||||
# ioapic_init=0 可禁用 IOAPIC 重定向表编程 (留 PIC 模式)
|
|
||||||
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=1
|
|
||||||
# B7 阶段2: 随 apic_route=1 额外放开 legacy IRQ1(键盘)/12(鼠标)/11(e1000)
|
|
||||||
# 真机冒烟前回 0, 避免 legacy IRQ 持续投递在非常规 IOAPIC 拓扑下出问题
|
|
||||||
apic_route_legacy=0
|
|
||||||
# B7 阶段3: NVMe MSI/MSI-X 中断驱动开关 (nvme.drv 自解析, 缺失=1)
|
|
||||||
nvme_msi=1
|
|
||||||
# B7 诊断 (apic.drv): 1=启用 HPET 绝对时基对拍 + IRQ12 鼠标流观察窗
|
|
||||||
# 常规构建保持 0
|
|
||||||
apic_diag=0
|
|
||||||
|
|
||||||
[utsm]
|
|
||||||
max_segments=1024
|
|
||||||
max_capabilities=256
|
|
||||||
max_pckc_keys=8
|
|
||||||
dirty_shard_pages=1024
|
|
||||||
arena_size_mb=64
|
|
||||||
drr_emergency_pool_kb=64
|
|
||||||
drr_recovery_log_size=4096
|
|
||||||
drr_heartbeat_interval=0
|
|
||||||
|
|
||||||
[dsk]
|
|
||||||
show_logo=1
|
|
||||||
# release: 要求登录, 验证 USER.CONF 解密链路
|
|
||||||
skip_login=0
|
|
||||||
default_shell=cmd
|
|
||||||
mouse_init=1
|
|
||||||
netman=1
|
|
||||||
|
|
||||||
[debug]
|
|
||||||
# 全局插桩开关 (release 零开销)
|
|
||||||
enabled=0
|
|
||||||
# 日志级别阈值 (0=TRACE, 1=DEBUG, 2=INFO, 3=WARN, 4=ERROR, 5=SILENT)
|
|
||||||
log_level=3
|
|
||||||
# 探测缓冲开关 (release 关闭省内存)
|
|
||||||
probe_enable=0
|
|
||||||
# 探测缓冲容量 (条目数,每条 40 字节,默认 4096 条 ≈ 160KB)
|
|
||||||
probe_capacity=4096
|
|
||||||
# 统计摘要间隔 (每 N 次增量输出一次摘要)
|
|
||||||
stat_summary_interval=256
|
|
||||||
# 模块级过滤: release 全关
|
|
||||||
utsm_trace=0
|
|
||||||
vmm_trace=0
|
|
||||||
dkm_trace=0
|
|
||||||
dsk_trace=0
|
|
||||||
block_trace=0
|
|
||||||
net_trace=0
|
|
||||||
input_trace=0
|
|
||||||
|
|
||||||
[compat]
|
|
||||||
# 兼容层动态链接库搜索路径配置
|
|
||||||
# lib_path: FAT32 根目录下存放 .dll/.so 的子目录名
|
|
||||||
lib_path=lib
|
|
||||||
# linux_lib_guest_path: Linux guest 内库目录路径
|
|
||||||
linux_lib_guest_path=/usr/lib/deshab
|
|
||||||
# linux_lib_sync: 启动时推送 .so 到 Linux guest (0=关闭, 1=开启)
|
|
||||||
linux_lib_sync=1
|
|
||||||
# pe_dll_search: PE shim miss 后从 SYSTEM/lib 加载真实 DLL (0=关闭, 1=开启)
|
|
||||||
pe_dll_search=1
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
1
|
|
||||||
0
|
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 243 KiB |
@@ -1,48 +0,0 @@
|
|||||||
# 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,填修复提交与验证结果
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
#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\<case>.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 }
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
#requires -Version 5.1
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
boot-regression — 启动链路回归测试(docs/RE/test-cases/boot-regression.md)
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
场景覆盖:
|
|
||||||
B1 首次启动 firstInit.txt="0\n0" UTSM → DSK → mouseInit → netman → FirstInit 向导(自动按键) → desktop
|
|
||||||
B2 正常启动 firstInit.txt="1\n0" UTSM → DSK → login(skip) → cmd(AUTOEXEC=ver/exit) → desktop
|
|
||||||
B3 dev_mode firstInit.txt="1\n1" DSK → shell dev_tests → Esc → cmd → desktop
|
|
||||||
B4 无 SATA 盘 UTSM DSK 加载回退 Limine boot module,DSK 降级运行不崩溃
|
|
||||||
B5 无 NIC e1000/virtio_net not found,系统继续启动到 desktop
|
|
||||||
B6 Linux 兼容层 见 linux-compat.ps1
|
|
||||||
|
|
||||||
退出码:全部通过 0,任一 FAIL 1。
|
|
||||||
#>
|
|
||||||
param(
|
|
||||||
[switch]$Build,
|
|
||||||
[string[]]$Scenarios = @('B1', 'B2', 'B3', 'B4', 'B5')
|
|
||||||
)
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
. (Join-Path $PSScriptRoot 'lib\QemuTest.ps1')
|
|
||||||
|
|
||||||
Initialize-QemuTest -Build:$Build
|
|
||||||
$result = New-TestCaseResult 'boot-regression'
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
|
||||||
# FirstInit 设置向导自动填表:
|
|
||||||
# 账户卡片(3 字段) → 偏好页(5×Enter) → 网络页(3×Enter) → 完成
|
|
||||||
# 每步都等串口锚点再发键,字段验证失败自动重试。
|
|
||||||
# ----------------------------------------------------------------
|
|
||||||
function Complete-FirstInitWizard {
|
|
||||||
param([Parameter(Mandatory)]$Session)
|
|
||||||
|
|
||||||
$hit = Wait-QemuLog -Session $Session -Patterns @('[FI] fadein done') -TimeoutSeconds 150
|
|
||||||
if (-not $hit) { Add-TestCheck $result 'B1: FirstInit 账户卡片就绪' 'FAIL' 'missing [FI] fadein done'; return $false }
|
|
||||||
|
|
||||||
$done = $false
|
|
||||||
for ($attempt = 1; $attempt -le 3 -and -not $done; $attempt++) {
|
|
||||||
Send-QemuText -Session $Session -Text 'testpc' -Enter
|
|
||||||
Start-Sleep -Milliseconds 400
|
|
||||||
Send-QemuText -Session $Session -Text 'tester' -Enter
|
|
||||||
Start-Sleep -Milliseconds 400
|
|
||||||
Send-QemuText -Session $Session -Text 'pass123' -Enter
|
|
||||||
$hit = Wait-QemuLog -Session $Session -Patterns @('[FI] all fields done', '[FI] validation failed') -TimeoutSeconds 30
|
|
||||||
if ($hit -eq '[FI] all fields done') { $done = $true }
|
|
||||||
else { Add-TestNote $result "B1: FirstInit 字段验证失败,重试第 $attempt 次" }
|
|
||||||
}
|
|
||||||
if (-not $done) { Add-TestCheck $result 'B1: FirstInit 账户字段填写' 'FAIL' 'validation did not pass after 3 attempts'; return $false }
|
|
||||||
Add-TestCheck $result 'B1: FirstInit 账户字段填写' 'PASS'
|
|
||||||
|
|
||||||
Start-Sleep -Milliseconds 600
|
|
||||||
Send-QemuKeys -Session $Session -Keys @('ret', 'ret', 'ret', 'ret', 'ret') -DelayMs 350
|
|
||||||
$hit = Wait-QemuLog -Session $Session -Patterns @('[FirstInit] entering network setup page') -TimeoutSeconds 40
|
|
||||||
if (-not $hit) { Add-TestCheck $result 'B1: FirstInit 偏好页' 'FAIL' 'did not reach network setup page'; return $false }
|
|
||||||
Add-TestCheck $result 'B1: FirstInit 偏好页(5xEnter)' 'PASS'
|
|
||||||
|
|
||||||
Start-Sleep -Milliseconds 400
|
|
||||||
Send-QemuKeys -Session $Session -Keys @('ret', 'ret', 'ret') -DelayMs 350
|
|
||||||
$hit = Wait-QemuLog -Session $Session -Patterns @('[FirstInit] user setup finished') -TimeoutSeconds 40
|
|
||||||
if (-not $hit) { Add-TestCheck $result 'B1: FirstInit 网络页' 'FAIL' 'setup did not finish'; return $false }
|
|
||||||
Add-TestCheck $result 'B1: FirstInit 网络页(3xEnter)' 'PASS'
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
|
||||||
function Invoke-ScenarioB1 {
|
|
||||||
Write-Host "`n--- B1 首次启动 (firstInit=0) ---"
|
|
||||||
Set-DeshabFirstInit -First 0 -DevMode 0
|
|
||||||
$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
|
|
||||||
$log = Read-QemuLogText $s.LogPath
|
|
||||||
$result.LogPath = $s.LogPath
|
|
||||||
Assert-QemuLog $result $log -Ordered @(
|
|
||||||
'[UTSM] SELFTEST PASS',
|
|
||||||
'[DSK] boot',
|
|
||||||
'[DSK] firstInit=0',
|
|
||||||
'[DSK:SCHED] decision: is_first=1 -> mouseInit -> netman -> FirstInit -> desktop',
|
|
||||||
'[DSK] loading mouseInit',
|
|
||||||
'[DSK] loading netman',
|
|
||||||
'[DSK] loading FirstInit',
|
|
||||||
'[FirstInit] boot',
|
|
||||||
'[FirstInit] user setup finished',
|
|
||||||
'[DSK] FirstInit returned, persisting user.conf',
|
|
||||||
'[DSK] writing USER.CONF',
|
|
||||||
'[DSK] firstInit.txt flip rc=',
|
|
||||||
'[DSK] loading desktop',
|
|
||||||
'desktop ready'
|
|
||||||
) -MustNotContain @('[PANIC]')
|
|
||||||
if ($hit) { Add-TestCheck $result 'B1: desktop ready' 'PASS' }
|
|
||||||
else { Add-TestCheck $result 'B1: desktop ready' 'FAIL' 'timeout waiting desktop ready' }
|
|
||||||
} finally {
|
|
||||||
Stop-QemuSession $s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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' -MonitorPort 45501
|
|
||||||
try {
|
|
||||||
$hit = Wait-QemuLog -Session $s -Patterns @('desktop ready') -TimeoutSeconds 150
|
|
||||||
$log = Read-QemuLogText $s.LogPath
|
|
||||||
$result.LogPath = $s.LogPath
|
|
||||||
Assert-QemuLog $result $log -Ordered @(
|
|
||||||
'[UTSM] SELFTEST PASS',
|
|
||||||
'[UTSM] loading DSK',
|
|
||||||
'[DSK] boot',
|
|
||||||
'[DSK] context ok',
|
|
||||||
'[DSK] firstInit != 0',
|
|
||||||
'[DSK:SCHED] decision: is_first=0 -> login then desktop',
|
|
||||||
'[DSK:SCHED] login skipped by FUCK config',
|
|
||||||
'[DSK:SCHED] default_shell=cmd -> cmd.elf',
|
|
||||||
'[cmd] boot',
|
|
||||||
'[cmd] running AUTOEXEC.BAT',
|
|
||||||
'[DSK] cmd.elf returned, falling back to desktop',
|
|
||||||
'[DSK:SCHED] loading desktop',
|
|
||||||
'desktop ready'
|
|
||||||
) -MustNotContain @('[PANIC]')
|
|
||||||
if ($hit) { Add-TestCheck $result 'B2: desktop ready' 'PASS' }
|
|
||||||
else { Add-TestCheck $result 'B2: desktop ready' 'FAIL' 'timeout waiting desktop ready' }
|
|
||||||
} finally {
|
|
||||||
Stop-QemuSession $s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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' -MonitorPort 45501
|
|
||||||
try {
|
|
||||||
$hit = Wait-QemuLog -Session $s -Patterns @('[8] rm TEST.TXT') -TimeoutSeconds 150
|
|
||||||
if (-not $hit) {
|
|
||||||
$result.LogPath = $s.LogPath
|
|
||||||
Add-TestCheck $result 'B3: shell dev_tests 执行' 'FAIL' 'missing [8] rm TEST.TXT'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Start-Sleep -Seconds 2
|
|
||||||
Send-QemuKeys -Session $s -Keys @('esc')
|
|
||||||
$hit2 = Wait-QemuLog -Session $s -Patterns @('desktop ready') -TimeoutSeconds 150
|
|
||||||
$log = Read-QemuLogText $s.LogPath
|
|
||||||
$result.LogPath = $s.LogPath
|
|
||||||
Assert-QemuLog $result $log -Ordered @(
|
|
||||||
'[DSK] dev_mode=1 (developer auto-test)',
|
|
||||||
'[DSK:SCHED] decision: dev_mode -> shell.elf',
|
|
||||||
'[shell] boot',
|
|
||||||
'[1] ls',
|
|
||||||
'[4] cp TEST.TXT COPY.TXT',
|
|
||||||
'[8] rm TEST.TXT',
|
|
||||||
'[shell] esc -> return to DSK',
|
|
||||||
'[DSK] shell auto-test returned',
|
|
||||||
'[DSK:SCHED] default_shell=cmd -> cmd.elf',
|
|
||||||
'desktop ready'
|
|
||||||
) -MustNotContain @('[PANIC]')
|
|
||||||
if ($hit2) { Add-TestCheck $result 'B3: Esc 返回后到 desktop' 'PASS' }
|
|
||||||
else { Add-TestCheck $result 'B3: Esc 返回后到 desktop' 'FAIL' 'timeout waiting desktop ready' }
|
|
||||||
} finally {
|
|
||||||
Stop-QemuSession $s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-ScenarioB4 {
|
|
||||||
Write-Host "`n--- B4 无 SATA 盘 (DSK 回退 Limine module) ---"
|
|
||||||
$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
|
|
||||||
$log = Read-QemuLogText $s.LogPath
|
|
||||||
$result.LogPath = $s.LogPath
|
|
||||||
Assert-QemuLog $result $log -Ordered @(
|
|
||||||
'[UTSM] SELFTEST PASS',
|
|
||||||
'[UTSM] loading DSK',
|
|
||||||
'[UTSM] FAT32 block path unavailable; trying Limine module',
|
|
||||||
'[DSK] boot',
|
|
||||||
'[DSK] context ok',
|
|
||||||
'[DSK] firstInit.txt not found, assuming first boot'
|
|
||||||
) -MustContain @('[DSK] mouseInit not found') -MustNotContain @('[PANIC]')
|
|
||||||
if ($hit -eq '[PANIC]') { Add-TestCheck $result 'B4: 无盘降级不崩溃' 'FAIL' 'kernel panicked' }
|
|
||||||
else { Add-TestCheck $result 'B4: 无盘降级不崩溃' 'PASS' }
|
|
||||||
} finally {
|
|
||||||
Stop-QemuSession $s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 -MonitorPort 45501
|
|
||||||
try {
|
|
||||||
$hit = Wait-QemuLog -Session $s -Patterns @('desktop ready') -TimeoutSeconds 150
|
|
||||||
$log = Read-QemuLogText $s.LogPath
|
|
||||||
$result.LogPath = $s.LogPath
|
|
||||||
Assert-QemuLog $result $log -MustContain @(
|
|
||||||
'[e1000] device not found',
|
|
||||||
'[virtio_net] virtio-net device not found'
|
|
||||||
) -Ordered @(
|
|
||||||
'[UTSM] SELFTEST PASS',
|
|
||||||
'[DSK] boot',
|
|
||||||
'[DSK] firstInit != 0',
|
|
||||||
'desktop ready'
|
|
||||||
) -MustNotContain @('[PANIC]')
|
|
||||||
if ($hit) { Add-TestCheck $result 'B5: 无 NIC 到 desktop' 'PASS' }
|
|
||||||
else { Add-TestCheck $result 'B5: 无 NIC 到 desktop' 'FAIL' 'timeout waiting desktop ready' }
|
|
||||||
} finally {
|
|
||||||
Stop-QemuSession $s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
|
||||||
try {
|
|
||||||
foreach ($sc in $Scenarios) {
|
|
||||||
switch ($sc.ToUpper()) {
|
|
||||||
'B1' { Invoke-ScenarioB1 }
|
|
||||||
'B2' { Invoke-ScenarioB2 }
|
|
||||||
'B3' { Invoke-ScenarioB3 }
|
|
||||||
'B4' { Invoke-ScenarioB4 }
|
|
||||||
'B5' { Invoke-ScenarioB5 }
|
|
||||||
default { Add-TestNote $result "未知场景 $sc,跳过" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Add-TestNote $result 'B6 (Linux 兼容层启用) 由 linux-compat.ps1 覆盖'
|
|
||||||
} finally {
|
|
||||||
Restore-DeshabScenario
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-TestCaseResult $result
|
|
||||||
if ($result.Status -eq 'FAIL') { exit 1 } else { exit 0 }
|
|
||||||
@@ -1,369 +0,0 @@
|
|||||||
#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<w> <h>\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 }
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
#requires -Version 5.1
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
driver-matrix — DKM 驱动功能矩阵(docs/RE/test-cases/driver-matrix.md)
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
单次 QEMU 会话挂全设备(SATA 测试盘 + NVMe 测试盘 + xHCI USB 盘 + e1000),
|
|
||||||
验证 4 个 stage 共 16 个清单驱动的加载顺序与功能锚点:
|
|
||||||
|
|
||||||
stage0: timer / apic / acpi / pci
|
|
||||||
stage1: console_fb / ahci / nvme / xhci / bootfs
|
|
||||||
stage2: vfs / fat32 / devfs
|
|
||||||
stage3: e1000 / virtio_net(无设备,降级) / ath9k(FUCK 禁用) / ps2kbd
|
|
||||||
|
|
||||||
已知限制(与文档一致):
|
|
||||||
- virtio_net 在无 virtio-net 设备时走 not-found 降级路径(断言降级锚点)
|
|
||||||
- ath9k 被 FUCK [drivers] ath9k=0 禁用(断言 DSM 禁用锚点;QEMU 无 WiFi 设备)
|
|
||||||
- xhci USB MSC 枚举在当前实现下可能失败,仅断言驱动就绪
|
|
||||||
|
|
||||||
退出码:全部通过 0,任一 FAIL 1。
|
|
||||||
#>
|
|
||||||
param([switch]$Build)
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
. (Join-Path $PSScriptRoot 'lib\QemuTest.ps1')
|
|
||||||
|
|
||||||
Initialize-QemuTest -Build:$Build
|
|
||||||
$result = New-TestCaseResult 'driver-matrix'
|
|
||||||
|
|
||||||
Write-Host "`n--- 全设备单会话驱动矩阵 ---"
|
|
||||||
$s = Start-QemuSession -Name 'driver-matrix' -MonitorPort 45502
|
|
||||||
try {
|
|
||||||
$hit = Wait-QemuLog -Session $s -Patterns @('[DSK] boot') -TimeoutSeconds 150
|
|
||||||
Start-Sleep -Seconds 2
|
|
||||||
$log = Read-QemuLogText $s.LogPath
|
|
||||||
$result.LogPath = $s.LogPath
|
|
||||||
|
|
||||||
# ---- stage 加载顺序(driver ready 锚点按 manifest 顺序) ----
|
|
||||||
Assert-QemuLog $result $log -Ordered @(
|
|
||||||
'[timer] driver ready',
|
|
||||||
'[apic] driver ready',
|
|
||||||
'[acpi] driver ready',
|
|
||||||
'[pci] driver ready',
|
|
||||||
'[console_fb] ready',
|
|
||||||
'[ahci] driver ready',
|
|
||||||
'[nvme] driver ready',
|
|
||||||
'[xhci] driver ready',
|
|
||||||
'[bootfs] driver ready',
|
|
||||||
'[vfs] driver ready',
|
|
||||||
'[fat32] driver ready',
|
|
||||||
'[devfs] driver ready',
|
|
||||||
'[e1000] driver ready',
|
|
||||||
'[virtio_net] driver ready',
|
|
||||||
'[kbd] driver ready',
|
|
||||||
'[UTSM] SELFTEST PASS'
|
|
||||||
)
|
|
||||||
|
|
||||||
# ---- 功能验证点 ----
|
|
||||||
Assert-QemuLog $result $log -MustContain @(
|
|
||||||
'[ahci] block provider index=', # ahci: ahci0 block provider 注册
|
|
||||||
'[nvme] block provider index=', # nvme: block provider 注册
|
|
||||||
'[fat32] using block provider image', # fat32: 经块设备解析测试镜像
|
|
||||||
'[e1000] netdev registered', # e1000: 注册 netdev
|
|
||||||
'[e1000] link=up', # e1000: 链路就绪
|
|
||||||
'[kbd] IRQ1 registered', # ps2kbd: IRQ1 注册
|
|
||||||
'[virtio_net] virtio-net device not found', # virtio_net: e1000-only 拓扑降级
|
|
||||||
'[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
|
|
||||||
|
|
||||||
# ---- 4 个 stage 均完成 ----
|
|
||||||
Assert-QemuLog $result $log -CountPattern '[DSM] stage end' -MinCount 4
|
|
||||||
|
|
||||||
if ($hit) { Add-TestCheck $result '系统到达 DSK' 'PASS' }
|
|
||||||
else { Add-TestCheck $result '系统到达 DSK' 'FAIL' 'timeout waiting [DSK] boot' }
|
|
||||||
|
|
||||||
Add-TestNote $result 'ath9k: QEMU 无 WiFi 设备模拟,FUCK ath9k=0,走 DSM 禁用路径(文档第 14 行 ⏭)'
|
|
||||||
Add-TestNote $result 'xhci: USB MSC 枚举当前可能失败(已知限制),仅断言 [xhci] driver ready'
|
|
||||||
} finally {
|
|
||||||
Stop-QemuSession $s
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-TestCaseResult $result
|
|
||||||
if ($result.Status -eq 'FAIL') { exit 1 } else { exit 0 }
|
|
||||||
@@ -1,861 +0,0 @@
|
|||||||
#requires -Version 5.1
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Deshab QEMU 自动化测试公共库(tests/qemu/lib/QemuTest.ps1)
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
提供:
|
|
||||||
- QEMU 会话管理(启动 / 计时杀进程 / 串口日志收集)
|
|
||||||
- 日志断言引擎(必须匹配 / 禁止匹配 / 顺序匹配 / 计数匹配)
|
|
||||||
- QEMU monitor TCP 客户端(sendkey / mouse_move / screendump)
|
|
||||||
- 场景准备(firstInit.txt 与 AUTOEXEC.BAT 备份-替换-恢复 + SATA 镜像重建)
|
|
||||||
- 用例结果报告(PASS/FAIL/MANUAL/SKIP,失败时输出日志尾部上下文)
|
|
||||||
|
|
||||||
兼容 PowerShell 5.1。所有临时文件写入 .build_tmp\tests\。
|
|
||||||
#>
|
|
||||||
|
|
||||||
# ---------------- 路径 ----------------
|
|
||||||
$script:RepoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..'))
|
|
||||||
$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
|
|
||||||
|
|
||||||
# 场景备份(模块级,保证只备份一次、恢复幂等)
|
|
||||||
$script:FirstInitBackup = $null
|
|
||||||
$script:AutoexecBackup = $null
|
|
||||||
$script:FuckBackup = $null
|
|
||||||
|
|
||||||
# ================================================================
|
|
||||||
# 环境初始化
|
|
||||||
# ================================================================
|
|
||||||
|
|
||||||
function Initialize-QemuTest {
|
|
||||||
param([switch]$Build)
|
|
||||||
|
|
||||||
# --- 定位 QEMU ---
|
|
||||||
$qemu = $null
|
|
||||||
$cmd = Get-Command 'qemu-system-x86_64' -ErrorAction SilentlyContinue
|
|
||||||
if ($cmd) { $qemu = $cmd.Source }
|
|
||||||
foreach ($c in @('C:\Program Files\qemu\qemu-system-x86_64.exe',
|
|
||||||
'C:\Program Files (x86)\qemu\qemu-system-x86_64.exe',
|
|
||||||
'C:\msys64\mingw64\bin\qemu-system-x86_64.exe',
|
|
||||||
'C:\msys64\ucrt64\bin\qemu-system-x86_64.exe')) {
|
|
||||||
if (-not $qemu -and (Test-Path $c)) { $qemu = $c }
|
|
||||||
}
|
|
||||||
if (-not $qemu) { throw 'qemu-system-x86_64 not found (install QEMU or add to PATH)' }
|
|
||||||
|
|
||||||
# --- 定位 OVMF ---
|
|
||||||
$ovmf = $null
|
|
||||||
foreach ($c in @('C:\Program Files\qemu\share\edk2-x86_64-code.fd',
|
|
||||||
'C:\Program Files\qemu\share\OVMF_CODE.fd',
|
|
||||||
'C:\msys64\mingw64\share\edk2\ovmf\OVMF_CODE.fd',
|
|
||||||
'C:\msys64\ucrt64\share\edk2\ovmf\OVMF_CODE.fd')) {
|
|
||||||
if (Test-Path $c) { $ovmf = $c; break }
|
|
||||||
}
|
|
||||||
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
|
|
||||||
|
|
||||||
$imgSrc = Join-Path $script:IsoDir 'deshab-dev.img'
|
|
||||||
$nvmeSrc = Join-Path $script:BuildTmp 'nvme_test.img'
|
|
||||||
$usbSrc = Join-Path $script:BuildTmp 'usb_test.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 $imgSrc)) { throw "image missing: $imgSrc (run with -Build)" }
|
|
||||||
|
|
||||||
# --- 并发隔离: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 = $nvme
|
|
||||||
UsbImg = $usb
|
|
||||||
}
|
|
||||||
Write-Host "[init] QEMU: $qemu"
|
|
||||||
Write-Host "[init] OVMF: $ovmf"
|
|
||||||
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(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.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 {
|
|
||||||
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)
|
|
||||||
$listener.Start()
|
|
||||||
$listener.Stop()
|
|
||||||
return $port
|
|
||||||
} catch {
|
|
||||||
if ($listener) { try { $listener.Stop() } catch {} }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw 'no free TCP port available for QEMU monitor'
|
|
||||||
}
|
|
||||||
|
|
||||||
# ================================================================
|
|
||||||
# QEMU 会话
|
|
||||||
# ================================================================
|
|
||||||
|
|
||||||
function Start-QemuSession {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)][string]$Name,
|
|
||||||
[string]$Accel = 'whpx',
|
|
||||||
[switch]$NoSata,
|
|
||||||
[switch]$NoNvme,
|
|
||||||
[switch]$NoUsb,
|
|
||||||
[switch]$NoNet,
|
|
||||||
[string]$NetModel = 'e1000',
|
|
||||||
[switch]$NoMonitor,
|
|
||||||
[int]$MonitorPort = 0,
|
|
||||||
[switch]$NoSnapshot,
|
|
||||||
[string]$Cpu = '',
|
|
||||||
[string[]]$ExtraArgs = @()
|
|
||||||
)
|
|
||||||
if (-not $script:Config) { throw 'call Initialize-QemuTest first' }
|
|
||||||
$cfg = $script:Config
|
|
||||||
|
|
||||||
$tag = "deshabtest-$Name-" + [guid]::NewGuid().ToString('N').Substring(0, 8)
|
|
||||||
$stamp = Get-Date -Format 'HHmmss'
|
|
||||||
$log = Join-Path $script:TestWorkDir "$Name-$stamp.log"
|
|
||||||
$bat = Join-Path $script:TestWorkDir "$Name-$stamp.bat"
|
|
||||||
|
|
||||||
# WHPX 不支持 nested VMX:默认关闭 guest VMX CPUID(与 ISO/run_qemu.bat 一致)。
|
|
||||||
# -Cpu 可显式覆盖(如 'max' 用于探测 nested VMX 可用性)。
|
|
||||||
$cpu = 'qemu64'
|
|
||||||
if ($Accel -eq 'whpx') { $cpu = 'qemu64,-vmx' }
|
|
||||||
if ($Cpu -ne '') { $cpu = $Cpu }
|
|
||||||
|
|
||||||
$qargs = @(
|
|
||||||
'-accel', $Accel,
|
|
||||||
'-machine', 'q35',
|
|
||||||
'-m', '2G',
|
|
||||||
'-cpu', $cpu,
|
|
||||||
'-serial', 'stdio',
|
|
||||||
'-display', 'none',
|
|
||||||
'-no-reboot',
|
|
||||||
'-name', $tag,
|
|
||||||
'-drive', ('if=pflash,format=raw,readonly=on,file="{0}"' -f $cfg.Ovmf),
|
|
||||||
# 已知坑4:主盘必须显式 virtio-blk-pci + bootindex=1。
|
|
||||||
# `-drive if=virtio` 自动插件时,OVMF 只在 virtio-blk 恰好落到
|
|
||||||
# PCI 0x3(无其他 PCI 设备)才会为其自动创建启动项;附加 NVMe/
|
|
||||||
# USB/e1000 任一设备后 virtio 挪到 0x4/0x5,OVMF 启动项枚举整个
|
|
||||||
# 失效,直接掉 EFI Internal Shell(ESP/BOOTX64.EFI 均正常也没用)。
|
|
||||||
# bootindex=1 经 fw_cfg bootorder 强制 OVMF 首选本盘,与设备组合无关。
|
|
||||||
'-drive', ('if=none,id=osdisk,format=raw,file="{0}"' -f $cfg.Img),
|
|
||||||
'-device', 'virtio-blk-pci,drive=osdisk,bootindex=1'
|
|
||||||
)
|
|
||||||
|
|
||||||
# -snapshot 默认开:镜像只读 + 写入落临时 overlay,避免镜像写锁与并发代理冲突。
|
|
||||||
if (-not $NoSnapshot) { $qargs += '-snapshot' }
|
|
||||||
|
|
||||||
$port = 0
|
|
||||||
if (-not $NoMonitor) {
|
|
||||||
$port = Get-FreeMonitorPort -Preferred $MonitorPort
|
|
||||||
$qargs += @('-monitor', "tcp:127.0.0.1:$port,server,nowait")
|
|
||||||
}
|
|
||||||
if (-not $NoSata) {
|
|
||||||
$qargs += @('-drive', ('id=sata0,format=raw,file="{0}",if=none' -f $cfg.SataImg),
|
|
||||||
'-device', 'ide-hd,drive=sata0,bus=ide.0')
|
|
||||||
}
|
|
||||||
if (-not $NoNvme -and $cfg.NvmeImg) {
|
|
||||||
$qargs += @('-drive', ('if=none,id=nvme0,file="{0}",format=raw' -f $cfg.NvmeImg),
|
|
||||||
'-device', 'nvme,drive=nvme0,serial=deadbeef')
|
|
||||||
}
|
|
||||||
if (-not $NoUsb -and $cfg.UsbImg) {
|
|
||||||
$qargs += @('-device', 'qemu-xhci,id=xhci0',
|
|
||||||
'-drive', ('id=usb0,if=none,file="{0}",format=raw' -f $cfg.UsbImg),
|
|
||||||
'-device', 'usb-storage,bus=xhci0.0,drive=usb0')
|
|
||||||
}
|
|
||||||
if (-not $NoNet) {
|
|
||||||
$qargs += @('-netdev', 'user,id=net0')
|
|
||||||
if ($NetModel -eq 'e1000') {
|
|
||||||
$qargs += @('-device', 'e1000,netdev=net0,mac=52:54:00:12:34:56')
|
|
||||||
} 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')
|
|
||||||
|
|
||||||
# 已知坑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`nset `"TEMP=$qemuTmp`"`r`nset `"TMP=$qemuTmp`"`r`n$cmdline > `"$log`" 2>&1`r`n"
|
|
||||||
[System.IO.File]::WriteAllText($bat, $batContent)
|
|
||||||
|
|
||||||
Stop-ResidualQemu
|
|
||||||
Start-Process -FilePath $bat -WindowStyle Hidden
|
|
||||||
|
|
||||||
$session = [pscustomobject]@{
|
|
||||||
Name = $Name
|
|
||||||
Tag = $tag
|
|
||||||
BatPath = $bat
|
|
||||||
LogPath = $log
|
|
||||||
MonitorPort = $port
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
function Test-QemuSessionExited {
|
|
||||||
param([Parameter(Mandatory)]$Session)
|
|
||||||
$procs = Get-CimInstance Win32_Process -Filter "Name='qemu-system-x86_64.exe'" -ErrorAction SilentlyContinue
|
|
||||||
foreach ($p in $procs) {
|
|
||||||
if ($p.CommandLine -and $p.CommandLine.Contains($Session.Tag)) { return $false }
|
|
||||||
}
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
|
|
||||||
function Stop-QemuSession {
|
|
||||||
param([Parameter(Mandatory)]$Session)
|
|
||||||
$procs = Get-CimInstance Win32_Process -Filter "Name='qemu-system-x86_64.exe'" -ErrorAction SilentlyContinue
|
|
||||||
foreach ($p in $procs) {
|
|
||||||
if ($p.CommandLine -and $p.CommandLine.Contains($Session.Tag)) {
|
|
||||||
Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Start-Sleep -Milliseconds 500
|
|
||||||
}
|
|
||||||
|
|
||||||
# ================================================================
|
|
||||||
# 串口日志
|
|
||||||
# ================================================================
|
|
||||||
|
|
||||||
function Read-QemuLogText {
|
|
||||||
param([Parameter(Mandatory)][string]$Path)
|
|
||||||
if (-not (Test-Path $Path)) { return '' }
|
|
||||||
$fs = $null
|
|
||||||
try {
|
|
||||||
$fs = [System.IO.File]::Open($Path, 'Open', 'Read', 'ReadWrite')
|
|
||||||
$sr = New-Object System.IO.StreamReader($fs, [System.Text.Encoding]::UTF8)
|
|
||||||
$text = $sr.ReadToEnd()
|
|
||||||
$sr.Close()
|
|
||||||
return $text
|
|
||||||
} catch {
|
|
||||||
return ''
|
|
||||||
} finally {
|
|
||||||
if ($fs) { $fs.Close() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 轮询串口日志直到任一 pattern 出现 / QEMU 退出 / 超时。返回匹配到的 pattern 或 $null。
|
|
||||||
function Wait-QemuLog {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]$Session,
|
|
||||||
[Parameter(Mandatory)][string[]]$Patterns,
|
|
||||||
[int]$TimeoutSeconds = 90,
|
|
||||||
[int]$PollMs = 1000
|
|
||||||
)
|
|
||||||
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
||||||
while ((Get-Date) -lt $deadline) {
|
|
||||||
$text = Read-QemuLogText $Session.LogPath
|
|
||||||
foreach ($p in $Patterns) {
|
|
||||||
if ($text.Contains($p)) { return $p }
|
|
||||||
}
|
|
||||||
if (Test-QemuSessionExited $Session) {
|
|
||||||
Write-Host '[qemu] session exited unexpectedly'
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
Start-Sleep -Milliseconds $PollMs
|
|
||||||
}
|
|
||||||
Write-Host "[qemu] wait timeout after ${TimeoutSeconds}s (patterns: $($Patterns -join ' | '))"
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
# ================================================================
|
|
||||||
# QEMU monitor(TCP)
|
|
||||||
# ================================================================
|
|
||||||
|
|
||||||
function Invoke-QemuMonitor {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]$Session,
|
|
||||||
[Parameter(Mandatory)][string]$Command,
|
|
||||||
[int]$SettleMs = 250
|
|
||||||
)
|
|
||||||
if (-not $Session.MonitorPort) { throw 'session started with -NoMonitor' }
|
|
||||||
$client = New-Object System.Net.Sockets.TcpClient
|
|
||||||
$client.ReceiveTimeout = 3000
|
|
||||||
$client.SendTimeout = 3000
|
|
||||||
$client.Connect('127.0.0.1', [int]$Session.MonitorPort)
|
|
||||||
try {
|
|
||||||
$stream = $client.GetStream()
|
|
||||||
$buf = New-Object byte[] 8192
|
|
||||||
Start-Sleep -Milliseconds 200
|
|
||||||
while ($stream.DataAvailable) { [void]$stream.Read($buf, 0, $buf.Length) } # banner
|
|
||||||
$writer = New-Object System.IO.StreamWriter($stream)
|
|
||||||
$writer.AutoFlush = $true
|
|
||||||
$writer.WriteLine($Command)
|
|
||||||
Start-Sleep -Milliseconds $SettleMs
|
|
||||||
$resp = ''
|
|
||||||
while ($stream.DataAvailable) {
|
|
||||||
$n = $stream.Read($buf, 0, $buf.Length)
|
|
||||||
$resp += [System.Text.Encoding]::ASCII.GetString($buf, 0, $n)
|
|
||||||
}
|
|
||||||
return $resp
|
|
||||||
} finally {
|
|
||||||
$client.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ConvertTo-QemuKeyName {
|
|
||||||
param([Parameter(Mandatory)][char]$C)
|
|
||||||
$i = [int]$C
|
|
||||||
if ($i -ge 97 -and $i -le 122) { return [string]$C } # a-z
|
|
||||||
if ($i -ge 65 -and $i -le 90) { return 'shift-' + ([string]$C).ToLower() } # A-Z
|
|
||||||
if ($i -ge 48 -and $i -le 57) { return [string]$C } # 0-9
|
|
||||||
switch ($C) {
|
|
||||||
' ' { return 'spc' }
|
|
||||||
'.' { return 'dot' }
|
|
||||||
',' { return 'comma' }
|
|
||||||
'/' { return 'slash' }
|
|
||||||
'\' { return 'backslash' }
|
|
||||||
'-' { return 'minus' }
|
|
||||||
'=' { return 'equal' }
|
|
||||||
';' { return 'semicolon' }
|
|
||||||
':' { return 'shift-semicolon' }
|
|
||||||
'>' { return 'shift-dot' }
|
|
||||||
'<' { return 'shift-comma' }
|
|
||||||
'_' { return 'shift-minus' }
|
|
||||||
'!' { return 'shift-1' }
|
|
||||||
'"' { return 'shift-apostrophe' }
|
|
||||||
'''' { return 'apostrophe' }
|
|
||||||
'(' { return 'shift-9' }
|
|
||||||
')' { return 'shift-0' }
|
|
||||||
'?' { return 'shift-slash' }
|
|
||||||
'*' { return 'shift-8' }
|
|
||||||
'+' { return 'shift-equal' }
|
|
||||||
'@' { return 'shift-2' }
|
|
||||||
'#' { return 'shift-3' }
|
|
||||||
'$' { return 'shift-4' }
|
|
||||||
'%' { return 'shift-5' }
|
|
||||||
'^' { return 'shift-6' }
|
|
||||||
'&' { return 'shift-7' }
|
|
||||||
'|' { return 'shift-backslash' }
|
|
||||||
'~' { return 'shift-grave_accent' }
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
# 通过 monitor 发送一串按键(单 TCP 连接内完成)
|
|
||||||
function Send-QemuKeys {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]$Session,
|
|
||||||
[Parameter(Mandatory)][string[]]$Keys,
|
|
||||||
[int]$DelayMs = 120
|
|
||||||
)
|
|
||||||
if (-not $Session.MonitorPort) { throw 'session started with -NoMonitor' }
|
|
||||||
$client = New-Object System.Net.Sockets.TcpClient
|
|
||||||
$client.Connect('127.0.0.1', [int]$Session.MonitorPort)
|
|
||||||
try {
|
|
||||||
$stream = $client.GetStream()
|
|
||||||
$buf = New-Object byte[] 4096
|
|
||||||
Start-Sleep -Milliseconds 200
|
|
||||||
while ($stream.DataAvailable) { [void]$stream.Read($buf, 0, $buf.Length) }
|
|
||||||
$writer = New-Object System.IO.StreamWriter($stream)
|
|
||||||
$writer.AutoFlush = $true
|
|
||||||
foreach ($k in $Keys) {
|
|
||||||
$writer.WriteLine("sendkey $k")
|
|
||||||
Start-Sleep -Milliseconds $DelayMs
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
$client.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 输入一段文本(自动映射大小写/符号),可选追加回车
|
|
||||||
function Send-QemuText {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]$Session,
|
|
||||||
[Parameter(Mandatory)][string]$Text,
|
|
||||||
[switch]$Enter,
|
|
||||||
[int]$DelayMs = 90
|
|
||||||
)
|
|
||||||
$keys = New-Object System.Collections.ArrayList
|
|
||||||
foreach ($c in $Text.ToCharArray()) {
|
|
||||||
$k = ConvertTo-QemuKeyName $c
|
|
||||||
if ($k) { [void]$keys.Add($k) }
|
|
||||||
}
|
|
||||||
if ($Enter) { [void]$keys.Add('ret') }
|
|
||||||
Send-QemuKeys -Session $Session -Keys ($keys.ToArray()) -DelayMs $DelayMs
|
|
||||||
}
|
|
||||||
|
|
||||||
function Send-QemuMouseMove {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]$Session,
|
|
||||||
[int]$Dx = 0,
|
|
||||||
[int]$Dy = 0,
|
|
||||||
[string]$Button = ''
|
|
||||||
)
|
|
||||||
$cmd = "mouse_move $Dx $Dy"
|
|
||||||
[void](Invoke-QemuMonitor -Session $Session -Command $cmd)
|
|
||||||
if ($Button) {
|
|
||||||
[void](Invoke-QemuMonitor -Session $Session -Command "mouse_button $Button")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-QemuScreenshot {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]$Session,
|
|
||||||
[Parameter(Mandatory)][string]$OutPath
|
|
||||||
)
|
|
||||||
$resp = Invoke-QemuMonitor -Session $Session -Command "screendump `"$OutPath`"" -SettleMs 600
|
|
||||||
return (Test-Path $OutPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
# ================================================================
|
|
||||||
# 场景准备(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: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(格式 "<first>`n<dev_mode>",LF)并重建私有 SATA 镜像;原内容自动备份。
|
|
||||||
# 只写 .build_tmp\tests\SYSTEM 镜像树,共享 SYSTEM 工作树保持不变(并发代理可能正在翻转它)。
|
|
||||||
# 已知坑5:DSK 的 dev_mode 是「FUCK dev_mode OR firstInit 第二行」单向合并,
|
|
||||||
# firstInit 只能置 1 不能清 0。dev 镜像 FUCK dev_mode=1 时 shell.elf 自动
|
|
||||||
# 测试后进入交互主循环阻塞启动流,cmd.elf 永远调度不到。因此 -DevMode 0
|
|
||||||
# 必须同步把镜像树 FUCK [boot] dev_mode 也改 0(仅测试镜像树,不动共享树)。
|
|
||||||
function Set-DeshabFirstInit {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)][int]$First,
|
|
||||||
[Parameter(Mandatory)][int]$DevMode
|
|
||||||
)
|
|
||||||
$path = Join-Path $script:MirrorSystem 'system\user\use\firstInit.txt'
|
|
||||||
if ($null -eq $script:FirstInitBackup) {
|
|
||||||
$script:FirstInitBackup = [System.IO.File]::ReadAllBytes($path)
|
|
||||||
}
|
|
||||||
[System.IO.File]::WriteAllText($path, "$First`n$DevMode")
|
|
||||||
# 同步 FUCK [boot] dev_mode(单向合并导致 firstInit 无法清 dev_mode,见上)
|
|
||||||
$fuckPath = Join-Path $script:MirrorSystem 'system\deshab64\FUCK'
|
|
||||||
if ($null -eq $script:FuckBackup) {
|
|
||||||
$script:FuckBackup = [System.IO.File]::ReadAllBytes($fuckPath)
|
|
||||||
}
|
|
||||||
$fuckTxt = [System.IO.File]::ReadAllText($fuckPath)
|
|
||||||
$fuckTxt = $fuckTxt -replace '(?m)^dev_mode=\d', "dev_mode=$DevMode"
|
|
||||||
[System.IO.File]::WriteAllText($fuckPath, $fuckTxt)
|
|
||||||
Invoke-MkFat32
|
|
||||||
}
|
|
||||||
|
|
||||||
function Restore-DeshabFirstInit {
|
|
||||||
$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
|
|
||||||
$restored = $true
|
|
||||||
}
|
|
||||||
$fuckPath = Join-Path $script:MirrorSystem 'system\deshab64\FUCK'
|
|
||||||
if ($null -ne $script:FuckBackup) {
|
|
||||||
[System.IO.File]::WriteAllBytes($fuckPath, $script:FuckBackup)
|
|
||||||
$script:FuckBackup = $null
|
|
||||||
$restored = $true
|
|
||||||
}
|
|
||||||
if ($restored) { Invoke-MkFat32 }
|
|
||||||
}
|
|
||||||
|
|
||||||
# 替换镜像树 AUTOEXEC.BAT 内容(cmd.elf 启动时逐行自动执行)并重建私有 SATA 镜像;原内容自动备份
|
|
||||||
function Set-DeshabAutoexec {
|
|
||||||
param([Parameter(Mandatory)][string[]]$Lines)
|
|
||||||
$path = Join-Path $script:MirrorSystem 'bin\AUTOEXEC.BAT'
|
|
||||||
if ($null -eq $script:AutoexecBackup) {
|
|
||||||
$script:AutoexecBackup = [System.IO.File]::ReadAllBytes($path)
|
|
||||||
}
|
|
||||||
[System.IO.File]::WriteAllText($path, (($Lines -join "`r`n") + "`r`n"))
|
|
||||||
Invoke-MkFat32
|
|
||||||
}
|
|
||||||
|
|
||||||
function Restore-DeshabAutoexec {
|
|
||||||
$path = Join-Path $script:MirrorSystem 'bin\AUTOEXEC.BAT'
|
|
||||||
if ($null -ne $script:AutoexecBackup) {
|
|
||||||
[System.IO.File]::WriteAllBytes($path, $script:AutoexecBackup)
|
|
||||||
$script:AutoexecBackup = $null
|
|
||||||
Invoke-MkFat32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Restore-DeshabScenario {
|
|
||||||
Restore-DeshabFirstInit
|
|
||||||
Restore-DeshabAutoexec
|
|
||||||
}
|
|
||||||
|
|
||||||
# ================================================================
|
|
||||||
# 结果与断言
|
|
||||||
# ================================================================
|
|
||||||
|
|
||||||
function New-TestCaseResult {
|
|
||||||
param([Parameter(Mandatory)][string]$Case)
|
|
||||||
return [pscustomobject]@{
|
|
||||||
Case = $Case
|
|
||||||
Status = 'PASS'
|
|
||||||
StartTime = Get-Date
|
|
||||||
DurationSec = 0.0
|
|
||||||
LogPath = ''
|
|
||||||
Checks = New-Object System.Collections.ArrayList
|
|
||||||
Notes = New-Object System.Collections.ArrayList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Add-TestCheck {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]$Result,
|
|
||||||
[Parameter(Mandatory)][string]$Name,
|
|
||||||
[Parameter(Mandatory)][string]$Status, # PASS / FAIL / MANUAL / SKIP
|
|
||||||
[string]$Detail = ''
|
|
||||||
)
|
|
||||||
[void]$Result.Checks.Add([pscustomobject]@{ Name = $Name; Status = $Status; Detail = $Detail })
|
|
||||||
if ($Status -eq 'FAIL') { $Result.Status = 'FAIL' }
|
|
||||||
}
|
|
||||||
|
|
||||||
function Add-TestNote {
|
|
||||||
param([Parameter(Mandatory)]$Result, [Parameter(Mandatory)][string]$Note)
|
|
||||||
[void]$Result.Notes.Add($Note)
|
|
||||||
}
|
|
||||||
|
|
||||||
# 日志断言引擎:必须包含 / 禁止包含 / 按顺序包含 / 出现次数下限
|
|
||||||
function Assert-QemuLog {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]$Result,
|
|
||||||
[Parameter(Mandatory)][string]$LogText,
|
|
||||||
[string[]]$MustContain = @(),
|
|
||||||
[string[]]$MustNotContain = @(),
|
|
||||||
[string[]]$Ordered = @(),
|
|
||||||
[string]$CountPattern = '',
|
|
||||||
[int]$MinCount = 0
|
|
||||||
)
|
|
||||||
foreach ($p in $MustContain) {
|
|
||||||
if ($LogText.Contains($p)) {
|
|
||||||
Add-TestCheck $Result "锚点: $p" 'PASS'
|
|
||||||
} else {
|
|
||||||
Add-TestCheck $Result "锚点: $p" 'FAIL' 'pattern not found in serial log'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach ($p in $MustNotContain) {
|
|
||||||
if ($LogText.Contains($p)) {
|
|
||||||
Add-TestCheck $Result "禁止: $p" 'FAIL' 'forbidden pattern found in serial log'
|
|
||||||
} else {
|
|
||||||
Add-TestCheck $Result "禁止: $p" 'PASS'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($Ordered.Count -gt 0) {
|
|
||||||
$pos = 0
|
|
||||||
$failAt = ''
|
|
||||||
foreach ($p in $Ordered) {
|
|
||||||
$idx = $LogText.IndexOf($p, $pos)
|
|
||||||
if ($idx -lt 0) { $failAt = $p; break }
|
|
||||||
$pos = $idx + $p.Length
|
|
||||||
}
|
|
||||||
if ($failAt -eq '') {
|
|
||||||
Add-TestCheck $Result ("顺序链({0} 锚点)" -f $Ordered.Count) 'PASS' ($Ordered -join ' -> ')
|
|
||||||
} else {
|
|
||||||
Add-TestCheck $Result ("顺序链({0} 锚点)" -f $Ordered.Count) 'FAIL' "missing/out-of-order: $failAt"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($CountPattern -ne '' -and $MinCount -gt 0) {
|
|
||||||
$count = 0
|
|
||||||
$pos = 0
|
|
||||||
while ($true) {
|
|
||||||
$idx = $LogText.IndexOf($CountPattern, $pos)
|
|
||||||
if ($idx -lt 0) { break }
|
|
||||||
$count++
|
|
||||||
$pos = $idx + $CountPattern.Length
|
|
||||||
}
|
|
||||||
if ($count -ge $MinCount) {
|
|
||||||
Add-TestCheck $Result "计数: '$CountPattern' x$count (>= $MinCount)" 'PASS'
|
|
||||||
} else {
|
|
||||||
Add-TestCheck $Result "计数: '$CountPattern' x$count (>= $MinCount)" 'FAIL' "only $count occurrences"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 输出用例结果;FAIL 时附带日志尾部上下文
|
|
||||||
function Write-TestCaseResult {
|
|
||||||
param([Parameter(Mandatory)]$Result)
|
|
||||||
$Result.DurationSec = [math]::Round(((Get-Date) - $Result.StartTime).TotalSeconds, 1)
|
|
||||||
Write-Host ''
|
|
||||||
Write-Host ("=== [{0}] {1} ({2}s) ===" -f $Result.Case, $Result.Status, $Result.DurationSec)
|
|
||||||
foreach ($c in $Result.Checks) {
|
|
||||||
$mark = '[PASS]'
|
|
||||||
if ($c.Status -eq 'FAIL') { $mark = '[FAIL] ' }
|
|
||||||
if ($c.Status -eq 'MANUAL') { $mark = '[MANUAL]' }
|
|
||||||
if ($c.Status -eq 'SKIP') { $mark = '[SKIP] ' }
|
|
||||||
Write-Host (" {0} {1}" -f $mark, $c.Name)
|
|
||||||
if ($c.Detail -and $c.Status -ne 'PASS') {
|
|
||||||
Write-Host (" -> {0}" -f $c.Detail)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach ($n in $Result.Notes) { Write-Host " [note] $n" }
|
|
||||||
if ($Result.Status -eq 'FAIL' -and $Result.LogPath -and (Test-Path $Result.LogPath)) {
|
|
||||||
Write-Host ' --- serial log tail (25 lines) ---'
|
|
||||||
Get-Content $Result.LogPath -Tail 25 | ForEach-Object { Write-Host " | $_" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# ================================================================
|
|
||||||
# FirstInit 设置向导自动填表(首启流程公共助手)
|
|
||||||
# 账户卡片(3 字段) → 偏好页(5xEnter) → 网络页(3xEnter) → 完成
|
|
||||||
# 每步等串口锚点再发键;字段验证失败自动重试 3 次。
|
|
||||||
# ================================================================
|
|
||||||
function Complete-DeshabFirstInitWizard {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]$Session,
|
|
||||||
[Parameter(Mandatory)]$Result,
|
|
||||||
[string]$Prefix = 'FirstInit'
|
|
||||||
)
|
|
||||||
|
|
||||||
$hit = Wait-QemuLog -Session $Session -Patterns @('[FI] fadein done') -TimeoutSeconds 150
|
|
||||||
if (-not $hit) { Add-TestCheck $Result "${Prefix}: 账户卡片就绪" 'FAIL' 'missing [FI] fadein done'; return $false }
|
|
||||||
|
|
||||||
$done = $false
|
|
||||||
for ($attempt = 1; $attempt -le 3 -and -not $done; $attempt++) {
|
|
||||||
Send-QemuText -Session $Session -Text 'testpc' -Enter
|
|
||||||
Start-Sleep -Milliseconds 400
|
|
||||||
Send-QemuText -Session $Session -Text 'tester' -Enter
|
|
||||||
Start-Sleep -Milliseconds 400
|
|
||||||
Send-QemuText -Session $Session -Text 'pass123' -Enter
|
|
||||||
$hit = Wait-QemuLog -Session $Session -Patterns @('[FI] all fields done', '[FI] validation failed') -TimeoutSeconds 30
|
|
||||||
if ($hit -eq '[FI] all fields done') { $done = $true }
|
|
||||||
else { Add-TestNote $Result "${Prefix}: 字段验证失败,重试第 $attempt 次" }
|
|
||||||
}
|
|
||||||
if (-not $done) { Add-TestCheck $Result "${Prefix}: 账户字段填写" 'FAIL' 'validation did not pass after 3 attempts'; return $false }
|
|
||||||
Add-TestCheck $Result "${Prefix}: 账户字段填写" 'PASS'
|
|
||||||
|
|
||||||
Start-Sleep -Milliseconds 600
|
|
||||||
Send-QemuKeys -Session $Session -Keys @('ret', 'ret', 'ret', 'ret', 'ret') -DelayMs 350
|
|
||||||
$hit = Wait-QemuLog -Session $Session -Patterns @('[FirstInit] entering network setup page') -TimeoutSeconds 40
|
|
||||||
if (-not $hit) { Add-TestCheck $Result "${Prefix}: 偏好页" 'FAIL' 'did not reach network setup page'; return $false }
|
|
||||||
Add-TestCheck $Result "${Prefix}: 偏好页(5xEnter)" 'PASS'
|
|
||||||
|
|
||||||
Start-Sleep -Milliseconds 400
|
|
||||||
Send-QemuKeys -Session $Session -Keys @('ret', 'ret', 'ret') -DelayMs 350
|
|
||||||
$hit = Wait-QemuLog -Session $Session -Patterns @('[FirstInit] user setup finished') -TimeoutSeconds 40
|
|
||||||
if (-not $hit) { Add-TestCheck $Result "${Prefix}: 网络页" 'FAIL' 'setup did not finish'; return $false }
|
|
||||||
Add-TestCheck $Result "${Prefix}: 网络页(3xEnter)" 'PASS'
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
|
|
||||||
# ================================================================
|
|
||||||
# 极简 HTTP 测试服务器(network-e2e 用)
|
|
||||||
# slirp 网关 10.0.2.2 → 主机 127.0.0.1;guest curl 经 slirp 访问主机服务。
|
|
||||||
# 后台 Job 接收连接,把每个请求首行写入 $LogPath(主机侧端到端证据),
|
|
||||||
# 回复固定 HTTP/1.0 200 + 标记体后关闭。
|
|
||||||
# ================================================================
|
|
||||||
function Start-TestHttpServer {
|
|
||||||
param(
|
|
||||||
[int]$Port = 8000,
|
|
||||||
[Parameter(Mandatory)][string]$LogPath,
|
|
||||||
[string]$Marker = 'DESHAB-NET-E2E-MARKER'
|
|
||||||
)
|
|
||||||
if (Test-Path $LogPath) { Remove-Item $LogPath -Force }
|
|
||||||
$job = Start-Job -ScriptBlock {
|
|
||||||
param($Port, $LogPath, $Marker)
|
|
||||||
$listener = $null
|
|
||||||
try {
|
|
||||||
$listener = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Loopback, $Port)
|
|
||||||
$listener.Start()
|
|
||||||
$body = "<html><body><h1>$Marker</h1></body></html>"
|
|
||||||
$deadline = (Get-Date).AddMinutes(30)
|
|
||||||
while ((Get-Date) -lt $deadline) {
|
|
||||||
if (-not $listener.Pending()) { Start-Sleep -Milliseconds 200; continue }
|
|
||||||
$client = $listener.AcceptTcpClient()
|
|
||||||
try {
|
|
||||||
$stream = $client.GetStream()
|
|
||||||
$stream.ReadTimeout = 5000
|
|
||||||
$buf = New-Object byte[] 4096
|
|
||||||
$req = ''
|
|
||||||
while ($stream.DataAvailable -or $req -eq '') {
|
|
||||||
$n = 0
|
|
||||||
try { $n = $stream.Read($buf, 0, $buf.Length) } catch { break }
|
|
||||||
if ($n -le 0) { break }
|
|
||||||
$req += [System.Text.Encoding]::ASCII.GetString($buf, 0, $n)
|
|
||||||
if ($req.Contains("`r`n`r`n")) { break }
|
|
||||||
}
|
|
||||||
$firstLine = ($req -split "`r`n")[0]
|
|
||||||
[System.IO.File]::AppendAllText($LogPath, "$(Get-Date -Format 'HH:mm:ss') $firstLine`r`n")
|
|
||||||
$resp = "HTTP/1.0 200 OK`r`nContent-Type: text/html`r`nContent-Length: $($body.Length)`r`nConnection: close`r`n`r`n$body"
|
|
||||||
$bytes = [System.Text.Encoding]::ASCII.GetBytes($resp)
|
|
||||||
$stream.Write($bytes, 0, $bytes.Length)
|
|
||||||
$stream.Flush()
|
|
||||||
} finally {
|
|
||||||
$client.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if ($listener) { $listener.Stop() }
|
|
||||||
}
|
|
||||||
} -ArgumentList $Port, $LogPath, $Marker
|
|
||||||
Start-Sleep -Milliseconds 800
|
|
||||||
if ($job.State -ne 'Running') {
|
|
||||||
$err = Receive-Job $job 2>&1 | Out-String
|
|
||||||
throw "http test server failed to start on port ${Port}: $err"
|
|
||||||
}
|
|
||||||
Write-Host "[http] test server listening on 127.0.0.1:$Port (log=$LogPath)"
|
|
||||||
return $job
|
|
||||||
}
|
|
||||||
|
|
||||||
function Stop-TestHttpServer {
|
|
||||||
param($Job)
|
|
||||||
if ($Job) {
|
|
||||||
Stop-Job $Job -ErrorAction SilentlyContinue
|
|
||||||
Remove-Job $Job -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
#requires -Version 5.1
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
linux-compat — UTSM+Linux 双内核兼容层端到端(docs/RE/test-cases/linux-compat.md)
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
动态检测 VMM/Linux guest 可用性,自动分流两条验证路径:
|
|
||||||
|
|
||||||
路径 A(guest VMX 可用,真机/KVM):
|
|
||||||
[UTSM] VMM init ok → VMM self-test PASS → bzImage 解析 → IPC shm →
|
|
||||||
vmlaunch → Linux version → initramfs init → daemon park →
|
|
||||||
[LNXC] service ready → DSK → shell dev_tests → dsl 命令实测
|
|
||||||
|
|
||||||
路径 B(QEMU WHPX/TCG 无 nested VMX —— 本环境预期路径):
|
|
||||||
[UTSM] VMM unavailable → Linux loader 跳过 → DSK 正常启动不崩溃 →
|
|
||||||
shell 收到 linux_compat 服务表 → dsl 报告运行态不可用 → desktop。
|
|
||||||
A5.1~A5.12 标记 SKIP(环境限制,非产品缺陷)。
|
|
||||||
|
|
||||||
依据 ISO/run_qemu.bat 说明:WHPX 下 VMXON 会触发致命 hypervisor exit,
|
|
||||||
因此框架默认 -cpu qemu64,-vmx 关闭 guest VMX CPUID,UTSM 走优雅降级。
|
|
||||||
若未来 WHPX/KVM 支持 nested VMX,可用 -Cpu 'max' 复测路径 A。
|
|
||||||
|
|
||||||
退出码:全部通过 0,任一 FAIL 1。
|
|
||||||
#>
|
|
||||||
param(
|
|
||||||
[switch]$Build,
|
|
||||||
[string]$Cpu = ''
|
|
||||||
)
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
. (Join-Path $PSScriptRoot 'lib\QemuTest.ps1')
|
|
||||||
|
|
||||||
Initialize-QemuTest -Build:$Build
|
|
||||||
$result = New-TestCaseResult 'linux-compat'
|
|
||||||
|
|
||||||
Set-DeshabFirstInit -First 1 -DevMode 1
|
|
||||||
Set-DeshabAutoexec -Lines @('ver', 'exit')
|
|
||||||
|
|
||||||
$s = Start-QemuSession -Name 'linux-compat' -Cpu $Cpu -MonitorPort 45503
|
|
||||||
try {
|
|
||||||
# ---- 分流点:VMM 自检通过 还是 VMM 不可用 ----
|
|
||||||
$fork = Wait-QemuLog -Session $s -Patterns @(
|
|
||||||
'[UTSM] VMM self-test PASS',
|
|
||||||
'[UTSM] VMM unavailable'
|
|
||||||
) -TimeoutSeconds 180
|
|
||||||
|
|
||||||
if ($fork -eq '[UTSM] VMM self-test PASS') {
|
|
||||||
# ================= 路径 A:VMX 可用,完整双内核流程 =================
|
|
||||||
Write-Host "`n--- 路径 A: guest VMX 可用,验证完整 Linux 兼容层 ---"
|
|
||||||
$hit = Wait-QemuLog -Session $s -Patterns @('=== 自动测试完成 ===', '[PANIC]') -TimeoutSeconds 300
|
|
||||||
$log = Read-QemuLogText $s.LogPath
|
|
||||||
$result.LogPath = $s.LogPath
|
|
||||||
|
|
||||||
# A5.1 ~ A5.7 启动链
|
|
||||||
Assert-QemuLog $result $log -Ordered @(
|
|
||||||
'[UTSM] VMM init ok',
|
|
||||||
'[UTSM] VMM self-test PASS',
|
|
||||||
'[LINUX] found bzImage module:',
|
|
||||||
'[UTSM] Linux loader init ok',
|
|
||||||
'[UTSM] IPC shm init ok',
|
|
||||||
'[VMM] launching guest',
|
|
||||||
'Linux version',
|
|
||||||
'[utsm-linux] init started',
|
|
||||||
'[LINUX] guest parked (daemon ready)',
|
|
||||||
'[LNXC] service ready',
|
|
||||||
'[DSK] boot',
|
|
||||||
'[shell] linux_compat service ok'
|
|
||||||
) -MustNotContain @('[PANIC]')
|
|
||||||
if (-not $hit -or $hit -eq '[PANIC]') {
|
|
||||||
Add-TestCheck $result 'A5.7: DSK/shell 接管' 'FAIL' 'dev_tests did not complete'
|
|
||||||
} else {
|
|
||||||
Add-TestCheck $result 'A5.7: DSK/shell 接管' 'PASS'
|
|
||||||
}
|
|
||||||
|
|
||||||
# A5.8 ~ A5.12 shell dsl 交互(dev_mode 串口镜像开启)
|
|
||||||
if ($hit -eq '=== 自动测试完成 ===') {
|
|
||||||
Start-Sleep -Seconds 1
|
|
||||||
Send-QemuText -Session $s -Text 'dsl' -Enter
|
|
||||||
$h1 = Wait-QemuLog -Session $s -Patterns @('Linux 兼容层已就绪') -TimeoutSeconds 60
|
|
||||||
if ($h1) { Add-TestCheck $result 'A5.8: dsl 状态查询' 'PASS' }
|
|
||||||
else { Add-TestCheck $result 'A5.8: dsl 状态查询' 'FAIL' 'missing 兼容层已就绪' }
|
|
||||||
|
|
||||||
Send-QemuText -Session $s -Text 'dsl ls' -Enter
|
|
||||||
$h2 = Wait-QemuLog -Session $s -Patterns @('bin', 'etc', 'deshab#') -TimeoutSeconds 60
|
|
||||||
if ($h2) { Add-TestCheck $result 'A5.9: dsl ls 目录列表' 'PASS' }
|
|
||||||
else { Add-TestCheck $result 'A5.9: dsl ls 目录列表' 'FAIL' 'no listing output' }
|
|
||||||
|
|
||||||
Send-QemuText -Session $s -Text 'dsl uname -a' -Enter
|
|
||||||
$h3 = Wait-QemuLog -Session $s -Patterns @('Linux') -TimeoutSeconds 60
|
|
||||||
if ($h3) { Add-TestCheck $result 'A5.10: dsl uname -a' 'PASS' }
|
|
||||||
else { Add-TestCheck $result 'A5.10: dsl uname -a' 'FAIL' 'no uname output' }
|
|
||||||
|
|
||||||
Send-QemuText -Session $s -Text 'dsl nosuchprog' -Enter
|
|
||||||
$h4 = Wait-QemuLog -Session $s -Patterns @('deshab# ') -TimeoutSeconds 60
|
|
||||||
Start-Sleep -Seconds 1
|
|
||||||
$log2 = Read-QemuLogText $s.LogPath
|
|
||||||
if ($h4 -and -not $log2.Contains('[PANIC]')) {
|
|
||||||
Add-TestCheck $result 'A5.12: dsl 错误路径不崩溃' 'PASS'
|
|
||||||
} else {
|
|
||||||
Add-TestCheck $result 'A5.12: dsl 错误路径不崩溃' 'FAIL' 'shell did not recover'
|
|
||||||
}
|
|
||||||
|
|
||||||
Send-QemuKeys -Session $s -Keys @('esc')
|
|
||||||
$h5 = Wait-QemuLog -Session $s -Patterns @('desktop ready') -TimeoutSeconds 150
|
|
||||||
if ($h5) { Add-TestCheck $result 'A5.x: Esc 后到 desktop' 'PASS' }
|
|
||||||
else { Add-TestCheck $result 'A5.x: Esc 后到 desktop' 'FAIL' 'no desktop ready' }
|
|
||||||
}
|
|
||||||
Add-TestNote $result '路径 A 执行:guest VMX 可用(真机/KVM 环境)'
|
|
||||||
}
|
|
||||||
elseif ($fork -eq '[UTSM] VMM unavailable') {
|
|
||||||
# ================= 路径 B:无 nested VMX,优雅降级 =================
|
|
||||||
Write-Host "`n--- 路径 B: guest 无 VMX(QEMU 预期),验证优雅降级 ---"
|
|
||||||
$hit = Wait-QemuLog -Session $s -Patterns @('=== 自动测试完成 ===', '[PANIC]') -TimeoutSeconds 240
|
|
||||||
$log = Read-QemuLogText $s.LogPath
|
|
||||||
$result.LogPath = $s.LogPath
|
|
||||||
|
|
||||||
# 降级锚点 + 系统继续正常启动
|
|
||||||
Assert-QemuLog $result $log -MustContain @('[UTSM] VMM unavailable')
|
|
||||||
Assert-QemuLog $result $log -Ordered @(
|
|
||||||
'[UTSM] SELFTEST PASS',
|
|
||||||
'[UTSM] VMM unavailable',
|
|
||||||
'[DSK] boot',
|
|
||||||
'[DSK] dev_mode=1 (developer auto-test)',
|
|
||||||
'[shell] boot',
|
|
||||||
'[shell] linux_compat service ok'
|
|
||||||
) -MustNotContain @('[PANIC]')
|
|
||||||
if ($hit -eq '=== 自动测试完成 ===') {
|
|
||||||
Add-TestCheck $result '降级路径: dev_tests 完成(Linux 缺失不阻塞启动)' 'PASS'
|
|
||||||
} else {
|
|
||||||
Add-TestCheck $result '降级路径: dev_tests 完成(Linux 缺失不阻塞启动)' 'FAIL' "hit=$hit"
|
|
||||||
}
|
|
||||||
|
|
||||||
# dsl 运行态不可用报告(服务表已传递,is_available()=0)
|
|
||||||
if ($hit -eq '=== 自动测试完成 ===') {
|
|
||||||
Start-Sleep -Seconds 1
|
|
||||||
Send-QemuText -Session $s -Text 'dsl' -Enter
|
|
||||||
$h = Wait-QemuLog -Session $s -Patterns @('linux_compat: not initialized') -TimeoutSeconds 40
|
|
||||||
if ($h) { Add-TestCheck $result '降级路径: dsl 报告运行态不可用' 'PASS' }
|
|
||||||
else { Add-TestCheck $result '降级路径: dsl 报告运行态不可用' 'FAIL' 'missing linux_compat: not initialized' }
|
|
||||||
|
|
||||||
Send-QemuKeys -Session $s -Keys @('esc')
|
|
||||||
$h2 = Wait-QemuLog -Session $s -Patterns @('desktop ready') -TimeoutSeconds 150
|
|
||||||
if ($h2) { Add-TestCheck $result '降级路径: Esc 后经 cmd 到 desktop' 'PASS' }
|
|
||||||
else { Add-TestCheck $result '降级路径: Esc 后经 cmd 到 desktop' 'FAIL' 'no desktop ready' }
|
|
||||||
}
|
|
||||||
|
|
||||||
# A5.1~A5.12 环境性跳过
|
|
||||||
foreach ($t in @('A5.1 VMM self-test', 'A5.2 bzImage 加载', 'A5.3 EPT+vmlaunch',
|
|
||||||
'A5.4 Linux 内核启动', 'A5.5 init 脚本', 'A5.6 daemon park',
|
|
||||||
'A5.8 dsl 状态就绪', 'A5.9 dsl ls', 'A5.10 dsl uname',
|
|
||||||
'A5.11 dsl cat hostname', 'A5.12 dsl 错误路径')) {
|
|
||||||
Add-TestCheck $result $t 'SKIP' 'QEMU WHPX/TCG 无 nested VMX(见 run_qemu.bat 注释),需真机或 KVM 复测'
|
|
||||||
}
|
|
||||||
Add-TestCheck $result 'A5.7: Linux 缺失时 DSK 正常接管' 'PASS'
|
|
||||||
Add-TestNote $result '路径 B 执行:QEMU 无 nested VMX,Linux guest 不可用属预期环境限制'
|
|
||||||
Add-TestNote $result '如需完整验证:真机(VT-x)或 Linux KVM (-enable-kvm -cpu host) 下以 -Cpu max 重跑'
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$result.LogPath = $s.LogPath
|
|
||||||
Add-TestCheck $result 'VMM 分流探测' 'FAIL' 'neither VMM self-test PASS nor VMM unavailable within 180s'
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
Stop-QemuSession $s
|
|
||||||
Restore-DeshabScenario
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-TestCaseResult $result
|
|
||||||
if ($result.Status -eq 'FAIL') { exit 1 } else { exit 0 }
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
#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 }
|
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
#requires -Version 5.1
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
pe-compat — PE/EXE 兼容层测试(docs/RE/test-cases/pe-compat.md)
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
正常启动到 cmd.elf,用 AUTOEXEC.BAT 自动执行 PE 命令序列。
|
|
||||||
因两个已登记系统 bug(详见 docs/RE/Bug记录.txt),拆三个独立会话:
|
|
||||||
|
|
||||||
会话 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, 始终上串口)
|
|
||||||
[PE] PE32+ (64-bit) / [PE] running PE32+ natively (pe_service)
|
|
||||||
[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,
|
|
||||||
[string]$Accel = 'whpx'
|
|
||||||
)
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
. (Join-Path $PSScriptRoot 'lib\QemuTest.ps1')
|
|
||||||
|
|
||||||
Initialize-QemuTest -Build:$Build
|
|
||||||
$result = New-TestCaseResult 'pe-compat'
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
|
||||||
# 单会话执行一轮 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
|
|
||||||
|
|
||||||
$timeout = 300
|
|
||||||
if ($AccelMode -eq 'tcg') { $timeout = 600 } # TCG 全模拟显著更慢
|
|
||||||
# 注:2GB ESP + Linux bzImage/initrd/512MB rootfs boot module 使 Limine
|
|
||||||
# 加载 + UTSM/DSK 启动到 cmd.elf 需要 ~150s(WHPX),AUTOEXEC/PE 执行
|
|
||||||
# 还需额外时间,故 whpx 300s / tcg 600s。
|
|
||||||
$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 {
|
|
||||||
# ================================================================
|
|
||||||
# 会话 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
|
|
||||||
|
|
||||||
Assert-QemuLog $result $logA -Ordered @(
|
|
||||||
'[cmd] running AUTOEXEC.BAT',
|
|
||||||
'[cmd] PE run begin',
|
|
||||||
'[cmd] cmdline: HELLO64.EXE',
|
|
||||||
'[PE] PE32+ (64-bit)',
|
|
||||||
'[PE] running PE32+ natively',
|
|
||||||
'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 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'
|
|
||||||
)
|
|
||||||
|
|
||||||
# ================================================================
|
|
||||||
# 会话 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
|
|
||||||
|
|
||||||
# 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]')
|
|
||||||
|
|
||||||
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)"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ================================================================
|
|
||||||
# 会话 E:真实 DLL 链(pe_dll_manager → SATA /lib 真实 DLL)
|
|
||||||
# RDLL64.EXE 静态导入 msvcrt!strcat/strstr/atoi(shim 表外函数)
|
|
||||||
# → resolve_imports 落 pe_dll_resolve → fat32_read_path("lib/msvcrt.dll")
|
|
||||||
# → pe_load_image 真实 DLL(DllMain ATTACH)→ 导出表解析
|
|
||||||
# (Win10 msvcrt 转发 ucrtbase → 验证转发导出跟随)。
|
|
||||||
# 另 LoadLibraryA("ucrtbase.dll") + GetProcAddress(strlen/memcpy)
|
|
||||||
# → 按真实镜像基址解析并直接调用真实代码。
|
|
||||||
# ================================================================
|
|
||||||
$hitE = Invoke-PeSessionWithFallback -Name 'realdll' -Autoexec @('pe RDLL64.EXE') -Anchors @('[REALDLL] end', '[PANIC]')
|
|
||||||
$logE = $script:RunLog
|
|
||||||
|
|
||||||
Assert-QemuLog $result $logE -Ordered @(
|
|
||||||
'[cmd] cmdline: RDLL64.EXE',
|
|
||||||
'[REALDLL] begin',
|
|
||||||
'[REALDLL] A1 strcat: OK',
|
|
||||||
'[REALDLL] A2 strstr: OK',
|
|
||||||
'[REALDLL] A3 atoi: OK',
|
|
||||||
'[REALDLL] B1 LoadLibraryA(ucrtbase.dll): OK',
|
|
||||||
'[REALDLL] B2 GetProcAddress(strlen): OK',
|
|
||||||
'[REALDLL] B2 strlen call: OK',
|
|
||||||
'[REALDLL] B3 GetProcAddress(memcpy): OK',
|
|
||||||
'[REALDLL] B3 memcpy call: OK',
|
|
||||||
'[REALDLL] end fails=0'
|
|
||||||
) -MustContain @(
|
|
||||||
'[PE] DLL manager initialized',
|
|
||||||
'[PE] loaded DLL:',
|
|
||||||
'[PE] DllMain ATTACH ok'
|
|
||||||
) -MustNotContain @(
|
|
||||||
'[PANIC]',
|
|
||||||
'[PE] unimplemented import',
|
|
||||||
'[PE] ordinal forward target not found',
|
|
||||||
'[PE] malformed forwarder string',
|
|
||||||
'[PE] DllMain returned FALSE'
|
|
||||||
)
|
|
||||||
|
|
||||||
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' '用法提示仅帧缓冲输出'
|
|
||||||
} finally {
|
|
||||||
Restore-DeshabScenario
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-TestCaseResult $result
|
|
||||||
if ($result.Status -eq 'FAIL') { exit 1 } else { exit 0 }
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
#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 }
|
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
#requires -Version 5.1
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
userapps-matrix — 用户态程序功能矩阵(docs/RE/test-cases/userapps-matrix.md)
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
dev_mode 单会话覆盖 shell 内建命令与工具程序(串口镜像开启,全程可断言)。
|
|
||||||
|
|
||||||
会话 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
|
|
||||||
|
|
||||||
历史背景(详见 docs/RE/Bug记录.txt,均已 FIXED,相关降级已全部回收):
|
|
||||||
|
|
||||||
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。
|
|
||||||
#>
|
|
||||||
param([switch]$Build)
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
. (Join-Path $PSScriptRoot 'lib\QemuTest.ps1')
|
|
||||||
|
|
||||||
Initialize-QemuTest -Build:$Build
|
|
||||||
$result = New-TestCaseResult 'userapps-matrix'
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
|
||||||
# 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
|
|
||||||
}
|
|
||||||
|
|
||||||
# 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
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
# ================================================================
|
|
||||||
# 会话 A(唯一):shell 内建命令 + ping/editor/fileman/browser
|
|
||||||
# 工具链全交互 + Esc→cmd→desktop 链路
|
|
||||||
# ================================================================
|
|
||||||
Set-DeshabFirstInit -First 1 -DevMode 1
|
|
||||||
Set-DeshabAutoexec -Lines @('ver', 'exit')
|
|
||||||
|
|
||||||
$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
|
|
||||||
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
|
|
||||||
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 验证'
|
|
||||||
Add-TestCheck $result 'cmd pe 兼容(程序 5)' 'MANUAL' '由 pe-compat.ps1 自动化覆盖'
|
|
||||||
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 服务器断言)'
|
|
||||||
} finally {
|
|
||||||
Restore-DeshabScenario
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-TestCaseResult $result
|
|
||||||
if ($result.Status -eq 'FAIL') { exit 1 } else { exit 0 }
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
#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 }
|
|
||||||
Reference in New Issue
Block a user