From 17a577f2edb22b85ff1b4121581df5fe13057b65 Mon Sep 17 00:00:00 2001 From: xyou Date: Tue, 28 Jul 2026 12:00:02 +0800 Subject: [PATCH] FUCK --- CODE/UTSM/MAKEFILE | 8 +- CODE/UTSM/include/utsm/deshab_ui.h | 262 ++ CODE/UTSM/include/utsm/ept.h | 33 + CODE/UTSM/include/utsm/hypercall.h | 39 + CODE/UTSM/include/utsm/ipc_shm.h | 68 + CODE/UTSM/include/utsm/linux_loader.h | 170 ++ CODE/UTSM/include/utsm/utsm.h | 4 + CODE/UTSM/include/utsm/vmm.h | 63 + CODE/UTSM/include/utsm/vmx.h | 268 ++ CODE/UTSM/kernel/main.c | 44 + CODE/UTSM/kernel/main.o | Bin 3000 -> 4464 bytes CODE/UTSM/vmm/ept.c | 168 ++ CODE/UTSM/vmm/hypercall.c | 247 ++ CODE/UTSM/vmm/ipc_shm.c | 208 ++ CODE/UTSM/vmm/linux_boot.c | 343 +++ CODE/UTSM/vmm/linux_loader.c | 608 +++++ CODE/UTSM/vmm/vmexit.c | 125 + CODE/UTSM/vmm/vmexit_asm.S | 90 + CODE/UTSM/vmm/vmm.c | 523 ++++ CODE/UTSM/vmm/vmx.c | 257 ++ CODE/desktop/linker.ld | 4 +- CODE/desktop/main.c | 2886 +++++++++++++++++----- CODE/dsk/linker.ld | 4 +- CODE/dsk/main.c | 4 +- CODE/dsk/main.o | Bin 19816 -> 19816 bytes CODE/linux/README.md | 114 + CODE/linux/build.sh | 134 + CODE/linux/configs/utsm_x86_64_defconfig | 231 ++ CODE/linux/patches/utsm_Kconfig | 33 + CODE/linux/patches/utsm_Makefile | 10 + CODE/linux/patches/utsm_hcall.c | 369 +++ CODE/login/MAKEFILE | 4 + CODE/login/linker.ld | 16 + CODE/login/main.c | 81 + CODE/tools/browser/main.c | 359 ++- CODE/tools/desktop_app.h | 28 +- CODE/tools/editor/main.c | 142 +- CODE/tools/fat32_io.h | 557 +++++ CODE/tools/fileman/main.c | 309 ++- CODE/utsm-ipc/README.md | 174 ++ CODE/utsm-ipc/ipc_proto.h | 231 ++ RE/Bug记录.txt | 6 + SYSTEM/boot/utsm.elf | Bin 55176 -> 87120 bytes SYSTEM/limine/limine.conf | 7 + SYSTEM/system/deshab64/deshab.elf | Bin 1545792 -> 20504 bytes SYSTEM/system/deshab64/desktop.elf | Bin 73832 -> 145240 bytes SYSTEM/system/deshab64/tools/browser.elf | Bin 41136 -> 121608 bytes SYSTEM/system/deshab64/tools/editor.elf | Bin 104552 -> 506464 bytes SYSTEM/system/deshab64/tools/fileman.elf | Bin 47384 -> 213200 bytes SYSTEM/system/user/use/firstInit.txt | 2 +- SYSTEM/system/user/use/login.elf | Bin 41392 -> 41936 bytes 51 files changed, 8446 insertions(+), 787 deletions(-) create mode 100644 CODE/UTSM/include/utsm/ept.h create mode 100644 CODE/UTSM/include/utsm/hypercall.h create mode 100644 CODE/UTSM/include/utsm/ipc_shm.h create mode 100644 CODE/UTSM/include/utsm/linux_loader.h create mode 100644 CODE/UTSM/include/utsm/vmm.h create mode 100644 CODE/UTSM/include/utsm/vmx.h create mode 100644 CODE/UTSM/vmm/ept.c create mode 100644 CODE/UTSM/vmm/hypercall.c create mode 100644 CODE/UTSM/vmm/ipc_shm.c create mode 100644 CODE/UTSM/vmm/linux_boot.c create mode 100644 CODE/UTSM/vmm/linux_loader.c create mode 100644 CODE/UTSM/vmm/vmexit.c create mode 100644 CODE/UTSM/vmm/vmexit_asm.S create mode 100644 CODE/UTSM/vmm/vmm.c create mode 100644 CODE/UTSM/vmm/vmx.c create mode 100644 CODE/linux/README.md create mode 100644 CODE/linux/build.sh create mode 100644 CODE/linux/configs/utsm_x86_64_defconfig create mode 100644 CODE/linux/patches/utsm_Kconfig create mode 100644 CODE/linux/patches/utsm_Makefile create mode 100644 CODE/linux/patches/utsm_hcall.c create mode 100644 CODE/tools/fat32_io.h create mode 100644 CODE/utsm-ipc/README.md create mode 100644 CODE/utsm-ipc/ipc_proto.h create mode 100644 RE/Bug记录.txt diff --git a/CODE/UTSM/MAKEFILE b/CODE/UTSM/MAKEFILE index 6e88ac0..56636d8 100644 --- a/CODE/UTSM/MAKEFILE +++ b/CODE/UTSM/MAKEFILE @@ -5,12 +5,12 @@ OBJCOPY := llvm-objcopy ROOT := ../.. OUT := $(ROOT)/SYSTEM/boot/utsm.elf -CFLAGS := -target x86_64-unknown-none -std=c11 -ffreestanding -fno-builtin -fno-stack-protector -fno-pic -fno-pie -mcmodel=kernel -mno-red-zone -mno-sse -mno-sse2 -mno-mmx -msoft-float -m64 -Wall -Wextra -Iinclude -O2 -ASFLAGS := -target x86_64-unknown-none -ffreestanding -fno-pic -fno-pie -mcmodel=kernel -mno-red-zone -mno-sse -mno-sse2 -mno-mmx -msoft-float -m64 -Iinclude +CFLAGS := -target x86_64-unknown-none -std=c11 -ffreestanding -fno-builtin -fno-stack-protector -fno-pic -fno-pie -mcmodel=kernel -mno-red-zone -mno-sse -mno-sse2 -mno-mmx -msoft-float -m64 -Wall -Wextra -Iinclude -I../utsm-ipc -O2 +ASFLAGS := -target x86_64-unknown-none -ffreestanding -fno-pic -fno-pie -mcmodel=kernel -mno-red-zone -mno-sse -mno-sse2 -mno-mmx -msoft-float -m64 -Iinclude -I../utsm-ipc LDFLAGS := -nostdlib -static -z max-page-size=0x1000 -T linker.ld -C_SOURCES := $(wildcard arch/x86_64/*.c kernel/*.c runtime/*.c mm/*.c core/*.c crypto/*.c pckc/*.c utrw/*.c drr/*.c dkm/*.c test/*.c) -S_SOURCES := $(wildcard arch/x86_64/*.S) +C_SOURCES := $(wildcard arch/x86_64/*.c kernel/*.c runtime/*.c mm/*.c core/*.c crypto/*.c pckc/*.c utrw/*.c drr/*.c dkm/*.c test/*.c vmm/*.c) +S_SOURCES := $(wildcard arch/x86_64/*.S vmm/*.S) OBJECTS := $(C_SOURCES:.c=.o) $(S_SOURCES:.S=.o) .PHONY: all clean diff --git a/CODE/UTSM/include/utsm/deshab_ui.h b/CODE/UTSM/include/utsm/deshab_ui.h index fc39341..665ba10 100644 --- a/CODE/UTSM/include/utsm/deshab_ui.h +++ b/CODE/UTSM/include/utsm/deshab_ui.h @@ -706,4 +706,266 @@ static inline du_u32 du_theme_accent(du_theme t) { return (t == DU_THEME_DARK) ? DS_DARK_ACCENT : DS_LIGHT_ACCENT; } +/* =================================================================== + * L0+ — Winux-Kate 调色板 (eDEX-UI Neon Cyber Theme) + * + * 源自 D:\Code\Winux-Kate\src\styles\theme.css + * 风格:深蓝黑底 + 青色霓虹 + 绿色辅助 + 扫描线纹理 + * =================================================================== */ + +/* --- 主色调:深空蓝黑 (Deep Space Black) --- */ +#define DP_KATE_BG 0xFF02040Au /* --bg: #02040a */ +#define DP_KATE_BG2 0xFF050A18u /* --bg2: #050a18 */ +#define DP_KATE_PANEL 0xB20A1228u /* --panel: rgba(10,18,40,0.72) 半透明 */ +#define DP_KATE_PANEL_SOLID 0xFF0A1228u /* 不透明版本(用于无 alpha 混合场景) */ + +/* --- 强调色:霓虹青 (Neon Cyan) --- */ +#define DP_KATE_ACCENT 0xFF00E5FFu /* --accent: #00e5ff */ +#define DP_KATE_ACCENT_DIM 0x2E00E5FFu /* --accent-dim: rgba(0,229,255,0.18) */ +#define DP_KATE_ACCENT_DIM_SOLID 0xFF1A3A4Au /* 不透明近似 */ +#define DP_KATE_PANEL_BORDER 0x5900E5FFu /* --panel-border: rgba(0,229,255,0.35) */ +#define DP_KATE_PANEL_BORDER_SOLID 0xFF005A6Au /* 不透明近似 */ + +/* --- 辅助色:霓虹绿 (Neon Green) --- */ +#define DP_KATE_ACCENT2 0xFF39FF14u /* --accent2: #39ff14 */ + +/* --- 文本色 --- */ +#define DP_KATE_TEXT 0xFFCFEFFBu /* --text: #cfeffb */ +#define DP_KATE_TEXT_DIM 0xFF6F8AA8u /* --text-dim: #6f8aa8 */ + +/* --- 状态色 --- */ +#define DP_KATE_WARN 0xFFFFCC00u /* --warn: #ffcc00 */ +#define DP_KATE_DANGER 0xFFFF4D6Du /* --danger: #ff4d6d */ + +/* =================================================================== + * L1 — Winux-Kate 语义色值 + * =================================================================== */ + +#define KS_BG_PRIMARY DP_KATE_BG +#define KS_BG_SECONDARY DP_KATE_PANEL_SOLID +#define KS_BG_TERTIARY DP_KATE_BG2 + +#define KS_TEXT_PRIMARY DP_KATE_TEXT +#define KS_TEXT_DIM DP_KATE_TEXT_DIM +#define KS_TEXT_INVERT DP_KATE_BG + +#define KS_ACCENT DP_KATE_ACCENT +#define KS_ACCENT2 DP_KATE_ACCENT2 +#define KS_ACCENT_DIM DP_KATE_ACCENT_DIM_SOLID + +#define KS_BORDER DP_KATE_PANEL_BORDER_SOLID +#define KS_BORDER_FOCUS DP_KATE_ACCENT +#define KS_BORDER_DIM 0xFF1A2840u /* 暗边框 */ + +#define KS_DANGER DP_KATE_DANGER +#define KS_WARN DP_KATE_WARN + +/* =================================================================== + * Winux-Kate 布局常量(源自 theme.css / pages.css) + * =================================================================== */ + +#define KATE_TOPBAR_H 34 /* .topbar height */ +#define KATE_STATUSBAR_H 32 /* .statusbar height */ +#define KATE_TASKBAR_H 40 /* .taskbar height */ +#define KATE_PANEL_HEADER_H 24 /* .panel-header */ +#define KATE_TITLEBAR_H 28 /* 窗口标题栏 */ +#define KATE_ICON_W 104 /* .desktop-icon width */ +#define KATE_ICON_H 90 /* .desktop-icon height (含 label) */ +#define KATE_ICON_IMG 40 /* .di-img / .di-fallback */ +#define KATE_ICON_GAP 18 /* .desktop-grid gap */ +#define KATE_ICON_PAD 24 /* .desktop-canvas padding */ +#define KATE_TB_ITEM_MAX_W 200 /* .tb-item max-width */ +#define KATE_PAGE_PAD 8 /* 页面 padding */ + +/* =================================================================== + * Winux-Kate 特效函数 + * =================================================================== */ + +/* 绘制扫描线纹理( repeating-linear-gradient 0deg, 3px 周期) + * 在指定矩形区域内叠加半透明青色水平细线 */ +static inline void du_kate_scanlines(du_context *ctx, du_i64 x, du_i64 y, + du_i64 w, du_i64 h) { + for (du_i64 ry = 0; ry < h; ry++) { + du_i64 yy = y + ry; + if (yy < 0 || (du_u64)yy >= ctx->height) continue; /* 每 3 像素一条线 */ + du_u32 *line = (du_u32 *)((du_u8 *)ctx->fb + (du_u64)yy * ctx->pitch); + for (du_i64 rx = 0; rx < w; rx++) { + du_i64 xx = x + rx; + if (xx < 0 || (du_u64)xx >= ctx->width) continue; + /* 叠加极淡青色(alpha ~0.03 ≈ 8/255)*/ + du_u32 cur = line[(du_u64)xx]; + du_u32 b = (cur >> 16) & 0xFF; + du_u32 g = (cur >> 8) & 0xFF; + du_u32 r = cur & 0xFF; + b = (b + 2 < 255) ? b + 2 : 255; + g = (g + 4 < 255) ? g + 4 : 255; + r = (r + 2 < 255) ? r + 2 : 255; + line[(du_u64)xx] = 0xFF000000u | (b << 16) | (g << 8) | r; + } + } +} + +/* 绘制带光晕的矩形边框(box-shadow 效果) + * 外发光:在矩形外围绘制渐变青色光晕 */ +static inline void du_kate_glow_border(du_context *ctx, du_i64 x, du_i64 y, + du_i64 w, du_i64 h, du_u32 glow_color) { + /* 2px 渐变光晕,alpha 混合到实际背景(逐像素读-混合-写) */ + for (du_i64 i = 2; i >= 1; i--) { + du_u32 alpha = (i == 2) ? 50 : 100; + /* 上下边 */ + for (du_i64 c = -i; c < w + i; c++) { + du_i64 xt = x + c; + du_pixel(ctx, xt, y - i, + du_blend(du_pixel_read(ctx, xt, y - i), glow_color, alpha)); + du_pixel(ctx, xt, y + h + i - 1, + du_blend(du_pixel_read(ctx, xt, y + h + i - 1), glow_color, alpha)); + } + /* 左右边 */ + for (du_i64 r = -i; r < h + i; r++) { + du_i64 yt = y + r; + du_pixel(ctx, x - i, yt, + du_blend(du_pixel_read(ctx, x - i, yt), glow_color, alpha)); + du_pixel(ctx, x + w + i - 1, yt, + du_blend(du_pixel_read(ctx, x + w + i - 1, yt), glow_color, alpha)); + } + } +} + +/* 绘制 Winux-Kate 面板(.panel 样式) + * - 半透明深蓝背景 + * - 青色边框 + * - 内外光晕 + * - 扫描线纹理 + * - 可选标题栏 */ +static inline void du_kate_panel(du_context *ctx, du_i64 x, du_i64 y, + du_i64 w, du_i64 h, const char *header) { + /* 1. 背景 */ + du_fill_rounded_rect(ctx, x, y, w, h, KS_BG_SECONDARY, 2); + /* 2. 边框 */ + du_rect_outline(ctx, x, y, w, h, KS_BORDER, 2); + /* 3. 外光晕 */ + du_kate_glow_border(ctx, x, y, w, h, KS_ACCENT); + /* 4. 扫描线纹理 */ + du_kate_scanlines(ctx, x + 1, y + 1, w - 2, h - 2); + /* 5. 标题栏 */ + if (header) { + du_fill_rect(ctx, x, y, w, KATE_PANEL_HEADER_H, 0xFF062838u); /* 深青色标题栏 */ + du_divider_h(ctx, x, y + KATE_PANEL_HEADER_H, w, KS_BORDER); + /* 标题文字:青色,大写,letter-spacing */ + du_draw_string(ctx, header, x + 10, y + (KATE_PANEL_HEADER_H - DU_ASCII_LINE_H) / 2 + 2, + KS_ACCENT, 0xFF062838u, DU_ASCII_STEP); + /* 标题前的小绿点 */ + du_fill_rect(ctx, x + 4, y + KATE_PANEL_HEADER_H / 2 - 3, 6, 6, KS_ACCENT2); + } +} + +/* 绘制 Winux-Kate 顶部栏(.topbar 样式) + * - 34px 高 + * - 渐变背景 + * - 底部边框 + * - 品牌名 + 时钟槽位 */ +static inline void du_kate_topbar(du_context *ctx, du_i64 w, + const char *brand, const char *clock) { + /* 渐变背景:左青色微亮 → 右深色 */ + du_fill_rect_gradient(ctx, 0, 0, w, KATE_TOPBAR_H, + 0xFF0A3050u, 0xFF02040Au); + /* 底部分隔线 */ + du_divider_h(ctx, 0, KATE_TOPBAR_H, w, KS_BORDER); + /* 品牌名(青色,加粗,letter-spacing 4px)*/ + if (brand) { + du_draw_string(ctx, brand, 12, (KATE_TOPBAR_H - DU_ASCII_LINE_H) / 2 + 1, + KS_ACCENT, 0, DU_ASCII_STEP); + } + /* 时钟(右侧,绿色)*/ + if (clock) { + du_i64 cl = 0; + while (clock[cl]) cl++; + du_draw_string(ctx, clock, w - 12 - cl * DU_ASCII_STEP, + (KATE_TOPBAR_H - DU_ASCII_LINE_H) / 2 + 1, + KS_ACCENT2, 0, DU_ASCII_STEP); + } +} + +/* 绘制 Winux-Kate 状态栏(.statusbar 样式) + * - 32px 高 + * - 顶部边框 + * - 半透明青色背景 */ +static inline void du_kate_statusbar(du_context *ctx, du_i64 x, du_i64 y, + du_i64 w, const char *status_text) { + du_fill_rect(ctx, x, y, w, KATE_STATUSBAR_H, 0xFF051828u); + du_divider_h(ctx, x, y, w, KS_BORDER); + if (status_text) { + du_draw_string(ctx, status_text, x + 12, + y + (KATE_STATUSBAR_H - DU_ASCII_LINE_H) / 2 + 1, + KS_TEXT_DIM, 0, DU_ASCII_STEP); + } +} + +/* 绘制 Winux-Kate 桌面图标(.desktop-icon 样式) + * - 104px 宽 + * - 40x40 图标区域 + * - 下方文字标签 + * - 选中态:青色边框 + 光晕 */ +static inline void du_kate_desktop_icon(du_context *ctx, du_i64 x, du_i64 y, + const char *label, int selected) { + /* 悬停/选中背景 */ + if (selected) { + du_fill_rect(ctx, x, y, KATE_ICON_W, KATE_ICON_H, KS_ACCENT_DIM); + du_rect_outline(ctx, x, y, KATE_ICON_W, KATE_ICON_H, KS_ACCENT, 1); + du_kate_glow_border(ctx, x, y, KATE_ICON_W, KATE_ICON_H, KS_ACCENT); + } + /* 图标占位区域(40x40 居中)*/ + du_i64 ix = x + (KATE_ICON_W - KATE_ICON_IMG) / 2; + du_i64 iy = y + 10; + du_rect_outline(ctx, ix, iy, KATE_ICON_IMG, KATE_ICON_IMG, KS_BORDER, 1); + /* 标签 */ + if (label) { + du_i64 ll = 0; + while (label[ll]) ll++; + du_i64 lx = x + (KATE_ICON_W - ll * DU_ASCII_STEP) / 2; + if (lx < x + 2) lx = x + 2; + du_draw_string(ctx, label, lx, y + KATE_ICON_IMG + 18, + KS_TEXT_PRIMARY, 0, DU_ASCII_STEP); + } +} + +/* 绘制 Winux-Kate 任务栏项(.tb-item 样式) */ +static inline void du_kate_tb_item(du_context *ctx, du_i64 x, du_i64 y, + const char *title, int active) { + du_i64 tl = 0; + while (title[tl]) tl++; + du_i64 w = tl * DU_ASCII_STEP + 24; + if (w > KATE_TB_ITEM_MAX_W) w = KATE_TB_ITEM_MAX_W; + + du_u32 fg = active ? KS_ACCENT : KS_TEXT_PRIMARY; + if (active) { + du_fill_rect(ctx, x, y, w, 28, KS_ACCENT_DIM); + } + du_rect_outline(ctx, x, y, w, 28, KS_BORDER, 1); + if (active) { + du_kate_glow_border(ctx, x, y, w, 28, KS_ACCENT); + } + du_draw_string(ctx, title, x + 8, y + (28 - DU_ASCII_LINE_H) / 2 + 1, + fg, 0, DU_ASCII_STEP); +} + +/* 绘制 Winux-Kate 按钮 (.btn 样式) */ +static inline void du_kate_button(du_context *ctx, du_i64 x, du_i64 y, + du_i64 w, du_i64 h, const char *label, + int hover) { + du_u32 fg = KS_ACCENT; + if (hover) { + du_fill_rect(ctx, x, y, w, h, KS_ACCENT_DIM); + } + du_rect_outline(ctx, x, y, w, h, KS_BORDER, 1); + if (hover) { + du_kate_glow_border(ctx, x, y, w, h, KS_ACCENT); + } + du_i64 ll = 0; + while (label[ll]) ll++; + du_i64 lx = x + (w - ll * DU_ASCII_STEP) / 2; + du_draw_string(ctx, label, lx, y + (h - DU_ASCII_LINE_H) / 2 + 1, + fg, hover ? KS_ACCENT_DIM : KS_BG_SECONDARY, DU_ASCII_STEP); +} + #endif /* DESHAB_UI_H */ diff --git a/CODE/UTSM/include/utsm/ept.h b/CODE/UTSM/include/utsm/ept.h new file mode 100644 index 0000000..aa3335b --- /dev/null +++ b/CODE/UTSM/include/utsm/ept.h @@ -0,0 +1,33 @@ +#ifndef UTSM_EPT_H +#define UTSM_EPT_H + +#include + +/* EPT 页表结构:4 级页表(PML4 / PDPT / PD / PT),每级 512 项,每页 4KB。 */ + +#define EPT_PAGE_SIZE 4096ULL +#define EPT_ENTRIES 512ULL + +/* EPTP 构造:bits [5:0]=memory type, bits [7:6]=walk length-1, bits [51:12]=PML4 HPA */ +#define EPTP_WALK_LEN_4 (3ULL << 6) /* 4 级页表 */ +#define EPTP_MEMTYPE_WB (EPT_MEMORY_TYPE_WB) + +/* 初始化 EPT:分配 PML4,置零。返回 0 成功。 */ +int ept_init(void); + +/* 构造 EPTP 值(用于 VMCS EPT_POINTER 字段)。 */ +u64 ept_get_eptp(void); + +/* 在 EPT 中映射一段 GPA→HPA 区间,权限由 flags 决定(EPT_READ/WRITE/EXECUTE)。 + * 自动按 4KB 分配中间页表页。返回 0 成功。 */ +int ept_map_range(u64 gpa, u64 hpa, u64 size, u64 flags); + +/* 便利函数:1:1 映射(GPA==HPA)。 */ +int ept_identity_map(u64 gpa, u64 size, u64 flags); + +/* Walk the EPT to translate a GPA to its mapped HPA. + * Returns the HPA on success, 0 if the GPA is not mapped. + * Handles 4KB pages and 2MB large pages. */ +u64 ept_gpa_to_hpa(u64 gpa); + +#endif diff --git a/CODE/UTSM/include/utsm/hypercall.h b/CODE/UTSM/include/utsm/hypercall.h new file mode 100644 index 0000000..832355c --- /dev/null +++ b/CODE/UTSM/include/utsm/hypercall.h @@ -0,0 +1,39 @@ +#ifndef UTSM_HYPERCALL_H +#define UTSM_HYPERCALL_H + +/* UTSM-side VMCALL hypercall handler. + * + * When the Linux guest executes a VMCALL instruction, it triggers a VM-Exit + * with exit reason EXIT_VMCALL (18). The guest places: + * RAX = UTSM_HCALL_MAGIC | op (identifies this as a UTSM hypercall) + * RDI = arg0 + * RSI = arg1 + * RDX = arg2 + * + * The handler validates the magic, dispatches to the operation handler, + * writes the return value into GUEST_RAX, and advances GUEST_RIP past + * the VMCALL instruction so the guest resumes normally. + * + * If the magic doesn't match, the handler returns a VMFAIL indication + * (by setting the VMCS vm-exit instruction error), which causes the guest + * to see VMCALL fail. Phase 1.3: we just return -UTSM_HCALL_NOSYS. + * + * Protocol details in CODE/utsm-ipc/ipc_proto.h. + */ + +#include + +/* VM-Exit handler for VMCALL (exit reason 18). + * + * Reads guest RAX/RDI/RSI/RDX from the VMCS save area (passed in by the + * assembly exit stub), dispatches the hypercall, writes the result to + * GUEST_RAX, and advances GUEST_RIP. + * + * Returns 1 to resume the guest, 0 to terminate. */ +int hypercall_handle(u64 guest_rax, u64 guest_rdi, u64 guest_rsi, u64 guest_rdx, + u64 guest_rip, u64 instr_len); + +/* Statistics */ +u64 hypercall_get_count(void); + +#endif /* UTSM_HYPERCALL_H */ diff --git a/CODE/UTSM/include/utsm/ipc_shm.h b/CODE/UTSM/include/utsm/ipc_shm.h new file mode 100644 index 0000000..7c0adb8 --- /dev/null +++ b/CODE/UTSM/include/utsm/ipc_shm.h @@ -0,0 +1,68 @@ +#ifndef UTSM_IPC_SHM_H +#define UTSM_IPC_SHM_H + +/* UTSM-side IPC shared memory manager. + * + * Allocates a 1MB contiguous physical region, initializes the + * utsm_ipc_shm structure (header + two ring buffers + payload pool), + * and EPT-maps it at LINUX_GUEST_IPC_SHM_GPA so the Linux guest can + * access it after querying the GPA via UTSM_HCALL_SHM_INFO. + * + * The shared protocol structures are defined in CODE/utsm-ipc/ipc_proto.h, + * which is included by both UTSM and the Linux utsm-ipc driver. + * + * Ring buffer discipline: + * utsm_to_linux: UTSM produces (ipc_shm_send), Linux consumes + * linux_to_utsm: Linux produces, UTSM consumes (ipc_shm_recv) + */ + +#include +#include /* shared UTSM↔Linux IPC protocol (struct utsm_ipc_msg, etc.) */ + +/* ===== Initialization ===== */ + +/* Allocate, initialize, and EPT-map the IPC shared memory region. + * Must be called after vmm_init() (EPT is required) and before linux_launch(). + * Returns 0 on success, negative on error. */ +int ipc_shm_init(void); + +/* Shut down IPC: mark UTSM as not ready. + * Does not free the shared memory (it may still be referenced by EPT). */ +void ipc_shm_shutdown(void); + +/* Query state */ +int ipc_shm_is_ready(void); + +/* Get the HPA of the shared memory region (for EPT mapping). + * Returns 0 if not initialized. */ +u64 ipc_shm_get_hpa(void); + +/* Get the GPA of the shared memory region (passed to Linux via hypercall). */ +u64 ipc_shm_get_gpa(void); + +/* Get the size of the shared memory region. */ +u64 ipc_shm_get_size(void); + +/* ===== Message API (UTSM-side) ===== */ + +/* Send a message from UTSM to Linux (enqueues into utsm_to_linux ring). + * Returns 0 on success, -1 if the ring is full. */ +int ipc_shm_send(u32 msg_type, const void *data, u32 data_len); + +/* Receive a message from Linux to UTSM (dequeues from linux_to_utsm ring). + * Returns 0 on success (msg filled in), -1 if the ring is empty. */ +int ipc_shm_recv(struct utsm_ipc_msg *msg_out); + +/* Check if there are pending messages from Linux. */ +int ipc_shm_has_pending(void); + +/* Notify Linux that a message is waiting (placeholder for IPI injection). + * Phase 1.3: logs the event; Phase 1.4+ will inject a guest IRQ. */ +void ipc_shm_notify_linux(void); + +/* ===== Debug ===== */ + +/* Dump ring buffer statistics to the UTSM serial console. */ +void ipc_shm_dump_stats(void); + +#endif /* UTSM_IPC_SHM_H */ diff --git a/CODE/UTSM/include/utsm/linux_loader.h b/CODE/UTSM/include/utsm/linux_loader.h new file mode 100644 index 0000000..0f6f204 --- /dev/null +++ b/CODE/UTSM/include/utsm/linux_loader.h @@ -0,0 +1,170 @@ +#ifndef UTSM_LINUX_LOADER_H +#define UTSM_LINUX_LOADER_H + +#include + +/* ===== Linux boot protocol structures (Documentation/x86/boot.rst) ===== */ + +/* bzImage setup_header — lives at offset 0x1f1 inside bzImage and boot_params. + * Only the fields we actually use are listed; gaps are reserved/padding. */ +struct linux_setup_header { + u8 setup_sects; /* 0x1f1: number of setup sectors */ + u16 root_flags; /* 0x1f2 */ + u32 syssize; /* 0x1f4: protected-mode code size / 16 */ + u16 ram_size; /* 0x1f8 */ + u16 vid_mode; /* 0x1fa */ + u16 boot_flag; /* 0x1fe: 0xAA55 */ + u16 jump; /* 0x200: jump insn */ + u32 header; /* 0x202: "HdrS" magic */ + u16 version; /* 0x206: boot protocol version */ + u32 realmode_swtch; /* 0x208 */ + u16 start_sys_seg; /* 0x20c */ + u16 kernel_version; /* 0x20e: offset to version string */ + u8 type_of_loader; /* 0x210 */ + u8 loadflags; /* 0x211 */ + u16 setup_move_size; /* 0x212 */ + u32 code32_start; /* 0x214: 32-bit entry (also 64-bit entry base) */ + u32 ramdisk_image; /* 0x218 */ + u32 ramdisk_size; /* 0x21c */ + u32 bootsect_kludge; /* 0x220 */ + u16 heap_end_ptr; /* 0x224 */ + u8 ext_loader_ver; /* 0x226 */ + u8 ext_loader_type; /* 0x227 */ + u32 cmd_line_ptr; /* 0x228: command line physical address */ + u32 init_size; /* 0x22c: total init memory needed */ + u32 acpi_rsdp_addr; /* 0x230 */ + u8 _pad0[4]; /* 0x234: relocatable_kernel + min_alignment */ + u16 xloadflags; /* 0x236: bit0=XLF_KERNEL_64 */ + u32 cmdline_size; /* 0x238 */ + u32 _pad1; /* 0x23c */ + u64 setup_data; /* 0x240: linked list of setup_data */ + u64 pref_address; /* 0x248 → wait, this is payload_offset */ + + /* NOTE: The real layout continues with payload_offset at 0x248. + * However, pref_address is at 0x258 and init_size at 0x260. + * The struct above is intentionally simplified — we read fields + * by offset rather than relying on this struct for the full layout. */ +} __attribute__((packed)); + +/* boot_params is 4096 bytes. The setup_header is at offset 0x1f1. */ +#define BOOT_PARAMS_SIZE 4096 +#define SETUP_HEADER_OFFSET 0x1f1 + +/* Key field offsets within bzImage / boot_params */ +#define BZ_BOOT_FLAG_OFF 0x1fe +#define BZ_HEADER_MAGIC 0x53726448 /* "HdrS" */ +#define BZ_HEADER_OFF 0x202 +#define BZ_VERSION_OFF 0x206 +#define BZ_TYPE_OF_LOADER_OFF 0x210 +#define BZ_LOADFLAGS_OFF 0x211 +#define BZ_CODE32_START_OFF 0x214 +#define BZ_RAMDISK_IMAGE_OFF 0x218 +#define BZ_RAMDISK_SIZE_OFF 0x21c +#define BZ_CMD_LINE_PTR_OFF 0x228 +#define BZ_INIT_SIZE_OFF 0x22c /* wait — actually init_size is at 0x260 */ +#define BZ_XLOADFLAGS_OFF 0x236 +#define BZ_CMDLINE_SIZE_OFF 0x238 +#define BZ_PAYLOAD_OFFSET_OFF 0x248 +#define BZ_PAYLOAD_LENGTH_OFF 0x24c +#define BZ_PREF_ADDRESS_OFF 0x258 +#define BZ_INIT_SIZE_ACTUAL_OFF 0x260 + +/* xloadflags bits */ +#define XLF_KERNEL_64 0x0001 +#define XLF_CAN_BE_LOADED_ABOVE 0x0002 +#define XLF_EFI_HANDOVER 0x0010 +#define XLF_EFI_KEXEC 0x0020 + +/* loadflags bits */ +#define LOADFLAG_LOADED_HIGH 0x01 +#define LOADFLAG_KEEP_ROOT 0x02 +#define LOADFLAG_CAN_USE_HEAP 0x80 + +/* e820 entry types */ +#define E820_TYPE_RAM 1 +#define E820_TYPE_RESERVED 2 +#define E820_TYPE_ACPI 3 +#define E820_TYPE_NVS 4 +#define E820_TYPE_UNUSABLE 5 +#define E820_TYPE_PMEM 7 + +/* e820_table entry in boot_params at offset 0x2e0 (actually 0x2d0 in older, + * 0x2e0 in newer). We use 0x2e0 and the e820_entries count at 0x1e8. + * Each entry is 20 bytes: u64 addr, u64 size, u32 type. */ +#define E820_ENTRIES_OFFSET 0x1e8 +#define E820_TABLE_OFFSET 0x2e0 +#define E820_MAX_ENTRIES 128 +#define E820_ENTRY_SIZE 20 + +/* ===== Guest memory layout (GPA) ===== + * Based on utsm-linux-dual-kernel-architecture.md. + * 0x00000000-0x003FFFFF : UTSM reserved (denied to Linux) + * 0x00400000-0x004FFFFF : boot_params / cmdline / GDT / page tables / stack + * 0x01000000-0x02FFFFFF : Linux kernel image + * 0x03000000-0x03FFFFFF : Linux initrd + * 0x04000000-0x040FFFFF : IPC shared memory (1MB, both kernels RW) + * 0x05000000-MAX : Linux general RAM + */ +#define LINUX_GUEST_KERNEL_GPA 0x01000000ULL /* 16 MB, 2MB-aligned */ +#define LINUX_GUEST_BOOTPARAMS_GPA 0x00400000ULL /* 4 MB */ +#define LINUX_GUEST_CMDLINE_GPA 0x00500000ULL /* 5 MB */ +#define LINUX_GUEST_INITRD_GPA 0x03000000ULL /* 48 MB (within kernel+initrd region) */ +#define LINUX_GUEST_STACK_GPA 0x00800000ULL /* 8 MB (stack grows down) */ +#define LINUX_GUEST_GDT_GPA 0x00600000ULL /* 6 MB (guest GDT) */ +#define LINUX_GUEST_PGT_GPA 0x00700000ULL /* 7 MB (guest page tables) */ +#define LINUX_GUEST_CMDLINE_MAX 4096 + +/* IPC shared memory GPA (must match CODE/utsm-ipc/ipc_proto.h) */ +#define LINUX_GUEST_IPC_SHM_GPA 0x04000000ULL /* 64 MB */ +#define LINUX_GUEST_IPC_SHM_SIZE (1024 * 1024) /* 1MB */ + +/* General RAM starts after IPC region */ +#define LINUX_GUEST_RAM_GPA 0x05000000ULL /* 80 MB */ + +/* ===== Linux loader API ===== */ + +/* Loaded Linux guest state */ +struct linux_guest_info { + u64 kernel_gpa; /* GPA where kernel was loaded */ + u64 kernel_entry; /* 64-bit entry point GPA */ + u64 kernel_size; /* Size of loaded kernel image */ + u64 bootparams_gpa; /* GPA of boot_params */ + u64 cmdline_gpa; /* GPA of command line */ + u64 cmdline_size; /* Command line length (with NUL) */ + u64 initrd_gpa; /* GPA of initrd (0 = none) */ + u64 initrd_size; /* Size of initrd */ + u64 pgt_gpa; /* GPA of guest page tables */ + u64 gdt_gpa; /* GPA of guest GDT */ + int loaded; /* 1 = successfully loaded */ +}; + +/* Find the Linux bzImage boot module among Limine modules. + * Returns pointer to the module data, or NULL if not found. + * Sets *size_out to the module size. */ +void *linux_find_bzimage_module(u64 *size_out); + +/* Parse bzImage header and extract key parameters. + * Returns 0 on success, negative on error. */ +int linux_parse_bzimage(const void *bzimage, u64 size, + u64 *payload_offset_out, u64 *payload_length_out, + u64 *init_size_out, u64 *entry_offset_out); + +/* Initialize the Linux guest: parse bzImage, allocate guest memory, + * set up boot_params, page tables, GDT, and EPT mappings. + * Returns 0 on success, negative on error. */ +int linux_loader_init(void); + +/* Get the loaded Linux guest info (NULL if not loaded). */ +const struct linux_guest_info *linux_get_guest_info(void); + +/* Configure VMCS for Linux guest and perform vmlaunch. + * Must be called after vmm_init() + linux_loader_init(). + * Returns 0 on successful guest termination, negative on error. */ +int linux_launch(void); + +/* Default Linux command line (can be overridden). */ +#define LINUX_DEFAULT_CMDLINE \ + "console=ttyS0,115200 earlyprintk=serial nokaslr " \ + "no_timer_check loglevel=7" + +#endif diff --git a/CODE/UTSM/include/utsm/utsm.h b/CODE/UTSM/include/utsm/utsm.h index 1740c5c..7dbe15a 100644 --- a/CODE/UTSM/include/utsm/utsm.h +++ b/CODE/UTSM/include/utsm/utsm.h @@ -9,6 +9,10 @@ #include #include #include +#include +#include +#include +#include void utsm_init(void); int utsm_selftest_run(void); diff --git a/CODE/UTSM/include/utsm/vmm.h b/CODE/UTSM/include/utsm/vmm.h new file mode 100644 index 0000000..d130267 --- /dev/null +++ b/CODE/UTSM/include/utsm/vmm.h @@ -0,0 +1,63 @@ +#ifndef UTSM_VMM_H +#define UTSM_VMM_H + +#include + +/* VMM 顶层 API:协调 VMX/EPT/VMCS/VMEXIT,启动 guest。 + * + * Phase 1.1(当前):vmm_init() 完成 VMX root 启用 + EPT 初始化, + * vmm_self_test() 启动一个最小 guest(HLT 指令)验证 VMEXIT 路径。 + * + * Phase 1.2+:vmm_launch_linux() 加载并启动 Linux 6.6 LTS guest。 + */ + +/* ===== Guest register save area ===== + * + * VMCS does NOT save guest GPRs (RAX-R15) on VM-Exit. The assembly VM-Exit + * handler (vmexit_asm.S) saves all 16 GPRs into this structure before + * calling the C dispatcher, and restores them before vmresume. + * + * C handlers (e.g. hypercall.c) can read guest arguments from this structure + * and write return values (e.g. g_guest_regs.rax = retval) which will be + * loaded into the CPU before vmresume resumes the guest. */ +struct vmexit_guest_regs { + u64 rax; + u64 rbx; + u64 rcx; + u64 rdx; + u64 rsi; + u64 rdi; + u64 rbp; + u64 r8; + u64 r9; + u64 r10; + u64 r11; + u64 r12; + u64 r13; + u64 r14; + u64 r15; +}; + +/* Global guest register save area (referenced by vmexit_asm.S). + * Non-static: shared between vmm.c, vmexit.c, hypercall.c, and vmexit_asm.S. */ +extern volatile struct vmexit_guest_regs g_guest_regs; + +/* ===== VMM API ===== */ + +/* 初始化 VMM:VMX root 模式 + EPT 表 + 分配 VMCS。 + * 返回 0 成功,负值失败。 */ +int vmm_init(void); + +/* 关闭 VMM:vmxoff。 */ +void vmm_shutdown(void); + +/* Phase 1.1 自检:启动最小 guest 验证 VMX 路径。 + * guest 代码为内嵌的 HLT 指令,预期触发 HLT VMEXIT。 + * 返回 0 成功。 */ +int vmm_self_test(void); + +/* VMM 状态查询 */ +int vmm_is_ready(void); +u64 vmm_get_eptp(void); + +#endif diff --git a/CODE/UTSM/include/utsm/vmx.h b/CODE/UTSM/include/utsm/vmx.h new file mode 100644 index 0000000..4d737f3 --- /dev/null +++ b/CODE/UTSM/include/utsm/vmx.h @@ -0,0 +1,268 @@ +#ifndef UTSM_VMX_H +#define UTSM_VMX_H + +#include + +/* ===== VMX MSRs ===== */ +#define IA32_VMX_BASIC 0x480 +#define IA32_VMX_PINBASED_CTLS 0x481 +#define IA32_VMX_PROCBASED_CTLS 0x482 +#define IA32_VMX_EXIT_CTLS 0x483 +#define IA32_VMX_ENTRY_CTLS 0x484 +#define IA32_VMX_PROCBASED_CTLS2 0x48B +#define IA32_VMX_EPT_VPID_CAP 0x48C +#define IA32_VMX_TRUE_PINBASED_CTLS 0x48D +#define IA32_VMX_TRUE_PROCBASED_CTLS 0x48E +#define IA32_VMX_TRUE_EXIT_CTLS 0x48F +#define IA32_VMX_TRUE_ENTRY_CTLS 0x490 + +/* ===== CR4 / CR0 bits ===== */ +#define CR4_VMXE_BIT 13ULL +#define CR4_VMXE (1ULL << CR4_VMXE_BIT) +#define CR4_PAE (1ULL << 5) +#define CR4_PGE (1ULL << 7) +#define CR4_PSE (1ULL << 4) + +#define CR0_PE (1ULL << 0) +#define CR0_NE (1ULL << 5) +#define CR0_WP (1ULL << 16) +#define CR0_PG (1ULL << 31) + +/* ===== VMCS field encodings (Intel SDM Vol 3, Appendix H) ===== */ +/* Field encoding: bit 13=1 -> host root, bit 12=1 -> guest, bits [11:10] = type, bits [9:1] = index */ +#define VMCS_FIELD_HOST 0x2000ULL +#define VMCS_FIELD_GUEST 0x1000ULL +#define VMCS_FIELD_CTRL 0x4000ULL +#define VMCS_FIELD_READ 0x6000ULL + +/* Control fields */ +#define VMCS_VPID 0x0000 +#define VMCS_GUEST_ES_ADDR 0x0006 +#define VMCS_GUEST_CS_ADDR 0x0008 +#define VMCS_GUEST_SS_ADDR 0x000A +#define VMCS_GUEST_DS_ADDR 0x000C +#define VMCS_GUEST_FS_ADDR 0x000E +#define VMCS_GUEST_GS_ADDR 0x0010 +#define VMCS_GUEST_LDTR_ADDR 0x0012 +#define VMCS_GUEST_TR_ADDR 0x0014 +#define VMCS_GUEST_INTR_STATUS 0x0009 +#define VMCS_IO_BITMAP_A 0x2000 +#define VMCS_IO_BITMAP_B 0x2002 +#define VMCS_MSR_BITMAP 0x2004 +#define VMCS_TSC_OFFSET 0x2010 +#define VMCS_EPT_POINTER 0x201A + +/* Read-only data fields */ +#define VMCS_GUEST_PHYSICAL_ADDR 0x2400 +#define VMCS_EXIT_REASON 0x4402 +#define VMCS_EXIT_QUALIFICATION 0x6400 +#define VMCS_IO_RCX 0x6404 +#define VMCS_IO_RSI 0x6406 +#define VMCS_IO_RDI 0x6408 +#define VMCS_IO_RIP 0x640A +#define VMCS_GUEST_LINEAR_ADDR 0x640A +#define VMCS_INSTRUCTION_LENGTH 0x440C +#define VMCS_VMX_INSTRUCTION_ERROR 0x4406 + +/* Guest-state fields */ +#define VMCS_GUEST_ES_SELECTOR 0x0800 +#define VMCS_GUEST_CS_SELECTOR 0x0802 +#define VMCS_GUEST_SS_SELECTOR 0x0804 +#define VMCS_GUEST_DS_SELECTOR 0x0806 +#define VMCS_GUEST_FS_SELECTOR 0x0808 +#define VMCS_GUEST_GS_SELECTOR 0x080A +#define VMCS_GUEST_LDTR_SELECTOR 0x080C +#define VMCS_GUEST_TR_SELECTOR 0x080E +#define VMCS_GUEST_CS_LIMIT 0x4800 +#define VMCS_GUEST_SS_LIMIT 0x4804 +#define VMCS_GUEST_DS_LIMIT 0x4806 +#define VMCS_GUEST_ES_LIMIT 0x4801 +#define VMCS_GUEST_FS_LIMIT 0x4808 +#define VMCS_GUEST_GS_LIMIT 0x480A +#define VMCS_GUEST_LDTR_LIMIT 0x480C +#define VMCS_GUEST_TR_LIMIT 0x480E +#define VMCS_GUEST_CS_ACCESS 0x4816 +#define VMCS_GUEST_SS_ACCESS 0x481A +#define VMCS_GUEST_DS_ACCESS 0x4818 +#define VMCS_GUEST_ES_ACCESS 0x4814 +#define VMCS_GUEST_FS_ACCESS 0x481C +#define VMCS_GUEST_GS_ACCESS 0x481E +#define VMCS_GUEST_LDTR_ACCESS 0x4820 +#define VMCS_GUEST_TR_ACCESS 0x4822 +#define VMCS_GUEST_INTERRUPTIBILITY 0x4824 +#define VMCS_GUEST_ACTIVITY_STATE 0x4826 +#define VMCS_GUEST_SYSENTER_CS 0x482A +#define VMCS_GUEST_PREEMPTION_TIMER 0x482E + +/* 64-bit guest-state fields(与 PDPTR 共享编码,仅在 EPT 启用时为 EFER/PAT) */ +#define VMCS_GUEST_IA32_EFER 0x2806 +#define VMCS_GUEST_IA32_PAT 0x2804 +#define VMCS_GUEST_IA32_PERF_GLOBAL_CTRL 0x2808 +#define VMCS_GUEST_CR0 0x6800 +#define VMCS_GUEST_CR3 0x6802 +#define VMCS_GUEST_CR4 0x6804 +#define VMCS_GUEST_ES_BASE 0x6806 +#define VMCS_GUEST_CS_BASE 0x6808 +#define VMCS_GUEST_SS_BASE 0x680A +#define VMCS_GUEST_DS_BASE 0x680C +#define VMCS_GUEST_FS_BASE 0x680E +#define VMCS_GUEST_GS_BASE 0x6810 +#define VMCS_GUEST_LDTR_BASE 0x6812 +#define VMCS_GUEST_TR_BASE 0x6814 +#define VMCS_GUEST_GDTR_BASE 0x6816 +#define VMCS_GUEST_GDTR_LIMIT 0x4810 +#define VMCS_GUEST_IDTR_BASE 0x6818 +#define VMCS_GUEST_IDTR_LIMIT 0x4812 +#define VMCS_GUEST_DR7 0x681A +#define VMCS_GUEST_RSP 0x681C +#define VMCS_GUEST_RIP 0x681E +#define VMCS_GUEST_RFLAGS 0x6820 +#define VMCS_GUEST_SYSENTER_ESP 0x6824 +#define VMCS_GUEST_SYSENTER_EIP 0x6826 + +/* Host-state fields (Intel SDM Vol 3D, Appendix H, Table H-6) */ +#define VMCS_HOST_CR0 0x6C00 +#define VMCS_HOST_CR3 0x6C02 +#define VMCS_HOST_CR4 0x6C04 +#define VMCS_HOST_ES_SELECTOR 0x0C00 +#define VMCS_HOST_CS_SELECTOR 0x0C02 +#define VMCS_HOST_SS_SELECTOR 0x0C04 +#define VMCS_HOST_DS_SELECTOR 0x0C06 +#define VMCS_HOST_FS_SELECTOR 0x0C08 +#define VMCS_HOST_GS_SELECTOR 0x0C0A +#define VMCS_HOST_TR_SELECTOR 0x0C0C +#define VMCS_HOST_FS_BASE 0x6C06 +#define VMCS_HOST_GS_BASE 0x6C08 +#define VMCS_HOST_TR_BASE 0x6C0A +#define VMCS_HOST_GDTR_BASE 0x6C0C +#define VMCS_HOST_IDTR_BASE 0x6C0E +#define VMCS_HOST_SYSENTER_CS 0x4C00 +#define VMCS_HOST_RSP 0x6C14 +#define VMCS_HOST_RIP 0x6C16 +#define VMCS_HOST_SYSENTER_ESP 0x6C10 +#define VMCS_HOST_SYSENTER_EIP 0x6C12 + +/* 64-bit host-state fields */ +#define VMCS_HOST_IA32_EFER 0x2C02 +#define VMCS_HOST_IA32_PAT 0x2C00 +#define VMCS_HOST_IA32_PERF_GLOBAL_CTRL 0x2C04 + +/* IA32_EFER MSR (0xC0000080) bits */ +#define EFER_SCE (1ULL << 0) +#define EFER_LME (1ULL << 8) +#define EFER_LMA (1ULL << 10) +#define EFER_NXE (1ULL << 11) +#define EFER_SVME (1ULL << 12) +#define EFER_LMSLE (1ULL << 13) +#define EFER_FFXSR (1ULL << 14) +#define EFER_TCE (1ULL << 15) + +/* VM-execution control fields */ +#define VMCS_PIN_BASED_VM_EXEC_CONTROL 0x4000 +#define VMCS_CPU_BASED_VM_EXEC_CONTROL 0x4002 +#define VMCS_EXCEPTION_BITMAP 0x4004 +#define VMCS_VM_EXIT_CONTROLS 0x400C +#define VMCS_VM_ENTRY_CONTROLS 0x4012 +#define VMCS_VM_ENTRY_INTERRUPT_INFO 0x4016 +#define VMCS_SECONDARY_VM_EXEC_CONTROL 0x401E + +/* ===== Pin-based controls ===== */ +#define PIN_EXT_INTERRUPT_EXITING (1ULL << 0) +#define PIN_NMI_EXITING (1ULL << 3) + +/* ===== Primary processor-based controls ===== */ +#define CPU_BASED_HLT_EXITING (1ULL << 7) +#define CPU_BASED_INVLPG_EXITING (1ULL << 9) +#define CPU_BASED_MWAIT_EXITING (1ULL << 10) +#define CPU_BASED_RDPMC_EXITING (1ULL << 11) +#define CPU_BASED_RDTSC_EXITING (1ULL << 12) +#define CPU_BASED_CR8_LOAD_EXITING (1ULL << 19) +#define CPU_BASED_CR8_STORE_EXITING (1ULL << 20) +#define CPU_BASED_MOV_DR_EXITING (1ULL << 23) +#define CPU_BASED_UNCOND_IO_EXITING (1ULL << 24) +#define CPU_BASED_USE_IO_BITMAPS (1ULL << 25) +#define CPU_BASED_PAUSE_EXITING (1ULL << 30) +#define CPU_BASED_ACTIVATE_SECONDARY (1ULL << 31) +#define CPU_BASED_USE_MSR_BITMAPS (1ULL << 28) + +/* ===== Secondary processor-based controls ===== */ +#define SEC_EXEC_ENABLE_EPT (1ULL << 1) +#define SEC_EXEC_ENABLE_VPID (1ULL << 5) +#define SEC_EXEC_UNRESTRICTED_GUEST (1ULL << 7) + +/* ===== VM-exit controls ===== */ +#define VM_EXIT_SAVE_DEBUG_CONTROLS (1ULL << 2) +#define VM_EXIT_HOST_ADDR_SPACE_SIZE (1ULL << 9) +#define VM_EXIT_LOAD_PERF_GLOBAL_CTRL (1ULL << 12) +#define VM_EXIT_ACK_INTR_ON_EXIT (1ULL << 15) +#define VM_EXIT_SAVE_GUEST_EFER (1ULL << 20) +#define VM_EXIT_LOAD_HOST_EFER (1ULL << 21) + +/* ===== VM-entry controls ===== */ +#define VM_ENTRY_LOAD_DEBUG_CONTROLS (1ULL << 2) +#define VM_ENTRY_IA32E_MODE_GUEST (1ULL << 9) +#define VM_ENTRY_LOAD_GUEST_EFER (1ULL << 15) + +/* ===== Exit reasons ===== */ +#define EXIT_EXCEPTION_NMI 0 +#define EXIT_EXTERNAL_INTERRUPT 1 +#define EXIT_TRIPLE_FAULT 2 +#define EXIT_INIT_SIGNAL 3 +#define EXIT_CPUID 10 +#define EXIT_INVD 13 +#define EXIT_RDMSR 31 +#define EXIT_WRMSR 32 +#define EXIT_MONITOR 39 +#define EXIT_MWAIT 36 +#define EXIT_VMCALL 18 +#define EXIT_VMCLEAR 19 +#define EXIT_VMLAUNCH 20 +#define EXIT_VMPTRLD 21 +#define EXIT_VMPTRST 22 +#define EXIT_VMREAD 23 +#define EXIT_VMRESUME 24 +#define EXIT_VMWRITE 25 +#define EXIT_VMXOFF 26 +#define EXIT_VMXON 27 +#define EXIT_CONTROL_ACCESS 28 +#define EXIT_IO_INSTRUCTION 30 +#define EXIT_RDPMC 15 +#define EXIT_RDTSC 16 +#define EXIT_HLT 12 +#define EXIT_INVLPG 14 +#define EXIT_EPT_VIOLATION 48 +#define EXIT_EPT_MISCONFIG 49 +#define EXIT_APIC_ACCESS 44 + +/* ===== EPT memory types ===== */ +#define EPT_MEMORY_TYPE_UC 0ULL +#define EPT_MEMORY_TYPE_WC 1ULL +#define EPT_MEMORY_TYPE_WT 4ULL +#define EPT_MEMORY_TYPE_WP 5ULL +#define EPT_MEMORY_TYPE_WB 6ULL + +/* ===== EPT entry bits ===== */ +#define EPT_READ (1ULL << 0) +#define EPT_WRITE (1ULL << 1) +#define EPT_EXECUTE (1ULL << 2) +#define EPT_RWX (EPT_READ | EPT_WRITE | EPT_EXECUTE) +#define EPT_LARGE_PAGE (1ULL << 7) + +/* ===== VMX instruction errors ===== */ +#define VMXERR_VMCALL_INVALID 1 + +/* ===== Public API ===== */ +int vmx_supported(void); +int vmx_enable(void); +int vmx_disable(void); +int vmx_vmcs_alloc(u64 *phys_out); +int vmx_vmcs_load(u64 phys); +u64 vmx_vmcs_read(u64 field); +void vmx_vmcs_write(u64 field, u64 value); +int vmx_vmlaunch(void); +int vmx_vmresume(void); + +u64 vmx_read_msr(u32 msr); +u64 vmx_get_host_cr3(void); + +#endif diff --git a/CODE/UTSM/kernel/main.c b/CODE/UTSM/kernel/main.c index 53fb922..3eabec6 100644 --- a/CODE/UTSM/kernel/main.c +++ b/CODE/UTSM/kernel/main.c @@ -7,6 +7,7 @@ #include #include #include +#include void arch_halt_forever(void); @@ -43,6 +44,49 @@ void kernel_main(void) { int result = utsm_selftest_run(); if (result == 0) { log_info("[UTSM] SELFTEST PASS"); + + /* Phase 1.1: VMM (VMX/EPT) 初始化 + 自检 + * 若 VMX 不可用(如 QEMU 未启用),仅警告,不阻断 DSK 启动 */ + if (vmm_init() == 0) { + log_info("[UTSM] VMM init ok"); + int vmm_st = vmm_self_test(); + if (vmm_st == 0) { + log_info("[UTSM] VMM self-test PASS"); + + /* Phase 1.2: Linux 加载器 + 启动 + * 若 bzImage 模块存在,加载并启动 Linux guest。 + * 若无 bzImage,跳过 Linux 启动,继续 DSK 流程。 */ + if (linux_loader_init() == 0) { + log_info("[UTSM] Linux loader init ok"); + + /* Phase 1.3: Initialize IPC shared memory before + * launching Linux. Allocates 1MB, initializes ring + * buffers, and EPT-maps at GPA 0x04000000 so the + * Linux guest can access it via UTSM_HCALL_SHM_INFO. */ + if (ipc_shm_init() == 0) { + log_info("[UTSM] IPC shm init ok"); + } else { + log_error("[UTSM] IPC shm init failed (non-fatal)"); + } + + int lin_st = linux_launch(); + if (lin_st == 0) { + log_info("[UTSM] Linux guest terminated"); + } else { + log_error("[UTSM] Linux launch failed"); + log_hex64("[UTSM] Linux st=", (u64)(i64)lin_st); + } + } else { + log_warn("[UTSM] Linux loader unavailable (no bzImage)"); + } + } else { + log_error("[UTSM] VMM self-test FAIL"); + log_hex64("[UTSM] VMM st=", (u64)(i64)vmm_st); + } + } else { + log_warn("[UTSM] VMM unavailable (VMX not supported or disabled)"); + } + if (dsk_load_and_jump() != 0) { log_error("[UTSM] DSK jump failed"); } diff --git a/CODE/UTSM/kernel/main.o b/CODE/UTSM/kernel/main.o index 84e874674cedccabad51796695c82a9b1dcef97e..0c4c444a060697767941f94ffba1776e7839c071 100644 GIT binary patch literal 4464 zcmcInO^6&t6s}~AW;I5Q(Ii5!Q4gEL&i*93$%>I(OyX|bjlszVLyS#LclAtX`)BCs z*=$raB0-~~f)^1z2r7smf(Jb)M2rT!2;xQ1n}SQcXh1y#*7v&Ko381d(u4S5rr!JN ztM9$4{+TyV?VmlccFh{Wwnp45iilAmhHohR$;_P;{lJ``KKFC=@Gpo=U%ZGz=KnkJ zkL>?xvM8)KaNskpBv>-ZhqIxj1trV;(-1kTJSC3eS zr>a%n$I-dDd}YBysHFmLFjcLzi#+Q`?IkOSrK_m2V%5yy`>a+YEQ@u)p7aCdS_4rW z4SF(_!457XSFX1)W2%zSk7TOc?%6=LBd1~IBHdH1bZ<96HMS$U2svdfP?*)KJvI}{ zy6U9u-s*v=nOT;}W(rjdyTp#pJ#0mBYPH(UW}HB+R-9O_-@;UGM}J@S0qbNtY?g}d zT!}2BZ~__Ctwoiz{5Y~kM=K+haciKFrp?yfLql~xZM16@Ck}^#U@`w1Y$owZ<)oJ9 z?2^*mT^n<2J3Y_aH9F#Y6LMmF=kD?0vD$!F!5Q3*N+)=?kVvnD4sI#4`}#$ z@Hy`s;hgtDiXYPWdi}>r{2IlN8u*Te_k+)QPZG{~@w%en{fUO>qvu`B(BgW%N%7+} zmicEi{95oi@3Vw+-j67LQRC}*UoG*!qWCofpX;FVJ>kp9rvc|tjkdMze^S74x z+o;a%8h^9KzeB@s(eNE59h-DUHNKvAuZG{M>Fg`%Y^Hcduznf^duPyN5eH|<5 zd`oeygjXo8(}+5{exTv_4rKjPB^^oe6B=LN@7WUnAjQvXd_C{W8oo`-`$kFUU5ek* z_nRG_YlNR zDO6D^ptZKK1s1+b4z$iW5?~{nEmefHQ_#xTm?CrB1#q3877s#(T zq5}U%QdxuXbciEnY^bI2U2xj53%xlKMk*t4{r{`Y!*%WJA@U)z+qq)qipP z%|{B3x=m8R{U^_rdnsIt_tYd4Xlzh}GEKtx+l;Rc9MQP%h(Q1~Ml-bdT1W4h7ii=e zDZb&%sM6h#;bfl)VVWi=IS55J9m8k3yX{`%=OVc{B6P-#3BTF1_Sy6?Mgk z$A;s$6uWOt3NXkC8q>x=Q+q(C{qduua1EfnoI>;k%lg*^gGdf4f~N!0jw&Hhe=p z1nXJuj>yV9TCr0>;*zLw4@o3uWbq|)Jg{}nCTY%5m_dJ<{NvEpxqDi6B{QoM*J$R2 zp6peLFHts-NH7}s62^I824_uTiNHty2KhH3Ft~eHb|s7(60g$CxF@?Q@dL^}lt?g& z{)F)n>>$fiVtEduIqt5fDvPPg7jQ1ePrXWI%~bZCj3>J}tQ+{y6aOXgN80L>L}Duo zU$WH~u=Cvgk;>Ys>~EOK^PJ)>g$scfg)7i0P8F_4%gxz(s~#zH4LCAzZ+|0PYK054 z&7j^=yw+kAdMD4GA774wYYq4_P=0)xJ6vI0kgl*F~ye3ve zd5ae=M6`%YlC6p(f+#Nd7B_tP_v1K`8xkm^(6+dGMTJ$XtbNB~2EI9{zE{x=Rh7_n9FD*pkRH`0{= diff --git a/CODE/UTSM/vmm/ept.c b/CODE/UTSM/vmm/ept.c new file mode 100644 index 0000000..197e634 --- /dev/null +++ b/CODE/UTSM/vmm/ept.c @@ -0,0 +1,168 @@ +#include +#include +#include +#include +#include +#include "../arch/x86_64/limine.h" + +extern volatile struct limine_hhdm_request g_hhdm_request; + +/* EPT 4 级页表:PML4[512] -> PDPT[512] -> PD[512] -> PT[512] -> 4KB 页 + * 虚拟地址(GPA)拆分: + * bits [51:48] reserved + * bits [47:39] PML4 index + * bits [38:30] PDPT index + * bits [29:21] PD index + * bits [20:12] PT index + * bits [11:0] page offset + * + * EPT entry 格式(4KB 页): + * bits [2:0] R/W/X + * bits [5:3] memory type + * bit 6 ignore PAT + * bit 7 large page + * bits [11:8] ignored + * bits [51:12] physical address (page-aligned) + */ + +static u64 g_ept_pml4_phys; +static u64 g_ept_pml4_virt; +static int g_ept_ready; + +/* 物理地址 → 虚拟地址(HHDM 直映射) */ +static void *phys_to_virt(u64 phys) { + if (!g_hhdm_request.response) return (void *)0; + u64 hhdm = g_hhdm_request.response->offset; + return (void *)(hhdm + phys); +} + +/* 分配一页 4KB 对齐物理页,返回物理地址;置零。 */ +static u64 ept_alloc_page(void) { + dkm_dma_buffer buf; + if (dma_alloc_pages(1, EPT_PAGE_SIZE, 0x100000000ULL, &buf) != 0) { + log_error("[EPT] alloc page failed"); + return 0; + } + /* dma_alloc_pages 已清零 */ + return buf.phys; +} + +int ept_init(void) { + g_ept_pml4_phys = ept_alloc_page(); + if (g_ept_pml4_phys == 0) { + log_error("[EPT] failed to alloc PML4"); + return -1; + } + g_ept_pml4_virt = (u64)phys_to_virt(g_ept_pml4_phys); + g_ept_ready = 1; + log_hex64("[EPT] PML4 phys=", g_ept_pml4_phys); + log_info("[EPT] init ok"); + return 0; +} + +/* 读取 EPT 表项指针,必要时分配中间页表。 + * level: 0=PT, 1=PD, 2=PDPT, 3=PML4 */ +static u64 *ept_walk(u64 gpa, int alloc_missing) { + u64 *table = (u64 *)g_ept_pml4_virt; + u64 indices[4]; + indices[3] = (gpa >> 39) & 0x1FF; + indices[2] = (gpa >> 30) & 0x1FF; + indices[1] = (gpa >> 21) & 0x1FF; + indices[0] = (gpa >> 12) & 0x1FF; + + for (int level = 3; level > 0; level--) { + u64 entry = table[indices[level]]; + if (!(entry & EPT_READ)) { + if (!alloc_missing) return (u64 *)0; + u64 new_phys = ept_alloc_page(); + if (new_phys == 0) return (u64 *)0; + /* 中间页表项:RWX + WB 内存类型 */ + table[indices[level]] = new_phys | EPT_RWX | (EPT_MEMORY_TYPE_WB << 3); + /* 切换到新分配的下一级页表 */ + table = (u64 *)phys_to_virt(new_phys); + } else { + /* 清除标志位取出物理地址 */ + u64 child_phys = entry & 0x000FFFFFFFFFF000ULL; + table = (u64 *)phys_to_virt(child_phys); + } + } + return &table[indices[0]]; +} + +int ept_map_range(u64 gpa, u64 hpa, u64 size, u64 flags) { + if (!g_ept_ready) return -1; + if ((gpa & (EPT_PAGE_SIZE - 1)) || (hpa & (EPT_PAGE_SIZE - 1)) || + (size & (EPT_PAGE_SIZE - 1))) { + log_error("[EPT] unaligned map request"); + return -2; + } + + u64 pages = size / EPT_PAGE_SIZE; + for (u64 i = 0; i < pages; i++) { + u64 cur_gpa = gpa + i * EPT_PAGE_SIZE; + u64 cur_hpa = hpa + i * EPT_PAGE_SIZE; + u64 *entry = ept_walk(cur_gpa, 1); + if (!entry) { + log_error("[EPT] walk failed"); + return -3; + } + *entry = cur_hpa | flags | (EPT_MEMORY_TYPE_WB << 3); + } + return 0; +} + +int ept_identity_map(u64 gpa, u64 size, u64 flags) { + return ept_map_range(gpa, gpa, size, flags); +} + +/* Walk the EPT to translate a GPA to its mapped HPA. + * Returns 0 if the GPA is not mapped (no present entry at any level). + * Handles 4KB pages and 2MB large pages (bit 7 = PS in PD entry). */ +u64 ept_gpa_to_hpa(u64 gpa) { + if (!g_ept_ready) return 0; + + u64 *table = (u64 *)g_ept_pml4_virt; + u64 pml4_idx = (gpa >> 39) & 0x1FF; + u64 pdpt_idx = (gpa >> 30) & 0x1FF; + u64 pd_idx = (gpa >> 21) & 0x1FF; + u64 pt_idx = (gpa >> 12) & 0x1FF; + u64 offset = gpa & 0xFFF; + + /* Level 3: PML4 */ + u64 entry = table[pml4_idx]; + if (!(entry & EPT_READ)) return 0; + table = (u64 *)phys_to_virt(entry & 0x000FFFFFFFFFF000ULL); + + /* Level 2: PDPT */ + entry = table[pdpt_idx]; + if (!(entry & EPT_READ)) return 0; + /* 1GB large page (bit 7 set in PDPT entry) */ + if (entry & EPT_LARGE_PAGE) { + u64 hpa = entry & 0x000FFFFFC0000000ULL; /* 1GB page base */ + return hpa + (gpa & 0x3FFFFFFF); /* offset within 1GB */ + } + table = (u64 *)phys_to_virt(entry & 0x000FFFFFFFFFF000ULL); + + /* Level 1: PD */ + entry = table[pd_idx]; + if (!(entry & EPT_READ)) return 0; + /* 2MB large page (bit 7 set in PD entry) */ + if (entry & EPT_LARGE_PAGE) { + u64 hpa = entry & 0x000FFFFFFFE00000ULL; /* 2MB page base */ + return hpa + (gpa & 0x1FFFFF); /* offset within 2MB */ + } + table = (u64 *)phys_to_virt(entry & 0x000FFFFFFFFFF000ULL); + + /* Level 0: PT */ + entry = table[pt_idx]; + if (!(entry & EPT_READ)) return 0; + u64 hpa = entry & 0x000FFFFFFFFFF000ULL; + return hpa + offset; +} + +u64 ept_get_eptp(void) { + if (!g_ept_ready) return 0; + /* EPTP: bits [5:0] = memtype, bits [7:6] = walk length-1 (3 for 4-level), + * bits [51:12] = PML4 HPA */ + return g_ept_pml4_phys | EPTP_WALK_LEN_4 | EPTP_MEMTYPE_WB; +} diff --git a/CODE/UTSM/vmm/hypercall.c b/CODE/UTSM/vmm/hypercall.c new file mode 100644 index 0000000..afc80f9 --- /dev/null +++ b/CODE/UTSM/vmm/hypercall.c @@ -0,0 +1,247 @@ +/* hypercall.c — UTSM-side VMCALL hypercall handler. + * + * When the Linux guest executes a VMCALL instruction, it triggers a VM-Exit + * with exit reason EXIT_VCALL (18). The guest places: + * RAX = UTSM_HCALL_MAGIC | op (identifies this as a UTSM hypercall) + * RDI = arg0 + * RSI = arg1 + * RDX = arg2 + * + * The VM-Exit assembly stub saves all guest GPRs into g_guest_regs before + * calling the C dispatcher. This handler reads arguments from g_guest_regs, + * dispatches to the operation handler, writes the return value into + * g_guest_regs.rax, and advances GUEST_RIP so the guest resumes after + * the VMCALL instruction. + * + * Protocol details in CODE/utsm-ipc/ipc_proto.h. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "../arch/x86_64/limine.h" + +extern volatile struct limine_hhdm_request g_hhdm_request; + +static u64 g_hypercall_count; + +/* ===== Guest memory access helpers ===== + * + * The Linux guest uses identity-mapped page tables (GVA == GPA), so + * pointers passed via hypercall arguments are GPAs. We translate GPA→HPA + * via EPT walk, then add HHDM offset to get the host virtual address. */ + +static void *gpa_to_hva(u64 gpa) { + u64 hpa = ept_gpa_to_hpa(gpa); + if (hpa == 0) return (void *)0; + if (!g_hhdm_request.response) return (void *)0; + u64 hhdm = g_hhdm_request.response->offset; + return (void *)(hhdm + hpa); +} + +/* Write a u64 to guest memory at the given GPA. + * Returns 0 on success, -1 on failure. */ +static int guest_write_u64(u64 gpa, u64 value) { + void *hva = gpa_to_hva(gpa); + if (!hva) return -1; + *(volatile u64 *)hva = value; + return 0; +} + +/* Copy bytes from guest memory (GVA=GPA) to host buffer. + * Handles cross-page boundaries by translating each page separately. + * Returns 0 on success, -1 on failure. */ +static int guest_read_buf(u64 gpa, void *host_dst, u64 len) { + u8 *dst = (u8 *)host_dst; + u64 copied = 0; + while (copied < len) { + void *hva = gpa_to_hva(gpa + copied); + if (!hva) return -1; + u64 page_end = (gpa + copied + 4096) & ~0xFFFULL; + u64 chunk = page_end - (gpa + copied); + if (chunk > len - copied) chunk = len - copied; + const u8 *src = (const u8 *)hva; + for (u64 i = 0; i < chunk; i++) { + dst[copied + i] = src[i]; + } + copied += chunk; + } + return 0; +} + +/* ===== Individual hypercall handlers ===== */ + +static long hcall_ping(u64 a0, u64 a1, u64 a2) { + (void)a0; (void)a1; (void)a2; + /* Return a recognizable PONG value */ + return 0x504F4E47; /* "PONG" */ +} + +static long hcall_hello(u64 a0, u64 a1, u64 a2) { + (void)a0; (void)a1; (void)a2; + log_info("[HCALL] HELLO from Linux guest"); + + /* Linux is announcing itself; the IPC shared memory's linux_ready + * flag is set separately when Linux maps the shared memory. */ + if (ipc_shm_is_ready()) { + /* Send a HELLO_ACK message via the shared memory ring */ + const char *msg = "HELLO from UTSM"; + ipc_shm_send(UTSM_MSG_HELLO_ACK, msg, 15); + } + return UTSM_HCALL_OK; +} + +static long hcall_shm_info(u64 a0, u64 a1, u64 a2) { + /* a0 = GPA of gpa_out (u64), a1 = GPA of size_out (u64) */ + (void)a2; + if (!ipc_shm_is_ready()) { + return UTSM_HCALL_NOMEM; + } + + u64 shm_gpa = ipc_shm_get_gpa(); + u64 shm_size = ipc_shm_get_size(); + + /* Write results to guest memory */ + if (a0 != 0 && guest_write_u64(a0, shm_gpa) != 0) { + log_error("[HCALL] SHM_INFO: failed to write gpa to guest"); + return UTSM_HCALL_INVAL; + } + if (a1 != 0 && guest_write_u64(a1, shm_size) != 0) { + log_error("[HCALL] SHM_INFO: failed to write size to guest"); + return UTSM_HCALL_INVAL; + } + + log_hex64("[HCALL] SHM_INFO: gpa=", shm_gpa); + log_hex64("[HCALL] SHM_INFO: size=", shm_size); + return UTSM_HCALL_OK; +} + +static long hcall_cap_validate(u64 a0, u64 a1, u64 a2) { + /* Phase 1.3: stub — capability validation not yet implemented */ + (void)a0; (void)a1; (void)a2; + log_info("[HCALL] CAP_VALIDATE (stub)"); + return UTSM_HCALL_NOSYS; +} + +static long hcall_utrw_read(u64 a0, u64 a1, u64 a2) { + /* Phase 1.3: stub — sealed memory read not yet implemented */ + (void)a0; (void)a1; (void)a2; + log_info("[HCALL] UTRW_READ (stub)"); + return UTSM_HCALL_NOSYS; +} + +static long hcall_utrw_write(u64 a0, u64 a1, u64 a2) { + /* Phase 1.3: stub — sealed memory write not yet implemented */ + (void)a0; (void)a1; (void)a2; + log_info("[HCALL] UTRW_WRITE (stub)"); + return UTSM_HCALL_NOSYS; +} + +static long hcall_drr_checkpoint(u64 a0, u64 a1, u64 a2) { + /* Phase 1.3: stub — DRR checkpoint trigger not yet implemented */ + (void)a0; (void)a1; (void)a2; + log_info("[HCALL] DRR_CHECKPOINT (stub)"); + return UTSM_HCALL_NOSYS; +} + +static long hcall_console_write(u64 a0, u64 a1, u64 a2) { + /* a0 = GPA of string buffer, a1 = length */ + (void)a2; + if (a0 == 0 || a1 == 0) { + return UTSM_HCALL_INVAL; + } + + /* Cap length to prevent runaway reads */ + if (a1 > 4096) a1 = 4096; + + /* Read the string from guest memory */ + char buf[4097]; + if (guest_read_buf(a0, buf, a1) != 0) { + log_error("[HCALL] CONSOLE_WRITE: failed to read guest buffer"); + return UTSM_HCALL_INVAL; + } + buf[a1] = '\0'; + + /* Output to UTSM serial console with a [LINUX] prefix */ + serial_write("[LINUX] "); + serial_write(buf); + return (long)a1; +} + +static long hcall_console_read(u64 a0, u64 a1, u64 a2) { + /* Phase 1.3: stub — console read not yet implemented */ + (void)a0; (void)a1; (void)a2; + return UTSM_HCALL_NOSYS; +} + +/* ===== Main hypercall dispatcher ===== */ + +int hypercall_handle(u64 guest_rax, u64 guest_rdi, u64 guest_rsi, u64 guest_rdx, + u64 guest_rip, u64 instr_len) { + g_hypercall_count++; + + /* Validate magic: the high bits of RAX must be UTSM_HCALL_MAGIC */ + if ((guest_rax & ~0xFFFFULL) != UTSM_HCALL_MAGIC) { + log_hex64("[HCALL] bad magic: ", guest_rax); + /* Return error in guest RAX */ + g_guest_regs.rax = (u64)UTSM_HCALL_INVAL; + /* Still advance RIP to avoid infinite loop */ + vmx_vmcs_write(VMCS_GUEST_RIP, guest_rip + instr_len); + return 1; /* resume guest */ + } + + /* Extract operation from low 16 bits */ + u64 op = guest_rax & 0xFFFFULL; + long result = UTSM_HCALL_NOSYS; + + switch (op) { + case UTSM_HCALL_PING: + result = hcall_ping(guest_rdi, guest_rsi, guest_rdx); + break; + case UTSM_HCALL_HELLO: + result = hcall_hello(guest_rdi, guest_rsi, guest_rdx); + break; + case UTSM_HCALL_SHM_INFO: + result = hcall_shm_info(guest_rdi, guest_rsi, guest_rdx); + break; + case UTSM_HCALL_CAP_VALIDATE: + result = hcall_cap_validate(guest_rdi, guest_rsi, guest_rdx); + break; + case UTSM_HCALL_UTRW_READ: + result = hcall_utrw_read(guest_rdi, guest_rsi, guest_rdx); + break; + case UTSM_HCALL_UTRW_WRITE: + result = hcall_utrw_write(guest_rdi, guest_rsi, guest_rdx); + break; + case UTSM_HCALL_DRR_CHECKPOINT: + result = hcall_drr_checkpoint(guest_rdi, guest_rsi, guest_rdx); + break; + case UTSM_HCALL_CONSOLE_WRITE: + result = hcall_console_write(guest_rdi, guest_rsi, guest_rdx); + break; + case UTSM_HCALL_CONSOLE_READ: + result = hcall_console_read(guest_rdi, guest_rsi, guest_rdx); + break; + default: + log_hex64("[HCALL] unknown op: ", op); + result = UTSM_HCALL_NOSYS; + break; + } + + /* Write return value into guest RAX (restored by assembly before vmresume) */ + g_guest_regs.rax = (u64)result; + + /* Advance guest RIP past the VMCALL instruction */ + vmx_vmcs_write(VMCS_GUEST_RIP, guest_rip + instr_len); + + return 1; /* resume guest */ +} + +u64 hypercall_get_count(void) { + return g_hypercall_count; +} diff --git a/CODE/UTSM/vmm/ipc_shm.c b/CODE/UTSM/vmm/ipc_shm.c new file mode 100644 index 0000000..2e90381 --- /dev/null +++ b/CODE/UTSM/vmm/ipc_shm.c @@ -0,0 +1,208 @@ +/* ipc_shm.c — UTSM-side IPC shared memory manager. + * + * Allocates a 1MB contiguous physical region, initializes the shared + * utsm_ipc_shm structure (header + two ring buffers + payload pool), + * and EPT-maps it at LINUX_GUEST_IPC_SHM_GPA (0x04000000) so the Linux + * guest can access it after querying the GPA via UTSM_HCALL_SHM_INFO. + * + * Ring buffer discipline (single-producer, single-consumer): + * utsm_to_linux: UTSM produces via ipc_shm_send, Linux consumes + * linux_to_utsm: Linux produces, UTSM consumes via ipc_shm_recv + * + * Memory barriers ensure correct visibility across the two kernels + * (which share the same physical memory via EPT). + */ + +#include +#include +#include +#include +#include +#include +#include +#include "../arch/x86_64/limine.h" + +extern volatile struct limine_hhdm_request g_hhdm_request; + +/* ===== Internal state ===== */ + +static dkm_dma_buffer g_shm_buf; /* allocated DMA buffer for the SHM region */ +static struct utsm_ipc_shm *g_shm; /* virtual address (HHDM) of the SHM region */ +static int g_shm_ready; +static u32 g_send_seq; /* monotonic sequence for UTSM→Linux messages */ +static u64 g_send_count; +static u64 g_recv_count; + +/* ===== Initialization ===== */ + +int ipc_shm_init(void) { + if (g_shm_ready) { + log_warn("[IPC] shm already initialized"); + return 0; + } + + log_info("[IPC] shm init begin"); + + /* 1. Allocate 1MB contiguous physical memory for the shared region. + * Alignment: 4KB (page-aligned). Max phys: none (EPT maps any HPA). */ + u64 page_count = UTSM_IPC_SHM_SIZE / 4096; + if (dma_alloc_pages(page_count, 4096, 0, &g_shm_buf) != 0) { + log_error("[IPC] failed to alloc shared memory (1MB)"); + return -1; + } + log_hex64("[IPC] shm HPA=", g_shm_buf.phys); + log_hex64("[IPC] shm virt=", (u64)g_shm_buf.virt); + log_hex64("[IPC] shm size=", g_shm_buf.size); + + /* 2. Zero the entire region */ + u8 *p = (u8 *)g_shm_buf.virt; + for (u64 i = 0; i < g_shm_buf.size; i++) { + p[i] = 0; + } + + /* 3. Initialize the shared memory structure */ + g_shm = (struct utsm_ipc_shm *)g_shm_buf.virt; + + /* Header */ + g_shm->header.magic = UTSM_IPC_MAGIC; + g_shm->header.version = UTSM_IPC_VERSION; + g_shm->header.header_size = sizeof(struct utsm_ipc_shm_header); + g_shm->header.shm_phys = LINUX_GUEST_IPC_SHM_GPA; /* GPA as seen by Linux */ + g_shm->header.shm_size = UTSM_IPC_SHM_SIZE; + g_shm->header.utsm_ready = 1; /* UTSM is ready now */ + g_shm->header.linux_ready = 0; /* Linux will set this when it connects */ + + /* Ring buffers */ + utsm_ipc_ring_init(&g_shm->utsm_to_linux); + utsm_ipc_ring_init(&g_shm->linux_to_utsm); + + /* Memory barrier: ensure all writes are visible before EPT mapping */ + utsm_ipc_mb(); + + /* 4. EPT-map the shared memory at the guest-visible GPA. + * Both kernels need read/write access. Execute is not needed. */ + if (ept_map_range(LINUX_GUEST_IPC_SHM_GPA, g_shm_buf.phys, + UTSM_IPC_SHM_SIZE, EPT_READ | EPT_WRITE) != 0) { + log_error("[IPC] EPT map shared memory failed"); + return -2; + } + log_hex64("[IPC] shm mapped GPA=", LINUX_GUEST_IPC_SHM_GPA); + + g_shm_ready = 1; + g_send_seq = 0; + g_send_count = 0; + g_recv_count = 0; + + log_info("[IPC] shm init ok"); + return 0; +} + +void ipc_shm_shutdown(void) { + if (!g_shm_ready) return; + g_shm->header.utsm_ready = 0; + utsm_ipc_mb(); + g_shm_ready = 0; + log_info("[IPC] shm shutdown"); +} + +int ipc_shm_is_ready(void) { + return g_shm_ready; +} + +u64 ipc_shm_get_hpa(void) { + return g_shm_ready ? g_shm_buf.phys : 0; +} + +u64 ipc_shm_get_gpa(void) { + return LINUX_GUEST_IPC_SHM_GPA; +} + +u64 ipc_shm_get_size(void) { + return UTSM_IPC_SHM_SIZE; +} + +/* ===== Message API (UTSM-side) ===== */ + +int ipc_shm_send(u32 msg_type, const void *data, u32 data_len) { + if (!g_shm_ready || !g_shm) { + return -1; + } + + /* Check if Linux is ready to receive */ + if (!g_shm->header.linux_ready) { + log_warn("[IPC] send: Linux not ready, dropping message"); + return -1; + } + + /* Cap data length to inline buffer size */ + if (data_len > UTSM_IPC_MSG_DATA_SIZE) { + data_len = UTSM_IPC_MSG_DATA_SIZE; + } + + /* Build the message */ + struct utsm_ipc_msg msg; + msg.type = msg_type; + msg.seq = g_send_seq++; + msg.data_len = data_len; + msg.flags = 0; + + /* Copy data if present */ + if (data && data_len > 0) { + const u8 *src = (const u8 *)data; + for (u32 i = 0; i < data_len; i++) { + msg.data[i] = src[i]; + } + } + + /* Enqueue into utsm_to_linux ring */ + if (utsm_ipc_ring_push(&g_shm->utsm_to_linux, &msg) != 0) { + log_warn("[IPC] send: ring full, dropping message"); + return -1; + } + + g_send_count++; + return 0; +} + +int ipc_shm_recv(struct utsm_ipc_msg *msg_out) { + if (!g_shm_ready || !g_shm || !msg_out) { + return -1; + } + + if (utsm_ipc_ring_pop(&g_shm->linux_to_utsm, msg_out) != 0) { + return -1; /* empty */ + } + + g_recv_count++; + return 0; +} + +int ipc_shm_has_pending(void) { + if (!g_shm_ready || !g_shm) return 0; + return !utsm_ipc_ring_empty(&g_shm->linux_to_utsm); +} + +void ipc_shm_notify_linux(void) { + /* Phase 1.3: log only. + * Phase 1.4+: inject a guest IRQ (via VMCS VM-entry interruption info) + * to notify Linux that a message is waiting in the utsm_to_linux ring. */ + log_info("[IPC] notify_linux (IPI injection not yet implemented)"); +} + +/* ===== Debug ===== */ + +void ipc_shm_dump_stats(void) { + if (!g_shm_ready) { + log_info("[IPC] not initialized"); + return; + } + + log_hex64("[IPC] utsm_ready=", g_shm->header.utsm_ready); + log_hex64("[IPC] linux_ready=", g_shm->header.linux_ready); + log_hex64("[IPC] utsm_to_linux head=", g_shm->utsm_to_linux.head); + log_hex64("[IPC] utsm_to_linux tail=", g_shm->utsm_to_linux.tail); + log_hex64("[IPC] linux_to_utsm head=", g_shm->linux_to_utsm.head); + log_hex64("[IPC] linux_to_utsm tail=", g_shm->linux_to_utsm.tail); + log_hex64("[IPC] send_count=", g_send_count); + log_hex64("[IPC] recv_count=", g_recv_count); +} diff --git a/CODE/UTSM/vmm/linux_boot.c b/CODE/UTSM/vmm/linux_boot.c new file mode 100644 index 0000000..b880620 --- /dev/null +++ b/CODE/UTSM/vmm/linux_boot.c @@ -0,0 +1,343 @@ +/* linux_boot.c — VMCS configuration and vmlaunch for Linux 6.6 guest. + * + * Configures the VMCS guest state for 64-bit Linux entry (startup_64): + * - RIP = kernel 64-bit entry point + * - RSI = boot_params GPA (Linux calling convention) + * - RSP = guest stack top + * - CR3 = guest page tables GPA (identity-mapped 4GB) + * - GDT = guest GDT GPA (64-bit code/data/TSS) + * - EFER.LME | EFER.LMA | EFER.NXE + * + * After vmlaunch, the VM-Exit handler (vmexit.c) processes exits and + * resumes the guest. + */ + +#include +#include +#include +#include +#include +#include +#include "../arch/x86_64/limine.h" + +extern volatile struct limine_hhdm_request g_hhdm_request; + +/* VM-Exit assembly entry and C dispatcher (from vmm.c / vmexit_asm.S) */ +extern void vmx_vm_exit_handler(void); +extern int vmexit_dispatch(void); +extern u64 vmexit_get_count(void); + +/* Globals shared with vmexit_asm.S (must match vmm.c) */ +extern volatile u64 g_saved_host_rsp; +extern volatile u64 g_saved_return_rip; +extern volatile int g_guest_terminated; +extern volatile u64 g_last_exit_reason; + +/* Host stack for VM-Exit (reuse the one from vmm.c) */ +extern u8 g_host_stack[8192]; + +/* CR helpers */ +static inline u64 read_cr0_local(void) { + u64 v; + __asm__ volatile("mov %%cr0, %0" : "=r"(v)); + return v; +} +static inline u64 read_cr3_local(void) { + u64 v; + __asm__ volatile("mov %%cr3, %0" : "=r"(v)); + return v; +} +static inline u64 read_cr4_local(void) { + u64 v; + __asm__ volatile("mov %%cr4, %0" : "=r"(v)); + return v; +} + +static u64 vmx_adjust_control(u64 value, u32 msr) { + u32 allowed0 = (u32)vmx_read_msr(msr); + u32 allowed1 = (u32)(vmx_read_msr(msr) >> 32); + u32 adjusted = (u32)value | allowed0; + adjusted &= allowed1; + return adjusted; +} + +/* MSR bitmap (4KB, all zeros = don't intercept any MSR) */ +static u8 g_linux_msr_bitmap[4096] __attribute__((aligned(4096))); + +/* ===== VMCS host state (same as vmm.c, but for Linux guest) ===== */ + +static void linux_vmcs_setup_host_state(void) { + vmx_vmcs_write(VMCS_HOST_CR0, read_cr0_local() | CR0_PE | CR0_NE | CR0_PG); + vmx_vmcs_write(VMCS_HOST_CR3, read_cr3_local()); + vmx_vmcs_write(VMCS_HOST_CR4, read_cr4_local() | CR4_VMXE); + + /* Host segment selectors: UTSM GDT 0x08 (code) / 0x10 (data) */ + vmx_vmcs_write(VMCS_HOST_CS_SELECTOR, 0x08); + vmx_vmcs_write(VMCS_HOST_SS_SELECTOR, 0x10); + vmx_vmcs_write(VMCS_HOST_DS_SELECTOR, 0x10); + vmx_vmcs_write(VMCS_HOST_ES_SELECTOR, 0x10); + vmx_vmcs_write(VMCS_HOST_FS_SELECTOR, 0); + vmx_vmcs_write(VMCS_HOST_GS_SELECTOR, 0); + vmx_vmcs_write(VMCS_HOST_FS_BASE, 0); + vmx_vmcs_write(VMCS_HOST_GS_BASE, 0); + + /* Host GDT/IDT + TR */ + { + struct __attribute__((packed)) { u16 limit; u64 base; } gdtr, idtr; + __asm__ volatile("sgdt %0" : "=m"(gdtr)); + __asm__ volatile("sidt %0" : "=m"(idtr)); + vmx_vmcs_write(VMCS_HOST_GDTR_BASE, gdtr.base); + vmx_vmcs_write(VMCS_HOST_IDTR_BASE, idtr.base); + + u16 tr_sel; + u64 tr_base; + __asm__ volatile("str %0" : "=r"(tr_sel)); + u64 gdt_base = gdtr.base; + u64 *tss_desc = (u64 *)(gdt_base + (tr_sel & 0xFFF8)); + u64 low = tss_desc[0]; + u64 high = tss_desc[1]; + tr_base = (low >> 16) & 0xFFFFFF; + tr_base |= ((low >> 32) & 0xFF) << 24; + tr_base |= (high & 0xFFFFFFFF) << 32; + vmx_vmcs_write(VMCS_HOST_TR_SELECTOR, tr_sel); + vmx_vmcs_write(VMCS_HOST_TR_BASE, tr_base); + } + + /* Host SYSENTER */ + { + u64 sysenter_cs; + __asm__ volatile("rdmsr" : "=a"(sysenter_cs) : "c"(0x174)); + vmx_vmcs_write(VMCS_HOST_SYSENTER_CS, sysenter_cs & 0xFFFFFFFF); + vmx_vmcs_write(VMCS_HOST_SYSENTER_ESP, 0); + vmx_vmcs_write(VMCS_HOST_SYSENTER_EIP, 0); + } + + /* Host EFER */ + { + u32 low, high; + __asm__ volatile("rdmsr" : "=a"(low), "=d"(high) : "c"(0xC0000080)); + u64 host_efer = ((u64)high << 32) | low; + vmx_vmcs_write(VMCS_HOST_IA32_EFER, host_efer); + } + + /* Host RSP/RIP: VM-Exit handler */ + vmx_vmcs_write(VMCS_HOST_RSP, (u64)&g_host_stack[sizeof(g_host_stack) - 16]); + vmx_vmcs_write(VMCS_HOST_RIP, (u64)vmx_vm_exit_handler); +} + +/* ===== VMCS guest state for Linux ===== */ + +static void linux_vmcs_setup_guest_state(const struct linux_guest_info *gi) { + /* CR0/CR3/CR4: long mode with paging */ + vmx_vmcs_write(VMCS_GUEST_CR0, CR0_PE | CR0_NE | CR0_PG | CR0_WP); + /* CR3 points to guest PML4 (GPA) */ + vmx_vmcs_write(VMCS_GUEST_CR3, gi->pgt_gpa); + vmx_vmcs_write(VMCS_GUEST_CR4, CR4_PAE | CR4_PGE | CR4_PSE); + + /* Segment selectors from guest GDT */ + vmx_vmcs_write(VMCS_GUEST_CS_SELECTOR, 0x08); + vmx_vmcs_write(VMCS_GUEST_SS_SELECTOR, 0x10); + vmx_vmcs_write(VMCS_GUEST_DS_SELECTOR, 0x10); + vmx_vmcs_write(VMCS_GUEST_ES_SELECTOR, 0x10); + vmx_vmcs_write(VMCS_GUEST_FS_SELECTOR, 0); + vmx_vmcs_write(VMCS_GUEST_GS_SELECTOR, 0); + vmx_vmcs_write(VMCS_GUEST_LDTR_SELECTOR, 0); + vmx_vmcs_write(VMCS_GUEST_TR_SELECTOR, 0x18); + + /* Segment bases: flat model, TR points to TSS in GDT page */ + vmx_vmcs_write(VMCS_GUEST_CS_BASE, 0); + vmx_vmcs_write(VMCS_GUEST_SS_BASE, 0); + vmx_vmcs_write(VMCS_GUEST_DS_BASE, 0); + vmx_vmcs_write(VMCS_GUEST_ES_BASE, 0); + vmx_vmcs_write(VMCS_GUEST_FS_BASE, 0); + vmx_vmcs_write(VMCS_GUEST_GS_BASE, 0); + vmx_vmcs_write(VMCS_GUEST_LDTR_BASE, 0); + vmx_vmcs_write(VMCS_GUEST_TR_BASE, gi->gdt_gpa + 0x100); + vmx_vmcs_write(VMCS_GUEST_GDTR_BASE, gi->gdt_gpa); + vmx_vmcs_write(VMCS_GUEST_IDTR_BASE, 0); + + /* Segment limits */ + vmx_vmcs_write(VMCS_GUEST_CS_LIMIT, 0xFFFFFFFF); + vmx_vmcs_write(VMCS_GUEST_SS_LIMIT, 0xFFFFFFFF); + vmx_vmcs_write(VMCS_GUEST_DS_LIMIT, 0xFFFFFFFF); + vmx_vmcs_write(VMCS_GUEST_ES_LIMIT, 0xFFFFFFFF); + vmx_vmcs_write(VMCS_GUEST_FS_LIMIT, 0xFFFFFFFF); + vmx_vmcs_write(VMCS_GUEST_GS_LIMIT, 0xFFFFFFFF); + vmx_vmcs_write(VMCS_GUEST_LDTR_LIMIT, 0xFFFF); + vmx_vmcs_write(VMCS_GUEST_TR_LIMIT, 0x67); + vmx_vmcs_write(VMCS_GUEST_GDTR_LIMIT, 0x28); + vmx_vmcs_write(VMCS_GUEST_IDTR_LIMIT, 0xFFFF); + + /* Access rights */ + vmx_vmcs_write(VMCS_GUEST_CS_ACCESS, 0xA09B); /* 64-bit code, L=1 */ + vmx_vmcs_write(VMCS_GUEST_SS_ACCESS, 0xC093); + vmx_vmcs_write(VMCS_GUEST_DS_ACCESS, 0xC093); + vmx_vmcs_write(VMCS_GUEST_ES_ACCESS, 0xC093); + vmx_vmcs_write(VMCS_GUEST_FS_ACCESS, 0xC093); + vmx_vmcs_write(VMCS_GUEST_GS_ACCESS, 0xC093); + vmx_vmcs_write(VMCS_GUEST_LDTR_ACCESS, 0x82); /* unusable */ + vmx_vmcs_write(VMCS_GUEST_TR_ACCESS, 0x8B); /* busy 64-bit TSS */ + + /* Guest RIP = Linux 64-bit entry (startup_64) */ + vmx_vmcs_write(VMCS_GUEST_RIP, gi->kernel_entry); + + /* Guest RSP = top of stack page */ + vmx_vmcs_write(VMCS_GUEST_RSP, LINUX_GUEST_STACK_GPA + 0x1000); + + /* NOTE: RSI (boot_params address) is NOT a VMCS field. + * VMX does not save/restore GPRs (RAX-R15) in VMCS. + * RSI is set via the inline asm launch stub in linux_launch(). */ + + vmx_vmcs_write(VMCS_GUEST_RFLAGS, 0x2); /* interrupts disabled */ + + /* Guest EFER: long mode active */ + vmx_vmcs_write(VMCS_GUEST_IA32_EFER, EFER_LME | EFER_LMA | EFER_NXE); + + /* Guest SYSENTER (unused but required by VMX) */ + vmx_vmcs_write(VMCS_GUEST_SYSENTER_CS, 0); + vmx_vmcs_write(VMCS_GUEST_SYSENTER_ESP, 0); + vmx_vmcs_write(VMCS_GUEST_SYSENTER_EIP, 0); + + /* Guest DR7, activity state, interruptibility */ + vmx_vmcs_write(VMCS_GUEST_DR7, 0x400); + vmx_vmcs_write(VMCS_GUEST_ACTIVITY_STATE, 0); + vmx_vmcs_write(VMCS_GUEST_INTERRUPTIBILITY, 0); +} + +/* ===== VMCS controls for Linux ===== */ + +static void linux_vmcs_setup_controls(u64 eptp) { + u64 pin = vmx_adjust_control(PIN_EXT_INTERRUPT_EXITING | PIN_NMI_EXITING, + IA32_VMX_TRUE_PINBASED_CTLS); + + /* CPU-based: exit on HLT, INVLPG, MWAIT, IO, use MSR bitmap, + * activate secondary controls. Don't intercept RDTSC (Linux needs it). */ + u64 cpu = CPU_BASED_HLT_EXITING + | CPU_BASED_ACTIVATE_SECONDARY + | CPU_BASED_USE_MSR_BITMAPS + | CPU_BASED_UNCOND_IO_EXITING + | CPU_BASED_INVLPG_EXITING; + cpu = vmx_adjust_control(cpu, IA32_VMX_TRUE_PROCBASED_CTLS); + + /* Secondary: EPT + VPID */ + u64 cpu2 = SEC_EXEC_ENABLE_EPT | SEC_EXEC_ENABLE_VPID; + { + u64 msr = vmx_read_msr(IA32_VMX_PROCBASED_CTLS2); + u32 allowed1 = (u32)(msr >> 32); + cpu2 &= allowed1; + cpu2 |= SEC_EXEC_ENABLE_EPT; + } + + u64 exit_ctrl = VM_EXIT_SAVE_DEBUG_CONTROLS + | VM_EXIT_HOST_ADDR_SPACE_SIZE + | VM_EXIT_LOAD_HOST_EFER; + exit_ctrl = vmx_adjust_control(exit_ctrl, IA32_VMX_TRUE_EXIT_CTLS); + + u64 entry_ctrl = VM_ENTRY_LOAD_DEBUG_CONTROLS + | VM_ENTRY_IA32E_MODE_GUEST + | VM_ENTRY_LOAD_GUEST_EFER; + entry_ctrl = vmx_adjust_control(entry_ctrl, IA32_VMX_TRUE_ENTRY_CTLS); + + vmx_vmcs_write(VMCS_PIN_BASED_VM_EXEC_CONTROL, pin); + vmx_vmcs_write(VMCS_CPU_BASED_VM_EXEC_CONTROL, cpu); + vmx_vmcs_write(VMCS_SECONDARY_VM_EXEC_CONTROL, cpu2); + vmx_vmcs_write(VMCS_VM_EXIT_CONTROLS, exit_ctrl); + vmx_vmcs_write(VMCS_VM_ENTRY_CONTROLS, entry_ctrl); + + /* MSR bitmap */ + { + u64 hhdm = g_hhdm_request.response ? g_hhdm_request.response->offset : 0; + u64 msr_bitmap_phys = (u64)g_linux_msr_bitmap - hhdm; + vmx_vmcs_write(VMCS_MSR_BITMAP, msr_bitmap_phys); + } + + vmx_vmcs_write(VMCS_EPT_POINTER, eptp); + vmx_vmcs_write(VMCS_VPID, 2); /* VPID 1 = self-test, VPID 2 = Linux */ + + /* Exception bitmap: catch #GP, #PF, #UD, #DF (triple fault handled separately) */ + vmx_vmcs_write(VMCS_EXCEPTION_BITMAP, (1ULL << 13) | (1ULL << 14) | (1ULL << 6) | (1ULL << 8)); +} + +/* ===== Global for passing RSI to launch asm ===== */ +static volatile u64 g_linux_rsi; + +/* ===== Linux launch ===== + * + * This function is called after vmm_init() + linux_loader_init(). + * It configures the VMCS for the Linux guest and performs vmlaunch. + * + * NOTE: VMX general registers (RAX-R15) are NOT saved in VMCS. + * We use inline assembly to set RSI = boot_params GPA before vmlaunch. + * + * Returns 0 on successful guest termination, negative on error. + */ +int linux_launch(void) { + const struct linux_guest_info *gi = linux_get_guest_info(); + if (!gi || !gi->loaded) { + log_error("[LINUX] guest not loaded"); + return -1; + } + + if (!vmm_is_ready()) { + log_error("[LINUX] VMM not ready"); + return -2; + } + + log_info("[LINUX] launch begin"); + + /* Reset VM-Exit state */ + g_guest_terminated = 0; + g_last_exit_reason = 0xFFFFFFFFULL; + + /* Configure VMCS */ + linux_vmcs_setup_guest_state(gi); + linux_vmcs_setup_controls(vmm_get_eptp()); + linux_vmcs_setup_host_state(); + + /* Set RSI for Linux boot_params (passed via global, loaded in asm) */ + g_linux_rsi = gi->bootparams_gpa; + + log_hex64("[LINUX] RIP=", gi->kernel_entry); + log_hex64("[LINUX] RSP=", LINUX_GUEST_STACK_GPA + 0x1000); + log_hex64("[LINUX] RSI=", gi->bootparams_gpa); + log_hex64("[LINUX] CR3=", gi->pgt_gpa); + log_hex64("[LINUX] GDTR=", gi->gdt_gpa); + log_info("[LINUX] vmlaunch"); + + /* vmlaunch with RSI = boot_params. + * Same control flow pattern as vmm_self_test: + * - On failure: failed=1, falls through + * - On success: enters guest, VM-Exit → vmx_vm_exit_handler + * - On terminate: handler restores RSP, jmps to post_guest label */ + int failed; + __asm__ volatile( + "movq %%rsp, g_saved_host_rsp(%%rip)\n\t" + "leaq 1f(%%rip), %%rax\n\t" + "movq %%rax, g_saved_return_rip(%%rip)\n\t" + "movq g_linux_rsi(%%rip), %%rsi\n\t" /* RSI = boot_params GPA */ + "vmlaunch\n\t" + /* ---- failure path ---- */ + "movl $1, %0\n\t" + "jmp 2f\n\t" + /* ---- post_guest: terminate path jmps here ---- */ + "1:\n\t" + "movl $0, %0\n\t" + "2:\n\t" + : "=r"(failed) + :: "rax", "rcx", "rdx", "rsi", "rdi", + "r8", "r9", "r10", "r11", "memory" + ); + + if (failed) { + u64 error = vmx_vmcs_read(VMCS_VMX_INSTRUCTION_ERROR); + log_hex64("[LINUX] vmlaunch failed, error=", error); + return -3; + } + + /* Guest terminated */ + log_info("[LINUX] guest terminated"); + log_hex64("[LINUX] last exit reason=", g_last_exit_reason); + log_hex64("[LINUX] vmexit count=", vmexit_get_count()); + + return 0; +} diff --git a/CODE/UTSM/vmm/linux_loader.c b/CODE/UTSM/vmm/linux_loader.c new file mode 100644 index 0000000..bb45b01 --- /dev/null +++ b/CODE/UTSM/vmm/linux_loader.c @@ -0,0 +1,608 @@ +/* linux_loader.c — Linux 6.6 bzImage loader for UTSM VMX non-root guest. + * + * Responsibilities: + * 1. Find the bzImage among Limine boot modules + * 2. Parse the bzImage setup_header (Linux boot protocol) + * 3. Allocate host physical memory for guest regions + * 4. Load the protected-mode kernel into guest memory + * 5. Set up boot_params (e820, cmdline, initrd pointers) + * 6. Build guest page tables (identity-mapped 4GB, 2MB pages) + * 7. Build guest GDT (64-bit code/data/TSS) + * 8. EPT-map all guest regions (GPA → HPA) + * + * The actual VMCS configuration and vmlaunch happen in linux_boot.c. + */ + +#include +#include +#include +#include +#include +#include +#include "../arch/x86_64/limine.h" + +extern volatile struct limine_module_request g_module_request; +extern volatile struct limine_hhdm_request g_hhdm_request; +extern volatile struct limine_memmap_request g_memmap_request; + +/* ===== Runtime helpers ===== */ + +static void mem_copy(void *dst, const void *src, u64 n) { + u8 *d = (u8 *)dst; + const u8 *s = (const u8 *)src; + for (u64 i = 0; i < n; i++) d[i] = s[i]; +} + +static void mem_zero(void *dst, u64 n) { + u8 *d = (u8 *)dst; + for (u64 i = 0; i < n; i++) d[i] = 0; +} + +static u64 str_len(const char *s) { + u64 n = 0; + while (s[n]) n++; + return n; +} + +/* ===== Guest state ===== */ + +static struct linux_guest_info g_guest; +static const char *g_default_cmdline = LINUX_DEFAULT_CMDLINE; + +/* ===== Guest page table helpers ===== + * + * Build identity-mapped 4-level page tables for the guest. + * Maps the first 4GB of GPA space using 2MB large pages. + * + * Layout (all at known GPAs for guest CR3): + * PML4 at g_guest.pgt_gpa + 0x0000 (1 page) + * PDPT at g_guest.pgt_gpa + 0x1000 (1 page) + * PD[0] at g_guest.pgt_gpa + 0x2000 (1 page, covers 0-1GB) + * PD[1] at g_guest.pgt_gpa + 0x3000 (1 page, covers 1-2GB) + * PD[2] at g_guest.pgt_gpa + 0x4000 (1 page, covers 2-3GB) + * PD[3] at g_guest.pgt_gpa + 0x5000 (1 page, covers 3-4GB) + */ + +#define PGT_PML4_OFF 0x0000 +#define PGT_PDPT_OFF 0x1000 +#define PGT_PD_BASE 0x2000 +#define PGT_PD_COUNT 4 /* 4 PD pages = 4GB */ +#define PGT_TOTAL_PAGES (2 + PGT_PD_COUNT) +#define PGT_TOTAL_SIZE (PGT_TOTAL_PAGES * 4096) + +/* x86-64 page table entry bits */ +#define PTE_P (1ULL << 0) /* Present */ +#define PTE_RW (1ULL << 1) /* Read/Write */ +#define PTE_US (1ULL << 2) /* User/Supervisor */ +#define PTE_PS (1ULL << 7) /* Page Size (large page) */ +#define PTE_G (1ULL << 8) /* Global */ +#define PTE_NX (1ULL << 63) /* No Execute */ + +static int build_guest_page_tables(u64 pgt_hpa) { + u64 *pml4 = (u64 *)(pgt_hpa + (g_hhdm_request.response ? g_hhdm_request.response->offset : 0)); + u64 *pdpt = (u64 *)((u8 *)pml4 + 4096); + + /* PML4[0] → PDPT */ + u64 pdpt_gpa = g_guest.pgt_gpa + PGT_PDPT_OFF; + pml4[0] = pdpt_gpa | PTE_P | PTE_RW; + + /* PDPT[0..3] → PD pages (each covers 1GB) */ + for (int i = 0; i < PGT_PD_COUNT; i++) { + u64 pd_gpa = g_guest.pgt_gpa + PGT_PD_BASE + (u64)i * 4096; + pdpt[i] = pd_gpa | PTE_P | PTE_RW; + } + + /* Each PD has 512 entries, each mapping a 2MB page (PS=1). + * PD[i] entry j maps GPA = i*1GB + j*2MB. */ + for (int pd_idx = 0; pd_idx < PGT_PD_COUNT; pd_idx++) { + u64 *pd = (u64 *)((u8 *)pml4 + PGT_PD_BASE + (u64)pd_idx * 4096); + for (int j = 0; j < 512; j++) { + u64 page_gpa = (u64)pd_idx * (1ULL << 30) + (u64)j * (2ULL << 20); + pd[j] = page_gpa | PTE_P | PTE_RW | PTE_PS; + } + } + + return 0; +} + +/* ===== Guest GDT builder ===== + * + * GDT layout (at g_guest.gdt_gpa): + * [0x00] null + * [0x08] 64-bit code: base=0, limit=0xFFFFF, L=1, G=1, present, DPL0, type=0x9B + * [0x10] 64-bit data: base=0, limit=0xFFFFF, G=1, present, DPL0, type=0x93 + * [0x18] 64-bit TSS descriptor (16 bytes) + * + * TSS itself is placed in the same 4KB page after the GDT. + */ + +typedef struct __attribute__((packed)) { + u16 limit_low; + u16 base_low; + u8 base_mid; + u8 access; + u8 flags_limit_high; + u8 base_high; +} gdt_entry64; + +typedef struct __attribute__((packed)) { + u32 limit_low_base_low; + u8 base_mid; + u8 access; + u8 flags_limit_high; + u8 base_high; + u32 base_upper; + u32 reserved; +} tss_desc64; + +static int build_guest_gdt(u64 gdt_hpa) { + u8 *gdt_page = (u8 *)(gdt_hpa + (g_hhdm_request.response ? g_hhdm_request.response->offset : 0)); + mem_zero(gdt_page, 4096); + + gdt_entry64 *gdt = (gdt_entry64 *)gdt_page; + + /* [0x08] 64-bit code segment */ + gdt[1].limit_low = 0xFFFF; + gdt[1].base_low = 0; + gdt[1].base_mid = 0; + gdt[1].access = 0x9B; /* present, DPL0, code, exec/read */ + gdt[1].flags_limit_high = 0xAF; /* G=1, L=1 (64-bit), limit[19:16]=0xF */ + gdt[1].base_high = 0; + + /* [0x10] 64-bit data segment */ + gdt[2].limit_low = 0xFFFF; + gdt[2].base_low = 0; + gdt[2].base_mid = 0; + gdt[2].access = 0x93; /* present, DPL0, data, read/write */ + gdt[2].flags_limit_high = 0xCF; /* G=1, DB=1, limit[19:16]=0xF */ + gdt[2].base_high = 0; + + /* [0x18] 64-bit TSS descriptor (16 bytes). + * TSS structure lives at offset 0x100 in the same page (GPA = gdt_gpa + 0x100). */ + tss_desc64 *tss_d = (tss_desc64 *)&gdt_page[0x18]; + u64 tss_base = g_guest.gdt_gpa + 0x100; + u32 tss_limit = 0x67; /* 104-byte minimum 64-bit TSS */ + tss_d->limit_low_base_low = (tss_limit & 0xFFFF) | ((u32)(tss_base & 0xFFFFFF) << 16); + tss_d->base_mid = (u8)((tss_base >> 24) & 0xFF); + tss_d->access = 0x89; /* present, DPL0, 64-bit TSS (available) */ + tss_d->flags_limit_high = (u8)((tss_limit >> 16) & 0x0F); + tss_d->base_high = 0; + tss_d->base_upper = 0; + tss_d->reserved = 0; + + return 0; +} + +/* ===== e820 setup ===== */ + +struct __attribute__((packed)) e820_entry { + u64 addr; + u64 size; + u32 type; +}; + +static int setup_e820(void *bootparams_virt) { + u8 *bp = (u8 *)bootparams_virt; + + /* e820_entries count at offset 0x1e8 */ + u8 *e820_count = &bp[E820_ENTRIES_OFFSET]; + struct e820_entry *table = (struct e820_entry *)&bp[E820_TABLE_OFFSET]; + + int n = 0; + + /* Entry 0: 0x00000000-0x0009FFFF: usable RAM (640KB) */ + table[n].addr = 0x00000000; + table[n].size = 0x000A0000; + table[n].type = E820_TYPE_RAM; + n++; + + /* Entry 1: 0x000A0000-0x000FFFFF: reserved (VGA + BIOS) */ + table[n].addr = 0x000A0000; + table[n].size = 0x00060000; + table[n].type = E820_TYPE_RESERVED; + n++; + + /* Entry 2: 0x00100000-0x003FFFFF: reserved (UTSM area) */ + table[n].addr = 0x00100000; + table[n].size = 0x00300000; + table[n].type = E820_TYPE_RESERVED; + n++; + + /* Entry 3: 0x00400000-0x03FFFFFF: usable RAM (kernel + initrd + low RAM) */ + table[n].addr = 0x00400000; + table[n].size = 0x03C00000; /* 60MB */ + table[n].type = E820_TYPE_RAM; + n++; + + /* Entry 4: 0x04000000-0x040FFFFF: reserved (IPC shared memory, 1MB) */ + table[n].addr = 0x04000000; + table[n].size = 0x00100000; /* 1MB */ + table[n].type = E820_TYPE_RESERVED; + n++; + + /* Entry 5: 0x04100000-0x0FFFFFFF: usable RAM (general RAM, ~188MB) */ + table[n].addr = 0x04100000; + table[n].size = 0x0BF00000; /* 191MB */ + table[n].type = E820_TYPE_RAM; + n++; + + *e820_count = (u8)n; + return n; +} + +/* ===== bzImage module finder ===== */ + +void *linux_find_bzimage_module(u64 *size_out) { + if (!g_module_request.response) { + log_error("[LINUX] no Limine module response"); + return (void *)0; + } + + struct limine_module_response *resp = g_module_request.response; + for (u64 i = 0; i < resp->module_count; i++) { + struct limine_file *file = resp->modules[i]; + if (!file || !file->path) continue; + + /* Look for paths containing "linux" or "bzImage" */ + const char *p = file->path; + int match = 0; + /* Simple case-insensitive substring search for "linux" */ + for (const char *s = p; *s; s++) { + if ((s[0] == 'l' || s[0] == 'L') && + (s[1] == 'i' || s[1] == 'I') && + (s[2] == 'n' || s[2] == 'N') && + (s[3] == 'u' || s[3] == 'U') && + (s[4] == 'x' || s[4] == 'X')) { + match = 1; + break; + } + } + if (!match) continue; + + if (size_out) *size_out = file->size; + log_info("[LINUX] found bzImage module:"); + log_info(file->path); + log_hex64("[LINUX] module size=", file->size); + return file->address; + } + + log_warn("[LINUX] no bzImage module found"); + return (void *)0; +} + +/* ===== bzImage parser ===== */ + +int linux_parse_bzimage(const void *bzimage, u64 size, + u64 *payload_offset_out, u64 *payload_length_out, + u64 *init_size_out, u64 *entry_offset_out) { + const u8 *img = (const u8 *)bzimage; + + if (size < 0x300) { + log_error("[LINUX] bzImage too small"); + return -1; + } + + /* Check boot_flag at 0x1fe */ + u16 boot_flag = *(u16 *)&img[BZ_BOOT_FLAG_OFF]; + if (boot_flag != 0xAA55) { + log_hex64("[LINUX] bad boot_flag=", boot_flag); + return -2; + } + + /* Check header magic "HdrS" at 0x202 */ + u32 magic = *(u32 *)&img[BZ_HEADER_OFF]; + if (magic != BZ_HEADER_MAGIC) { + log_hex64("[LINUX] bad header magic=", magic); + return -3; + } + + /* Check protocol version */ + u16 version = *(u16 *)&img[BZ_VERSION_OFF]; + log_hex64("[LINUX] boot protocol version=", version); + if (version < 0x020c) { + log_error("[LINUX] boot protocol too old"); + return -4; + } + + /* Check xloadflags for 64-bit kernel */ + u16 xloadflags = *(u16 *)&img[BZ_XLOADFLAGS_OFF]; + log_hex64("[LINUX] xloadflags=", xloadflags); + if (!(xloadflags & XLF_KERNEL_64)) { + log_error("[LINUX] not a 64-bit kernel"); + return -5; + } + + /* Payload offset and length */ + u32 payload_offset = *(u32 *)&img[BZ_PAYLOAD_OFFSET_OFF]; + u32 payload_length = *(u32 *)&img[BZ_PAYLOAD_LENGTH_OFF]; + log_hex64("[LINUX] payload_offset=", payload_offset); + log_hex64("[LINUX] payload_length=", payload_length); + + if (payload_offset + payload_length > size) { + log_error("[LINUX] payload exceeds image size"); + return -6; + } + + /* Init size (total memory needed for kernel init) */ + u32 init_size = *(u32 *)&img[BZ_INIT_SIZE_ACTUAL_OFF]; + if (init_size == 0) init_size = payload_length; + log_hex64("[LINUX] init_size=", init_size); + + /* The 64-bit entry point is at offset 0x200 from the payload start. + * This is the "startup_64" symbol. */ + u64 entry_offset = payload_offset + 0x200; + log_hex64("[LINUX] entry_offset=", entry_offset); + + /* Setup sectors (for real-mode header size) */ + u8 setup_sects = img[0x1f1]; + if (setup_sects == 0) setup_sects = 4; + u64 setup_size = (setup_sects + 1) * 512; + log_hex64("[LINUX] setup_sects=", setup_sects); + log_hex64("[LINUX] setup_size=", setup_size); + + if (payload_offset_out) *payload_offset_out = payload_offset; + if (payload_length_out) *payload_length_out = payload_length; + if (init_size_out) *init_size_out = init_size; + if (entry_offset_out) *entry_offset_out = entry_offset; + + return 0; +} + +/* ===== Boot params setup ===== */ + +static int setup_boot_params(void *bp_virt, u64 bp_gpa, + u64 kernel_entry_gpa, + u64 cmdline_gpa, u64 cmdline_len, + u64 initrd_gpa, u64 initrd_size, + const void *bzimage) { + (void)bp_gpa; /* GPA is used by EPT mapping, not needed here */ + const u8 *img = (const u8 *)bzimage; + u8 *bp = (u8 *)bp_virt; + + /* Zero entire boot_params */ + mem_zero(bp, BOOT_PARAMS_SIZE); + + /* Copy setup_header from bzImage (offset 0x1f1, length 0x1ef = 0x200 - 0x11) */ + /* The setup_header in boot_params starts at 0x1f1 and goes to 0x2xx. + * We copy from bzImage offset 0x1f1 to boot_params offset 0x1f1. */ + u64 hdr_copy_len = 0x210; /* copy a generous chunk */ + if (hdr_copy_len > BOOT_PARAMS_SIZE - SETUP_HEADER_OFFSET) { + hdr_copy_len = BOOT_PARAMS_SIZE - SETUP_HEADER_OFFSET; + } + mem_copy(&bp[SETUP_HEADER_OFFSET], &img[SETUP_HEADER_OFFSET], hdr_copy_len); + + /* Override key fields */ + bp[BZ_TYPE_OF_LOADER_OFF] = 0xFF; /* custom bootloader */ + + /* loadflags: LOADED_HIGH | CAN_USE_HEAP */ + u8 loadflags = bp[BZ_LOADFLAGS_OFF]; + loadflags |= LOADFLAG_LOADED_HIGH | LOADFLAG_CAN_USE_HEAP; + bp[BZ_LOADFLAGS_OFF] = loadflags; + + /* Command line pointer and size */ + *(u32 *)&bp[BZ_CMD_LINE_PTR_OFF] = (u32)cmdline_gpa; + *(u32 *)&bp[BZ_CMDLINE_SIZE_OFF] = (u32)cmdline_len; + + /* Initrd */ + if (initrd_size > 0) { + *(u32 *)&bp[BZ_RAMDISK_IMAGE_OFF] = (u32)initrd_gpa; + *(u32 *)&bp[BZ_RAMDISK_SIZE_OFF] = (u32)initrd_size; + } + + /* code32_start: set to kernel entry (informational) */ + *(u32 *)&bp[BZ_CODE32_START_OFF] = (u32)kernel_entry_gpa; + + /* Heap end pointer (offset 0x224): end of real-mode heap. + * Not critical for 64-bit boot, but set a reasonable value. */ + *(u16 *)&bp[0x224] = 0xFE00; /* heap ends at 0xFE00 */ + + /* Set up e820 memory map */ + int e820_count = setup_e820(bp_virt); + log_hex64("[LINUX] e820 entries=", (u64)e820_count); + + return 0; +} + +/* ===== Main loader entry ===== */ + +int linux_loader_init(void) { + log_info("[LINUX] loader init begin"); + + /* 1. Find bzImage module */ + u64 bzimage_size = 0; + void *bzimage = linux_find_bzimage_module(&bzimage_size); + if (!bzimage) { + log_warn("[LINUX] no bzImage found, skipping Linux guest"); + return -1; + } + + /* 2. Parse bzImage header */ + u64 payload_offset, payload_length, init_size, entry_offset; + if (linux_parse_bzimage(bzimage, bzimage_size, + &payload_offset, &payload_length, + &init_size, &entry_offset) != 0) { + log_error("[LINUX] bzImage parse failed"); + return -2; + } + + /* 3. Allocate host physical memory for guest regions */ + + /* Kernel: round up init_size to page boundary, align to 2MB */ + u64 kernel_alloc_size = ((init_size + 0xFFFFF) & ~0xFFFFFULL); + if (kernel_alloc_size < payload_length) kernel_alloc_size = payload_length; + dkm_dma_buffer kernel_buf; + if (dma_alloc_pages(kernel_alloc_size / 4096, 2 * 1024 * 1024, 0, &kernel_buf) != 0) { + log_error("[LINUX] failed to alloc kernel memory"); + return -3; + } + log_hex64("[LINUX] kernel HPA=", kernel_buf.phys); + log_hex64("[LINUX] kernel alloc size=", kernel_alloc_size); + + /* Boot params: 1 page */ + dkm_dma_buffer bp_buf; + if (dma_alloc_pages(1, 4096, 0, &bp_buf) != 0) { + log_error("[LINUX] failed to alloc boot_params"); + return -4; + } + + /* Page tables: PGT_TOTAL_PAGES pages */ + dkm_dma_buffer pgt_buf; + if (dma_alloc_pages(PGT_TOTAL_PAGES, 4096, 0, &pgt_buf) != 0) { + log_error("[LINUX] failed to alloc page tables"); + return -5; + } + + /* GDT: 1 page */ + dkm_dma_buffer gdt_buf; + if (dma_alloc_pages(1, 4096, 0, &gdt_buf) != 0) { + log_error("[LINUX] failed to alloc GDT"); + return -6; + } + + /* Stack: 1 page (stack grows down from top) */ + dkm_dma_buffer stack_buf; + if (dma_alloc_pages(1, 4096, 0, &stack_buf) != 0) { + log_error("[LINUX] failed to alloc stack"); + return -7; + } + + /* Command line: 1 page */ + dkm_dma_buffer cmdline_buf; + if (dma_alloc_pages(1, 4096, 0, &cmdline_buf) != 0) { + log_error("[LINUX] failed to alloc cmdline"); + return -8; + } + + /* General RAM: 64MB for Linux, mapped at GPA 0x05000000 (after IPC region) */ + u64 ram_size = 64 * 1024 * 1024; + dkm_dma_buffer ram_buf; + if (dma_alloc_pages(ram_size / 4096, 2 * 1024 * 1024, 0, &ram_buf) != 0) { + log_error("[LINUX] failed to alloc guest RAM (64MB)"); + /* Try smaller: 32MB */ + ram_size = 32 * 1024 * 1024; + if (dma_alloc_pages(ram_size / 4096, 2 * 1024 * 1024, 0, &ram_buf) != 0) { + log_error("[LINUX] failed to alloc guest RAM (32MB)"); + return -9; + } + } + log_hex64("[LINUX] guest RAM HPA=", ram_buf.phys); + log_hex64("[LINUX] guest RAM size=", ram_size); + + /* 4. Load kernel image into allocated memory */ + const u8 *kernel_src = (const u8 *)bzimage + payload_offset; + mem_copy(kernel_buf.virt, kernel_src, payload_length); + log_hex64("[LINUX] kernel loaded, size=", payload_length); + + /* 5. Set up guest page tables */ + g_guest.pgt_gpa = LINUX_GUEST_PGT_GPA; + build_guest_page_tables(pgt_buf.phys); + log_hex64("[LINUX] page tables built at HPA=", pgt_buf.phys); + + /* 6. Set up guest GDT */ + g_guest.gdt_gpa = LINUX_GUEST_GDT_GPA; + build_guest_gdt(gdt_buf.phys); + log_hex64("[LINUX] GDT built at HPA=", gdt_buf.phys); + + /* 7. Set up command line */ + u64 cmdline_len = str_len(g_default_cmdline) + 1; /* include NUL */ + mem_copy(cmdline_buf.virt, g_default_cmdline, cmdline_len); + log_info("[LINUX] cmdline:"); + log_info(g_default_cmdline); + + /* 8. Set up boot_params */ + g_guest.bootparams_gpa = LINUX_GUEST_BOOTPARAMS_GPA; + g_guest.kernel_gpa = LINUX_GUEST_KERNEL_GPA; + g_guest.kernel_entry = LINUX_GUEST_KERNEL_GPA + entry_offset; + g_guest.cmdline_gpa = LINUX_GUEST_CMDLINE_GPA; + g_guest.cmdline_size = cmdline_len; + + /* For now, no initrd (Phase 1.3+ will add it) */ + g_guest.initrd_gpa = 0; + g_guest.initrd_size = 0; + + setup_boot_params(bp_buf.virt, g_guest.bootparams_gpa, + g_guest.kernel_entry, + g_guest.cmdline_gpa, cmdline_len, + g_guest.initrd_gpa, g_guest.initrd_size, + bzimage); + log_hex64("[LINUX] boot_params at GPA=", g_guest.bootparams_gpa); + + /* 9. EPT-map all guest regions (GPA → HPA) */ + + /* Kernel: GPA 0x01000000 → kernel HPA, RWX */ + if (ept_map_range(g_guest.kernel_gpa, kernel_buf.phys, + kernel_alloc_size, EPT_RWX) != 0) { + log_error("[LINUX] EPT map kernel failed"); + return -10; + } + + /* Boot params: GPA 0x00400000 → bp HPA, RW */ + if (ept_map_range(g_guest.bootparams_gpa, bp_buf.phys, + 4096, EPT_READ | EPT_WRITE) != 0) { + log_error("[LINUX] EPT map boot_params failed"); + return -11; + } + + /* Page tables: GPA 0x00700000 → pgt HPA, RW */ + if (ept_map_range(g_guest.pgt_gpa, pgt_buf.phys, + PGT_TOTAL_SIZE, EPT_READ | EPT_WRITE) != 0) { + log_error("[LINUX] EPT map page tables failed"); + return -12; + } + + /* GDT: GPA 0x00600000 → gdt HPA, RW */ + if (ept_map_range(g_guest.gdt_gpa, gdt_buf.phys, + 4096, EPT_READ | EPT_WRITE) != 0) { + log_error("[LINUX] EPT map GDT failed"); + return -13; + } + + /* Stack: GPA 0x00800000 → stack HPA, RW */ + if (ept_map_range(LINUX_GUEST_STACK_GPA, stack_buf.phys, + 4096, EPT_READ | EPT_WRITE) != 0) { + log_error("[LINUX] EPT map stack failed"); + return -14; + } + + /* Command line: GPA 0x00500000 → cmdline HPA, R */ + if (ept_map_range(g_guest.cmdline_gpa, cmdline_buf.phys, + 4096, EPT_READ) != 0) { + log_error("[LINUX] EPT map cmdline failed"); + return -15; + } + + /* General RAM: map the allocated RAM at GPA 0x05000000 (after IPC region). + * GPA layout (matches e820): + * 0x00400000-0x009FFFFF: fixed regions (boot_params, GDT, pgt, stack, cmdline) + * 0x01000000-0x02FFFFFF: kernel + * 0x03000000-0x03FFFFFF: initrd (if any) + * 0x04000000-0x040FFFFF: IPC shared memory (mapped by ipc_shm_init) + * 0x05000000-0x08FFFFFF: general RAM (64MB) + * Linux sees this as usable RAM in e820. */ + u64 ram_gpa = LINUX_GUEST_RAM_GPA; + if (ept_map_range(ram_gpa, ram_buf.phys, ram_size, EPT_RWX) != 0) { + log_error("[LINUX] EPT map guest RAM failed"); + return -16; + } + log_hex64("[LINUX] guest RAM mapped GPA=", ram_gpa); + + /* Identity-map the first 1MB (BIOS area, needed by some Linux code paths) */ + if (ept_identity_map(0, 0x100000, EPT_RWX) != 0) { + log_warn("[LINUX] EPT identity map first 1MB failed (non-critical)"); + } + + /* 10. Record guest info */ + g_guest.kernel_size = payload_length; + g_guest.loaded = 1; + + log_hex64("[LINUX] kernel GPA=", g_guest.kernel_gpa); + log_hex64("[LINUX] kernel entry=", g_guest.kernel_entry); + log_hex64("[LINUX] kernel size=", g_guest.kernel_size); + log_info("[LINUX] loader init ok"); + + return 0; +} + +const struct linux_guest_info *linux_get_guest_info(void) { + return g_guest.loaded ? &g_guest : (struct linux_guest_info *)0; +} diff --git a/CODE/UTSM/vmm/vmexit.c b/CODE/UTSM/vmm/vmexit.c new file mode 100644 index 0000000..176bf5f --- /dev/null +++ b/CODE/UTSM/vmm/vmexit.c @@ -0,0 +1,125 @@ +#include +#include +#include +#include +#include +#include +#include + +/* VM-Exit 处理器:从 VMCS 读取 exit reason 与 guest 状态,分发处理。 + * + * Phase 1.1:处理 HLT(终止 guest)、EPT violation(动态映射)、异常。 + * Phase 1.3:增加 VMCALL hypercall 处理(读 guest GPR、分发、写返回值)。 + */ + +extern void vmx_vmresume_wrapper(void); + +static u64 g_vmexit_count; + +static void log_exit_diagnostics(u64 reason, u64 qualification, u64 rip, u64 len) { + log_hex64("[VMEXIT] reason=", reason); + log_hex64("[VMEXIT] qual=", qualification); + log_hex64("[VMEXIT] rip=", rip); + log_hex64("[VMEXIT] ilen=", len); +} + +/* 处理 HLT:Phase 1.1 自检场景下表示 guest 完成测试,停止 guest。 */ +static int handle_hlt(u64 rip, u64 instr_len, int *out_resume) { + *out_resume = 0; /* 不 resume,guest 终止 */ + log_info("[VMEXIT] HLT - guest halted"); + (void)rip; (void)instr_len; + return 0; +} + +/* 处理 EPT violation:动态映射缺失的 GPA。 + * Phase 1.1:1:1 映射缺失的页面,权限 RWX。 */ +static int handle_ept_violation(u64 qualification, int *out_resume) { + u64 gpa = vmx_vmcs_read(VMCS_GUEST_PHYSICAL_ADDR); + u64 page = gpa & ~(EPT_PAGE_SIZE - 1); + log_hex64("[VMEXIT] EPT violation gpa=", gpa); + (void)qualification; + /* 1:1 映射该页(假设 HPA=GPA) */ + if (ept_identity_map(page, EPT_PAGE_SIZE, EPT_RWX) != 0) { + log_error("[VMEXIT] failed to map EPT page"); + *out_resume = 0; + return -1; + } + *out_resume = 1; + return 0; +} + +/* 处理通用异常:打印诊断信息,终止 guest。 */ +static int handle_exception(u64 qualification, u64 rip, int *out_resume) { + u64 intr_info = vmx_vmcs_read(0x4404); /* VM_EXIT_INTR_INFO */ + log_hex64("[VMEXIT] exception intr_info=", intr_info); + log_hex64("[VMEXIT] exception qual=", qualification); + log_hex64("[VMEXIT] exception rip=", rip); + *out_resume = 0; + return 0; +} + +/* VM-Exit 主分发器。从 vmx_vm_exit_handler(汇编入口)调用。 + * 返回 1 表示 vmresume;0 表示终止 guest。 */ +int vmexit_dispatch(void) { + u64 reason = vmx_vmcs_read(VMCS_EXIT_REASON) & 0xFFFF; + u64 qualification = vmx_vmcs_read(VMCS_EXIT_QUALIFICATION); + u64 rip = vmx_vmcs_read(VMCS_GUEST_RIP); + u64 instr_len = vmx_vmcs_read(VMCS_INSTRUCTION_LENGTH); + int resume = 0; + + g_vmexit_count++; + + switch (reason) { + case EXIT_HLT: + handle_hlt(rip, instr_len, &resume); + break; + case EXIT_EPT_VIOLATION: + handle_ept_violation(qualification, &resume); + break; + case EXIT_EXCEPTION_NMI: + handle_exception(qualification, rip, &resume); + break; + case EXIT_TRIPLE_FAULT: + log_error("[VMEXIT] triple fault"); + resume = 0; + break; + case EXIT_CPUID: + /* 简化:让 guest 通过 CPUID,前进 RIP。Phase 1.2+ 完整模拟。 */ + log_info("[VMEXIT] CPUID - skip"); + vmx_vmcs_write(VMCS_GUEST_RIP, rip + instr_len); + resume = 1; + break; + case EXIT_IO_INSTRUCTION: + /* 简化:跳过 IO 指令。Phase 1.4 virtio-console 完整处理。 */ + vmx_vmcs_write(VMCS_GUEST_RIP, rip + instr_len); + resume = 1; + break; + case EXIT_RDMSR: + case EXIT_WRMSR: + vmx_vmcs_write(VMCS_GUEST_RIP, rip + instr_len); + resume = 1; + break; + case EXIT_VMCALL: + /* VMCALL hypercall: read guest GPRs from g_guest_regs (saved by + * vmexit_asm.S), dispatch to the hypercall handler, and write + * the return value into g_guest_regs.rax. The handler also + * advances GUEST_RIP past the VMCALL instruction. */ + { + int hcall_resume = hypercall_handle( + g_guest_regs.rax, g_guest_regs.rdi, + g_guest_regs.rsi, g_guest_regs.rdx, + rip, instr_len); + resume = hcall_resume; + } + break; + default: + log_exit_diagnostics(reason, qualification, rip, instr_len); + resume = 0; + break; + } + + (void)qualification; + return resume; +} + +u64 vmexit_get_count(void) { return g_vmexit_count; } diff --git a/CODE/UTSM/vmm/vmexit_asm.S b/CODE/UTSM/vmm/vmexit_asm.S new file mode 100644 index 0000000..415037c --- /dev/null +++ b/CODE/UTSM/vmm/vmexit_asm.S @@ -0,0 +1,90 @@ +/* VM-Exit assembly entry. + * + * On VM-Exit, CPU loads host state from VMCS: + * - host CR0/CR3/CR4, segment registers + * - host RSP = VMCS_HOST_RSP (top of g_host_stack) + * - host RIP = VMCS_HOST_RIP (this function) + * + * VMCS does NOT save guest GPRs (RAX-R15). They remain in the CPU registers + * and must be saved explicitly if the C dispatcher wants to read or modify + * them (e.g. for VMCALL hypercall arguments and return values). + * + * This function: + * 1. Saves all 16 guest GPRs into g_guest_regs + * 2. Calls vmm_vmexit_entry() (C dispatcher, returns void) + * - Internally calls vmexit_dispatch(); sets g_guest_terminated if !resume + * 3. Restores all 16 guest GPRs from g_guest_regs (C may have modified rax) + * 4. Checks g_guest_terminated: + * - 0: executes vmresume (GPRs = guest values, possibly modified by C) + * If vmresume fails, falls through to terminate + * - 1: restores saved host RSP, jmps to saved return RIP + * + * The saved RSP/RIP point into the vmlaunch inline asm block (in vmm_self_test + * or linux_launch). After the jmp, C flow continues with failed=0. + */ + +.section .text +.global vmx_vm_exit_handler +vmx_vm_exit_handler: + /* Step 1: Save all 16 guest GPRs to g_guest_regs. + * GPRs at this point hold guest values (VMCS doesn't save them). */ + movq %rax, g_guest_regs + 0(%rip) + movq %rbx, g_guest_regs + 8(%rip) + movq %rcx, g_guest_regs + 16(%rip) + movq %rdx, g_guest_regs + 24(%rip) + movq %rsi, g_guest_regs + 32(%rip) + movq %rdi, g_guest_regs + 40(%rip) + movq %rbp, g_guest_regs + 48(%rip) + movq %r8, g_guest_regs + 56(%rip) + movq %r9, g_guest_regs + 64(%rip) + movq %r10, g_guest_regs + 72(%rip) + movq %r11, g_guest_regs + 80(%rip) + movq %r12, g_guest_regs + 88(%rip) + movq %r13, g_guest_regs + 96(%rip) + movq %r14, g_guest_regs + 104(%rip) + movq %r15, g_guest_regs + 112(%rip) + + /* Step 2: Align stack to 16 bytes and call C dispatcher. + * The host RSP (from VMCS_HOST_RSP) is 16-byte aligned minus 8 + * (for the implicit return address), so we sub 8 for alignment. */ + subq $8, %rsp + call vmm_vmexit_entry + addq $8, %rsp + + /* Step 3: Restore all 16 guest GPRs from g_guest_regs. + * C handlers may have modified fields (e.g. rax = hypercall return value). */ + movq g_guest_regs + 0(%rip), %rax + movq g_guest_regs + 8(%rip), %rbx + movq g_guest_regs + 16(%rip), %rcx + movq g_guest_regs + 24(%rip), %rdx + movq g_guest_regs + 32(%rip), %rsi + movq g_guest_regs + 40(%rip), %rdi + movq g_guest_regs + 48(%rip), %rbp + movq g_guest_regs + 56(%rip), %r8 + movq g_guest_regs + 64(%rip), %r9 + movq g_guest_regs + 72(%rip), %r10 + movq g_guest_regs + 80(%rip), %r11 + movq g_guest_regs + 88(%rip), %r12 + movq g_guest_regs + 96(%rip), %r13 + movq g_guest_regs + 104(%rip), %r14 + movq g_guest_regs + 112(%rip), %r15 + + /* Step 4: Check terminate flag */ + movl g_guest_terminated(%rip), %eax + testl %eax, %eax + jnz .Lterminate + + /* Resume guest: GPRs are now restored (with any C modifications). + * vmresume does not return on success — CPU enters guest. */ + vmresume + /* If we reach here, vmresume failed (CF=1 or ZF=1). */ + movl $1, g_guest_terminated(%rip) + /* Fall through to terminate */ + +.Lterminate: + /* Terminate: restore saved RSP and jmp to saved return RIP. + * The saved RSP is the caller's stack pointer before vmlaunch. + * The saved RIP is the "1:" label in the vmlaunch inline asm, + * which sets failed=0 and continues C flow. */ + movq g_saved_host_rsp(%rip), %rsp + jmpq *g_saved_return_rip(%rip) diff --git a/CODE/UTSM/vmm/vmm.c b/CODE/UTSM/vmm/vmm.c new file mode 100644 index 0000000..b973286 --- /dev/null +++ b/CODE/UTSM/vmm/vmm.c @@ -0,0 +1,523 @@ +#include +#include +#include +#include +#include +#include +#include "../arch/x86_64/limine.h" + +extern volatile struct limine_hhdm_request g_hhdm_request; + +/* ===== VMX 指令错误码 ===== */ +static const char *vmx_error_str(u64 err) { + switch (err) { + case 1: return "VMCALL/VMCLEAR/VMLAUNCH/VMRESUME with non-launched VMCS"; + case 2: return "VMRESUME after VMXOFF"; + case 3: return "VMRESUME with corrupted VMCS"; + case 4: return "vmlaunch with non-clear VMCS"; + case 5: return "VM operation invalid"; + case 6: return "VMCS revision mismatch"; + case 7: return "VMCS shadowing mismatch"; + case 8: return "invalid guest state"; + case 9: return "host state invalid"; + case 10: return "VM-execution control invalid"; + case 11: return "VM-exit control invalid"; + case 12: return "VM-entry control invalid"; + case 13: return "VM-exit control fields invalid"; + case 15: return "address out of width"; + case 16: return "MSR bitmap addr invalid"; + case 17: return "VMREAD/VMWRITE from unsupported field"; + case 18: return "VMCS addr invalid"; + default: return "unknown"; + } +} + +/* ===== VMM 全局状态 ===== */ +static int g_vmm_ready; +static u64 g_vmcs_phys; + +/* VM-Exit 后回到 host 的栈与 RIP 恢复点。 + * vmm_self_test 在 vmlaunch 前保存当前 RSP 与"launch 后"的 RIP, + * VM-Exit 处理器在决定终止 guest 时恢复 RSP 并 jmp 到该 RIP。 + * 注意:这些变量必须是非 static 的全局符号,因为 vmexit_asm.S 通过 + * RIP 相对寻址引用它们。 */ +volatile u64 g_saved_host_rsp; +volatile u64 g_saved_return_rip; +volatile int g_guest_terminated; +volatile u64 g_last_exit_reason; + +/* Guest GPR save area (referenced by vmexit_asm.S). + * VM-Exit 时 CPU 不自动保存 guest GPR,由汇编入口保存到此结构。 + * C 处理器可读写此结构(如 VMCALL 返回值写入 rax)。 */ +volatile struct vmexit_guest_regs g_guest_regs; + +/* host 栈:VM-Exit 时使用(HOST_RSP)。 + * 非static:linux_boot.c 共用同一个 host 栈。 */ +u8 g_host_stack[8192] __attribute__((aligned(16))); + +/* VM-Exit 汇编入口(vmexit_asm.S) */ +extern void vmx_vm_exit_handler(void); + +/* C 分发器(vmexit.c) */ +extern int vmexit_dispatch(void); +extern u64 vmexit_get_count(void); + +/* ===== Phase 1.1 自检 guest 代码 ===== + * guest 在 GPA 0x1000 运行,执行 nop;nop;nop;hlt。 + * HLT 触发 VMEXIT,dispatch 返回 0(终止)。 */ +static u8 g_guest_code[4096] __attribute__((aligned(4096))) = { + 0x90, 0x90, 0x90, 0xF4, /* nop; nop; nop; hlt */ +}; + +/* guest 栈页 */ +static u8 g_guest_stack[4096] __attribute__((aligned(4096))); + +/* MSR bitmap(4KB 全 0 = 不拦截 MSR) */ +static u8 g_msr_bitmap[4096] __attribute__((aligned(4096))); + +/* guest GDT(GPA 0x3000)与 TSS(GPA 0x4000)。 + * VMX 要求 guest TR 非空且引用 GDT 中的 TSS 描述符。 */ +static u8 g_guest_gdt[4096] __attribute__((aligned(4096))); +static u8 g_guest_tss[4096] __attribute__((aligned(4096))); + +/* 64-bit GDT entry */ +typedef struct __attribute__((packed)) { + u16 limit_low; + u16 base_low; + u8 base_mid; + u8 access; + u8 flags_limit_high; + u8 base_high; +} gdt_entry64; + +/* 64-bit TSS descriptor (16 bytes) */ +typedef struct __attribute__((packed)) { + u32 limit_low_base_low; + u8 base_mid; + u8 access; + u8 flags_limit_high; + u8 base_high; + u32 base_upper; + u32 reserved; +} tss_desc64; + +static void build_guest_gdt(void) { + /* 清零 */ + for (int i = 0; i < 4096; i++) g_guest_gdt[i] = 0; + for (int i = 0; i < 4096; i++) g_guest_tss[i] = 0; + + gdt_entry64 *gdt = (gdt_entry64 *)g_guest_gdt; + /* [0x00] null */ + /* [0x08] code: base=0, limit=0xFFFFF, L=1, G=1, present, DPL0, type=0x9B */ + gdt[1].limit_low = 0xFFFF; + gdt[1].base_low = 0; + gdt[1].base_mid = 0; + gdt[1].access = 0x9B; /* present, DPL0, code, executed/read */ + gdt[1].flags_limit_high = 0xAF; /* G=1, L=1 (64-bit), limit[19:16]=0xF */ + gdt[1].base_high = 0; + /* [0x10] data: base=0, limit=0xFFFFF, G=1, present, DPL0, type=0x93 */ + gdt[2].limit_low = 0xFFFF; + gdt[2].base_low = 0; + gdt[2].base_mid = 0; + gdt[2].access = 0x93; /* present, DPL0, data, read/write */ + gdt[2].flags_limit_high = 0xCF; /* G=1, DB=1, limit[19:16]=0xF */ + gdt[2].base_high = 0; + + /* [0x18] TSS descriptor (16 bytes, 64-bit TSS) */ + tss_desc64 *tss_d = (tss_desc64 *)&g_guest_gdt[0x18]; + u64 tss_base = 0x4000; /* TSS at GPA 0x4000 */ + u32 tss_limit = 0x67; /* 104-byte minimum TSS */ + tss_d->limit_low_base_low = (tss_limit & 0xFFFF) | ((u32)(tss_base & 0xFFFFFF) << 16); + tss_d->base_mid = (u8)((tss_base >> 24) & 0xFF); + tss_d->access = 0x89; /* present, DPL0, 64-bit TSS */ + tss_d->flags_limit_high = (u8)((tss_limit >> 16) & 0x0F) | 0x00; + tss_d->base_high = 0; + tss_d->base_upper = 0; + tss_d->reserved = 0; +} + +/* ===== CR 读取辅助 ===== */ +static inline u64 read_cr0_local(void) { + u64 v; + __asm__ volatile("mov %%cr0, %0" : "=r"(v)); + return v; +} +static inline u64 read_cr4_local(void) { + u64 v; + __asm__ volatile("mov %%cr4, %0" : "=r"(v)); + return v; +} +static inline u64 read_cr3_local(void) { + u64 v; + __asm__ volatile("mov %%cr3, %0" : "=r"(v)); + return v; +} + +/* ===== VMCS 配置 ===== */ + +static u64 vmx_adjust_control(u64 value, u32 msr) { + u32 allowed0 = (u32)vmx_read_msr(msr); + u32 allowed1 = (u32)(vmx_read_msr(msr) >> 32); + u32 adjusted = (u32)value | allowed0; + adjusted &= allowed1; + return adjusted; +} + +static void vmcs_setup_host_state(void) { + vmx_vmcs_write(VMCS_HOST_CR0, read_cr0_local() | CR0_PE | CR0_NE | CR0_PG); + vmx_vmcs_write(VMCS_HOST_CR3, read_cr3_local()); + vmx_vmcs_write(VMCS_HOST_CR4, read_cr4_local() | CR4_VMXE); + + /* 段选择子:UTSM GDT 0x08 (code) / 0x10 (data) */ + vmx_vmcs_write(VMCS_HOST_CS_SELECTOR, 0x08); + vmx_vmcs_write(VMCS_HOST_SS_SELECTOR, 0x10); + vmx_vmcs_write(VMCS_HOST_DS_SELECTOR, 0x10); + vmx_vmcs_write(VMCS_HOST_ES_SELECTOR, 0x10); + vmx_vmcs_write(VMCS_HOST_FS_SELECTOR, 0); + vmx_vmcs_write(VMCS_HOST_GS_SELECTOR, 0); + + vmx_vmcs_write(VMCS_HOST_FS_BASE, 0); + vmx_vmcs_write(VMCS_HOST_GS_BASE, 0); + + /* Host GDT/IDT base + TR(VM-Exit 后 CPU 加载 host TR,必须有效) */ + { + struct __attribute__((packed)) { u16 limit; u64 base; } gdtr, idtr; + __asm__ volatile("sgdt %0" : "=m"(gdtr)); + __asm__ volatile("sidt %0" : "=m"(idtr)); + vmx_vmcs_write(VMCS_HOST_GDTR_BASE, gdtr.base); + vmx_vmcs_write(VMCS_HOST_IDTR_BASE, idtr.base); + + /* 读取当前 TR 寄存器(UTSM 已加载 TSS) */ + u16 tr_sel; + u64 tr_base; + __asm__ volatile("str %0" : "=r"(tr_sel)); + /* 从 GDT 中读取 TR base:TR 选择子指向的描述符 */ + u64 gdt_base = gdtr.base; + u64 *tss_desc = (u64 *)(gdt_base + (tr_sel & 0xFFF8)); + /* 64-bit TSS 描述符 16 字节:base 由 bits 拼接 */ + u64 low = tss_desc[0]; + u64 high = tss_desc[1]; + tr_base = (low >> 16) & 0xFFFFFF; /* base[15:0] + base[23:16] */ + tr_base |= ((low >> 32) & 0xFF) << 24; /* base[31:24] */ + tr_base |= (high & 0xFFFFFFFF) << 32; /* base[63:32] */ + vmx_vmcs_write(VMCS_HOST_TR_SELECTOR, tr_sel); + vmx_vmcs_write(VMCS_HOST_TR_BASE, tr_base); + } + + /* Host SYSENTER(VMX 要求字段有效) */ + { + u64 sysenter_cs; + __asm__ volatile("rdmsr" : "=a"(sysenter_cs) : "c"(0x174)); + vmx_vmcs_write(VMCS_HOST_SYSENTER_CS, sysenter_cs & 0xFFFFFFFF); + vmx_vmcs_write(VMCS_HOST_SYSENTER_ESP, 0); + vmx_vmcs_write(VMCS_HOST_SYSENTER_EIP, 0); + } + + /* Host EFER:VM_EXIT_LOAD_HOST_EFER 控制位要求 VMCS 中有 host EFER 值, + * VM-Exit 时加载。读取当前 host EFER MSR 填入。 */ + { + u32 low, high; + __asm__ volatile("rdmsr" : "=a"(low), "=d"(high) : "c"(0xC0000080)); + u64 host_efer = ((u64)high << 32) | low; + vmx_vmcs_write(VMCS_HOST_IA32_EFER, host_efer); + } + + /* Host RSP/RIP:VM-Exit 后跳到 vmx_vm_exit_handler */ + vmx_vmcs_write(VMCS_HOST_RSP, (u64)&g_host_stack[sizeof(g_host_stack) - 16]); + vmx_vmcs_write(VMCS_HOST_RIP, (u64)vmx_vm_exit_handler); +} + +static void vmcs_setup_guest_state_phase1(void) { + /* Guest CR0/CR3/CR4:启用 PE/NE/PG/PAE,复用 host 页表 */ + vmx_vmcs_write(VMCS_GUEST_CR0, CR0_PE | CR0_NE | CR0_PG | CR0_WP); + vmx_vmcs_write(VMCS_GUEST_CR3, read_cr3_local()); + vmx_vmcs_write(VMCS_GUEST_CR4, CR4_PAE | CR4_PGE | CR4_PSE); + + /* 段选择子:flat 模型。 + * CS=0x08 (GDT[1] code), DS/SS/ES=0x10 (GDT[2] data), + * TR=0x18 (GDT[3] 64-bit TSS)。 */ + vmx_vmcs_write(VMCS_GUEST_CS_SELECTOR, 0x08); + vmx_vmcs_write(VMCS_GUEST_SS_SELECTOR, 0x10); + vmx_vmcs_write(VMCS_GUEST_DS_SELECTOR, 0x10); + vmx_vmcs_write(VMCS_GUEST_ES_SELECTOR, 0x10); + vmx_vmcs_write(VMCS_GUEST_FS_SELECTOR, 0); + vmx_vmcs_write(VMCS_GUEST_GS_SELECTOR, 0); + vmx_vmcs_write(VMCS_GUEST_LDTR_SELECTOR, 0); + vmx_vmcs_write(VMCS_GUEST_TR_SELECTOR, 0x18); + + /* 段 base:flat 模型全 0,TR base 指向 GPA 0x4000 的 TSS */ + vmx_vmcs_write(VMCS_GUEST_CS_BASE, 0); + vmx_vmcs_write(VMCS_GUEST_SS_BASE, 0); + vmx_vmcs_write(VMCS_GUEST_DS_BASE, 0); + vmx_vmcs_write(VMCS_GUEST_ES_BASE, 0); + vmx_vmcs_write(VMCS_GUEST_FS_BASE, 0); + vmx_vmcs_write(VMCS_GUEST_GS_BASE, 0); + vmx_vmcs_write(VMCS_GUEST_LDTR_BASE, 0); + vmx_vmcs_write(VMCS_GUEST_TR_BASE, 0x4000); + vmx_vmcs_write(VMCS_GUEST_GDTR_BASE, 0x3000); + vmx_vmcs_write(VMCS_GUEST_IDTR_BASE, 0); + + /* 段 limit:代码/数据段 4GB,GDTR 覆盖 0x28 字节(3 个描述符 + TSS), + * TR limit = 0x67(104 字节最小 TSS) */ + vmx_vmcs_write(VMCS_GUEST_CS_LIMIT, 0xFFFFFFFF); + vmx_vmcs_write(VMCS_GUEST_SS_LIMIT, 0xFFFFFFFF); + vmx_vmcs_write(VMCS_GUEST_DS_LIMIT, 0xFFFFFFFF); + vmx_vmcs_write(VMCS_GUEST_ES_LIMIT, 0xFFFFFFFF); + vmx_vmcs_write(VMCS_GUEST_FS_LIMIT, 0xFFFFFFFF); + vmx_vmcs_write(VMCS_GUEST_GS_LIMIT, 0xFFFFFFFF); + vmx_vmcs_write(VMCS_GUEST_LDTR_LIMIT, 0xFFFF); + vmx_vmcs_write(VMCS_GUEST_TR_LIMIT, 0x67); + vmx_vmcs_write(VMCS_GUEST_GDTR_LIMIT, 0x28); + vmx_vmcs_write(VMCS_GUEST_IDTR_LIMIT, 0xFFFF); + + /* 段 access rights: + * CS: 0xA09B = present, DPL0, code, L=1 (64-bit), executed/read + * DS/SS/ES/FS/GS: 0xC093 = present, DPL0, data, writable + * LDTR: 0x82 = unusable (L=0 in high nibble) + * TR: 0x8B = present, DPL0, 64-bit busy TSS */ + vmx_vmcs_write(VMCS_GUEST_CS_ACCESS, 0xA09B); + vmx_vmcs_write(VMCS_GUEST_SS_ACCESS, 0xC093); + vmx_vmcs_write(VMCS_GUEST_DS_ACCESS, 0xC093); + vmx_vmcs_write(VMCS_GUEST_ES_ACCESS, 0xC093); + vmx_vmcs_write(VMCS_GUEST_FS_ACCESS, 0xC093); + vmx_vmcs_write(VMCS_GUEST_GS_ACCESS, 0xC093); + vmx_vmcs_write(VMCS_GUEST_LDTR_ACCESS, 0x82); + vmx_vmcs_write(VMCS_GUEST_TR_ACCESS, 0x8B); + + /* Guest RIP/RSP/RFLAGS */ + vmx_vmcs_write(VMCS_GUEST_RIP, 0x1000); + vmx_vmcs_write(VMCS_GUEST_RSP, 0x8000); + vmx_vmcs_write(VMCS_GUEST_RFLAGS, 0x2); + + /* Guest SYSENTER(VMX 要求字段有效,即使 guest 不使用) */ + vmx_vmcs_write(VMCS_GUEST_SYSENTER_CS, 0); + vmx_vmcs_write(VMCS_GUEST_SYSENTER_ESP, 0); + vmx_vmcs_write(VMCS_GUEST_SYSENTER_EIP, 0); + + /* Guest DR7 = 默认值 */ + vmx_vmcs_write(VMCS_GUEST_DR7, 0x400); + + /* Guest EFER:64-bit guest (CS.L=1) 要求 EFER.LME=1 且 EFER.LMA=1, + * 否则 VMX guest-state 检查失败(error 8: invalid guest state)。 + * NXE 允许 guest 使用 NX 位。 */ + vmx_vmcs_write(VMCS_GUEST_IA32_EFER, EFER_LME | EFER_LMA | EFER_NXE); + + /* Guest activity state = 0 (active) */ + vmx_vmcs_write(VMCS_GUEST_ACTIVITY_STATE, 0); + /* Guest interruptibility = 0(无阻塞) */ + vmx_vmcs_write(VMCS_GUEST_INTERRUPTIBILITY, 0); +} + +static void vmcs_setup_controls(u64 eptp) { + u64 pin = vmx_adjust_control(PIN_EXT_INTERRUPT_EXITING | PIN_NMI_EXITING, + IA32_VMX_TRUE_PINBASED_CTLS); + + u64 cpu = CPU_BASED_HLT_EXITING + | CPU_BASED_ACTIVATE_SECONDARY + | CPU_BASED_USE_MSR_BITMAPS + | CPU_BASED_UNCOND_IO_EXITING + | CPU_BASED_INVLPG_EXITING; + cpu = vmx_adjust_control(cpu, IA32_VMX_TRUE_PROCBASED_CTLS); + + /* Secondary controls:启用 EPT + VPID */ + u64 cpu2 = SEC_EXEC_ENABLE_EPT | SEC_EXEC_ENABLE_VPID; + { + u64 msr = vmx_read_msr(IA32_VMX_PROCBASED_CTLS2); + u32 allowed1 = (u32)(msr >> 32); + cpu2 &= allowed1; + cpu2 |= SEC_EXEC_ENABLE_EPT; + } + + u64 exit_ctrl = VM_EXIT_SAVE_DEBUG_CONTROLS + | VM_EXIT_HOST_ADDR_SPACE_SIZE + | VM_EXIT_LOAD_HOST_EFER; + exit_ctrl = vmx_adjust_control(exit_ctrl, IA32_VMX_TRUE_EXIT_CTLS); + + u64 entry_ctrl = VM_ENTRY_LOAD_DEBUG_CONTROLS + | VM_ENTRY_IA32E_MODE_GUEST + | VM_ENTRY_LOAD_GUEST_EFER; + entry_ctrl = vmx_adjust_control(entry_ctrl, IA32_VMX_TRUE_ENTRY_CTLS); + + vmx_vmcs_write(VMCS_PIN_BASED_VM_EXEC_CONTROL, pin); + vmx_vmcs_write(VMCS_CPU_BASED_VM_EXEC_CONTROL, cpu); + vmx_vmcs_write(VMCS_SECONDARY_VM_EXEC_CONTROL, cpu2); + vmx_vmcs_write(VMCS_VM_EXIT_CONTROLS, exit_ctrl); + vmx_vmcs_write(VMCS_VM_ENTRY_CONTROLS, entry_ctrl); + + /* MSR bitmap 物理地址 */ + { + u64 hhdm = g_hhdm_request.response ? g_hhdm_request.response->offset : 0; + u64 msr_bitmap_phys = (u64)g_msr_bitmap - hhdm; + vmx_vmcs_write(VMCS_MSR_BITMAP, msr_bitmap_phys); + } + + vmx_vmcs_write(VMCS_EPT_POINTER, eptp); + vmx_vmcs_write(VMCS_VPID, 1); + + /* Exception bitmap:捕获 #GP(13)/#PF(14)/#UD(6) */ + vmx_vmcs_write(VMCS_EXCEPTION_BITMAP, (1ULL << 13) | (1ULL << 14) | (1ULL << 6)); +} + +/* ===== VMM API ===== */ + +int vmm_init(void) { + log_info("[VMM] init begin"); + + if (vmx_enable() != 0) { + log_error("[VMM] VMX enable failed"); + return -1; + } + + if (ept_init() != 0) { + log_error("[VMM] EPT init failed"); + return -2; + } + + if (vmx_vmcs_alloc(&g_vmcs_phys) != 0) { + log_error("[VMM] VMCS alloc failed"); + return -3; + } + log_hex64("[VMM] VMCS phys=", g_vmcs_phys); + + if (vmx_vmcs_load(g_vmcs_phys) != 0) { + log_error("[VMM] VMCS load failed"); + return -4; + } + + /* VMCS 字段读写自检 */ + vmx_vmcs_write(VMCS_VPID, 0x1234); + u64 vpid_readback = vmx_vmcs_read(VMCS_VPID); + if (vpid_readback != 0x1234) { + log_error("[VMM] VMCS read/write verify failed"); + log_hex64("[VMM] wrote=0x1234 read=", vpid_readback); + return -5; + } + vmx_vmcs_write(VMCS_VPID, 1); + + g_vmm_ready = 1; + log_info("[VMM] init ok"); + return 0; +} + +void vmm_shutdown(void) { + if (!g_vmm_ready) return; + vmx_disable(); + g_vmm_ready = 0; +} + +int vmm_is_ready(void) { return g_vmm_ready; } +u64 vmm_get_eptp(void) { return ept_get_eptp(); } + +int vmm_self_test(void) { + if (!g_vmm_ready) { + log_error("[VMM] not ready"); + return -1; + } + + log_info("[VMM] self-test begin"); + + /* 重置状态 */ + g_guest_terminated = 0; + g_last_exit_reason = 0xFFFFFFFFULL; + + /* 构建 guest GDT + TSS(满足 VMX 对 guest TR 非空的要求) */ + build_guest_gdt(); + + /* EPT 映射 guest 代码页(GPA 0x1000)、栈页(GPA 0x8000)、 + * GDT 页(GPA 0x3000)、TSS 页(GPA 0x4000)。 + * g_guest_* 是 UTSM 虚拟地址,物理地址 = virt - HHDM */ + u64 hhdm = g_hhdm_request.response ? g_hhdm_request.response->offset : 0; + u64 guest_code_phys = (u64)g_guest_code - hhdm; + u64 guest_stack_phys = (u64)g_guest_stack - hhdm; + u64 guest_gdt_phys = (u64)g_guest_gdt - hhdm; + u64 guest_tss_phys = (u64)g_guest_tss - hhdm; + + if (ept_map_range(0x1000, guest_code_phys, 4096, EPT_RWX) != 0) { + log_error("[VMM] map guest code failed"); + return -3; + } + if (ept_map_range(0x8000, guest_stack_phys, 4096, EPT_RWX) != 0) { + log_error("[VMM] map guest stack failed"); + return -4; + } + if (ept_map_range(0x3000, guest_gdt_phys, 4096, EPT_READ | EPT_WRITE) != 0) { + log_error("[VMM] map guest GDT failed"); + return -5; + } + if (ept_map_range(0x4000, guest_tss_phys, 4096, EPT_READ | EPT_WRITE) != 0) { + log_error("[VMM] map guest TSS failed"); + return -6; + } + + /* 配置 VMCS */ + vmcs_setup_guest_state_phase1(); + vmcs_setup_controls(ept_get_eptp()); + vmcs_setup_host_state(); + + log_info("[VMM] launching guest"); + + /* vmlaunch 控制流: + * - 失败:执行下一条指令,failed=1 + * - 成功:进入 guest,不返回。VM-Exit 后跳到 vmx_vm_exit_handler。 + * - guest 终止时,handler 恢复 saved_rsp 并 jmp 到 post_guest 标签。 + * + * 我们在 vmlaunch 前保存 RSP 与"vmlaunch 后"的 RIP(= post_guest 标签地址)。 */ + int failed; + __asm__ volatile( + "movq %%rsp, g_saved_host_rsp(%%rip)\n\t" /* 保存当前 RSP */ + "leaq 1f(%%rip), %%rax\n\t" /* 取 post_guest 标签地址 */ + "movq %%rax, g_saved_return_rip(%%rip)\n\t" /* 保存返回 RIP */ + "vmlaunch\n\t" /* 启动 guest */ + /* ---- 失败路径 ---- */ + "movl $1, %0\n\t" + "jmp 2f\n\t" + /* ---- post_guest 标签:终止路径 jmp 到这里 ---- */ + "1:\n\t" + "movl $0, %0\n\t" + "2:\n\t" + : "=r"(failed) + :: "rax", "rcx", "rdx", "rsi", "rdi", "r8", "r9", "r10", "r11", "memory" + ); + + if (failed) { + u64 error = vmx_vmcs_read(VMCS_VMX_INSTRUCTION_ERROR); + log_hex64("[VMM] vmlaunch failed, error=", error); + serial_write("[VMM] "); + serial_write(vmx_error_str(error)); + serial_putc('\n'); + return -7; + } + + /* 到达这里:guest 已终止(HLT 被处理) */ + log_info("[VMM] guest terminated successfully"); + log_hex64("[VMM] last exit reason=", g_last_exit_reason); + log_hex64("[VMM] vmexit count=", vmexit_get_count()); + + if (g_last_exit_reason != EXIT_HLT) { + log_warn("[VMM] expected HLT exit"); + return -8; + } + + log_info("[VMM] self-test PASS"); + return 0; +} + +/* 由 vmexit_asm.S 调用:dispatch 返回 1 = vmresume,返回 0 = 终止。 + * 终止时,asm 侧恢复 saved_rsp 并 jmp saved_return_rip。 + * + * 注意:vmresume 现在在 vmexit_asm.S 中执行(在恢复 guest GPR 之后), + * 不再在 C 中调用。这样 C 处理器可以修改 g_guest_regs.rax 等, + * 汇编恢复后 vmresume 时 CPU GPR = 修改后的 guest 值。 */ +void vmm_vmexit_entry(void) { + int resume = vmexit_dispatch(); + g_last_exit_reason = vmx_vmcs_read(VMCS_EXIT_REASON) & 0xFFFF; + + if (resume) { + /* 汇编侧会恢复 GPR 并执行 vmresume,无需在此调用。 + * 如果 vmresume 失败,汇编会设置 g_guest_terminated=1。 */ + } else { + g_guest_terminated = 1; + } +} + +/* 供 vmexit_asm.S 读取的全局变量符号已在文件顶部声明为非 static 全局变量。 */ diff --git a/CODE/UTSM/vmm/vmx.c b/CODE/UTSM/vmm/vmx.c new file mode 100644 index 0000000..92555f9 --- /dev/null +++ b/CODE/UTSM/vmm/vmx.c @@ -0,0 +1,257 @@ +#include +#include +#include +#include +#include +#include "../arch/x86_64/limine.h" + +extern volatile struct limine_hhdm_request g_hhdm_request; + +/* ===== MSR / CR helpers ===== */ + +u64 vmx_read_msr(u32 msr) { + u32 lo, hi; + __asm__ volatile("rdmsr" : "=a"(lo), "=d"(hi) : "c"(msr)); + return ((u64)hi << 32) | lo; +} + +static void vmx_write_msr(u32 msr, u64 value) { + u32 lo = (u32)(value & 0xFFFFFFFFULL); + u32 hi = (u32)(value >> 32); + __asm__ volatile("wrmsr" :: "a"(lo), "d"(hi), "c"(msr)); +} + +static inline u64 read_cr0(void) { + u64 v; + __asm__ volatile("mov %%cr0, %0" : "=r"(v)); + return v; +} + +static inline u64 read_cr4(void) { + u64 v; + __asm__ volatile("mov %%cr4, %0" : "=r"(v)); + return v; +} + +static inline void write_cr4(u64 v) { + __asm__ volatile("mov %0, %%cr4" :: "r"(v)); +} + +static inline u64 read_cr3(void) { + u64 v; + __asm__ volatile("mov %%cr3, %0" : "=r"(v)); + return v; +} + +static inline u64 read_rflags(void) { + u64 v; + __asm__ volatile("pushfq; popq %0" : "=r"(v)); + return v; +} + +/* ===== CPUID ===== */ + +static void cpuid(u32 leaf, u32 *eax, u32 *ebx, u32 *ecx, u32 *edx) { + __asm__ volatile("cpuid" + : "=a"(*eax), "=b"(*ebx), "=c"(*ecx), "=d"(*edx) + : "a"(leaf)); +} + +int vmx_supported(void) { + u32 eax, ebx, ecx, edx; + cpuid(1, &eax, &ebx, &ecx, &edx); + return (ecx & (1U << 5)) != 0; /* CPUID.1:ECX[5] = VMX */ +} + +/* ===== VMXON region ===== */ + +static u64 g_vmxon_phys; +static u64 g_vmcs_revision; +static u64 g_vmx_basic; +static int g_vmx_enabled; + +/* VMX 要求 CR0 固定位必须为 1(VMX operation 不允许清除)。 + * 这些位由 IA32_VMX_BASIC MSR 的高 32 位(CR0 fixed-0)和 + * IA32_VMX_CR0_FIXED1 MSR 决定。简化:直接 OR 上 PE/NE/PG/WP。 */ +static u64 vmx_required_cr0(void) { + return read_cr0() | CR0_PE | CR0_NE | CR0_WP | CR0_PG; +} + +static u64 vmx_required_cr4(void) { + return read_cr4() | CR4_VMXE | CR4_PAE | CR4_PGE; +} + +int vmx_enable(void) { + if (g_vmx_enabled) return 0; + + if (!vmx_supported()) { + log_error("[VMX] CPU does not support VMX"); + return -1; + } + + /* 读 IA32_VMX_BASIC,获取 VMCS revision ID 与 region 大小 */ + g_vmx_basic = vmx_read_msr(IA32_VMX_BASIC); + g_vmcs_revision = g_vmx_basic & 0x7FFFFFFFULL; + u32 vmcs_size = (u32)((g_vmx_basic >> 32) & 0x1FFF); + if (vmcs_size == 0) vmcs_size = 4096; + + log_hex64("[VMX] VMX_BASIC=", g_vmx_basic); + log_hex64("[VMX] VMCS revision=", g_vmcs_revision); + + /* 分配 VMXON region(4KB 对齐,物理页) */ + dkm_dma_buffer vmxon_buf; + if (dma_alloc_pages(1, EPT_PAGE_SIZE, 0x100000000ULL, &vmxon_buf) != 0) { + log_error("[VMX] failed to alloc VMXON region"); + return -2; + } + g_vmxon_phys = vmxon_buf.phys; + + /* 在 VMXON region 偏移 0 写入 VMCS revision ID */ + u32 *vmxon_ptr = (u32 *)vmxon_buf.virt; + vmxon_ptr[0] = (u32)g_vmcs_revision; + log_hex64("[VMX] VMXON phys=", g_vmxon_phys); + + /* 设置 CR4.VMXE */ + u64 cr4 = vmx_required_cr4(); + write_cr4(cr4); + log_hex64("[VMX] CR4=", cr4); + + /* 确保 CR0 满足 VMX 要求(这里只读不写,UEFI 已配置好) */ + (void)vmx_required_cr0(); + + /* 执行 vmxon */ + int err; + __asm__ volatile( + "vmxon %1\n" + "jnc 1f\n" + "jmp 2f\n" + "1:\n" + "mov $0, %0\n" + "jmp 3f\n" + "2:\n" + "mov $1, %0\n" + "3:\n" + : "=r"(err) + : "m"(g_vmxon_phys) + : "memory" + ); + + if (err) { + /* 第二次尝试:读 vmx instruction error 通过 VMCS 不适用 vmxon; + * 简化:返回失败。 */ + log_error("[VMX] vmxon failed"); + return -3; + } + + g_vmx_enabled = 1; + log_info("[VMX] VMX root mode enabled"); + return 0; +} + +int vmx_disable(void) { + if (!g_vmx_enabled) return 0; + __asm__ volatile("vmxoff" ::: "memory"); + g_vmx_enabled = 0; + /* 清 CR4.VMXE */ + u64 cr4 = read_cr4(); + cr4 &= ~CR4_VMXE; + write_cr4(cr4); + log_info("[VMX] VMX root mode disabled"); + return 0; +} + +int vmx_vmcs_alloc(u64 *phys_out) { + if (!phys_out) return -1; + dkm_dma_buffer buf; + if (dma_alloc_pages(1, EPT_PAGE_SIZE, 0x100000000ULL, &buf) != 0) { + log_error("[VMX] failed to alloc VMCS region"); + return -2; + } + /* 偏移 0 写入 VMCS revision ID */ + u32 *p = (u32 *)buf.virt; + p[0] = (u32)g_vmcs_revision; + *phys_out = buf.phys; + return 0; +} + +int vmx_vmcs_load(u64 phys) { + int err; + __asm__ volatile( + "vmptrld %1\n" + "jnc 1f\n" + "jmp 2f\n" + "1:\n" + "mov $0, %0\n" + "jmp 3f\n" + "2:\n" + "mov $1, %0\n" + "3:\n" + : "=r"(err) + : "m"(phys) + : "memory" + ); + if (err) { + log_error("[VMX] vmptrld failed"); + return -1; + } + return 0; +} + +u64 vmx_vmcs_read(u64 field) { + u64 value; + __asm__ volatile( + "vmread %1, %0\n" + : "=r"(value) + : "r"(field) + : "memory" + ); + return value; +} + +void vmx_vmcs_write(u64 field, u64 value) { + __asm__ volatile( + "vmwrite %1, %0\n" + :: "r"(field), "r"(value) + : "memory" + ); +} + +int vmx_vmlaunch(void) { + int err; + __asm__ volatile( + "vmlaunch\n" + "jnc 1f\n" + "jmp 2f\n" + "1:\n" + "mov $0, %0\n" + "jmp 3f\n" + "2:\n" + "mov $1, %0\n" + "3:\n" + : "=r"(err) + :: "memory" + ); + return err; +} + +int vmx_vmresume(void) { + int err; + __asm__ volatile( + "vmresume\n" + "jnc 1f\n" + "jmp 2f\n" + "1:\n" + "mov $0, %0\n" + "jmp 3f\n" + "2:\n" + "mov $1, %0\n" + "3:\n" + : "=r"(err) + :: "memory" + ); + return err; +} + +/* 暴露给其它模块用的辅助 */ +u64 vmx_get_host_cr3(void) { return read_cr3(); } +u64 vmx_get_host_rflags(void) { return read_rflags(); } diff --git a/CODE/desktop/linker.ld b/CODE/desktop/linker.ld index 473e5fc..ffdf62d 100644 --- a/CODE/desktop/linker.ld +++ b/CODE/desktop/linker.ld @@ -17,8 +17,10 @@ SECTIONS *(.data*) *(.data.rel.ro*) *(COMMON) - *(.bss*) } :data .rela.dyn : ALIGN(8) { *(.rela.dyn*) } :data .dynamic : ALIGN(8) { *(.dynamic*) } :data :dynamic + .bss : ALIGN(4K) { + *(.bss*) + } :data } diff --git a/CODE/desktop/main.c b/CODE/desktop/main.c index 5e25b72..bbfffe7 100644 --- a/CODE/desktop/main.c +++ b/CODE/desktop/main.c @@ -1,20 +1,38 @@ -/* Deshab Desktop Manager — 图形桌面环境 +/* Deshab Desktop Manager — Winux-Kate 结构完整复刻 * - * 功能:窗口管理、任务栏、桌面图标、事件路由、应用加载 - * 风格:Sealed Arc 视觉设计语言 - * 输入:PS/2 键盘 + PS/2 鼠标 + * 布局结构与 D:\Code\Winux-Kate 完全一致: + * app-shell + * ├── topbar (34px):品牌 + 工作区切换 + 页面切换 + PG 时钟 + * ├── page-host + * │ ├── 1·DASHBOARD:TERM-01 / TERM-02 / EDITOR / FILES 四面板 + StatusBar + * │ ├── 2·IDE:标签栏 + 编辑宿主 + * │ ├── 3·IM:栏 + 分屏槽(未检测卡片) + * │ ├── 4·DESKTOP:图标网格 + 窗口 + 任务栏 + * │ └── 5+/CUSTOM:外部 .elf 全屏宿主(双击启动) + * └── task-view overlay(工作区卡片) + * + * 新 UI 特性(Kate 之外): + * - RTC 实时时钟(秒级)+ 开机至今 UP 计时 + * - VOL / BRIGHT 可拖滑块(BRIGHT 真实调暗全屏) + * - 开机打字机 Boot 屏 + * - FILES 面板双击文件载入 EDITOR(FAT32 真实读写 + SAVE 回写) + * - 窗口单实例聚焦策略 */ #include "../UTSM/include/utsm/dsk.h" -/* block_read 函数类型(从 kernel_api + 0xA8 获取) */ -typedef int (*desktop_block_read_fn)(void *ctx, unsigned long long lba, unsigned int count, void *buffer); +/* block 设备函数类型(从 kernel_api + 0xA8 获取 block_api,read @ +0x10, write @ +0x18) */ +typedef int (*desktop_block_read_fn)(u32 index, u64 lba, u32 count, void *buffer); +typedef int (*desktop_block_write_fn)(u32 index, u64 lba, u32 count, const void *buffer); /* 先包含 ascii_bitmaps.c(定义 g_ascii),再包含 deshab_ui.h(引用 g_ascii) */ #include "../firstInit/ascii_bitmaps.c" #include "../UTSM/include/utsm/deshab_ui.h" +/* 用户态 FAT32 读写 */ +#include "../tools/fat32_io.h" typedef unsigned char u8; +typedef signed char i8; typedef unsigned short u16; typedef unsigned int u32; typedef unsigned long long u64; @@ -32,24 +50,57 @@ static void sputc(char c) { static void swrite(const char *s) { while(*s) { if(*s=='\n')sputc('\r'); sputc(*s++); } } static void slog(const char *s) { swrite("[desktop] "); swrite(s); swrite("\n"); } +static int kstrlen(const char *s) { int n = 0; while (s[n]) n++; return n; } +static void kstrcpy(char *d, const char *s, int cap) { + int i = 0; + for (; i < cap - 1 && s[i]; i++) d[i] = s[i]; + d[i] = 0; +} +static void kstrcat(char *d, const char *s, int cap) { + int n = kstrlen(d); + if (n < cap) kstrcpy(d + n, s, cap - n); +} + +static u64 rdtsc(void) { + u32 lo, hi; + __asm__ volatile("rdtsc" : "=a"(lo), "=d"(hi)); + return ((u64)hi << 32) | lo; +} + /* 前向声明 */ static void redraw_all(void); static int ps2_mouse_poll(void); +static void draw_statusbar_full(void); +static void files_refresh(void); /* ============================================================ - * 常量 + * 常量(Winux-Kate 布局) * ============================================================ */ #define MAX_WINDOWS 8 -#define TASKBAR_H 40 -#define TITLEBAR_H 28 -#define BORDER_W 2 -#define DESKTOP_ICON_W 80 -#define DESKTOP_ICON_H 72 -#define ICON_GUTTER_X 16 -#define ICON_GUTTER_Y 16 +#define BORDER_W 1 #define CURSOR_SIZE 24 +#define ICON_W KATE_ICON_W /* 104 */ +#define ICON_H KATE_ICON_H /* 90 */ +#define ICON_GAP KATE_ICON_GAP /* 18 */ +#define ICON_PAD KATE_ICON_PAD /* 24 */ + +#define CLOSE_BTN_SIZE 16 +#define CLOSE_BTN_OFFSET 22 + +#define MAX_WS 6 /* 工作区上限 */ +#define MAX_CPAGES 4 /* 自定义页上限 */ +#define MAX_FILES 24 /* FILES 面板条目上限 */ + +/* 应用固定状态地址(单实例策略 → 每类应用唯一实例) */ +#define ADDR_TERM1 0x4000000ULL +#define ADDR_TERM2 0x4100000ULL +#define ADDR_TERMWIN 0x4200000ULL +#define ADDR_EDDASH 0x5000000ULL +#define ADDR_EDWIN 0x5100000ULL +#define ADDR_CALC 0x6000000ULL + /* ============================================================ * 事件系统 * ============================================================ */ @@ -64,7 +115,7 @@ typedef enum { typedef struct { ev_type type; - int mx, my; /* 鼠标全局坐标 */ + int mx, my; int button; /* 0=左, 1=右 */ u8 scancode; int shift, ctrl, alt; @@ -81,18 +132,13 @@ typedef struct desktop_window { int focused; int minimized; char title[48]; - int app_id; /* 关联应用 ID */ - void *app_state; /* 应用私有状态 */ - int closing; /* 关闭中标记 */ - int dirty; /* 需要重绘 */ + int app_id; + void *app_state; + int closing; + int dirty; + int ws; /* 所属工作区 */ } desktop_window; -/* ============================================================ - * 应用描述符 - * ============================================================ */ - -/* 前向声明 */ -struct desktop_window; typedef struct desktop_window desktop_win; typedef struct app_ctx { @@ -100,7 +146,8 @@ typedef struct app_ctx { desktop_win *win; int client_x, client_y, client_w, client_h; int should_exit; - desktop_block_read_fn block_read; + desktop_block_read_fn block_read; + desktop_block_write_fn block_write; const void *kernel_api; } app_ctx; @@ -117,8 +164,39 @@ typedef struct { app_event_fn on_event; app_draw_fn on_draw; app_destroy_fn on_destroy; + const char *elf_name; } app_descriptor; +/* 桌面图标 */ +typedef struct { + int app_id; + int x, y; + const char *label; + int selected; +} desktop_icon; + +#define MAX_ICONS 8 + +/* 工作区 */ +typedef struct { + char name[16]; + int page; /* 该工作区离开时的页面 */ +} workspace; + +/* 自定义页 */ +typedef struct { + char name[12]; + char elf11[11]; + int id; /* 页面号 5..8 */ +} cpage; + +/* FILES 面板条目 */ +typedef struct { + char name11[11]; + char disp[13]; + u32 size; +} fentry; + /* ============================================================ * 全局状态 * ============================================================ */ @@ -126,11 +204,14 @@ typedef struct { static du_context g_fb; static u64 g_fb_addr, g_fb_w, g_fb_h, g_fb_pitch; static const void *g_kernel_api; -static desktop_block_read_fn g_block_read; +static desktop_block_read_fn g_block_read; +static desktop_block_write_fn g_block_write; +static const dsk_boot_context *g_boot_ctx = 0; /* 鼠标状态 */ static int g_mouse_x = 400, g_mouse_y = 300; -static int g_mouse_btn = 0; /* bit0=左键 */ +static int g_mouse_btn = 0; /* bit0=左 bit1=右 */ +static int g_left_pressed = 0, g_left_released = 0, g_right_pressed = 0; static u8 g_mouse_buf[3]; static int g_mouse_idx = 0; static int g_mouse_has_pkt = 0; @@ -160,17 +241,75 @@ static int g_drag_off_x, g_drag_off_y; static app_descriptor g_apps[MAX_APPS]; static int g_app_count = 0; -/* 桌面图标 */ -typedef struct { - int app_id; - int x, y; /* 图标位置 */ - const char *label; -} desktop_icon; - -#define MAX_ICONS 8 static desktop_icon g_icons[MAX_ICONS]; static int g_icon_count = 0; +/* ---- Kate 外壳状态 ---- */ +static int g_booted = 0; +static int g_page = 1; /* 1..4 内置, 5..8 自定义 */ +static int g_taskview = 0; +static int g_dash_focus = 0; /* 0=t1 1=t2 2=ed 3=files */ + +static workspace g_ws[MAX_WS]; +static int g_ws_count = 1; +static int g_ws_cur = 0; + +static cpage g_cpages[MAX_CPAGES]; +static int g_cpage_count = 0; + +/* 右键菜单(移到工作区) */ +static int g_ctx_open = 0; +static int g_ctx_x = 0, g_ctx_y = 0; +static int g_ctx_win = -1; + +/* 状态栏滑块 */ +static int g_vol = 65; +static int g_bright = 100; +static int g_drag_slider = 0; /* 1=vol 2=bright */ +static int g_vol_rect[4]; /* x,y,w,h 滑轨 */ +static int g_bri_rect[4]; + +static int g_quit = 0; /* EXIT 按钮 → 返回 DSK */ + +/* 时钟 */ +static int g_rtc_boot_sec = 0; /* 开机时刻(秒级当天秒) */ +static int g_last_sec = -1; +static u64 g_tsc_per_sec = 0; + +/* FILES 面板 */ +static fentry g_files[MAX_FILES]; +static int g_file_count = 0; +static int g_file_sel = -1; +static int g_files_loaded = 0; + +/* 双击检测 */ +static u64 g_last_click_tsc = 0; +static int g_last_click_x = -100, g_last_click_y = -100; + +/* 面板/控件命中矩形(绘制时更新,点击时使用) */ +static int g_dash_rect[4][4]; /* 4 面板 x,y,w,h */ +static int g_save_rect[4]; +static int g_pg_rect[8][4]; /* 页面切换按钮 */ +static int g_pg_rm_rect[8][4]; +static int g_pgadd_rect[4]; +static int g_tvbtn_rect[4]; /* 任务视图按钮 */ +static int g_ws_rect[MAX_WS][4]; +static int g_wsadd_rect[4]; +static int g_tb_rect[MAX_WINDOWS][4]; /* 任务栏项 */ +static int g_tb_close_rect[MAX_WINDOWS][4]; +static int g_tb_quit_rect[4]; +static int g_tb_adopt_rect[4]; /* 收纳窗口按钮 */ +static int g_tv_card[MAX_WS + 1][4]; /* 任务视图卡片(+add) */ +static int g_tv_close[MAX_WS][4]; +static int g_ctx_rect[4]; +static int g_ctx_item[MAX_WS + 1][4]; +static int g_ctx_item_ws[MAX_WS + 1]; /* 菜单行 → 真实工作区索引,-1=新建 */ +static int g_ide_host[4]; +static int g_ide_tabx[4]; /* IDE 标签 × */ +static int g_ide_new_rect[4]; /* IDE "+ NEW IDE" 按钮 */ +static int g_im_toggle_rect[4]; /* IM bar 切换视图按钮 */ +static int g_cp_host[4]; /* 自定义页宿主 */ + /* ============================================================ * 箭头光标形状(24×24,1=前景 0=透明) * ============================================================ */ @@ -203,70 +342,130 @@ static const u8 cursor_shape[24][24] = { }; /* ============================================================ - * 应用图标形状(32×32 像素数据,用程序化方式生成) + * RTC 时钟 * ============================================================ */ -/* 绘制终端图标(32×32)到指定位置 */ -static void draw_icon_terminal(du_context *ctx, int cx, int cy) { - u32 bg = DP_ABYSS_700; - u32 fg = DP_SEAL_300; - u32 border = DP_SEAL_500; - /* 圆角矩形背景 */ - du_fill_rounded_rect(ctx, cx, cy, 32, 32, bg, 6); - du_rect_outline(ctx, cx, cy, 32, 32, border, 6); - /* >_ 符号 */ - du_fill_rect(ctx, cx+5, cy+10, 8, 2, fg); - du_fill_rect(ctx, cx+5, cy+10, 2, 6, fg); - du_fill_rect(ctx, cx+13, cy+12, 2, 2, fg); - du_fill_rect(ctx, cx+17, cy+16, 6, 2, fg); +static int bcd2bin(u8 v) { return (v >> 4) * 10 + (v & 0xF); } + +static void rtc_read(int *yy, int *mo, int *dd, int *hh, int *mi, int *ss) { + outb(0x70, 0x00); *ss = bcd2bin(inb(0x71)); + outb(0x70, 0x02); *mi = bcd2bin(inb(0x71)); + outb(0x70, 0x04); *hh = bcd2bin(inb(0x71)); + outb(0x70, 0x07); *dd = bcd2bin(inb(0x71)); + outb(0x70, 0x08); *mo = bcd2bin(inb(0x71)); + outb(0x70, 0x09); *yy = bcd2bin(inb(0x71)); } -/* 绘制编辑器图标(32×32) */ -static void draw_icon_editor(du_context *ctx, int cx, int cy) { - u32 bg = DP_ABYSS_700; - u32 fg = DP_SEAL_500; - u32 border = DP_SEAL_300; - du_fill_rounded_rect(ctx, cx, cy, 32, 32, bg, 6); - du_rect_outline(ctx, cx, cy, 32, 32, border, 6); - /* 文本行 */ - for (int i = 0; i < 5; i++) { - int lw = (i == 0) ? 20 : (i == 3 ? 12 : 16); - du_fill_rect(ctx, cx+6, cy+6+i*5, lw, 2, fg); +static int rtc_day_sec(void) { + int y, mo, d, h, mi, s; + rtc_read(&y, &mo, &d, &h, &mi, &s); + return h * 3600 + mi * 60 + s; +} + +/* 格式化 HH:MM:SS */ +static void fmt_hms(char *out, int sec) { + int h = sec / 3600, m = (sec / 60) % 60, s = sec % 60; + out[0] = '0' + h / 10; out[1] = '0' + h % 10; out[2] = ':'; + out[3] = '0' + m / 10; out[4] = '0' + m % 10; out[5] = ':'; + out[6] = '0' + s / 10; out[7] = '0' + s % 10; out[8] = 0; +} + +/* 校准 TSC(约 1 秒):用于双击间隔判定 */ +static void tsc_calibrate(void) { + int s0 = rtc_day_sec(); + while (rtc_day_sec() == s0) __asm__("pause"); + u64 t0 = rdtsc(); + int s1 = rtc_day_sec(); + while (rtc_day_sec() == s1) __asm__("pause"); + g_tsc_per_sec = rdtsc() - t0; +} + +/* ============================================================ + * Winux-Kate 应用图标(40×40,青色几何图形) + * ============================================================ */ + +static void draw_circle_outline(du_context *ctx, int cx, int cy, int r, u32 color) { + for (int dy = -r; dy <= r; dy++) { + for (int dx = -r; dx <= r; dx++) { + int d2 = dx * dx + dy * dy; + if (d2 <= r * r && d2 > (r - 1) * (r - 1)) { + du_pixel(ctx, cx + dx, cy + dy, color); + } + } } } -/* 绘制文件夹图标(32×32) */ -static void draw_icon_folder(du_context *ctx, int cx, int cy) { - u32 bg = DP_ARC_500; - u32 fg = DP_ARC_300; - u32 border = DP_ARC_700; - du_fill_rounded_rect(ctx, cx, cy, 32, 32, bg, 6); - du_rect_outline(ctx, cx, cy, 32, 32, border, 6); - /* 文件夹标签 */ - du_fill_rect(ctx, cx+4, cy+6, 12, 3, fg); - /* 文件夹主体 */ - du_fill_rect(ctx, cx+4, cy+12, 24, 2, border); - du_fill_rect(ctx, cx+4, cy+14, 24, 12, fg); +static void draw_icon_terminal(du_context *ctx, int cx, int cy) { + du_fill_rect(ctx, cx, cy, 40, 40, KS_BG_TERTIARY); + du_rect_outline(ctx, cx, cy, 40, 40, KS_ACCENT, 2); + du_fill_rect(ctx, cx + 8, cy + 12, 2, 2, KS_ACCENT2); + du_fill_rect(ctx, cx + 10, cy + 14, 2, 2, KS_ACCENT2); + du_fill_rect(ctx, cx + 12, cy + 16, 2, 2, KS_ACCENT2); + du_fill_rect(ctx, cx + 10, cy + 18, 2, 2, KS_ACCENT2); + du_fill_rect(ctx, cx + 8, cy + 20, 2, 2, KS_ACCENT2); + du_fill_rect(ctx, cx + 16, cy + 22, 8, 2, KS_ACCENT); +} + +static void draw_icon_editor(du_context *ctx, int cx, int cy) { + du_fill_rect(ctx, cx, cy, 40, 40, KS_BG_TERTIARY); + du_rect_outline(ctx, cx, cy, 40, 40, KS_ACCENT, 2); + int widths[] = {24, 20, 22, 14, 20, 18}; + for (int i = 0; i < 6; i++) { + du_fill_rect(ctx, cx + 8, cy + 8 + i * 4, widths[i], 2, + (i == 0) ? KS_ACCENT2 : KS_ACCENT); + } +} + +static void draw_icon_folder(du_context *ctx, int cx, int cy) { + du_fill_rect(ctx, cx, cy, 40, 40, KS_BG_TERTIARY); + du_rect_outline(ctx, cx, cy, 40, 40, KS_BORDER, 2); + du_fill_rect(ctx, cx + 8, cy + 10, 12, 4, KS_ACCENT_DIM); + du_rect_outline(ctx, cx + 8, cy + 10, 12, 4, KS_ACCENT, 1); + du_fill_rect(ctx, cx + 6, cy + 14, 28, 16, KS_BG_SECONDARY); + du_rect_outline(ctx, cx + 6, cy + 14, 28, 16, KS_ACCENT, 1); + du_fill_rect(ctx, cx + 6, cy + 14, 28, 1, KS_ACCENT); + du_fill_rect(ctx, cx + 10, cy + 19, 12, 1, KS_ACCENT2); + du_fill_rect(ctx, cx + 10, cy + 23, 16, 1, KS_TEXT_DIM); + du_fill_rect(ctx, cx + 10, cy + 26, 10, 1, KS_TEXT_DIM); } -/* 绘制计算器图标(32×32) */ static void draw_icon_calc(du_context *ctx, int cx, int cy) { - u32 bg = DP_ABYSS_700; - u32 fg = DP_WARNING; - u32 border = DP_WARNING; - du_fill_rounded_rect(ctx, cx, cy, 32, 32, bg, 6); - du_rect_outline(ctx, cx, cy, 32, 32, border, 6); - /* 屏幕 */ - du_fill_rect(ctx, cx+4, cy+4, 24, 6, DP_ABYSS_900); - du_fill_rect(ctx, cx+22, cy+5, 4, 4, fg); - /* 按键网格 */ + du_fill_rect(ctx, cx, cy, 40, 40, KS_BG_TERTIARY); + du_rect_outline(ctx, cx, cy, 40, 40, KS_ACCENT, 2); + du_fill_rect(ctx, cx + 6, cy + 6, 28, 8, KS_BG_PRIMARY); + du_rect_outline(ctx, cx + 6, cy + 6, 28, 8, KS_ACCENT2, 1); + du_fill_rect(ctx, cx + 28, cy + 8, 4, 4, KS_ACCENT2); for (int r = 0; r < 3; r++) { for (int c = 0; c < 3; c++) { - du_fill_rect(ctx, cx+4+c*8, cy+14+r*6, 6, 4, DP_NEUTRAL_400); + int bx = cx + 6 + c * 9; + int by = cy + 18 + r * 7; + du_fill_rect(ctx, bx, by, 7, 5, KS_ACCENT_DIM); + du_rect_outline(ctx, bx, by, 7, 5, KS_ACCENT, 1); } } } +static void draw_icon_browser(du_context *ctx, int cx, int cy) { + du_fill_rect(ctx, cx, cy, 40, 40, KS_BG_TERTIARY); + du_rect_outline(ctx, cx, cy, 40, 40, KS_ACCENT, 2); + int ccx = cx + 20, ccy = cy + 20, r = 11; + draw_circle_outline(ctx, ccx, ccy, r, KS_ACCENT); + du_fill_rect(ctx, ccx - r, ccy, 2 * r + 1, 1, KS_ACCENT); + du_fill_rect(ctx, ccx, ccy - r, 1, 2 * r + 1, KS_ACCENT); + du_pixel(ctx, ccx, ccy, KS_ACCENT2); +} + +static void draw_app_icon(du_context *ctx, int app_id, int cx, int cy) { + switch (app_id) { + case 0: draw_icon_terminal(ctx, cx, cy); break; + case 1: draw_icon_editor(ctx, cx, cy); break; + case 2: draw_icon_folder(ctx, cx, cy); break; + case 3: draw_icon_calc(ctx, cx, cy); break; + case 4: draw_icon_browser(ctx, cx, cy); break; + default: draw_icon_terminal(ctx, cx, cy); break; + } +} + /* ============================================================ * 光标管理 * ============================================================ */ @@ -306,15 +505,13 @@ static void cursor_draw(int mx, int my) { for (int r = 0; r < CURSOR_SIZE; r++) { for (int c = 0; c < CURSOR_SIZE; c++) { if (cursor_shape[r][c]) { - du_pixel(&g_fb, mx + c, my + r, DS_DARK_TEXT_PRIMARY); + du_pixel(&g_fb, mx + c, my + r, KS_ACCENT); } } } } -/* 非阻塞延时:动画期间持续轮询鼠标,保持光标响应。 - * 将 ms 个 50000-iter pause 块拆开,每块结束后轮询一次鼠标。 - * 若鼠标有新数据包,立即擦除旧光标、在新位置保存背景并重绘光标。 */ +/* 非阻塞延时:动画期间持续轮询鼠标,保持光标响应。 */ static void anim_delay_poll_mouse(u32 ms) { for (u32 i = 0; i < ms; i++) { for (volatile u32 j = 0; j < 50000; j++) { @@ -340,10 +537,10 @@ static desktop_window *win_find(int id) { } static desktop_window *win_find_at(int mx, int my) { - /* 从后往前搜索(后面的窗口在上面) */ for (int i = g_win_count - 1; i >= 0; i--) { desktop_window *w = &g_windows[i]; if (!w->visible || w->minimized) continue; + if (w->ws != g_ws_cur) continue; if (mx >= w->x && mx < w->x + w->w && my >= w->y && my < w->y + w->h) { return w; } @@ -364,6 +561,15 @@ static void win_bring_to_front(desktop_window *w) { g_windows[g_win_count - 1] = tmp; } +static void win_focus(desktop_window *w) { + int id = w->id; /* bring_to_front 会重排数组,先保存 id 避免悬空指针 */ + win_bring_to_front(w); + for (int i = 0; i < g_win_count; i++) + g_windows[i].focused = (g_windows[i].id == id); + g_focused_win = id; + if (g_win_count > 0) g_windows[g_win_count - 1].minimized = 0; +} + static desktop_window *win_create(int app_id, const char *title, int x, int y, int w, int h) { if (g_win_count >= MAX_WINDOWS) return 0; desktop_window *win = &g_windows[g_win_count++]; @@ -377,10 +583,9 @@ static desktop_window *win_create(int app_id, const char *title, int x, int y, i win->app_state = 0; win->closing = 0; win->dirty = 1; - /* 拷贝标题 */ + win->ws = g_ws_cur; for (int i = 0; i < 47 && title[i]; i++) win->title[i] = title[i]; win->title[47] = 0; - /* 取消其他窗口焦点 */ for (int i = 0; i < g_win_count - 1; i++) g_windows[i].focused = 0; g_focused_win = id; return win; @@ -393,200 +598,103 @@ static void win_destroy(desktop_window *w) { } if (idx < 0) return; - /* Window close animation - shrinking rectangle */ + /* 窗口关闭动画 — 收缩矩形 */ { int wx = w->x, wy = w->y, ww = w->w, wh = w->h; + int was_cur = (w->ws == g_ws_cur); w->visible = 0; - for (int step = 5; step >= 0; step--) { - int scale = (step + 1) * 100 / 6; - int aw = ww * scale / 100; - int ah = wh * scale / 100; - int ax = wx + (ww - aw) / 2; - int ay = wy + (wh - ah) / 2; - cursor_restore_bg(); - redraw_all(); - /* redraw_all 末尾已绘制光标,这里先擦除以保证保存的背景干净(不含光标像素) */ - cursor_restore_bg(); - du_rect_outline(&g_fb, ax, ay, aw, ah, DS_DARK_ACCENT, DU_RADIUS_SM); - cursor_save_bg(g_mouse_x, g_mouse_y); - cursor_draw(g_mouse_x, g_mouse_y); - anim_delay_poll_mouse(16); + if (was_cur) { + for (int step = 5; step >= 0; step--) { + int scale = (step + 1) * 100 / 6; + int aw = ww * scale / 100; + int ah = wh * scale / 100; + int ax = wx + (ww - aw) / 2; + int ay = wy + (wh - ah) / 2; + cursor_restore_bg(); + redraw_all(); + cursor_restore_bg(); + du_rect_outline(&g_fb, ax, ay, aw, ah, KS_ACCENT, DU_RADIUS_SM); + cursor_save_bg(g_mouse_x, g_mouse_y); + cursor_draw(g_mouse_x, g_mouse_y); + anim_delay_poll_mouse(16); + } } } - /* 调用应用的 on_destroy */ if (w->app_id >= 0 && w->app_id < g_app_count && g_apps[w->app_id].on_destroy && w->app_state) { g_apps[w->app_id].on_destroy(w->app_state); } for (int i = idx; i < g_win_count - 1; i++) g_windows[i] = g_windows[i+1]; g_win_count--; if (g_focused_win == w->id) { - g_focused_win = g_win_count > 0 ? g_windows[g_win_count-1].id : -1; - if (g_focused_win >= 0) win_find(g_focused_win)->focused = 1; + g_focused_win = -1; + for (int i = g_win_count - 1; i >= 0; i--) { + if (g_windows[i].ws == g_ws_cur && g_windows[i].visible && !g_windows[i].minimized) { + g_focused_win = g_windows[i].id; + g_windows[i].focused = 1; + break; + } + } } } static int win_hit_titlebar(desktop_window *w, int mx, int my) { return mx >= w->x && mx < w->x + w->w && - my >= w->y && my < w->y + TITLEBAR_H; + my >= w->y && my < w->y + KATE_TITLEBAR_H; } static int win_hit_close_btn(desktop_window *w, int mx, int my) { - int bx = w->x + w->w - 28; - int by = w->y + 4; - return mx >= bx && mx < bx + 24 && my >= by && my < by + 20; + int bx = w->x + w->w - CLOSE_BTN_OFFSET; + int by = w->y + (KATE_TITLEBAR_H - CLOSE_BTN_SIZE) / 2; + return mx >= bx && mx < bx + CLOSE_BTN_SIZE && + my >= by && my < by + CLOSE_BTN_SIZE; +} + +static int ws_win_count(int ws) { + int n = 0; + for (int i = 0; i < g_win_count; i++) + if (g_windows[i].ws == ws && g_windows[i].visible) n++; + return n; } /* ============================================================ - * 窗口装饰绘制 + * 窗口装饰绘制(Winux-Kate 风格) * ============================================================ */ static void draw_window_frame(desktop_window *w) { if (!w->visible || w->minimized) return; - u32 titlebar_bg = w->focused ? DP_ABYSS_600 : DP_ABYSS_800; - u32 border_col = w->focused ? DS_DARK_BORDER_FOCUS : DS_DARK_BORDER; + u32 border = w->focused ? KS_BORDER_FOCUS : KS_BORDER; - /* 发光效果(焦点窗口) */ if (w->focused) { - du_rect_glow(&g_fb, w->x-2, w->y-2, w->w+4, w->h+4, - DS_DARK_BORDER_FOCUS, 10, 3); + du_kate_glow_border(&g_fb, w->x, w->y, w->w, w->h, KS_ACCENT); } - /* 窗口主体背景 */ - du_fill_rounded_rect(&g_fb, w->x, w->y, w->w, w->h, - DS_DARK_BG_SECONDARY, DU_RADIUS_LG); + du_fill_rounded_rect(&g_fb, w->x, w->y, w->w, w->h, KS_BG_SECONDARY, 2); - /* 标题栏 */ - du_fill_rect(&g_fb, w->x + BORDER_W, w->y + BORDER_W, - w->w - 2*BORDER_W, TITLEBAR_H - BORDER_W, titlebar_bg); + du_fill_rect_gradient(&g_fb, w->x + 1, w->y + 1, w->w - 2, KATE_TITLEBAR_H - 1, + 0xFF0A3050u, KS_BG_SECONDARY); + du_divider_h(&g_fb, w->x, w->y + KATE_TITLEBAR_H, w->w, KS_BORDER); + + du_fill_rect(&g_fb, w->x + 6, w->y + KATE_TITLEBAR_H / 2 - 2, 4, 4, KS_ACCENT2); - /* 标题文字 */ du_draw_string(&g_fb, w->title, - w->x + (i64)DU_SPACE_SM, - w->y + (TITLEBAR_H - (i64)DU_ASCII_CELL_H) / 2, - DS_DARK_TEXT_PRIMARY, titlebar_bg, DU_ASCII_STEP); + w->x + 14, + w->y + (KATE_TITLEBAR_H - (i64)DU_ASCII_LINE_H) / 2 + 1, + KS_ACCENT, 0xFF0A3050u, DU_ASCII_STEP); - /* 关闭按钮 */ - int bx = w->x + w->w - 28; - int by = w->y + 4; - du_fill_rounded_rect(&g_fb, bx, by, 24, 20, DP_ERROR, DU_RADIUS_SM); - du_draw_string(&g_fb, "X", bx + 8, by + 1, DP_NEUTRAL_900, DP_ERROR, DU_ASCII_STEP); + int bx = w->x + w->w - CLOSE_BTN_OFFSET; + int by = w->y + (KATE_TITLEBAR_H - CLOSE_BTN_SIZE) / 2; + du_rect_outline(&g_fb, bx, by, CLOSE_BTN_SIZE, CLOSE_BTN_SIZE, KS_DANGER, 1); + du_draw_string(&g_fb, "X", bx + 3, by - 1, + KS_DANGER, KS_BG_SECONDARY, DU_ASCII_STEP); - /* 边框 */ - du_rect_outline(&g_fb, w->x, w->y, w->w, w->h, border_col, DU_RADIUS_LG); - - /* 标题栏底线 */ - du_fill_rect(&g_fb, w->x + BORDER_W, w->y + TITLEBAR_H, - w->w - 2*BORDER_W, 1, border_col); -} - -/* ============================================================ - * 任务栏绘制 - * ============================================================ */ - -static void draw_taskbar(void) { - i64 tb_y = (i64)g_fb_h - TASKBAR_H; - du_fill_rect(&g_fb, 0, tb_y, (i64)g_fb_w, TASKBAR_H, DP_ABYSS_800); - /* 顶部强调线 */ - du_fill_rect(&g_fb, 0, tb_y, (i64)g_fb_w, 2, DS_DARK_ACCENT); - - /* Start 按钮 */ - du_fill_rounded_rect(&g_fb, (i64)DU_SPACE_SM, tb_y + 4, 48, 32, - DP_ABYSS_600, DU_RADIUS_SM); - du_draw_string(&g_fb, "D", - (i64)DU_SPACE_SM + 18, tb_y + 8, - DS_DARK_ACCENT_LIGHT, DP_ABYSS_600, DU_ASCII_STEP); - - /* 快速启动图标 */ - int qx = 64; - for (int i = 0; i < g_app_count && i < 4; i++) { - int icon_x = qx + i * 40; - switch (i) { - case 0: draw_icon_terminal(&g_fb, icon_x + 4, (int)tb_y + 4); break; - case 1: draw_icon_editor(&g_fb, icon_x + 4, (int)tb_y + 4); break; - case 2: draw_icon_folder(&g_fb, icon_x + 4, (int)tb_y + 4); break; - case 3: draw_icon_calc(&g_fb, icon_x + 4, (int)tb_y + 4); break; - } - } - - /* 窗口按钮 */ - int wx = qx + 4 * 40 + (int)DU_SPACE_LG; - for (int i = 0; i < g_win_count; i++) { - desktop_window *w = &g_windows[i]; - if (w->minimized) continue; - u32 btn_bg = w->focused ? DS_DARK_ACCENT : DP_ABYSS_600; - du_fill_rounded_rect(&g_fb, wx, tb_y + 4, 100, 32, btn_bg, DU_RADIUS_SM); - /* Truncate title to fit button width (100px - 8px padding = ~11 chars) */ - char truncated[16]; - int max_chars = 11; - int tlen = 0; - while (w->title[tlen] && tlen < max_chars) { truncated[tlen] = w->title[tlen]; tlen++; } - if (tlen == max_chars && w->title[tlen]) { - /* Add ellipsis */ - if (tlen > 8) tlen = 8; - truncated[tlen++] = '.'; - truncated[tlen++] = '.'; - truncated[tlen++] = '.'; - } - truncated[tlen] = 0; - du_draw_string(&g_fb, truncated, wx + 4, tb_y + 8, - DS_DARK_TEXT_PRIMARY, btn_bg, DU_ASCII_STEP); - wx += 104; - if (wx > (int)g_fb_w - 200) break; - } - - /* 时钟 */ - u8 rtc_h = 0, rtc_m = 0; - outb(0x70, 4); rtc_h = inb(0x71); - outb(0x70, 2); rtc_m = inb(0x71); - /* BCD → 二进制 */ - rtc_h = (u8)((rtc_h >> 4) * 10 + (rtc_h & 0xF)); - rtc_m = (u8)((rtc_m >> 4) * 10 + (rtc_m & 0xF)); - char clock_str[8]; - clock_str[0] = '0' + rtc_h / 10; - clock_str[1] = '0' + rtc_h % 10; - clock_str[2] = ':'; - clock_str[3] = '0' + rtc_m / 10; - clock_str[4] = '0' + rtc_m % 10; - clock_str[5] = 0; - du_draw_string(&g_fb, clock_str, - (i64)g_fb_w - 5 * (i64)DU_ASCII_STEP - (i64)DU_SPACE_MD, - tb_y + (TASKBAR_H - (i64)DU_ASCII_CELL_H) / 2, - DS_DARK_TEXT_SECONDARY, DP_ABYSS_800, DU_ASCII_STEP); -} - -/* ============================================================ - * 桌面图标绘制 - * ============================================================ */ - -static void draw_desktop_icons(void) { - for (int i = 0; i < g_icon_count; i++) { - desktop_icon *ic = &g_icons[i]; - int cx = ic->x + (DESKTOP_ICON_W - 32) / 2; /* center icon in slot */ - int cy = ic->y; - /* 根据应用 ID 画不同图标 */ - switch (ic->app_id) { - case 0: draw_icon_terminal(&g_fb, cx, cy); break; - case 1: draw_icon_editor(&g_fb, cx, cy); break; - case 2: draw_icon_folder(&g_fb, cx, cy); break; - case 3: draw_icon_calc(&g_fb, cx, cy); break; - default: draw_icon_terminal(&g_fb, cx, cy); break; - } - /* 标签文字 */ - int label_len = 0; - while (ic->label[label_len]) label_len++; - int label_x = ic->x + (DESKTOP_ICON_W - label_len * (int)DU_ASCII_STEP) / 2; - if (label_x < ic->x) label_x = ic->x; - du_draw_string(&g_fb, ic->label, label_x, cy + 36, - DS_DARK_TEXT_PRIMARY, DS_DARK_BG_PRIMARY, DU_ASCII_STEP); - } + du_rect_outline(&g_fb, w->x, w->y, w->w, w->h, border, 2); } /* ============================================================ * PS/2 鼠标处理 * ============================================================ */ -/* PS/2 控制器等待 — 输入缓冲区空(可写) */ static int ps2_wait_write(void) { for (int t = 0; t < 200000; t++) { if (!(inb(0x64) & 0x02)) return 0; @@ -595,7 +703,6 @@ static int ps2_wait_write(void) { return -1; } -/* PS/2 控制器等待 — 输出缓冲区满(可读) */ static int ps2_wait_read(void) { for (int t = 0; t < 200000; t++) { if (inb(0x64) & 0x01) return 0; @@ -604,7 +711,6 @@ static int ps2_wait_read(void) { return -1; } -/* 排干输出缓冲区中所有残留数据(带超时) */ static void ps2_drain(void) { for (int i = 0; i < 16; i++) { if (!(inb(0x64) & 0x01)) break; @@ -613,56 +719,45 @@ static void ps2_drain(void) { } static void ps2_mouse_init(void) { - /* 1. 刷新固件/上一阶段留下的残留数据 */ ps2_drain(); - /* 2. 启用 AUX(鼠标)端口 */ ps2_wait_write(); outb(0x64, 0xA8); - /* 3. 读取控制器配置字节 */ ps2_wait_write(); outb(0x64, 0x20); ps2_wait_read(); u8 cfg = inb(0x60); - /* 4. 设置配置:bit5=0(启用 AUX 时钟), bit1=1(启用 AUX IRQ12), bit6=1(Set2→Set1 转换) */ - cfg &= ~0x20; /* enable AUX clock */ - cfg |= 0x02; /* enable AUX IRQ12 */ - cfg |= 0x40; /* enable Set2→Set1 translation */ + cfg &= ~0x20; + cfg |= 0x02; + cfg |= 0x40; - /* 5. 写回控制器配置 */ ps2_wait_write(); outb(0x64, 0x60); ps2_wait_write(); outb(0x60, cfg); - /* 6. 复位鼠标 (0xFF) — 响应为 ACK(0xFA) + 自检(0xAA) + 设备ID(0x00),共 3 字节 */ ps2_wait_write(); outb(0x64, 0xD4); ps2_wait_write(); outb(0x60, 0xFF); - /* 排干所有复位响应字节(最多 8 字节,带超时) */ for (int i = 0; i < 8; i++) { if (ps2_wait_read() != 0) break; inb(0x60); } - /* 7. 启用数据流模式 (0xF4) — 响应为 ACK(0xFA) */ ps2_wait_write(); outb(0x64, 0xD4); ps2_wait_write(); outb(0x60, 0xF4); - /* 排干 ACK */ for (int i = 0; i < 4; i++) { if (ps2_wait_read() != 0) break; inb(0x60); } - /* 8. 最终刷新:丢弃任何杂散字节,确保数据包从干净状态开始 */ ps2_drain(); - /* 重置鼠标数据包解析状态 */ g_mouse_idx = 0; g_mouse_has_pkt = 0; @@ -672,20 +767,18 @@ static void ps2_mouse_init(void) { static int ps2_mouse_poll(void) { u8 st = inb(0x64); if (!(st & 1)) return 0; - if (!(st & 0x20)) return 0; /* 不是鼠标数据(键盘数据留给键盘处理) */ + if (!(st & 0x20)) return 0; u8 data = inb(0x60); g_mouse_buf[g_mouse_idx++] = data; if (g_mouse_idx < 3) return 0; g_mouse_idx = 0; - /* 检查 bit3 同步 */ if (!(g_mouse_buf[0] & 0x08)) return 0; int dx = (int)(i8)g_mouse_buf[1]; int dy = (int)(i8)g_mouse_buf[2]; - /* (i8) 转换已处理符号位,无需再检查 sign bits */ - dy = -dy; /* PS/2 Y 向上为正,屏幕 Y 向下为正,需翻转 */ + dy = -dy; g_mouse_x += dx; g_mouse_y += dy; @@ -695,8 +788,10 @@ static int ps2_mouse_poll(void) { if (g_mouse_y >= (int)g_fb_h - CURSOR_SIZE) g_mouse_y = (int)g_fb_h - CURSOR_SIZE; int old_btn = g_mouse_btn; - g_mouse_btn = (g_mouse_buf[0] & 0x01) ? 1 : 0; - /* Only signal redraw if something visual changed */ + g_mouse_btn = g_mouse_buf[0] & 0x03; + if ((g_mouse_btn & 1) && !(old_btn & 1)) g_left_pressed = 1; + if (!(g_mouse_btn & 1) && (old_btn & 1)) g_left_released = 1; + if ((g_mouse_btn & 2) && !(old_btn & 2)) g_right_pressed = 1; if (dx == 0 && dy == 0 && g_mouse_btn == old_btn) return 0; g_mouse_has_pkt = 1; return 1; @@ -724,7 +819,7 @@ static char scan_to_ascii(u8 sc, int shift) { } /* ============================================================ - * 内置应用:终端 (Bash) + * 终端 (Bash) — 面板/窗口共用核心 * ============================================================ */ #define TERM_MAX_COLS 80 @@ -743,42 +838,24 @@ typedef struct { static const char *BASH_PROMPT = "deshab# "; -static void *bash_on_create(app_ctx *ctx) { - bash_state *s = (bash_state *)0x4000000; /* 固定地址分配,避免堆依赖 */ +static void bash_reset(bash_state *s, int w, int h) { for (int i = 0; i < TERM_MAX_CHARS; i++) { - s->ch[i] = ' '; s->fg[i] = DS_DARK_TEXT_PRIMARY; + s->ch[i] = ' '; s->fg[i] = KS_TEXT_PRIMARY; } - s->cols = (ctx->client_w - (int)DU_SPACE_SM * 2) / (int)DU_ASCII_STEP; - s->rows = (ctx->client_h - (int)DU_SPACE_SM * 2) / (int)DU_ASCII_LINE_H; + s->cols = (w - (int)DU_SPACE_SM * 2) / (int)DU_ASCII_STEP; + s->rows = (h - (int)DU_SPACE_SM * 2) / (int)DU_ASCII_LINE_H; if (s->cols > TERM_MAX_COLS) s->cols = TERM_MAX_COLS; if (s->rows > TERM_MAX_ROWS) s->rows = TERM_MAX_ROWS; + if (s->cols < 10) s->cols = 10; + if (s->rows < 3) s->rows = 3; s->cur_col = 0; s->cur_row = 0; s->input_len = 0; s->input_cursor = 0; - s->prompt_len = 0; - while (BASH_PROMPT[s->prompt_len]) s->prompt_len++; - - /* 打印欢迎 */ - const char *welcome = "Deshab Bash v0.1\n输入 help 查看命令\n\n"; - while (*welcome) { - if (*welcome == '\n') { - s->cur_col = 0; s->cur_row++; - } else { - if (s->cur_row < s->rows && s->cur_col < s->cols) { - int idx = s->cur_row * TERM_MAX_COLS + s->cur_col; - s->ch[idx] = (u8)*welcome; - s->fg[idx] = DS_DARK_TEXT_PRIMARY; - } - s->cur_col++; - } - welcome++; - } - return s; + s->prompt_len = kstrlen(BASH_PROMPT); } static void bash_putc(bash_state *s, char c, u32 color) { if (c == '\n') { s->cur_col = 0; s->cur_row++; return; } if (s->cur_row >= s->rows) { - /* 滚动 */ for (int r = 1; r < s->rows; r++) { for (int c2 = 0; c2 < s->cols; c2++) { s->ch[(r-1)*TERM_MAX_COLS+c2] = s->ch[r*TERM_MAX_COLS+c2]; @@ -802,66 +879,63 @@ static void bash_puts(bash_state *s, const char *str, u32 color) { static void bash_execute(bash_state *s, const char *cmd) { while (*cmd == ' ') cmd++; - bash_puts(s, BASH_PROMPT, DS_DARK_PROMPT); - bash_puts(s, cmd, DS_DARK_TEXT_PRIMARY); + bash_puts(s, BASH_PROMPT, KS_ACCENT2); + bash_puts(s, cmd, KS_TEXT_PRIMARY); bash_putc(s, '\n', 0); if (*cmd == 0) return; if (cmd[0]=='h'&&cmd[1]=='e'&&cmd[2]=='l'&&cmd[3]=='p') { - bash_puts(s, " help clear echo version\n", DP_SUCCESS); - bash_puts(s, " uname date about reboot\n", DP_SUCCESS); - bash_puts(s, " halt pwd whoami id\n", DP_SUCCESS); + bash_puts(s, " help clear echo version\n", KS_ACCENT2); + bash_puts(s, " uname about reboot halt\n", KS_ACCENT2); + bash_puts(s, " pwd whoami id\n", KS_ACCENT2); } else if (cmd[0]=='c'&&cmd[1]=='l'&&cmd[2]=='e'&&cmd[3]=='a'&&cmd[4]=='r') { for (int i = 0; i < TERM_MAX_CHARS; i++) { s->ch[i] = ' '; } s->cur_col = 0; s->cur_row = 0; } else if (cmd[0]=='v'&&cmd[1]=='e'&&cmd[2]=='r') { - bash_puts(s, "Deshab OS v0.1.0\n", DP_SUCCESS); + bash_puts(s, "Deshab OS v0.1.0 (Winux-Kate shell)\n", KS_ACCENT2); } else if (cmd[0]=='u'&&cmd[1]=='n'&&cmd[2]=='a') { - bash_puts(s, "Deshab\n", DS_DARK_TEXT_PRIMARY); + bash_puts(s, "Deshab\n", KS_TEXT_PRIMARY); } else if (cmd[0]=='a'&&cmd[1]=='b'&&cmd[2]=='o'&&cmd[3]=='u'&&cmd[4]=='t') { - bash_puts(s, "Deshab OS - SAS-R0 Kernel\n", DP_SEAL_300); - bash_puts(s, "Sealed Arc UI Design\n", DP_ARC_300); + bash_puts(s, "Deshab OS - SAS-R0 Kernel\n", KS_ACCENT); + bash_puts(s, "Winux-Kate UI Structure\n", KS_ACCENT2); } else if (cmd[0]=='e'&&cmd[1]=='c'&&cmd[2]=='h'&&cmd[3]=='o') { const char *arg = cmd + 4; while (*arg == ' ') arg++; - bash_puts(s, arg, DS_DARK_TEXT_PRIMARY); + bash_puts(s, arg, KS_TEXT_PRIMARY); bash_putc(s, '\n', 0); } else if (cmd[0]=='p'&&cmd[1]=='w'&&cmd[2]=='d') { - bash_puts(s, "/\n", DS_DARK_TEXT_PRIMARY); + bash_puts(s, "/\n", KS_TEXT_PRIMARY); } else if (cmd[0]=='w'&&cmd[1]=='h'&&cmd[2]=='o') { - bash_puts(s, "root\n", DS_DARK_TEXT_PRIMARY); + bash_puts(s, "root\n", KS_TEXT_PRIMARY); } else if (cmd[0]=='i'&&cmd[1]=='d') { - bash_puts(s, "uid=0(root) gid=0(root)\n", DS_DARK_TEXT_PRIMARY); + bash_puts(s, "uid=0(root) gid=0(root)\n", KS_TEXT_PRIMARY); } else if (cmd[0]=='r'&&cmd[1]=='e'&&cmd[2]=='b') { - bash_puts(s, "Rebooting...\n", DS_DARK_PROMPT); + bash_puts(s, "Rebooting...\n", KS_ACCENT2); outb(0x64, 0xFE); for(;;) __asm__("hlt"); } else if (cmd[0]=='h'&&cmd[1]=='a'&&cmd[2]=='l'&&cmd[3]=='t') { - bash_puts(s, "Halted.\n", DS_DARK_PROMPT); + bash_puts(s, "Halted.\n", KS_ACCENT2); for(;;) __asm__("hlt"); } else { - bash_puts(s, "unknown: ", DP_ERROR); - bash_puts(s, cmd, DP_ERROR); + bash_puts(s, "unknown: ", KS_DANGER); + bash_puts(s, cmd, KS_DANGER); bash_putc(s, '\n', 0); } } -static void bash_on_event(void *state, app_ctx *ctx, desktop_event *ev) { - bash_state *s = (bash_state *)state; - if (ev->type != EV_KEY_DOWN) return; - u8 sc = ev->scancode; - if (sc == 0x1C) { /* Enter */ +static void bash_key(bash_state *s, u8 sc, int shift) { + if (sc == 0x1C) { s->input_buf[s->input_len] = 0; bash_execute(s, s->input_buf); s->input_len = 0; s->input_cursor = 0; - } else if (sc == 0x0E) { /* Backspace */ + } else if (sc == 0x0E) { if (s->input_cursor > 0) { for (int i = s->input_cursor-1; i < s->input_len-1; i++) s->input_buf[i] = s->input_buf[i+1]; s->input_len--; s->input_cursor--; } } else { - char c = scan_to_ascii(sc, ev->shift); + char c = scan_to_ascii(sc, shift); if (c && c >= 32 && c <= 126 && s->input_len < 255) { for (int i = s->input_len; i > s->input_cursor; i--) s->input_buf[i] = s->input_buf[i-1]; @@ -869,55 +943,59 @@ static void bash_on_event(void *state, app_ctx *ctx, desktop_event *ev) { s->input_len++; } } - ctx->win->dirty = 1; } -static void bash_on_draw(void *state, app_ctx *ctx) { - bash_state *s = (bash_state *)state; - int cx = ctx->client_x; - int cy = ctx->client_y; - int cw = ctx->client_w; - int ch = ctx->client_h; +/* 绘制终端到指定矩形区域(面板或窗口客户区) */ +static void bash_draw_to(bash_state *s, int cx, int cy, int cw, int ch) { + du_fill_rect(&g_fb, cx, cy, cw, ch, KS_BG_PRIMARY); - /* 背景 */ - du_fill_rect(&g_fb, cx, cy, cw, ch, DS_DARK_BG_PRIMARY); - - /* 渲染所有字符 */ for (int r = 0; r < s->rows; r++) { for (int c = 0; c < s->cols; c++) { int idx = r * TERM_MAX_COLS + c; if (s->ch[idx] == ' ') continue; - du_draw_char(&g_fb, s->ch[idx], - cx + (int)DU_SPACE_SM + c * (int)DU_ASCII_STEP, - cy + (int)DU_SPACE_SM + r * (int)DU_ASCII_LINE_H, - s->fg[idx], DS_DARK_BG_PRIMARY); + int px = cx + (int)DU_SPACE_SM + c * (int)DU_ASCII_STEP; + int py = cy + (int)DU_SPACE_SM + r * (int)DU_ASCII_LINE_H; + if (px + (int)DU_ASCII_CELL_W > cx + cw) continue; + if (py + (int)DU_ASCII_CELL_H > cy + ch) continue; + du_draw_char(&g_fb, s->ch[idx], px, py, s->fg[idx], KS_BG_PRIMARY); } } - /* 绘制当前输入行 */ int input_y = cy + (int)DU_SPACE_SM + s->cur_row * (int)DU_ASCII_LINE_H; - /* 提示符 */ - du_draw_string(&g_fb, BASH_PROMPT, - cx + (int)DU_SPACE_SM, input_y, - DS_DARK_PROMPT, DS_DARK_BG_PRIMARY, DU_ASCII_STEP); - /* 输入内容 */ - du_draw_string(&g_fb, s->input_buf, - cx + (int)DU_SPACE_SM + s->prompt_len * (int)DU_ASCII_STEP, - input_y, - DS_DARK_TEXT_PRIMARY, DS_DARK_BG_PRIMARY, DU_ASCII_STEP); - - /* 光标 */ - int cursor_x = cx + (int)DU_SPACE_SM + (s->prompt_len + s->input_cursor) * (int)DU_ASCII_STEP; - du_fill_rect(&g_fb, cursor_x, input_y + (int)DU_ASCII_CELL_H - 3, - (int)DU_ASCII_CELL_W, 2, DS_DARK_CURSOR); + if (input_y + (int)DU_ASCII_CELL_H <= cy + ch) { + du_draw_string(&g_fb, BASH_PROMPT, + cx + (int)DU_SPACE_SM, input_y, + KS_ACCENT2, KS_BG_PRIMARY, DU_ASCII_STEP); + du_draw_string(&g_fb, s->input_buf, + cx + (int)DU_SPACE_SM + s->prompt_len * (int)DU_ASCII_STEP, + input_y, + KS_TEXT_PRIMARY, KS_BG_PRIMARY, DU_ASCII_STEP); + int cursor_x = cx + (int)DU_SPACE_SM + (s->prompt_len + s->input_cursor) * (int)DU_ASCII_STEP; + du_fill_rect(&g_fb, cursor_x, input_y + (int)DU_ASCII_CELL_H - 3, + (int)DU_ASCII_CELL_W, 2, KS_ACCENT); + } } -static void bash_on_destroy(void *state) { - (void)state; +/* ---- 窗口适配 ---- */ +static void *bash_on_create(app_ctx *ctx) { + bash_state *s = (bash_state *)ADDR_TERMWIN; + bash_reset(s, ctx->client_w, ctx->client_h); + bash_puts(s, "Deshab Bash v0.1\nType help for commands\n\n", KS_TEXT_PRIMARY); + return s; } +static void bash_on_event(void *state, app_ctx *ctx, desktop_event *ev) { + if (ev->type != EV_KEY_DOWN) return; + bash_key((bash_state *)state, ev->scancode, ev->shift); + ctx->win->dirty = 1; +} +static void bash_on_draw(void *state, app_ctx *ctx) { + bash_draw_to((bash_state *)state, ctx->client_x, ctx->client_y, + ctx->client_w, ctx->client_h); +} +static void bash_on_destroy(void *state) { (void)state; } /* ============================================================ - * 内置应用:文本编辑器 (Editor) + * 文本编辑器 (Editor) — 面板/IDE/窗口共用核心 * ============================================================ */ #define EDITOR_BUF_SIZE 65536 @@ -926,11 +1004,13 @@ typedef struct { char text[EDITOR_BUF_SIZE]; int text_len; int cursor_pos; - int scroll_y; /* 行偏移 */ + int scroll_y; int cursor_line; int cursor_col; int modified; - char filename[64]; + int has_file; /* 关联了磁盘文件 */ + char name11[11]; /* FAT32 8.3 名 */ + char disp[13]; /* 显示名 */ } editor_state; static void editor_recalc_cursor(editor_state *s) { @@ -950,37 +1030,20 @@ static int editor_line_start(editor_state *s, int line) { return s->text_len; } -static void *editor_on_create(app_ctx *ctx) { - editor_state *s = (editor_state *)0x5000000; /* 固定地址 */ +static void editor_reset(editor_state *s, const char *sample) { s->text_len = 0; s->cursor_pos = 0; s->scroll_y = 0; s->modified = 0; - s->filename[0] = 0; - - /* 默认内容 */ - const char *sample = "# Welcome to Deshab Editor\n#\n\nStart typing here...\n"; + s->has_file = 0; + s->disp[0] = 0; while (*sample && s->text_len < EDITOR_BUF_SIZE - 1) { s->text[s->text_len++] = *sample++; } s->cursor_pos = s->text_len; editor_recalc_cursor(s); - - /* 设置窗口标题 */ - du_draw_string(&g_fb, "", 0, 0, 0, 0, 0); /* 占位 */ - for (int i = 0; i < 63; i++) ctx->win->title[i] = 0; - const char *t = "Editor - untitled"; - for (int i = 0; t[i] && i < 47; i++) ctx->win->title[i] = t[i]; - - (void)ctx; - return s; } -static void editor_on_event(void *state, app_ctx *ctx, desktop_event *ev) { - editor_state *s = (editor_state *)state; - if (ev->type != EV_KEY_DOWN) return; - u8 sc = ev->scancode; - int e0 = 0; /* TODO: 扩展键处理 */ - - if (sc == 0x1C) { /* Enter */ +static void editor_key(editor_state *s, u8 sc, int shift) { + if (sc == 0x1C) { if (s->text_len < EDITOR_BUF_SIZE - 1) { for (int i = s->text_len; i > s->cursor_pos; i--) s->text[i] = s->text[i-1]; @@ -988,14 +1051,14 @@ static void editor_on_event(void *state, app_ctx *ctx, desktop_event *ev) { s->text_len++; s->cursor_pos++; s->modified = 1; } - } else if (sc == 0x0E) { /* Backspace */ + } else if (sc == 0x0E) { if (s->cursor_pos > 0) { for (int i = s->cursor_pos - 1; i < s->text_len - 1; i++) s->text[i] = s->text[i+1]; s->text_len--; s->cursor_pos--; s->modified = 1; } - } else if (sc == 0x48) { /* Up */ + } else if (sc == 0x48) { if (s->cursor_line > 0) { int prev_start = editor_line_start(s, s->cursor_line - 1); int col = s->cursor_col; @@ -1003,26 +1066,26 @@ static void editor_on_event(void *state, app_ctx *ctx, desktop_event *ev) { for (int i = 0; i < col && s->cursor_pos < s->text_len && s->text[s->cursor_pos] != '\n'; i++) s->cursor_pos++; } - } else if (sc == 0x50) { /* Down */ - if (s->cursor_line < 999) { - int next_start = editor_line_start(s, s->cursor_line + 1); + } else if (sc == 0x50) { + int next_start = editor_line_start(s, s->cursor_line + 1); + if (next_start < s->text_len || s->cursor_line < 999) { int col = s->cursor_col; s->cursor_pos = next_start; for (int i = 0; i < col && s->cursor_pos < s->text_len && s->text[s->cursor_pos] != '\n'; i++) s->cursor_pos++; } - } else if (sc == 0x4B) { /* Left */ + } else if (sc == 0x4B) { if (s->cursor_pos > 0) s->cursor_pos--; - } else if (sc == 0x4D) { /* Right */ + } else if (sc == 0x4D) { if (s->cursor_pos < s->text_len) s->cursor_pos++; - } else if (sc == 0x47) { /* Home */ + } else if (sc == 0x47) { s->cursor_pos = editor_line_start(s, s->cursor_line); - } else if (sc == 0x4F) { /* End */ + } else if (sc == 0x4F) { s->cursor_pos = editor_line_start(s, s->cursor_line); while (s->cursor_pos < s->text_len && s->text[s->cursor_pos] != '\n') s->cursor_pos++; } else { - char c = scan_to_ascii(sc, ev->shift); + char c = scan_to_ascii(sc, shift); if (c && c >= 32 && c <= 126 && s->text_len < EDITOR_BUF_SIZE - 1) { for (int i = s->text_len; i > s->cursor_pos; i--) s->text[i] = s->text[i-1]; @@ -1032,31 +1095,23 @@ static void editor_on_event(void *state, app_ctx *ctx, desktop_event *ev) { } } editor_recalc_cursor(s); - ctx->win->dirty = 1; - (void)e0; } -static void editor_on_draw(void *state, app_ctx *ctx) { - editor_state *s = (editor_state *)state; - int cx = ctx->client_x; - int cy = ctx->client_y; - int cw = ctx->client_w; - int ch = ctx->client_h; - +/* 绘制编辑器到指定矩形区域 */ +static void editor_draw_to(editor_state *s, int cx, int cy, int cw, int ch) { int line_num_w = 5 * (int)DU_ASCII_STEP + (int)DU_SPACE_SM; int text_area_x = cx + line_num_w; int text_area_w = cw - line_num_w; - int visible_rows = ch / (int)DU_ASCII_LINE_H; + int visible_rows = (ch - 20) / (int)DU_ASCII_LINE_H; int visible_cols = text_area_w / (int)DU_ASCII_STEP; + if (visible_rows < 1) visible_rows = 1; + if (visible_cols < 4) visible_cols = 4; - /* 背景 */ - du_fill_rect(&g_fb, cx, cy, cw, ch, DS_DARK_BG_SECONDARY); + du_fill_rect(&g_fb, cx, cy, cw, ch, KS_BG_SECONDARY); - /* 自动滚动 */ if (s->cursor_line < s->scroll_y) s->scroll_y = s->cursor_line; if (s->cursor_line >= s->scroll_y + visible_rows) s->scroll_y = s->cursor_line - visible_rows + 1; - /* 渲染可见行 */ int line = 0; int pos = 0; for (int i = 0; i < s->text_len || line <= s->cursor_line; ) { @@ -1065,26 +1120,23 @@ static void editor_on_draw(void *state, app_ctx *ctx) { if (line >= s->scroll_y) { int screen_y = cy + (line - s->scroll_y) * (int)DU_ASCII_LINE_H; - /* 行号 */ char ln[8]; ln[0] = '0' + ((line+1)/100)%10; ln[1] = '0' + ((line+1)/10)%10; ln[2] = '0' + (line+1)%10; ln[3] = 0; du_draw_string(&g_fb, ln, cx + 2, screen_y, - DS_DARK_TEXT_DIM, DS_DARK_BG_SECONDARY, DU_ASCII_STEP); + KS_TEXT_DIM, KS_BG_SECONDARY, DU_ASCII_STEP); - /* 行号分隔线 */ du_fill_rect(&g_fb, cx + line_num_w - 2, screen_y, 1, - (int)DU_ASCII_CELL_H, DS_DARK_DIVIDER); + (int)DU_ASCII_CELL_H, KS_BORDER_DIM); - /* 行文本 */ int col = 0; while (pos < s->text_len && s->text[pos] != '\n' && col < visible_cols) { du_draw_char(&g_fb, s->text[pos], text_area_x + col * (int)DU_ASCII_STEP, screen_y, - DS_DARK_TEXT_PRIMARY, DS_DARK_BG_SECONDARY); + KS_TEXT_PRIMARY, KS_BG_SECONDARY); col++; pos++; } if (pos < s->text_len && s->text[pos] == '\n') pos++; @@ -1096,19 +1148,19 @@ static void editor_on_draw(void *state, app_ctx *ctx) { if (pos >= s->text_len && line > s->cursor_line) break; } - /* 光标 */ + /* 光标(青色竖条) */ if (s->cursor_line >= s->scroll_y && s->cursor_line < s->scroll_y + visible_rows) { int cursor_y = cy + (s->cursor_line - s->scroll_y) * (int)DU_ASCII_LINE_H; int cursor_x = text_area_x + s->cursor_col * (int)DU_ASCII_STEP; - du_fill_rect(&g_fb, cursor_x, cursor_y, 2, (int)DU_ASCII_CELL_H, DS_DARK_CURSOR); + du_fill_rect(&g_fb, cursor_x, cursor_y, 2, (int)DU_ASCII_CELL_H, KS_ACCENT); } - /* 状态栏 */ + /* 底部迷你状态栏 */ int status_y = cy + ch - 20; - du_fill_rect(&g_fb, cx, status_y, cw, 20, DP_ABYSS_800); + du_fill_rect(&g_fb, cx, status_y, cw, 20, KS_BG_TERTIARY); + du_divider_h(&g_fb, cx, status_y, cw, KS_BORDER); char status[64]; int sp = 0; - /* 行:列 */ const char *p1 = "L:"; while(*p1) status[sp++] = *p1++; status[sp++] = '0' + (s->cursor_line+1)/10%10; status[sp++] = '0' + (s->cursor_line+1)%10; @@ -1116,15 +1168,37 @@ static void editor_on_draw(void *state, app_ctx *ctx) { status[sp++] = '0' + (s->cursor_col+1)/10%10; status[sp++] = '0' + (s->cursor_col+1)%10; if (s->modified) { const char *m = " *"; while(*m) status[sp++] = *m++; } + if (s->has_file) { + const char *m = " "; while(*m) status[sp++] = *m++; + for (int i = 0; s->disp[i] && sp < 60; i++) status[sp++] = s->disp[i]; + } status[sp] = 0; du_draw_string(&g_fb, status, cx + 4, status_y + 1, - DS_DARK_TEXT_DIM, DP_ABYSS_800, DU_ASCII_STEP); + KS_TEXT_DIM, KS_BG_TERTIARY, DU_ASCII_STEP); } +/* ---- 窗口适配 ---- */ +static void *editor_on_create(app_ctx *ctx) { + editor_state *s = (editor_state *)ADDR_EDWIN; + editor_reset(s, "# Welcome to Deshab Editor\n#\n\nStart typing here...\n"); + for (int i = 0; i < 47; i++) ctx->win->title[i] = 0; + const char *t = "Editor - untitled"; + for (int i = 0; t[i] && i < 47; i++) ctx->win->title[i] = t[i]; + return s; +} +static void editor_on_event(void *state, app_ctx *ctx, desktop_event *ev) { + if (ev->type != EV_KEY_DOWN) return; + editor_key((editor_state *)state, ev->scancode, ev->shift); + ctx->win->dirty = 1; +} +static void editor_on_draw(void *state, app_ctx *ctx) { + editor_draw_to((editor_state *)state, ctx->client_x, ctx->client_y, + ctx->client_w, ctx->client_h); +} static void editor_on_destroy(void *state) { (void)state; } /* ============================================================ - * 内置应用:计算器 (Calculator) + * 计算器 (Calculator) * ============================================================ */ typedef struct { @@ -1132,7 +1206,7 @@ typedef struct { int display_len; i64 accumulator; i64 current; - int op; /* 0=none, 1=+, 2=-, 3=*, 4=/ */ + int op; int new_number; } calc_state; @@ -1150,7 +1224,7 @@ static void calc_update_display(calc_state *s) { } static void *calc_on_create(app_ctx *ctx) { - calc_state *s = (calc_state *)0x6000000; + calc_state *s = (calc_state *)ADDR_CALC; s->accumulator = 0; s->current = 0; s->op = 0; s->new_number = 1; calc_update_display(s); @@ -1192,20 +1266,17 @@ static void calc_on_draw(void *state, app_ctx *ctx) { int cw = ctx->client_w; int ch = ctx->client_h; - du_fill_rect(&g_fb, cx, cy, cw, ch, DS_DARK_BG_SECONDARY); + du_fill_rect(&g_fb, cx, cy, cw, ch, KS_BG_SECONDARY); - /* 显示屏 */ int disp_h = 40; du_fill_rounded_rect(&g_fb, cx + 8, cy + 8, cw - 16, disp_h, - DP_ABYSS_900, DU_RADIUS_MD); + KS_BG_PRIMARY, DU_RADIUS_MD); du_rect_outline(&g_fb, cx + 8, cy + 8, cw - 16, disp_h, - DS_DARK_BORDER, DU_RADIUS_MD); - /* 数字右对齐 */ + KS_ACCENT, DU_RADIUS_MD); int text_x = cx + cw - 16 - s->display_len * (int)DU_ASCII_STEP - 8; du_draw_string(&g_fb, s->display, text_x, cy + 16, - DS_DARK_TEXT_PRIMARY, DP_ABYSS_900, DU_ASCII_STEP); + KS_ACCENT, KS_BG_PRIMARY, DU_ASCII_STEP); - /* 按钮网格 4×4 */ const char *btn_labels[] = { "C", "+/-", "%", "/", "7", "8", "9", "*", @@ -1221,15 +1292,17 @@ static void calc_on_draw(void *state, app_ctx *ctx) { int bx = cx + 8 + col * (btn_w + 3); int by = btn_y + row * (btn_h + 3); if (by + btn_h > cy + ch - 4) break; - u32 bg = (i < 4) ? DP_ABYSS_600 : - (i % 4 == 3) ? DS_DARK_ACCENT : - (i == 18) ? DP_SUCCESS : - DP_ABYSS_700; + u32 bg = (i < 4) ? KS_ACCENT_DIM : + (i % 4 == 3) ? KS_ACCENT : + (i == 18) ? KS_ACCENT2 : + KS_BG_TERTIARY; + u32 fg = ((i % 4 == 3) || (i == 18)) ? KS_TEXT_INVERT : KS_TEXT_PRIMARY; du_fill_rounded_rect(&g_fb, bx, by, btn_w, btn_h, bg, DU_RADIUS_SM); + du_rect_outline(&g_fb, bx, by, btn_w, btn_h, KS_BORDER, DU_RADIUS_SM); du_draw_string(&g_fb, btn_labels[i], bx + (btn_w - 1*(int)DU_ASCII_STEP)/2, by + (btn_h - (int)DU_ASCII_CELL_H)/2, - DS_DARK_TEXT_PRIMARY, bg, DU_ASCII_STEP); + fg, bg, DU_ASCII_STEP); } } @@ -1240,84 +1313,204 @@ static void calc_on_destroy(void *state) { (void)state; } * ============================================================ */ static void register_apps(void) { - /* 0: Terminal */ g_apps[0] = (app_descriptor){ "bash", "Terminal", 640, 440, - bash_on_create, bash_on_event, bash_on_draw, bash_on_destroy + bash_on_create, bash_on_event, bash_on_draw, bash_on_destroy, 0 }; g_app_count++; - - /* 1: Editor */ g_apps[1] = (app_descriptor){ "editor", "Editor", 600, 450, - editor_on_create, editor_on_event, editor_on_draw, editor_on_destroy + editor_on_create, editor_on_event, editor_on_draw, editor_on_destroy, 0 }; g_app_count++; - - /* 2: File Manager */ g_apps[2] = (app_descriptor){ "fileman", "Files", 500, 400, - 0, 0, 0, 0 /* placeholder */ + 0, 0, 0, 0, "FILEMAN ELF" }; g_app_count++; - - /* 3: Calculator */ g_apps[3] = (app_descriptor){ "calc", "Calculator", 280, 400, - calc_on_create, calc_on_event, calc_on_draw, calc_on_destroy + calc_on_create, calc_on_event, calc_on_draw, calc_on_destroy, 0 + }; + g_app_count++; + g_apps[4] = (app_descriptor){ + "browser", "Browser", 700, 500, + 0, 0, 0, 0, "BROWSER ELF" }; g_app_count++; } +/* Winux-Kate .desktop-grid:canvas padding 24px,列宽 104px,gap 18px, + * 左→右排列,超出画布宽度则换行(align-content: start) */ static void setup_desktop_icons(void) { - int start_x = (int)g_fb_w - DESKTOP_ICON_W - ICON_GUTTER_X; - int y = ICON_GUTTER_Y; + int pad = KATE_ICON_PAD; /* 24 */ + int x = pad; + int y = KATE_TOPBAR_H + KATE_PAGE_PAD + pad; + int max_x = (int)g_fb_w - pad; /* canvas 右缘 */ for (int i = 0; i < g_app_count; i++) { - if (!g_apps[i].on_create) continue; + if (!g_apps[i].on_create && !g_apps[i].elf_name) continue; + if (x > pad && x + KATE_ICON_W > max_x) { + x = pad; + y += KATE_ICON_H + KATE_ICON_GAP; + } g_icons[g_icon_count].app_id = i; - g_icons[g_icon_count].x = start_x; + g_icons[g_icon_count].x = x; g_icons[g_icon_count].y = y; g_icons[g_icon_count].label = g_apps[i].display_name; + g_icons[g_icon_count].selected = 0; g_icon_count++; - y += DESKTOP_ICON_H; + x += KATE_ICON_W + KATE_ICON_GAP; } } /* ============================================================ - * 应用启动 + * 外部 ELF 加载 * ============================================================ */ +#define DT_ELFCLASS64 2 +#define DT_EM_X86_64 62 +#define DT_PT_LOAD 1 +#define DT_PT_DYNAMIC 2 +#define DT_DT_RELA 7 +#define DT_DT_RELASZ 8 +#define DT_R_X86_64_RELATIVE 8 + +typedef struct { u8 ident[16]; u16 type,machine; u32 ver; u64 entry,phoff,shoff; u32 flags; u16 ehsize,phentsize,phnum,shentsize,shnum,shstrndx; } dt_elf64_ehdr; +typedef struct { u32 type,flags; u64 offset,vaddr,paddr,filesz,memsz,align; } dt_elf64_phdr; +typedef struct { i64 tag; u64 val; } dt_elf64_dyn; +typedef struct { u64 offset; u64 info; i64 addend; } dt_elf64_rela; + +static u8 g_elf_image[262144]; + +static int dt_load_elf(u8 *data, u32 data_size, void **entry_out) { + const dt_elf64_ehdr *eh = (const dt_elf64_ehdr *)data; + if (data_size < sizeof(dt_elf64_ehdr)) return -1; + if (eh->ident[0] != 0x7F || eh->ident[4] != DT_ELFCLASS64) return -2; + if (eh->machine != DT_EM_X86_64) return -3; + u64 min_vaddr = ~0ULL, max_vaddr = 0; + u32 lc = 0; + for (u16 i = 0; i < eh->phnum; i++) { + const dt_elf64_phdr *ph = (const dt_elf64_phdr *)(data + eh->phoff + (u64)i * eh->phentsize); + if (ph->type != DT_PT_LOAD) continue; + if (ph->filesz > ph->memsz) return -4; + if (ph->vaddr < min_vaddr) min_vaddr = ph->vaddr; + if (ph->vaddr + ph->memsz > max_vaddr) max_vaddr = ph->vaddr + ph->memsz; + lc++; + } + if (!lc || min_vaddr == ~0ULL) return -5; + u64 isize = (max_vaddr - min_vaddr + 0xFFF) & ~0xFFFULL; + if (isize > sizeof(g_elf_image)) return -6; + u8 *image = g_elf_image; + for (u64 i = 0; i < isize; i++) image[i] = 0; + for (u16 i = 0; i < eh->phnum; i++) { + const dt_elf64_phdr *ph = (const dt_elf64_phdr *)(data + eh->phoff + (u64)i * eh->phentsize); + if (ph->type != DT_PT_LOAD) continue; + u64 off = ph->vaddr - min_vaddr; + for (u64 j = 0; j < ph->filesz; j++) + image[off + j] = data[ph->offset + j]; + } + for (u16 i = 0; i < eh->phnum; i++) { + const dt_elf64_phdr *ph = (const dt_elf64_phdr *)(data + eh->phoff + (u64)i * eh->phentsize); + if (ph->type != DT_PT_DYNAMIC) continue; + const dt_elf64_dyn *dyn = (const dt_elf64_dyn *)(data + ph->offset); + u64 rela_off = 0, rela_sz = 0; + for (u32 j = 0; j < ph->filesz / sizeof(dt_elf64_dyn); j++) { + if (dyn[j].tag == DT_DT_RELA) rela_off = dyn[j].val; + else if (dyn[j].tag == DT_DT_RELASZ) rela_sz = dyn[j].val; + else if (dyn[j].tag == 0) break; + } + if (rela_off && rela_sz) { + const dt_elf64_rela *r = (const dt_elf64_rela *)(data + rela_off); + u32 rn = (u32)(rela_sz / sizeof(dt_elf64_rela)); + for (u32 j = 0; j < rn; j++) { + u32 type = (u32)(r[j].info & 0xFFFFFFFF); + if (type == DT_R_X86_64_RELATIVE) { + u64 target = r[j].offset - min_vaddr; + u64 val = min_vaddr + (u64)r[j].addend; + *(u64 *)(image + target) = val; + } + } + } + } + *entry_out = (void *)(image + (eh->entry - min_vaddr)); + return 0; +} + +static void launch_external_elf(const char *name11) { + u8 *data = 0; u32 size = 0; + if (f32_read_root_file(name11, &data, &size) != 0) { + slog("elf not found"); + return; + } + void *entry = 0; + if (dt_load_elf(data, size, &entry) != 0) { + slog("elf load failed"); + return; + } + slog("launching external elf"); + void (*elf_entry)(const dsk_boot_context *) = (void (*)(const dsk_boot_context *))entry; + elf_entry(g_boot_ctx); + g_cursor_saved = 0; + g_cursor_old_x = -1; g_cursor_old_y = -1; + files_refresh(); /* 外部应用可能改动了磁盘 */ + redraw_all(); + slog("external elf returned"); +} + +/* ============================================================ + * 应用启动(单实例聚焦策略) + * ============================================================ */ + +static void fill_app_ctx(app_ctx *ctx, desktop_window *win) { + ctx->fb = &g_fb; + ctx->win = win; + ctx->client_x = win->x + BORDER_W; + ctx->client_y = win->y + KATE_TITLEBAR_H; + ctx->client_w = win->w - 2 * BORDER_W; + ctx->client_h = win->h - KATE_TITLEBAR_H - BORDER_W; + ctx->should_exit = 0; + ctx->block_read = g_block_read; + ctx->block_write = g_block_write; + ctx->kernel_api = g_kernel_api; +} + static void launch_app(int app_id) { if (app_id < 0 || app_id >= g_app_count) return; app_descriptor *app = &g_apps[app_id]; + + if (app->elf_name) { + launch_external_elf(app->elf_name); + return; + } if (!app->on_create) return; - /* 计算窗口位置(居中偏移) */ - int wx = 80 + g_win_count * 30; - int wy = 40 + g_win_count * 30; - if (wx + (int)app->default_w > (int)g_fb_w - 20) wx = 80; - if (wy + (int)app->default_h > (int)g_fb_h - TASKBAR_H - 20) wy = 40; + /* 单实例:已有同类窗口则聚焦并迁移到当前工作区 */ + for (int i = 0; i < g_win_count; i++) { + if (g_windows[i].app_id == app_id && g_windows[i].visible) { + g_windows[i].ws = g_ws_cur; + win_focus(&g_windows[i]); + slog("focus existing app window"); + return; + } + } + + int top = KATE_TOPBAR_H, bot = (int)g_fb_h - KATE_TASKBAR_H; + int wx = ICON_PAD + g_win_count * 30; + int wy = top + 8 + g_win_count * 30; + if (wx + (int)app->default_w > (int)g_fb_w - 20) wx = ICON_PAD; + if (wy + (int)app->default_h > bot - 20) wy = top + 8; desktop_window *win = win_create(app_id, app->display_name, wx, wy, (int)app->default_w, (int)app->default_h); if (!win) return; - /* 构建应用上下文 */ app_ctx ctx; - ctx.fb = &g_fb; - ctx.win = win; - ctx.client_x = win->x + BORDER_W; - ctx.client_y = win->y + TITLEBAR_H; - ctx.client_w = win->w - 2 * BORDER_W; - ctx.client_h = win->h - TITLEBAR_H - BORDER_W; - ctx.should_exit = 0; - ctx.block_read = g_block_read; - ctx.kernel_api = g_kernel_api; + fill_app_ctx(&ctx, win); win->app_state = app->on_create(&ctx); win->dirty = 1; - /* Window open animation - expanding rectangle */ + /* 窗口打开动画 — 展开矩形 */ for (int step = 0; step < 6; step++) { int scale = (step + 1) * 100 / 6; int aw = win->w * scale / 100; @@ -1326,9 +1519,8 @@ static void launch_app(int app_id) { int ay = win->y + (win->h - ah) / 2; cursor_restore_bg(); redraw_all(); - /* redraw_all 末尾已绘制光标,这里先擦除以保证保存的背景干净(不含光标像素) */ cursor_restore_bg(); - du_rect_outline(&g_fb, ax, ay, aw, ah, DS_DARK_ACCENT, DU_RADIUS_SM); + du_rect_outline(&g_fb, ax, ay, aw, ah, KS_ACCENT, DU_RADIUS_SM); cursor_save_bg(g_mouse_x, g_mouse_y); cursor_draw(g_mouse_x, g_mouse_y); anim_delay_poll_mouse(16); @@ -1338,46 +1530,1104 @@ static void launch_app(int app_id) { } /* ============================================================ - * 全屏重绘 + * FILES 面板(FAT32 真实读写) * ============================================================ */ -static void redraw_all(void) { - /* 渐变背景 */ - du_fill_bg_gradient(&g_fb, DP_ABYSS_900, DP_ABYSS_800); +static int files_list_cb(const char *name, u32 size, u8 attr, void *ud) { + (void)ud; + if (attr & 0x10) return 0; /* 跳过子目录 */ + if (g_file_count >= MAX_FILES) return 1; /* 停止 */ + fentry *f = &g_files[g_file_count]; + f->size = size; + kstrcpy(f->disp, name, 13); + /* disp (NAME.EXT) → name11 (NAME EXT) */ + for (int i = 0; i < 11; i++) f->name11[i] = ' '; + int i = 0, o = 0; + while (name[i] && name[i] != '.' && o < 8) f->name11[o++] = name[i++]; + while (name[i] && name[i] != '.') i++; + if (name[i] == '.') { + i++; + o = 8; + while (name[i] && o < 11) f->name11[o++] = name[i++]; + } + g_file_count++; + return 0; +} - /* 桌面图标 */ - draw_desktop_icons(); +static void files_refresh(void) { + g_file_count = 0; + g_file_sel = -1; + if (!g_block_read) { g_files_loaded = 0; return; } + if (f32_list_root(files_list_cb, 0) == 0) g_files_loaded = 1; + else g_files_loaded = 0; +} - /* 窗口(从底到顶) */ - for (int i = 0; i < g_win_count; i++) { - desktop_window *w = &g_windows[i]; - if (w->minimized || !w->visible) continue; +/* 双击打开文件 → 载入 DASHBOARD EDITOR */ +static void files_open(int idx) { + if (idx < 0 || idx >= g_file_count) return; + editor_state *ed = (editor_state *)ADDR_EDDASH; + fentry *f = &g_files[idx]; - /* 窗口装饰(先画框架作为窗口背景:主体、标题栏、边框) */ - draw_window_frame(w); + u8 *data = 0; u32 size = 0; + if (f32_read_root_file(f->name11, &data, &size) != 0) { + slog("file open failed"); + return; + } + ed->text_len = 0; + u32 cap = EDITOR_BUF_SIZE - 1; + if (size > cap) size = cap; + for (u32 i = 0; i < size; i++) { + char c = (char)data[i]; + if ((u8)c >= 32 || c == '\n' || c == '\t') + ed->text[ed->text_len++] = c; + } + for (int i = 0; i < 11; i++) ed->name11[i] = f->name11[i]; + kstrcpy(ed->disp, f->disp, 13); + ed->has_file = 1; + ed->modified = 0; + ed->scroll_y = 0; + ed->cursor_pos = 0; + editor_recalc_cursor(ed); + g_dash_focus = 2; + slog("file loaded into editor"); +} - /* 窗口客户区背景 */ - int cx = w->x + BORDER_W; - int cy = w->y + TITLEBAR_H; - int cw = w->w - 2 * BORDER_W; - int ch = w->h - TITLEBAR_H - BORDER_W; +/* EDITOR SAVE 回写磁盘 */ +static void editor_save(void) { + editor_state *ed = (editor_state *)ADDR_EDDASH; + if (!ed->has_file) return; + if (f32_write_root_file(ed->name11, (const u8 *)ed->text, (u32)ed->text_len) == 0) { + ed->modified = 0; + slog("file saved"); + } else { + slog("file save failed"); + } +} - /* 调用应用绘制(在框架之上绘制客户区内容) */ - if (w->app_id >= 0 && w->app_id < g_app_count && g_apps[w->app_id].on_draw && w->app_state) { - app_ctx ctx; - ctx.fb = &g_fb; - ctx.win = w; - ctx.client_x = cx; ctx.client_y = cy; - ctx.client_w = cw; ctx.client_h = ch; - ctx.should_exit = 0; - ctx.block_read = g_block_read; - ctx.kernel_api = g_kernel_api; - g_apps[w->app_id].on_draw(w->app_state, &ctx); +/* ============================================================ + * 通用小绘制 + * ============================================================ */ + +/* 矩形内居中文字 */ +static void draw_centered(const char *s, int cx, int y, u32 fg, u32 bg) { + int l = kstrlen(s); + du_draw_string(&g_fb, s, cx - l * (int)DU_ASCII_STEP / 2, y, fg, bg, DU_ASCII_STEP); +} + +/* 虚线矩形(ide-host / im-slot / custom host) */ +static void draw_dashed_rect(int x, int y, int w, int h, u32 color) { + for (int i = x; i < x + w; i += 8) { + du_fill_rect(&g_fb, i, y, 4, 1, color); + du_fill_rect(&g_fb, i, y + h - 1, 4, 1, color); + } + for (int i = y; i < y + h; i += 8) { + du_fill_rect(&g_fb, x, i, 1, 4, color); + du_fill_rect(&g_fb, x + w - 1, i, 1, 4, color); + } +} + +/* ============================================================ + * Boot 屏(打字机) + * ============================================================ */ + +static const char *BOOT_LINES[] = { + "DESHAB SHELL v0.1.0", + "initializing UTSM sealed memory...", + "mounting FAT32 root filesystem............[ OK ]", + "loading window manager (SAS-R0 Ring0).....[ OK ]", + "registering PS/2 input drivers............[ OK ]", + "probing AHCI block device.................[ OK ]", + "resolving desktop shortcuts...............[ OK ]", + "desktop replaced by Winux-Kate shell......[ OK ]", + "shell ready.", + "DEAICUP STUDIO", +}; +#define BOOT_LINE_COUNT 10 + +static void busy_delay(u32 cycles) { + for (volatile u32 i = 0; i < cycles; i++) __asm__("pause"); +} + +static void boot_screen(void) { + int W = (int)g_fb_w, H = (int)g_fb_h; + du_fill_bg_solid(&g_fb, KS_BG_PRIMARY); + + int lw = 46 * (int)DU_ASCII_STEP; + int lx = (W - lw) / 2; + int ty = H / 2 - 180; + + /* boot-title */ + const char *title = "DESHAB"; + int tl = kstrlen(title); + for (int i = 0; i < tl; i++) { + char t[2] = { title[i], 0 }; + du_draw_string(&g_fb, t, W / 2 - tl * (int)DU_ASCII_STEP + i * (int)DU_ASCII_STEP * 2, + ty, KS_ACCENT, 0, DU_ASCII_STEP * 2); + busy_delay(8000000); + } + du_divider_h(&g_fb, W / 2 - 120, ty + 30, 240, KS_ACCENT); + + /* boot-subtitle(对应 Kate "由 Deaicup 工作室制作") */ + draw_centered("DEAICUP STUDIO", W / 2, ty + 44, KS_ACCENT2, 0); + busy_delay(10000000); + + /* boot-lines 打字机;进度条固定在全部行下方(对应 Kate .boot-bar) */ + int ly = ty + 76; + int bar_y = ly + BOOT_LINE_COUNT * ((int)DU_ASCII_LINE_H + 2) + 12; + int bw = 260; + for (int li = 0; li < BOOT_LINE_COUNT; li++) { + const char *s = BOOT_LINES[li]; + u32 fg = (li == 1 || li == BOOT_LINE_COUNT - 1) ? KS_TEXT_DIM : KS_ACCENT2; + du_draw_string(&g_fb, ">", lx, ly, KS_ACCENT, 0, DU_ASCII_STEP); + for (int ci = 0; s[ci]; ci++) { + char t[2] = { s[ci], 0 }; + du_draw_string(&g_fb, t, lx + (ci + 1) * (int)DU_ASCII_STEP, ly, + fg, 0, DU_ASCII_STEP); + busy_delay(500000); + } + ly += (int)DU_ASCII_LINE_H + 2; + /* boot-bar:渐变填充(accent → accent2) */ + int fill = bw * (li + 1) / BOOT_LINE_COUNT; + du_rect_outline(&g_fb, W / 2 - bw / 2, bar_y, bw, 6, KS_BORDER, 1); + if (fill > 2) du_fill_rect_gradient(&g_fb, W / 2 - bw / 2 + 1, bar_y + 1, + fill - 2, 4, KS_ACCENT, KS_ACCENT2); + } + + /* boot-hint + credit(对应 Kate "SYSTEM ONLINE" + "© 2026 Deaicup Studio") */ + draw_centered("SYSTEM ONLINE", W / 2, H - 64, KS_ACCENT2, 0); + draw_centered("(C) 2026 DEAICUP STUDIO", W / 2, H - 40, KS_TEXT_DIM, 0); + busy_delay(40000000); +} + +/* ============================================================ + * 顶栏(brand + WorkspaceSwitcher + PageSwitcher + PG 时钟) + * ============================================================ */ + +static void draw_topbar(void) { + int W = (int)g_fb_w; + du_fill_rect_gradient(&g_fb, 0, 0, W, KATE_TOPBAR_H, + 0xFF0A2840u, KS_BG_PRIMARY); + du_divider_h(&g_fb, 0, KATE_TOPBAR_H, W, KS_BORDER); + + int x = 12; + int ty = (KATE_TOPBAR_H - (int)DU_ASCII_LINE_H) / 2 + 1; + + /* brand:方块点 + DESHAB + brand-credit(对应 Kate "由 Deaicup 工作室制作") */ + du_fill_rect(&g_fb, x, KATE_TOPBAR_H / 2 - 3, 6, 6, KS_ACCENT); + du_draw_string(&g_fb, "DESHAB", x + 12, ty, KS_ACCENT, 0, DU_ASCII_STEP + 2); + x += 12 + 6 * ((int)DU_ASCII_STEP + 2) + 10; + du_draw_string(&g_fb, "DEAICUP STUDIO", x, ty + 2, KS_ACCENT2, 0, DU_ASCII_STEP); + x += 14 * (int)DU_ASCII_STEP + 14; + + /* 分隔线 + 任务视图按钮(三横线图标) */ + du_divider_v(&g_fb, x - 6, 6, KATE_TOPBAR_H - 12, KS_BORDER); + g_tvbtn_rect[0] = x; g_tvbtn_rect[1] = 5; + g_tvbtn_rect[2] = 28; g_tvbtn_rect[3] = 24; + { + int hover = g_mouse_x >= x && g_mouse_x < x + 28 && + g_mouse_y >= 5 && g_mouse_y < 29; + if (hover) du_fill_rect(&g_fb, x, 5, 28, 24, KS_ACCENT_DIM); + du_rect_outline(&g_fb, x, 5, 28, 24, KS_BORDER, 1); + du_fill_rect(&g_fb, x + 6, 11, 16, 2, KS_ACCENT); + du_fill_rect(&g_fb, x + 6, 16, 16, 2, KS_ACCENT); + du_fill_rect(&g_fb, x + 6, 21, 16, 2, KS_ACCENT); + } + x += 34; + + /* 工作区按钮 "name|n" */ + for (int i = 0; i < g_ws_count; i++) { + char label[24]; + kstrcpy(label, g_ws[i].name, 18); + kstrcat(label, "|", 24); + int cnt = ws_win_count(i); + char cn[4]; + cn[0] = '0' + (char)(cnt % 10); cn[1] = 0; + kstrcat(label, cn, 24); + int bw = kstrlen(label) * (int)DU_ASCII_STEP + 16; + int active = (i == g_ws_cur); + int hover = g_mouse_x >= x && g_mouse_x < x + bw && + g_mouse_y >= 5 && g_mouse_y < 29; + if (active) { + du_fill_rect(&g_fb, x, 5, bw, 24, KS_ACCENT2); + du_kate_glow_border(&g_fb, x, 5, bw, 24, KS_ACCENT2); + } else if (hover) { + du_fill_rect(&g_fb, x, 5, bw, 24, KS_ACCENT_DIM); + } + du_rect_outline(&g_fb, x, 5, bw, 24, active ? KS_ACCENT2 : KS_BORDER, 1); + du_draw_string(&g_fb, label, x + 8, ty, + active ? KS_TEXT_INVERT : KS_TEXT_DIM, 0, DU_ASCII_STEP); + g_ws_rect[i][0] = x; g_ws_rect[i][1] = 5; + g_ws_rect[i][2] = bw; g_ws_rect[i][3] = 24; + x += bw + 6; + } + + /* ws-add "+" */ + g_wsadd_rect[0] = x; g_wsadd_rect[1] = 5; + g_wsadd_rect[2] = 26; g_wsadd_rect[3] = 24; + { + int hover = g_mouse_x >= x && g_mouse_x < x + 26 && + g_mouse_y >= 5 && g_mouse_y < 29; + if (hover) du_fill_rect(&g_fb, x, 5, 26, 24, KS_ACCENT_DIM); + du_rect_outline(&g_fb, x, 5, 26, 24, KS_ACCENT2, 1); + du_draw_string(&g_fb, "+", x + 7, ty - 1, KS_ACCENT2, 0, DU_ASCII_STEP); + } + x += 32; + + /* 分隔线 */ + du_divider_v(&g_fb, x - 4, 6, KATE_TOPBAR_H - 12, KS_BORDER); + x += 4; + + /* 页面切换 */ + static const char *pg_names[4] = { "1.DASHBOARD", "2.IDE", "3.IM", "4.DESKTOP" }; + for (int i = 0; i < 4; i++) { + int bw = kstrlen(pg_names[i]) * (int)DU_ASCII_STEP + 16; + int active = (g_page == i + 1); + int hover = g_mouse_x >= x && g_mouse_x < x + bw && + g_mouse_y >= 5 && g_mouse_y < 29; + if (active) { + du_fill_rect(&g_fb, x, 5, bw, 24, KS_ACCENT); + du_kate_glow_border(&g_fb, x, 5, bw, 24, KS_ACCENT); + } else if (hover) { + du_fill_rect(&g_fb, x, 5, bw, 24, KS_ACCENT_DIM); + } + du_rect_outline(&g_fb, x, 5, bw, 24, active ? KS_ACCENT : KS_BORDER, 1); + du_draw_string(&g_fb, pg_names[i], x + 8, ty, + active ? KS_TEXT_INVERT : KS_TEXT_DIM, 0, DU_ASCII_STEP); + g_pg_rect[i][0] = x; g_pg_rect[i][1] = 5; + g_pg_rect[i][2] = bw; g_pg_rect[i][3] = 24; + x += bw + 6; + } + /* 自定义页 */ + for (int i = 0; i < g_cpage_count; i++) { + char label[20]; + label[0] = '0' + (char)g_cpages[i].id; + label[1] = '.'; label[2] = 0; + kstrcat(label, g_cpages[i].name, 20); + int bw = kstrlen(label) * (int)DU_ASCII_STEP + 28; + int active = (g_page == g_cpages[i].id); + int hover = g_mouse_x >= x && g_mouse_x < x + bw && + g_mouse_y >= 5 && g_mouse_y < 29; + if (active) { + du_fill_rect(&g_fb, x, 5, bw, 24, KS_ACCENT); + } else if (hover) { + du_fill_rect(&g_fb, x, 5, bw, 24, KS_ACCENT_DIM); + } + du_rect_outline(&g_fb, x, 5, bw, 24, active ? KS_ACCENT : KS_BORDER, 1); + du_draw_string(&g_fb, label, x + 6, ty, + active ? KS_TEXT_INVERT : KS_TEXT_DIM, 0, DU_ASCII_STEP); + du_draw_string(&g_fb, "x", x + bw - 14, ty, KS_DANGER, 0, DU_ASCII_STEP); + g_pg_rect[4 + i][0] = x; g_pg_rect[4 + i][1] = 5; + g_pg_rect[4 + i][2] = bw; g_pg_rect[4 + i][3] = 24; + g_pg_rm_rect[4 + i][0] = x + bw - 20; g_pg_rm_rect[4 + i][1] = 5; + g_pg_rm_rect[4 + i][2] = 20; g_pg_rm_rect[4 + i][3] = 24; + x += bw + 6; + } + /* pg-add "+" */ + if (g_cpage_count < MAX_CPAGES) { + g_pgadd_rect[0] = x; g_pgadd_rect[1] = 5; + g_pgadd_rect[2] = 26; g_pgadd_rect[3] = 24; + int hover = g_mouse_x >= x && g_mouse_x < x + 26 && + g_mouse_y >= 5 && g_mouse_y < 29; + if (hover) du_fill_rect(&g_fb, x, 5, 26, 24, KS_ACCENT_DIM); + du_rect_outline(&g_fb, x, 5, 26, 24, KS_ACCENT2, 1); + du_draw_string(&g_fb, "+", x + 7, ty - 1, KS_ACCENT2, 0, DU_ASCII_STEP); + } else { + g_pgadd_rect[2] = 0; + } + + /* 右侧:credit-top + PG 时钟(对应 Kate "© Deaicup Studio" + "PG n") */ + char pgclk[8]; + pgclk[0] = 'P'; pgclk[1] = 'G'; pgclk[2] = ' '; + pgclk[3] = '0' + (char)(g_page % 10); pgclk[4] = 0; + const char *credit = "(C) DEAICUP STUDIO"; + int cl = kstrlen(credit) + 1 + kstrlen(pgclk); + du_draw_string(&g_fb, credit, W - 12 - cl * (int)DU_ASCII_STEP, ty, + KS_TEXT_DIM, 0, DU_ASCII_STEP); + du_draw_string(&g_fb, pgclk, W - 12 - kstrlen(pgclk) * (int)DU_ASCII_STEP, ty, + KS_ACCENT2, 0, DU_ASCII_STEP); +} + +/* ============================================================ + * 滑块(VOL / BRIGHT) + * ============================================================ */ + +static void draw_slider(int x, int y, int w, int val, int rect_out[4]) { + du_fill_rect(&g_fb, x, y + 5, w, 3, KS_BG_TERTIARY); + du_rect_outline(&g_fb, x, y + 4, w, 5, KS_BORDER, 1); + int fw = w * val / 100; + if (fw > 0) du_fill_rect(&g_fb, x, y + 5, fw, 3, KS_ACCENT); + int kx = x + fw - 3; + if (kx < x) kx = x; + if (kx > x + w - 6) kx = x + w - 6; + du_fill_rect(&g_fb, kx, y, 6, 13, KS_ACCENT); + du_rect_outline(&g_fb, kx, y, 6, 13, KS_ACCENT, 1); + rect_out[0] = x; rect_out[1] = y - 2; + rect_out[2] = w; rect_out[3] = 17; +} + +/* ============================================================ + * 状态栏(DASHBOARD 底部 32px) + * ============================================================ */ + +static void draw_statusbar_full(void) { + int W = (int)g_fb_w; + int y = (int)g_fb_h - KATE_STATUSBAR_H; + du_fill_rect(&g_fb, 0, y, W, KATE_STATUSBAR_H, 0xFF051828u); + du_divider_h(&g_fb, 0, y, W, KS_BORDER); + + int x = 12; + int ty = y + (KATE_STATUSBAR_H - (int)DU_ASCII_LINE_H) / 2 + 1; + + /* 时钟(日期 · 秒级时间,accent2) */ + int yy, mo, dd, hh, mi, ss; + rtc_read(&yy, &mo, &dd, &hh, &mi, &ss); + char clk[32]; + int cp2 = 0; + clk[cp2++] = '2'; clk[cp2++] = '0'; + clk[cp2++] = '0' + (char)(yy / 10); clk[cp2++] = '0' + (char)(yy % 10); + clk[cp2++] = '-'; + clk[cp2++] = '0' + (char)(mo / 10); clk[cp2++] = '0' + (char)(mo % 10); + clk[cp2++] = '-'; + clk[cp2++] = '0' + (char)(dd / 10); clk[cp2++] = '0' + (char)(dd % 10); + clk[cp2++] = ' '; clk[cp2++] = '.'; clk[cp2++] = ' '; + char hms[9]; + fmt_hms(hms, hh * 3600 + mi * 60 + ss); + for (int i = 0; hms[i]; i++) clk[cp2++] = hms[i]; + clk[cp2] = 0; + du_draw_string(&g_fb, clk, x, ty, KS_ACCENT2, 0, DU_ASCII_STEP); + x += cp2 * (int)DU_ASCII_STEP + 18; + + /* UP 计时 */ + { + int up = hh * 3600 + mi * 60 + ss - g_rtc_boot_sec; + if (up < 0) up += 86400; + char upb[16]; + kstrcpy(upb, "UP ", 16); + char uh[9]; + fmt_hms(uh, up); + kstrcat(upb, uh, 16); + du_draw_string(&g_fb, upb, x, ty, KS_TEXT_DIM, 0, DU_ASCII_STEP); + x += kstrlen(upb) * (int)DU_ASCII_STEP + 18; + } + + /* VOL 滑块 */ + du_draw_string(&g_fb, "VOL", x, ty, KS_TEXT_DIM, 0, DU_ASCII_STEP); + x += 3 * (int)DU_ASCII_STEP + 6; + draw_slider(x, y + 9, 90, g_vol, g_vol_rect); + x += 90 + 8; + { + char vb[6]; + vb[0] = '0' + (char)(g_vol / 100 % 10); + vb[1] = '0' + (char)(g_vol / 10 % 10); + vb[2] = '0' + (char)(g_vol % 10); + vb[3] = '%'; vb[4] = 0; + const char *vp = (g_vol >= 100) ? vb : (g_vol >= 10 ? vb + 1 : vb + 2); + du_draw_string(&g_fb, vp, x, ty, KS_ACCENT2, 0, DU_ASCII_STEP); + x += 4 * (int)DU_ASCII_STEP + 14; + } + + /* BRIGHT 滑块 */ + du_draw_string(&g_fb, "BRIGHT", x, ty, KS_TEXT_DIM, 0, DU_ASCII_STEP); + x += 6 * (int)DU_ASCII_STEP + 6; + draw_slider(x, y + 9, 90, g_bright, g_bri_rect); + x += 90 + 8; + { + char vb[6]; + vb[0] = '0' + (char)(g_bright / 100 % 10); + vb[1] = '0' + (char)(g_bright / 10 % 10); + vb[2] = '0' + (char)(g_bright % 10); + vb[3] = '%'; vb[4] = 0; + const char *vp = (g_bright >= 100) ? vb : (g_bright >= 10 ? vb + 1 : vb + 2); + du_draw_string(&g_fb, vp, x, ty, KS_ACCENT2, 0, DU_ASCII_STEP); + x += 4 * (int)DU_ASCII_STEP + 14; + } + + /* BT / WIFI */ + du_draw_string(&g_fb, "BT OFF", x, ty, KS_TEXT_DIM, 0, DU_ASCII_STEP); + x += 6 * (int)DU_ASCII_STEP + 14; + du_draw_string(&g_fb, "WIFI --", x, ty, KS_TEXT_DIM, 0, DU_ASCII_STEP); + + /* 右侧 hint */ + const char *hint = "Ctrl+Tab PAGE . Ctrl+Shift+Tab CTX"; + du_draw_string(&g_fb, hint, W - 12 - kstrlen(hint) * (int)DU_ASCII_STEP, ty, + KS_TEXT_DIM, 0, DU_ASCII_STEP); +} + +/* ============================================================ + * 页面绘制 + * ============================================================ */ + +static void draw_desktop_icons(void) { + /* desktop-empty(Kate:无快捷方式提示) */ + if (g_icon_count == 0) { + du_draw_string(&g_fb, "NO SHORTCUTS ON DESKTOP", + KATE_ICON_PAD, KATE_TOPBAR_H + KATE_PAGE_PAD + KATE_ICON_PAD + 16, + KS_TEXT_DIM, 0, DU_ASCII_STEP); + return; + } + for (int i = 0; i < g_icon_count; i++) { + desktop_icon *ic = &g_icons[i]; + du_kate_desktop_icon(&g_fb, ic->x, ic->y, ic->label, ic->selected); + int ix = ic->x + (KATE_ICON_W - KATE_ICON_IMG) / 2; + int iy = ic->y + 10; + draw_app_icon(&g_fb, ic->app_id, ix, iy); + } +} + +/* ---- 1·DASHBOARD ---- */ +static void dash_layout(void) { + int top = KATE_TOPBAR_H, bot = (int)g_fb_h - KATE_STATUSBAR_H; + int pad = 8, gap = 8; + int avail_w = (int)g_fb_w - 2 * pad; + int avail_h = bot - top - 2 * pad; + int left_w = avail_w * 42 / 100; + int right_x = pad + left_w + gap; + int right_w = (int)g_fb_w - pad - right_x; + int row_h = (avail_h - gap) / 2; + + g_dash_rect[0][0] = pad; g_dash_rect[0][1] = top + pad; + g_dash_rect[0][2] = left_w; g_dash_rect[0][3] = row_h; + g_dash_rect[1][0] = pad; g_dash_rect[1][1] = top + pad + row_h + gap; + g_dash_rect[1][2] = left_w; g_dash_rect[1][3] = row_h; + g_dash_rect[2][0] = right_x; g_dash_rect[2][1] = top + pad; + g_dash_rect[2][2] = right_w; g_dash_rect[2][3] = row_h; + g_dash_rect[3][0] = right_x; g_dash_rect[3][1] = top + pad + row_h + gap; + g_dash_rect[3][2] = right_w; g_dash_rect[3][3] = row_h; +} + +static void draw_dashboard(void) { + dash_layout(); + const char *headers[4] = { "TERM-01", "TERM-02", 0, "FILES" }; + char ed_hdr[40]; + editor_state *ed = (editor_state *)ADDR_EDDASH; + kstrcpy(ed_hdr, "EDITOR . ", 40); + kstrcat(ed_hdr, ed->has_file ? ed->disp : "untitled", 40); + headers[2] = ed_hdr; + + for (int i = 0; i < 4; i++) { + int x = g_dash_rect[i][0], y = g_dash_rect[i][1]; + int w = g_dash_rect[i][2], h = g_dash_rect[i][3]; + du_kate_panel(&g_fb, x, y, w, h, headers[i]); + if (g_dash_focus == i) { + du_rect_outline(&g_fb, x, y, w, h, KS_ACCENT, 2); } } - /* 任务栏 */ - draw_taskbar(); + /* EDITOR 头 SAVE 按钮(仅关联文件后可用) */ + { + int ex = g_dash_rect[2][0], ey = g_dash_rect[2][1], ew = g_dash_rect[2][2]; + int bw = 52, bh = 18; + int bx = ex + ew - bw - 6, by = ey + 3; + g_save_rect[0] = bx; g_save_rect[1] = by; + g_save_rect[2] = bw; g_save_rect[3] = bh; + u32 fg = ed->has_file ? KS_ACCENT : KS_TEXT_DIM; + int hover = ed->has_file && + g_mouse_x >= bx && g_mouse_x < bx + bw && + g_mouse_y >= by && g_mouse_y < by + bh; + if (hover) du_fill_rect(&g_fb, bx, by, bw, bh, KS_ACCENT_DIM); + du_rect_outline(&g_fb, bx, by, bw, bh, ed->has_file ? KS_BORDER : KS_BORDER_DIM, 1); + du_draw_string(&g_fb, "SAVE", bx + 5, by + 1, fg, 0, DU_ASCII_STEP); + } + + /* 面板内容 */ + bash_draw_to((bash_state *)ADDR_TERM1, + g_dash_rect[0][0] + 2, g_dash_rect[0][1] + KATE_PANEL_HEADER_H + 2, + g_dash_rect[0][2] - 4, g_dash_rect[0][3] - KATE_PANEL_HEADER_H - 4); + + bash_draw_to((bash_state *)ADDR_TERM2, + g_dash_rect[1][0] + 2, g_dash_rect[1][1] + KATE_PANEL_HEADER_H + 2, + g_dash_rect[1][2] - 4, g_dash_rect[1][3] - KATE_PANEL_HEADER_H - 4); + + editor_draw_to(ed, + g_dash_rect[2][0] + 2, g_dash_rect[2][1] + KATE_PANEL_HEADER_H + 2, + g_dash_rect[2][2] - 4, g_dash_rect[2][3] - KATE_PANEL_HEADER_H - 4); + + /* FILES 面板内容 */ + { + int fx = g_dash_rect[3][0] + 2, fy = g_dash_rect[3][1] + KATE_PANEL_HEADER_H + 2; + int fw = g_dash_rect[3][2] - 4, fh = g_dash_rect[3][3] - KATE_PANEL_HEADER_H - 4; + du_fill_rect(&g_fb, fx, fy, fw, fh, KS_BG_PRIMARY); + + char path_buf[32]; + kstrcpy(path_buf, "FAT32:/ROOT (", 32); + int pl = kstrlen(path_buf); + if (g_file_count >= 10) path_buf[pl++] = '0' + (char)(g_file_count / 10); + path_buf[pl++] = '0' + (char)(g_file_count % 10); + path_buf[pl++] = ')'; path_buf[pl] = 0; + du_draw_string(&g_fb, path_buf, fx + 8, fy + 3, KS_ACCENT, KS_BG_PRIMARY, DU_ASCII_STEP); + du_divider_h(&g_fb, fx, fy + 22, fw, KS_BORDER); + + int row_h = 22; + int max_rows = (fh - 24) / row_h; + for (int i = 0; i < g_file_count && i < max_rows; i++) { + int ry = fy + 24 + i * row_h; + if (i == g_file_sel) { + du_fill_rect(&g_fb, fx, ry, fw, row_h, KS_ACCENT_DIM); + } + int hover = (g_mouse_x >= fx && g_mouse_x < fx + fw && + g_mouse_y >= ry && g_mouse_y < ry + row_h); + if (hover && i != g_file_sel) { + du_fill_rect(&g_fb, fx, ry, fw, row_h, KS_ACCENT_DIM); + } + u32 fg = (i == g_file_sel) ? KS_ACCENT : KS_TEXT_PRIMARY; + du_draw_string(&g_fb, g_files[i].disp, fx + 8, ry + 2, fg, 0, DU_ASCII_STEP); + char sz[12]; + u32 kb = g_files[i].size / 1024; + int sp = 0; + if (kb >= 100) { sz[sp++] = '0' + (char)(kb / 100 % 10); } + if (kb >= 10) { sz[sp++] = '0' + (char)(kb / 10 % 10); } + sz[sp++] = '0' + (char)(kb % 10); + sz[sp++] = 'K'; sz[sp] = 0; + du_draw_string(&g_fb, sz, fx + fw - sp * (int)DU_ASCII_STEP - 8, ry + 2, + KS_TEXT_DIM, 0, DU_ASCII_STEP); + } + if (!g_files_loaded) { + du_draw_string(&g_fb, "NO BLOCK DEVICE", fx + 8, fy + 28, + KS_DANGER, KS_BG_PRIMARY, DU_ASCII_STEP); + } else if (g_file_count == 0) { + du_draw_string(&g_fb, "(empty)", fx + 8, fy + 28, + KS_TEXT_DIM, KS_BG_PRIMARY, DU_ASCII_STEP); + } + } + + draw_statusbar_full(); +} + +/* ---- 2·IDE ---- */ +static int g_ide_attached = 0; + +static void draw_ide(void) { + int W = (int)g_fb_w; + int top = KATE_TOPBAR_H; + + /* ide-tabs(36px):dot + 实例标签 + "+ NEW IDE" + ide-hint(Kate 结构) */ + du_fill_rect(&g_fb, 0, top, W, 36, 0xFF051828u); + du_divider_h(&g_fb, 0, top + 36, W, KS_BORDER); + int x = 12; + int ty = top + (36 - (int)DU_ASCII_LINE_H) / 2 + 1; + + /* .dot(8×8 accent2 发光点) */ + du_fill_rect(&g_fb, x, top + 14, 8, 8, KS_ACCENT2); + x += 16; + + /* 实例标签 EDITOR ×(Kate .ide-tab) */ + { + const char *label = "EDITOR"; + int bw = kstrlen(label) * (int)DU_ASCII_STEP + 32; + if (g_ide_attached) { + du_fill_rect(&g_fb, x, top + 5, bw, 26, KS_ACCENT); + du_kate_glow_border(&g_fb, x, top + 5, bw, 26, KS_ACCENT); + } + du_rect_outline(&g_fb, x, top + 5, bw, 26, + g_ide_attached ? KS_ACCENT : KS_BORDER, 1); + du_draw_string(&g_fb, label, x + 8, ty, + g_ide_attached ? KS_TEXT_INVERT : KS_TEXT_DIM, 0, DU_ASCII_STEP); + du_draw_string(&g_fb, "x", x + bw - 14, ty, KS_DANGER, 0, DU_ASCII_STEP); + g_ide_tabx[0] = x + bw - 20; g_ide_tabx[1] = top + 5; + g_ide_tabx[2] = 20; g_ide_tabx[3] = 26; + x += bw + 8; + } + + /* "+ NEW IDE" 按钮(Kate .btn,透明底 + 边框 + accent 字) */ + { + const char *nb = "+ NEW IDE"; + int bw = kstrlen(nb) * (int)DU_ASCII_STEP + 20; + g_ide_new_rect[0] = x; g_ide_new_rect[1] = top + 7; + g_ide_new_rect[2] = bw; g_ide_new_rect[3] = 22; + int hover = g_mouse_x >= x && g_mouse_x < x + bw && + g_mouse_y >= top + 7 && g_mouse_y < top + 29; + if (hover) du_fill_rect(&g_fb, x, top + 7, bw, 22, KS_ACCENT_DIM); + du_rect_outline(&g_fb, x, top + 7, bw, 22, KS_BORDER, 1); + du_draw_string(&g_fb, nb, x + 10, ty, KS_ACCENT, 0, DU_ASCII_STEP); + x += bw + 8; + } + + /* ide-hint */ + du_draw_string(&g_fb, "CTRL+SHIFT+TAB CTX . x DETACH . DBL-CLICK HOST ATTACH", + x + 8, ty + 1, KS_TEXT_DIM, 0, DU_ASCII_STEP); + + /* page-credit(对应 Kate "由 Deaicup 工作室制作") */ + du_draw_string(&g_fb, "DEAICUP STUDIO", + W - 12 - 14 * (int)DU_ASCII_STEP, ty + 1, + KS_TEXT_DIM, 0, DU_ASCII_STEP); + + /* ide-host(虚线,margin 8px) */ + int hx = 8, hy = top + 36 + 8; + int hw = W - 16, hh = (int)g_fb_h - hy - 8; + g_ide_host[0] = hx; g_ide_host[1] = hy; + g_ide_host[2] = hw; g_ide_host[3] = hh; + draw_dashed_rect(hx, hy, hw, hh, KS_BORDER); + + if (g_ide_attached) { + editor_draw_to((editor_state *)ADDR_EDDASH, hx + 2, hy + 2, hw - 4, hh - 4); + } else { + /* embed-empty(Kate:未启动 VSCode 提示) */ + draw_centered("EDITOR NOT ATTACHED . DOUBLE-CLICK OR + NEW IDE", + W / 2, hy + hh / 2 - 8, KS_TEXT_DIM, 0); + } +} + +/* ---- 3·IM ---- */ +static int g_im_view = 0; /* 0=split(WECHAT/QQ) 1=full(WECOM),对应 Kate imView */ + +/* Kate .missing-card:h2 + p + a.btn 下载按钮 */ +static void draw_missing_card(int sx, int sy, int sw, int sh, + const char *title, const char *desc, + const char *btn) { + int cw = 340, ch = 150; + int cx = sx + (sw - cw) / 2, cy = sy + (sh - ch) / 2; + du_kate_panel(&g_fb, cx, cy, cw, ch, 0); + draw_centered(title, sx + sw / 2, cy + 28, KS_ACCENT, 0); + draw_centered(desc, sx + sw / 2, cy + 56, KS_TEXT_DIM, 0); + /* .btn 下载按钮(视觉复刻,Deshab 无网络安装渠道) */ + int bw = kstrlen(btn) * (int)DU_ASCII_STEP + 20; + int bx = sx + sw / 2 - bw / 2, by = cy + 88; + int hover = g_mouse_x >= bx && g_mouse_x < bx + bw && + g_mouse_y >= by && g_mouse_y < by + 22; + if (hover) du_fill_rect(&g_fb, bx, by, bw, 22, KS_ACCENT_DIM); + du_rect_outline(&g_fb, bx, by, bw, 22, KS_BORDER, 1); + du_draw_string(&g_fb, btn, bx + 10, by + 3, KS_ACCENT, 0, DU_ASCII_STEP); +} + +static void draw_im(void) { + int W = (int)g_fb_w; + int top = KATE_TOPBAR_H; + + /* im-bar(36px):dot + 视图标签 + page-credit + 切换按钮(margin-left:auto) */ + du_fill_rect(&g_fb, 0, top, W, 36, 0xFF051828u); + du_divider_h(&g_fb, 0, top + 36, W, KS_BORDER); + int ty = top + (36 - (int)DU_ASCII_LINE_H) / 2 + 1; + + /* .dot(8×8 accent2 发光点) */ + du_fill_rect(&g_fb, 12, top + 14, 8, 8, KS_ACCENT2); + + du_draw_string(&g_fb, + g_im_view ? "WECOM FULLSCREEN" : "WECHAT / QQ SPLIT", + 28, ty, KS_ACCENT, 0, DU_ASCII_STEP); + /* page-credit(对应 Kate "由 Deaicup 工作室制作") */ + du_draw_string(&g_fb, "DEAICUP STUDIO", + 28 + (g_im_view ? 16 : 17) * (int)DU_ASCII_STEP + 12, + ty + 1, KS_TEXT_DIM, 0, DU_ASCII_STEP); + + /* 切换按钮(对应 Kate im_toggle,Ctrl+Shift+Tab) */ + { + const char *tb = "CTRL+SHIFT+TAB SWITCH"; + int bw = kstrlen(tb) * (int)DU_ASCII_STEP + 20; + int bx = W - 12 - bw; + g_im_toggle_rect[0] = bx; g_im_toggle_rect[1] = top + 7; + g_im_toggle_rect[2] = bw; g_im_toggle_rect[3] = 22; + int hover = g_mouse_x >= bx && g_mouse_x < bx + bw && + g_mouse_y >= top + 7 && g_mouse_y < top + 29; + if (hover) du_fill_rect(&g_fb, bx, top + 7, bw, 22, KS_ACCENT_DIM); + du_rect_outline(&g_fb, bx, top + 7, bw, 22, KS_BORDER, 1); + du_draw_string(&g_fb, tb, bx + 10, ty, KS_ACCENT, 0, DU_ASCII_STEP); + } + + if (g_im_view == 0) { + /* im-split 双槽(grid 1fr 1fr, gap/padding 8px) */ + int pad = 8, gap = 8; + int hy = top + 36 + pad; + int hh = (int)g_fb_h - hy - pad; + int sw = (W - 2 * pad - gap) / 2; + for (int i = 0; i < 2; i++) { + int sx = pad + i * (sw + gap); + draw_dashed_rect(sx, hy, sw, hh, KS_BORDER); + } + draw_missing_card(pad, hy, sw, hh, + "WECHAT NOT DETECTED", + "IM MODULE NOT AVAILABLE IN DESHAB", + "DOWNLOAD WECHAT"); + draw_missing_card(pad + sw + gap, hy, sw, hh, + "QQ NOT DETECTED", + "IM MODULE NOT AVAILABLE IN DESHAB", + "DOWNLOAD QQ"); + } else { + /* im-full 单槽(margin 8px) */ + int hx = 8, hy = top + 36 + 8; + int hw = W - 16, hh = (int)g_fb_h - hy - 8; + draw_dashed_rect(hx, hy, hw, hh, KS_BORDER); + draw_missing_card(hx, hy, hw, hh, + "WECOM NOT DETECTED", + "IM MODULE NOT AVAILABLE IN DESHAB", + "DOWNLOAD WECOM"); + } +} + +/* ---- 4·DESKTOP ---- */ +static void draw_desktop_page(void) { + /* 图标网格 */ + draw_desktop_icons(); + + /* 窗口(当前工作区,从底到顶) */ + for (int i = 0; i < g_win_count; i++) { + desktop_window *w = &g_windows[i]; + if (w->minimized || !w->visible) continue; + if (w->ws != g_ws_cur) continue; + + draw_window_frame(w); + + if (w->app_id >= 0 && w->app_id < g_app_count && + g_apps[w->app_id].on_draw && w->app_state) { + app_ctx ctx; + fill_app_ctx(&ctx, w); + g_apps[w->app_id].on_draw(w->app_state, &ctx); + } + + du_kate_scanlines(&g_fb, w->x + 1, w->y + KATE_TITLEBAR_H, + w->w - 2, w->h - KATE_TITLEBAR_H - 1); + } + + /* 任务栏(40px) */ + int W = (int)g_fb_w; + int tb_y = (int)g_fb_h - KATE_TASKBAR_H; + du_fill_rect_gradient(&g_fb, 0, tb_y, W, KATE_TASKBAR_H, + 0xFF0A2840u, KS_BG_PRIMARY); + du_divider_h(&g_fb, 0, tb_y, W, KS_BORDER); + + int x = 8; + int ty = tb_y + (KATE_TASKBAR_H - (int)DU_ASCII_LINE_H) / 2 + 1; + + /* tb-item per window(当前工作区) */ + for (int i = 0; i < g_win_count; i++) { + desktop_window *w = &g_windows[i]; + if (!w->visible || w->ws != g_ws_cur) { + g_tb_rect[i][2] = 0; g_tb_close_rect[i][2] = 0; + continue; + } + char t[16]; + int tl = 0; + while (w->title[tl] && tl < 11) { t[tl] = w->title[tl]; tl++; } + if (w->title[tl]) { t[tl-1] = '.'; t[tl] = '.'; tl++; } + t[tl] = 0; + int bw = tl * (int)DU_ASCII_STEP + 34; + if (bw > 150) bw = 150; + int active = w->focused; + int hover = g_mouse_x >= x && g_mouse_x < x + bw && + g_mouse_y >= tb_y + 6 && g_mouse_y < tb_y + 34; + if (active) du_fill_rect(&g_fb, x, tb_y + 6, bw, 28, KS_ACCENT_DIM); + else if (hover) du_fill_rect(&g_fb, x, tb_y + 6, bw, 28, KS_ACCENT_DIM); + du_rect_outline(&g_fb, x, tb_y + 6, bw, 28, + active ? KS_ACCENT : KS_BORDER, 1); + du_draw_string(&g_fb, t, x + 6, ty, + active ? KS_ACCENT : KS_TEXT_PRIMARY, 0, DU_ASCII_STEP); + du_draw_string(&g_fb, "x", x + bw - 14, ty, KS_TEXT_DIM, 0, DU_ASCII_STEP); + g_tb_rect[i][0] = x; g_tb_rect[i][1] = tb_y + 6; + g_tb_rect[i][2] = bw; g_tb_rect[i][3] = 28; + g_tb_close_rect[i][0] = x + bw - 20; g_tb_close_rect[i][1] = tb_y + 6; + g_tb_close_rect[i][2] = 20; g_tb_close_rect[i][3] = 28; + x += bw + 6; + if (x > W - 260) break; + } + + /* Winux-Kate 任务栏右组(margin-left:auto): + * [+ ADOPT 虚框] [tb-credit 署名] [EXIT 危险色] */ + const char *credit = "DEAICUP STUDIO"; + const char *quit = "EXIT"; + const char *adopt = "+ ADOPT"; + + int qw = 60; + int qx = W - qw - 8; + int credit_w = kstrlen(credit) * (int)DU_ASCII_STEP; + int cx = qx - 8 - credit_w; + int aw = kstrlen(adopt) * (int)DU_ASCII_STEP + 16; + int ax = cx - 8 - aw; + + /* tb-adopt:虚线 accent 边框 */ + g_tb_adopt_rect[0] = ax; g_tb_adopt_rect[1] = tb_y + 6; + g_tb_adopt_rect[2] = aw; g_tb_adopt_rect[3] = 28; + { + int hover = g_mouse_x >= ax && g_mouse_x < ax + aw && + g_mouse_y >= tb_y + 6 && g_mouse_y < tb_y + 34; + if (hover) du_fill_rect(&g_fb, ax, tb_y + 6, aw, 28, KS_ACCENT_DIM); + draw_dashed_rect(ax, tb_y + 6, aw, 28, KS_ACCENT); + du_draw_string(&g_fb, adopt, ax + 8, ty, KS_ACCENT, 0, DU_ASCII_STEP); + } + + /* tb-credit */ + du_draw_string(&g_fb, credit, cx, ty, KS_TEXT_DIM, 0, DU_ASCII_STEP); + + /* tb-quit */ + g_tb_quit_rect[0] = qx; g_tb_quit_rect[1] = tb_y + 6; + g_tb_quit_rect[2] = qw; g_tb_quit_rect[3] = 28; + { + int hover = g_mouse_x >= qx && g_mouse_x < qx + qw && + g_mouse_y >= tb_y + 6 && g_mouse_y < tb_y + 34; + if (hover) du_fill_rect(&g_fb, qx, tb_y + 6, qw, 28, 0x30FF4D6Du); + du_rect_outline(&g_fb, qx, tb_y + 6, qw, 28, KS_DANGER, 1); + du_draw_string(&g_fb, quit, qx + (qw - 4 * (int)DU_ASCII_STEP) / 2, ty, + KS_DANGER, 0, DU_ASCII_STEP); + } +} + +/* ---- 5+/CUSTOM ---- */ +static int g_cp_new_rect[4]; /* 自定义页 "+ NEW INSTANCE" 按钮 */ + +static void draw_custom_page(int cp_idx) { + int W = (int)g_fb_w; + int top = KATE_TOPBAR_H; + cpage *cp = &g_cpages[cp_idx]; + + /* ide-tabs 栏(36px):dot + 页名 + "+ NEW INSTANCE" + ide-hint(Kate 结构) */ + du_fill_rect(&g_fb, 0, top, W, 36, 0xFF051828u); + du_divider_h(&g_fb, 0, top + 36, W, KS_BORDER); + int ty = top + (36 - (int)DU_ASCII_LINE_H) / 2 + 1; + + /* .dot + 页名 */ + du_fill_rect(&g_fb, 12, top + 14, 8, 8, KS_ACCENT2); + du_draw_string(&g_fb, cp->name, 28, ty, KS_ACCENT, 0, DU_ASCII_STEP); + int x = 28 + kstrlen(cp->name) * (int)DU_ASCII_STEP + 12; + + /* "+ NEW INSTANCE" 按钮(对应 Kate custom launch_custom_new) */ + { + const char *nb = "+ NEW INSTANCE"; + int bw = kstrlen(nb) * (int)DU_ASCII_STEP + 20; + g_cp_new_rect[0] = x; g_cp_new_rect[1] = top + 7; + g_cp_new_rect[2] = bw; g_cp_new_rect[3] = 22; + int hover = g_mouse_x >= x && g_mouse_x < x + bw && + g_mouse_y >= top + 7 && g_mouse_y < top + 29; + if (hover) du_fill_rect(&g_fb, x, top + 7, bw, 22, KS_ACCENT_DIM); + du_rect_outline(&g_fb, x, top + 7, bw, 22, KS_BORDER, 1); + du_draw_string(&g_fb, nb, x + 10, ty, KS_ACCENT, 0, DU_ASCII_STEP); + x += bw + 8; + } + + /* ide-hint */ + du_draw_string(&g_fb, "DBL-CLICK HOST LAUNCH . EXTERNAL ELF FULLSCREEN", + x + 8, ty + 1, KS_TEXT_DIM, 0, DU_ASCII_STEP); + + /* page-credit */ + du_draw_string(&g_fb, "DEAICUP STUDIO", + W - 12 - 14 * (int)DU_ASCII_STEP, ty + 1, + KS_TEXT_DIM, 0, DU_ASCII_STEP); + + /* ide-host(虚线,margin 8px) */ + int hx = 8, hy = top + 36 + 8; + int hw = W - 16, hh = (int)g_fb_h - hy - 8; + g_cp_host[0] = hx; g_cp_host[1] = hy; + g_cp_host[2] = hw; g_cp_host[3] = hh; + draw_dashed_rect(hx, hy, hw, hh, KS_BORDER); + + /* embed-empty(Kate:正在启动提示) */ + char msg[40]; + kstrcpy(msg, "DOUBLE-CLICK TO LAUNCH ", 40); + kstrcat(msg, cp->name, 40); + draw_centered(msg, hx + hw / 2, hy + hh / 2 - 12, KS_TEXT_DIM, 0); + + char sub[40]; + kstrcpy(sub, "ELF: ", 40); + { + char disp[13]; + f32_name_from_83(cp->elf11, disp); + kstrcat(sub, disp, 40); + } + draw_centered(sub, hx + hw / 2, hy + hh / 2 + 12, KS_ACCENT, 0); +} + +/* ============================================================ + * 任务视图 overlay + * ============================================================ */ + +static void draw_taskview(void) { + int W = (int)g_fb_w; + + /* 背景暗化(全屏 blend,模态 overlay 下文字可读性优先) */ + for (u64 ry = 0; ry < g_fb_h; ry++) { + u32 *line = (u32 *)((u8 *)g_fb.fb + ry * g_fb_pitch); + for (u64 rx = 0; rx < g_fb_w; rx++) { + line[rx] = du_blend(line[rx], KS_BG_PRIMARY, 220); + } + } + + /* tv-header */ + du_draw_string(&g_fb, "TASK VIEW", 60, 44, KS_ACCENT, 0, DU_ASCII_STEP + 4); + du_draw_string(&g_fb, "CLICK TO SWITCH . ESC CLOSE . x CLOSE WS", + 60 + 9 * ((int)DU_ASCII_STEP + 4) + 20, 48, + KS_TEXT_DIM, 0, DU_ASCII_STEP); + du_divider_h(&g_fb, 60, 76, W - 120, KS_BORDER); + + /* tv-grid 卡片 */ + int cw = 260, ch = 170, gap = 20; + int cols = (W - 120 + gap) / (cw + gap); + if (cols < 1) cols = 1; + for (int i = 0; i <= g_ws_count; i++) { + int row = i / cols, col = i % cols; + int x = 60 + col * (cw + gap); + int y = 96 + row * (ch + gap); + g_tv_card[i][0] = x; g_tv_card[i][1] = y; + g_tv_card[i][2] = cw; g_tv_card[i][3] = ch; + + if (i == g_ws_count) { + /* 新建工作区卡片(虚线 +) */ + draw_dashed_rect(x, y, cw, ch, KS_ACCENT2); + du_draw_string(&g_fb, "+", x + cw / 2 - 6, y + ch / 2 - 24, + KS_ACCENT2, 0, DU_ASCII_STEP); + draw_centered("NEW WORKSPACE", x + cw / 2, y + ch / 2 + 8, + KS_ACCENT2, 0); + continue; + } + + int active = (i == g_ws_cur); + int hover = g_mouse_x >= x && g_mouse_x < x + cw && + g_mouse_y >= y && g_mouse_y < y + ch; + du_fill_rect(&g_fb, x, y, cw, ch, KS_BG_SECONDARY); + du_rect_outline(&g_fb, x, y, cw, ch, + active ? KS_ACCENT2 : (hover ? KS_ACCENT : KS_BORDER), 1); + if (active) du_kate_glow_border(&g_fb, x, y, cw, ch, KS_ACCENT2); + + /* tv-card-top */ + du_draw_string(&g_fb, g_ws[i].name, x + 12, y + 10, KS_ACCENT, 0, DU_ASCII_STEP); + du_divider_h(&g_fb, x, y + 34, cw, KS_BORDER); + /* × close */ + g_tv_close[i][0] = x + cw - 26; g_tv_close[i][1] = y + 4; + g_tv_close[i][2] = 22; g_tv_close[i][3] = 26; + du_draw_string(&g_fb, "x", x + cw - 20, y + 8, KS_TEXT_DIM, 0, DU_ASCII_STEP); + + /* tv-stat */ + char nbuf[4]; + int wc = ws_win_count(i); + nbuf[0] = '0' + (char)(wc % 10); nbuf[1] = 0; + draw_centered(nbuf, x + cw / 4, y + 70, KS_ACCENT2, 0); + draw_centered("WINS", x + cw / 4, y + 96, KS_TEXT_DIM, 0); + char pbuf[6]; + kstrcpy(pbuf, "PG", 6); + pbuf[2] = '0' + (char)(g_ws[i].page % 10); pbuf[3] = 0; + draw_centered(pbuf, x + cw * 3 / 4, y + 70, KS_ACCENT2, 0); + draw_centered("PAGE", x + cw * 3 / 4, y + 96, KS_TEXT_DIM, 0); + + /* badge CURRENT */ + if (active) { + int bx = x + cw - 84, by = y + 6; + du_fill_rect(&g_fb, bx, by, 56, 14, KS_ACCENT2); + du_draw_string(&g_fb, "CURRENT", bx + 2, by - 1, + KS_TEXT_INVERT, 0, DU_ASCII_STEP); + } + } +} + +/* ============================================================ + * 右键菜单(移到工作区) + * ============================================================ */ + +/* 构建"移到工作区"列表(排除当前工作区,与 Winux-Kate 一致),返回行数 */ +static int ctx_list_build(int *map) { + int n = 0; + for (int i = 0; i < g_ws_count; i++) { + if (i == g_ws_cur) continue; + map[n++] = i; + } + return n; +} + +static void draw_ctx_menu(void) { + if (!g_ctx_open) return; + int lmap[MAX_WS]; + int ln = ctx_list_build(lmap); + int rows = (ln ? ln : 1) + 1; /* 列表行(或空态行) + 新建行 */ + int mw = 200; + int mh = 24 + rows * 22 + 8; + int x = g_ctx_x, y = g_ctx_y; + if (x + mw > (int)g_fb_w) x = (int)g_fb_w - mw - 4; + if (y + mh > (int)g_fb_h) y = (int)g_fb_h - mh - 4; + g_ctx_rect[0] = x; g_ctx_rect[1] = y; + g_ctx_rect[2] = mw; g_ctx_rect[3] = mh; + + du_fill_rect(&g_fb, x, y, mw, mh, 0xFF050A18u); + du_rect_outline(&g_fb, x, y, mw, mh, KS_ACCENT, 1); + du_kate_glow_border(&g_fb, x, y, mw, mh, KS_ACCENT); + + du_draw_string(&g_fb, "MOVE TO WORKSPACE", x + 12, y + 5, + KS_ACCENT, 0, DU_ASCII_STEP); + du_divider_h(&g_fb, x, y + 22, mw, KS_BORDER); + + int iy = y + 26; + int row = 0; + if (ln == 0) { + /* 空态(不可点击) */ + du_draw_string(&g_fb, "NO OTHER WORKSPACE", x + 12, iy + 2, + KS_TEXT_DIM, 0, DU_ASCII_STEP); + g_ctx_item[0][2] = 0; /* 禁用命中 */ + g_ctx_item_ws[0] = -2; + iy += 22; + row = 1; + } + for (int r = 0; r < ln; r++) { + int wi = lmap[r]; + int hover = g_mouse_x >= x && g_mouse_x < x + mw && + g_mouse_y >= iy && g_mouse_y < iy + 22; + if (hover) du_fill_rect(&g_fb, x + 1, iy, mw - 2, 22, KS_ACCENT_DIM); + du_draw_string(&g_fb, g_ws[wi].name, x + 12, iy + 2, + hover ? KS_ACCENT : KS_TEXT_PRIMARY, 0, DU_ASCII_STEP); + char cn[4]; + cn[0] = '0' + (char)(ws_win_count(wi) % 10); cn[1] = 0; + du_draw_string(&g_fb, cn, x + mw - 24, iy + 2, KS_TEXT_DIM, 0, DU_ASCII_STEP); + g_ctx_item[row][0] = x; g_ctx_item[row][1] = iy; + g_ctx_item[row][2] = mw; g_ctx_item[row][3] = 22; + g_ctx_item_ws[row] = wi; + iy += 22; + row++; + } + /* + NEW WORKSPACE */ + du_divider_h(&g_fb, x, iy, mw, KS_BORDER); + int hover = g_mouse_x >= x && g_mouse_x < x + mw && + g_mouse_y >= iy && g_mouse_y < iy + 22; + if (hover) du_fill_rect(&g_fb, x + 1, iy, mw - 2, 22, KS_ACCENT_DIM); + du_draw_string(&g_fb, "+ NEW WORKSPACE", x + 12, iy + 2, + KS_ACCENT2, 0, DU_ASCII_STEP); + g_ctx_item[row][0] = x; g_ctx_item[row][1] = iy; + g_ctx_item[row][2] = mw; g_ctx_item[row][3] = 22; + g_ctx_item_ws[row] = -1; + /* 清除残余行命中 */ + for (int r = row + 1; r <= MAX_WS; r++) g_ctx_item[r][2] = 0; +} + +/* ============================================================ + * 全屏重绘 + * ============================================================ */ + +/* 全局亮度(BRIGHT 滑块真实调暗) */ +static void apply_brightness(void) { + if (g_bright >= 100) return; + u32 f = (u32)g_bright; + for (u64 y = 0; y < g_fb_h; y++) { + u32 *line = (u32 *)((u8 *)g_fb.fb + y * g_fb_pitch); + for (u64 x = 0; x < g_fb_w; x++) { + u32 c = line[x]; + u32 b = ((c >> 16) & 0xFF) * f / 100; + u32 g = ((c >> 8) & 0xFF) * f / 100; + u32 r = (c & 0xFF) * f / 100; + line[x] = 0xFF000000u | (b << 16) | (g << 8) | r; + } + } +} + +static void redraw_all(void) { + /* 深空黑渐变背景 */ + du_fill_bg_gradient(&g_fb, KS_BG_PRIMARY, KS_BG_TERTIARY); + + /* 顶栏 */ + draw_topbar(); + + /* 页面 */ + if (g_page == 1) draw_dashboard(); + else if (g_page == 2) draw_ide(); + else if (g_page == 3) draw_im(); + else if (g_page == 4) draw_desktop_page(); + else { + int idx = -1; + for (int i = 0; i < g_cpage_count; i++) + if (g_cpages[i].id == g_page) { idx = i; break; } + if (idx >= 0) draw_custom_page(idx); + else g_page = 1; + } + + /* 任务视图 overlay */ + if (g_taskview) draw_taskview(); + + /* 右键菜单(最顶) */ + if (g_ctx_open) draw_ctx_menu(); + + /* 亮度调整 */ + apply_brightness(); /* 鼠标光标 */ cursor_save_bg(g_mouse_x, g_mouse_y); @@ -1385,7 +2635,439 @@ static void redraw_all(void) { } /* ============================================================ - * 主循环 + * 工作区 / 页面管理 + * ============================================================ */ + +static void ws_switch(int id) { + if (id < 0 || id >= g_ws_count || id == g_ws_cur) { g_taskview = 0; return; } + g_ws[g_ws_cur].page = g_page; /* 记住离开时的页面 */ + g_ws_cur = id; + g_page = g_ws[id].page; + g_taskview = 0; +} + +static void ws_create(void) { + if (g_ws_count >= MAX_WS) return; + char *nm = g_ws[g_ws_count].name; + nm[0] = 'W'; nm[1] = 'S'; nm[2] = '-'; + nm[3] = '0' + (char)(g_ws_count + 1); nm[4] = 0; + g_ws[g_ws_count].page = 1; + g_ws_count++; +} + +/* 关闭工作区:窗口释放到关闭后的当前工作区(Kate release 语义) */ +static void ws_close(int id) { + if (g_ws_count <= 1 || id < 0 || id >= g_ws_count) return; + int cur_after = g_ws_cur; + if (g_ws_cur == id) cur_after = (id > 0) ? id - 1 : 0; + else if (g_ws_cur > id) cur_after = g_ws_cur - 1; + for (int i = 0; i < g_win_count; i++) { + if (g_windows[i].ws == id) g_windows[i].ws = cur_after; + else if (g_windows[i].ws > id) g_windows[i].ws--; + } + for (int i = id; i < g_ws_count - 1; i++) g_ws[i] = g_ws[i + 1]; + g_ws_count--; + g_ws_cur = cur_after; + g_taskview = 0; +} + +static void page_switch(int p) { + if (p < 1 || p > 4 + g_cpage_count) return; + g_page = p; +} + +/* 自定义页:为尚未挂载的外部 .elf 应用新建页面 */ +static void cpage_add(void) { + if (g_cpage_count >= MAX_CPAGES) return; + for (int a = 0; a < g_app_count; a++) { + if (!g_apps[a].elf_name) continue; + int dup = 0; + for (int i = 0; i < g_cpage_count; i++) + if (f32_neq11(g_cpages[i].elf11, g_apps[a].elf_name)) { dup = 1; break; } + if (dup) continue; + cpage *cp = &g_cpages[g_cpage_count]; + int n = kstrlen(g_apps[a].display_name); + if (n > 11) n = 11; + for (int i = 0; i < n; i++) cp->name[i] = g_apps[a].display_name[i]; + cp->name[n] = 0; + for (int i = 0; i < 11; i++) cp->elf11[i] = g_apps[a].elf_name[i]; + cp->id = 5 + g_cpage_count; + g_cpage_count++; + g_page = cp->id; + return; + } +} + +static void cpage_remove(int idx) { + if (idx < 0 || idx >= g_cpage_count) return; + int removed_page = g_cpages[idx].id; + for (int i = idx; i < g_cpage_count - 1; i++) g_cpages[i] = g_cpages[i + 1]; + g_cpage_count--; + for (int i = 0; i < g_cpage_count; i++) g_cpages[i].id = 5 + i; + if (g_page == removed_page) g_page = 1; + else if (g_page > removed_page) g_page--; +} + +/* ============================================================ + * 事件处理 + * ============================================================ */ + +static int rect_hit(const int r[4], int mx, int my) { + return r[2] > 0 && mx >= r[0] && mx < r[0] + r[2] && + my >= r[1] && my < r[1] + r[3]; +} + +static void slider_set(const int rect[4], int *val, int mx) { + int v = (mx - rect[0]) * 100 / rect[2]; + if (v < 0) v = 0; + if (v > 100) v = 100; + *val = v; +} + +/* 双击判定(TSC 半秒 + 8px 容差),不更新状态 */ +static int dbl_check(int mx, int my) { + if (!g_tsc_per_sec) return 0; + u64 now = rdtsc(); + int dx = mx - g_last_click_x, dy = my - g_last_click_y; + return (now - g_last_click_tsc) < g_tsc_per_sec / 2 && + dx > -8 && dx < 8 && dy > -8 && dy < 8; +} + +/* ---- 1·DASHBOARD 点击 ---- */ +static int dash_click(int mx, int my, int dbl) { + editor_state *ed = (editor_state *)ADDR_EDDASH; + + /* SAVE 按钮 */ + if (ed->has_file && rect_hit(g_save_rect, mx, my)) { editor_save(); return 1; } + + /* 面板聚焦 */ + for (int i = 0; i < 4; i++) { + if (!rect_hit(g_dash_rect[i], mx, my)) continue; + g_dash_focus = i; + if (i == 3) { + /* FILES 行命中 / 双击打开 */ + int fx = g_dash_rect[3][0] + 2; + int fy = g_dash_rect[3][1] + KATE_PANEL_HEADER_H + 2; + int fw = g_dash_rect[3][2] - 4; + int fh = g_dash_rect[3][3] - KATE_PANEL_HEADER_H - 4; + if (mx >= fx && mx < fx + fw && my >= fy + 24) { + int max_rows = (fh - 24) / 22; + int row = (my - fy - 24) / 22; + if (row >= 0 && row < max_rows && row < g_file_count) { + if (dbl && row == g_file_sel) files_open(row); + else g_file_sel = row; + } + } + } + return 1; + } + + /* 状态栏滑块 */ + if (rect_hit(g_vol_rect, mx, my)) { + g_drag_slider = 1; + slider_set(g_vol_rect, &g_vol, mx); + return 1; + } + if (rect_hit(g_bri_rect, mx, my)) { + g_drag_slider = 2; + slider_set(g_bri_rect, &g_bright, mx); + return 1; + } + return 0; +} + +/* ---- 2·IDE 点击 ---- */ +static int ide_click(int mx, int my, int dbl) { + if (g_ide_attached && rect_hit(g_ide_tabx, mx, my)) { g_ide_attached = 0; return 1; } + if (!g_ide_attached && rect_hit(g_ide_new_rect, mx, my)) { g_ide_attached = 1; return 1; } + if (!g_ide_attached && dbl && rect_hit(g_ide_host, mx, my)) { + g_ide_attached = 1; + return 1; + } + return 0; +} + +/* ---- 4·DESKTOP 点击 ---- */ +static int desktop_click(int mx, int my, int dbl) { + int tb_y = (int)g_fb_h - KATE_TASKBAR_H; + + /* 任务栏 */ + if (my >= tb_y) { + if (rect_hit(g_tb_quit_rect, mx, my)) { g_quit = 1; return 1; } + if (rect_hit(g_tb_adopt_rect, mx, my)) { + /* 收纳窗口:全部工作区窗口收归当前工作区并还原最小化 */ + for (int i = 0; i < g_win_count; i++) { + g_windows[i].ws = g_ws_cur; + g_windows[i].minimized = 0; + } + return 1; + } + for (int i = 0; i < g_win_count; i++) { + if (!g_tb_rect[i][2] || !rect_hit(g_tb_rect[i], mx, my)) continue; + if (rect_hit(g_tb_close_rect[i], mx, my)) { + win_destroy(&g_windows[i]); + } else { + win_focus(&g_windows[i]); + } + return 1; + } + return 0; + } + + /* 窗口 */ + desktop_window *w = win_find_at(mx, my); + if (w) { + int wid = w->id; + win_focus(w); + w = win_find(wid); /* focus 重排后重新定位 */ + if (!w) return 1; + if (win_hit_close_btn(w, mx, my)) { win_destroy(w); return 1; } + if (win_hit_titlebar(w, mx, my)) { + g_dragging = 1; + g_drag_win = w->id; + g_drag_off_x = mx - w->x; + g_drag_off_y = my - w->y; + return 1; + } + /* 客户区事件 → 应用 */ + if (w->app_id >= 0 && w->app_id < g_app_count && + g_apps[w->app_id].on_event && w->app_state) { + desktop_event dev; + dev.type = EV_MOUSE_DOWN; + dev.mx = mx; dev.my = my; + dev.button = 0; dev.scancode = 0; + dev.shift = g_shift; dev.ctrl = g_ctrl; dev.alt = g_alt; + app_ctx actx; + fill_app_ctx(&actx, w); + g_apps[w->app_id].on_event(w->app_state, &actx, &dev); + } + return 1; + } + + /* 图标(单击选中 / 双击启动) */ + for (int i = 0; i < g_icon_count; i++) { + int ix = g_icons[i].x, iy = g_icons[i].y; + if (mx >= ix && mx < ix + ICON_W && my >= iy && my < iy + ICON_H) { + if (dbl && g_icons[i].selected) { + launch_app(g_icons[i].app_id); + } else { + for (int j = 0; j < g_icon_count; j++) g_icons[j].selected = 0; + g_icons[i].selected = 1; + } + return 1; + } + } + + /* 空白:取消选择 */ + for (int j = 0; j < g_icon_count; j++) g_icons[j].selected = 0; + return 1; +} + +/* ---- 左键按下 ---- */ +static int on_left_press(void) { + int mx = g_mouse_x, my = g_mouse_y; + int dbl = dbl_check(mx, my); + g_last_click_tsc = rdtsc(); + g_last_click_x = mx; g_last_click_y = my; + + /* 任务视图 overlay(模态) */ + if (g_taskview) { + for (int i = 0; i <= g_ws_count; i++) { + if (!rect_hit(g_tv_card[i], mx, my)) continue; + if (i == g_ws_count) ws_create(); + else if (rect_hit(g_tv_close[i], mx, my)) ws_close(i); + else ws_switch(i); + return 1; + } + g_taskview = 0; /* 背景点击关闭 */ + return 1; + } + + /* 右键菜单(模态) */ + if (g_ctx_open) { + if (rect_hit(g_ctx_rect, mx, my)) { + for (int i = 0; i <= MAX_WS; i++) { + if (!rect_hit(g_ctx_item[i], mx, my)) continue; + int wi = g_ctx_item_ws[i]; + if (wi == -1) { + /* + NEW WORKSPACE:新建并迁移 */ + ws_create(); + desktop_window *w = win_find(g_ctx_win); + if (w && g_ws_count > 0) w->ws = g_ws_count - 1; + } else if (wi >= 0) { + desktop_window *w = win_find(g_ctx_win); + if (w) w->ws = wi; + } + break; + } + } + g_ctx_open = 0; + return 1; + } + + /* 顶栏 */ + if (my < KATE_TOPBAR_H) { + if (rect_hit(g_tvbtn_rect, mx, my)) { g_taskview = 1; return 1; } + for (int i = 0; i < g_ws_count; i++) + if (rect_hit(g_ws_rect[i], mx, my)) { ws_switch(i); return 1; } + if (rect_hit(g_wsadd_rect, mx, my)) { ws_create(); return 1; } + for (int i = 0; i < 4 + g_cpage_count; i++) { + if (i >= 4 && rect_hit(g_pg_rm_rect[i], mx, my)) { + cpage_remove(i - 4); + return 1; + } + if (rect_hit(g_pg_rect[i], mx, my)) { page_switch(i + 1); return 1; } + } + if (rect_hit(g_pgadd_rect, mx, my)) { cpage_add(); return 1; } + return 0; + } + + /* 页面内容 */ + if (g_page == 1) return dash_click(mx, my, dbl); + if (g_page == 2) return ide_click(mx, my, dbl); + if (g_page == 3) { + /* IM 切换视图按钮(对应 Kate im_toggle) */ + if (rect_hit(g_im_toggle_rect, mx, my)) { g_im_view ^= 1; return 1; } + return 0; + } + if (g_page == 4) return desktop_click(mx, my, dbl); + if (g_page >= 5) { + int launch = 0; + if (rect_hit(g_cp_new_rect, mx, my)) launch = 1; + if (dbl && rect_hit(g_cp_host, mx, my)) launch = 1; + if (launch) { + for (int i = 0; i < g_cpage_count; i++) { + if (g_cpages[i].id != g_page) continue; + launch_external_elf(g_cpages[i].elf11); + break; + } + return 1; + } + return 0; + } + return 0; +} + +/* ---- 右键按下:窗口/任务栏项 → 移到工作区菜单 ---- */ +static int on_right_press(void) { + int mx = g_mouse_x, my = g_mouse_y; + + if (g_taskview) { g_taskview = 0; return 1; } + if (g_ctx_open) { g_ctx_open = 0; return 1; } + if (g_page != 4) return 0; + + int tb_y = (int)g_fb_h - KATE_TASKBAR_H; + if (my >= tb_y) { + for (int i = 0; i < g_win_count; i++) { + if (!g_tb_rect[i][2] || !rect_hit(g_tb_rect[i], mx, my)) continue; + int lmap[MAX_WS]; + int ln = ctx_list_build(lmap); + int mh = 24 + ((ln ? ln : 1) + 1) * 22 + 8; + g_ctx_win = g_windows[i].id; + g_ctx_x = mx; + g_ctx_y = my - mh; /* 菜单向上展开 */ + if (g_ctx_y < KATE_TOPBAR_H) g_ctx_y = my; + g_ctx_open = 1; + return 1; + } + return 0; + } + + desktop_window *w = win_find_at(mx, my); + if (w) { + g_ctx_win = w->id; + g_ctx_x = mx; g_ctx_y = my; + g_ctx_open = 1; + return 1; + } + return 0; +} + +/* ---- 键盘 ---- */ +static int handle_key(u8 sc) { + if (sc == 0xE0) { g_e0 = 1; return 0; } + if (sc == 0x2A || sc == 0x36) { g_shift = 1; return 0; } + if (sc == 0xAA || sc == 0xB6) { g_shift = 0; return 0; } + if (sc == 0x1D) { g_ctrl = 1; g_e0 = 0; return 0; } + if (sc == 0x9D) { g_ctrl = 0; return 0; } + if (sc == 0x38) { g_alt = 1; g_e0 = 0; return 0; } + if (sc == 0xB8) { g_alt = 0; return 0; } + if (sc & 0x80) { g_e0 = 0; return 0; } /* 其他释放码忽略 */ + + /* Esc:关闭 overlay */ + if (sc == 0x01) { + if (g_taskview) { g_taskview = 0; return 1; } + if (g_ctx_open) { g_ctx_open = 0; return 1; } + return 0; + } + if (g_taskview || g_ctx_open) return 0; /* 模态期间吞掉按键 */ + + /* Ctrl+Tab 切页 / Ctrl+Shift+Tab 上下文菜单(IM 页为切换视图,对应 Kate imView) */ + if (g_ctrl && sc == 0x0F) { + if (g_shift) { + if (g_page == 3) { g_im_view ^= 1; return 1; } + /* Ctrl+Shift+Tab:对当前聚焦窗口打开"移到工作区"菜单 */ + if (g_page == 4 && win_find(g_focused_win)) { + g_ctx_win = g_focused_win; + g_ctx_x = g_mouse_x; g_ctx_y = g_mouse_y; + g_ctx_open = 1; + return 1; + } + return 0; + } + int maxp = 4 + g_cpage_count; + int p = g_page + 1; + if (p > maxp) p = 1; + page_switch(p); + return 1; + } + if (g_ctrl && sc == 0x1F) { editor_save(); return 1; } + + /* 页面路由 */ + if (g_page == 1) { + if (g_dash_focus <= 1) { + bash_key((bash_state *)(g_dash_focus == 0 ? ADDR_TERM1 : ADDR_TERM2), + sc, g_shift); + return 1; + } + if (g_dash_focus == 2) { + editor_key((editor_state *)ADDR_EDDASH, sc, g_shift); + return 1; + } + /* FILES 面板导航 */ + if (sc == 0x48) { if (g_file_sel > 0) g_file_sel--; return 1; } + if (sc == 0x50) { if (g_file_sel < g_file_count - 1) g_file_sel++; return 1; } + if (sc == 0x1C) { files_open(g_file_sel); return 1; } + return 0; + } + if (g_page == 2) { + if (g_ide_attached) { + editor_key((editor_state *)ADDR_EDDASH, sc, g_shift); + return 1; + } + return 0; + } + if (g_page == 4) { + desktop_window *fw = win_find(g_focused_win); + if (fw && fw->ws == g_ws_cur && fw->app_id >= 0 && fw->app_id < g_app_count && + g_apps[fw->app_id].on_event && fw->app_state) { + desktop_event dev; + dev.type = EV_KEY_DOWN; + dev.mx = g_mouse_x; dev.my = g_mouse_y; + dev.button = 0; + dev.scancode = sc; + dev.shift = g_shift; dev.ctrl = g_ctrl; dev.alt = g_alt; + app_ctx actx; + fill_app_ctx(&actx, fw); + g_apps[fw->app_id].on_event(fw->app_state, &actx, &dev); + return 1; + } + } + return 0; +} + +/* ============================================================ + * 主入口 * ============================================================ */ __attribute__((visibility("default"))) @@ -1395,167 +3077,134 @@ void dsk_entry(const dsk_boot_context *ctx) { if (!ctx || ctx->magic != 0x44534B31424F4F54ULL) { slog("bad context"); - for(;;) __asm__("hlt"); + for (;;) __asm__("hlt"); } + g_boot_ctx = ctx; g_fb_addr = ctx->framebuffer_address; g_fb_w = ctx->framebuffer_width; g_fb_h = ctx->framebuffer_height; g_fb_pitch = ctx->framebuffer_pitch; g_kernel_api = (const void *)ctx->dkm_kernel_api; - g_block_read = (desktop_block_read_fn)(*(u64 *)(ctx->dkm_kernel_api + 0xA8)); + + /* block API:kernel_api + 0xA8 → block_api 结构,read @ +0x10, write @ +0x18 */ + { + u64 blk = *(u64 *)(ctx->dkm_kernel_api + 0xA8); + g_block_read = blk ? (desktop_block_read_fn)*(u64 *)(blk + 16) : 0; + g_block_write = blk ? (desktop_block_write_fn)*(u64 *)(blk + 24) : 0; + } du_context_init(&g_fb, g_fb_addr, g_fb_w, g_fb_h, g_fb_pitch); + f32_init(g_block_read, g_block_write); + + tsc_calibrate(); + g_rtc_boot_sec = rtc_day_sec(); + g_last_sec = g_rtc_boot_sec; - /* 初始化 PS/2 鼠标 */ ps2_mouse_init(); - /* 注册应用 */ register_apps(); setup_desktop_icons(); - /* 初始绘制 */ + /* 工作区 0 */ + kstrcpy(g_ws[0].name, "WS-1", 16); + g_ws[0].page = 1; + g_ws_count = 1; + g_ws_cur = 0; + + /* DASHBOARD 面板共享状态 */ + { + bash_state *t1 = (bash_state *)ADDR_TERM1; + bash_reset(t1, 640, 280); + bash_puts(t1, "Deshab Bash v0.1 - TERM-01\nType help for commands\n\n", KS_TEXT_PRIMARY); + + bash_state *t2 = (bash_state *)ADDR_TERM2; + bash_reset(t2, 640, 280); + bash_puts(t2, "Deshab Bash v0.1 - TERM-02\nType help for commands\n\n", KS_TEXT_PRIMARY); + + editor_reset((editor_state *)ADDR_EDDASH, + "Welcome to Deshab Editor\n" + "\n" + "- Double-click a file in FILES panel to open it here\n" + "- SAVE button / Ctrl+S writes back to FAT32 disk\n" + "- IDE page hosts this same editor full-screen\n"); + } + + files_refresh(); + + boot_screen(); + g_booted = 1; + redraw_all(); slog("desktop ready"); /* 主事件循环 */ - for (;;) { + while (!g_quit) { int need_redraw = 0; - g_mouse_has_pkt = 0; + int moved = 0; - /* 轮询 PS/2 鼠标 */ - if (ps2_mouse_poll()) { - /* 拖拽处理 - 需要全屏重绘 */ - if (g_dragging && g_drag_win >= 0) { - desktop_window *w = win_find(g_drag_win); - if (w) { - w->x = g_mouse_x - g_drag_off_x; - w->y = g_mouse_y - g_drag_off_y; - } + /* 鼠标:排空所有数据包 */ + while (ps2_mouse_poll()) moved = 1; + + if (g_left_pressed) { + g_left_pressed = 0; + if (on_left_press()) need_redraw = 1; + } + if (g_right_pressed) { + g_right_pressed = 0; + if (on_right_press()) need_redraw = 1; + } + if (g_left_released) { + g_left_released = 0; + if (g_dragging || g_drag_slider) { + g_dragging = 0; + g_drag_win = -1; + g_drag_slider = 0; need_redraw = 1; } - - /* 鼠标点击事件 - 需要全屏重绘 */ - if (g_mouse_btn) { - /* 检查任务栏点击 */ - int tb_y = (int)g_fb_h - TASKBAR_H; - if (g_mouse_y >= tb_y) { - /* 快速启动图标 */ - int qx = 64; - for (int i = 0; i < g_app_count && i < 4; i++) { - int icon_x = qx + i * 40; - if (g_mouse_x >= icon_x && g_mouse_x < icon_x + 40) { - launch_app(i); - need_redraw = 1; - break; - } - } - } else { - /* 窗口命中测试 */ - desktop_window *hit = win_find_at(g_mouse_x, g_mouse_y); - if (hit) { - win_bring_to_front(hit); - /* 取消其他焦点 */ - for (int i = 0; i < g_win_count; i++) - g_windows[i].focused = (g_windows[i].id == hit->id); - g_focused_win = hit->id; - - /* 检查关闭按钮 */ - if (win_hit_close_btn(hit, g_mouse_x, g_mouse_y)) { - win_destroy(hit); - need_redraw = 1; - } - /* 检查标题栏拖拽 */ - else if (win_hit_titlebar(hit, g_mouse_x, g_mouse_y)) { - g_dragging = 1; - g_drag_win = hit->id; - g_drag_off_x = g_mouse_x - hit->x; - g_drag_off_y = g_mouse_y - hit->y; - } - /* 传递鼠标事件给应用 */ - else if (hit->app_id >= 0 && g_apps[hit->app_id].on_event && hit->app_state) { - desktop_event dev; - dev.type = EV_MOUSE_DOWN; - dev.mx = g_mouse_x; dev.my = g_mouse_y; - dev.button = 0; - app_ctx actx; - actx.fb = &g_fb; actx.win = hit; - actx.client_x = hit->x + BORDER_W; - actx.client_y = hit->y + TITLEBAR_H; - actx.client_w = hit->w - 2*BORDER_W; - actx.client_h = hit->h - TITLEBAR_H - BORDER_W; - actx.should_exit = 0; - actx.block_read = g_block_read; - actx.kernel_api = g_kernel_api; - g_apps[hit->app_id].on_event(hit->app_state, &actx, &dev); - } - } else { - /* 桌面图标点击 */ - for (int i = 0; i < g_icon_count; i++) { - int ix = g_icons[i].x, iy = g_icons[i].y; - if (g_mouse_x >= ix && g_mouse_x < ix + 32 && - g_mouse_y >= iy && g_mouse_y < iy + 32) { - launch_app(g_icons[i].app_id); - need_redraw = 1; - break; - } - } - } - } - } else { - if (g_dragging) { - g_dragging = 0; - g_drag_win = -1; - } - } } - /* 轮询 PS/2 键盘 */ - { + /* 窗口拖拽 */ + if (moved && g_dragging && g_drag_win >= 0) { + desktop_window *w = win_find(g_drag_win); + if (w) { + w->x = g_mouse_x - g_drag_off_x; + w->y = g_mouse_y - g_drag_off_y; + if (w->y < KATE_TOPBAR_H) w->y = KATE_TOPBAR_H; + if (w->x < 0 - w->w + 40) w->x = 0 - w->w + 40; + if (w->x > (int)g_fb_w - 40) w->x = (int)g_fb_w - 40; + if (w->y > (int)g_fb_h - KATE_TASKBAR_H - 20) + w->y = (int)g_fb_h - KATE_TASKBAR_H - 20; + } + need_redraw = 1; + } + + /* 滑块拖拽 */ + if (moved && g_drag_slider == 1) { slider_set(g_vol_rect, &g_vol, g_mouse_x); need_redraw = 1; } + if (moved && g_drag_slider == 2) { slider_set(g_bri_rect, &g_bright, g_mouse_x); need_redraw = 1; } + + /* 键盘:排空 */ + for (;;) { u8 st = inb(0x64); - if ((st & 1) && !(st & 0x20)) { - u8 data = inb(0x60); - u8 sc = data; - if (sc == 0xE0) { g_e0 = 1; goto next; } - if (sc == 0x2A || sc == 0x36) { g_shift = 1; goto next; } - if (sc == 0xAA || sc == 0xB6) { g_shift = 0; goto next; } - if (sc & 0x80) { g_e0 = 0; goto next; } + if (!(st & 1) || (st & 0x20)) break; + u8 sc = inb(0x60); + if (handle_key(sc)) need_redraw = 1; + } - /* 发送键盘事件到焦点窗口 */ - if (g_focused_win >= 0) { - desktop_window *fw = win_find(g_focused_win); - if (fw && fw->app_id >= 0 && g_apps[fw->app_id].on_event && fw->app_state) { - desktop_event dev; - dev.type = EV_KEY_DOWN; - dev.mx = g_mouse_x; dev.my = g_mouse_y; - dev.button = 0; - dev.scancode = sc; - dev.shift = g_shift; - dev.ctrl = g_ctrl; - dev.alt = g_alt; - app_ctx actx; - actx.fb = &g_fb; actx.win = fw; - actx.client_x = fw->x + BORDER_W; - actx.client_y = fw->y + TITLEBAR_H; - actx.client_w = fw->w - 2*BORDER_W; - actx.client_h = fw->h - TITLEBAR_H - BORDER_W; - actx.should_exit = 0; - actx.block_read = g_block_read; - actx.kernel_api = g_kernel_api; - g_apps[fw->app_id].on_event(fw->app_state, &actx, &dev); - need_redraw = 1; - } - } - g_e0 = 0; + /* RTC 秒 tick:DASHBOARD 状态栏时钟/UP 计时刷新 */ + { + int sec = rtc_day_sec(); + if (sec != g_last_sec) { + g_last_sec = sec; + if (g_page == 1 && !g_taskview) need_redraw = 1; } } -next: if (need_redraw) { cursor_restore_bg(); redraw_all(); - } else if (g_mouse_has_pkt) { - /* 鼠标移动但无需全屏重绘:只更新光标位置 */ + } else if (moved) { + /* 仅光标移动:局部更新 */ cursor_restore_bg(); cursor_save_bg(g_mouse_x, g_mouse_y); cursor_draw(g_mouse_x, g_mouse_y); @@ -1563,4 +3212,7 @@ next: __asm__("pause"); } + + slog("exit"); + /* 返回到 DSK */ } diff --git a/CODE/dsk/linker.ld b/CODE/dsk/linker.ld index 473e5fc..ffdf62d 100644 --- a/CODE/dsk/linker.ld +++ b/CODE/dsk/linker.ld @@ -17,8 +17,10 @@ SECTIONS *(.data*) *(.data.rel.ro*) *(COMMON) - *(.bss*) } :data .rela.dyn : ALIGN(8) { *(.rela.dyn*) } :data .dynamic : ALIGN(8) { *(.dynamic*) } :data :dynamic + .bss : ALIGN(4K) { + *(.bss*) + } :data } diff --git a/CODE/dsk/main.c b/CODE/dsk/main.c index 7c6b497..c8aa172 100644 --- a/CODE/dsk/main.c +++ b/CODE/dsk/main.c @@ -66,7 +66,7 @@ static block_write_fn g_block_write; static u64 g_tsc_per_ms = 0; static u8 g_disk[131072]; static u8 g_cluster[4096]; -static u8 g_fdata[262144]; +static u8 g_fdata[1048576]; static int fat32_read_sectors(u32 lba, u32 count, u8 *out) { return g_block_read ? g_block_read(0,lba,count,out) : -1; } static int fat32_write_sectors(u32 lba, u32 count, const u8 *buf) { return g_block_write ? g_block_write(0,lba,count,buf) : -1; } @@ -529,7 +529,7 @@ static int dsk_load_elf(u8 *data, u32 size, void **entry_out) { if (!lc || min_vaddr == ~0ULL) return -4; u64 isize = max_vaddr - min_vaddr; isize = (isize + 0xFFF) & ~0xFFFULL; - /* 加载缓冲:需容纳 shell.elf(~536KB,含 FAT32 读写缓冲 BSS)。1MB 留余量。 */ + /* 加载缓冲:需容纳 shell.elf(~862KB memsz,含 FAT32 读写缓冲 BSS)。1MB 留余量。 */ static u8 ibuf[1048576]; u8 *image = ibuf; if (isize > 1048576) return -5; /* 镜像过大,防止越界 */ diff --git a/CODE/dsk/main.o b/CODE/dsk/main.o index 622fb3e40f0bd7c70f9d77518fb1a8c6dfc3757d..267f9d92dbdceae6ca01b3089b6607fad533ce91 100644 GIT binary patch delta 175 zcmaDci}A%Q#tqL!83i`K5Z$E6C@^`pxjLip=ELUmxeSFFK)_uBOffJ4DSsfAv4e=G tOJaymp6ulA36cFQg&_+T;FrcOZiXp7S=m`1=#*gR?Hr8Co4I{r838X97>WP@ delta 175 zcmaDci}A%Q#tqL!8Cf>J5Z$E6$TE4hxjG~J=ELUmxeVDEK)_uBOffJ4DSsfAv4e=G tOJaymp6ulA36cFQg&_+T;FrcOZiXp7S=m`1=#*gR?Hr8ao4I{r83Fu?7y1AI diff --git a/CODE/linux/README.md b/CODE/linux/README.md new file mode 100644 index 0000000..d08eb49 --- /dev/null +++ b/CODE/linux/README.md @@ -0,0 +1,114 @@ +# Linux 6.6 LTS for UTSM Dual-Kernel Architecture + +## Overview + +This directory contains the Linux 6.6 LTS configuration and build scripts for the +UTSM + Linux dual-kernel architecture. Linux runs as a VMX non-root guest under +the UTSM hypervisor (VMX root). + +## Prerequisites + +- **WSL** (Windows Subsystem for Linux) with Ubuntu 22.04+ installed + ```powersShell + wsl --install -d Ubuntu-22.04 + ``` +- Build dependencies (inside WSL): + ```bash + sudo apt update + sudo apt install -y build-essential bc flex bison libelf-dev libssl-dev cpio gzip git + ``` +- ~3GB free disk space + +## Building Linux + +### 1. From WSL + +```bash +# Navigate to the project (adjust path for your WSL mount) +cd /mnt/d/Code/Deshab/CODE/linux + +# Make the build script executable +chmod +x build.sh + +# Run the build (clones Linux 6.6 LTS, configures, builds) +./build.sh +``` + +The build will: +1. Clone Linux 6.6 LTS source to `CODE/linux/src/` +2. Apply UTSM patches from `CODE/linux/patches/` (if any) +3. Configure with `configs/utsm_x86_64_defconfig` +4. Build `bzImage` +5. Copy to `SYSTEM/boot/linux-bzImage` +6. Build initramfs if `CODE/linux/initramfs/` exists + +### 2. Enable in Limine + +After building, uncomment the Linux module entries in `SYSTEM/limine/limine.conf`: + +``` + module_path: boot():/boot/linux-bzImage + module_cmdline: linux:bzImage + module_path: boot():/boot/linux-initrd.img + module_cmdline: linux:initrd +``` + +### 3. Rebuild the disk image + +```PowerShell +cd d:\Code\Deshab +.\build.bat +``` + +### 4. Test with QEMU + +**Note:** VMX requires hardware virtualization support. QEMU's TCG (software +emulation) does NOT support VMX instructions. To test the VMX path: + +- **Real hardware**: Boot on an Intel machine with VT-x enabled in BIOS +- **QEMU with WHPX**: `qemu-system-x86_64 -accel whpx -cpu max` (may not expose + nested VMX to the guest) +- **QEMU with KVM** (Linux host only): `qemu-system-x86_64 -enable-kvm -cpu host` + +Without VMX, UTSM gracefully falls back to the normal DSK boot flow. + +## Configuration Details + +The `utsm_x86_64_defconfig` enables: +- 64-bit kernel with SMP support +- Serial console (8250/16550 at 0x3F8, 115200 baud) +- virtio: console, block, net, fs (for UTSM host backends) +- ext4, FAT, tmpfs, proc, sysfs filesystems +- No module loading (all drivers built-in for Phase 1) +- No EFI stub (UTSM uses custom VMX boot protocol) +- Debug info and printk timestamps + +## File Structure + +``` +CODE/linux/ +├── configs/ +│ └── utsm_x86_64_defconfig # Kernel configuration +├── patches/ # UTSM-specific kernel patches +│ ├── utsm-hcall.patch # /dev/utsm hypercall driver (Phase 1.3) +│ ├── utsm-ipc.patch # Shared memory IPC driver (Phase 1.3) +│ └── utsm-virtio.patch # virtio enhancements (Phase 1.4+) +├── initramfs/ # Minimal initramfs contents (optional) +├── build.sh # Build script (run in WSL/Linux) +└── README.md # This file +``` + +## Boot Flow + +``` +Limine (UEFI) + → utsm.elf (VMX root) + → vmm_init() + vmm_self_test() + → linux_loader_init() (parse bzImage, set up boot_params, EPT) + → linux_launch() (VMCS config + vmlaunch) + → Linux 6.6 startup_64 (VMX non-root) + → Serial console output + → virtio-console ↔ UTSM + → virtio-fs mount /utsm + → User space (init/shell) +``` diff --git a/CODE/linux/build.sh b/CODE/linux/build.sh new file mode 100644 index 0000000..4dd375b --- /dev/null +++ b/CODE/linux/build.sh @@ -0,0 +1,134 @@ +#!/bin/bash +# +# Build Linux 6.6 LTS bzImage for UTSM dual-kernel guest. +# +# Prerequisites: +# - Linux build environment (WSL, native Linux, or Docker) +# - gcc, make, bc, flex, bison, libelf-dev, libssl-dev +# - ~3GB free disk space +# +# Usage: +# cd CODE/linux +# ./build.sh +# +# Output: +# - SYSTEM/boot/linux-bzImage (kernel image) +# - SYSTEM/boot/linux-initrd.img (minimal initramfs, if initramfs/ exists) + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +LINUX_SRC="${LINUX_SRC:-$SCRIPT_DIR/src}" +OUTPUT_DIR="$PROJECT_ROOT/SYSTEM/boot" +CONFIG_FILE="$SCRIPT_DIR/configs/utsm_x86_64_defconfig" +INITRAMFS_DIR="$SCRIPT_DIR/initramfs" + +echo "[build] Linux kernel build for UTSM" +echo "[build] Project root: $PROJECT_ROOT" +echo "[build] Linux source: $LINUX_SRC" + +# 1. Clone Linux 6.6 LTS if not present +if [ ! -d "$LINUX_SRC" ]; then + echo "[build] Cloning Linux 6.6 LTS..." + git clone --depth 1 --branch v6.6 \ + https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git \ + "$LINUX_SRC" +fi + +cd "$LINUX_SRC" + +# 2. Apply UTSM patches if any +PATCH_DIR="$SCRIPT_DIR/patches" +if [ -d "$PATCH_DIR" ] && [ "$(ls -A $PATCH_DIR 2>/dev/null)" ]; then + echo "[build] Applying UTSM patches..." + + # 2a. Apply .patch files (if any) + for patch in "$PATCH_DIR"/*.patch; do + if [ -f "$patch" ]; then + echo "[build] applying $(basename $patch)" + git apply --check "$patch" 2>/dev/null && git apply "$patch" || \ + echo "[build] WARNING: $patch already applied or failed" + fi + done + + # 2b. Install UTSM driver source files into drivers/utsm/ + UTSM_DRV_DIR="$LINUX_SRC/drivers/utsm" + echo "[build] installing UTSM driver into drivers/utsm/" + mkdir -p "$UTSM_DRV_DIR/include" + + # Copy driver source, Kconfig, and Makefile + cp "$PATCH_DIR/utsm_hcall.c" "$UTSM_DRV_DIR/utsm_hcall.c" + cp "$PATCH_DIR/utsm_Kconfig" "$UTSM_DRV_DIR/Kconfig" + cp "$PATCH_DIR/utsm_Makefile" "$UTSM_DRV_DIR/Makefile" + + # Copy shared IPC protocol header (included by both UTSM and Linux) + cp "$PROJECT_ROOT/CODE/utsm-ipc/ipc_proto.h" "$UTSM_DRV_DIR/include/ipc_proto.h" + + # Add UTSM to drivers/Kconfig (if not already present) + if ! grep -q "drivers/utsm/Kconfig" "$LINUX_SRC/drivers/Kconfig"; then + echo '[build] adding source "drivers/utsm/Kconfig" to drivers/Kconfig' + echo 'source "drivers/utsm/Kconfig"' >> "$LINUX_SRC/drivers/Kconfig" + fi + + # Add UTSM to drivers/Makefile (if not already present) + if ! grep -q "CONFIG_UTSM_HCALL" "$LINUX_SRC/drivers/Makefile"; then + echo '[build] adding obj-$(CONFIG_UTSM_HCALL) += utsm/ to drivers/Makefile' + echo 'obj-$(CONFIG_UTSM_HCALL) += utsm/' >> "$LINUX_SRC/drivers/Makefile" + fi + + echo "[build] UTSM driver installed:" + echo "[build] $UTSM_DRV_DIR/utsm_hcall.c" + echo "[build] $UTSM_DRV_DIR/Kconfig" + echo "[build] $UTSM_DRV_DIR/Makefile" + echo "[build] $UTSM_DRV_DIR/include/ipc_proto.h" +fi + +# 3. Configure +echo "[build] Configuring kernel..." +cp "$CONFIG_FILE" .config +make olddefconfig + +# 4. Build bzImage +echo "[build] Building bzImage (this may take 10-30 minutes)..." +make -j"$(nproc)" bzImage + +# 5. Copy bzImage to SYSTEM/boot/ +mkdir -p "$OUTPUT_DIR" +cp arch/x86/boot/bzImage "$OUTPUT_DIR/linux-bzImage" +echo "[build] bzImage: $OUTPUT_DIR/linux-bzImage ($(du -h $OUTPUT_DIR/linux-bzImage | cut -f1))" + +# 6. Build initramfs if initramfs/ directory exists +if [ -d "$INITRAMFS_DIR" ]; then + echo "[build] Building initramfs..." + # Create a minimal initramfs with busybox or a simple init + INITRAMFS_TMP="$SCRIPT_DIR/initramfs.tmp" + rm -rf "$INITRAMFS_TMP" + mkdir -p "$INITRAMFS_TMP" + + # Copy initramfs contents + cp -a "$INITRAMFS_DIR"/* "$INITRAMFS_TMP"/ + + # Create init script if not present + if [ ! -f "$INITRAMFS_TMP/init" ]; then + cat > "$INITRAMFS_TMP/init" <<'INITEOF' +#!/bin/sh +mount -t proc none /proc +mount -t sysfs none /sys +mount -t devtmpfs none /dev +echo "[utsm-linux] Boot complete" +echo "[utsm-linux] Console ready on /dev/hvc0 (virtio-console)" +exec /bin/sh +INITEOF + chmod +x "$INITRAMFS_TMP/init" + fi + + # Pack initramfs + cd "$INITRAMFS_TMP" + find . | cpio -H newc -o | gzip -9 > "$OUTPUT_DIR/linux-initrd.img" + echo "[build] initrd: $OUTPUT_DIR/linux-initrd.img ($(du -h $OUTPUT_DIR/linux-initrd.img | cut -f1))" + cd "$LINUX_SRC" +fi + +echo "[build] Done. Copy linux-bzImage (and linux-initrd.img) to your FAT32 image." +echo "[build] Limine will load them as boot modules for UTSM." diff --git a/CODE/linux/configs/utsm_x86_64_defconfig b/CODE/linux/configs/utsm_x86_64_defconfig new file mode 100644 index 0000000..c98aa68 --- /dev/null +++ b/CODE/linux/configs/utsm_x86_64_defconfig @@ -0,0 +1,231 @@ +# +# Linux 6.6 LTS minimal config for UTSM dual-kernel guest +# +# Build: make utsm_x86_64_defconfig && make -j$(nproc) bzImage +# +# Key design choices: +# - 64-bit only (CONFIG_64BIT=y) +# - Serial console (8250/16550 at 0x3F8) +# - virtio: console, block, net, fs (for UTSM host backends) +# - EFI stub disabled (UTSM uses custom VMX boot, not EFI) +# - Built-in drivers (no module loading needed for Phase 1) +# - No SMP (single CPU for Phase 1 simplicity) +# - printk to serial for debugging + +CONFIG_64BIT=y +CONFIG_X86_64=y +CONFIG_X86=y +CONFIG_INSTRUCTION_DECODER=y +CONFIG_OUTPUT_FORMAT="elf64-x86-64" +CONFIG_ARCH_MMAP_RND_BITS_MIN=28 +CONFIG_ARCH_MMAP_RND_BITS_MAX=32 +CONFIG_GENERIC_ISA_DMA=y +CONFIG_GENERIC_BUG=y +CONFIG_GENERIC_BUG_RELATIVE_POINTERS=y +CONFIG_GENERIC_HWEIGHT=y +CONFIG_ARCH_MMAP_RND_BITS=28 +CONFIG_SMP=y +CONFIG_NR_CPUS=4 +CONFIG_X86_64_SMP=y +CONFIG_SCHED_MC=y +CONFIG_SCHED_SMT=y + +# Processor type +CONFIG_MK8=y +CONFIG_X86_CPU=y +CONFIG_X86_L1_CACHE_SHIFT=6 +CONFIG_X86_TSC=y +CONFIG_X86_CMPXCHG64=y +CONFIG_X86_CMOV=y +CONFIG_X86_MINIMUM_CPU_FAMILY=64 +CONFIG_PROCESSOR_SELECT=y +CONFIG_CPU_SUP_INTEL=y +CONFIG_CPU_SUP_AMD=y +CONFIG_CPU_SUP_CENTAUR=y +CONFIG_HPET_TIMER=y +CONFIG_HPET_EMULATE_RTC=y +CONFIG_DMI=y + +# Memory +CONFIG_PHYS_ADDR_T_64BIT=y +CONFIG_ARCH_PHYS_ADDR_T_64BIT=y +CONFIG_ARCH_DMA_ADDR_T_64BIT=y +CONFIG_DIRECT_GBPAGES=y +CONFIG_ARCH_SPARSEMEM_ENABLE=y +CONFIG_ARCH_SPARSEMEM_DEFAULT=y +CONFIG_ILLEGAL_POINTER_VALUE=0xdead000000000000 +CONFIG_X86_PMEM_LEGACY_DEVICE=y +CONFIG_X86_PMEM_LEGACY=y +CONFIG_X86_CHECK_BIOS_CORRUPTION=y +CONFIG_X86_BOOTPARAM_MEMORY_CORRUPTION_CHECK=y +CONFIG_X86_RESERVE_LOW=64 +CONFIG_MTRR=y +CONFIG_MTRR_SANITIZER=y +CONFIG_MTRR_SANITIZER_ENABLE_DEFAULT=0 +CONFIG_X86_PAT=y +CONFIG_ARCH_USES_PG_UNCACHED=y +CONFIG_ARCH_RANDOM=y +CONFIG_X86_SMAP=y +CONFIG_X86_INTEL_UMIP=y +CONFIG_X86_UMIP=y + +# Power management (minimal) +CONFIG_PM=y +CONFIG_PM_CLK=y + +# Bus options +CONFIG_PCI=y +CONFIG_PCI_DIRECT=y +CONFIG_PCI_MMCONFIG=y +CONFIG_PCI_DOMAINS=y +CONFIG_PCI_MSI=y +CONFIG_PCI_MSI_IRQ_DOMAIN=y +CONFIG_PCI_LABEL=y +CONFIG_ISA_DMA_API=y +CONFIG_AMD_NB=y + +#Executable file formats +CONFIG_BINFMT_ELF=y +CONFIG_BINFMT_SCRIPT=y +CONFIG_BINFMT_MISC=y + +# Networking +CONFIG_NET=y +CONFIG_PACKET=y +CONFIG_UNIX=y +CONFIG_INET=y +CONFIG_IP_MULTICAST=y +CONFIG_IP_ADVANCED_ROUTER=y +CONFIG_IP_PNP=y +CONFIG_IP_PNP_DHCP=y +CONFIG_SYN_COOKIES=y +CONFIG_TCP_CONG_CUBIC=y + +# Device Drivers +# Serial console (critical: UTSM captures serial output) +CONFIG_SERIAL_8250=y +CONFIG_SERIAL_8250_CONSOLE=y +CONFIG_SERIAL_8250_PCI=y +CONFIG_SERIAL_8250_NR_UARTS=4 +CONFIG_SERIAL_8250_RUNTIME_UARTS=4 +CONFIG_SERIAL_CORE=y +CONFIG_SERIAL_CORE_CONSOLE=y +CONFIG_EARLY_PRINTK=y +CONFIG_EARLY_PRINTK_SERIAL=y + +# PCI host bridge +CONFIG_PCI_HOST_GENERIC=y + +# virtio (critical: UTSM provides virtio backends) +CONFIG_VIRTIO=y +CONFIG_VIRTIO_PCI=y +CONFIG_VIRTIO_PCI_LEGACY=y +CONFIG_VIRTIO_BALLOON=y +CONFIG_VIRTIO_INPUT=y +CONFIG_VIRTIO_MMIO=y +CONFIG_VIRTIO_MMIO_CMDLINE_DEVICES=y + +# virtio console (Phase 1.4: UTSM↔Linux console) +CONFIG_VIRTIO_CONSOLE=y +CONFIG_HW_RANDOM=y +CONFIG_HW_RANDOM_VIRTIO=y + +# UTSM dual-kernel hypercall and IPC driver (Phase 1.3) +CONFIG_UTSM_HCALL=y + +# virtio block (Phase 2: block device) +CONFIG_VIRTIO_BLK=y +CONFIG_VIRTIO_BLK_SCSI=y + +# virtio net (Phase 2: network) +CONFIG_VIRTIO_NET=y + +# virtio-fs (Phase 1.5: shared filesystem) +CONFIG_VIRTIO_FS=y +CONFIG_FUSE_FS=y + +# Filesystems +CONFIG_EXT4_FS=y +CONFIG_EXT4_USE_FOR_EXT2=y +CONFIG_FAT_FS=y +CONFIG_VFAT_FS=y +CONFIG_FAT_DEFAULT_CODEPAGE=437 +CONFIG_FAT_DEFAULT_IOCHARSET="iso8859-1" +CONFIG_TMPFS=y +CONFIG_PROC_FS=y +CONFIG_PROC_SYSCTL=y +CONFIG_SYSFS=y +CONFIG_DEVTMPFS=y +CONFIG_DEVTMPFS_MOUNT=y + +# Block devices +CONFIG_BLK_DEV=y +CONFIG_BLK_DEV_LOOP=y + +# Input (minimal) +CONFIG_INPUT=y +CONFIG_INPUT_KEYBOARD=y + +# RTC +CONFIG_RTC_LIB=y +CONFIG_RTC_CLASS=y +CONFIG_RTC_HCTOSYS=y +CONFIG_RTC_HCTOSYS_DEVICE="rtc0" + +# PNP/ACPI (minimal for QEMU) +CONFIG_ACPI=y +CONFIG_ACPI_AC=y +CONFIG_ACPI_BUTTON=y +CONFIG_ACPI_FAN=y +CONFIG_ACPI_PROCESSOR=y +CONFIG_ACPI_THERMAL=y + +# File systems +CONFIG_FHANDLE=y + +# Kernel hacking +CONFIG_PRINTK=y +CONFIG_PRINTK_TIME=y +CONFIG_CONSOLE_LOGLEVEL_DEFAULT=7 +CONFIG_MESSAGE_LOGLEVEL_DEFAULT=4 +CONFIG_DEBUG_INFO=y +CONFIG_DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT=y +CONFIG_FRAME_WARN=2048 +CONFIG_MAGIC_SYSRQ=y +CONFIG_DEBUG_KERNEL=y +CONFIG_DEBUG_RODATA=y +CONFIG_DEBUG_SET_MODULE_RONX=y +CONFIG_DEBUG_WX=y + +# Security (minimal) +CONFIG_SECURITY=y +CONFIG_SECURITYFS=y + +# Crypto (minimal) +CONFIG_CRYPTO=y +CONFIG_CRYPTO_CRC32C=y +CONFIG_CRYPTO_SHA256=y + +# Library routines +CONFIG_CRC16=y +CONFIG_CRC32=y +CONFIG_ZLIB_INFLATE=y +CONFIG_ZLIB_DEFLATE=y +CONFIG_LZO_COMPRESS=y +CONFIG_LZO_DECOMPRESS=y +CONFIG_XZ_DEC=y +CONFIG_XZ_DEC_X86=y + +# Initramfs +CONFIG_BLK_DEV_INITRD=y +CONFIG_INITRAMFS_SOURCE="" + +# No module support (build everything in for Phase 1) +# CONFIG_MODULES is not set + +# No EFI stub (UTSM uses custom VMX boot) +# CONFIG_EFI_STUB is not set +# CONFIG_EFI is not set + +# Boot protocol +CONFIG_EARLY_PRINTK_SERIAL=y diff --git a/CODE/linux/patches/utsm_Kconfig b/CODE/linux/patches/utsm_Kconfig new file mode 100644 index 0000000..a317728 --- /dev/null +++ b/CODE/linux/patches/utsm_Kconfig @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# UTSM dual-kernel hypercall driver configuration +# + +config UTSM_HCALL + tristate "UTSM dual-kernel hypercall and IPC driver" + default y + help + This driver enables communication between the Linux guest and + the UTSM monitor kernel in the Deshab dual-kernel architecture. + + When loaded, it: + - Probes for the UTSM monitor via a VMCALL PING hypercall + - Maps the IPC shared memory ring buffer + - Registers /dev/utsm for userspace IPC message passing + + If running on bare metal or a non-UTSM hypervisor, the driver + loads in passive mode (no /dev/utsm registered). + + If unsure, say Y for UTSM guest builds, N otherwise. + +config UTSM_EARLY_CONSOLE + bool "Redirect kernel printk to UTSM serial console" + depends on UTSM_HCALL + default n + help + Redirects Linux kernel console output to the UTSM monitor's + serial port via UTSM_HCALL_CONSOLE_WRITE hypercalls. Useful + for early boot debugging when the guest's own serial is not + configured. + + If unsure, say N. diff --git a/CODE/linux/patches/utsm_Makefile b/CODE/linux/patches/utsm_Makefile new file mode 100644 index 0000000..e362e74 --- /dev/null +++ b/CODE/linux/patches/utsm_Makefile @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# UTSM dual-kernel driver Makefile +# + +obj-$(CONFIG_UTSM_HCALL) += utsm_hcall.o + +# Include the shared IPC protocol header from CODE/utsm-ipc/ +# This path is set up by build.sh when copying files into the source tree +ccflags-y += -I$(src)/include diff --git a/CODE/linux/patches/utsm_hcall.c b/CODE/linux/patches/utsm_hcall.c new file mode 100644 index 0000000..f61b653 --- /dev/null +++ b/CODE/linux/patches/utsm_hcall.c @@ -0,0 +1,369 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * UTSM hypercall driver — Linux guest side. + * + * Provides: + * 1. VMCALL hypercall interface to communicate with the UTSM monitor kernel + * 2. /dev/utsm character device for userspace IPC message passing + * 3. Shared memory ring buffer mapping (queried via UTSM_HCALL_SHM_INFO) + * + * The driver is platform-agnostic: it probes for UTSM by issuing a PING + * hypercall at init. If UTSM is not present (e.g. running on bare metal + * or a different hypervisor), the driver exits gracefully. + * + * Build: configured via CONFIG_UTSM_HCALL (built-in or module). + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Shared IPC protocol header (included via build.sh -I flag) */ +#include + +#define UTSM_DEV_NAME "utsm" + +/* ===== Hypercall ABI ===== + * + * Linux calls VMCALL with: + * RAX = UTSM_HCALL_MAGIC | op + * RDI = arg0, RSI = arg1, RDX = arg2 + * + * UTSM handles the VMCALL VM-Exit and returns: + * RAX = return value + * + * Since we run in the guest, the VMCALL instruction triggers a VM-Exit + * to UTSM. The inline assembly must use the same register conventions. + */ + +static inline long utsm_hcall(u64 op, u64 a0, u64 a1, u64 a2) +{ + long ret; + register u64 rax asm("rax") = UTSM_HCALL_MAGIC | op; + register u64 rdi asm("rdi") = a0; + register u64 rsi asm("rsi") = a1; + register u64 rdx asm("rdx") = a2; + + asm volatile("vmcall" + : "+r"(rax) + : "r"(rdi), "r"(rsi), "r"(rdx) + : "memory"); + + return (long)rax; +} + +static inline long utsm_hcall_ping(void) +{ + return utsm_hcall(UTSM_HCALL_PING, 0, 0, 0); +} + +static inline long utsm_hcall_hello(void) +{ + return utsm_hcall(UTSM_HCALL_HELLO, 0, 0, 0); +} + +static inline long utsm_hcall_shm_info(u64 *gpa_out, u64 *size_out) +{ + u64 gpa = 0, size = 0; + long ret; + + /* Pass stack addresses (GVA=GPA due to identity mapping in early boot). + * UTSM translates GPA→HPA via EPT walk and writes the values there. */ + ret = utsm_hcall(UTSM_HCALL_SHM_INFO, (u64)&gpa, (u64)&size, 0); + if (ret == 0) { + *gpa_out = gpa; + *size_out = size; + } + return ret; +} + +static inline long utsm_hcall_console_write(const char *buf, u64 len) +{ + return utsm_hcall(UTSM_HCALL_CONSOLE_WRITE, (u64)buf, len, 0); +} + +/* ===== Shared memory state ===== */ + +static struct utsm_ipc_shm *g_shm; /* kernel virtual address of SHM */ +static u64 g_shm_gpa; /* guest physical address of SHM */ +static u64 g_shm_size; /* size of SHM region */ +static int g_utsm_present; /* 1 = UTSM detected */ +static int g_shm_mapped; + +/* ===== /dev/utsm character device ===== */ + +static ssize_t utsm_dev_write(struct file *f, const char __user *buf, + size_t len, loff_t *off) +{ + struct utsm_ipc_msg msg; + size_t copy_len; + ssize_t ret; + + if (!g_shm_mapped) + return -ENODEV; + + if (len == 0) + return 0; + + /* Cap data to inline message size */ + copy_len = min_t(size_t, len, UTSM_IPC_MSG_DATA_SIZE); + + memset(&msg, 0, sizeof(msg)); + msg.type = UTSM_MSG_DATA; + msg.data_len = (u32)copy_len; + + if (copy_from_user(msg.data, buf, copy_len)) { + pr_err("[utsm] copy_from_user failed\n"); + return -EFAULT; + } + + /* Enqueue into linux_to_utsm ring (Linux produces, UTSM consumes) */ + if (utsm_ipc_ring_push(&g_shm->linux_to_utsm, &msg) != 0) { + pr_warn("[utsm] linux_to_utsm ring full\n"); + return -EAGAIN; + } + + ret = (ssize_t)copy_len; + + /* Also relay the data to the UTSM serial console via hypercall. + * This gives immediate visibility into Linux userspace output. */ + if (copy_len > 0) { + char kbuf[UTSM_IPC_MSG_DATA_SIZE + 1]; + memcpy(kbuf, msg.data, copy_len); + kbuf[copy_len] = '\0'; + utsm_hcall_console_write(kbuf, copy_len); + } + + return ret; +} + +static ssize_t utsm_dev_read(struct file *f, char __user *buf, + size_t len, loff_t *off) +{ + struct utsm_ipc_msg msg; + + if (!g_shm_mapped) + return -ENODEV; + + /* Dequeue from utsm_to_linux ring (UTSM produces, Linux consumes) */ + if (utsm_ipc_ring_pop(&g_shm->utsm_to_linux, &msg) != 0) { + /* No message available */ + return -EAGAIN; /* Non-blocking; userspace can poll */ + } + + if (msg.data_len == 0) + return 0; + + if (len < msg.data_len) + return -EINVAL; + + if (copy_to_user(buf, msg.data, msg.data_len)) + return -EFAULT; + + return (ssize_t)msg.data_len; +} + +static const struct file_operations utsm_fops = { + .owner = THIS_MODULE, + .write = utsm_dev_write, + .read = utsm_dev_read, +}; + +static struct miscdevice utsm_miscdev = { + .minor = MISC_DYNAMIC_MINOR, + .name = UTSM_DEV_NAME, + .fops = &utsm_fops, + .mode = 0666, +}; + +/* ===== Early console redirect (optional) ===== + * + * Redirect kernel printk output to UTSM serial console via hypercall. + * This is useful for debugging when the guest's own serial is not set up. + * Currently disabled; can be enabled via CONFIG_UTSM_EARLY_CONSOLE. + */ + +#ifdef CONFIG_UTSM_EARLY_CONSOLE +static void utsm_console_write(struct console *con, const char *s, unsigned int n) +{ + if (g_utsm_present) { + char buf[256]; + unsigned int chunk; + + while (n > 0) { + chunk = min_t(unsigned int, n, sizeof(buf) - 1); + memcpy(buf, s, chunk); + buf[chunk] = '\0'; + utsm_hcall_console_write(buf, chunk); + s += chunk; + n -= chunk; + } + } +} + +static struct console utsm_console = { + .name = "utsmcon", + .write = utsm_console_write, + .flags = CON_PRINTBUFFER | CON_ENABLED, + .index = -1, +}; +#endif + +/* ===== Initialization ===== */ + +/* Forward declaration: used by init before /dev/utsm is registered */ +static inline int ipc_shm_send_to_utsm(u32 type, const void *data, u32 len); + +static int __init utsm_hcall_init(void) +{ + long ping_ret; + u64 shm_gpa = 0, shm_size = 0; + int ret; + + pr_info("[utsm] probing for UTSM monitor...\n"); + + /* 1. Ping UTSM to verify it's present */ + ping_ret = utsm_hcall_ping(); + if (ping_ret != 0x504F4E47) { /* "PONG" */ + pr_info("[utsm] UTSM not detected (ping returned 0x%lx)\n", ping_ret); + pr_info("[utsm] Driver loaded in passive mode (no /dev/utsm)\n"); + return 0; /* Not an error: we might be running on bare metal */ + } + + g_utsm_present = 1; + pr_info("[utsm] UTSM monitor detected (PONG=0x%lx)\n", ping_ret); + + /* 2. Say hello */ + utsm_hcall_hello(); + + /* 3. Query shared memory info */ + ret = utsm_hcall_shm_info(&shm_gpa, &shm_size); + if (ret != 0) { + pr_err("[utsm] SHM_INFO hypercall failed: %ld\n", ret); + goto err_no_shm; + } + + pr_info("[utsm] shared memory: gpa=0x%llx size=%llu KB\n", + shm_gpa, shm_size / 1024); + + if (shm_gpa == 0 || shm_size == 0) { + pr_err("[utsm] invalid SHM info\n"); + goto err_no_shm; + } + + /* 4. Map the shared memory region. + * The GPA is a physical address from Linux's perspective. + * Use ioremap_cache for cacheable memory (the SHM region is + * configured as WB in EPT). */ + g_shm = (struct utsm_ipc_shm *)ioremap_cache(shm_gpa, shm_size); + if (!g_shm) { + pr_err("[utsm] failed to ioremap SHM at 0x%llx\n", shm_gpa); + goto err_ioremap; + } + + g_shm_gpa = shm_gpa; + g_shm_size = shm_size; + g_shm_mapped = 1; + + /* 5. Verify SHM header */ + if (g_shm->header.magic != UTSM_IPC_MAGIC) { + pr_err("[utsm] SHM magic mismatch: 0x%llx\n", g_shm->header.magic); + goto err_bad_magic; + } + + pr_info("[utsm] SHM header: magic=OK version=%u utsm_ready=%u\n", + g_shm->header.version, g_shm->header.utsm_ready); + + /* 6. Set linux_ready flag */ + g_shm->header.linux_ready = 1; + mb(); /* Ensure visible to UTSM */ + + /* 7. Register /dev/utsm */ + ret = misc_register(&utsm_miscdev); + if (ret) { + pr_err("[utsm] misc_register failed: %d\n", ret); + goto err_misc; + } + +#ifdef CONFIG_UTSM_EARLY_CONSOLE + register_console(&utsm_console); +#endif + + pr_info("[utsm] /dev/%s registered (IPC ready)\n", UTSM_DEV_NAME); + + /* 8. Send a HELLO message via the ring buffer */ + { + const char *hello_msg = "Linux booted OK"; + ipc_shm_send_to_utsm(UTSM_MSG_DATA, hello_msg, 15); + } + + return 0; + +err_misc: + g_shm->header.linux_ready = 0; +err_bad_magic: + iounmap(g_shm); + g_shm = NULL; + g_shm_mapped = 0; +err_ioremap: +err_no_shm: + g_utsm_present = 0; + return -ENODEV; +} + +/* Helper to send a message (used by init before /dev/utsm is available) */ +static inline int ipc_shm_send_to_utsm(u32 type, const void *data, u32 len) +{ + struct utsm_ipc_msg msg; + + if (!g_shm_mapped || !g_shm) + return -1; + + if (len > UTSM_IPC_MSG_DATA_SIZE) + len = UTSM_IPC_MSG_DATA_SIZE; + + memset(&msg, 0, sizeof(msg)); + msg.type = type; + msg.data_len = len; + if (data && len > 0) + memcpy(msg.data, data, len); + + return utsm_ipc_ring_push(&g_shm->linux_to_utsm, &msg); +} + +static void __exit utsm_hcall_exit(void) +{ + if (g_shm_mapped) { + /* Clear linux_ready flag */ + g_shm->header.linux_ready = 0; + mb(); + + /* Send shutdown notification */ + ipc_shm_send_to_utsm(UTSM_MSG_SHUTDOWN, NULL, 0); + + misc_deregister(&utsm_miscdev); + +#ifdef CONFIG_UTSM_EARLY_CONSOLE + unregister_console(&utsm_console); +#endif + + iounmap(g_shm); + g_shm = NULL; + g_shm_mapped = 0; + } + + pr_info("[utsm] driver unloaded\n"); +} + +module_init(utsm_hcall_init); +module_exit(utsm_hcall_exit); + +MODULE_AUTHOR("Deshab Project"); +MODULE_DESCRIPTION("UTSM dual-kernel hypercall and IPC driver"); +MODULE_LICENSE("GPL"); diff --git a/CODE/login/MAKEFILE b/CODE/login/MAKEFILE index fc329ac..339efb9 100644 --- a/CODE/login/MAKEFILE +++ b/CODE/login/MAKEFILE @@ -3,6 +3,7 @@ LD := ld.lld ROOT := ../.. OUT := $(ROOT)/SYSTEM/system/user/use/login.elf +PATCHER := $(ROOT)/.build_tmp/patch_login_hash.ps1 CFLAGS := -target x86_64-unknown-none -std=c11 -ffreestanding -fno-builtin -fno-stack-protector -fpie -mno-red-zone -mno-sse -mno-sse2 -mno-mmx -msoft-float -m64 -Wall -Wextra -I../UTSM/include -I../firstInit -O2 LDFLAGS := -nostdlib -static -pie -z max-page-size=0x1000 -T linker.ld @@ -14,9 +15,12 @@ OBJECTS := $(C_SOURCES:.c=.o) all: $(OUT) +# 构建后用 patch_login_hash.ps1 计算 .text SHA256 并写入 .hashseg $(OUT): $(OBJECTS) linker.ld @if not exist "$(ROOT)\SYSTEM\system\user\use" mkdir "$(ROOT)\SYSTEM\system\user\use" $(LD) $(LDFLAGS) -o $@ $(OBJECTS) + @echo [login] patching .text SHA256 into .hashseg... + powershell -NoProfile -ExecutionPolicy Bypass -File $(PATCHER) $@ %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ diff --git a/CODE/login/linker.ld b/CODE/login/linker.ld index 473e5fc..627093f 100644 --- a/CODE/login/linker.ld +++ b/CODE/login/linker.ld @@ -11,12 +11,28 @@ PHDRS SECTIONS { . = 0x1000; + /* 防篡改:导出 .text 段范围供运行时自校验 */ + __text_start = .; .text : ALIGN(4K) { *(.text*) } :text + __text_end = .; + .rodata : ALIGN(4K) { *(.rodata*) } :rodata + + /* 防篡改:独立的 .hashseg 段存放预期 SHA256(32 字节)。 + * 由构建后补丁脚本 patch_login_hash.exe 写入。 + * 初始填充 0x00,运行时若发现全 0 则跳过校验(开发期便利)。 */ + .hashseg : ALIGN(16) { + __expected_hash = .; + . = . + 32; + __expected_hash_end = .; + } :rodata + .data : ALIGN(4K) { *(.data*) *(.data.rel.ro*) *(COMMON) + } :data + .bss : ALIGN(4K) { *(.bss*) } :data .rela.dyn : ALIGN(8) { *(.rela.dyn*) } :data diff --git a/CODE/login/main.c b/CODE/login/main.c index 4c73aee..ab49451 100644 --- a/CODE/login/main.c +++ b/CODE/login/main.c @@ -4,12 +4,22 @@ * extracts username + password hash, prompts for password, * verifies via SHA256 comparison, sets ctx->reserved[2]=1 on success. * + * 防篡改:对 .text 段做 SHA256 自校验,预期哈希由构建后补丁脚本 + * 写入 .hashseg 段(__expected_hash)。运行时若发现哈希不匹配, + * 拒绝继续并触发串口告警。开发期哈希为全 0 时跳过校验。 + * * Returns to DSK in all cases (success, skip via Esc, or fatal error). * DSK loads desktop.elf after this returns. */ #include "../UTSM/include/utsm/dsk.h" +/* 防篡改:linker 导出的 .text 段范围与预期哈希存储位置 */ +extern const u8 __text_start[]; +extern const u8 __text_end[]; +extern const u8 __expected_hash[]; +extern const u8 __expected_hash_end[]; + typedef signed char i8; typedef unsigned char u8; typedef unsigned short u16; @@ -253,6 +263,69 @@ static void sha256(const char *msg, u8 out[32]) { for(int i=0;i<8;i++){out[i*4]=(u8)(h[i]>>24);out[i*4+1]=(u8)(h[i]>>16);out[i*4+2]=(u8)(h[i]>>8);out[i*4+3]=(u8)h[i];} } +/* ---- 防篡改:对任意字节缓冲计算 SHA256(分块处理,支持 >55 字节) ---- */ +static void sha256_buf(const u8 *msg, u64 len, u8 out[32]) { + u32 h[8]={0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19}; + u8 block[64]; + u64 processed = 0; + while (processed + 64 <= len) { + for (int i = 0; i < 64; i++) block[i] = msg[processed + i]; + u32 w[64]; + for(int i=0;i<16;i++) w[i]=((u32)block[i*4]<<24)|((u32)block[i*4+1]<<16)|((u32)block[i*4+2]<<8)|block[i*4+3]; + for(int i=16;i<64;i++){u32 s0=ROR(w[i-15],7)^ROR(w[i-15],18)^(w[i-15]>>3);u32 s1=ROR(w[i-2],17)^ROR(w[i-2],19)^(w[i-2]>>10);w[i]=w[i-16]+s0+w[i-7]+s1;} + u32 a=h[0],b=h[1],c=h[2],d=h[3],e=h[4],f=h[5],g=h[6],hh=h[7]; + for(int i=0;i<64;i++){u32 S1=ROR(e,6)^ROR(e,11)^ROR(e,25);u32 ch=(e&f)^((~e)&g);u32 t1=hh+S1+ch+K256[i]+w[i];u32 S0=ROR(a,2)^ROR(a,13)^ROR(a,22);u32 maj=(a&b)^(a&c)^(b&c);u32 t2=S0+maj;hh=g;g=f;f=e;e=d+t1;d=c;c=b;b=a;a=t1+t2;} + h[0]+=a;h[1]+=b;h[2]+=c;h[3]+=d;h[4]+=e;h[5]+=f;h[6]+=g;h[7]+=hh; + processed += 64; + } + /* 最终块:剩余数据 + 0x80 + 长度(64 位大端) */ + u32 rem = (u32)(len - processed); + for (u32 i = 0; i < 64; i++) block[i] = 0; + for (u32 i = 0; i < rem; i++) block[i] = msg[processed + i]; + block[rem] = 0x80; + u64 bit = len * 8; + for (int i = 0; i < 8; i++) block[63 - i] = (u8)(bit >> (i * 8)); + u32 w[64]; + for(int i=0;i<16;i++) w[i]=((u32)block[i*4]<<24)|((u32)block[i*4+1]<<16)|((u32)block[i*4+2]<<8)|block[i*4+3]; + for(int i=16;i<64;i++){u32 s0=ROR(w[i-15],7)^ROR(w[i-15],18)^(w[i-15]>>3);u32 s1=ROR(w[i-2],17)^ROR(w[i-2],19)^(w[i-2]>>10);w[i]=w[i-16]+s0+w[i-7]+s1;} + u32 a=h[0],b=h[1],c=h[2],d=h[3],e=h[4],f=h[5],g=h[6],hh=h[7]; + for(int i=0;i<64;i++){u32 S1=ROR(e,6)^ROR(e,11)^ROR(e,25);u32 ch=(e&f)^((~e)&g);u32 t1=hh+S1+ch+K256[i]+w[i];u32 S0=ROR(a,2)^ROR(a,13)^ROR(a,22);u32 maj=(a&b)^(a&c)^(b&c);u32 t2=S0+maj;hh=g;g=f;f=e;e=d+t1;d=c;c=b;b=a;a=t1+t2;} + h[0]+=a;h[1]+=b;h[2]+=c;h[3]+=d;h[4]+=e;h[5]+=f;h[6]+=g;h[7]+=hh; + for(int i=0;i<8;i++){out[i*4]=(u8)(h[i]>>24);out[i*4+1]=(u8)(h[i]>>16);out[i*4+2]=(u8)(h[i]>>8);out[i*4+3]=(u8)h[i];} +} + +/* ---- 防篡改:运行时 .text 段完整性校验 ---- + * 计算 __text_start ~ __text_end 的 SHA256,与 __expected_hash 对比。 + * 返回 1=校验通过(或开发期哈希为空跳过),0=校验失败(被篡改) */ +static int login_verify_integrity(void) { + const u8 *start = __text_start; + const u8 *end = __text_end; + const u8 *expected = __expected_hash; + u64 len = (u64)(end - start); + if (len == 0) return 0; + + /* 开发期便利:预期哈希全 0 时跳过校验 */ + int all_zero = 1; + for (int i = 0; i < 32; i++) if (expected[i] != 0) { all_zero = 0; break; } + if (all_zero) { + logl("[login] integrity: expected hash empty (dev mode), skip"); + return 1; + } + + u8 actual[32]; + sha256_buf(start, len, actual); + + /* 常量时间比较 */ + u8 diff = 0; + for (int i = 0; i < 32; i++) diff |= actual[i] ^ expected[i]; + if (diff == 0) { + logl("[login] integrity: OK"); + return 1; + } + logl("[login] integrity: FAIL — code tampered!"); + return 0; +} + /* ---- conf parsing ---- * USER.CONF format (built by FirstInit's build_user_conf, but stored unencrypted * on disk for login.elf to parse): @@ -404,6 +477,14 @@ __attribute__((visibility("default"))) void dsk_entry(const dsk_boot_context *ctx) { __asm__ volatile("cli"); /* prevent IRQ1 (ps2kbd) from racing with our polling */ logl("[login] boot"); + + /* 防篡改:第一时间校验 .text 段完整性。失败则拒绝继续,立即返回。 */ + if (!login_verify_integrity()) { + logl("[login] integrity check failed, refusing to continue"); + if (ctx) ((dsk_boot_context *)ctx)->reserved[2] = 0; + return; + } + logl("[login] calibrating TSC"); tsc_calibrate_fi(); diff --git a/CODE/tools/browser/main.c b/CODE/tools/browser/main.c index 26844ad..b9934b1 100644 --- a/CODE/tools/browser/main.c +++ b/CODE/tools/browser/main.c @@ -1,22 +1,170 @@ -/* Deshab Browser — 全屏浏览器 (Stub) - * 地址栏 + 导航按钮 + 内容渲染区域。 - * 当前显示 stub 提示,Esc 退出。 +/* Deshab Browser — 全屏浏览器 + * 接入 net_stack.h 共享协议栈,实现 HTTP GET 与简单内容显示。 + * 地址栏输入 URL(如 http://10.0.2.2:8000/ 或 10.0.2.2), + * Enter 发起请求,内容区显示 HTTP 响应(分离 header/body,移除 HTML 标签)。 + * Esc 退出。 */ #include "../../firstInit/ascii_bitmaps.c" #include "../desktop_app.h" +#include "../net_stack.h" #define CHAR_STEP 12 #define CHAR_H 18 +#define URL_BUF_SIZE 128 +#define CONTENT_BUF_SIZE 32768 +#define DISPLAY_LINES 200 + +/* 前向声明(do_fetch 引用 redraw_all_imm 和 br_ns_init) */ +static void redraw_all_imm(void); +static int br_ns_init(void); static da_app_context g_ac; static da_cursor g_cursor; static da_mouse g_mouse; -static void redraw_all(void) { - da_fill_bg(&g_ac, DA_BG_PRIMARY); +/* 地址栏 */ +static char url_buf[URL_BUF_SIZE] = "http://10.0.2.2:8000/"; +static int url_len = 22; +static int url_focused = 1; /* 启动时地址栏聚焦 */ - /* 标题栏 */ +/* 内容缓冲 */ +static u8 content_buf[CONTENT_BUF_SIZE]; +static u32 content_len = 0; +static int content_scroll = 0; +static int fetching = 0; /* 1=正在请求 */ +static char status_msg[64] = "Ready"; + +/* ---- 状态消息 ---- */ +static void br_set_status(const char *s) { + int i = 0; + while (*s && i < 63) status_msg[i++] = *s++; + status_msg[i] = 0; +} + +/* ---- HTTP 请求(复制 shell cmd_curl 核心逻辑) ---- */ +static void do_fetch(void) { + if (url_len == 0) { br_set_status("Empty URL"); return; } + + br_set_status("Initializing net..."); + redraw_all_imm(); + + if (br_ns_init() != 0) { + br_set_status("Net init failed"); + return; + } + + /* 解析 URL: [http://]host[:port][/path] */ + const char *p = url_buf; + if (p[0]=='h'&&p[1]=='t'&&p[2]=='t'&&p[3]=='p'&&p[4]==':'&&p[5]=='/'&&p[6]=='/') p += 7; + + char host[64]; int hl = 0; + while (*p && *p != ':' && *p != '/' && hl < 63) host[hl++] = *p++; + host[hl] = 0; + if (!hl) { br_set_status("Bad host"); return; } + + u16 port = 80; + if (*p == ':') { + p++; + u32 pv = 0; int digits = 0; + while (*p >= '0' && *p <= '9') { pv = pv * 10 + (u32)(*p - '0'); p++; digits++; } + if (digits && pv <= 65535) port = (u16)pv; + } + char path[128]; int pl = 0; + if (*p == '/') { + while (*p && *p != ' ' && pl < 127) path[pl++] = *p++; + } + if (!pl) { path[0] = '/'; pl = 1; } + path[pl] = 0; + + /* DNS 解析 */ + br_set_status("Resolving..."); + redraw_all_imm(); + u32 ip; + if (ns_dns_resolve(host, &ip) != 0) { + br_set_status("DNS failed"); + return; + } + + /* TCP 连接 */ + br_set_status("Connecting..."); + redraw_all_imm(); + if (ns_tcp_connect(ip, port, 3000) != 0) { + br_set_status("TCP connect failed"); + return; + } + + /* 构造 HTTP/1.0 请求 */ + static char req[512]; + int r = 0; + const char *m = "GET "; while (*m) req[r++] = *m++; + for (int i = 0; i < pl; i++) req[r++] = path[i]; + m = " HTTP/1.0\r\nHost: "; while (*m) req[r++] = *m++; + for (int i = 0; i < hl; i++) req[r++] = host[i]; + m = "\r\nConnection: close\r\n\r\n"; while (*m) req[r++] = *m++; + req[r] = 0; + + br_set_status("Sending request..."); + redraw_all_imm(); + if (ns_tcp_send((const u8 *)req, (u32)r) != 0) { + br_set_status("Send failed"); + ns_tcp_close(); + return; + } + + br_set_status("Receiving..."); + redraw_all_imm(); + int n = ns_tcp_recv(0, 0, 5000); + ns_tcp_close(); + + if (n < 0) { br_set_status("Connection reset"); return; } + if (n == 0) { br_set_status("No response (timeout)"); return; } + + /* 复制响应到 content_buf */ + u32 total = ns_tcp_rx_len; + u32 copy = (total < CONTENT_BUF_SIZE) ? total : CONTENT_BUF_SIZE; + for (u32 i = 0; i < copy; i++) content_buf[i] = ns_tcp_rx[i]; + content_len = copy; + content_scroll = 0; + + char sm[32]; int sp = 0; + const char *ok = "OK "; while (*ok && sp < 30) sm[sp++] = *ok++; + char nb[12]; ns_u32_dec(nb, copy); + for (int i = 0; nb[i] && sp < 30; i++) sm[sp++] = nb[i]; + sm[sp] = 0; + br_set_status(sm); +} + +/* ---- 极简 HTML 文本提取:移除 <...> 标签,保留纯文本 ---- */ +static int html_to_text(const u8 *in, u32 in_len, u8 *out, u32 out_cap) { + u32 oi = 0; + int in_tag = 0; + int prev_space = 0; + for (u32 i = 0; i < in_len && oi < out_cap - 1; i++) { + u8 c = in[i]; + if (c == '<') { in_tag = 1; continue; } + if (c == '>') { in_tag = 0; continue; } + if (in_tag) continue; + /* 跳过 HTTP header(找到 \r\n\r\n 后才开始) */ + /* 将   等实体简化为空格 */ + if (c == '\r') continue; + if (c == '\t') c = ' '; + /* 连续空格压缩 */ + if (c == ' ') { + if (prev_space) continue; + prev_space = 1; + } else { + prev_space = 0; + } + out[oi++] = c; + } + out[oi] = 0; + return (int)oi; +} + +/* ---- 立即重绘(无鼠标光标保存/恢复,用于 fetching 过程) ---- */ +static void redraw_all_imm(void) { + da_fill_bg(&g_ac, DA_BG_PRIMARY); da_draw_titlebar(&g_ac, "Browser", (i64)g_ac.fb_w); /* 地址栏 */ @@ -24,69 +172,100 @@ static void redraw_all(void) { int bar_h = 28; int bar_x = 12; int bar_w = (int)g_ac.fb_w - 24; - da_fill_rounded_rect(&g_ac, bar_x, bar_y, bar_w, bar_h, DA_BG_TERTIARY, 6); - da_rect_outline(&g_ac, bar_x, bar_y, bar_w, bar_h, DA_BORDER, 6); + u32 bar_bg = url_focused ? DA_BG_TERTIARY : DA_BG_SECONDARY; + da_fill_rounded_rect(&g_ac, bar_x, bar_y, bar_w, bar_h, bar_bg, 6); + da_rect_outline(&g_ac, bar_x, bar_y, bar_w, bar_h, url_focused ? DA_ACCENT : DA_BORDER, 6); - /* 导航按钮 */ - int btn_w = 32; - da_fill_rounded_rect(&g_ac, bar_x + 4, bar_y + 4, btn_w, bar_h - 8, DA_BG_SECONDARY, 4); - da_draw_string(&g_ac, "<", bar_x + 14, bar_y + 5, DA_TEXT_PRIMARY, DA_BG_SECONDARY, CHAR_STEP); - da_fill_rounded_rect(&g_ac, bar_x + 4 + btn_w + 4, bar_y + 4, btn_w, bar_h - 8, DA_BG_SECONDARY, 4); - da_draw_string(&g_ac, ">", bar_x + 4 + btn_w + 4 + 12, bar_y + 5, DA_TEXT_PRIMARY, DA_BG_SECONDARY, CHAR_STEP); - da_fill_rounded_rect(&g_ac, bar_x + 4 + (btn_w + 4) * 2, bar_y + 4, btn_w, bar_h - 8, DA_BG_SECONDARY, 4); - da_draw_string(&g_ac, "R", bar_x + 4 + (btn_w + 4) * 2 + 12, bar_y + 5, DA_TEXT_PRIMARY, DA_BG_SECONDARY, CHAR_STEP); + /* URL 文本 + 光标 */ + int url_x = bar_x + 12; + da_draw_string(&g_ac, url_buf, url_x, bar_y + 5, DA_TEXT_PRIMARY, bar_bg, CHAR_STEP); + if (url_focused) { + int cx = url_x + url_len * CHAR_STEP; + da_fill_rect(&g_ac, cx, bar_y + 5, 2, CHAR_H, DA_ACCENT_LIGHT); + } - /* URL 文本 */ - int url_x = bar_x + (btn_w + 4) * 3 + 8; - da_draw_string(&g_ac, "deshab://home", url_x, bar_y + 5, DA_TEXT_DIM, DA_BG_TERTIARY, CHAR_STEP); + /* 提示 */ + int hint_y = bar_y + bar_h + 4; + const char *hint = "Enter: fetch Esc: exit Up/Down: scroll"; + da_draw_string(&g_ac, hint, 12, hint_y, DA_TEXT_DIM, DA_BG_PRIMARY, CHAR_STEP); - /* 内容区域 */ - int content_y = bar_y + bar_h + 12; - int content_x = 16; - int content_w = (int)g_ac.fb_w - 32; + /* 内容区 */ + int content_y = hint_y + CHAR_H + 4; + int content_x = 12; + int content_w = (int)g_ac.fb_w - 24; + int content_h = (int)g_ac.fb_h - content_y - DA_STATUSBAR_H - 8; + da_fill_rounded_rect(&g_ac, content_x, content_y, content_w, content_h, DA_BG_SECONDARY, 4); + da_rect_outline(&g_ac, content_x, content_y, content_w, content_h, DA_BORDER, 4); - /* Stub 页面 */ - da_fill_rounded_rect(&g_ac, content_x, content_y, content_w, 200, DA_BG_SECONDARY, 8); - da_rect_outline(&g_ac, content_x, content_y, content_w, 200, DA_BORDER, 8); + /* 内容显示 */ + if (content_len > 0) { + /* 提取 body(跳过 HTTP header) */ + u32 body_off = 0; + for (u32 i = 0; i + 3 < content_len; i++) { + if (content_buf[i]=='\r' && content_buf[i+1]=='\n' && + content_buf[i+2]=='\r' && content_buf[i+3]=='\n') { + body_off = i + 4; + break; + } + } + /* HTML 转纯文本 */ + static u8 plain[CONTENT_BUF_SIZE]; + int plain_len = html_to_text(content_buf + body_off, content_len - body_off, plain, CONTENT_BUF_SIZE); - /* 居中图标 */ - int icon_cx = content_x + content_w / 2 - 32; - int icon_cy = content_y + 24; - da_fill_rounded_rect(&g_ac, icon_cx, icon_cy, 64, 48, DA_BG_TERTIARY, 8); - /* 地球图标简化:圆圈 */ - int cx = icon_cx + 32, cy = icon_cy + 24, r = 16; - for (int dy = -r; dy <= r; dy++) - for (int dx = -r; dx <= r; dx++) - if (dx*dx+dy*dy <= r*r && dx*dx+dy*dy > (r-1)*(r-1)) - da_pixel(&g_ac, cx+dx, cy+dy, DA_ACCENT); - /* 横线 */ - for (int dx = -r; dx <= r; dx++) - if (dx*dx + 0 <= r*r) - da_pixel(&g_ac, cx+dx, cy, DA_ACCENT); - /* 竖线 */ - for (int dy = -r; dy <= r; dy++) - if (0 + dy*dy <= r*r) - da_pixel(&g_ac, cx, cy+dy, DA_ACCENT); - - /* 提示文字 */ - da_draw_string(&g_ac, "Deshab Browser", - content_x + content_w / 2 - 7 * CHAR_STEP, icon_cy + 60, - DA_TEXT_PRIMARY, DA_BG_SECONDARY, CHAR_STEP); - da_draw_string(&g_ac, "HTTP + HTML rendering required", - content_x + content_w / 2 - 15 * CHAR_STEP, icon_cy + 82, - DA_TEXT_DIM, DA_BG_SECONDARY, CHAR_STEP); - da_draw_string(&g_ac, "Press Esc to exit", - content_x + content_w / 2 - 9 * CHAR_STEP, icon_cy + 104, - DA_TEXT_DIM, DA_BG_SECONDARY, CHAR_STEP); + /* 按行显示 */ + int visible_cols = (content_w - 16) / CHAR_STEP; + int visible_rows = (content_h - 8) / CHAR_H; + int y = content_y + 4; + int pos = 0; + int line = 0; + /* 跳过 scroll 行 */ + int skip = content_scroll; + while (pos < plain_len && skip > 0) { + if (plain[pos] == '\n') skip--; + pos++; + } + while (pos < plain_len && line < visible_rows) { + int ls = pos; + while (pos < plain_len && plain[pos] != '\n' && pos - ls < visible_cols) pos++; + int llen = pos - ls; + for (int i = 0; i < llen; i++) { + char c = (char)plain[ls + i]; + if (c < ' ' || c > '~') c = '.'; + char buf[2]; buf[0] = c; buf[1] = 0; + da_draw_string(&g_ac, buf, content_x + 8 + i * CHAR_STEP, y, DA_TEXT_PRIMARY, DA_BG_SECONDARY, CHAR_STEP); + } + y += CHAR_H; + line++; + if (pos < plain_len && plain[pos] == '\n') pos++; + } + } else if (!fetching) { + const char *empty = "No content. Enter URL and press Enter to fetch."; + da_draw_string(&g_ac, empty, content_x + 16, content_y + 16, DA_TEXT_DIM, DA_BG_SECONDARY, CHAR_STEP); + } /* 状态栏 */ - da_draw_statusbar(&g_ac, "Browser | Stub - No network stack", (i64)g_ac.fb_w, (i64)g_ac.fb_h); + char status[96]; int p = 0; + const char *s1 = "Browser | "; while (*s1 && p < 95) status[p++] = *s1++; + for (int i = 0; status_msg[i] && p < 95; i++) status[p++] = status_msg[i]; + status[p] = 0; + da_draw_statusbar(&g_ac, status, (i64)g_ac.fb_w, (i64)g_ac.fb_h); +} - /* 鼠标光标 */ +static void redraw_all(void) { + redraw_all_imm(); da_cursor_save(&g_ac, &g_cursor); da_cursor_draw(&g_ac, &g_cursor, DA_CURSOR_COLOR); } +/* ---- net_stack.h 的 ns_init 需要 dkm_kernel_api ---- */ +/* 重写 ns_init 以从 da_app_context 获取 kernel_api */ +static int br_ns_init(void) { + if (ns_ready) return 0; + u64 dkm_kernel_api = (u64)g_ac.kernel_api; + if (!dkm_kernel_api) return -1; + return ns_init(dkm_kernel_api); +} + __attribute__((visibility("default"))) void dsk_entry(const da_boot_context *ctx) { __asm__ volatile("cli"); @@ -100,25 +279,85 @@ void dsk_entry(const da_boot_context *ctx) { da_init(&g_ac, ctx); da_cursor_init(&g_cursor, (i64)g_ac.fb_w, (i64)g_ac.fb_h); da_mouse_init(); + + /* 初始化网络协议栈 */ + br_ns_init(); + + br_set_status("Ready"); redraw_all(); + int e0 = 0, shift = 0; for (;;) { int need_redraw = 0; /* 鼠标轮询 */ - if (da_mouse_poll(&g_mouse, &g_cursor, (i64)g_ac.fb_w, (i64)g_ac.fb_h)) + if (da_mouse_poll(&g_mouse, &g_cursor, (i64)g_ac.fb_w, (i64)g_ac.fb_h)) { need_redraw = 1; + /* 点击地址栏聚焦 */ + int bar_y = DA_TITLEBAR_H + 6; + int bar_h = 28; + if (g_cursor.btn && + g_cursor.my >= bar_y && g_cursor.my < bar_y + bar_h && + g_cursor.mx >= 12 && g_cursor.mx < (int)g_ac.fb_w - 12) { + url_focused = 1; + need_redraw = 1; + } + } /* 键盘轮询 */ u8 st = inb(0x64); if ((st & 1) && !(st & 0x20)) { u8 data = inb(0x60); - if (!(data & 0x80) && data == 0x01) { + u8 sc = data; + if (sc == 0xE0) { e0 = 1; goto skip; } + if (sc == 0x2A || sc == 0x36) { shift = 1; goto skip; } + if (sc == 0xAA || sc == 0xB6) { shift = 0; goto skip; } + if (sc & 0x80) { e0 = 0; goto skip; } + + if (sc == 0x01) { /* Esc */ da_slog("browser", "exit"); return; } - } + /* 地址栏输入模式 */ + if (url_focused) { + if (sc == 0x1C) { /* Enter - 发起请求 */ + url_focused = 0; + fetching = 1; + br_set_status("Fetching..."); + redraw_all(); + do_fetch(); + fetching = 0; + need_redraw = 1; + goto skip; + } + if (sc == 0x0E) { /* Backspace */ + if (url_len > 0) { url_len--; url_buf[url_len] = 0; } + need_redraw = 1; + goto skip; + } + char c = da_scan_to_ascii(sc, shift); + if (c && c >= 32 && c <= 126 && url_len < URL_BUF_SIZE - 1) { + /* 小写转大写便于 URL 输入(除协议头) */ + url_buf[url_len++] = c; + url_buf[url_len] = 0; + need_redraw = 1; + } + goto skip; + } + + /* 内容浏览模式 */ + if (e0) { + if (sc == 0x48) { if (content_scroll > 0) content_scroll--; need_redraw = 1; } + else if (sc == 0x50) { content_scroll++; need_redraw = 1; } + e0 = 0; + goto skip; + } + if (sc == 0x49) { content_scroll -= 10; if (content_scroll < 0) content_scroll = 0; need_redraw = 1; } + else if (sc == 0x51) { content_scroll += 10; need_redraw = 1; } + else if (sc == 0x1C) { url_focused = 1; need_redraw = 1; } /* Enter 回到地址栏 */ + } + skip: if (need_redraw) { da_cursor_restore(&g_ac, &g_cursor); redraw_all(); diff --git a/CODE/tools/desktop_app.h b/CODE/tools/desktop_app.h index 82cada8..5a4ac60 100644 --- a/CODE/tools/desktop_app.h +++ b/CODE/tools/desktop_app.h @@ -40,8 +40,9 @@ typedef struct { u64 kernel_stack_top; u64 reserved[8]; } da_boot_context; -/* block_read 函数类型 */ -typedef int (*da_block_read_fn)(void *ctx, u64 lba, u32 count, void *buffer); +/* block 设备函数类型(与 shell/fat32_io.h 一致:u32 index 固定 0) */ +typedef int (*da_block_read_fn)(u32 index, u64 lba, u32 count, void *buffer); +typedef int (*da_block_write_fn)(u32 index, u64 lba, u32 count, const void *buffer); /* 应用上下文 — 从 dsk_boot_context 提取的常用数据 */ typedef struct { @@ -49,22 +50,27 @@ typedef struct { u64 fb_w; /* 宽度 */ u64 fb_h; /* 高度 */ u64 fb_pitch; /* 行宽(字节) */ - da_block_read_fn block_read; + da_block_read_fn block_read; + da_block_write_fn block_write; const void *kernel_api; } da_app_context; -/* 从 dsk_boot_context 初始化 da_app_context */ +/* 从 dsk_boot_context 初始化 da_app_context。 + * block API 从 kernel_api + 0xA8 获取:read @ +0x10,write @ +0x18。 */ static inline void da_init(da_app_context *ac, const da_boot_context *ctx) { ac->fb = (u32 *)ctx->framebuffer_address; ac->fb_w = ctx->framebuffer_width; ac->fb_h = ctx->framebuffer_height; ac->fb_pitch = ctx->framebuffer_pitch; ac->kernel_api = (const void *)ctx->dkm_kernel_api; - /* block_read 从 kernel_api + 0xA8 获取 */ + ac->block_read = 0; + ac->block_write = 0; u64 api = ctx->dkm_kernel_api; - ac->block_read = (da_block_read_fn)*(u64 *)(api + 0xA8); - if (ac->block_read) - ac->block_read = (da_block_read_fn)*(u64 *)((u64)ac->block_read + 16); + u64 blk_api = *(u64 *)(api + 0xA8); + if (blk_api) { + ac->block_read = (da_block_read_fn)*(u64 *)(blk_api + 0x10); + ac->block_write = (da_block_write_fn)*(u64 *)(blk_api + 0x18); + } } /* ========== 帧缓冲绘制 ========== */ @@ -285,10 +291,12 @@ static inline int da_mouse_poll(da_mouse *m, da_cursor *c, i64 fb_w, i64 fb_h) { if (m->idx < 3) return 0; m->idx = 0; if (!(m->buf[0] & 0x08)) return 0; + /* PS/2 鼠标包:buf[1]/buf[2] 已是 9 位补码的低 8 位, + * (signed char) 强转即可正确符号扩展,无需再依据 buf[0] 的 sign bit 反转。 + * X 直接相加;Y 需翻转一次(PS/2 Y 向上为正,屏幕 Y 向下为正)。 + * 与 desktop/main.c:ps2_mouse_poll 保持一致。 */ int dx = (int)(signed char)m->buf[1]; int dy = (int)(signed char)m->buf[2]; - if (m->buf[0] & 0x10) dx = -dx; - if (!(m->buf[0] & 0x20)) dy = -dy; else dy = -dy; dy = -dy; c->mx += dx; c->my += dy; if (c->mx < 0) c->mx = 0; diff --git a/CODE/tools/editor/main.c b/CODE/tools/editor/main.c index fdb863a..866852c 100644 --- a/CODE/tools/editor/main.c +++ b/CODE/tools/editor/main.c @@ -1,14 +1,18 @@ /* Deshab Editor — 全屏文本编辑器 * PS/2 键盘输入,行号 + 光标 + 垂直滚动。 - * Ctrl+O 打开文件,Ctrl+S 保存(暂不支持),Esc 退出。 + * Ctrl+O 打开文件(输入 8.3 名,从 FAT32 根目录读取) + * Ctrl+S 保存文件(输入 8.3 名,写入 FAT32 根目录) + * Esc 退出。 */ #include "../../firstInit/ascii_bitmaps.c" #include "../desktop_app.h" +#include "../fat32_io.h" #define CHAR_STEP 12 #define CHAR_H 18 #define EDITOR_BUF_SIZE 65536 +#define INPUT_BUF_SIZE 16 static da_app_context g_ac; @@ -22,6 +26,11 @@ static int modified = 0; static char filename[64] = "untitled"; static int ctrl_down = 0; +/* 输入模式:Ctrl+O/Ctrl+S 时进入,在状态栏输入文件名 */ +static int input_mode = 0; /* 0=编辑, 1=open, 2=save */ +static char input_buf[INPUT_BUF_SIZE]; +static int input_len = 0; + static int line_start(int line) { int l = 0; for (int i = 0; i < text_len; i++) { @@ -47,6 +56,63 @@ static void init_default_text(void) { recalc_cursor(); } +/* ---- 文件打开/保存 ---- */ +static void do_open(void) { + if (input_len == 0) return; + char name11[11]; + if (f32_name_to_83(input_buf, name11) != 0) { + da_slog("editor", "open: bad name"); + return; + } + u8 *data = 0; u32 size = 0; + if (f32_read_root_file(name11, &data, &size) != 0) { + da_slog("editor", "open: read failed"); + return; + } + if (size > EDITOR_BUF_SIZE) size = EDITOR_BUF_SIZE; + text_len = (int)size; + for (int i = 0; i < text_len; i++) text_buf[i] = (char)data[i]; + cursor_pos = 0; + modified = 0; + /* 更新 filename 显示 */ + int p = 0; + for (int i = 0; i < input_len && p < 60; i++) filename[p++] = input_buf[i]; + filename[p] = 0; + recalc_cursor(); + da_slog("editor", "open: ok"); +} + +static void do_save(void) { + if (input_len == 0) return; + char name11[11]; + if (f32_name_to_83(input_buf, name11) != 0) { + da_slog("editor", "save: bad name"); + return; + } + if (f32_write_root_file(name11, (const u8 *)text_buf, (u32)text_len) != 0) { + da_slog("editor", "save: write failed"); + return; + } + modified = 0; + int p = 0; + for (int i = 0; i < input_len && p < 60; i++) filename[p++] = input_buf[i]; + filename[p] = 0; + da_slog("editor", "save: ok"); +} + +static void enter_input_mode(int mode) { + input_mode = mode; + input_len = 0; + input_buf[0] = 0; +} + +static void commit_input(void) { + int mode = input_mode; + input_mode = 0; + if (mode == 1) do_open(); + else if (mode == 2) do_save(); +} + static void redraw_all(void) { da_fill_bg(&g_ac, DA_BG_SECONDARY); char title[80]; @@ -74,16 +140,13 @@ static void redraw_all(void) { for (; pos <= text_len && line < scroll_y + visible_rows; ) { if (line >= scroll_y) { int screen_y = top_y + (line - scroll_y) * CHAR_H; - /* 行号 */ char ln[8]; ln[0] = '0' + ((line+1)/100)%10; ln[1] = '0' + ((line+1)/10)%10; ln[2] = '0' + (line+1)%10; ln[3] = 0; da_draw_string(&g_ac, ln, margin_x, screen_y, DA_TEXT_DIM, DA_BG_SECONDARY, CHAR_STEP); - /* 分隔线 */ da_fill_rect(&g_ac, margin_x + line_num_w - 2, screen_y, 1, CHAR_H, DA_BORDER); - /* 文本 */ int col = 0; while (pos < text_len && text_buf[pos] != '\n' && col < visible_cols) { da_draw_char(&g_ac, text_buf[pos], @@ -97,25 +160,35 @@ static void redraw_all(void) { if (pos < text_len && text_buf[pos] == '\n') pos++; line++; if (pos >= text_len) { - /* 空行也画光标 */ if (line > cursor_line) break; } } - /* 光标 */ - if (cursor_line >= scroll_y && cursor_line < scroll_y + visible_rows) { + /* 光标(编辑模式) */ + if (!input_mode && cursor_line >= scroll_y && cursor_line < scroll_y + visible_rows) { int cy = top_y + (cursor_line - scroll_y) * CHAR_H; int cx = text_x + cursor_col * CHAR_STEP; da_fill_rect(&g_ac, cx, cy, 2, CHAR_H, DA_ACCENT_LIGHT); } /* 状态栏 */ - char status[64]; p = 0; - const char *s1 = "L:"; while (*s1) status[p++] = *s1++; - status[p++] = '0' + (cursor_line+1)/10%10; status[p++] = '0' + (cursor_line+1)%10; - const char *s2 = " C:"; while (*s2) status[p++] = *s2++; - status[p++] = '0' + (cursor_col+1)/10%10; status[p++] = '0' + (cursor_col+1)%10; - if (modified) { const char *m = " *"; while (*m) status[p++] = *m++; } + char status[96]; p = 0; + if (input_mode) { + const char *prompt = (input_mode == 1) ? "Open: " : "Save: "; + while (*prompt) status[p++] = *prompt++; + for (int i = 0; i < input_len; i++) status[p++] = input_buf[i]; + status[p++] = '_'; + const char *hint = " Enter:confirm Esc:cancel"; + while (*hint && p < 95) status[p++] = *hint++; + } else { + const char *s1 = "L:"; while (*s1) status[p++] = *s1++; + status[p++] = '0' + (cursor_line+1)/10%10; status[p++] = '0' + (cursor_line+1)%10; + const char *s2 = " C:"; while (*s2) status[p++] = *s2++; + status[p++] = '0' + (cursor_col+1)/10%10; status[p++] = '0' + (cursor_col+1)%10; + if (modified) { const char *m = " *"; while (*m) status[p++] = *m++; } + const char *s3 = " Ctrl+O:Open Ctrl+S:Save Esc:Exit"; + while (*s3 && p < 95) status[p++] = *s3++; + } status[p] = 0; da_draw_statusbar(&g_ac, status, (i64)g_ac.fb_w, (i64)g_ac.fb_h); } @@ -131,6 +204,7 @@ void dsk_entry(const da_boot_context *ctx) { } da_init(&g_ac, ctx); + f32_init((f32_block_read_fn)g_ac.block_read, (f32_block_write_fn)g_ac.block_write); init_default_text(); redraw_all(); @@ -148,6 +222,36 @@ void dsk_entry(const da_boot_context *ctx) { if (sc == 0x9D) { ctrl_down = 0; continue; } if (sc & 0x80) { e0 = 0; continue; } + /* Esc:输入模式取消 / 编辑模式退出 */ + if (sc == 0x01 && !e0) { + if (input_mode) { input_mode = 0; redraw_all(); continue; } + da_slog("editor", "exit"); + return; + } + + /* ---- 输入模式:输入文件名 ---- */ + if (input_mode) { + if (sc == 0x1C) { /* Enter - 确认 */ + commit_input(); + redraw_all(); + continue; + } + if (sc == 0x0E) { /* Backspace */ + if (input_len > 0) input_len--; + input_buf[input_len] = 0; + redraw_all(); + continue; + } + char c = da_scan_to_ascii(sc, shift); + if (c && c >= 32 && c <= 126 && input_len < INPUT_BUF_SIZE - 1) { + input_buf[input_len++] = c; + input_buf[input_len] = 0; + redraw_all(); + } + continue; + } + + /* ---- 编辑模式 ---- */ /* Esc 退出 */ if (sc == 0x01 && !e0) { da_slog("editor", "exit"); return; } @@ -212,16 +316,16 @@ void dsk_entry(const da_boot_context *ctx) { modified = 1; } } else { - /* Ctrl+O: 打开文件(提示暂不支持) */ + /* Ctrl+O: 打开文件 */ if (ctrl_down && (sc == 0x18)) { - /* Ctrl+O - stub */ - da_slog("editor", "Ctrl+O - open not yet"); + enter_input_mode(1); + redraw_all(); continue; } - /* Ctrl+S: 保存(提示暂不支持) */ + /* Ctrl+S: 保存文件 */ if (ctrl_down && (sc == 0x1F)) { - da_slog("editor", "Ctrl+S - save not yet"); - modified = 0; + enter_input_mode(2); + redraw_all(); continue; } char c = da_scan_to_ascii(sc, shift); diff --git a/CODE/tools/fat32_io.h b/CODE/tools/fat32_io.h new file mode 100644 index 0000000..653a3c4 --- /dev/null +++ b/CODE/tools/fat32_io.h @@ -0,0 +1,557 @@ +/* fat32_io.h — Deshab 用户态共享 FAT32 读写库(static inline,无库) + * + * 前置条件:包含者已定义 u8/u16/u32/u64/i64 基础类型。 + * 通过 f32_init() 注入 block_read/block_write 函数指针(来自 dkm_kernel_api + 0xA8)。 + * + * 能力:FAT32 根目录文件的读/写/删/列举,支持 FAT 链跟随、多簇文件。 + * 限制:仅根目录(不支持子目录,Phase 2.2 fileman 将扩展);单文件最大 256KB。 + * + * 用法: + * f32_init(block_read, block_write) — 初始化(block_write 可为 NULL,仅读场景) + * f32_name_to_83("file.txt", name11) — "file.txt" → 11 字符 8.3 名 + * f32_read_root_file(name11, &data, &size) — 读根目录文件到 f32_data 缓冲 + * f32_write_root_file(name11, data, size) — 写/替换根目录文件 + * f32_delete_root_file(name11) — 删除根目录文件 + * f32_list_root(emit_cb, user_data) — 列举根目录 + * + * 缓冲区:每个包含本头的 .elf 独立持有 256KB g_disk + 4KB g_cluster + 256KB g_fdata(共 ~516KB BSS)。 + * 与 net_stack.h 同模式,接受 static 缓冲的代价以换取无库依赖。 + */ + +#ifndef DESHAB_FAT32_IO_H +#define DESHAB_FAT32_IO_H + +/* ---- block 设备函数类型(与 shell/DSK 一致:index 参数固定 0) ---- */ +typedef int (*f32_block_read_fn)(u32 index, u64 lba, u32 count, void *buf); +typedef int (*f32_block_write_fn)(u32 index, u64 lba, u32 count, const void *buf); + +static f32_block_read_fn f32_blk_read = 0; +static f32_block_write_fn f32_blk_write = 0; + +/* ---- FAT32 BPB / 目录项结构(packed,与 DSK/shell 一致) ---- */ +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; +} f32_bpb; + +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; +} f32_dirent; + +/* ---- 内部小工具 ---- */ +static inline u32 f32_r32(const u8 *p) { return (u32)p[0]|((u32)p[1]<<8)|((u32)p[2]<<16)|((u32)p[3]<<24); } +static inline u16 f32_r16(const u8 *p) { return (u16)p[0]|((u16)p[1]<<8); } +static inline int f32_neq11(const char *a, const char *b) { + for (int i = 0; i < 11; i++) if (a[i] != b[i]) return 0; + return 1; +} + +/* ---- 批量读写缓冲区(与 DSK/shell 对齐:256 扇区覆盖 BPB+FAT+根目录+小文件) ---- */ +static u8 f32_disk[131072]; /* 256 扇区 BPB+FAT+根目录缓存 */ +static u8 f32_cluster[4096]; /* 单簇缓冲 */ +static u8 f32_data[262144]; /* 文件数据缓冲(最大 256KB) */ +static int f32_disk_loaded = 0; /* f32_disk 是否已加载 BPB+FAT */ + +/* 初始化:注入 block_read/block_write 函数指针 */ +static inline void f32_init(f32_block_read_fn rd, f32_block_write_fn wr) { + f32_blk_read = rd; + f32_blk_write = wr; + f32_disk_loaded = 0; +} + +static inline int f32_read_sectors(u32 lba, u32 count, u8 *out) { + return f32_blk_read ? f32_blk_read(0, lba, count, out) : -1; +} +static inline int f32_write_sectors(u32 lba, u32 count, const u8 *buf) { + return f32_blk_write ? f32_blk_write(0, lba, count, buf) : -1; +} + +/* 确保 f32_disk 已加载 BPB+FAT+根目录(256 扇区) */ +static inline int f32_disk_load(void) { + if (f32_disk_loaded) return 0; + if (f32_read_sectors(0, 256, f32_disk) != 0) return -1; + const f32_bpb *bpb = (const f32_bpb *)f32_disk; + if (bpb->boot_sig != 0xAA55 || bpb->bps != 512) return -2; + if (bpb->spf == 0 || bpb->root_clus < 2) return -3; + f32_disk_loaded = 1; + return 0; +} + +/* ---- 8.3 名字转换 ---- */ + +/* "file.txt" / "NAME" → 11 字符 8.3 名(大写,空格填充) + * 返回 0 成功,-1 失败(过长或非法字符) */ +static inline int f32_name_to_83(const char *in, char out[11]) { + for (int i = 0; i < 11; i++) out[i] = ' '; + int inlen = 0; + while (in[inlen] && inlen < 13) inlen++; + if (inlen == 0 || inlen > 12) return -1; + int dot = -1; + for (int i = 0; i < inlen; i++) if (in[i] == '.') dot = i; + int base_end = (dot >= 0) ? dot : inlen; + if (base_end == 0 || base_end > 8) return -1; + for (int j = 0; j < base_end; j++) { + char c = in[j]; + if (c >= 'a' && c <= 'z') c -= 32; + if (c == '.' || c == ' ' || c == '/' || c == '\\') return -1; + out[j] = c; + } + if (dot >= 0) { + int extlen = inlen - dot - 1; + if (extlen > 3) return -1; + for (int j = 0; j < extlen; j++) { + char c = in[dot + 1 + j]; + if (c >= 'a' && c <= 'z') c -= 32; + if (c == '.' || c == ' ' || c == '/' || c == '\\') return -1; + out[8 + j] = c; + } + } + return 0; +} + +/* 11 字符 8.3 名 → 可显示字符串(如 "README.TXT") */ +static inline void f32_name_from_83(const char in[11], char out[13]) { + int p = 0; + for (int j = 0; j < 8; j++) { + if (in[j] == ' ') break; + out[p++] = in[j]; + } + if (in[8] != ' ') { + out[p++] = '.'; + for (int j = 8; j < 11; j++) { + if (in[j] == ' ') break; + out[p++] = in[j]; + } + } + out[p] = 0; +} + +/* ---- 根目录文件查找 ---- */ + +/* 在根目录簇链中查找 11 字符名,输出首簇号与文件大小 */ +static inline int f32_find_in_root(const u8 *clus, u32 clus_sectors, const char *target, + u32 *out_clus, u32 *out_size, u32 *out_idx) { + const f32_dirent *dir = (const f32_dirent *)clus; + for (u32 e = 0; e * 32 < clus_sectors * 512; e++) { + if (dir[e].name[0] == 0) break; + if ((u8)dir[e].name[0] == 0xE5) continue; + if (dir[e].attr == 0x0F) continue; + if (dir[e].attr & 0x08) continue; + if (f32_neq11(dir[e].name, target)) { + *out_clus = (u32)f32_r16((const u8*)&dir[e].clow) | ((u32)f32_r16((const u8*)&dir[e].chigh) << 16); + *out_size = dir[e].fsize; + if (out_idx) *out_idx = e; + return 0; + } + } + return -1; +} + +/* 读取根目录下指定 8.3 名文件到 f32_data。 + * 返回 0 成功,*out_data 指向 f32_data,*out_size 为字节数;非 0 失败。 */ +static inline int f32_read_root_file(const char *name11, u8 **out_data, u32 *out_size) { + if (f32_disk_load() != 0) return -1; + const f32_bpb *bpb = (const f32_bpb *)f32_disk; + u32 data_lba = bpb->rsvd + (u32)bpb->fc * bpb->spf; + u32 fat_byte_off = bpb->rsvd * 512; + u32 spc = bpb->spc; + + u32 clus = bpb->root_clus; + u32 found_clus = 0, found_size = 0; + int found = 0; + while (clus >= 2 && clus < 0x0FFFFFF8 && !found) { + u32 lba = data_lba + (clus - 2) * spc; + const u8 *cb; + if ((u64)lba * 512 + (u64)spc * 512 <= 256ULL * 512) + cb = f32_disk + (u64)lba * 512; + else { + if (f32_read_sectors(lba, 8, f32_cluster) != 0) return -4; + cb = f32_cluster; + } + if (f32_find_in_root(cb, spc, name11, &found_clus, &found_size, 0) == 0) { found = 1; break; } + u32 fo = fat_byte_off + clus * 4; + if (fo + 4 > sizeof(f32_disk)) break; + clus = f32_r32(f32_disk + fo) & 0x0FFFFFFF; + } + if (!found) return -5; + if (found_size > sizeof(f32_data)) return -6; + + u8 *dst = f32_data; u32 remaining = found_size; u32 fc = found_clus; + while (fc >= 2 && fc < 0x0FFFFFF8 && remaining > 0) { + u32 fc_lba = data_lba + (fc - 2) * spc; + u32 fc_bytes = spc * 512; + if (fc_bytes > remaining) fc_bytes = remaining; + const u8 *fb; + if ((u64)fc_lba * 512 + (u64)spc * 512 <= 256ULL * 512) + fb = f32_disk + (u64)fc_lba * 512; + else { + if (f32_read_sectors(fc_lba, 8, f32_cluster) != 0) return -7; + fb = f32_cluster; + } + for (u32 b = 0; b < fc_bytes; b++) dst[b] = fb[b]; + dst += fc_bytes; remaining -= fc_bytes; + u32 fo = fat_byte_off + fc * 4; + if (fo + 4 > sizeof(f32_disk)) break; + fc = f32_r32(f32_disk + fo) & 0x0FFFFFFF; + } + *out_data = f32_data; *out_size = found_size; + return 0; +} + +/* 写入根目录下指定 8.3 名文件(存在则替换,不存在则新建)。 + * 依赖 f32_disk 已加载 BPB+FAT。返回 0 成功。 */ +static inline int f32_write_root_file(const char *name11, const u8 *data, u32 size) { + if (f32_disk_load() != 0) return -1; + if (!f32_blk_write) return -10; + /* 写入前重新加载 BPB+FAT,保证与盘上一致 */ + if (f32_read_sectors(0, 256, f32_disk) != 0) return -2; + const f32_bpb *bpb = (const f32_bpb *)f32_disk; + u32 spc = bpb->spc; + u32 fat_lba = bpb->rsvd; + u32 fat_sectors = bpb->spf; + u32 data_lba = bpb->rsvd + (u32)bpb->fc * bpb->spf; + u32 fat_byte_off = bpb->rsvd * 512; + u32 root_clus = bpb->root_clus; + u32 cluster_bytes = spc * 512; + + u32 root_lba = data_lba + (root_clus - 2) * spc; + u8 *root_buf = f32_disk + (u64)root_lba * 512; + u32 max_entries = cluster_bytes / 32; + f32_dirent *dir = (f32_dirent *)root_buf; + int free_entry = -1; + u32 existing_clus = 0; + int found = 0; + u32 existing_idx = 0; + for (u32 e = 0; e < max_entries; e++) { + if (dir[e].name[0] == 0) { if (free_entry < 0) free_entry = (int)e; break; } + if ((u8)dir[e].name[0] == 0xE5) { if (free_entry < 0) free_entry = (int)e; continue; } + if (dir[e].attr == 0x0F) continue; + if (dir[e].attr & 0x08) continue; + if (f32_neq11(dir[e].name, name11)) { + existing_clus = (u32)f32_r16((const u8*)&dir[e].clow) | ((u32)f32_r16((const u8*)&dir[e].chigh) << 16); + found = 1; existing_idx = e; + if (free_entry < 0) free_entry = (int)e; + break; + } + } + + u32 bytes_needed = size > 0 ? size : 1; + u32 clusters_needed = (bytes_needed + cluster_bytes - 1) / cluster_bytes; + u32 first_clus = 0; + u32 prev_clus = 0; + + if (found && existing_clus >= 2) { + first_clus = existing_clus; + u32 cur = existing_clus; + u32 count = 0; + while (cur >= 2 && cur < 0x0FFFFFF8 && count < clusters_needed) { + prev_clus = cur; + u32 fo = fat_byte_off + cur * 4; + if (fo + 4 > sizeof(f32_disk)) break; + cur = f32_r32(f32_disk + fo) & 0x0FFFFFFF; + count++; + } + while (count < clusters_needed) { + u32 newc = 0; + for (u32 c = 2; c < (fat_sectors * 512) / 4; c++) { + u32 fo = fat_byte_off + c * 4; + if (fo + 4 > sizeof(f32_disk)) break; + if ((f32_r32(f32_disk + fo) & 0x0FFFFFFF) == 0) { newc = c; break; } + } + if (newc == 0) return -3; + u32 pfo = fat_byte_off + prev_clus * 4; + if (pfo + 4 <= sizeof(f32_disk)) { + f32_disk[pfo] = (u8)(newc & 0xFF); + f32_disk[pfo+1] = (u8)((newc >> 8) & 0xFF); + f32_disk[pfo+2] = (u8)((newc >> 16) & 0xFF); + f32_disk[pfo+3] = (u8)((newc >> 24) & 0x0F); + } + prev_clus = newc; + count++; + } + if (prev_clus >= 2) { + u32 fo = fat_byte_off + prev_clus * 4; + if (fo + 4 <= sizeof(f32_disk)) { + f32_disk[fo] = 0xF8; f32_disk[fo+1] = 0xFF; f32_disk[fo+2] = 0xFF; f32_disk[fo+3] = 0x0F; + } + } + /* 释放多余旧簇 */ + u32 next = 0; + u32 fo = fat_byte_off + prev_clus * 4; + if (fo + 4 <= sizeof(f32_disk)) next = f32_r32(f32_disk + fo) & 0x0FFFFFFF; + while (next >= 2 && next < 0x0FFFFFF8) { + u32 nfo = fat_byte_off + next * 4; + u32 nn = 0; + if (nfo + 4 <= sizeof(f32_disk)) nn = f32_r32(f32_disk + nfo) & 0x0FFFFFFF; + f32_disk[nfo] = 0; f32_disk[nfo+1] = 0; f32_disk[nfo+2] = 0; f32_disk[nfo+3] = 0; + next = nn; + } + } else { + if (free_entry < 0) return -4; + for (u32 i = 0; i < clusters_needed; i++) { + u32 newc = 0; + for (u32 c = 2; c < (fat_sectors * 512) / 4; c++) { + u32 fo = fat_byte_off + c * 4; + if (fo + 4 > sizeof(f32_disk)) break; + if ((f32_r32(f32_disk + fo) & 0x0FFFFFFF) == 0) { newc = c; break; } + } + if (newc == 0) return -3; + if (i == 0) first_clus = newc; + if (prev_clus >= 2) { + u32 pfo = fat_byte_off + prev_clus * 4; + f32_disk[pfo] = (u8)(newc & 0xFF); + f32_disk[pfo+1] = (u8)((newc >> 8) & 0xFF); + f32_disk[pfo+2] = (u8)((newc >> 16) & 0xFF); + f32_disk[pfo+3] = (u8)((newc >> 24) & 0x0F); + } + prev_clus = newc; + } + if (prev_clus >= 2) { + u32 fo = fat_byte_off + prev_clus * 4; + f32_disk[fo] = 0xF8; f32_disk[fo+1] = 0xFF; f32_disk[fo+2] = 0xFF; f32_disk[fo+3] = 0x0F; + } + } + + /* 写数据到簇 */ + u32 remaining = size; + const u8 *src = data; + u32 cur = first_clus; + u32 ci = 0; + while (cur >= 2 && cur < 0x0FFFFFF8 && ci < clusters_needed) { + u32 clba = data_lba + (cur - 2) * spc; + u32 chunk = remaining < cluster_bytes ? remaining : cluster_bytes; + if ((u64)clba * 512 + cluster_bytes <= 256ULL * 512) { + u8 *dst = f32_disk + (u64)clba * 512; + for (u32 b = 0; b < chunk; b++) dst[b] = src[b]; + for (u32 b = chunk; b < cluster_bytes; b++) dst[b] = 0; + } else { + if (f32_read_sectors(clba, spc, f32_cluster) != 0) return -5; + for (u32 b = 0; b < chunk; b++) f32_cluster[b] = src[b]; + for (u32 b = chunk; b < cluster_bytes; b++) f32_cluster[b] = 0; + if (f32_write_sectors(clba, spc, f32_cluster) != 0) return -6; + } + src += chunk; remaining -= chunk; ci++; + u32 fo = fat_byte_off + cur * 4; + if (fo + 4 > sizeof(f32_disk)) break; + cur = f32_r32(f32_disk + fo) & 0x0FFFFFFF; + } + + /* 更新目录项 */ + u32 entry_idx = found ? existing_idx : (u32)free_entry; + f32_dirent *e = &dir[entry_idx]; + if (!found) { + for (int i = 0; i < 11; i++) e->name[i] = name11[i]; + e->attr = 0x20; e->ntr = 0; e->ctenth = 0; e->ctime = 0; e->cdate = 0; e->adate = 0; + } + e->chigh = (u16)((first_clus >> 16) & 0xFFFF); + e->clow = (u16)(first_clus & 0xFFFF); + e->fsize = size; + + /* 回写 FAT(两份)+ 根目录 + 缓冲区内的数据簇 */ + for (u32 f = 0; f < bpb->fc; f++) { + u32 flba = fat_lba + f * fat_sectors; + if (f32_write_sectors(flba, fat_sectors, f32_disk + (u64)flba * 512) != 0) return -7; + } + u32 root_dir_lba = data_lba + (root_clus - 2) * spc; + if ((u64)root_dir_lba * 512 + cluster_bytes <= 256ULL * 512) { + if (f32_write_sectors(root_dir_lba, spc, f32_disk + (u64)root_dir_lba * 512) != 0) return -8; + } + cur = first_clus; ci = 0; + while (cur >= 2 && cur < 0x0FFFFFF8 && ci < clusters_needed) { + u32 clba = data_lba + (cur - 2) * spc; + if ((u64)clba * 512 + cluster_bytes <= 256ULL * 512) { + if (f32_write_sectors(clba, spc, f32_disk + (u64)clba * 512) != 0) return -9; + } + u32 fo = fat_byte_off + cur * 4; + if (fo + 4 > sizeof(f32_disk)) break; + cur = f32_r32(f32_disk + fo) & 0x0FFFFFFF; + ci++; + } + f32_disk_loaded = 0; /* 下次操作重新加载,避免使用脏缓存 */ + return 0; +} + +/* 删除根目录下指定 8.3 名文件:清空簇链 + 标记目录项为 0xE5。 + * 返回 0 成功,-1 未找到,其他负值失败。 */ +static inline int f32_delete_root_file(const char *name11) { + if (f32_disk_load() != 0) return -2; + if (!f32_blk_write) return -10; + if (f32_read_sectors(0, 256, f32_disk) != 0) return -3; + const f32_bpb *bpb = (const f32_bpb *)f32_disk; + u32 spc = bpb->spc; + u32 fat_lba = bpb->rsvd; + u32 fat_sectors = bpb->spf; + u32 data_lba = bpb->rsvd + (u32)bpb->fc * bpb->spf; + u32 fat_byte_off = bpb->rsvd * 512; + u32 root_clus = bpb->root_clus; + + u32 root_lba = data_lba + (root_clus - 2) * spc; + u8 *root_buf = f32_disk + (u64)root_lba * 512; + u32 max_entries = (spc * 512) / 32; + f32_dirent *dir = (f32_dirent *)root_buf; + u32 found_idx = 0xFFFFFFFFu; + for (u32 e = 0; e < max_entries; e++) { + if (dir[e].name[0] == 0) break; + if ((u8)dir[e].name[0] == 0xE5) continue; + if (dir[e].attr == 0x0F) continue; + if (dir[e].attr & 0x08) continue; + if (f32_neq11(dir[e].name, name11)) { found_idx = e; break; } + } + if (found_idx == 0xFFFFFFFFu) return -1; + + /* 释放簇链 */ + u32 cur = (u32)f32_r16((const u8*)&dir[found_idx].clow) | ((u32)f32_r16((const u8*)&dir[found_idx].chigh) << 16); + while (cur >= 2 && cur < 0x0FFFFFF8) { + u32 fo = fat_byte_off + cur * 4; + u32 next = 0; + if (fo + 4 <= sizeof(f32_disk)) next = f32_r32(f32_disk + fo) & 0x0FFFFFFF; + f32_disk[fo] = 0; f32_disk[fo+1] = 0; f32_disk[fo+2] = 0; f32_disk[fo+3] = 0; + cur = next; + } + /* 标记目录项为已删除 */ + dir[found_idx].name[0] = (char)0xE5; + + /* 回写 FAT + 根目录 */ + for (u32 f = 0; f < bpb->fc; f++) { + u32 flba = fat_lba + f * fat_sectors; + if (f32_write_sectors(flba, fat_sectors, f32_disk + (u64)flba * 512) != 0) return -4; + } + if (f32_write_sectors(root_lba, spc, f32_disk + (u64)root_lba * 512) != 0) return -5; + f32_disk_loaded = 0; + return 0; +} + +/* 列举根目录条目回调签名:返回 0 继续,非 0 停止 */ +typedef int (*f32_list_cb)(const char *name, u32 size, u8 attr, void *user_data); + +/* 列出根目录所有文件/目录,调用回调输出。 + * 回调返回非 0 立即停止。返回 0 成功(含回调提前停止),负值失败。 */ +static inline int f32_list_root(f32_list_cb emit, void *user_data) { + if (f32_disk_load() != 0) return -1; + const f32_bpb *bpb = (const f32_bpb *)f32_disk; + u32 data_lba = bpb->rsvd + (u32)bpb->fc * bpb->spf; + u32 fat_byte_off = bpb->rsvd * 512; + u32 spc = bpb->spc; + u32 clus = bpb->root_clus; + while (clus >= 2 && clus < 0x0FFFFFF8) { + u32 lba = data_lba + (clus - 2) * spc; + const u8 *cb; + if ((u64)lba * 512 + (u64)spc * 512 <= 256ULL * 512) + cb = f32_disk + (u64)lba * 512; + else { + if (f32_read_sectors(lba, 8, f32_cluster) != 0) return -4; + cb = f32_cluster; + } + const f32_dirent *dir = (const f32_dirent *)cb; + for (u32 e = 0; e * 32 < spc * 512; e++) { + if (dir[e].name[0] == 0) goto done; + if ((u8)dir[e].name[0] == 0xE5) continue; + if (dir[e].attr == 0x0F) continue; + if (dir[e].attr & 0x08) continue; + char disp[13]; + f32_name_from_83(dir[e].name, disp); + if (emit(disp, dir[e].fsize, dir[e].attr, user_data) != 0) goto done; + } + u32 fo = fat_byte_off + clus * 4; + if (fo + 4 > sizeof(f32_disk)) break; + clus = f32_r32(f32_disk + fo) & 0x0FFFFFFF; + } +done: + return 0; +} + +/* ---- 子目录遍历支持(Phase 2.2 fileman 扩展) ---- */ + +/* 目录条目(供 fileman 等工具使用) */ +typedef struct { + char name[13]; /* 8.3 显示名 */ + u32 clus; /* 首簇号 */ + u32 size; /* 文件大小(目录为 0) */ + u8 attr; + u8 is_dir; +} f32_entry; + +typedef int (*f32_list_entry_cb)(const f32_entry *e, void *user_data); + +/* 列举指定目录簇的内容。 + * dir_clus = 0 表示根目录(自动从 BPB 取 root_clus)。 + * 回调返回非 0 立即停止。返回 0 成功,负值失败。 */ +static inline int f32_list_dir(u32 dir_clus, f32_list_entry_cb emit, void *user_data) { + if (f32_disk_load() != 0) return -1; + const f32_bpb *bpb = (const f32_bpb *)f32_disk; + u32 data_lba = bpb->rsvd + (u32)bpb->fc * bpb->spf; + u32 fat_byte_off = bpb->rsvd * 512; + u32 spc = bpb->spc; + u32 clus = (dir_clus == 0) ? bpb->root_clus : dir_clus; + while (clus >= 2 && clus < 0x0FFFFFF8) { + u32 lba = data_lba + (clus - 2) * spc; + const u8 *cb; + if ((u64)lba * 512 + (u64)spc * 512 <= 256ULL * 512) + cb = f32_disk + (u64)lba * 512; + else { + if (f32_read_sectors(lba, 8, f32_cluster) != 0) return -4; + cb = f32_cluster; + } + const f32_dirent *dir = (const f32_dirent *)cb; + for (u32 e = 0; e * 32 < spc * 512; e++) { + if (dir[e].name[0] == 0) goto done; + if ((u8)dir[e].name[0] == 0xE5) continue; + if (dir[e].attr == 0x0F) continue; + if (dir[e].attr & 0x08) continue; + /* 跳过 "." 和 ".." 条目 */ + if (dir[e].name[0] == '.' && (dir[e].name[1] == ' ' || dir[e].name[1] == '.')) + continue; + f32_entry fe; + f32_name_from_83(dir[e].name, fe.name); + fe.clus = (u32)f32_r16((const u8*)&dir[e].clow) | ((u32)f32_r16((const u8*)&dir[e].chigh) << 16); + fe.size = dir[e].fsize; + fe.attr = dir[e].attr; + fe.is_dir = (dir[e].attr & 0x10) ? 1 : 0; + if (emit(&fe, user_data) != 0) goto done; + } + u32 fo = fat_byte_off + clus * 4; + if (fo + 4 > sizeof(f32_disk)) break; + clus = f32_r32(f32_disk + fo) & 0x0FFFFFFF; + } +done: + return 0; +} + +/* 按首簇号 + 大小读取文件内容到 out_buf。 + * out_buf 至少 size 字节容量。返回 0 成功,负值失败。 */ +static inline int f32_read_file_by_clus(u32 clus, u32 size, u8 *out_buf, u32 buf_cap) { + if (f32_disk_load() != 0) return -1; + if (size > buf_cap) size = buf_cap; + const f32_bpb *bpb = (const f32_bpb *)f32_disk; + u32 data_lba = bpb->rsvd + (u32)bpb->fc * bpb->spf; + u32 fat_byte_off = bpb->rsvd * 512; + u32 spc = bpb->spc; + u32 remaining = size; + u8 *dst = out_buf; + u32 fc = clus; + while (fc >= 2 && fc < 0x0FFFFFF8 && remaining > 0) { + u32 fc_lba = data_lba + (fc - 2) * spc; + u32 fc_bytes = spc * 512; + if (fc_bytes > remaining) fc_bytes = remaining; + const u8 *fb; + if ((u64)fc_lba * 512 + (u64)spc * 512 <= 256ULL * 512) + fb = f32_disk + (u64)fc_lba * 512; + else { + if (f32_read_sectors(fc_lba, 8, f32_cluster) != 0) return -7; + fb = f32_cluster; + } + for (u32 b = 0; b < fc_bytes; b++) dst[b] = fb[b]; + dst += fc_bytes; remaining -= fc_bytes; + u32 fo = fat_byte_off + fc * 4; + if (fo + 4 > sizeof(f32_disk)) break; + fc = f32_r32(f32_disk + fo) & 0x0FFFFFFF; + } + return 0; +} + +#endif /* DESHAB_FAT32_IO_H */ diff --git a/CODE/tools/fileman/main.c b/CODE/tools/fileman/main.c index 41b69be..ca8ff4e 100644 --- a/CODE/tools/fileman/main.c +++ b/CODE/tools/fileman/main.c @@ -1,80 +1,107 @@ /* Deshab File Manager — 全屏文件管理器 - * 文件列表视图,鼠标点击选择,键盘导航。 - * FAT32 根目录遍历,Esc 退出。 + * 接入 fat32_io.h 共享头,支持: + * - 子目录遍历(路径栈,Enter 进入 / Backspace 返回) + * - FAT 链跟随(多簇文件/目录) + * - 文件查看(Enter 打开文本文件,只读浏览,Esc 返回列表) + * - 鼠标点击选择 + 键盘导航 + * Esc 退出返回 DSK/desktop。 */ #include "../../firstInit/ascii_bitmaps.c" #include "../desktop_app.h" +#include "../fat32_io.h" #define CHAR_STEP 12 #define CHAR_H 18 #define MAX_FILES 256 +#define MAX_DEPTH 16 +#define VIEW_BUF 24576 /* 文件查看器缓冲 24KB */ static da_app_context g_ac; static da_cursor g_cursor; static da_mouse g_mouse; typedef struct { - char name[13]; /* 8.3 */ - int is_dir; + char name[13]; + u32 clus; u32 size; -} file_entry; + u8 is_dir; +} fm_entry; -static file_entry files[MAX_FILES]; +static fm_entry files[MAX_FILES]; static int file_count = 0; static int selected = 0; static int scroll_off = 0; -static void load_root_dir(void) { - file_count = 0; - if (!g_ac.block_read) return; +/* 路径栈:每层记录目录首簇(0=根)与显示名 */ +static u32 path_clus[MAX_DEPTH]; +static char path_name[MAX_DEPTH][13]; +static int path_depth = 0; /* 0 = 根目录 */ - u8 bpb[512]; - if (g_ac.block_read(0, 0, 1, bpb) != 0) return; - if (bpb[510] != 0x55 || bpb[511] != 0xAA) return; +/* 文件查看器状态 */ +static int view_mode = 0; +static u8 view_buf[VIEW_BUF]; +static u32 view_size = 0; +static int view_scroll = 0; - u32 root_cluster = *(u32 *)(bpb + 44); - u8 sectors_per_cluster = bpb[13]; - u32 fat_start = *(u16 *)(bpb + 14); - u32 data_start = fat_start + (*(u16 *)(bpb + 22)) * 2; - - u8 dir_buf[8192]; - u32 lba = data_start + (root_cluster - 2) * sectors_per_cluster; - if (g_ac.block_read(0, lba, sectors_per_cluster, dir_buf) != 0) return; - - for (int i = 0; i < (int)(sectors_per_cluster * 512) && file_count < MAX_FILES; i += 32) { - u8 first = dir_buf[i]; - if (first == 0x00) break; - if (first == 0xE5) continue; - if (dir_buf[i+11] & 0x08) continue; - if (dir_buf[i+11] & 0x10) continue; /* 暂跳过目录 */ - - file_entry *f = &files[file_count]; - f->is_dir = (dir_buf[i+11] & 0x10) ? 1 : 0; - f->size = *(u32 *)(dir_buf + i + 28); - int p = 0; - for (int j = 0; j < 8 && dir_buf[i+j] != ' '; j++) - f->name[p++] = (char)dir_buf[i+j]; - if (dir_buf[i+8] != ' ') { - f->name[p++] = '.'; - for (int j = 8; j < 11 && dir_buf[i+j] != ' '; j++) - f->name[p++] = (char)dir_buf[i+j]; - } - f->name[p] = 0; - file_count++; - } +/* ---- 目录列举回调 ---- */ +static int fm_collect_cb(const f32_entry *e, void *user_data) { + (void)user_data; + if (file_count >= MAX_FILES) return 1; + fm_entry *f = &files[file_count]; + for (int i = 0; i < 13; i++) f->name[i] = e->name[i]; + f->clus = e->clus; + f->size = e->size; + f->is_dir = e->is_dir; + file_count++; + return 0; } -static void redraw_all(void) { +static void load_current_dir(void) { + file_count = 0; + selected = 0; + scroll_off = 0; + u32 dir_clus = (path_depth == 0) ? 0 : path_clus[path_depth - 1]; + f32_list_dir(dir_clus, fm_collect_cb, 0); +} + +/* ---- 路径显示 ---- */ +static void build_path_str(char *out, int cap) { + int p = 0; + out[p++] = '/'; + for (int i = 0; i < path_depth && p < cap - 13; i++) { + const char *n = path_name[i]; + while (*n && p < cap - 2) out[p++] = *n++; + out[p++] = '/'; + } + out[p] = 0; +} + +/* ---- 数字转字符串 ---- */ +static void fm_u32_dec(char *buf, u32 v) { + char tmp[12]; int n = 0; + if (!v) { buf[0] = '0'; buf[1] = 0; return; } + while (v && n < 11) { tmp[n++] = '0' + (v % 10); v /= 10; } + for (int i = 0; i < n; i++) buf[i] = tmp[n - 1 - i]; + buf[n] = 0; +} + +/* ---- 列表视图绘制 ---- */ +static void redraw_list(void) { da_fill_bg(&g_ac, DA_BG_PRIMARY); da_draw_titlebar(&g_ac, "Files", (i64)g_ac.fb_w); + /* 当前路径 */ + char path_str[200]; + build_path_str(path_str, sizeof(path_str)); + da_draw_string(&g_ac, path_str, 12, DA_TITLEBAR_H + 4, DA_ACCENT, DA_BG_PRIMARY, CHAR_STEP); + int margin = 12; - int top_y = DA_TITLEBAR_H + 4; + int top_y = DA_TITLEBAR_H + 4 + CHAR_H + 4; int row_h = CHAR_H + 6; int visible_rows = ((int)g_ac.fb_h - top_y - DA_STATUSBAR_H - 8) / row_h; int col_name_x = margin + 32; - int col_size_x = (int)g_ac.fb_w - 120; + int col_size_x = (int)g_ac.fb_w - 160; int col_type_x = (int)g_ac.fb_w - 60; /* 列标题 */ @@ -83,9 +110,10 @@ static void redraw_all(void) { da_draw_string(&g_ac, "Type", col_type_x, top_y, DA_TEXT_DIM, DA_BG_PRIMARY, CHAR_STEP); da_fill_rect(&g_ac, margin, top_y + CHAR_H, (i64)g_ac.fb_w - margin * 2, 1, DA_BORDER); - /* 文件列表 */ + /* 滚动范围调整 */ if (scroll_off < 0) scroll_off = 0; - if (scroll_off > file_count - visible_rows && file_count > visible_rows) scroll_off = file_count - visible_rows; + if (file_count > visible_rows && scroll_off > file_count - visible_rows) + scroll_off = file_count - visible_rows; if (scroll_off < 0) scroll_off = 0; for (int i = 0; i < visible_rows; i++) { @@ -93,28 +121,23 @@ static void redraw_all(void) { if (idx >= file_count) break; int y = top_y + CHAR_H + 6 + i * row_h; - /* 选中高亮 */ if (idx == selected) { da_fill_rect(&g_ac, margin, y - 2, (i64)g_ac.fb_w - margin * 2, row_h, DA_BG_TERTIARY); da_rect_outline(&g_ac, margin, y - 2, (i64)g_ac.fb_w - margin * 2, row_h, DA_ACCENT, 4); } - /* 图标 */ u32 icon_color = files[idx].is_dir ? DA_WARNING : DA_ACCENT; da_fill_rounded_rect(&g_ac, margin + 4, y, 24, 16, icon_color, 3); - /* 文件名 */ u32 name_color = (idx == selected) ? DA_ACCENT_LIGHT : DA_TEXT_PRIMARY; da_draw_string(&g_ac, files[idx].name, col_name_x, y, name_color, (idx == selected) ? DA_BG_TERTIARY : DA_BG_PRIMARY, CHAR_STEP); - /* 大小 */ if (!files[idx].is_dir) { - char sz[16]; int p = 0; - u32 s = files[idx].size; - if (s == 0) { sz[p++] = '0'; } - else { char tmp[12]; int tl = 0; while(s>0){tmp[tl++]='0'+(s%10);s/=10;} for(int j=tl-1;j>=0;j--)sz[p++]=tmp[j]; } - sz[p++] = ' '; sz[p++] = 'B'; sz[p] = 0; + char sz[16]; + fm_u32_dec(sz, files[idx].size); + int sl = 0; while (sz[sl]) sl++; + sz[sl++] = ' '; sz[sl++] = 'B'; sz[sl] = 0; da_draw_string(&g_ac, sz, col_size_x, y, DA_TEXT_DIM, (idx == selected) ? DA_BG_TERTIARY : DA_BG_PRIMARY, CHAR_STEP); } else { @@ -122,7 +145,6 @@ static void redraw_all(void) { (idx == selected) ? DA_BG_TERTIARY : DA_BG_PRIMARY, CHAR_STEP); } - /* 类型 */ const char *type = files[idx].is_dir ? "DIR" : "FILE"; da_draw_string(&g_ac, type, col_type_x, y, DA_TEXT_DIM, (idx == selected) ? DA_BG_TERTIARY : DA_BG_PRIMARY, CHAR_STEP); @@ -134,18 +156,129 @@ static void redraw_all(void) { } /* 状态栏 */ - char status[64]; int p = 0; - const char *p1 = "/ "; while (*p1) status[p++] = *p1++; - status[p++] = '0' + file_count / 10; status[p++] = '0' + file_count % 10; - const char *p2 = " items"; while (*p2) status[p++] = *p2++; + char status[80]; int p = 0; + char nc[12]; fm_u32_dec(nc, (u32)file_count); + const char *s1 = "items: "; while (*s1) status[p++] = *s1++; + for (int i = 0; nc[i]; i++) status[p++] = nc[i]; + const char *s2 = " | Enter:open Backspace:up Esc:exit"; + while (*s2) status[p++] = *s2++; status[p] = 0; da_draw_statusbar(&g_ac, status, (i64)g_ac.fb_w, (i64)g_ac.fb_h); - /* 鼠标光标 */ da_cursor_save(&g_ac, &g_cursor); da_cursor_draw(&g_ac, &g_cursor, DA_CURSOR_COLOR); } +/* ---- 文件查看器绘制 ---- */ +static void redraw_view(void) { + da_fill_bg(&g_ac, DA_BG_PRIMARY); + + /* 标题栏显示文件名 */ + char title[32]; int p = 0; + const char *t = "View: "; + while (*t && p < 30) title[p++] = *t++; + if (selected >= 0 && selected < file_count) { + const char *fn = files[selected].name; + while (*fn && p < 30) title[p++] = *fn++; + } + title[p] = 0; + da_draw_titlebar(&g_ac, title, (i64)g_ac.fb_w); + + int margin = 12; + int top_y = DA_TITLEBAR_H + 4; + int row_h = CHAR_H + 2; + int visible_rows = ((int)g_ac.fb_h - top_y - DA_STATUSBAR_H - 8) / row_h; + + /* 按行显示文本内容 */ + int y = top_y; + u32 pos = 0; + int line = 0; + /* 跳过 view_scroll 行 */ + int skip = view_scroll; + while (pos < view_size && skip > 0) { + if (view_buf[pos] == '\n') { skip--; } + pos++; + } + while (pos < view_size && line < visible_rows) { + /* 提取一行 */ + int line_start = pos; + while (pos < view_size && view_buf[pos] != '\n' && pos - line_start < 80) pos++; + int line_len = pos - line_start; + /* 绘制字符 */ + for (int i = 0; i < line_len && i < 80; i++) { + char c = (char)view_buf[line_start + i]; + if (c == '\r') continue; + if (c < ' ' || c > '~') c = '.'; + char buf[2]; buf[0] = c; buf[1] = 0; + da_draw_string(&g_ac, buf, margin + i * CHAR_STEP, y, DA_TEXT_PRIMARY, DA_BG_PRIMARY, CHAR_STEP); + } + y += row_h; + line++; + if (pos < view_size && view_buf[pos] == '\n') pos++; + /* 处理 \r\n */ + if (pos < view_size && view_buf[pos] == '\r') pos++; + } + + /* 状态栏 */ + char status[80]; p = 0; + char sz[12]; fm_u32_dec(sz, view_size); + const char *s1 = "size: "; while (*s1) status[p++] = *s1++; + for (int i = 0; sz[i]; i++) status[p++] = sz[i]; + const char *s2 = "B | Up/Down:scroll Esc:back"; + while (*s2) status[p++] = *s2++; + status[p] = 0; + da_draw_statusbar(&g_ac, status, (i64)g_ac.fb_w, (i64)g_ac.fb_h); + + da_cursor_save(&g_ac, &g_cursor); + da_cursor_draw(&g_ac, &g_cursor, DA_CURSOR_COLOR); +} + +static void redraw_all(void) { + if (view_mode) redraw_view(); + else redraw_list(); +} + +/* ---- 打开文件/目录 ---- */ +static void open_selected(void) { + if (selected < 0 || selected >= file_count) return; + fm_entry *f = &files[selected]; + + if (f->is_dir) { + /* 进入子目录 */ + if (path_depth < MAX_DEPTH - 1) { + path_clus[path_depth] = f->clus; + for (int i = 0; i < 13; i++) path_name[path_depth][i] = f->name[i]; + path_name[path_depth][12] = 0; + path_depth++; + load_current_dir(); + } + return; + } + + /* 文件:读取内容进查看器 */ + if (f->size == 0) { + view_size = 0; + } else { + u32 rd = f->size; + if (rd > VIEW_BUF) rd = VIEW_BUF; + if (f32_read_file_by_clus(f->clus, f->size, view_buf, VIEW_BUF) != 0) { + da_slog("fileman", "read file failed"); + return; + } + view_size = rd; + } + view_mode = 1; + view_scroll = 0; +} + +/* ---- 返回上级目录 ---- */ +static void go_up(void) { + if (path_depth > 0) { + path_depth--; + load_current_dir(); + } +} + __attribute__((visibility("default"))) void dsk_entry(const da_boot_context *ctx) { __asm__ volatile("cli"); @@ -160,7 +293,11 @@ void dsk_entry(const da_boot_context *ctx) { da_cursor_init(&g_cursor, (i64)g_ac.fb_w, (i64)g_ac.fb_h); da_mouse_init(); - load_root_dir(); + /* 初始化 fat32_io:注入 block_read/block_write */ + f32_init((f32_block_read_fn)g_ac.block_read, (f32_block_write_fn)g_ac.block_write); + + path_depth = 0; + load_current_dir(); redraw_all(); int e0 = 0, shift = 0; (void)shift; @@ -170,10 +307,8 @@ void dsk_entry(const da_boot_context *ctx) { /* 鼠标轮询 */ if (da_mouse_poll(&g_mouse, &g_cursor, (i64)g_ac.fb_w, (i64)g_ac.fb_h)) { need_redraw = 1; - if (g_cursor.btn) { - /* 点击选择文件 */ - int margin = 12; - int top_y = DA_TITLEBAR_H + 4; + if (g_cursor.btn && !view_mode) { + int top_y = DA_TITLEBAR_H + 4 + CHAR_H + 4; int row_h = CHAR_H + 6; int click_y = g_cursor.my - top_y - CHAR_H - 6; if (click_y >= 0) { @@ -196,11 +331,36 @@ void dsk_entry(const da_boot_context *ctx) { if (sc == 0xAA || sc == 0xB6) { shift = 0; goto skip; } if (sc & 0x80) { e0 = 0; goto skip; } - if (sc == 0x01) { da_slog("fileman", "exit"); return; } + if (sc == 0x01) { /* Esc */ + if (view_mode) { + view_mode = 0; + need_redraw = 1; + } else { + da_slog("fileman", "exit"); + return; + } + goto skip; + } + if (view_mode) { + /* 查看器模式:Up/Down 滚动 */ + if (e0) { + if (sc == 0x48) { if (view_scroll > 0) view_scroll--; need_redraw = 1; } + else if (sc == 0x50) { view_scroll++; need_redraw = 1; } + e0 = 0; + goto skip; + } + /* PageUp/PageDown */ + if (sc == 0x49) { view_scroll -= 10; if (view_scroll < 0) view_scroll = 0; need_redraw = 1; } + else if (sc == 0x51) { view_scroll += 10; need_redraw = 1; } + goto skip; + } + + /* 列表模式 */ if (e0) { if (sc == 0x48) { /* Up */ if (selected > 0) selected--; + if (selected < scroll_off) scroll_off = selected; need_redraw = 1; } else if (sc == 0x50) { /* Down */ if (selected < file_count - 1) selected++; @@ -211,12 +371,17 @@ void dsk_entry(const da_boot_context *ctx) { } if (sc == 0x1C) { /* Enter - 打开 */ - if (selected >= 0 && selected < file_count) { - da_slog("fileman", "open file (stub)"); - } + open_selected(); need_redraw = 1; } else if (sc == 0x0E) { /* Backspace - 返回上级 */ - da_slog("fileman", "back (root only)"); + go_up(); + need_redraw = 1; + } else if (sc == 0x49) { /* PageUp */ + selected -= 10; if (selected < 0) selected = 0; + if (selected < scroll_off) scroll_off = selected; + need_redraw = 1; + } else if (sc == 0x51) { /* PageDown */ + selected += 10; if (selected >= file_count) selected = file_count - 1; need_redraw = 1; } } diff --git a/CODE/utsm-ipc/README.md b/CODE/utsm-ipc/README.md new file mode 100644 index 0000000..fe10e37 --- /dev/null +++ b/CODE/utsm-ipc/README.md @@ -0,0 +1,174 @@ +# UTSM ↔ Linux IPC 骨架 (Phase 1.3) + +## 概述 + +Phase 1.3 实现了 UTSM 监控核与 Linux 主导核之间的双向 IPC 通信骨架,包含三个通信通道中的两个: + +1. **共享内存环形缓冲区**(异步消息) — ✅ 已实现 +2. **VMCALL hypercall**(同步请求/响应) — ✅ 已实现 +3. **IPI 通知**(中断注入) — ⏳ Phase 1.4+ + +## 文件结构 + +### UTSM 侧(监控核) + +``` +CODE/UTSM/ +├── include/utsm/ +│ ├── ipc_shm.h — IPC 共享内存 API 声明 +│ └── hypercall.h — VMCALL 处理器 API 声明 +├── vmm/ +│ ├── ipc_shm.c — 共享内存分配、初始化、EPT 映射、ring buffer 操作 +│ ├── hypercall.c — VMCALL VM-Exit 处理(PING/HELLO/SHM_INFO/CONSOLE_WRITE) +│ ├── vmexit.c — VM-Exit 分发器(增加 VMCALL case) +│ ├── vmexit_asm.S — VM-Exit 汇编入口(重写:保存/恢复 guest GPR + 汇编 vmresume) +│ ├── vmm.c — VMM 核心(g_guest_regs 全局变量 + vmm_vmexit_entry 简化) +│ └── ept.c — EPT 页表(增加 ept_gpa_to_hpa() 翻译函数) +``` + +### 共享协议 + +``` +CODE/utsm-ipc/ +└── ipc_proto.h — 双内核共享的 IPC 协议头文件 +``` + +### Linux 侧(主导核) + +``` +CODE/linux/ +├── configs/ +│ └── utsm_x86_64_defconfig — 增加 CONFIG_UTSM_HCALL=y +├── patches/ +│ ├── utsm_hcall.c — Linux 内核驱动:VMCALL + /dev/utsm + SHM 映射 +│ ├── utsm_Kconfig — Kconfig 配置 +│ └── utsm_Makefile — 模块 Makefile +└── build.sh — 更新:自动安装驱动到 Linux 源码树 +``` + +## 通信协议 + +### 共享内存布局(GPA 0x04000000,1MB) + +``` +0x04000000 utsm_ipc_shm_header (64 bytes) + ├── magic = "UTSMHC\0\0" + ├── version = 1 + ├── utsm_ready (UTSM 设置) + └── linux_ready (Linux 设置) + +0x04000040 utsm_ipc_ring utsm_to_linux (UTSM 写,Linux 读) + └── 64 条消息,每条 256 字节 + +0x04001040 utsm_ipc_ring linux_to_utsm (Linux 写,UTSM 读) + └── 64 条消息,每条 256 字节 + +0x04002040 payload pool (~1MB,用于大数据传输) +``` + +### VMCALL Hypercall ABI + +``` +Linux guest 执行 VMCALL 指令: + RAX = UTSM_HCALL_MAGIC | op (0x5554534D48430000 | op) + RDI = arg0 + RSI = arg1 + RDX = arg2 + +UTSM 处理 VMCALL VM-Exit: + 1. 从 g_guest_regs 读取 RAX/RDI/RSI/RDX + 2. 验证 magic,提取 op + 3. 分发到对应处理器 + 4. 返回值写入 g_guest_regs.rax + 5. GUEST_RIP 前进过 VMCALL 指令 + 6. vmresume(GPR = 修改后的 guest 值) +``` + +### 已实现的 Hypercall 操作 + +| 操作码 | 名称 | 功能 | +|--------|------|------| +| 0x0001 | PING | 返回 "PONG" (0x504F4E47),用于探测 UTSM | +| 0x0002 | HELLO | 握手,UTSM 回复 HELLO_ACK 消息到 ring | +| 0x0010 | SHM_INFO | 返回共享内存 GPA + size | +| 0x0020 | CAP_VALIDATE | 能力校验(stub) | +| 0x0030 | UTRW_READ | 封缄内存读(stub) | +| 0x0031 | UTRW_WRITE | 封缄内存写(stub) | +| 0x0040 | DRR_CHECKPOINT | DRR checkpoint 触发(stub) | +| 0x0050 | CONSOLE_WRITE | 将 Linux 字符串输出到 UTSM 串口 | +| 0x0051 | CONSOLE_READ | 从 UTSM 串口读取(stub) | + +## 关键设计决策 + +### 1. VM-Exit 汇编重写 + +VMCS 不保存 guest GPR(RAX-R15)。原汇编只保存 host caller-saved 寄存器,无法让 C 处理器读写 guest GPR。 + +重写后: +- VM-Exit 时保存全部 16 个 guest GPR 到 `g_guest_regs` 结构 +- 调用 C 分发器(不再从 C 调用 vmresume) +- 恢复全部 16 个 guest GPR(C 可能已修改 rax = 返回值) +- 汇编中执行 vmresume(GPR = 修改后的 guest 值) + +### 2. EPT GPA→HPA 翻译 + +Linux guest 使用 identity-mapped 页表(GVA=GPA),hypercall 传递的指针是 GPA。UTSM 需要: +- `ept_gpa_to_hpa(gpa)` — 4 级 EPT walk,支持 4KB/2MB/1GB 页 +- 翻译后加 HHDM 偏移得到 host 虚拟地址 + +### 3. GPA 布局修复 + +原 `LINUX_GUEST_INITRD_GPA = 0x04000000` 与 `UTSM_IPC_SHM_GPA = 0x04000000` 冲突。 + +修复后: +``` +0x00400000-0x004FFFFF 固定区域 (boot_params/cmdline/GDT/PGT/stack) +0x01000000-0x02FFFFFF Linux kernel +0x03000000-0x03FFFFFF initrd +0x04000000-0x040FFFFF IPC shared memory (1MB) ← e820 reserved +0x05000000-0x08FFFFFF general RAM (64MB) +``` + +## 验证方法 + +### 1. UTSM 侧编译验证(已完成) + +```powershell +cd d:\Code\Deshab\CODE\UTSM +mingw32-make +# → utsm.elf 生成,0 warnings from new code +``` + +### 2. Linux 内核构建(需 Linux 环境) + +```bash +cd CODE/linux +./build.sh +# → SYSTEM/boot/linux-bzImage +# → SYSTEM/boot/linux-initrd.img +``` + +### 3. QEMU 端到端测试(需 VMX 支持) + +1. 取消 `SYSTEM/limine/limine.conf` 中 Linux 模块的注释 +2. 运行 `ISO/run_qemu.bat`(需 KVM 或硬件 VMX) +3. 预期串口输出: + - `[UTSM] IPC shm init ok` + - `[LINUX] loader init ok` + - `[LINUX] vmlaunch` + - Linux 启动日志 + - `[utsm] UTSM monitor detected (PONG=0x504F4E47)` + - `[utsm] /dev/utsm registered (IPC ready)` + - `[HCALL] HELLO from Linux guest` +4. Linux 用户态测试: + ```bash + echo "hello UTSM" > /dev/utsm # → UTSM 串口显示 [LINUX] hello UTSM + cat /dev/utsm # → 读取 UTSM 发来的消息 + ``` + +## 后续阶段 + +- **Phase 1.4**: virtio-console 双向通信(`/dev/hvc0` ↔ UTSM 串口) +- **Phase 1.5**: virtio-fs 文件共享(Linux 挂载 `/utsm`) +- IPI 中断注入(替代当前的轮询方式) +- UTRW/CAP/DRR hypercall 完整实现 diff --git a/CODE/utsm-ipc/ipc_proto.h b/CODE/utsm-ipc/ipc_proto.h new file mode 100644 index 0000000..5f1d1bd --- /dev/null +++ b/CODE/utsm-ipc/ipc_proto.h @@ -0,0 +1,231 @@ +#ifndef UTSM_IPC_PROTO_H +#define UTSM_IPC_PROTO_H + +/* + * UTSM ↔ Linux IPC Protocol — shared between both kernels. + * + * This header is included by: + * - UTSM kernel (CODE/UTSM/vmm/ipc_shm.c, hypercall.c) + * - Linux kernel module (CODE/linux/patches/utsm-ipc.patch → drivers/utsm/) + * + * Communication channels: + * 1. Shared memory ring buffer (async messages, bidirectional) + * 2. VMCALL hypercall (sync request/response) + * 3. IPI notification (UTSM → Linux via guest IRQ injection) + * + * Shared memory layout (at GPA 0x04000000, 1MB total): + * 0x04000000: utsm_ipc_shm_header (64 bytes) + * 0x04000040: utsm_ipc_ring utsm_to_linux (UTSM writes, Linux reads) + * 0x04001040: utsm_ipc_ring linux_to_utsm (Linux writes, UTSM reads) + * 0x04002040: payload pool (for large data transfers) + */ + +#include + +/* ===== Constants ===== */ + +#define UTSM_IPC_MAGIC 0x5554534D48430000ULL /* "UTSMHC\00\00" */ +#define UTSM_IPC_VERSION 1 + +#define UTSM_IPC_SHM_GPA 0x04000000ULL +#define UTSM_IPC_SHM_SIZE (1024 * 1024) /* 1MB */ + +#define UTSM_IPC_QUEUE_DEPTH 64 /* messages per ring */ +#define UTSM_IPC_MSG_DATA_SIZE 240 /* inline data per message */ +#define UTSM_IPC_PAYLOAD_SIZE (UTSM_IPC_SHM_SIZE - 2 * 1024 - 64) /* ~1MB */ + +/* ===== Hypercall operations ===== */ + +enum utsm_hcall_op { + UTSM_HCALL_PING = 0x0001, /* Simple ping (returns PONG) */ + UTSM_HCALL_HELLO = 0x0002, /* Hello handshake */ + UTSM_HCALL_SHM_INFO = 0x0010, /* Query shared memory GPA + size */ + UTSM_HCALL_CAP_VALIDATE = 0x0020, /* Capability validation */ + UTSM_HCALL_UTRW_READ = 0x0030, /* Sealed memory read (UTRW) */ + UTSM_HCALL_UTRW_WRITE = 0x0031, /* Sealed memory write (UTRW) */ + UTSM_HCALL_DRR_CHECKPOINT= 0x0040, /* Trigger DRR checkpoint */ + UTSM_HCALL_CONSOLE_WRITE = 0x0050, /* Write to UTSM serial console */ + UTSM_HCALL_CONSOLE_READ = 0x0051, /* Read from UTSM serial console */ +}; + +/* Hypercall return codes */ +#define UTSM_HCALL_OK 0 +#define UTSM_HCALL_INVAL (-1) +#define UTSM_HCALL_PERM (-2) +#define UTSM_HCALL_NOMEM (-3) +#define UTSM_HCALL_NOSYS (-4) + +/* ===== Message types ===== */ + +enum utsm_ipc_msg_type { + UTSM_MSG_NONE = 0, + UTSM_MSG_HELLO = 1, /* Initial handshake */ + UTSM_MSG_HELLO_ACK= 2, /* Handshake acknowledgment */ + UTSM_MSG_PING = 3, /* Ping request */ + UTSM_MSG_PONG = 4, /* Pong response */ + UTSM_MSG_DATA = 5, /* Generic data message */ + UTSM_MSG_ACK = 6, /* Acknowledgment */ + UTSM_MSG_NACK = 7, /* Negative acknowledgment */ + UTSM_MSG_SHUTDOWN = 8, /* Shutdown notification */ +}; + +/* ===== Ring buffer structures ===== */ + +/* Single IPC message (256 bytes, cache-line aligned) */ +struct utsm_ipc_msg { + uint32_t type; /* enum utsm_ipc_msg_type */ + uint32_t seq; /* Sequence number (monotonic) */ + uint32_t data_len; /* Actual bytes in data[] */ + uint32_t flags; /* Reserved for future use */ + uint8_t data[UTSM_IPC_MSG_DATA_SIZE]; /* Inline payload */ +} __attribute__((packed)); + +/* Ring buffer: single-producer, single-consumer queue. + * + * Producer writes at tail, then increments tail (with memory barrier). + * Consumer reads at head, then increments head (with memory barrier). + * + * Empty when head == tail. + * Full when (tail + 1) % capacity == head. */ +struct utsm_ipc_ring { + volatile uint32_t head; /* Consumer updates (Linux or UTSM) */ + volatile uint32_t tail; /* Producer updates (UTSM or Linux) */ + uint32_t capacity; /* Max messages (UTSM_IPC_QUEUE_DEPTH) */ + uint32_t reserved; + struct utsm_ipc_msg messages[UTSM_IPC_QUEUE_DEPTH]; +} __attribute__((packed)); + +/* Shared memory header */ +struct utsm_ipc_shm_header { + uint64_t magic; /* UTSM_IPC_MAGIC */ + uint32_t version; /* UTSM_IPC_VERSION */ + uint32_t header_size; /* sizeof(struct utsm_ipc_shm_header) */ + uint64_t shm_phys; /* Physical address of SHM region */ + uint64_t shm_size; /* Total SHM size */ + volatile uint32_t utsm_ready; /* UTSM sets to 1 when initialized */ + volatile uint32_t linux_ready; /* Linux sets to 1 when initialized */ + uint8_t reserved[24]; +} __attribute__((packed)); + +/* Full shared memory region */ +struct utsm_ipc_shm { + struct utsm_ipc_shm_header header; + struct utsm_ipc_ring utsm_to_linux; /* UTSM writes, Linux reads */ + struct utsm_ipc_ring linux_to_utsm; /* Linux writes, UTSM reads */ + uint8_t payload_pool[UTSM_IPC_PAYLOAD_SIZE]; +} __attribute__((packed)); + +/* ===== Ring buffer operations (inline, usable by both kernels) ===== */ + +/* Memory barriers — adapted for each kernel's environment. + * UTSM: use compiler barriers (__asm__ volatile("" ::: "memory")) + * Linux: use smp_store_release / smp_load_acquire + * + * For simplicity, we use full memory barriers here. */ + +static inline void utsm_ipc_mb(void) { + __asm__ volatile("mfence" ::: "memory"); +} + +/* Initialize a ring buffer */ +static inline void utsm_ipc_ring_init(struct utsm_ipc_ring *ring) { + ring->head = 0; + ring->tail = 0; + ring->capacity = UTSM_IPC_QUEUE_DEPTH; + ring->reserved = 0; + utsm_ipc_mb(); +} + +/* Try to enqueue a message. Returns 0 on success, -1 if full. */ +static inline int utsm_ipc_ring_push(struct utsm_ipc_ring *ring, + const struct utsm_ipc_msg *msg) { + uint32_t tail = ring->tail; + uint32_t next_tail = (tail + 1) % ring->capacity; + + if (next_tail == ring->head) { + return -1; /* Full */ + } + + ring->messages[tail] = *msg; + utsm_ipc_mb(); /* Ensure message is visible before updating tail */ + ring->tail = next_tail; + + return 0; +} + +/* Try to dequeue a message. Returns 0 on success, -1 if empty. */ +static inline int utsm_ipc_ring_pop(struct utsm_ipc_ring *ring, + struct utsm_ipc_msg *msg) { + uint32_t head = ring->head; + + if (head == ring->tail) { + return -1; /* Empty */ + } + + *msg = ring->messages[head]; + utsm_ipc_mb(); /* Ensure we've read the message before updating head */ + ring->head = (head + 1) % ring->capacity; + + return 0; +} + +/* Check if ring is empty */ +static inline int utsm_ipc_ring_empty(const struct utsm_ipc_ring *ring) { + return ring->head == ring->tail; +} + +/* Check if ring is full */ +static inline int utsm_ipc_ring_full(const struct utsm_ipc_ring *ring) { + uint32_t next_tail = (ring->tail + 1) % ring->capacity; + return next_tail == ring->head; +} + +/* ===== Hypercall ABI (Linux → UTSM) ===== + * + * Linux calls VMCALL with: + * RAX = UTSM_IPC_MAGIC | op + * RDI = arg0 + * RSI = arg1 + * RDX = arg2 + * + * UTSM handles the VMCALL VM-Exit and returns: + * RAX = return value (UTSM_HCALL_OK or error) + * + * The magic in RAX distinguishes UTSM hypercalls from other VMCALL users. + */ + +#define UTSM_HCALL_MAGIC 0x5554534D48430000ULL /* "UTSMHC\00\00" */ + +static inline long utsm_hcall(uint64_t op, uint64_t a0, uint64_t a1, uint64_t a2) { + long ret; + __asm__ volatile("vmcall" + : "=a"(ret) + : "a"(UTSM_HCALL_MAGIC | op), "D"(a0), "S"(a1), "d"(a2) + : "memory"); + return ret; +} + +/* Convenience hypercall wrappers */ +static inline long utsm_hcall_ping(void) { + return utsm_hcall(UTSM_HCALL_PING, 0, 0, 0); +} + +static inline long utsm_hcall_hello(void) { + return utsm_hcall(UTSM_HCALL_HELLO, 0, 0, 0); +} + +static inline long utsm_hcall_shm_info(uint64_t *gpa_out, uint64_t *size_out) { + uint64_t gpa = 0, size = 0; + long ret = utsm_hcall(UTSM_HCALL_SHM_INFO, (uint64_t)&gpa, (uint64_t)&size, 0); + if (ret == 0) { + if (gpa_out) *gpa_out = gpa; + if (size_out) *size_out = size; + } + return ret; +} + +static inline long utsm_hcall_console_write(const char *buf, uint64_t len) { + return utsm_hcall(UTSM_HCALL_CONSOLE_WRITE, (uint64_t)buf, len, 0); +} + +#endif /* UTSM_IPC_PROTO_H */ diff --git a/RE/Bug记录.txt b/RE/Bug记录.txt new file mode 100644 index 0000000..a0a97db --- /dev/null +++ b/RE/Bug记录.txt @@ -0,0 +1,6 @@ +开发者模式中启动的Shell的提示符没有在第一行也没有显示 +shell光标没有闪烁 +Login界面没有显示鼠标指针 +在桌面上时每按下按键屏幕就会闪烁 +窗口打开和关闭时的动画太卡,而且屏幕会闪,直接去掉动画 +启动一次之后,第二次开机启动会直接退出 \ No newline at end of file diff --git a/SYSTEM/boot/utsm.elf b/SYSTEM/boot/utsm.elf index fad5b5d5a755478bceb0754963ccb01db7f71e0e..0c3c3d86791dff3a7c86f7b6f417326973408a6f 100644 GIT binary patch literal 87120 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
Tf3Hn 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 diff --git a/SYSTEM/system/deshab64/desktop.elf b/SYSTEM/system/deshab64/desktop.elf index 56d3a31bd868f6079bc58012e3a5694817ff78ce..297d5507c40b825121dec81456d8b0842f18454b 100644 GIT binary patch literal 145240 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

>Gv+^4J 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 J6=)s*{{b?+{XPHy diff --git a/SYSTEM/system/deshab64/tools/browser.elf b/SYSTEM/system/deshab64/tools/browser.elf index ba1e3d51ea99ba81c5bcb5c01190481977e45cf9..489dce133988c237dd8555f077eb7da1c400c963 100644 GIT binary patch delta 19152 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 delta 8669 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 delta 7962 zcmZ`;4SW>UwVz3L*(Ge4kY)*lk8Jod0pTTRvzw6cUg!7Q zd+t5=+;h)8_k7K4`*))|>Ic+jCuYCQzBDPHpEi~;o*u}g#KtUuiFS)&U9Sr5dFg>t`cLH#f_oKD3`9ARdL^g@@KPZqJ*9Eoku4l3M+d&K? z1Ws|)f5H->@rU#0&&r$kuuE_za)DPUN|~K45c5ZVqnnFvW2`$J-JP*+5xVcix{so} z%jj0VMOPE^Z|Eox^bQh~q7;l@8Ulop{1>!UUAoRN6?3%K6vvVof@OWd|`uDH6&-)dm9*rB)^F^VtxfJeIn*98?>e&*$Epr zXkQH(Hc|pU@3NNjQ5L6cACi!d>`Beoak0$6% z#NL5^$GtX&ap&ohI{pHNk~;a7$qd8qr5BWUrE|qVL&9of>&vy|1o`&t(zjUecO-On zF*$K1n%>KxrgA~{zpMAGWY3Dey(OZ}yIrt}Cf zhih<+VdvDCAV3rL|3=ntt{pK~ojOL4!=}<&Wy3A9;ilYVqC@8~|2M?E4cm}p1#Q^; z`Y&0A?OXKotz5cU9y#QB>K@a)Grbs6Z{;xx|fF|_8P zpyzt%1?ROCS+DxW=e6Wrfsh6);;NToj9b*3oXx(k ziMbu|qw`VV6nh)GFpy1iF~TNCp4Dp8$Fo^lLwaV4A9ISmB2MvjqINDl^NGWW)D29| z*_8z*aNdidOEul+BIaoz6!eJmy6+#Ie*{{wJAl!f=rO@MlTI7*$0eUkCGV%=_zCFV zN8$U5{V-Z8-F?R1KONP7@4>hCeh!2^dtXHyRqU%N1M?cRc`ea#)Sn+mAAyC-tb{!> z5O@I)dWiZ&^}YlxWq5k(pRqJz9t%8uB4+zDnsfMMc7nEi_*iQ(CQ1JOb=t3n-;?v= zI)fSFRf6O^>MejevXXq`={~=}6yHdlc6R0ZEbYSZjQg|LB;Uu;_eyJvu7p{x`b#*5 zv4E>?L6_Hqoqq#V^0TlsaE06SDpo7zOpNkcpdoEeMlL%>TbtqVXJAIY&iYbtk~}@{ zN_0{DJUnT0lUBa!3>f+2lYPxsjwHDAPGMIEb?SXQ=j!I!%XwEIqk{#?rPF(IvHubg zD?QF8-x2;wi1nuP>D z7`~8-j6*KE{3O)*TVM)ia2sz?mdXDK7K*tZZ6`5E^K@e0h9Sw{Oxwbj&_=DKmDpwU zB4!_2`b5m-@t%@ghMra<)Ou>I0NnAiem-6}$u^{hw=lvK4 zP6?{HbQO$TwP&u<>PM&fw_&!Brr{#)b%c>4O0pjq8cckPVzJaClw9LUkk4rrNH;IH zCweo`F=7RRnKVJ4Q)8QeP%7$(3Lc?riyYn;WjTy7tpHi9qL~JS%uy1rDfh#Pdg?HM6JaEgxwC`N^hC>$z?!n z1tJ{Sfp#g^kgTj`naVD<#uM?S3aa`$pi3ix-vBr!MlHb5ZkVO#PJx5stXTjab^SP9 zQ1X*$pM2rTIw&ZG*I=oclFFOhD5ky_k5HoCB(ldu`T9t&S9E@>Et)=b(!abDC32a{ zY~v%Ytv9ES=5&EvlhksSB2V!V)7Cgn+cABl|CqqN(4z^uHi>4k_tQ^ z*IkVw>Kv*y6vh>eZf@^!UT$jVx+5ZsBJ`2;kQwwj724HW9t+; z&S7_1Ylks^n6Em==q%$N_)oRqW0#udceAa0=!_K^1JMTf|K_V&L8^B?4ZD#PU$dyr z>A+p5Q=d}oRto$|eiwFtCRMtRkXU#ILu8xi@ct^)g93XL!6ePQ*rN(2(b-lWolk(?Cu5D4~(&PyP; zE97>ZqzXNq&UNrX(+aN;^FxZHj1&3TFUFB(M&r5}nt; zB*;602nsvt2_!YG*~7L%7ApMdbxqAf9Z4Ze~ zy|0{f{#txKBQ}5Y&iR}1`Qhtd7o6SYb$lejkb4vCbt`OlheWOl@;6ope*d~BA(CPy z0F;|YeWXiNAGt1&1&x3=0PArTPheV5Mm#{Lu;)V5nUD!8p2H9v2vTodA{5dnO#qX5N1{!gUlh| z`ihQe?Y_dyL`-Xo3y1n2HLTd+no(f|S4+jPXesvDD$VrVX8AT{Da~B)Z&>!KD`S0n z@7iR^d8Qm!@9te(-izw72EUDa{se+d{9q#t5e7hnmB8%iC&F8X2#TkXV*me>^N1FyKk^xx-G!S3LA7_=3bl*Ty(gksMI0)$?;}fj{1Ki$ACzpp?_G$ z$n_b0IrK`>j|JzK58(v{%HrgG=s;l%Wzg9`vC1+87}vP3kSzw38E%_y0K3%y`CUJw z-f1H9RnrcMMG%hEU=Cbe(olSrC(UxIR`;r zt_AP64|S2LZy-e@I1J*dpMl=oT*K61TJ-)A{xul!d$ZG$^yofhoe7XyAnsa0zBWqS z3suv{&u#oI6)bR;+q4ZGIMDz~r8M4HN&Q7`dX7@h*X8j$#BQz_Eff zmBMstqJw*n?s2wGr1|rN=sa%Z&-$B(-eQnHuS@D9$e+iJ{8=BzY4t^mQXc|CrEtY^ zmT`7hSV{hpoB?8&G8uAJyVSxl3=(!Yo1{J6zghTD{lzb+TJ%T~;IeGa> zbfl8&I7UM$J&ip=fodCp^w6@~K@XbKg?!yAspD{h{f@m^2Cf^pZv`a8s&FY6oi=BAV)pk;vx&)mdN(S zR64SKI%Puk6ahqk;v#L(Sv59CrgpY*uLmW?$OqC)Hdw5hDqT52TP~`2e)r)+hYoqTcS4vP>nUrF(RG5Z zrC7BQH)(R`wDOg$xvrX*Ps2prtUt~mRN0bSqtZ9XaNrBH%x)`omdNjOA&iHdm()Nz z(9y~LlD!~@Y?9(eB55dMMIFkmKrD~sQXSFaL?Z4+Zf{Tyw#(NR@r}SBxy=qrWf#Nf zLLYzer$KLQ7|F01r9_IL2SxTDC~F77jZiN=pxUyL|7pE+D!Qu2qubmoPEU7}WBiKG zevU%M0v$BD*>*nEVJ%kE2D-^1K6Dy2P#e#lR#Tdf{et~&@cg@d#u16z% zUIeK&;tFg?23xu6F_e#L9$$jbS9?vYd=18H1rD=2 zIJKLyBc4W>uC~xU+~1l??=ymnhqDCft!4Jn4zw%K_kak3_6-DW8b{`-wn+l6_zNmd znGv&HIc+Z7FdwUMUF;VK+Aek6K%O+?UR^n--DPJwu(69hO>dWF2A-sDfV=k>x!E&) zYFmCFzKa;O?HrO;gJU8SB9Qyym&+NK3ke1b{VsPcyr}omSM< z*j_QKn4E+Ulz4Z2k$xvujIir`na*$`FDB4GVZHJPY8M9S(e{*#&VS{V2jh3X;=o%B=}>U|3lZfx!ei(e+eeeQm<~@%atB)9<@M8e1ss|9sCDfR$9<^W~Y%K*_)7Zs-gjmeHD@E_>$q1sn z53hR&Q4I*NeWF(jSA7(ua2Q0d7CF=|`@0qy(TkhiER|h~2d|Zbdt(=UolB+f=KDqV zDh*$Fn?v{#NSnl-Ue`gb3-)~>nrcLh*~Ipas2>pJTZ6oZW^)ZArvy%Uzow2L;cNT3 zsv8JcjQ8(`FXWtD5t`1M$!D$=2em9|xF2>B3u&%_ZK~-t1T+`hjCeMlzRR(pSSX7U zR1&qQtrygDFa8nSq?z^?7s7+(SLwmzien77X*2CA_NBl>U3M=ZE@L4Da>WjMPg}hU zaUb)5@u;R$qZF~iON}~2`&#-jE4(jw8WY~nWgSU(=l@OD7k;Aa$)D=_&Xc;Hjvs~h z#Nn5J*Y&|CbbT*=S{8fqee*-(#gVhUPd_>7VaH@g`I0ru94l9O9nUQDGEb~po=Rt?VUOW_;UV7CM+D=7}Xs9VM$)dY7&9GRxMLdBHrhZt*UQ_5GsP zf+pFqqt3B|eXH(Ry(K&0*b8;n>n)=b(v-T4cP*n+J6?bi@KDpBk4eGm^j%BdwOCkx z%E%aUE#6z`LtkQ9#=Yl0ND9QM~mI{OCA7385Ch~uZ;f&WPy|6m+X#iO4b7zBMo(tly8>gpi=p*At@ejuT-|WG^Xy6l=i}YVAJoG^H`8|HDA6wMlF|mhE z0H0fg4xVSl7(4(#H~3+EgC0$MN~aU7y??C6s2S!6I;mLE^9QWW7N417n9-D{S1nyq z_vB8?bJl!j`JyEyQ>ND4-C!w7ErGc%TDofOO6I9mOUp{jmMyK@(qOr#?n;B@g@kEi z>o)GP{Kt@@IIlQtomu!K%AipI>bGk0nXvzFvzcQZC diff --git a/SYSTEM/system/deshab64/tools/fileman.elf b/SYSTEM/system/deshab64/tools/fileman.elf index 218dbabf54f10266ba15c128b64e163f1d2f013a..dd7f399afb936e77a0f2f49249f4df86d50a19be 100644 GIT binary patch delta 14555 zcmcJ0eOOf2wf7ug6c9Y9pu~WZ#H6A|ArZqUBpjX5bIchvjkdO_Rf3QZHCkpyNvj#i znR&y>;TV(A)ZQd5dD}kqrf;J$k)TGMLBfC^1Ta;Mw+6q#F^L}uelz#C_6$h8xBchd z_dE}?)?WL2?X}llYwgX(%Ms1x@#{uMuFH*lV{DdiKw%jkhUZ4;H+-ZCkY<@bRfMRKvcbB?IqydmnjGxH3Fj{Ex? z45IrO@9OLaq-O5y*;6y0eA+CUB6;^=B5Bh`i>h(w>x4d;A*%5;44sw@>NbWxohPbE zTSMr4pnC}Q8~+4AAJF%DQ0>i4hM6F_TkS(dtt3HIvtD5pf1HepPM|xCR7Fo!fEN{w z=SRB2jhHLjD3L8 zJ&AXJO7wMR^^%}AxmtO5Hn^$e+BSg9yBqbU++Rn!CFc2Es~TQ$-HY%w=)==uBQwlcJdgM88w6TlG2@Q zKVng@3Y@?6TZ{Wx>3O-J-O7C@n@+C$%35&KYPz=a$TThGLz&+kl(?(Y)Reyj>N6W% zUcTZ9jFhj=J+t0m$l-W*D~6xf<>z?UTi6)&#si=mIv=D$yOXx9cVZRz-Np|<)r|j! zUvs)QGD!!bCcR3(MmK)>iYC-@qxCNV6@Q26#_E1*ZV|E+Ux%q-b+j74fJ!%Hw)%|) zz=-3@Sx(KGN!4BRyMw_l<5QHuyPrftxi*WhoZ2091{Lqt?SFm${r5$FGsLuGRAy_W zaXeM37=?0Ej1Lj6Cj(cFBY9zt-H#(*Gk)(u z^4fsy5J&?r`xI}NYCHiNc-{FUl<*t>=rLga_%d5DHM{Wm$9ka)Tn*2Kw)VqshPU(J%ch`WT#V?^@_+r)gXMt$O`JoY* ze0=2}k?#yPs7492&bN@tnZvvOU#KtM{cEaj%nX&s_}1D%pyDH__;canT2JvOS@CWR z)$M(GPOu>`T`^ukDwgJJVws;Ii4?UpI^33XD`*|5(3G7+S()8P-_5o6L-nQ@e+Puu zzrIClp#$$c2{dS28_Mte0x8Y78YwNl1i!&X^k!<{^_^7As~YpE&Id?9`@UpC$D=YZ ze12q};k&?X&`LlPmAdF>gkJ~?gkK5^7X4{S&w+|yf@?0OE$Z>FA+7zQ z{8m}+Dqtj_pqsU+)V`koRQ_xp+mv?`Ut;4@6Q5pV|)VGn#R~z4=vixS} zMGJRa@STb=^VJWQfmXvoDCP8aQMGKcsAmMP*BdaOUh?y(DSqY5MdDb&EJ{1 zE|X)^%E6-VY>cQIA46S{s}DR`v<@1&Wo>xGUtkYs26se*YZ&<8*CPzFx@L=2J!etB zv#Q5~&StB6#iE|Is<&iq%@%O%+qDt2aoumDrp0xdcZ~<48CRgi%*MbV;=QQ&(j?PC z-ZcbS7b0r$X5**hr&q{E8l5~ahH@N6#UBw!CF*WT@kX+oSXO6%;*TQwr>JXI z{q^SDPYt<2QYZZDk6^~$^HF6dIc@d86iA6YFj4TGNC0LYINTD`AQv>x*5g|7Ct0}` zv^ww8=@p3s2-!8gV(tKru-$}35VnUf>U^G;uqeWOgc%8|C9EG|d#5{r_9wKCh|z?7 zMi|NCyn4c73G)-ix@#aTZn|n7kT6wUJs@!^lF77HpT;59c@`=mwExWNHA#ceeiCqVn8FAg1%)cbw%vF-}7HTmwYL(T2S#AVsQqy%#$) z2MIw5M(}SsebGM0_IhJA)FRh83%&N9^rS(Ap9((_<_UA37RxWRp(#>N4Bn7e7gQ3API#Es_=`sVkrvRiH93b>LrTZ?*TD%`LS$`_ZX~{e$s#gMo z`H?ut_exx=WZKKSWDZqKH*FV0ch}43M5UW^kXn{Z$8ARgq9rpJh%H(t1^rf&-#*5w zc1xzOY;n{4=B>9zC&t3=*l*Qe=7@M~nm=l5m=<;~s}7|FiYU}mFL z^$BG;*|GfkS}EwWsts1tQCq)?mh=Y4NsFnCf72_fjxwct4*#w%%~y6E;`t3ESB6cm zS;`b|ow!jfZhj7pifXgy^mRNT`ffbH)r#td`B-?EqXGs95q*S#qz4G7UeVWX6eErC zNQtU@1xn7;qujL0q8?QS(jt-?wL#dE23lsTtiC%3ly&O7jiS0&Z42%dOveO%#xYT! zGaC*@*q(M8%5qGxdHni%(bOXFzpWRdyUZ1iEskU{=#x!HOK=b#4GidG4~we)JQ_~- zxf}Vlf51Kw3!0Vdb85&jx<|HB!$mvsG*4V(ncpL5&=2VxWZh!nc=tHOdf~cot%ceuNQ$l%n z5EbmrJ4wTu)34|$Z(EALxd@s?U-}P&;QKn-Qry*nw4lfRzWjs%y4@VpY0)SCQt+LP z7Rt{;F&l)KCZW6ug@S>~&{X_Dek4AzR8&Sa0%Y2l|ZDdsCxa2t*g?@Ga3G8=as zJaFJZuWC1|$1UnH_(ESgC12oyX?oPX5MuRex<9Y%^9)fJxV(x_<7vT^?${FS=<67+ zuLL?l3?dQ9EBVkFnT?%W0U$}Qj)Q5sX(ZewH&vPdn{hIhpD9uDFohZnf|hREph)Zu zSXqAsLPF{6ao0)P=s7#EFe#5)T*vH>TMHT_bssDR=HyMUc!!NA=$Fc`lmFsO!|u26 z)x+T*qP*ic+`&Flj(TS3Cb{7Asr4>aa{|OfG}YSTV7p|KITGo)?5HOBnAU=tKq<#cczvw$>6uEA;d-PpYQ>bAw zID8$fp*S?RkYh4fpTU}JC3nig?>;6dd)o!&=0br>2)LMM39z&G%_@~Dj3|4MG zPDz}Af#e=X9B!}3?Zx&P0Ph!4j}s3@DWslG$DawiNQN!xXJ+W~-wc-GFX)J#p(pMW zeCHv=r#G==T%}V8i80yPGxWuOGYI9U4Tg~hOUxIRm;*vNRSQm6NA1L6D=EPNc6A0{ zJ!*=@ecJx@Y<*m|Wcs`9v`~JFHjYCRK0UArV*fe#)BO^$DgIjqvf3F)xivLqH6Va8 zt*w`J#^GZ;bF=One5jX8>%gT+lF9FRHEV_Q;15P#@xluOp{1F3?Y@ip%mMS z3=`jtG#E91DW!>JZDb%<1(282PMWRftdk>iOOO;kd|HOq$H5hNG_?OErIsf0KvAFfx~SHR%Gq1u z)#EVbg76VC8)2@?dK5pMua1L*I`cLV*$@QQ&9H7@L11RXZF5BgH0~`?sf`j%O^(J* zk@jry>QPa5CZ>Z}DPC<6^=+gp;lAToSAEKf$jl%*fV%8wu85jU8Z+1=nyx#(=Burh zqBgG#zGtPu&kh?~@vjSE(ng_y$!W}#bv4@WAve9El4)^_%=oGp^tu`yU&D^uJ~b_E zbz0iI6;4qN6%e;iFfU*A;$&L^QKD%i+e?-&U7D6=u@NN!6u--u3{Wun+595lDTFUk zyd!!}3QD203uri(yvtbUG>N-{waKAit!s$EFiTGvCaQ-de)BPSS#*FU^{DuG z=z!kft=}U(HqI$+ho3+nyu?OXOX68iDMO*ZCn5;kK^wj&>Z^DotR|m*tfbG66QO0u z{ZnpLh@;{D9RV?(h)3c6DPdaRK$nlRCrM;rny$kR;4V+~n_tPGY+d|7ENFC!#f^xp zj_J7)@Lj*3YtfR<;&6Ajz7*#-wgICrv(>B_4(iTFjx$|;Ia6zbvt>LfE_;tnx=AI3oX08~AV#0iNyxFV{%$g`7{?3HKg6B97)iSXV25#mHt9tu;iTvF;sqPsZ8RFqO~wMKuYl4{lmg>NWb~-+(Uj5! zI5*6D#JWAcf@RES6^$7yup>p4T&HPNs2SbJ+Jl2xp?7Oo?P(k`=1>pnuF)aTfefrs zG*SQtfXqf&bzh{~>V{~R`)&dQg_}Cb81LDCY=WBJPSXj(Io{UhBs6jxaRW1xxu!s3HvzskRVcqvk@anW7s&l(o=cwGGiu zmSm&lU36(CPSI(*@U9!UlfaN@6G6^6F!&%1U!L+e##VJ(8x37$lfgX#vcSnI6w8xT zzpSs2MfD>1qOn~hYr06I(cYa85J)8@%rqE|M}ue2kCf>Re-RSxKK!0JS~1XuPLlbXJ77+p}l%C@15WrW5<%2WE$=&jH{o zUxXU=TN*ANFjJS}=%GO?T>FamK$g{IHCC?4s@}GyA4L4S0K1yFi-{1gF%h=z+6Gz& z1fK#AGiM3@^cD#b4NWImmAFPJmh&Q(BTs=Gd6uT9^c#yoG~f2DO~3bP5-0n6lKMGL zi0cyfl{{sa2-2$KK%NJ(^6$8OEloxsu5<{l(`*C9DUN1awBV0=gN%Jy3vDj$^U`5d zX0X+C&T}jM-iKI8OQ__VRMP4W@)gU`BDsYXML?)%?wE9Li|4s9vvZc!1o~m*RS4kg z6Z5c3XJH$D%^1#gz)8=Oxqz%^&2}UHU$%NgvH|hc5!eq6;G*Zmn6xo99ixZhNPdU4&u6e22S+25&LBnpfsoJ9eaV-n}@zxI@@vRK}{dkOz~5iEDp5PtT7L5(Gk3oHWGF&$Qq~GL zA)|}m{FRxz;yV>xeu}L4m_z0JQ8_56$2(VH5oEQacwZ~`YU>o5oq#sj&sIrv~v4O z+pVUPo?ktDf6S{7uol|7J?}g`e8_)Sr}1H@a|N*&)&7BMwl*Qe58iDDS=hmRial%v zVV1mr1EP~IZ^#7HRwTN+>?;Izp)KsdTwu8@sLX5)PgT7lsm*4{zS<_MEov7Fli&oo zEc(8AKu?^cC%!4DM+Ikki%`~>A?b5Cy27YbZpM}jHtVxFS)Y%;r#N_Tu_YhjH`0~_ z8)tbvi?(L44m&mUs^TPWkd*dYlIqLsx??awS_hdc)@Lmu4)WDc!GvC)l^;&Pie8`f zED}!2f+f8^s{kqRH44_yEy;Az;l;IXr2S!>MdQVCKeZyJUnB#%D?CHUG7R*n$H=q{ zHi)K6j<4%oNmLMK02?LgwmSnV%LYA#WGd8NkU4oz7Kx!+A)(X*bIdUHMjSpVy z&A}8$bkM$ev}fOR1q#EFxtoIeACTvEi+WpFauu%YIk>Jl-&(kHg74etQ057N`;LMc z*g2M%TL^Xa5qzhaJ5|1yM4xsB9s_I*Jt7#uyJ#Q4LC?vwxP$giJbaP4E3m9hr)>Wr zl!IxID>s$d$)v8XFY6$DHAzeXaTYu*a8=^sll0Fe<;3&qWyD>SlR3U`5|pze9>ZDI zPHUy@B6}h5Kxp5vfQM#<_g^i5T|TlbSWDB;l8m6JrzIoJ`5W|Mph!sF zc)>QPn?E4LfJ5VuOxoroakr=^o++qbfos=s^=Vo14Y+m|T$^~7SYYA01>ZN|92K}> z;l35h&xJU5mEU~a%msRNhS|y846zPu`yJhIC^zr0ZDKM!k1*Sg^|FnHReATyJ8Z)> zseMj3DhV6fqIz-q)Jnb%ibWdqyJL~7<12duk#O65B?s{Ny|WI_6H)am|koXG)qpL)-5p37@X*G2H3pe_KK8gX}G!^HoD~*$=Tjo%oSG{@FY1<>&Oq1#c z!BXlC>E=~@9IEk&eNcCi`mvGJ8bmshubwyvEgHEKoyytQ#A!upl zJ;$fLfXzzY^Ii+LpS?S z61Nw=v=(^ZZ)_;WB`9$~Gadx`IDF~dfJiEf2*leNNOA*cS%0Ohg4|62@TH4EgWh=M z=WHl=Ay(u`daNL#EyDBb#~yMf!+jV<<;h}d!kIS3I0#8zuVs;$I4`^@NyIZXJt_aC z2(TL<5N-BnC(YOWF{sAzY;<+WcdoEIwKnL?`H6IKQOCg_Hft*q%5rm25chch0GG{L z{5wbq`pWL|n+9+Xk#78&`Kf`PmnN@MG5(XJU!ojdn*0m^TCKstwFthGbg`d0jY#S- zdUm3=ZKoF}Vmdr=t5?<^9J4+GdCR^qt5#=&SzT}dH(q_V0iK>*F>_63Q^2Bri(YS< z9Nv`);YTQ6J%^U?2qh0|{~N-wZmyYV{>~GWPoJYFD2Ia0W^KU%N&OP{ZDy!^tiu0A z4$8ZS)9}@6IDeeH3oJ0Oh#0Qxqij)7_%l?0`MF!Tc_JgmOjE0l8fvMCq8BFSp`4|{ zoJg-B%tIpxrRNalp^=1QzflP8h~pscdhO|wo|7nYhcjE*`G3a%VC}(T7$ydtxG-sP ze2W);k{XN4@WIJmJ}4JPi#stB`kWZv_+OxJE&d4rSzn1WJZT)ET5c+3+;>IR^P(tlmjt!s zVpF7$9S&^edWwCSWNNW}ORpNvTX9lOrSmzOPEpe_`zWjWr4>P|7(}wx-O3)pJA+pJ z=NyraBaXEWlwjZs_J-jD_#_B_-LU;R5DgVZ*xOchKNQnZC?*v4u)?n*YS9qBvh`wN zN3vTddJYQR`P?%EjHL|37c-Fba{wGoWwZ2A{iMlCU7Ao6jXALE$v(TF?`;S6VG}WpN{9 z>)DVm?4Wo+z>k@FC4Kd%)4M4qwE`^;i1?e zAqEDrF8$0iXhh<$Ls_Kl8w2;Vt=Aho^<<2A!-;r>l{->;LNZ+?i<3EiA&XPkN#Te9 zF1mA*(0VHW${*3S)qR>@`)@$dOMj5Dq8;;z>v6bdrNf~G8OMpDtZ6XVpT*dkk-{3o zY4MVv!YFSJ@L_ks30bw8TI^$FwTT!JC-}`YLV(+J*cOYBUQaQlZuA`;zn5m+JL%rd7TRmh3)#}H-p!`YLo{qA z_?a}6#;h5C#D-o3vpqhMQId=R(tY343k|%7mGzVa$W#<|)sP-Lhf_?UxuoDJ_H!JilEF0O>VIM`2BU3ZmjXP;K1`*leh(eojXP?dZ zd)(h)Gaj*Rpv{P=#u1xlLTLLai;bJ~0B9dkF%2<`=#;y~9!r2VsmgRcL<( zT{q*IIj$J#${DB87bGg`nkvD=r44ago!l{*Z13Ed~ILvU8+ z+DkiRyh1odTl&W}v|rONE@{YCuP!{i??qR6iciq&INQ50Xz2b5W1m z1JUGKe9jZmxPAtaYcOJ5!D(vL2R)aGl?a7Q91R`$&69f61Q9)*3BAv7tgm6dS1TmC zIT23c7EHE=ul#^!HYMxQJe>N8)8g-6nuWZ_V)D zdIWfew`L6`v4J5lM}gS}uzQE7SE%6NyC3jT(D1Qw!>-L`0uV#P(o%0A1px@m_3VG_ zx5H+#l<^q`l4MpgK6kF?rR-hzu6!CLRm&C#k3y(sv@?)e__XIKQ(KJoV`?y~^2Z-N zGWq3*H}l41mlWnNPAe*2T9{UpkKe@xb~$Ew(Oy_GdHIUM7t_+F<`*ont;jDZoa|UZ zH(~{o3rmabp2>NmJ+{2do(wMLqe9{4@{g)2y`%oue=@igQJzH~j*eLIj_0)xM?`%5 zj_3UkM+~lb2dj#YbT55>ddG7gH|(QJANJ>*dBZ}qRWNwzOTZszfP7a`%3z@5j6Nzx z^yn%|-Jy>FPV|H-qTZoD2%PB2RYbi*r}qm)&!{3QV)#8BJc<-$q*W39PKC+9i9WiD zsCVcBaH3~d5!GNwjtRk;07NhS_fWd0edq&5FZ%%<3al`kQ4!9-&Sak>OrH~`_i(Qa z^awQNy@wSu3=3zx5vI4NhUhcy4?$u+4QOt-9oS;*`xDWf$XIk&$Fwu4!s5F+-X5ku ze;2(jOka8z{a~2B;x778&}jl0dBlHwc5+DZox>k>ye?cJkBIm#;g9IeVLDZ(??w-O zdsui<7?}!(%l8o~WW|9_9dDTyDi;|Q>feysQ{nHS3O%g;QJCHyrjzSJUk0N`7`Ds_ zrGG7jV0KUWtk8^_LuA8L(AkpB2oaLP6Q0ZAIOLF(K_&sdyzqb*hby#`G~v4%uAqQ! zh=}g}-!GPlpO-)%Ow*?AgKu(w&(@DOIwu*PTbN%keqsn@0Teh&Y|BdwMUOtR@Y$tH z7Lq>cO{^+GaDk;37u%LBT)I4eF*MAIeEahY7Z1R#03V zn#7Cw%L;p|+3fjthmGo?p%7OV+Mdr}!AfAbOBa@4xL^QyRABdbxBb+U@LH57r8*_z z$m5>L)e{oJYv1#)eJeaKRZoar5mt3jfLp64#r9~vig!Ghk^ECQ7Ygu^PbLleIGpT( zoQHdDq-WzNg9cZzRqR!QTi%NB?EK{60e$|}&vWII+?YIiB7(0G6^D7|)TAXPWQQp0 j!U98^{XvhrW^7E=ln^POb<$Ro9Lwc|D3zQippE!XvufKG delta 9114 zcmZu%4|o$*wx4MdXrb9DNTo=TR#990*9rwVAGSz$s-9shq&i_aOLjz+6M&=Z~@Hv#18sBZgSg)~aIb^RrO(iP) z)L-x)P8mC8Ru#vEEDroe@kwlt6g)Le;`c}gPO;QUU`s401v|hBN(VZa^(LczX4N~l z4lCn5A5{>3YbGvrX*+g4>0l~Y-1ZX-2b6oDhNQN-h zPV?Ry?~P^8Mep`K5TC2Dv$H22F51R#;oS>Z@WkS-V21l{MYEV`0?le6Qe1fF)0~I1 z>vMTKv)+zz6x})wWO|iZPz%SPWLi5mWEmEpLD*I}2*skVP@>?wAFC3^7I7yY9+z3b zmq{lU3){k#J^zEXK6!Ix&l{lb36(wDh~6xIM0=?0IYzaNjs}o^Ts>;fc=u4XXOiIi zC)i7CM0U8y+a>t!1eR*yIv=(5W}%{4%23g)F8oSbYQb6}dAAy*a4HD4cRPra^*De} znlQEkeK|SWAgjT}vf7&_c=un7MrZ2j!*W|ADKWW+lcJ51+Lz`&BC@YUK2-jr!`E7V zN-1ua`7agQsWo3Yi~F3m?lngo+PJ?f!q%wFpP$9XZH+WJa=W|%q3SLyu7DT3|G*}F z2L<1apas7ZyfUcTlP>sfHCBHNii93G$Eq8^$hYhTL-6RMP=VUl;AXZzj|1*V1j8?b)PzzZm;g^mmw+oYCE8~Ha+C4?6&ZI&hy#38L z-grY2wnC5F$K`gJELL<7s!WjPTP#%8&qH0A-iM*oQR&Jv_D0D!C+4i%aaM7RJofB&;#NEaiRDp zanZuYiglmN7ksrBqS1%=yZTgbP`+A!})}I7X0~dv@jeJ<2IFD}b zmvnR4=h4)6A2$xfcNcZHv^|AdtFS&;NMH*a8#4A(czSukmjiL|o22a-087$SccGnK z*ygp?VIZ-x`&+OD?*jC3kQGQk>n^YL0|?Uup95^ExKq*#)=2uq-jKaF1wIQ72O$vx1I!5gN(7?f^Zj)`_IK>n+(}Z-VnY`LI4w&RmNlqhU^`Znc^~c ziM?v3$>6_Bd~-kl_u!ii{(Ho?^z&`tTMYg-;t#_7#w6}c2x2RHp9HXCF@6&GgLfsO zieA;$dI-geo(y-9$hxODnFrlAvq~96tFE?gkITv|YA7KxmIMj18ldG>TX*z#9HNHI zGpmvZp}(Dl?r1VkmEUIR7kcmcfm68l!t^DJ3@d1S@l z=WgP-$j}=9ypfM5jJ(M|C+(Mn(Kq^MkMepdPq)*N>>%~U=TWr631whtc@qs{8DkLZ zZA5%s!n0xQP0|XlM%~G}<;)gEv+}4Y`aH8Ew^d=ik#u^DF1@X}V(UfNT65&80V5rh`LGOY>3&?copJq0+d1X* zb-5)V5J{A=V_U0xypw${$2U-OVal}vUR%ueN1m9pfto4TdJJZK2U}#d0XsP8dNq=W zM}bP~vFA&Bc_BVX#}n9#!pg!DTiBwob(^vNau@B(!VF^li$PLLiRc9tiau_ttnSV* zG*hgbNl#7=9T6E+I#4x~3~kwRJua=V&3*@ucSzBe)S*uf-Xe?O?LaGs@KFGU`k1J8 zFZzhO*@YQR&XS`|G=iS@40_A_IfX5$bL4&>8T~6A_*mw5%WC6LS^Z_WyT++IEV3=^ z`p)CXjYdj7APZy9ZnT%~o#H6z?4-H)PsHF4iFQ4CG;0ffW3gxNb(DnLX;OY~NC=zXEbD8>o%o1s#Eud85$mbAeW^VfZGUAH zwyN95orZ=atR~>CR~aPLY>>R4|C7|l%;xIm%?cj^zIBRjdEbeRL6NRrS|dk;62D*e z&zkvHkHi`k{!saG$#%~5g;?2l*>XN09gj3BTI!7$5l4}(u=xSu>5W*5qK!!c)9E`Q zX$xC~XP*NP8ejnr9>>X5bSTLIm|0oknF+!K*h!w;3CyJh|e>;=^E!f=lpM{OUMA=x@zCrNXi+esg@gL*O(S%baw=v4TsNC#Z@Qhn9(<yq~Ng($yj_9nJMO|A^$trlL6GGtR(RA zvJFyr4H4FY4g^}2VzO}H)ZHyHX9Xa;X{oV8JPCx@^s&E%SR-N7kC?dKgDNNf3bd){ zcL_AuJvgS9Lv|%%zZZ=wEM_;(($ilSSdS$gtIV&(A|YQY)0`H%gm zdF*`n-(ag5fK6t5oqVMHBt0C*W6-@2gKob>E&{ikjG?x$M&=vkU>_lt_^5$c&m%+% zPZ5bBc9(A60K{6rsk;ZdSwo0mF6{r+yUs`rmEUAs?6emsaPhnW~sjaeUvkUFiSOPq3G$b zCXEweQQE0pJXnFb3M4pfLHEtFUX(1^x{aq?y-+1(;qf{Grj?kVdGHjfab{#+SCDt7 z4s?J)nI5t~l|y2E#%^hYRMG`?7ZsD};U z1ZM*{8^D3({6{IQtuD1=+kO;b;{F@t19}!@wR^0`s<6FMFfvWy_euxgJXjtvTID?n zBJN5%mOoLzWdI4S!pS=f3}(Qlceu>ZXu;@Nt^Gg9`E*bMv7XR*PY z__UOL7;EqhlGFyU*Ds?fN)J!LDib8WMY51N#`e=_isB$jbodGjOX{(UsCA+yXMd(J z{U}QH#s;&GLeNbsWJj^AM%2owyFP@&fW=${d4R;~?skQ}-i{)qKVK#d8^e`_<@0R{dv6tL(CrFf_bj?Uizxu9FN2n# z!Kmuyr%|NC&Q-)=J99vATJvH=0?BPwm~S)nX6v^YWK$i8M`4fDb^I6h6t~@)UPU9p zL(tY&P|38OFpxAKZC2uaq#+=r5|Zj84M;EK2@P15XxP zIy3en_zY9S9GmG(Wwu+df+jzmH)#+zCHx*iwJi9eD{UQU9Vl#=*_O{xqH70NFU@hSPx7=QAF^g&~>i1Lg6+a`>@y9iC( zugPp55;n(W{tG4l^>U2%SQ`owWNSV}@~=D%@mKj(NbF#1NLr6bnz1;cssoe-*4VrZo8gsP<|P}46bWb$XG& z;ht<>ikmXANK%`o%+b?5ik_$AMHSVxZ7Fbj&28{csxO%9!)bK$pPZN` z8ak@YuR?&*pLWATk_@z#0SZLNv*G_CI$6J%fK?lEjNy|oJee@?{8RGAdcK0oF7t;y8**dWU0)1dKL+i>#*)m+}|3xW0_LjIkhT7Ats-+#vPB zPIE52+mEhq?bu4pcXxj(dUlZuDQ17=U>EJe*0Rm^l1L|Z=-_`A zgWo5{xMx^{u=S9gKTFOe#WSZA?U6b(M|$QfJKq=lCJ~ViJf@^Wm3z@Sikx2uMJaK& z7(q%E4rN>l@+~a{@B9y03&DFJIMo0*cq&Ex`dWl$$H&lJHB1sc(CPhtH5`A_I+$~E zgu8*jo0vt>oLl^Ck|(Lc$?qvY=Cqyi51V{l(%{$-8Bu@1+Mt)*y_Ud}7J$g90cb`RV0*vGjC zF@Ufwx2WpfR1WKbwnW2Hv{~)uJ?)X?CLb|#j70U)kk@FEF}=x~ngBMXZFG_wN;T`( zpk9V4)X%V*+B(G6#(VmLAta1@K+ zSvM-C`B1|k4xw+j`&CIRBNd!rsNiu5ke9>*DK)qiePUB0=xrhXYb@L z

M&{-05gFJoterxMk#3xYqSs3)(*BVs%~+Pa!ZCLu!5L8AibECRkDJ>te7=YEEz zWzK+qSbm1*WlZ5j8iobwY7ZN@%>m;!yI(~}0K#9=H*bNHfh7+VKm zgnAK-YXEf|Ms?jr+}%`|%%ve854Fq4(B^NHiW{Voz@LF}|KDPQ-Pa*qE9F$_lxOAx zy1BxW-B8hxQ?=dUbr=7N5*LV2a*OOmU z{!4~&88%?Hjl4qGJx)d%0V#weB=R;Gc`%v$C2-o{`EQ_{R^

8hQ7tFvZA!wYvUB z`L9+gRpUiGBJ&5)@Fi;4LGL2`yz;0A7n?2If|sTRq3>oq(_vZnd=Gc6YXNQ8GT7L+ zwcDtrb`W6=*xM0!#z?e&oPyjp-E`4N@zmWO$*4g_0Pl=5KzaxFiw+zOb4Y*p;OH<< zU?2$eT#5pZ#ZwKxv(oL}P3{~tlA>mq9w(1H!0w?EUKsagLYHqTZ}`i5@yz4b<_ zp|dnRg`KmPwk;2y9IQ`l<3B;}OHbxlBf|Xbu$@1816}5%PCfZ$Jz2K%p9MX~6A?{= zI}kkEcfyt!$u4&lo~MZgI1nTq%u^Lxi|gCUi==&JHL+mu0Of;52%7@XgQE`+Uh`XI zTd(j!&>I%K>u9rD-un;$jx7>=p=ZBx`9Ceno(y^lJokPjcAd)$URCQ!5`4C8L z`i~Zj<`?*XDtN|2A*+!LlWgZ>$uJaL!xa7?l3~V{w7gIMKszWrY!J&XA~2RlEwveh zp5;+A?tssOx7qkhV0W7P0cUZTwv=|6d?rhO9$UdSJZbJjQt_BA578*C4t7}N+hB8w z^Iif6uB!ts;Xx!AC0pB?J&Nu8nvZdSXOXUOXz8y+>O+=4p&f80go{y>nB^WKnkCa% zpLKX_nb(78c_N953#Wq6B?AxeTDiMva;(t*-n8O`cHVzY(T)Dj*&{|u_fxynKNw`p zY!H~F{QLc*=hTjT={{qYHw}`y5d?or(C_m%&)GS;>0Zc~qYix!QBkw(HprOw?)AT> z98GGSPp$L5nE%dG&LHajUYIvNW#y_Rk7g__S+OJ|%jGV6DBJ(gysVUm9xN`+ z$XdN}r8{HgqbnZI_Op5C-tqmuZ)I)WBvV$xyz1J|Cz-BIc(J;+7j*ctYG8qn;(GeL z)&5L=mJp-l^YR@2{!E6W~r*FIlh8ZpzgWW|zV_mYP>SMlnVD^@I8xpb+~#zj3{ z#U&-&!w)V(rxlA?THy1aFJ`-gR7R*K3-_LuXcZ-DKp`jn`_S&nr=@i zimxqB=hfy;HQhdBUR>dzh|8y%CJpIVz-867Z&2MEmwT&gPf`8FcpVBmewu0W@c2ph ztKi~HuGTfpbnDQ|{_9HZo6}5%rp~vx+6W4%0RuCR>!xduj}CW4ZDEn=W>fvUTy1HQ SsW55OdxnVnz@Pcj*#8I9p$xwO diff --git a/SYSTEM/system/user/use/firstInit.txt b/SYSTEM/system/user/use/firstInit.txt index 879cdec..cf3045c 100644 --- a/SYSTEM/system/user/use/firstInit.txt +++ b/SYSTEM/system/user/use/firstInit.txt @@ -1,2 +1,2 @@ -0 +1 1 \ No newline at end of file diff --git a/SYSTEM/system/user/use/login.elf b/SYSTEM/system/user/use/login.elf index 92d708c72d7461957b53bcfea02984b9fa965d0d..b95c3dcfa95c00d1b00cd7c4270464aacb1d3beb 100644 GIT binary patch delta 6047 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 Z+a-w2(&(0KvR1~H?^m!b()Ks9{|^<^=g9y7 delta 3918 zcmZV>3s4m2`QPP^m*5Ynh$4@}2N6NwY%$UCBBxxlG)JM;v`KuP0#zddcWhA`y)(CK z++2=e{!Z(oQKC&IO){0_+6*Qgb7*>)iETlf8S|J((u{?WH0H%q-G1Nh9#QfSyWjiy zfB(NnPdDFjj&GZv-gt~mtTJ*pF_AKoOr{)Mo|||mSC<^hJ-DqXA?v3)m38o$V*ADf zorpq`qSHJ86FfbKbb2yu)7hjgl*-+hy2Ah2PC*$T6N0v0Di-qvYz! zjz_MRbrfAKqgExN^ja@k!*#HVXpW@0V%Mf`@+SJgPJW+C=Quz*S65Z7u-><-Tv!rA z zL8UTuuZ-K@_)=9>@L7Z|ndWjXngQ#j5<>jriSBBG=HCht!tF$B%wrEKty0j#wEYgxl zDupDdo<#CuDs{rBKv*hYi_t1vUj(_d#}P7E0rlrO0(*L#%Kx0on7tG7 zCjCjUZw~8_5KKGUf?8SQ036m&2IK2!PB|gWGAq~n0ciS1q1~4N**E$Eys7jCyazM) zjlQZ;FKd!McsIYYZ}b|%7o_mc=az9)`Y}XWwxs2VLmtASY57_DswrL$Hy`ki#8XKD zW&cPbm46S2L;i$H7!Nk-3CO$u3ls4|zxRZNfY{*-K8sa2m5=++C*$~2xk=x%mr(aj zn1+ZPt-iXt@0NO<%13(1(2z3)xY-!ZIyAt6L;D$Q#=y``RmB8aKL^CFURV*4=7Be_ zN(Kw<{_8gSWM>sr;~ktBa|;0dO^}yqn)e_n+cPErA0PbAMP+{wWN9sLd^OVb(4}%IT}+ zylThkkpl4L04$kG`v5CHP=2>spIhu*v! zF1>l)oArA4)LQXqv;&)A3zf4@?yKADKMx1>*81`O*Xv&l*lqPMK*o3V&z#VtfI;0&7rq3xRe)D2BTjlMA}D)$D!36r>2|`$!xJi) z6BENfFq@U+<@3q2*_yoB{9C!qn>TaD=0&RQkH{^t_-5(6Lc@3{vpfGJp`N{JBO<1;Rt+ zK8@gksHHM~4}7+vhY4|0IieAt0_Nh2--QBURLK%B-i<+F7G2fPG< zfDh#UbdE4j!F2vV+d_eYsa)Ent+;ckpm-iI(i|5tS8Zc|F0`dI0qqPVyETxuW-A*n z?3=c43skREw!?>bFQCpq+OM{-FBTr>_hy9JiVC=C&(xuU<9TOb-Z4O7n;xqR{i`?- z%fGTVw07|y6GA0ib?j4RtJ$`)Ae(I;W2;yEBemM)@w7C!Yb;wmmfHHp=3Ok8`yk|S z6jYpST*Wp#EPb@i!R!PUL$v0$#9oz{7)y z1&`vH{vp!2gy5xq2P&oLoYX`^ zQq)|#vzZjR8%#B>W*58p^qeGsq^M@s4p;rw&8+a4HMHs23~uUui%J)h8qdQUYIiid zcd<8)J6TG*g+G+bUT)83)qwU^YG_-#sdfrxVMMrW)Eb2|3_k81W*eQ>C_f!|aN{&Y z?V~3l3XQ!dF)Wo!e<&BU;a`f0Xi2R=z=}enUSe#(#CeLIE8I|&MWbaJ_HYaroXF#f zS#SID+)_}$bfZqb_irY%K9imh4RsIeJ(13DJj^}?2luknlj->-N5C3ZyAdV{aGdSQ jwO6s9olH;Y`2*_0Wyq{2a<~UIo)v$A!m_cKPv-m|*7Ji&