From 472d5691223604edfffe98063f15c17a27e29ae5 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Tue, 14 Oct 2025 19:17:31 +0300 Subject: [PATCH 01/44] * Make Makefile a bit more generic. --- boron/Makefile | 119 ++++++++++++++++++--------- boron/{linker.ld => linker.amd64.ld} | 0 boron/source/build_number | 2 +- 3 files changed, 79 insertions(+), 42 deletions(-) rename boron/{linker.ld => linker.amd64.ld} (100%) diff --git a/boron/Makefile b/boron/Makefile index 37071b40..dcb68a8d 100644 --- a/boron/Makefile +++ b/boron/Makefile @@ -23,20 +23,21 @@ VER_MINOR = 0 # Currently the only application is clearing the input buffers with zero once read. SECURE ?= no +# Default Target +TARGET ?= AMD64 + +# This sucks. +TARGETL=$(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$(TARGET))))))))))))))))))))))))))) + BUILD_DIR = build SRC_DIR = source INC_DIR = include DDKI_DIR = ../common/include SCRIPTS_DIR = scripts -LINKER_FILE = linker.ld +LINKER_FILE = linker.$(TARGETL).ld ISO_DIR=$(BUILD_DIR)/iso_root IMAGE_TARGET=$(BUILD_DIR)/image.iso -TARGET ?= AMD64 - -# This sucks. -TARGETL=$(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$(TARGET))))))))))))))))))))))))))) - # This is the name that our final kernel executable will have. # Change as needed. override KERNEL := $(BUILD_DIR)/kernel.$(TARGETL).elf @@ -74,14 +75,57 @@ else DEFINES += -O3 endif -# It is highly recommended to use a custom built cross toolchain to build a kernel. -# We are only using "cc" as a placeholder here. It may work by using -# the host system's toolchain, but this is not guaranteed. -$(eval $(call DEFAULT_VAR,CC,cc)) -$(eval $(call DEFAULT_VAR,CXX,c++)) - -# Same thing for "ld" (the linker). -$(eval $(call DEFAULT_VAR,LD,ld)) +# Determine the build tools used automatically. +ifeq ($(TARGETL), amd64) + # Compiler Toolchain + BCC ?= gcc + BCXX ?= g++ + BLD ?= ld + BASM ?= nasm + + # Compiler and linker flags + ARCH_CFLAGS = \ + -m64 \ + -march=x86-64 \ + -mabi=sysv \ + -mno-80387 \ + -mno-mmx \ + -mno-sse \ + -mno-sse2 \ + -mno-red-zone \ + -mcmodel=kernel + + ARCH_LDFLAGS = \ + -m elf_x86_64 \ + -z max-page-size=0x1000 + + ARCH_ASFLAGS = \ + -f elf64 + +else ifeq ($(TARGETL), i386) + # Compiler Toolchain + BCC ?= clang + BCXX ?= clang++ + BLD ?= ld + BASM ?= nasm + + # Compiler and linker flags + ARCH_CFLAGS = \ + -target i686-elf \ + -mno-80387 \ + -mno-mmx \ + -mno-sse \ + -mno-sse2 + + ARCH_LDFLAGS = \ + -z max-page-size=0x1000 + + ARCH_ASFLAGS = \ + -f elf32 + +else + $(error You cannot build for this architecture right now.) +endif # User controllable CFLAGS. CFLAGS ?= -pipe -Wall -Wextra -DTARGET_$(TARGET) -DKERNEL -DIS_KERNEL_MODE $(DEFINES) @@ -127,7 +171,8 @@ CFLAGS += \ -mcmodel=kernel \ -MMD \ -MP \ - -I. + -I. \ + $(ARCH_CFLAGS) # Internal C++ flags that should not be changed by the user. CXXFLAGS += \ @@ -140,25 +185,16 @@ CXXFLAGS += \ -fno-pie \ -fno-pic \ -fno-reorder-functions \ - -m64 \ - -march=x86-64 \ - -mabi=sysv \ - -mno-80387 \ - -mno-mmx \ - -mno-sse \ - -mno-sse2 \ - -mno-red-zone \ - -mcmodel=kernel \ -MMD \ -MP \ -fno-exceptions \ -fno-rtti \ - -I. + -I. \ + $(ARCH_CFLAGS) LDFLAGSBASE += \ -nostdlib \ - -m elf_x86_64 \ - -z max-page-size=0x1000 + $(ARCH_LDFLAGS) # Internal linker flags that should not be changed by the user. LDFLAGS += \ @@ -175,11 +211,12 @@ NASMFLAGS += \ -f elf64 # Use find to glob all *.c, *.S, and *.asm files in the directory and extract the object names. -override CFILES := $(shell find $(SRC_DIR) -not -path '*/.*' -type f -name '*.c') -override CXXFILES := $(shell find $(SRC_DIR) -not -path '*/.*' -type f -name '*.cpp') -override ASFILES := $(shell find $(SRC_DIR) -not -path '*/.*' -type f -name '*.S') -override NASMFILES := $(shell find $(SRC_DIR) -not -path '*/.*' -type f -name '*.asm') -override OBJ := $(patsubst %.o,%.$(TARGETL).o,$(patsubst $(SRC_DIR)/%,$(BUILD_DIR)/%,$(CFILES:.c=.o) $(CXXFILES:.cpp=.o) $(ASFILES:.S=.o) $(NASMFILES:.asm=.o))) +EXCLUDE_WRONG_ARCH = '(' '(' -path 'source/ke/$(TARGETL)/*' ')' -o '(' -path 'source/mm/$(TARGETL)/*' ')' -o '(' -not -path 'source/ke/*/*' -not -path 'source/mm/*/*' ')' ')' +override CFILES := $(shell find $(SRC_DIR) -not -path '*/.*' $(EXCLUDE_WRONG_ARCH) -type f -name '*.c') +override CXXFILES := $(shell find $(SRC_DIR) -not -path '*/.*' $(EXCLUDE_WRONG_ARCH) -type f -name '*.cpp') +override ASFILES := $(shell find $(SRC_DIR) -not -path '*/.*' $(EXCLUDE_WRONG_ARCH) -type f -name '*.S') +override NASMFILES := $(shell find $(SRC_DIR) -not -path '*/.*' $(EXCLUDE_WRONG_ARCH) -type f -name '*.asm') +override OBJ := $(patsubst %.o,%.$(TARGETL).o,$(patsubst $(SRC_DIR)/%,$(BUILD_DIR)/%,$(CFILES:.c=.o) $(CXXFILES:.cpp=.o) $(ASFILES:.S=.o) $(NASMFILES:.asm=.o))) override HEADER_DEPS := $(patsubst %.o,%.d,$(OBJ)) # It's kind of a mess, but basically, here's what everything does: @@ -202,26 +239,26 @@ CFLAGS += -D__BORON_MAJOR=$(VER_MAJOR) -D__BORON_MINOR=$(VER_MINOR) -D__BORON_BU # Link rules for the final kernel executable. $(KERNEL): $(SYMBOLS) @echo "[LD]\tBuilding $(KERNEL)" - @$(LD) $(OBJ) $(SYMBOLS) $(LDFLAGS) -o $@ + @$(BLD) $(OBJ) $(SYMBOLS) $(LDFLAGS) -o $@ $(SYMBOLS): $(KERNEL2) @echo "[NM]\tDumping and compiling symbols" @nm -P $(KERNEL2) | $(SCRIPTS_DIR)/generate_symbols.py > $(BUILD_DIR)/_symtab.$(TARGETL).asm - @nasm $(NASMFLAGS) $(BUILD_DIR)/_symtab.$(TARGETL).asm -o $(BUILD_DIR)/_symtab.$(TARGETL).o + @$(BASM) $(NASMFLAGS) $(BUILD_DIR)/_symtab.$(TARGETL).asm -o $(BUILD_DIR)/_symtab.$(TARGETL).o $(KERNEL2): $(KERNEL_PARTIAL) @echo "[LD]\tLinking kernel to extract symbols" - @$(LD) $(KERNEL_PARTIAL) -static $(LDFLAGS) -o $@ + @$(BLD) $(KERNEL_PARTIAL) -static $(LDFLAGS) -o $@ # Link rules for the amalgam object file. $(KERNEL_PARTIAL): $(OBJ) $(LINKER_FILE) @echo "[LD]\tPartially linking kernel" - @$(LD) -r $(OBJ) $(LDFLAGSBASE) -o $@ + @$(BLD) -r $(OBJ) $(LDFLAGSBASE) -o $@ $(BUILD_DIR)/ke/version.$(TARGETL).o: $(filter-out $(BUILD_DIR)/ke/version.$(TARGETL).o, $(OBJ)) $(SRC_DIR)/ke/version.c @echo "[CC]\tCompiling $(SRC_DIR)/ke/version.c" @mkdir -p $(dir $@) - @$(CC) $(CPPFLAGS) $(CFLAGS) -c $(SRC_DIR)/ke/version.c -o $@ + @$(BCC) $(CPPFLAGS) $(CFLAGS) -c $(SRC_DIR)/ke/version.c -o $@ @echo "[MK]\tIncrementing build number" @echo $$(($(VER_BUILD) + 1)) > $(BUILD_NUMBER_FILE) @@ -232,25 +269,25 @@ $(BUILD_DIR)/ke/version.$(TARGETL).o: $(filter-out $(BUILD_DIR)/ke/version.$(TAR $(BUILD_DIR)/%.$(TARGETL).o: $(SRC_DIR)/%.c @echo "[CC]\tCompiling $<" @mkdir -p $(dir $@) - @$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ + @$(BCC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ # Compilation rules for *.cpp files. $(BUILD_DIR)/%.$(TARGETL).o: $(SRC_DIR)/%.cpp @echo "[CXX]\tCompiling $<" @mkdir -p $(dir $@) - @$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@ + @$(BCXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@ # Compilation rules for *.S files. $(BUILD_DIR)/%.$(TARGETL).o: $(SRC_DIR)/%.S @echo "[AS]\tCompiling $<" @mkdir -p $(dir $@) - @$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ + @$(BCC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ # Compilation rules for *.asm (nasm) files. $(BUILD_DIR)/%.$(TARGETL).o: $(SRC_DIR)/%.asm @echo "[AS]\tCompiling $<" @mkdir -p $(dir $@) - @nasm $(NASMFLAGS) $< -o $@ + @$(BASM) $(NASMFLAGS) $< -o $@ # Remove object files and the final executable. .PHONY: clean diff --git a/boron/linker.ld b/boron/linker.amd64.ld similarity index 100% rename from boron/linker.ld rename to boron/linker.amd64.ld diff --git a/boron/source/build_number b/boron/source/build_number index b0d73241..6d3e9dc7 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -129 +129 \ No newline at end of file From 0a5a3bef35e7858b23d506f698edecf3911e620f Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Tue, 14 Oct 2025 21:01:03 +0300 Subject: [PATCH 02/44] * X86: The code now compiles, but doesn't link yet. --- boron/Makefile | 37 +++-- boron/address_space.txt | 10 +- boron/address_space_i386.txt | 55 +++++++ boron/include/arch.h | 6 +- boron/include/arch/amd64.h | 6 +- boron/include/arch/i386.h | 283 ++++++++++++++++++++++++++++++++++ boron/include/arch/i386/ipl.h | 40 +++++ boron/include/arch/ipl.h | 6 +- boron/include/io/fcb.h | 1 + boron/include/ke/sched.h | 3 + boron/include/mm/mdl.h | 2 +- boron/include/mm/pfn.h | 4 +- boron/include/ob.h | 5 + boron/source/cc/vclru.c | 1 + boron/source/ke/except.c | 27 +++- boron/source/ke/sched.c | 4 +- boron/source/ke/tests.c | 5 + boron/source/ldr/initroot.c | 2 +- boron/source/ldr/loader.c | 14 +- boron/source/mm/amd64/pt.c | 2 +- boron/source/mm/fault.c | 2 +- boron/source/mm/mi.h | 21 ++- boron/source/mm/pmm.c | 99 +++++++++++- boron/source/mm/poolsup.c | 40 ++++- boron/source/ps/psp.h | 13 +- boron/source/rtl/elf.c | 28 +++- boron/source/rtl/rbtree.c | 1 + common/include/elf.h | 21 +++ common/include/mms.h | 2 +- common/include/rtl/check64.h | 12 +- 30 files changed, 677 insertions(+), 75 deletions(-) create mode 100644 boron/address_space_i386.txt create mode 100644 boron/include/arch/i386.h create mode 100644 boron/include/arch/i386/ipl.h diff --git a/boron/Makefile b/boron/Makefile index dcb68a8d..f69ef36e 100644 --- a/boron/Makefile +++ b/boron/Makefile @@ -84,6 +84,13 @@ ifeq ($(TARGETL), amd64) BASM ?= nasm # Compiler and linker flags + # + # NOTE 7.7.2024 -- No-reorder-functions was added because a certain functions + # was generating an "unlikely" section, which was placed at different addresses + # in kernel.elf and kernel2.elf, screwing up the symbol table... That's pretty + # bad. + # + # TODO: fix above ^^^ ARCH_CFLAGS = \ -m64 \ -march=x86-64 \ @@ -93,7 +100,8 @@ ifeq ($(TARGETL), amd64) -mno-sse \ -mno-sse2 \ -mno-red-zone \ - -mcmodel=kernel + -mcmodel=kernel \ + -fno-reorder-functions ARCH_LDFLAGS = \ -m elf_x86_64 \ @@ -102,6 +110,8 @@ ifeq ($(TARGETL), amd64) ARCH_ASFLAGS = \ -f elf64 + SMP = yes + else ifeq ($(TARGETL), i386) # Compiler Toolchain BCC ?= clang @@ -123,10 +133,16 @@ else ifeq ($(TARGETL), i386) ARCH_ASFLAGS = \ -f elf32 + SMP = no + else $(error You cannot build for this architecture right now.) endif +ifeq ($(SMP), yes) + ARCH_CFLAGS += -DCONFIG_SMP +endif + # User controllable CFLAGS. CFLAGS ?= -pipe -Wall -Wextra -DTARGET_$(TARGET) -DKERNEL -DIS_KERNEL_MODE $(DEFINES) @@ -143,32 +159,15 @@ NASMFLAGS ?= -F dwarf -I$(SRC_DIR) -I$(INC_DIR) -I$(DDKI_DIR) LDFLAGS ?= # Internal C flags that should not be changed by the user. -# -# NOTE 7.7.2024 -- No-reorder-functions was added because a certain functions -# was generating an "unlikely" section, which was placed at different addresses -# in kernel.elf and kernel2.elf, screwing up the symbol table... That's pretty -# bad. -# -# TODO: fix above ^^^ CFLAGS += \ -fno-omit-frame-pointer \ - -std=c11 \ + -std=c2x \ -ffreestanding \ -fno-stack-protector \ -fno-stack-check \ -fno-lto \ -fno-pie \ -fno-pic \ - -fno-reorder-functions \ - -m64 \ - -march=x86-64 \ - -mabi=sysv \ - -mno-80387 \ - -mno-mmx \ - -mno-sse \ - -mno-sse2 \ - -mno-red-zone \ - -mcmodel=kernel \ -MMD \ -MP \ -I. \ diff --git a/boron/address_space.txt b/boron/address_space.txt index ed9728fa..44e27610 100644 --- a/boron/address_space.txt +++ b/boron/address_space.txt @@ -26,8 +26,6 @@ BORON Operating System Address Space (AMD64) | higher half direct map | +------------------------------+ - 0xFFFF800000000000 -NOTE: file system caching will be implemented in the future - ** User Mode ** Currently this is nothing more than a plan. @@ -39,13 +37,13 @@ However, this is the plan for normal user processes. +-----------------------------+ - 0x0000800000000000 | process environment block | -+-----------------------------+ - 0x00007ffffffff000 ++-----------------------------+ - 0x00007FFFFFFFE000 | operating system DLLs | -+-----------------------------+ - 0x00007fff00000000 ++-----------------------------+ - 0x00007FFF00000000 | thread environment blocks | -+-----------------------------+ - 0x00007ffe00000000 ++-----------------------------+ - 0x00007FFE00000000 | thread stacks | -+-----------------------------+ - 0x00007e0000000000 ++-----------------------------+ - 0x00007E0000000000 | user mappings | +-----------------------------+ - 0x0000500000000000 | user heap | diff --git a/boron/address_space_i386.txt b/boron/address_space_i386.txt new file mode 100644 index 00000000..e8662769 --- /dev/null +++ b/boron/address_space_i386.txt @@ -0,0 +1,55 @@ +BORON Operating System Address Space (i386) + +** Kernel Mode ** + ++------------------------------+ - 0xFFFFFFFF +| virtually linear page tables | ++------------------------------+ - 0xFFC00000 +| unused | ++------------------------------+ - 0xF0000000 +| more dynamic pool space | ++------------------------------+ - 0xD0000000 +| page frame data base | ++------------------------------+ - 0xC2000000 +| system module DLLs | ++------------------------------+ - 0xC1000000 +| fast mapping in 8MB windows | ++------------------------------+ - 0xC0800000 +| kernel code & data | ++------------------------------+ - 0xC0001000 +| fast page mapping | ++------------------------------+ - 0xC0000000 +| dynamic pool space | ++------------------------------+ - 0x80000000 +| higher half direct map | ++------------------------------+ - 0x00000000 + +Notes: +- if we use the multiboot boot protocol, then "kernel code & data" really means "first 8MB of physical memory" + +** User Mode ** + +Currently this is nothing more than a plan. +Now, a user process has complete control over its own +address space. (They will even be able to unmap the +PEB/TEBs if they want as well!) + +However, this is the plan for normal user processes. + ++-----------------------------+ - 0x80000000 +| process environment block | ++-----------------------------+ - 0x7ffe0000 +| thread environment blocks | ++-----------------------------+ - 0x7fe00000 +| operating system DLLs | ++-----------------------------+ - 0x78000000 +| thread stacks | ++-----------------------------+ - 0x70000000 +| user DLLs | ++-----------------------------+ - 0x60000000 +| user heap + mappings | ++-----------------------------+ - 0x10000000 +| program executable | ++-----------------------------+ - 0x00001000 +| | ++-----------------------------+ - 0x00000000 diff --git a/boron/include/arch.h b/boron/include/arch.h index 0ee0675f..801efee0 100644 --- a/boron/include/arch.h +++ b/boron/include/arch.h @@ -4,8 +4,10 @@ #include // ==== Platform specific defintions ==== -#ifdef TARGET_AMD64 +#if defined TARGET_AMD64 #include +#elif defined TARGET_I386 +#include #endif // ==== Forward declarations. Depending on the platform, we'll include platform specific definitions. ==== @@ -13,7 +15,7 @@ typedef struct KREGISTERS_tag KREGISTERS, *PKREGISTERS; // List of registers. // Functions that do different things based on architecture, // but exist everywhere -#ifdef TARGET_AMD64 +#if defined TARGET_AMD64 || defined TARGET_I386 FORCE_INLINE void KeWaitForNextInterrupt(void) diff --git a/boron/include/arch/amd64.h b/boron/include/arch/amd64.h index 894ea386..e8dbf164 100644 --- a/boron/include/arch/amd64.h +++ b/boron/include/arch/amd64.h @@ -76,11 +76,11 @@ MMADDRESS_CONVERT; #define MM_PTE_PAGESIZE (1ULL << 7) // in terms of PML3/PML2 entries, for 1GB/2MB pages respectively. Not Used by the kernel #define MM_PTE_GLOBAL (1ULL << 8) // doesn't invalidate the pages from the TLB when CR3 is changed #define MM_PTE_ISFROMPMM (1ULL << 9) // if the allocated memory is managed by the PFN database -#define MM_PTE_COW (1ULL << 10) // if this page is to be copied after a write -#define MM_PTE_TRANSITION (1ULL << 11) // if this page is in transition (3) +#define MM_PTE_COW (1ULL << 10) // if this page is to be copied after a write -- TODO: We are supposed to be phasing this one out. +#define MM_PTE_TRANSITION (1ULL << 11) // if this page is in transition (3) (UNUSED) #define MM_PTE_NOEXEC (1ULL << 63) // aka eXecute Disable #define MM_PTE_PKMASK (15ULL<< 59) // protection key mask. We will not use it. -#define MM_PTE_ISPOOLHDR (1ULL << 58) // if the PTE actually contains the address of a pool entry (subtracted MM_KERNEL_SPACE_BASE from it) +#define MM_PTE_ISPOOLHDR (1ULL << 58) // if the PTE actually contains the address of a pool entry (subtracted MM_KERNEL_SPACE_BASE from it) (NOTE: This is supposed to be MM_DPTE_ISPOOLHDR) #define MM_PTE_ADDRESSMASK (0x000FFFFFFFFFF000) // description of the other bits that aren't 1 in the mask: // 63 - execute disable diff --git a/boron/include/arch/i386.h b/boron/include/arch/i386.h new file mode 100644 index 00000000..f3a5a442 --- /dev/null +++ b/boron/include/arch/i386.h @@ -0,0 +1,283 @@ +/*** + The Boron Operating System + Copyright (C) 2025 iProgramInCpp + +Module name: + arch/i386.h + +Abstract: + This header file contains the constant definitions for + the i386 platform. + +Author: + iProgramInCpp - 14 October 2025 +***/ +#pragma once + +#ifdef KERNEL + +// start PML2 index will be 512. The PFN database's is 776 +#define MI_GLOBAL_AREA_START (512) +#define MI_GLOBAL_AREA_START_2ND (832) + +#define MI_RECURSIVE_PAGING_START (511) + +#define MI_PML2_LOCATION ((uintptr_t)0xFFFFF000U) +#define MI_PML1_LOCATION ((uintptr_t)0xFFC00000U) +#define MI_PML1_LOC_END ((uint64_t)0x100000000U) +#define MI_PML_ADDRMASK ((uintptr_t)0xFFFFF000U) + +// MmGetHHDMOffsetAddr and other HHDM-related calls aren't implemented +// using an HHDM on 32-bit. Instead, they're implemented via 8MB windows +// that get spawned in everytime MmGetHHDMOffsetAddr is called with a +// different 8MB region. +#define MI_FASTMAP_START ((uintptr_t)0xC0800000U) +#define MI_FASTMAP_MASK ((uintptr_t)0xFF800000U) + +typedef union +{ + struct + { + uintptr_t PageOffset : 12; + uintptr_t Level1Index : 10; + uintptr_t Level2Index : 10; + }; + + uintptr_t Long; +} +MMADDRESS_CONVERT; + +#define MM_KERNEL_SPACE_BASE (0x80000000U) +#define MM_USER_SPACE_END (0x7FFFFFFFU) + +#define MM_PFNDB_BASE (0xC2000000U) + +#define MM_PTE_PRESENT (1 << 0) +#define MM_PTE_READWRITE (1 << 1) +#define MM_PTE_USERACCESS (1 << 2) +#define MM_PTE_WRITETHRU (1 << 3) +#define MM_PTE_CDISABLE (1 << 4) +#define MM_PTE_ACCESSED (1 << 5) +#define MM_PTE_DIRTY (1 << 6) +#define MM_PTE_PAT (1 << 7) +#define MM_PTE_PAGESIZE (1 << 7) // for 4MB pages. not used by the kernel +#define MM_PTE_GLOBAL (1 << 8) // doesn't invalidate the pages from the TLB when CR3 is changed +#define MM_PTE_ISFROMPMM (1 << 9) // if the allocated memory is managed by the PFN database +#define MM_PTE_COW (1 << 10) // if this page is to be copied after a write + +#define MM_PTE_NOEXEC (0) // no such thing on 32-bit (without PAE) +#define MM_PTE_PKMASK (0) // no such thing on 32-bit + +#define MM_PTE_ADDRESSMASK (0xFFFFF000U) + +// Disabled PTE (present bit is zero): +// bits 0..2 - Permission bits as usual +// bit 3 - Is decommitted (was previously committed but is no longer) +// bit 8 - Is demand paged +// bit 9 - 0 if from PMM (anonymous), 1 if mapped from a file +// bit 10 - Was swapped to pagefile (2) +// bit 30 - used by the unmap code, see (1) + +// NOTES: +// +// (1) - If MM_DPTE_WASPRESENT is set, it's treated as a regular PTE in terms of flags, except that MM_PTE_PRESENT is zero. +// It contains a valid PMM address which should be freed. +// +// (2) - If MM_DPTE_SWAPPED is set, bits 52...12 represent the offset into the pagefile, and bits 57...53 mean the pagefile index. +// +// (3) - If the PTE is in transition, then the physical page is part of either the standby or the modified page list. + +#define MM_DPTE_DECOMMITTED (1 << 3) +#define MM_DPTE_COMMITTED (1 << 8) +#define MM_DPTE_BACKEDBYFILE (1 << 9) +#define MM_DPTE_SWAPPED (1 << 10) +#define MM_DPTE_WASPRESENT (1 << 30) +#define MM_PTE_ISPOOLHDR (1 << 31) // if the PTE actually contains the address of a pool entry (subtracted MM_KERNEL_SPACE_BASE from it) + +// Page fault reasons +#define MM_FAULT_PROTECTION (1 << 0) // 0: Page wasn't marked present; 1: Page protection violation (e.g. writing to a readonly page) +#define MM_FAULT_WRITE (1 << 1) // 0: Caused by a read; 1: Caused by a write +#define MM_FAULT_USER (1 << 2) // 1: Fault was caused in user mode +#define MM_FAULT_INSNFETCH (1 << 0) // 1: Attempted to execute code from a page marked with the NOEXEC bit + +#define PAGE_SIZE (0x1000) + +typedef uint32_t MMPTE, *PMMPTE; + +// bits 0.11 - Offset within the page +// bits 12..21 - Index within the PML1 +// bits 22..31 - Index within the PML2 +#define PML1_IDX(addr) (((addr) >> 12) & 0x3FF) +#define PML2_IDX(addr) (((addr) >> 22) & 0x3FF) + +// ======== model specific registers ======== +#define MSR_FS_BASE (0xC0000100) +#define MSR_GS_BASE (0xC0000101) +#define MSR_GS_BASE_KERNEL (0xC0000102) +#define MSR_IA32_EFER (0xC0000080) +#define MSR_IA32_STAR (0xC0000081) +#define MSR_IA32_LSTAR (0xC0000082) +#define MSR_IA32_FMASK (0xC0000084) + +#define MSR_IA32_EFER_SCE (1 << 0) // SYSCALL/SYSRET enable +#define MSR_IA32_EFER_LME (1 << 8) // Long Mode enable - Limine sets this +#define MSR_IA32_EFER_LMA (1 << 10) // Long Mode active +#define MSR_IA32_EFER_NXE (1 << 11) // No Execute enable + +struct KREGISTERS_tag +{ + // Old IPL + uint32_t OldIpl; + + uint32_t Ebp; + uint32_t Sfra; // stack frame return address + + // Registers pushed by KiTrapCommon. Pushed in reverse order from how they're laid out. + uint32_t Cr2; + uint32_t Edi, Esi; + uint32_t CsDupl; + uint32_t Edx, Ecx, Ebx, Eax; + + // Registers pushed by each trap handler + uint32_t IntNumber; + uint32_t ErrorCode; + + // Registers pushed by the CPU when handling the interrupt + uint32_t Eip; + uint32_t Cs; + uint32_t Eflags; + // NOTE: these are only valid and used if the privilege level is different! + // (e.g. if we interrupted user mode), so DO NOT rely on setting these + // when returning to kernel mode + uint32_t Esp; + uint32_t Ss; +}; + +#include + +// IDT +#define C_IDT_MAX_ENTRIES (0x100) + +#define INTV_DBL_FAULT (0x08) +#define INTV_PROT_FAULT (0x0D) +#define INTV_PAGE_FAULT (0x0E) + +typedef struct KIDT_ENTRY_tag +{ + // bytes 0 and 1 + uint64_t OffsetLow : 16; + // bytes 2 and 3 + uint64_t SegmentSel : 16; + // byte 4 + uint64_t IST : 3; + uint64_t Reserved0 : 5; + // byte 5 + uint64_t GateType : 4; + uint64_t Reserved1 : 1; + uint64_t DPL : 2; + uint64_t Present : 1; + // bytes 6, 7 + uint64_t OffsetHigh : 16; +} +PACKED +KIDT_ENTRY, *PKIDT_ENTRY; + +typedef struct +{ + KIDT_ENTRY Entries[C_IDT_MAX_ENTRIES]; +} +KIDT, *PKIDT; + +// GDT + +// Note: Data and code segments are *swapped* for user mode because +// for whatever reason AMD or intel decided they just *had* to take +// the value of IA32_STAR 63:48 and add 16 to it. for "no reason". +// +// surely there was a reason... right? +#define SEG_NULL (0x00) +#define SEG_RING_0_CODE (0x08) +#define SEG_RING_0_DATA (0x10) +#define SEG_RING_3_DATA (0x18) +#define SEG_RING_3_CODE (0x20) +#define C_GDT_SEG_COUNT (5) + +// note: not packed, so this struct will get padding after each uint16_t +typedef struct +{ + uint16_t Link; + uint32_t Esp0; + uint16_t Ss0; + uint32_t Esp1; + uint16_t Ss1; + uint32_t Esp2; + uint16_t Ss2; + uint32_t Cr3; + uint32_t Eip; + uint32_t Eflags; + uint32_t Eax; + uint32_t Ecx; + uint32_t Edx; + uint32_t Ebx; + uint32_t Esp; + uint32_t Ebp; + uint32_t Esi; + uint32_t Edi; + uint16_t Es, Pad0; + uint16_t Cs, Pad1; + uint16_t Ss, Pad2; + uint16_t Ds, Pad3; + uint16_t Fs, Pad4; + uint16_t Gs, Pad5; + uint16_t Ldtr, Pad6; + uint16_t Pad7, Iopb; + uint32_t Ssp; +} +KTSS; + +typedef union KGDT_ENTRY_tag +{ + struct + { + uint64_t Limit1 : 16; + uint64_t Base1 : 24; + uint64_t Access : 8; + uint64_t Limit2 : 4; + uint64_t Flags : 4; + uint64_t Base2 : 16; + } + PACKED; + + uint64_t Entry; +} +PACKED +KGDT_ENTRY; + +// Global Descriptor Table +typedef struct +{ + uint64_t Segments[C_GDT_SEG_COUNT]; + KGDT_ENTRY TssEntry; +} +KGDT; + +typedef struct +{ + KGDT Gdt; + KTSS Tss; +} +KARCH_DATA, *PKARCH_DATA; + +// Manual interrupt disabling functions. Should instead use +// KeDisableInterrupts and KeRestoreInterrupts, but these have +// their uses too. +#define DISABLE_INTERRUPTS() ASM("cli") +#define ENABLE_INTERRUPTS() ASM("sti") + +// MSI message data register +#define MSI_TRIGGERLEVEL (1 << 15) +#define MSI_LEVELASSERT (1 << 14) + +#include + +#endif diff --git a/boron/include/arch/i386/ipl.h b/boron/include/arch/i386/ipl.h new file mode 100644 index 00000000..9ab6c96a --- /dev/null +++ b/boron/include/arch/i386/ipl.h @@ -0,0 +1,40 @@ +/*** + The Boron Operating System + Copyright (C) 2025 iProgramInCpp + +Module name: + arch/i386/ipl.h + +Abstract: + This header file contains the constant IPL definitions + for the i386 platform. + +Author: + iProgramInCpp - 14 October 2025 +***/ +#ifndef BORON_ARCH_IPL_I386_H +#define BORON_ARCH_IPL_I386_H + +// TODO: Currently copied from AMD64. +typedef enum KIPL_tag +{ + IPL_UNDEFINED = -1, + IPL_NORMAL = 0x0, // business as usual + IPL_APC = 0x3, // asynch procedure calls. Page faults only allowed up to this IPL + IPL_DPC = 0x4, // deferred procedure calls and the scheduler + IPL_DEVICES0 = 0x5, // tier 1 for devices (keyboard, mouse) + IPL_DEVICES1 = 0x6, // tier 2 for devices + IPL_DEVICES2 = 0x7, + IPL_DEVICES3 = 0x8, + IPL_DEVICES4 = 0x9, + IPL_DEVICES5 = 0xA, + IPL_DEVICES6 = 0xB, + IPL_DEVICES7 = 0xC, + IPL_DEVICES8 = 0xD, + IPL_CLOCK = 0xE, // for clock timers + IPL_NOINTS = 0xF, // total control of the CPU. Interrupts are disabled in this IPL and this IPL only. + IPL_COUNT, +} +KIPL, *PKIPL; + +#endif//BORON_ARCH_IPL_I386_H diff --git a/boron/include/arch/ipl.h b/boron/include/arch/ipl.h index 2ce09c99..cd7a7f87 100644 --- a/boron/include/arch/ipl.h +++ b/boron/include/arch/ipl.h @@ -15,10 +15,12 @@ Module name: #ifndef BORON_ARCH_IPL_H #define BORON_ARCH_IPL_H -#ifdef TARGET_AMD64 +#if defined TARGET_AMD64 #include +#elif defined TARGET_I386 +#include #else -#error Hello +#error Implement ipl.h for your architecture! #endif #endif//BORON_ARCH_IPL_H diff --git a/boron/include/io/fcb.h b/boron/include/io/fcb.h index bc07e4d2..702b92d8 100644 --- a/boron/include/io/fcb.h +++ b/boron/include/io/fcb.h @@ -38,6 +38,7 @@ typedef struct _FCB uint32_t Flags; // Valid only for files and block devices. Otherwise it's zero. + __attribute__((aligned(8))) uint64_t FileLength; // FSD specific extension. When the FCB is initialized, the diff --git a/boron/include/ke/sched.h b/boron/include/ke/sched.h index dad242ef..239f11db 100644 --- a/boron/include/ke/sched.h +++ b/boron/include/ke/sched.h @@ -67,6 +67,9 @@ typedef struct _KSCHEDULER int ThreadsOnQueueCount; +#if IS_32_BIT + __attribute__((aligned(8))) +#endif uint64_t TicksSpentNonIdle; // in ticks, copy of CurrentThread->QuantumUntil unless diff --git a/boron/include/mm/mdl.h b/boron/include/mm/mdl.h index d58ba2d6..9dc24796 100644 --- a/boron/include/mm/mdl.h +++ b/boron/include/mm/mdl.h @@ -49,7 +49,7 @@ typedef struct _MDL uintptr_t MappedStartVA; // The virtual address where this MDL is mapped into system memory PEPROCESS Process; // Process these pages belong to size_t NumberPages; // Size of the page frame number list - MMPFN Pages[]; + MMPFN Pages[0]; } MDL, *PMDL; diff --git a/boron/include/mm/pfn.h b/boron/include/mm/pfn.h index 1ba6852c..77ec2ee0 100644 --- a/boron/include/mm/pfn.h +++ b/boron/include/mm/pfn.h @@ -95,7 +95,9 @@ typedef struct uint32_t _Fcb; uint32_t _OffsetLower; - }; + } + PACKED + FileCache; }; #endif } diff --git a/boron/include/ob.h b/boron/include/ob.h index 56cec0b2..67c02604 100644 --- a/boron/include/ob.h +++ b/boron/include/ob.h @@ -147,6 +147,11 @@ struct _OBJECT_HEADER // among other things, to ensure that non-object-manager-managed objects aren't accidentally // used. int Signature; + +#ifdef IS_32_BIT + // This dummy is present to make sure the object header's struct size is aligned to 8 bytes. + int Dummy; +#endif #endif // Object behavior flags. diff --git a/boron/source/cc/vclru.c b/boron/source/cc/vclru.c index 7e72b051..82159e85 100644 --- a/boron/source/cc/vclru.c +++ b/boron/source/cc/vclru.c @@ -94,6 +94,7 @@ void CcPurgeViewsOverLimit(int LeaveSpaceFor) while (AtLoad(CcViewCacheLruSize) > Limit) { + // TODO: THIS DOES NOT WORK. You must NOT remove the head of view cache!!! PMMVAD Vad = CcRemoveHeadOfViewCacheLru(); MmUnmapViewOfFileInSystemSpace((void*) Vad->Node.StartVa, true); diff --git a/boron/source/ke/except.c b/boron/source/ke/except.c index 1a819f6b..263d2c1f 100644 --- a/boron/source/ke/except.c +++ b/boron/source/ke/except.c @@ -16,24 +16,36 @@ Module name: #include "ki.h" #include -#ifdef TARGET_AMD64 +#if defined TARGET_AMD64 + #define KI_EXCEPTION_HANDLER_INIT() \ UNUSED uint64_t FaultPC = TrapFrame->rip; \ UNUSED uint64_t FaultAddress = TrapFrame->cr2; \ UNUSED uint64_t FaultMode = TrapFrame->ErrorCode; \ + UNUSED int Vector = (int)TrapFrame->IntNumber + +#elif defined TARGET_I386 + +#define KI_EXCEPTION_HANDLER_INIT() \ + UNUSED uint32_t FaultPC = TrapFrame->Eip; \ + UNUSED uint32_t FaultAddress = TrapFrame->Cr2; \ + UNUSED uint64_t FaultMode = TrapFrame->ErrorCode; \ UNUSED int Vector = TrapFrame->IntNumber + #else + #error Go implement KI_EXCEPTION_HANDLER_INIT! + #endif void KeOnUnknownInterrupt(PKREGISTERS TrapFrame) { KI_EXCEPTION_HANDLER_INIT(); -#ifdef TARGET_AMD64 -#define SPECIFIER "%02x" +#if defined TARGET_AMD64 || defined TARGET_I386 +#define SPECIFIER "0x%02x" #else -#define SPECIFIER "%08x" +#define SPECIFIER "0x%08x" #endif DbgPrint("** Unknown interrupt " SPECIFIER " at %p on CPU %u", Vector, FaultPC, KeGetCurrentPRCB()->LapicId); KeCrash("Unknown interrupt " SPECIFIER " at %p on CPU %u", Vector, FaultPC, KeGetCurrentPRCB()->LapicId); @@ -116,6 +128,13 @@ void KeOnPageFault(PKREGISTERS TrapFrame) return; + #elif defined TARGET_I386 + + TrapFrame->Eip = (uint32_t) MmProbeAddressSubEarlyReturn; + TrapFrame->Eax = (uint32_t) STATUS_FAULT; + + return; + #else #error Hey! diff --git a/boron/source/ke/sched.c b/boron/source/ke/sched.c index 39a1c87e..1c4773e9 100644 --- a/boron/source/ke/sched.c +++ b/boron/source/ke/sched.c @@ -812,7 +812,7 @@ void KeReadyThread(PKTHREAD Thread) NO_RETURN void KeTerminateThread(KPRIORITY Increment) { - KIPL Ipl = KiLockDispatcher(); + UNUSED KIPL Ipl = KiLockDispatcher(); PKTHREAD Thread = KeGetCurrentThread(); @@ -828,7 +828,7 @@ NO_RETURN void KeTerminateThread(KPRIORITY Increment) // Unlock the dispatcher and request an end to current quantum. KiUnlockDispatcher(IPL_DPC); - KiHandleQuantumEnd(Ipl); + KiHandleQuantumEnd(); KeCrash("KeTerminateThread: After yielding, terminated thread was scheduled back in"); } diff --git a/boron/source/ke/tests.c b/boron/source/ke/tests.c index d59612b6..13f78a30 100644 --- a/boron/source/ke/tests.c +++ b/boron/source/ke/tests.c @@ -15,6 +15,9 @@ Module name: #include "../mm/mi.h" // horrible for now #include +// These tests are actually unused and they're only vestigial. - 14/10/2025 +#if 0 + static void KepTestAllMemory(void* Memory, size_t Size) { uint8_t* MemBytes = Memory; @@ -204,3 +207,5 @@ void KiPerformTests() KiPerformPageMapTest(); LogMsg("CPU %d finished all tests", KeGetCurrentPRCB()->LapicId); } + +#endif diff --git a/boron/source/ldr/initroot.c b/boron/source/ldr/initroot.c index a9c4d375..f6f31e27 100644 --- a/boron/source/ldr/initroot.c +++ b/boron/source/ldr/initroot.c @@ -82,7 +82,7 @@ POBJECT_DIRECTORY LdriCreateInitialDir() if (FAILED(Status)) { DbgPrint("Ldr: Failed to create /InitRoot directory (%d)", Status); - return false; + return NULL; } return RootDir; diff --git a/boron/source/ldr/loader.c b/boron/source/ldr/loader.c index dd079ef3..abb375e8 100644 --- a/boron/source/ldr/loader.c +++ b/boron/source/ldr/loader.c @@ -13,13 +13,21 @@ Module name: ***/ #include "ldri.h" -static uintptr_t LdrpCurrentBase = 0xFFFFF00000000000; - // TODO: Perhaps we could define it from the command line? Something like /HAL= #ifdef TARGET_AMD64 + +static uintptr_t LdrpCurrentBase = 0xFFFFF00000000000; static const char* LdrpHalPath = "halx86.sys"; + +#elif defined TARGET_I386 + +static uintptr_t LdrpCurrentBase = 0xC0800000; +static const char* LdrpHalPath = "hali386.sys"; // sorry bucko, halx86 is already taken + #else -#error Define your HAL path here. + +#error Define your loader base and HAL path here. + #endif INIT diff --git a/boron/source/mm/amd64/pt.c b/boron/source/mm/amd64/pt.c index 192af1bb..ba5433ce 100644 --- a/boron/source/mm/amd64/pt.c +++ b/boron/source/mm/amd64/pt.c @@ -268,7 +268,7 @@ PMMPTE MiGetPTEPointer(HPAGEMAP Mapping, uintptr_t Address, bool AllocateMissing MmFreePhysicalPage(PfnsAllocated[i]); } - return false; + return NULL; } PtesModified [NumPfnsAllocated] = EntryPointer; diff --git a/boron/source/mm/fault.c b/boron/source/mm/fault.c index 352dc9ee..6992603b 100644 --- a/boron/source/mm/fault.c +++ b/boron/source/mm/fault.c @@ -315,7 +315,7 @@ static BSTATUS MmpHandleFaultCommittedMappedPage( // If the CCB entry is zero, then it'll be populated with the // new CCB entry. Otherwise, there's something already there // and we have simply wasted our time and we need to refault. - uint64_t Expected = 0; + MMPTE Expected = 0; if (AtCompareExchange(&PCcbEntry->Long, &Expected, CcbEntryNew.Long)) { // Compare-exchange successful. Note: we are surrendering diff --git a/boron/source/mm/mi.h b/boron/source/mm/mi.h index 67945c4b..bfe7dbee 100644 --- a/boron/source/mm/mi.h +++ b/boron/source/mm/mi.h @@ -91,6 +91,10 @@ typedef struct MISLAB_ITEM_tag LIST_ENTRY ListEntry; struct MISLAB_CONTAINER_tag *Parent; +#if IS_32_BIT + int Dummy; // alignment +#endif + char Data[0]; } MISLAB_ITEM, *PMISLAB_ITEM; @@ -164,18 +168,6 @@ HUGE_MEMORY_BLOCK, *PHUGE_MEMORY_BLOCK; // ===== Pool Allocator ===== -#ifdef TARGET_AMD64 - -// One PML4 entry can map up to 1<<39 (512GB) of memory. -// Thus, our pool will be 512 GB in size. -#define MI_POOL_LOG2_SIZE (39) - -#else - -#error "Define the pool size for your platform!" - -#endif - typedef struct MIPOOL_ENTRY_tag { LIST_ENTRY ListEntry; // Qword 0, 1 @@ -251,6 +243,11 @@ void MiPrepareGlobalAreaForPool(HPAGEMAP PageMap); // Get the top of the area managed by the pool allocator. uintptr_t MiGetTopOfPoolManagedArea(); +#ifdef TARGET_I386 +// Get the top of the second area managed by the pool allocator. +uintptr_t MiGetTopOfSecondPoolManagedArea(); +#endif + // Gets the PTE's location in the recursive PTE. PMMPTE MmGetPteLocation(uintptr_t Address); diff --git a/boron/source/mm/pmm.c b/boron/source/mm/pmm.c index 4a5f92ee..ba7cc1f4 100644 --- a/boron/source/mm/pmm.c +++ b/boron/source/mm/pmm.c @@ -28,6 +28,7 @@ Module name: //extern volatile struct limine_hhdm_request KeLimineHhdmRequest; //extern volatile struct limine_memmap_request KeLimineMemMapRequest; +// Free page statistics size_t MmTotalAvailablePages; size_t MmTotalFreePages; @@ -35,13 +36,16 @@ size_t MmTotalFreePages; size_t MmReclaimedPageCount; #endif -uintptr_t MmHHDMBase; - size_t MmGetTotalFreePages() { return MmTotalFreePages; } +// HHDM +#ifdef IS_64_BIT + +uintptr_t MmHHDMBase; + uint8_t* MmGetHHDMBase() { return (uint8_t*)MmHHDMBase; @@ -57,6 +61,56 @@ uintptr_t MmGetHHDMOffsetFromAddr(void* addr) return (uintptr_t) addr - (uintptr_t) MmGetHHDMBase(); } +#else // IS_64_BIT + +#ifdef CONFIG_SMP +#error TODO: Add spinlocks or per-core separation! +#endif + +uintptr_t MmHHDMWindowBase; + +static void MiUpdateHHDMWindowBase(uintptr_t PhysAddr) +{ + PMMPTE Ptes = (PMMPTE)(MI_PML1_LOCATION); + + PhysAddr &= MI_FASTMAP_MASK; + MmHHDMWindowBase = PhysAddr; + + for (size_t i = 0; i < 8 * 1024 * 1024; i += 4096) + { + uintptr_t Address = MI_FASTMAP_START + i; + + MMADDRESS_CONVERT Convert; + Convert.Long = Address; + + Ptes[Convert.Level2Index * 1024 + Convert.Level1Index] = MM_PTE_PRESENT | MM_PTE_READWRITE | MM_PTE_NOEXEC | (PhysAddr + i); + KeInvalidatePage((void*)Address); + } +} + +void* MmGetHHDMOffsetAddr(uintptr_t PhysAddr) +{ + if ((PhysAddr & MI_FASTMAP_MASK) != MmHHDMWindowBase) + MiUpdateHHDMWindowBase(PhysAddr); + + return (void*)(MI_FASTMAP_START + (PhysAddr & ~MI_FASTMAP_MASK)); +} + +uintptr_t MmGetHHDMOffsetFromAddr(void* Addr) +{ + uintptr_t AddrInt = (uintptr_t) Addr; + + if ((AddrInt & MI_FASTMAP_MASK) != MmHHDMWindowBase) + { + DbgPrint("MmGetHHDMOffsetFromAddr: Address %p isn't in the currently selected window!", Addr); + return 0xFFFFFFFF; + } + + return MmHHDMWindowBase + (AddrInt & ~MI_FASTMAP_MASK); +} + +#endif // IS_64_BIT + // Allocates a page from the memmap for eternity during init. Used to prepare the PFN database. // Also used in the initial DLL loader. INIT @@ -173,6 +227,45 @@ static bool MiMapNewPageAtAddressIfNeeded(uintptr_t pageTable, uintptr_t address } } + return true; +#elif defined TARGET_I386 + (void)pageTable; // unused + + MMADDRESS_CONVERT Convert; + Convert.Long = address; + + PMMPTE Level1, Level2; + + Level2 = (PMMPTE)MI_PML2_LOCATION; + Level1 = (PMMPTE)(MI_PML1_LOCATION + 4096 * Convert.Level2Index); + + if (~Level2[Convert.Level2Index] & MM_PTE_PRESENT) + { + uintptr_t Addr = MiAllocatePageFromMemMap(); + + if (!Addr) + { + // TODO: Allow rollback + return false; + } + + Level2[Convert.Level2Index] = Addr | MM_PTE_PRESENT | MM_PTE_READWRITE; + } + + if (~Level1[Convert.Level1Index] & MM_PTE_PRESENT) + { + uintptr_t Addr = MiAllocatePageFromMemMap(); + + if (!Addr) + { + // TODO: Allow rollback + return false; + } + + memset(MmGetHHDMOffsetAddr(Addr), 0, PAGE_SIZE); + Level1[Convert.Level1Index] = Addr | MM_PTE_PRESENT | MM_PTE_READWRITE; + } + return true; #else #error "Implement this for your platform!" @@ -247,7 +340,9 @@ void MiInitPMM() DbgPrint("WARNING: The HHDM isn't at 0xFFFF 8000 0000 0000, things may go wrong! (It's actually at %p)", (void*) KeLoaderParameterBlock.HhdmBase); #endif +#ifdef IS_64_BIT MmHHDMBase = KeLoaderParameterBlock.HhdmBase; +#endif uintptr_t currPageTablePhys = KeGetCurrentPageTable(); diff --git a/boron/source/mm/poolsup.c b/boron/source/mm/poolsup.c index a068e213..2cc9cff0 100644 --- a/boron/source/mm/poolsup.c +++ b/boron/source/mm/poolsup.c @@ -32,6 +32,26 @@ static LIST_ENTRY MmpPoolList; #define MI_EMPTY_TAG MI_TAG(" ") +#ifdef TARGET_AMD64 + +// One PML4 entry can map up to 1<<39 (512GB) of memory. +// Thus, our pool will be 512 GB in size. +#define MI_POOL_LOG2_SIZE (39) + +#elif defined TARGET_I386 + +// There will actually be two arenas of pool space: +// 0x80000000 - 0xC0000000 and 0xD0000000 - 0xF0000000 +#define MI_POOL_LOG2_SIZE (30) + +#define MI_POOL_LOG2_SIZE_2ND (29) + +#else + +#error "Define the pool size for your platform!" + +#endif + INIT void MiInitPool() { @@ -43,6 +63,18 @@ void MiInitPool() Entry->Size = 1ULL << (MI_POOL_LOG2_SIZE - 12); Entry->Address = MiGetTopOfPoolManagedArea(); InsertTailList(&MmpPoolList, &Entry->ListEntry); + +#ifdef TARGET_I386 + + // TODO: Will other 32-bit platforms look similar? + Entry = MiCreatePoolEntry(); + Entry->Flags = 0; + Entry->Tag = MI_EMPTY_TAG; + Entry->Size = 1ULL << (MI_POOL_LOG2_SIZE_2ND - 12); + Entry->Address = MiGetTopOfSecondPoolManagedArea(); + InsertTailList(&MmpPoolList, &Entry->ListEntry); + +#endif } MIPOOL_SPACE_HANDLE MmpSplitEntry(PMIPOOL_ENTRY PoolEntry, size_t SizeInPages, void** OutputAddress, int Tag, uintptr_t UserData) @@ -305,9 +337,13 @@ MIPOOL_SPACE_HANDLE MiGetPoolSpaceHandleFromAddress(void* AddressV) return (MIPOOL_SPACE_HANDLE) NULL; } - ASSERT(*PtePtr & MM_PTE_ISPOOLHDR); + // N.B. This kind of relies on the notion that the address doesn't have + // the valid bit set. + uintptr_t PAddress = *PtePtr; + ASSERT(PAddress & MM_PTE_ISPOOLHDR); + PAddress &= ~MM_PTE_ISPOOLHDR; - MIPOOL_SPACE_HANDLE Handle = ((*PtePtr) & ~MM_PTE_ISPOOLHDR) + MM_KERNEL_SPACE_BASE; + MIPOOL_SPACE_HANDLE Handle = PAddress + MM_KERNEL_SPACE_BASE; MmUnlockKernelSpace(); return Handle; } diff --git a/boron/source/ps/psp.h b/boron/source/ps/psp.h index 08b7f805..29473f8e 100644 --- a/boron/source/ps/psp.h +++ b/boron/source/ps/psp.h @@ -22,8 +22,17 @@ Module name: #define USER_STACK_SIZE (256 * 1024) // Initial Virtual Address Range -#define INITIAL_BEG_VA 0x0000000000001000 -#define INITIAL_END_VA 0x00007FFFFFFFF000 +#ifdef IS_64_BIT + +#define INITIAL_BEG_VA (0x0000000000001000) +#define INITIAL_END_VA (0x00007FFFFFFFF000) + +#else + +#define INITIAL_BEG_VA (0x00001000U) +#define INITIAL_END_VA (0x7FFFF000U) + +#endif typedef struct { diff --git a/boron/source/rtl/elf.c b/boron/source/rtl/elf.c index 956fa6ec..1b17db2a 100644 --- a/boron/source/rtl/elf.c +++ b/boron/source/rtl/elf.c @@ -50,6 +50,7 @@ static bool RtlpComputeRelocation( switch (Type) { + // I prefer to go here with the "Add as you go with no plan" method #ifdef TARGET_AMD64 case R_X86_64_64: *Value = Addend + Symbol; @@ -64,14 +65,25 @@ static bool RtlpComputeRelocation( *Length = sizeof(uint64_t); break; case R_X86_64_GLOB_DAT: - *Value = Symbol; - *Length = sizeof(uint64_t); - break; case R_X86_64_JUMP_SLOT: *Value = Symbol; *Length = sizeof(uint64_t); break; - // I prefer to go here with the "Add as you go with no plan" method +#elif defined TARGET_I386 + case R_386_32: + *Value = Symbol + Addend; + *Length = sizeof(uint32_t); + break; + case R_386_RELATIVE: + *Value = Base + Addend; + *Length = sizeof(uint32_t); + break; + case R_386_GLOB_DAT: + case R_386_JUMP_SLOT: + *Value = Symbol; + *Length = sizeof(uint32_t); + break; + // TODO #else #error Hey! Add ELF relocation types here #endif @@ -130,14 +142,14 @@ static bool RtlpApplyRelocation( } // If there is no pre-resolved symbol and we actually have a symbol index, then look it up - if (ResolvedSymbol == 0 && (Rela.Info >> 32) != 0) - ResolvedSymbol = RtlpResolveSymbolAddress(DynInfo, Rela.Info >> 32, LoadBase); + if (ResolvedSymbol == 0 && ELF_R_SYM(Rela.Info) != 0) + ResolvedSymbol = RtlpResolveSymbolAddress(DynInfo, ELF_R_SYM(Rela.Info), LoadBase); uintptr_t Place = LoadBase + Rela.Offset; uintptr_t Addend = Rela.Addend; uintptr_t Value, Length; - uint32_t RelType = (uint32_t) Rela.Info; // ELF64_R_TYPE(x) => (x & 0xFFFFFFFF) + uint32_t RelType = ELF_R_TYPE(Rela.Info); if (!RtlpComputeRelocation(RelType, Addend, @@ -284,7 +296,7 @@ bool RtlLinkPlt(PELF_DYNAMIC_INFO DynInfo, uintptr_t LoadBase, UNUSED const char // NOTE: PELF_RELA and PELF_REL need to have the same starting members!! PELF_REL Rel = (PELF_REL)((uintptr_t)DynInfo->PltRelocations + i); - PELF_SYMBOL Symbol = &DynInfo->DynSymTable[Rel->Info >> 32]; + PELF_SYMBOL Symbol = &DynInfo->DynSymTable[ELF_R_SYM(Rel->Info)]; uintptr_t SymbolOffset = Symbol->Name; const char* SymbolName = DynInfo->DynStrTable + SymbolOffset; diff --git a/boron/source/rtl/rbtree.c b/boron/source/rtl/rbtree.c index 5ec78203..06a742f3 100644 --- a/boron/source/rtl/rbtree.c +++ b/boron/source/rtl/rbtree.c @@ -31,6 +31,7 @@ int RtlCompareRbTreeNodes(PRBTREE_ENTRY EntryA, PRBTREE_ENTRY EntryB) // // Disable the strict aliasing warning because I'm pretty sure it'll be fine. #pragma GCC diagnostic ignored "-Wstrict-aliasing" +#pragma GCC diagnostic ignored "-Wunused-function" #pragma GCC diagnostic push RB_GENERATE_INTERNAL(_RBTREE_HEAD, _RBTREE_ENTRY, Entry, RtlCompareRbTreeNodes, static inline ALWAYS_INLINE); diff --git a/common/include/elf.h b/common/include/elf.h index 71b8c83f..bf4c2b6c 100644 --- a/common/include/elf.h +++ b/common/include/elf.h @@ -120,6 +120,19 @@ enum R_X86_64_8, R_X86_64_8S, //... +#elif defined TARGET_I386 + R_386_NONE, // none + R_386_32, // S + A + R_386_PC32, // S + A - P + R_386_GOT32, // G + A + R_386_PLT32, // L + A - P + R_386_COPY, // None + R_386_GLOB_DAT, // S + R_386_JUMP_SLOT, // S + R_386_RELATIVE, // B + A + R_386_GOTOFF, // S + A - GOT + R_386_GOTPC, // GOT + A - P + R_386_32PLT, // L + A #else #error Hey! Add ELF relocation types here #endif @@ -239,4 +252,12 @@ typedef struct } ELF_HASH_TABLE, *PELF_HASH_TABLE; +#ifdef IS_64_BIT +#define ELF_R_SYM(x) ((x) >> 32) +#define ELF_R_TYPE(x) ((x) & 0xFFFFFFFF) +#else +#define ELF_R_SYM(x) ((x) >> 8) +#define ELF_R_TYPE(x) ((x) & 0xFF) +#endif + #endif//BORON_ELF_H \ No newline at end of file diff --git a/common/include/mms.h b/common/include/mms.h index 6408437c..e60a0f49 100644 --- a/common/include/mms.h +++ b/common/include/mms.h @@ -36,7 +36,7 @@ enum ACCESS_FLAG }; // Page Size definition -#ifdef TARGET_AMD64 +#if defined TARGET_AMD64 || defined TARGET_I386 #define PAGE_SIZE (0x1000) #else #error Define page size here! diff --git a/common/include/rtl/check64.h b/common/include/rtl/check64.h index 3d8f5a60..e3a2543e 100644 --- a/common/include/rtl/check64.h +++ b/common/include/rtl/check64.h @@ -14,10 +14,18 @@ Module name: ***/ #pragma once -#ifdef TARGET_AMD64 -#define IS_64_BIT +#if defined TARGET_AMD64 + +#define IS_64_BIT 1 + +#elif defined TARGET_I386 + +#define IS_32_BIT 1 + #else + #error Add your platform here! + #endif // In the future it should look something like this: From b7e64a878eeb14c01a5ea2cf62790a31bea7def2 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Tue, 14 Oct 2025 21:14:25 +0300 Subject: [PATCH 03/44] * X86: Still doesn't link, but now shows the undefined references. --- boron/Makefile | 10 ++-- boron/linker.amd64.ld | 18 ++++---- boron/linker.i386.ld | 105 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 13 deletions(-) create mode 100644 boron/linker.i386.ld diff --git a/boron/Makefile b/boron/Makefile index f69ef36e..666721c3 100644 --- a/boron/Makefile +++ b/boron/Makefile @@ -104,12 +104,12 @@ ifeq ($(TARGETL), amd64) -fno-reorder-functions ARCH_LDFLAGS = \ - -m elf_x86_64 \ -z max-page-size=0x1000 ARCH_ASFLAGS = \ -f elf64 + LINK_ARCH = elf_x86_64 SMP = yes else ifeq ($(TARGETL), i386) @@ -133,6 +133,7 @@ else ifeq ($(TARGETL), i386) ARCH_ASFLAGS = \ -f elf32 + LINK_ARCH = elf_i386 SMP = no else @@ -193,6 +194,7 @@ CXXFLAGS += \ LDFLAGSBASE += \ -nostdlib \ + -m $(LINK_ARCH) \ $(ARCH_LDFLAGS) # Internal linker flags that should not be changed by the user. @@ -238,7 +240,7 @@ CFLAGS += -D__BORON_MAJOR=$(VER_MAJOR) -D__BORON_MINOR=$(VER_MINOR) -D__BORON_BU # Link rules for the final kernel executable. $(KERNEL): $(SYMBOLS) @echo "[LD]\tBuilding $(KERNEL)" - @$(BLD) $(OBJ) $(SYMBOLS) $(LDFLAGS) -o $@ + $(BLD) $(OBJ) $(SYMBOLS) $(LDFLAGS) -o $@ $(SYMBOLS): $(KERNEL2) @echo "[NM]\tDumping and compiling symbols" @@ -247,12 +249,12 @@ $(SYMBOLS): $(KERNEL2) $(KERNEL2): $(KERNEL_PARTIAL) @echo "[LD]\tLinking kernel to extract symbols" - @$(BLD) $(KERNEL_PARTIAL) -static $(LDFLAGS) -o $@ + $(BLD) $(KERNEL_PARTIAL) -static $(LDFLAGS) -o $@ # Link rules for the amalgam object file. $(KERNEL_PARTIAL): $(OBJ) $(LINKER_FILE) @echo "[LD]\tPartially linking kernel" - @$(BLD) -r $(OBJ) $(LDFLAGSBASE) -o $@ + @$(BLD) -m $(LINK_ARCH) -r $(OBJ) $(LDFLAGSBASE) -o $@ $(BUILD_DIR)/ke/version.$(TARGETL).o: $(filter-out $(BUILD_DIR)/ke/version.$(TARGETL).o, $(OBJ)) $(SRC_DIR)/ke/version.c @echo "[CC]\tCompiling $(SRC_DIR)/ke/version.c" diff --git a/boron/linker.amd64.ld b/boron/linker.amd64.ld index a7a77a35..8d2c81ab 100644 --- a/boron/linker.amd64.ld +++ b/boron/linker.amd64.ld @@ -51,15 +51,6 @@ SECTIONS /* Move to the next memory page for .rodata */ . = ALIGN(CONSTANT(MAXPAGESIZE)); - .rodata : { - *(.rodata .rodata.*) - PROVIDE(KiSymbolTable = .); - PROVIDE(KiSymbolTableEnd = .); - } :rodata - - /* Move to the next memory page for .data */ - . = ALIGN(CONSTANT(MAXPAGESIZE)); - /* Global constructor array. */ .init_array : { g_init_array_start = .; @@ -74,6 +65,15 @@ SECTIONS g_fini_array_end = .; } + .rodata : { + *(.rodata .rodata.*) + PROVIDE(KiSymbolTable = .); + PROVIDE(KiSymbolTableEnd = .); + } :rodata + + /* Move to the next memory page for .data */ + . = ALIGN(CONSTANT(MAXPAGESIZE)); + .data : { *(.data .data.*) } :data diff --git a/boron/linker.i386.ld b/boron/linker.i386.ld new file mode 100644 index 00000000..5b3cbed0 --- /dev/null +++ b/boron/linker.i386.ld @@ -0,0 +1,105 @@ +/* Tell the linker that we want an ELF32 output file */ +OUTPUT_FORMAT(elf32-i386) +OUTPUT_ARCH(i386) + +/* Linker script for the OS */ + +OUTPUT_FORMAT(elf32-i386) +OUTPUT_ARCH(i386) + +ENTRY (KiBeforeSystemStartup) + +/* Define the program headers we want so the bootloader gives us the right */ +/* MMU permissions */ +PHDRS +{ + text PT_LOAD FLAGS((1 << 0) | (1 << 2)) ; /* Execute + Read */ + rodata PT_LOAD FLAGS((1 << 2)) ; /* Read only */ + data PT_LOAD FLAGS((1 << 1) | (1 << 2)) ; /* Write + Read */ +} + +/* Here is where all of the sections of the kernel are defined. */ +SECTIONS +{ + /* Begin loading at 0x100000, as that's where GRUB will place our data. */ + . = 1M; + + /* start blocking out writes from here */ + KiReadOnlyStart = . + 0xC0000000; + + .ipldata : + { + *(.ipldata .ipldata.*) + } :text + + .ipltext : + { + *(.ipltext .ipltext.*) + } :text + + . += 0xC0000000; + + KiTextInitStart = .; + .text.init ALIGN (4K) : AT (ADDR (.text.init) - 0xC0000000) + { + *(.text.init) + } :text + KiTextInitEnd = .; + + KiTextPageStart = .; + .text.page ALIGN (4K) : AT (ADDR (.text.page) - 0xC0000000) + { + *(.text.page) + } :text + KiTextPageEnd = .; + + .text ALIGN (4K) : AT (ADDR (.text) - 0xC0000000) + { + *(.text .text.*) + } :text + + .init_array ALIGN (4K) : AT (ADDR (.init_array) - 0xC0000000) + { + g_init_array_start = .; + *(.init_array .init_array.*) + g_init_array_end = .; + } :rodata + + .fini_array ALIGN (4K) : AT (ADDR (.fini_array) - 0xC0000000) + { + g_fini_array_start = .; + *(.fini_array .fini_array.*) + g_fini_array_end = .; + } :rodata + + .rodata ALIGN (4K) : AT (ADDR (.rodata) - 0xC0000000) + { + *(.rodata .rodata.*) + PROVIDE(KiSymbolTable = .); + PROVIDE(KiSymbolTableEnd = .); + } :rodata + + KiReadOnlyEnd = .; + + .data ALIGN (4K) : AT (ADDR (.data) - 0xC0000000) + { + /* place the end right where data starts to get that nice page alignment :) */ + l_code_and_rodata_end = .; + *(.data .data.*) + } :data + + .bss ALIGN (4K) : AT (ADDR (.bss) - 0xC0000000) + { + *(COMMON) + *(.bss .bss.*) + + /* Hack to keep the PsSystemProcess symbol while adding an object header on top */ + PROVIDE(PsSystemProcess = PspSystemProcessObject + 64); + } :data + + /* Discard .note.* and .eh_frame since they may cause issues on some hosts. */ + /DISCARD/ : { + *(.eh_frame) + *(.note .note.*) + } +} \ No newline at end of file From 7a1ef0973d053a0e565df34aeb044135bc7e5f9b Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Wed, 15 Oct 2025 10:45:08 +0300 Subject: [PATCH 04/44] * X86: Kernel now links. Untested. --- boron/Makefile | 74 +---- boron/include/arch/i386.h | 12 + boron/include/arch/i386.inc | 16 ++ boron/libgcc-i686.a | Bin 0 -> 953620 bytes boron/linker.i386.ld | 42 ++- boron/scripts/generate_symbols.py | 17 +- boron/source/build_number | 2 +- boron/source/ke/amd64/misc.asm | 2 +- boron/source/ke/amd64/pio.c | 1 - boron/source/ke/i386/cpu.c | 206 ++++++++++++++ boron/source/ke/i386/debug.c | 286 +++++++++++++++++++ boron/source/ke/i386/foreinit.asm | 127 +++++++++ boron/source/ke/i386/init.c | 34 +++ boron/source/ke/i386/intlist.inc | 84 ++++++ boron/source/ke/i386/intobj.c | 169 +++++++++++ boron/source/ke/i386/ipl.c | 25 ++ boron/source/ke/i386/misc.asm | 197 +++++++++++++ boron/source/ke/i386/pio.c | 50 ++++ boron/source/ke/i386/probe.c | 118 ++++++++ boron/source/ke/i386/thredsup.c | 36 +++ boron/source/ke/i386/tlbs.c | 37 +++ boron/source/ke/i386/trap.asm | 155 +++++++++++ boron/source/ke/i386/traplist.asm | 57 ++++ boron/source/ke/i386/traps.c | 237 ++++++++++++++++ boron/source/ldr/loader.c | 4 + boron/source/mm/i386/pt.c | 448 ++++++++++++++++++++++++++++++ boron/source/mm/i386/ptfree.c | 153 ++++++++++ drivers/CommonMakefile | 37 +-- tools/toolchain.mk | 67 +++++ 29 files changed, 2574 insertions(+), 119 deletions(-) create mode 100644 boron/include/arch/i386.inc create mode 100644 boron/libgcc-i686.a create mode 100644 boron/source/ke/i386/cpu.c create mode 100644 boron/source/ke/i386/debug.c create mode 100644 boron/source/ke/i386/foreinit.asm create mode 100644 boron/source/ke/i386/init.c create mode 100644 boron/source/ke/i386/intlist.inc create mode 100644 boron/source/ke/i386/intobj.c create mode 100644 boron/source/ke/i386/ipl.c create mode 100644 boron/source/ke/i386/misc.asm create mode 100644 boron/source/ke/i386/pio.c create mode 100644 boron/source/ke/i386/probe.c create mode 100644 boron/source/ke/i386/thredsup.c create mode 100644 boron/source/ke/i386/tlbs.c create mode 100644 boron/source/ke/i386/trap.asm create mode 100644 boron/source/ke/i386/traplist.asm create mode 100644 boron/source/ke/i386/traps.c create mode 100644 boron/source/mm/i386/pt.c create mode 100644 boron/source/mm/i386/ptfree.c create mode 100644 tools/toolchain.mk diff --git a/boron/Makefile b/boron/Makefile index 666721c3..2f4cd175 100644 --- a/boron/Makefile +++ b/boron/Makefile @@ -75,70 +75,7 @@ else DEFINES += -O3 endif -# Determine the build tools used automatically. -ifeq ($(TARGETL), amd64) - # Compiler Toolchain - BCC ?= gcc - BCXX ?= g++ - BLD ?= ld - BASM ?= nasm - - # Compiler and linker flags - # - # NOTE 7.7.2024 -- No-reorder-functions was added because a certain functions - # was generating an "unlikely" section, which was placed at different addresses - # in kernel.elf and kernel2.elf, screwing up the symbol table... That's pretty - # bad. - # - # TODO: fix above ^^^ - ARCH_CFLAGS = \ - -m64 \ - -march=x86-64 \ - -mabi=sysv \ - -mno-80387 \ - -mno-mmx \ - -mno-sse \ - -mno-sse2 \ - -mno-red-zone \ - -mcmodel=kernel \ - -fno-reorder-functions - - ARCH_LDFLAGS = \ - -z max-page-size=0x1000 - - ARCH_ASFLAGS = \ - -f elf64 - - LINK_ARCH = elf_x86_64 - SMP = yes - -else ifeq ($(TARGETL), i386) - # Compiler Toolchain - BCC ?= clang - BCXX ?= clang++ - BLD ?= ld - BASM ?= nasm - - # Compiler and linker flags - ARCH_CFLAGS = \ - -target i686-elf \ - -mno-80387 \ - -mno-mmx \ - -mno-sse \ - -mno-sse2 - - ARCH_LDFLAGS = \ - -z max-page-size=0x1000 - - ARCH_ASFLAGS = \ - -f elf32 - - LINK_ARCH = elf_i386 - SMP = no - -else - $(error You cannot build for this architecture right now.) -endif +include ../tools/toolchain.mk ifeq ($(SMP), yes) ARCH_CFLAGS += -DCONFIG_SMP @@ -208,8 +145,7 @@ ifeq ($(shell $(LD) --help 2>&1 | grep 'no-pie' >/dev/null 2>&1; echo $$?),0) endif # Internal nasm flags that should not be changed by the user. -NASMFLAGS += \ - -f elf64 +NASMFLAGS += $(ARCH_ASFLAGS) # Use find to glob all *.c, *.S, and *.asm files in the directory and extract the object names. EXCLUDE_WRONG_ARCH = '(' '(' -path 'source/ke/$(TARGETL)/*' ')' -o '(' -path 'source/mm/$(TARGETL)/*' ')' -o '(' -not -path 'source/ke/*/*' -not -path 'source/mm/*/*' ')' ')' @@ -240,16 +176,16 @@ CFLAGS += -D__BORON_MAJOR=$(VER_MAJOR) -D__BORON_MINOR=$(VER_MINOR) -D__BORON_BU # Link rules for the final kernel executable. $(KERNEL): $(SYMBOLS) @echo "[LD]\tBuilding $(KERNEL)" - $(BLD) $(OBJ) $(SYMBOLS) $(LDFLAGS) -o $@ + @$(BLD) $(OBJ) $(SYMBOLS) $(LDFLAGS) -o $@ $(SYMBOLS): $(KERNEL2) @echo "[NM]\tDumping and compiling symbols" - @nm -P $(KERNEL2) | $(SCRIPTS_DIR)/generate_symbols.py > $(BUILD_DIR)/_symtab.$(TARGETL).asm + @nm -P $(KERNEL2) | $(SCRIPTS_DIR)/generate_symbols.py $(TARGETL) > $(BUILD_DIR)/_symtab.$(TARGETL).asm @$(BASM) $(NASMFLAGS) $(BUILD_DIR)/_symtab.$(TARGETL).asm -o $(BUILD_DIR)/_symtab.$(TARGETL).o $(KERNEL2): $(KERNEL_PARTIAL) @echo "[LD]\tLinking kernel to extract symbols" - $(BLD) $(KERNEL_PARTIAL) -static $(LDFLAGS) -o $@ + @$(BLD) $(KERNEL_PARTIAL) -static $(LDFLAGS) -o $@ # Link rules for the amalgam object file. $(KERNEL_PARTIAL): $(OBJ) $(LINKER_FILE) diff --git a/boron/include/arch/i386.h b/boron/include/arch/i386.h index f3a5a442..0b632f44 100644 --- a/boron/include/arch/i386.h +++ b/boron/include/arch/i386.h @@ -14,6 +14,18 @@ Module name: ***/ #pragma once +// Model specific registers +uint64_t KeGetMSR(uint32_t msr); +void KeSetMSR(uint32_t msr, uint64_t value); + +// Port I/O +uint8_t KePortReadByte(uint16_t portNo); +void KePortWriteByte(uint16_t portNo, uint8_t data); +uint16_t KePortReadWord(uint16_t portNo); +void KePortWriteWord(uint16_t portNo, uint16_t data); +uint32_t KePortReadDword(uint16_t portNo); +void KePortWriteDword(uint16_t portNo, uint32_t data); + #ifdef KERNEL // start PML2 index will be 512. The PFN database's is 776 diff --git a/boron/include/arch/i386.inc b/boron/include/arch/i386.inc new file mode 100644 index 00000000..abc9ded5 --- /dev/null +++ b/boron/include/arch/i386.inc @@ -0,0 +1,16 @@ +; Boron - Include file for platform specific assembly stuff +%ifndef NS64_I386_INC +%define NS64_I386_INC +bits 32 + +%define SEG_NULL 0x00 +%define SEG_RING_0_CODE 0x08 +%define SEG_RING_0_DATA 0x10 +%define SEG_RING_3_DATA 0x18 +%define SEG_RING_3_CODE 0x20 + +; magic IPL to avoid popping registers we don't need to pop, since +; this interrupt frame was pushed by KeYieldCurrentThread in kernel mode +%define MAGIC_IPL 0x42424242 + +%endif ;NS64_I386_INC diff --git a/boron/libgcc-i686.a b/boron/libgcc-i686.a new file mode 100644 index 0000000000000000000000000000000000000000..8121787a9f054ff5e897bae5ac8d009ce7836cca GIT binary patch literal 953620 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

U_RK~jF-PDdJ^j7hK^cX%cP5Ybs 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
g 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

PGacwAPJ%>$8F_5c-4vYrx{X!GXmbyAks%juh&G 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 +#include +#include +#include +#include "../../ke/ki.h" + +extern void KeOnUpdateIPL(KIPL newIPL, KIPL oldIPL); // defined in x.asm + +static UNUSED uint32_t KepGetEflags() +{ + uint32_t eflags = 0; + ASM("pushfd\n" + "popd %0":"=r"(eflags)); + return eflags; +} + +// Model specific registers + +uint64_t KeGetMSR(uint32_t msr) +{ + uint32_t edx, eax; + + ASM("rdmsr":"=d"(edx),"=a"(eax):"c"(msr)); + + return ((uint64_t)edx << 32) | eax; +} + +void KeSetMSR(uint32_t msr, uint64_t value) +{ + uint32_t edx = (uint32_t)(value >> 32); + uint32_t eax = (uint32_t)(value); + + ASM("wrmsr"::"d"(edx),"a"(eax),"c"(msr)); +} + +#ifdef CONFIG_SMP // not supported but just in case you want it + +void KeSetCPUPointer(void* pGS) +{ + KeSetMSR(MSR_GS_BASE, (uint64_t) pGS); +} + +void* KeGetCPUPointer(void) +{ + return (void*) KeGetMSR(MSR_GS_BASE); +} + +#else + +static void* KiCPUPointer; + +void KeSetCPUPointer(void* pGS) +{ + KiCPUPointer = pGS; +} + +void* KeGetCPUPointer() +{ + return KiCPUPointer; +} + +#endif + + +extern void* KiIdtDescriptor; + +INIT +void KepLoadIdt() +{ + ASM("lidt (%0)"::"r"(&KiIdtDescriptor)); +} + +static uint64_t KepGdtEntries[] = +{ + 0x0000000000000000, // Null descriptor + 0x00cf9b000000ffff, // 32-bit ring-0 code + 0x00cf93000000ffff, // 32-bit ring-0 data + 0x00cff3000000ffff, // 32-bit ring-3 data + 0x00cffb000000ffff, // 32-bit ring-3 code +}; + +extern void KepLoadGdt(void* desc); +extern void KepLoadTss(int descriptor); + +INIT +static void KepSetupGdt(KARCH_DATA* Data) +{ + KGDT* Gdt = &Data->Gdt; + KTSS* Tss = &Data->Tss; + + for (int i = 0; i < C_GDT_SEG_COUNT; i++) + { + Gdt->Segments[i] = KepGdtEntries[i]; + } + + // setup the TSS entry: + uintptr_t TssAddress = (uintptr_t) Tss; + + Gdt->TssEntry.Limit1 = sizeof(KTSS); + Gdt->TssEntry.Base1 = TssAddress; + Gdt->TssEntry.Access = 0x89; + Gdt->TssEntry.Limit2 = 0x0; + Gdt->TssEntry.Flags = 0x0; + Gdt->TssEntry.Base2 = TssAddress >> 24; + + struct + { + uint16_t Length; + uint64_t Pointer; + } + PACKED GdtDescriptor; + + GdtDescriptor.Length = sizeof * Gdt; + GdtDescriptor.Pointer = (uint64_t) Gdt; + + void* Prcb = KeGetCPUPointer(); + KepLoadGdt(&GdtDescriptor); + + KeSetCPUPointer(Prcb); + + // also load the TSS + KepLoadTss(offsetof(KGDT, TssEntry)); +} + +INIT +static void KepSetupTss(KTSS* Tss) +{ + // we'll set it up later.. + memset(Tss, 0, sizeof * Tss); +} + +extern void KiSystemServiceHandler(); + +INIT +void KeInitCPU() +{ + KiSwitchToAddressSpaceProcess(KeGetSystemProcess()); + + PKARCH_DATA Data = &KeGetCurrentPRCB()->ArchData; + memset(&Data->Gdt, 0, sizeof Data->Gdt); + + KepSetupTss(&Data->Tss); + KepSetupGdt(Data); + KepLoadIdt(); +/* + // Set up the system call parameters now. + // Enable the SYSCALL/SYSRET instructions and the NX bit. + KeSetMSR(MSR_IA32_EFER, KeGetMSR(MSR_IA32_EFER) | MSR_IA32_EFER_SCE | 0); + + // Set the system call handler. + KeSetMSR(MSR_IA32_LSTAR, (uint64_t) KiSystemServiceHandler); + + // Set the system call CS and SS. + // + // ARCHITECTURAL CRIMES AGAINST HUMANITY: + // For the kernel CS/SS, you cannot pick SS. It is automatically + // picked as STAR_47_32 + 8. But CS is the proper value of STAR_47_32. + // + // For the *user* CS/SS, you cannot pick SS. It is automatically + // picked as STAR_63_48 + 8. But CS is NOT the proper value + // of STAR_63_48, instead, it's STAR_63_48 + 16. Why AMD?! + // + // (Or was it Intel?) + uint64_t Star = SEG_RING_0_CODE | (((SEG_RING_3_DATA - 8) | 3) << 16); + KeSetMSR(MSR_IA32_STAR, Star << 32); + + // Set the mask to disable interrupts on syscall. + KeSetMSR(MSR_IA32_FMASK, 0x200); +*/ +} + +/* +extern uintptr_t KiSystemServiceTable[]; +extern uintptr_t KiSystemServiceTableEnd[]; + +// If system call tracing is enabled, this shows all of the system calls happening. +void KePrintSystemServiceDebug(size_t Syscall) +{ + // Format: "[ThreadPointer] - Syscall [Number] ([FunctionName])" + size_t Size = KiSystemServiceTableEnd - KiSystemServiceTable; + + const char* FunctionName; + + if (Syscall >= Size) + FunctionName = "INVALID"; + else + FunctionName = DbgLookUpRoutineNameByAddressExact(KiSystemServiceTable[Syscall]); + + DbgPrint("SYSCALL: %p - %d %s", KeGetCurrentThread(), (int) Syscall, FunctionName); +} +*/ diff --git a/boron/source/ke/i386/debug.c b/boron/source/ke/i386/debug.c new file mode 100644 index 00000000..6035d24d --- /dev/null +++ b/boron/source/ke/i386/debug.c @@ -0,0 +1,286 @@ +/*** + The Boron Operating System + Copyright (C) 2025 iProgramInCpp + +Module name: + ke/i386/debug.c + +Abstract: + This module implements architecture specific debugging + routines. + +Author: + iProgramInCpp - 14 October 2025 +***/ +#include +#include +#include +#include +#include +#include + +#define KERNEL_IMAGE_BASE (0xC0100000) + +#ifdef DEBUG + +// if you want stack traces from user mode (NOT recommended for +// release because this is a vulnerability), do this: +#define DISABLE_USER_MODE_PREVENTION + +#endif + +// Defined in misc.asm: +uintptr_t KiGetEIP(); +uintptr_t KiGetEBP(); + +// Assume that EBP is the first thing pushed when entering a function. This is often +// the case because we specify -fno-omit-frame-pointer when compiling. +// If not, we are in trouble. +typedef struct STACK_FRAME_tag STACK_FRAME, *PSTACK_FRAME; + +struct STACK_FRAME_tag +{ + PSTACK_FRAME Next; + uintptr_t IP; +}; + +static void DbgResolveAddress(uintptr_t Address, char *SymbolName, size_t BufferSize) +{ + if (!Address) + { + // End of our stack trace + strcpy(SymbolName, "End"); + return; + } + + strcpy(SymbolName, "??"); + + uintptr_t BaseAddress = 0; + // Determine where that address came from. + if (Address >= KERNEL_IMAGE_BASE) + { + // Easy, it's in the kernel. + // Determine the symbol's name + const char* Name = DbgLookUpRoutineNameByAddress(Address, &BaseAddress); + + if (Name) + { + snprintf(SymbolName, + BufferSize, + "brn!%s+%x", + Name, + Address - BaseAddress); + } + return; + } + + // Determine which loaded DLL includes this address. + PLOADED_DLL Dll = NULL; + + for (int i = 0; i < KeLoadedDLLCount; i++) + { + PLOADED_DLL LoadedDll = &KeLoadedDLLs[i]; + if (LoadedDll->ImageBase <= Address && Address < LoadedDll->ImageBase + LoadedDll->ImageSize) + { + // It's the one! + Dll = LoadedDll; + break; + } + } + + if (!Dll) + return; + + Address -= Dll->ImageBase; + + const char* Name = LdrLookUpRoutineNameByAddress(Dll, Address, &BaseAddress); + + if (Name) + { + snprintf(SymbolName, + BufferSize, + "%s!%s+%x", + Dll->Name, + Name, + Address - BaseAddress); + } +} + +void DbgPrintDouble(const char* String) +{ + DbgPrintString(String); + HalDisplayString(String); +} + +void DbgPrintStackTrace(uintptr_t Ebp) +{ + if (Ebp == 0) + Ebp = KiGetEBP(); + + PSTACK_FRAME StackFrame = (PSTACK_FRAME) Ebp; + + int Depth = 30; + char Buffer[128]; + + DbgPrintDouble("\tAddress \tName\n"); + +#ifndef DISABLE_USER_MODE_PREVENTION + if (Rbp <= MM_USER_SPACE_END) + { + snprintf(Buffer, sizeof(Buffer), "\t%p\tUser Mode Address\n", (void*) Rbp); + DbgPrintDouble(Buffer); + return; + } +#endif + + // TODO: This might be broken and still access a user address. Debug this later + while (StackFrame && Depth > 0) + { + uintptr_t Address = StackFrame->IP; +#ifndef DISABLE_USER_MODE_PREVENTION + if (Address <= MM_USER_SPACE_END) + { + snprintf(Buffer, sizeof(Buffer), "\t%p\tUser Mode Address\n", (void*) Rbp); + DbgPrintDouble(Buffer); + return; + } +#endif + + char SymbolName[64]; + DbgResolveAddress(Address, SymbolName, sizeof SymbolName); + + snprintf(Buffer, sizeof(Buffer), "\t%p\t%s\n", (void*) Address, SymbolName); + DbgPrintDouble(Buffer); + + Depth--; + StackFrame = StackFrame->Next; + +#ifndef DISABLE_USER_MODE_PREVENTION + if ((uintptr_t)StackFrame <= MM_USER_SPACE_END) + { + snprintf(Buffer, sizeof(Buffer), "\t%p\tUser Mode Address\n", (void*) Rbp); + DbgPrintDouble(Buffer); + return; + } +#endif + } + + if (Depth == 0) + DbgPrintDouble("Warning, stack trace too deep, increase the depth in " __FILE__ " if you need it.\n"); +} + +#ifdef DEBUG + +//#define SERIAL +#ifdef SERIAL + +// Serial Port Defines - Copied from NanoShell +// +// We are using COM1 here. +#define PORT_BASE 0x3F8 +#define DLAB_ENABLE (1 << 7) +#define BAUD_DIVISOR (0x0003) +#define LCR_8BIT_DATA (3 << 0) // Data Bits - 8 +#define LCR_PAR_NONE (0 << 3) // No Parity. +#define FCR_ENABLE (1 << 0) +#define FCR_RFRES (1 << 1) +#define FCR_XFRES (1 << 2) +#define FCR_RXTRIG (3 << 6) +#define IER_NOINTS (0) +#define MCR_DTR (1 << 0) +#define MCR_RTS (1 << 1) +#define MCR_OUT1 (1 << 2) +#define MCR_LOOPBK (1 << 4) // loop-back +#define S_RBR 0x00 // Receive buffer register (read only) same as... +#define S_THR 0x00 // Transmitter holding register (write only) +#define S_IER 0x01 // Interrupt enable register +#define S_IIR 0x02 // Interrupt ident register (read only)... +#define S_FCR 0x02 // FIFO control register (write only) +#define S_LCR 0x03 // Line control register +#define S_MCR 0x04 // Modem control register +#define S_LSR 0x05 // Line status register +#define S_MSR 0x06 // Modem status register + +#define S_CHECK_BYTE 0xCA + +void DbgInit() +{ + KePortWriteByte(PORT_BASE+1, IER_NOINTS); + + // Set Divisor to 3 -- 38400 baud + KePortWriteByte(PORT_BASE+0, BAUD_DIVISOR & 0xFF); + KePortWriteByte(PORT_BASE+1, BAUD_DIVISOR >> 8); + + // Set the data parity and bit size. + KePortWriteByte(PORT_BASE+3, LCR_8BIT_DATA | LCR_PAR_NONE); + + // Enable FIFO + KePortWriteByte(PORT_BASE+2, FCR_RXTRIG | FCR_XFRES | FCR_RFRES | FCR_ENABLE); + + // Prepare modem control register + KePortWriteByte(PORT_BASE+4, MCR_OUT1 | MCR_RTS | MCR_DTR); // IRQs disabled, RTS/DSR set + + // set in loopback mode to test the serial chip + KePortWriteByte(PORT_BASE+4, MCR_LOOPBK | MCR_OUT1 | MCR_RTS); + + // Send a check byte, and check if we get it back + KePortWriteByte(PORT_BASE+0, S_CHECK_BYTE); + + if (KePortReadByte (PORT_BASE + 0) != S_CHECK_BYTE) + { + // Hope it still works + } + + // Set this serial PORT_BASE to normal operation + KePortWriteByte(PORT_BASE+4, MCR_OUT1 | MCR_RTS | MCR_DTR); // IRQs disabled, OUT#1 bit, no loop-back +} + +void DbgPrintChar(char c) +{ + while ((KePortReadByte(PORT_BASE + S_LSR) & 0x20) == 0) + __asm__("pause"); + + KePortWriteByte(PORT_BASE, c); +} + +void DbgPrintString(const char* str) +{ + while (*str) + { + if (*str == '\n') + DbgPrintChar('\r'); + DbgPrintChar(*str); + str++; + } +} + +#else + +void DbgInit() +{ + // E9 port doesn't need initialization. +} + +void DbgPrintString(const char* str) +{ + while (*str) + { + KePortWriteByte(0xE9, *str); + str++; + } +} + +#endif + +KSPIN_LOCK KiPrintLock; +KSPIN_LOCK KiDebugPrintLock; + +void DbgPrintStringLocked(const char* str) +{ + KIPL OldIpl; + KeAcquireSpinLock(&KiDebugPrintLock, &OldIpl); + DbgPrintString(str); + KeReleaseSpinLock(&KiDebugPrintLock, OldIpl); +} + +#endif diff --git a/boron/source/ke/i386/foreinit.asm b/boron/source/ke/i386/foreinit.asm new file mode 100644 index 00000000..11e91a70 --- /dev/null +++ b/boron/source/ke/i386/foreinit.asm @@ -0,0 +1,127 @@ +; +; The Boron Operating System +; Copyright (C) 2025 iProgramInCpp +; +; Module name: +; ke/i386/foreinit.asm +; +; Abstract: +; This module implements the entry point of the Boron kernel +; for the i386 architecture. +; +; Author: +; iProgramInCpp - 14 October 2025 +; +bits 32 + +; **** Initial Program Loader **** +; This was borrowed from NanoShell (https://github.com/iProgramMC/NanoShellOS) +; but it works well enough for it so it'll probably work here too. + +%define BASE_ADDRESS 0xC0000000 +%define V2P(k) ((k) - BASE_ADDRESS) + +section .ipldata + + ; Multiboot v0.6.96 specification + align 4 + + ; Header + dd 0x1BADB002 ; Signature + dd 7 ; Flags: + dd - (0x1BADB002 + 7) ; Check Sum + + ; A.out Kludge - blank because we're an ELF + dd 0 + dd 0 + dd 0 + dd 0 + dd 0 + + ; Video Mode + dd 0 ; Require a linear frame buffer + dd 1024 ; Width + dd 768 ; Height + dd 32 ; Bits per pixel + +section .ipltext +global KiBeforeSystemStartup +KiBeforeSystemStartup: + cli + + ; no need for a stack at this stage + xor ebp, ebp + + ; store the provided multiboot data + mov [V2P(KiMultibootSignature)], eax + mov [V2P(KiMultibootPointer)], ebx + + ; first address to map is 0x00000000. map 2048 pages + xor esi, esi + mov ecx, 2048 + + ; get the physical address of the bootstrap page directory + mov edi, V2P(KiBootstrapPageTables) + +.LoopFill: + mov edx, esi + or edx, 0x03 ; set present and r/w bits + mov [edi], edx + + add esi, 0x1000 + add edi, 4 + loop .LoopFill + + ; map the two page tables to both 0x00000000 and 0xC0000000 + mov dword [V2P(KiBootstrapPageDirectory) + 0 * 4], V2P(KiBootstrapPageTables + 0) + 0x03 + mov dword [V2P(KiBootstrapPageDirectory) + 1 * 4], V2P(KiBootstrapPageTables + 4096) + 0x03 + mov dword [V2P(KiBootstrapPageDirectory) + 768 * 4], V2P(KiBootstrapPageTables + 0) + 0x03 + mov dword [V2P(KiBootstrapPageDirectory) + 769 * 4], V2P(KiBootstrapPageTables + 4096) + 0x03 + + ; set CR3 to the physical address of the page directory + mov ecx, V2P(KiBootstrapPageDirectory) + mov cr3, ecx + + ; set PG and WP bit + ; note: WP bit is ignored/reserved on 80386. should probably test for that? + mov ecx, cr0 + or ecx, 0x80010000 + mov cr0, ecx + + ; jump to higher half + mov ecx, (KiBeforeSystemStartupHigherHalf) + jmp ecx + +extern KiSystemStartup + +section .text +global KiBeforeSystemStartupHigherHalf +KiBeforeSystemStartupHigherHalf: + ; unmap the identity mapping, we don't need it anymore + mov dword [KiBootstrapPageDirectory + 0], 0 + mov dword [KiBootstrapPageDirectory + 4], 0 + + ; reload CR3 to force a TLB flush (we updated the page directory but TLB + ; isn't aware of that). NOTE: you can probably just use invlpg. + mov ecx, cr3 + mov cr3, ecx + + ; setup the initial stack + mov esp, KiInitialStack + + ; GDT will be set up later + + call KiSystemStartup + + cli +.Stop: + hlt + jmp .Stop + +section .bss + +KiMultibootSignature: resd 1 +KiMultibootPointer: resd 1 +KiInitialStack: resb 4096 +KiBootstrapPageDirectory: resb 4096 +KiBootstrapPageTables: resb 8192 diff --git a/boron/source/ke/i386/init.c b/boron/source/ke/i386/init.c new file mode 100644 index 00000000..14037a9a --- /dev/null +++ b/boron/source/ke/i386/init.c @@ -0,0 +1,34 @@ +/*** + The Boron Operating System + Copyright (C) 2025 iProgramInCpp + +Module name: + ke/i386/init.c + +Abstract: + This module implements the architecture specific UP-init + routines. + +Author: + iProgramInCpp - 14 October 2025 +***/ +#include +#include + +void KiSetupIdt(); +void KiInitializeInterruptSystem(); + +INIT +void KeInitArchUP() +{ + KiSetupIdt(); + KiInitializeInterruptSystem(); +} + +INIT +void KeInitArchMP() +{ + KeInitCPU(); + KeLowerIPL(IPL_NORMAL); + HalInitSystemMP(); +} diff --git a/boron/source/ke/i386/intlist.inc b/boron/source/ke/i386/intlist.inc new file mode 100644 index 00000000..0105e078 --- /dev/null +++ b/boron/source/ke/i386/intlist.inc @@ -0,0 +1,84 @@ +; +; The Boron Operating System +; Copyright (C) 2025 iProgramInCpp +; +; Module name: +; ke/i386/intlist.inc +; +; Abstract: +; This is a define file. It is used to loop through all +; 256 interrupt vectors through the macro INT,2 in a way +; that doesn't repeat itself too much. +; +; Author: +; iProgramInCpp - 14 October 2025 +; + +INT 00, N +INT 01, N +INT 02, N +INT 03, N +INT 04, N +INT 05, N +INT 06, N +INT 07, N +INT 08, Y +INT 09, N +INT 0A, Y +INT 0B, Y +INT 0C, Y +INT 0D, Y +INT 0E, Y +INT 0F, N +INT 10, N +INT 11, Y +INT 12, N +INT 13, N +INT 14, N +INT 15, N +INT 16, N +INT 17, Y +INT 18, N +INT 19, N +INT 1A, N +INT 1B, Y +INT 1C, Y +INT 1D, N +INT 1E, N +INT 1F, N + +%macro INTGRP 1 +INT %{1}0, N +INT %{1}1, N +INT %{1}2, N +INT %{1}3, N +INT %{1}4, N +INT %{1}5, N +INT %{1}6, N +INT %{1}7, N +INT %{1}8, N +INT %{1}9, N +INT %{1}A, N +INT %{1}B, N +INT %{1}C, N +INT %{1}D, N +INT %{1}E, N +INT %{1}F, N +%endmacro + +INTGRP 2 +INTGRP 3 +INTGRP 4 +INTGRP 5 +INTGRP 6 +INTGRP 7 +INTGRP 8 +INTGRP 9 +INTGRP A +INTGRP B +INTGRP C +INTGRP D +INTGRP E +INTGRP F + +%unmacro INTGRP 1 diff --git a/boron/source/ke/i386/intobj.c b/boron/source/ke/i386/intobj.c new file mode 100644 index 00000000..30c42f60 --- /dev/null +++ b/boron/source/ke/i386/intobj.c @@ -0,0 +1,169 @@ +/*** + The Boron Operating System + Copyright (C) 2025 iProgramInCpp + +Module name: + ke/i386/intobj.c + +Abstract: + This module implements the interrupt object for + the i386 platform. + + N.B. The interval timer and DPC dispatch functions + do not use the interrupt object. + +Author: + iProgramInCpp - 14 October 2025 +***/ +#include +#include + +extern int8_t KiTrapIplList[]; // trap.asm +extern void* KiTrapCallList[]; // trap.asm + +typedef struct +{ + LIST_ENTRY List; + KSPIN_LOCK Lock; +} +INTERRUPT_LIST, *PINTERRUPT_LIST; + +static INTERRUPT_LIST KiInterruptList[256]; + +static PKREGISTERS KiInterruptDispatch(PKREGISTERS Regs) +{ + int Number = Regs->IntNumber; + PINTERRUPT_LIST InterruptList = &KiInterruptList[Number]; + KIPL Ipl; + + KeAcquireSpinLock(&InterruptList->Lock, &Ipl); + + for (PLIST_ENTRY Entry = InterruptList->List.Flink; + Entry != &InterruptList->List; + Entry = Entry->Flink) + { + PKINTERRUPT Interrupt = CONTAINING_RECORD(Entry, KINTERRUPT, Entry); + KIPL Unused; + KeAcquireSpinLock(Interrupt->SpinLock, &Unused); + + Interrupt->ServiceRoutine(Interrupt, Interrupt->ServiceContext); + + KeReleaseSpinLock(Interrupt->SpinLock, Unused); + } + + KeReleaseSpinLock(&InterruptList->Lock, Ipl); + + // Acknowledge the interrupt. + HalEndOfInterrupt(); + + // No change in registers. + return Regs; +} + +INIT +void KiInitializeInterruptSystem() +{ + for (int i = 0; i < 256; i++) + { + InitializeListHead(&KiInterruptList[i].List); + KeInitializeSpinLock(&KiInterruptList[i].Lock); + } +} + +void KeInitializeInterrupt( + PKINTERRUPT Interrupt, + PKSERVICE_ROUTINE ServiceRoutine, + void* ServiceContext, + PKSPIN_LOCK SpinLock, + int Vector, + KIPL InterruptIpl, + bool SharedVector) +{ + ASSERT((Vector >> 4) == InterruptIpl && "The interrupt vector must match its IPL on AMD64."); + ASSERT(InterruptIpl > IPL_DPC && "The caller may not override this IPL"); + ASSERT(InterruptIpl < IPL_CLOCK && "The caller may not override this IPL"); + + Interrupt->Connected = false; + Interrupt->SharedVector = SharedVector; + Interrupt->Vector = Vector; + Interrupt->Ipl = InterruptIpl; + Interrupt->ServiceRoutine = ServiceRoutine; + Interrupt->ServiceContext = ServiceContext; + Interrupt->SpinLock = SpinLock; +} + +bool KeConnectInterrupt(PKINTERRUPT Interrupt) +{ + ASSERT(!Interrupt->Connected && "It's already connected!"); + + PINTERRUPT_LIST InterruptList = &KiInterruptList[Interrupt->Vector]; + + KIPL Ipl, IplUnused; + Ipl = KeRaiseIPL(Interrupt->Ipl); + KeAcquireSpinLock(&InterruptList->Lock, &IplUnused); + + // Check if the vector may be shared. + if (!IsListEmpty(&InterruptList->List)) + { + // If the head has SharedVector == false, return false here. + PKINTERRUPT Head = CONTAINING_RECORD(InterruptList->List.Flink, KINTERRUPT, Entry); + + if (!Head->SharedVector) + { + KeReleaseSpinLock(&InterruptList->Lock, IplUnused); + KeLowerIPL(Ipl); + return false; + } + } + + // Connect the interrupt now. + InsertTailList(&InterruptList->List, &Interrupt->Entry); + Interrupt->Connected = true; + + // In case this wasn't already done, wire up the interrupt + // dispatcher routine we specified. + KeRegisterInterrupt(Interrupt->Vector, KiInterruptDispatch); + + KeReleaseSpinLock(&InterruptList->Lock, IplUnused); + KeLowerIPL(Ipl); + + return true; +} + +void KeDisconnectInterrupt(PKINTERRUPT Interrupt) +{ + ASSERT(Interrupt->Connected && "You need to have connected the interrupt to disconnect it!"); + + PINTERRUPT_LIST InterruptList = &KiInterruptList[Interrupt->Vector]; + KIPL Ipl, IplUnused; + Ipl = KeRaiseIPL(Interrupt->Ipl); + KeAcquireSpinLock(&InterruptList->Lock, &IplUnused); + + // Disconnect the interrupt now. + RemoveEntryList(&Interrupt->Entry); + Interrupt->Connected = false; + + KeReleaseSpinLock(&InterruptList->Lock, IplUnused); + KeLowerIPL(Ipl); +} + +int KeSynchronizeExecution( + PKINTERRUPT Interrupt, + KIPL SynchronizeIpl, + PKSYNCHRONIZE_ROUTINE Routine, + void* SynchronizeContext) +{ + if (SynchronizeIpl < Interrupt->Ipl) + SynchronizeIpl = Interrupt->Ipl; + + KIPL Ipl, IplUnused; + Ipl = KeRaiseIPL(SynchronizeIpl); + KeAcquireSpinLock(Interrupt->SpinLock, &IplUnused); + + int Result = Routine(SynchronizeContext); + + KeReleaseSpinLock(Interrupt->SpinLock, IplUnused); + KeLowerIPL(Ipl); + + return Result; +} diff --git a/boron/source/ke/i386/ipl.c b/boron/source/ke/i386/ipl.c new file mode 100644 index 00000000..e7011131 --- /dev/null +++ b/boron/source/ke/i386/ipl.c @@ -0,0 +1,25 @@ +/*** + The Boron Operating System + Copyright (C) 2025 iProgramInCpp + +Module name: + ke/i386/ipl.c + +Abstract: + This header file implements support for the interrupt + priority system. + +Author: + iProgramInCpp - 14 October 2025 +***/ +#include + +void KeOnUpdateIPL(KIPL NewIpl, KIPL OldIpl) +{ + // TODO: lazy IPL - only program the interrupt controller if needed + + if (NewIpl == OldIpl) + return; + + // TODO: placeholder method here +} diff --git a/boron/source/ke/i386/misc.asm b/boron/source/ke/i386/misc.asm new file mode 100644 index 00000000..b9e8b10b --- /dev/null +++ b/boron/source/ke/i386/misc.asm @@ -0,0 +1,197 @@ +; +; The Boron Operating System +; Copyright (C) 2025 iProgramInCpp +; +; Module name: +; ke/i386/misc.asm +; +; Abstract: +; This module implements certain utility functions for +; the i386 platform. +; +; Author: +; iProgramInCpp - 14 October 2025 +; +bits 32 + +%include "arch/i386.inc" + +; these functions get the EIP and EBP respectively +global KiGetEIP +global KiGetEBP + +KiGetEIP: + mov eax, [esp] + ret + +KiGetEBP: + mov eax, ebp + ret + +; these functions set the CR3 +global KeSetCurrentPageTable +global KeGetCurrentPageTable + +; void* KeGetCurrentPageTable() +KeGetCurrentPageTable: + mov eax, cr3 + ret + +; void KeSetCurrentPageTable(void* pt) +KeSetCurrentPageTable: + mov eax, [esp + 4] + mov cr3, eax + ret + +; int MmProbeAddressSub(void* Address, size_t Length, bool ProbeWrite); +global MmProbeAddressSub +MmProbeAddressSub: +; This function performs the actual work of probing the addresses. It is +; written in assembly to allow for predictability of the stack layout. This +; makes using the alternative exit, returning the STATUS_FAULT code, after +; an invalid page fault, much easier, since we can know the stack layout the +; entire time we're probing. + + push ebp + mov ebp, esp + push edi + push esi + + mov edi, [ebp + 8] + mov esi, [ebp + 12] + mov dl, [ebp + 16] + + ; edi - Address + ; esi - Length + ; dl - ProbeWrite + mov ecx, edi ; Load the address into ecx, a scratch register. + add ecx, esi ; Add the length to ecx. + dec ecx ; Decrement ecx. This will be the final byte of the range we're probing. + mov al, [ecx] ; Load a byte from that address. + test dl, dl ; Check if ProbeWrite is zero. + jz .StartLoop + mov [ecx], al ; If not, then store that byte to that address. +.StartLoop: + mov al, [edi] ; Load a byte from the start address. + test dl, dl ; Check if ProbeWrite is zero + jz .DontWrite + mov [edi], al ; If not, then store that byte to that address. +.DontWrite: + add edi, 4096 ; Add a page size to rdi. + cmp edi, ecx ; Check if the end address has been reached. + jb .StartLoop ; If not, then probe another page. + + xor eax, eax ; Return with an exit code of zero (STATUS_SUCCESS) + pop esi + pop edi + pop ebp + ret + +; void MmProbeAddressSubEarlyReturn() +; Returns early from MmProbeAddressSub and MmSafeCopySub. Called by the invalid page fault handler. +global MmProbeAddressSubEarlyReturn +MmProbeAddressSubEarlyReturn: + ret + +; int MmSafeCopySub(void* Address, void* Source, size_t Length); +global MmSafeCopySub +MmSafeCopySub: + push ebp + mov ebp, esp + + cld ; we don't want the copy the other way + push edi + push esi + mov edi, [ebp + 8] + mov esi, [ebp + 12] + mov ecx, [ebp + 16] + rep movsb + xor eax, eax + pop esi + pop edi + pop ebp + ret + +; void KiSwitchThreadStack(void** OldStack, void** NewStack) +global KiSwitchThreadStack +KiSwitchThreadStack: + push ebp + mov ebp, esp + + mov eax, [ebp + 8] + mov ecx, [ebp + 12] + + ; push System V ABI callee saved registers + pushfd + push ebp + push ebx + push esi + push edi + + ; Store the current rsp into *OldStack. + mov dword [eax], esp + + ; Load the current rsp from NewStack. + mov esp, dword [ecx] + + ; Pop everything and return as usual. +KiPopEverythingAndReturn: + pop edi + pop esi + pop ebx + pop ebp + xor eax, eax + + ; Pop EFLAGS and return + popfd + ret + +; Arguments: +; edi - Thread entry point. +; esi - Thread context. +extern KiUnlockDispatcher +global KiThreadEntryPoint +KiThreadEntryPoint: + push esi + + push dword 0 + call KiUnlockDispatcher + add esp, 4 + + ; ebx saved because KiUnlockDispatcher is SysV compliant + ; eax still pushed-- will be used as the argument + jmp edi + +; Used for the init phase of the scheduler, before a thread is scheduled in. +global KiSwitchThreadStackForever +KiSwitchThreadStackForever: + mov esp, [esp + 4] + jmp KiPopEverythingAndReturn + +; void KepLoadGdt(GdtDescriptor* desc); +global KepLoadGdt +KepLoadGdt: + mov eax, [esp + 4] + lgdt [eax] + + ; update the code segment + push dword 0x08 ; code segment + lea eax, [rel .a] ; jump address + push eax + retfd ; return far - will go to .a now +.a: + ; update the segments + mov ax, 0x10 + mov ds, ax + mov es, ax + mov fs, ax + mov gs, ax + mov ss, ax + ret + +; void KepLoadTss(int descriptor) +global KepLoadTss +KepLoadTss: + mov ax, [esp + 4] + ltr ax + ret diff --git a/boron/source/ke/i386/pio.c b/boron/source/ke/i386/pio.c new file mode 100644 index 00000000..877ba1be --- /dev/null +++ b/boron/source/ke/i386/pio.c @@ -0,0 +1,50 @@ +/*** + The Boron Operating System + Copyright (C) 2025 iProgramInCpp + +Module name: + ke/i386/pio.c + +Abstract: + This module implements x86 specific Port I/O functions. + +Author: + iProgramInCpp - 14 October 2025 +***/ +#include + +uint8_t KePortReadByte(uint16_t portNo) +{ + uint8_t rv; + ASM("inb %1, %0" : "=a" (rv) : "dN" (portNo)); + return rv; +} + +void KePortWriteByte(uint16_t portNo, uint8_t data) +{ + ASM("outb %0, %1"::"a"((uint8_t)data),"Nd"((uint16_t)portNo)); +} + +uint16_t KePortReadWord(uint16_t portNo) +{ + uint16_t rv; + ASM("inw %1, %0" : "=a" (rv) : "dN" (portNo)); + return rv; +} + +void KePortWriteWord(uint16_t portNo, uint16_t data) +{ + ASM("outw %0, %1"::"a"((uint16_t)data),"Nd"((uint16_t)portNo)); +} + +uint32_t KePortReadDword(uint16_t portNo) +{ + uint32_t rv; + ASM("inl %1, %0" : "=a" (rv) : "dN" (portNo)); + return rv; +} + +void KePortWriteDword(uint16_t portNo, uint32_t data) +{ + ASM("outl %0, %1"::"a"((uint32_t)data),"Nd"((uint16_t)portNo)); +} diff --git a/boron/source/ke/i386/probe.c b/boron/source/ke/i386/probe.c new file mode 100644 index 00000000..cca43f7e --- /dev/null +++ b/boron/source/ke/i386/probe.c @@ -0,0 +1,118 @@ +/*** + The Boron Operating System + Copyright (C) 2025 iProgramInCpp + +Module name: + ke/i386/probe.c + +Abstract: + This module implements the high level routines for address probing. + + Probing a set of addresses checks that they are usable in kernel mode. + It also brings all of the demand-pages back in to memory. It works by + attempting to read from / write to the memory. If the memory is not + accessible, an invalid page fault is raised by hardware. This makes + probing about as cheap as just copying the raw data. + +Author: + iProgramInCpp - 14 October 2025 +***/ +#include +#include + +bool MmIsAddressRangeValid(uintptr_t Address, size_t Size, KPROCESSOR_MODE AccessMode) +{ + // Size=0 is invalid. + if (Size == 0) { + DbgPrint("Size 0"); + return false; + } + + // Check for overflow. + uintptr_t AddressEnd = Address + Size; + if (AddressEnd < Address) { + DbgPrint("AddressEnd %p < Address %p", AddressEnd, Address); + return false; + } + + if (AccessMode == MODE_USER && AddressEnd > MM_USER_SPACE_END) { + DbgPrint("AccessMode==MODEUSER AddressEnd %p Address %p Size: %zu RA:%p", AddressEnd, Address, Size, CallerAddress()); + return false; + } + + return true; +} + +// Defined in arch/i386/misc.asm +int MmProbeAddressSub(void* Address, size_t Length, bool ProbeWrite); + +// This is the front-end for the probing code. The actual probing +// is performed in assembly, because it's impossible to predict what +// kind of stack layout the C version would use. (It could differ +// depending on compiler version, for example.) +BSTATUS MmProbeAddress(void* Address, size_t Length, bool ProbeWrite, KPROCESSOR_MODE AccessMode) +{ + if (!MmIsAddressRangeValid((uintptr_t)Address, Length, AccessMode)) + return STATUS_INVALID_PARAMETER; + + const uintptr_t MaxPtr = (uintptr_t) ~0ULL; // 0b1111...1111 + const uintptr_t HalfMaxPtr = MaxPtr >> 1; // 0b0111...1111 + const uintptr_t MSBPtrSet = ~HalfMaxPtr; // 0b1000...0000 + + // If the size is bigger than or equal to MAX_ADDRESS>>1 + if (Length > HalfMaxPtr) + return STATUS_INVALID_PARAMETER; + + uintptr_t AddressLimit = (uintptr_t) Address + Length; + + // If the address and the address limit are in + // different halves of the address space + if (((uintptr_t)Address ^ AddressLimit) == MSBPtrSet) + return STATUS_INVALID_PARAMETER; + + KeGetCurrentThread()->Probing = true; + + int Code = MmProbeAddressSub (Address, Length, ProbeWrite); + + if (Code != STATUS_SUCCESS) + { + KeGetCurrentThread()->Probing = false; + return Code; + } + + KeGetCurrentThread()->Probing = false; + return STATUS_SUCCESS; +} + +// Defined in arch/amd64/misc.asm +int MmSafeCopySub(void* Address, const void* Source, size_t Length); + +BSTATUS MmSafeCopy(void* Address, const void* Source, size_t Length, KPROCESSOR_MODE AccessMode, bool VerifyDest) +{ + if (VerifyDest) + { + if (!MmIsAddressRangeValid((uintptr_t)Address, Length, AccessMode)) + return STATUS_INVALID_PARAMETER; + } + else + { + if (!MmIsAddressRangeValid((uintptr_t)Source, Length, AccessMode)) + return STATUS_INVALID_PARAMETER; + } + + // Let the page fault handler know we are probing. + KeGetCurrentThread()->Probing = true; + + // This is just a regular old memcpy. Nothing different about it, + // other than the return value. It's a five instruction marvel. + int Code = MmSafeCopySub(Address, Source, Length); + + // If it returned through a path different than usual (i.e. it was + // detoured through MmProbeAddressSubEarlyReturn), then it's going + // to return STATUS_FAULT, which we'll mirror when returning. + + // No longer probing. + KeGetCurrentThread()->Probing = false; + + return Code; +} diff --git a/boron/source/ke/i386/thredsup.c b/boron/source/ke/i386/thredsup.c new file mode 100644 index 00000000..5f4c6cb8 --- /dev/null +++ b/boron/source/ke/i386/thredsup.c @@ -0,0 +1,36 @@ +/*** + The Boron Operating System + Copyright (C) 2025 iProgramInCpp + +Module name: + ke/i386/thredsup.c + +Abstract: + This module implements the architecture specific + thread state setup routine. + +Author: + iProgramInCpp - 14 October 2025 +***/ +#include +#include +#include + +NO_RETURN void KiThreadEntryPoint(); + +void KiSetupRegistersThread(PKTHREAD Thread) +{ + // Subtract 10 from the stack pointer to keep the final stack frame valid. + uintptr_t StackBottom = ((uintptr_t) Thread->Stack.Top + (uintptr_t) Thread->Stack.Size - 0x10) & ~0xF; + uint32_t* StackPointer = (uint32_t*) StackBottom; + + // KiPopEverythingAndReturn pops these + *(--StackPointer) = (uint32_t) KiThreadEntryPoint; // Set return address + *(--StackPointer) = 0x200; // Set IF when entering the thread + *(--StackPointer) = 0; // Set EBP + *(--StackPointer) = 0; // Set EBX + *(--StackPointer) = (uint32_t) Thread->StartContext; // Set ESI + *(--StackPointer) = (uint32_t) Thread->StartRoutine; // Set EDI + + Thread->StackPointer = StackPointer; +} diff --git a/boron/source/ke/i386/tlbs.c b/boron/source/ke/i386/tlbs.c new file mode 100644 index 00000000..048258ec --- /dev/null +++ b/boron/source/ke/i386/tlbs.c @@ -0,0 +1,37 @@ +/*** + The Boron Operating System + Copyright (C) 2025 iProgramInCpp + +Module name: + ke/i386/tlbs.c + +Abstract: + This module contains the i386 platform's specific + TLB shootdown routine. + +Author: + iProgramInCpp - 14 October 2025 +***/ +#include + +#ifdef CONFIG_SMP +#error If you want SMP 32-bit x86, you must copy the tlbs.c from AMD64! +#endif + +#define MAX_TLBS_LENGTH 4096 + +void KeIssueTLBShootDown(uintptr_t Address, size_t Length) +{ + if (Length == 0) + Length = 1; + + if (Length >= MAX_TLBS_LENGTH) + { + KeSetCurrentPageTable(KeGetCurrentPageTable()); + } + else + { + for (size_t i = 0; i < Length; i++) + KeInvalidatePage((void*)(Address + i * PAGE_SIZE)); + } +} diff --git a/boron/source/ke/i386/trap.asm b/boron/source/ke/i386/trap.asm new file mode 100644 index 00000000..e42f68b4 --- /dev/null +++ b/boron/source/ke/i386/trap.asm @@ -0,0 +1,155 @@ +; +; The Boron Operating System +; Copyright (C) 2025 iProgramInCpp +; +; Module name: +; ke/i386/trap.asm +; +; Abstract: +; This module contains the implementation for the common +; trap handler and the list of functions to enter when +; a certain interrupt vector is triggered. +; +; Author: +; iProgramInCpp - 14 October 2025 +; + +bits 32 + +%include "arch/i386.inc" + +extern KiTrapList +global KiTrapCallList + +section .text + +; int KiEnterHardwareInterrupt(int IntNo); +extern KiEnterHardwareInterrupt +; void KiExitHardwareInterrupt(int OldIpl); +extern KiExitHardwareInterrupt + +; Return Value: +; rax - Old interrupt state +global KeDisableInterrupts +KeDisableInterrupts: + pushfd ; push RFLAGS register + pop eax ; pop it into RAX + and eax, 0x200 ; check the previous interrupt flag (1 << 9) + shr eax, 9 ; shift right by 9 + cli ; disable interrupts + ret ; return rax + +; Arguments: +; 1 - Old interrupt state +global KeRestoreInterrupts +KeRestoreInterrupts: + mov eax, [esp + 4] + test eax, eax ; check if old state is zero + jz .ret ; if it is, return immediately + sti ; restore interrupts +.ret: + ret ; done + +; Push the entire state except EAX, EBX, ECX ,and EDX +%macro PUSH_STATE 0 + push esi + push edi + mov eax, cr2 + push eax +%endmacro + +; Pop the entire state except EAX, EBX, ECX, and EDX +%macro POP_STATE 0 + add esp, 4 ; the space occupied by the cr2 register + pop edi + pop esi +%endmacro + +global KiTrapCommon +KiTrapCommon: + push eax + push ebx + push ecx + push edx + lea ebx, [esp + 16] ; Get the pointer to the value after rbx and rax on the stack + lea ecx, [esp + 24] ; Get the pointer to the EIP from the interrupt frame. + lea edx, [esp + 28] ; Get the pointer to the CS from the interrupt frame. + mov eax, [edx] ; Load the CS. + push eax + ; Note that LEA doesn't actually perform any memory accesses, all it + ; does it load the address of certain things into a register. We then + ; defer actually loading those until after DS was changed. + PUSH_STATE ; Push the state, except for the old ipl + mov ebx, [ebx] ; Retrieve the interrupt number and RIP from interrupt frame. These were deferred + mov ecx, [ecx] ; so that we wouldn't attempt to access the kernel stack using the user's data segment. + mov edx, [edx] ; Load CS, to determine the previous mode when entering a hardware interrupt + push ecx ; Enter a stack frame so that stack printing doesn't skip over anything + push ebp + mov ebp, esp + cld ; Clear direction flag, will be restored by iretq + movsx edi, byte [KiTrapIplList + ebx] ; Get the IPL for the respective interrupt vector + push edi + call KiEnterHardwareInterrupt ; Tell the kernel we entered a hardware interrupt + add esp, 4 + push eax ; Push the old IPL that we obtained from the function + mov edi, esp ; Retrieve the PKREGISTERS to call the trap handler + push edi + call [KiTrapCallList + 8 * ebx] ; Call the trap handler. It returns the new RSP. + ; add esp, 4 skipped because redundant + mov esp, eax ; Use the new PKREGISTERS instance as what to pull + mov edi, eax ; Get the pointer to the register context + push edi + call KiExitHardwareInterrupt ; Tell the kernel we're exiting the hardware interrupt + add esp, 4 + pop edi ; Pop the old IPL because we don't need it any more + pop ebp ; Leave the stack frame + pop ecx ; Skip over the RIP duplicate that we pushed + POP_STATE ; Pop the state + pop eax ; Pop the RAX register - it has the old value of CS which we can check + pop edx ; Pop the EDX register + pop ecx ; Pop the ECX register + pop ebx ; Pop the EBX register + pop eax ; Pop the EAX register + add esp, 8 ; Pop the interrupt number and the error code + iretd + +%macro CLEAR_REGS 0 + xor ebx, ebx + xor ecx, ecx + xor edx, edx + xor esi, esi +%endmacro + +global KeDescendIntoUserMode +KeDescendIntoUserMode: + mov edi, [esp + 4] + mov esi, [esp + 8] + mov edx, [esp + 12] + + ; EDI - Initial program counter + ; ESI - Initial stack pointer + ; EDX - User context + push dword SEG_RING_3_DATA | 3 ; push SS + push esi ; push RSP + push dword 0x202 ; push RFLAGS + push dword SEG_RING_3_CODE | 3 ; push CS + push edi ; push RIP + + ; clear all the registers + xor eax, eax + xor ebp, ebp + mov edi, edx + CLEAR_REGS + + ; finally, swap gs and return to user mode. + cli + iretd + +section .bss +global KiTrapIplList +KiTrapIplList: + resb 256 ; Reserve a byte for each vector + +global KiTrapCallList +KiTrapCallList: + resq 256 ; Reserve a pointer for each vector diff --git a/boron/source/ke/i386/traplist.asm b/boron/source/ke/i386/traplist.asm new file mode 100644 index 00000000..941eb560 --- /dev/null +++ b/boron/source/ke/i386/traplist.asm @@ -0,0 +1,57 @@ +; +; The Boron Operating System +; Copyright (C) 2025 iProgramInCpp +; +; Module name: +; ke/i386/trap.asm +; +; Abstract: +; This module contains the implementation for each of the +; individual trap handlers, which call into the common +; trap handler. +; +; Author: +; iProgramInCpp - 14 October 2025 +; +bits 32 +section .text + +; NOTE: This uses 5120 bytes of data. Is that bad? I don't know. + +; Arguments: +; 0 - Interrupt number in hexadecimal +; 1 - If the interrupt has an error code, Y, otherwise, N +; %macro INT 2 + +extern KiTrapCommon + +%macro INT 2 +global KiTestTrap%1 +KiTestTrap%1: + %ifidn %2, N + push dword 0 ; Push a fake error code + %endif + push dword 0x%1 ; Push the interrupt number + jmp KiTrapCommon ; Jump to the common trap handler +%endmacro + +%include "ke/i386/intlist.inc" + +%unmacro INT 2 + +%macro INT 2 +extern KiTestTrap%1 +%endmacro +%include "ke/i386/intlist.inc" +%unmacro INT 2 + + +section .rodata + +global KiTrapList +KiTrapList: +%macro INT 2 + dd KiTestTrap%1 +%endmacro +%include "ke/i386/intlist.inc" +%unmacro INT 2 diff --git a/boron/source/ke/i386/traps.c b/boron/source/ke/i386/traps.c new file mode 100644 index 00000000..00a27583 --- /dev/null +++ b/boron/source/ke/i386/traps.c @@ -0,0 +1,237 @@ +/*** + The Boron Operating System + Copyright (C) 2025 iProgramInCpp + +Module name: + ke/i386/traps.c + +Abstract: + This header file implements support for the IDT (Interrupt + Dispatch Table). + +Author: + iProgramInCpp - 28 October 2025 +***/ +#include +#include +#include +#include +#include +#include "../../ke/ki.h" + +// The trap gate isn't likely to be used as it doesn't turn off +// interrupts when entering the interrupt handler. +enum KGATE_TYPE +{ + GATE_INT = 0xE, + GATE_TRAP = 0xF +}; + +// The IDT itself. +KIDT KiIdt; + +struct KIDT_DESCRIPTOR_tag +{ + uint16_t Limit; + PKIDT Base; +} +PACKED +KiIdtDescriptor; + +// The interrupt vector function type. It's not a function you can actually call from C. +typedef void(*KiInterruptVector)(); + +// Probably not going to be used because SYSCALL and SYSENTER both bypass the interrupt system. +// These are going to be optimized out, and are also niceties when needed, so I will keep them. +static UNUSED void KiSetInterruptDPL(PKIDT Idt, int Vector, int Ring) +{ + Idt->Entries[Vector].DPL = Ring; +} + +// This is also not likely to be used. We need interrupts to be disabled automatically +// before we raise the IPL to the interrupt's level, but trap gates will not disable interrupts. +// This is bad, since a keyboard interrupt could manage to sneak past an important clock interrupt +// before we manage to raise the IPL in the clock interrupt. +// Not to mention the performance gains would be minimal if at all existent. +static UNUSED void KiSetInterruptGateType(PKIDT Idt, int Vector, int GateType) +{ + Idt->Entries[Vector].GateType = GateType; +} + +// Set the IST of an interrupt vector. This is probably useful for the double fault handler, +// as it is triggered only when another interrupt failed to be called, which is useful in cases +// where the kernel stack went missing and bad (Which I hope there aren't any!) +static UNUSED void KiSetInterruptStackIndex(PKIDT Idt, int Vector, int Ist) +{ + Idt->Entries[Vector].IST = Ist; +} + +// This loads the interrupt vector handler into the IDT. +// Parameters: +// Idt - The IDT that the interrupt vector will be loaded in. +// Vector - The interrupt number that the handler will be assigned to. +static void KiLoadInterruptVector(PKIDT Idt, int Vector, KiInterruptVector Handler) +{ + PKIDT_ENTRY Entry = &Idt->Entries[Vector]; + memset(Entry, 0, sizeof * Entry); + + uintptr_t HandlerAddr = (uintptr_t) Handler; + + Entry->OffsetLow = HandlerAddr & 0xFFFF; + Entry->OffsetHigh = HandlerAddr >> 16; + + // The code segment that the interrupt handler will run in. + Entry->SegmentSel = SEG_RING_0_CODE; + + // Ist = 0 means the stack is not switched when the interrupt keeps the CPL the same. + // The IST should only be used in case of a fatal exception, such as a double fault. + Entry->IST = 0; + + // The ring that the interrupt can be called from. It's usually 0 (so you can't fake + // an exception from user mode), however, it can be set to 3 for userspace system calls. + // Usually we use `syscall` or `sysenter` for that, though. + Entry->DPL = 0; + + // This is an interrupt gate. + Entry->GateType = 0xE; + + // The entry is present. + Entry->Present = true; +} + +int KiEnterHardwareInterrupt(int NewIpl) +{ + PKPRCB Prcb = KeGetCurrentPRCB(); + + PKIPL IplPtr = &Prcb->Ipl; + + // grab old IPL + int OldIpl = (int) *IplPtr; + + // set new IPL, if not marked as "don't override" + if (NewIpl != -1) + { + *IplPtr = NewIpl; // specific to Amd64 + + if (OldIpl > NewIpl) + // uh oh! + KeCrash("KiEnterHardwareInterrupt: Old IPL of %d was higher than current IPL of %d.", OldIpl, NewIpl); + + KeOnUpdateIPL(NewIpl, OldIpl); + } + + // now that we've setup the hardware interrupt stuff, enable interrupts. + // we couldn't have done that before because the CPU would think that we're + // in a low IPL thing meanwhile we're not.. + ENABLE_INTERRUPTS(); + + return OldIpl; +} + +void KiExitHardwareInterrupt(PKREGISTERS Registers) +{ + DISABLE_INTERRUPTS(); + + int OldIpl = Registers->OldIpl; + + PKPRCB Prcb = KeGetCurrentPRCB(); + + KIPL PrevIpl = Prcb->Ipl; + Prcb->Ipl = OldIpl; + KeOnUpdateIPL(OldIpl, PrevIpl); + + // Check if the current thread is terminated and we are about to + // return to user mode. + if (KeGetCurrentThread()->PendingTermination && + Registers->OldIpl == IPL_NORMAL && + Registers->Cs == SEG_RING_3_CODE) + { + KiTerminateUserModeThread(KeGetCurrentThread()->IncrementTerminated); + } + + // Note: safe to call here because KiDispatchSoftwareInterrupts + // preserves interrupt disable state across a call to it + if (Prcb->PendingSoftInterrupts >> OldIpl) + KiDispatchSoftwareInterrupts(OldIpl); +} + +void KiCheckTerminatedUserMode() +{ + // Before entering user mode, check if the thread was terminated first. + if (KeGetCurrentThread()->PendingTermination) + KiTerminateUserModeThread(KeGetCurrentThread()->IncrementTerminated); +} + +// ==== Interrupt Handlers ==== + +// Generic interrupt handler. Used in case an interrupt is not implemented +PKREGISTERS KiTrapUnknownHandler(PKREGISTERS Regs) +{ + KeOnUnknownInterrupt(Regs); + return Regs; +} + +PKREGISTERS KiHandleDoubleFault(PKREGISTERS Regs) +{ + KeOnDoubleFault(Regs); + return Regs; +} + +PKREGISTERS KiHandleProtectionFault(PKREGISTERS Regs) +{ + KeOnProtectionFault(Regs); + return Regs; +} + +PKREGISTERS KiHandlePageFault(PKREGISTERS Regs) +{ + KeOnPageFault(Regs); + return Regs; +} + +extern void* const KiTrapList[]; // traplist.asm +extern int8_t KiTrapIplList[]; // trap.asm +extern void* KiTrapCallList[]; // trap.asm +static KSPIN_LOCK KiTrapLock; + +static int KepIplVectors[IPL_COUNT]; + +// Run on the BSP only. +void KiSetupIdt() +{ + KiIdtDescriptor.Base = &KiIdt; + KiIdtDescriptor.Limit = sizeof(KiIdt) - 1; + + for (int i = 0; i < 0x100; i++) + { + KiLoadInterruptVector(&KiIdt, i, KiTrapList[i]); + + KiTrapIplList[i] = IPL_NOINTS; + KiTrapCallList[i] = KiTrapUnknownHandler; + } + + for (int i = 0; i < IPL_COUNT; i++) + { + KepIplVectors[i] = i * 0x10; + } + + KeRegisterInterrupt(INTV_DBL_FAULT, KiHandleDoubleFault); + KeRegisterInterrupt(INTV_PROT_FAULT, KiHandleProtectionFault); + KeRegisterInterrupt(INTV_PAGE_FAULT, KiHandlePageFault); + KeSetInterruptIPL(INTV_DBL_FAULT, IPL_NOINTS); + KeSetInterruptIPL(INTV_PROT_FAULT, IPL_NOINTS); + KeSetInterruptIPL(INTV_PAGE_FAULT, IPL_UNDEFINED); +} + +void KeRegisterInterrupt(int Vector, PKINTERRUPT_HANDLER Handler) +{ + KIPL Ipl; + KeAcquireSpinLock(&KiTrapLock, &Ipl); + KiTrapCallList[Vector] = Handler; + KeReleaseSpinLock(&KiTrapLock, Ipl); +} + +void KeSetInterruptIPL(int Vector, KIPL Ipl) +{ + KiTrapIplList[Vector] = Ipl; +} diff --git a/boron/source/ldr/loader.c b/boron/source/ldr/loader.c index abb375e8..0b425941 100644 --- a/boron/source/ldr/loader.c +++ b/boron/source/ldr/loader.c @@ -107,6 +107,7 @@ void LdrInit() INIT static void LdrpReclaimFile(PLOADER_MODULE File) { +#ifdef IS_64_BIT if ((uintptr_t)File->Address < (uintptr_t)MmGetHHDMBase() || (uintptr_t)File->Address >= MM_PFNDB_BASE) { @@ -120,6 +121,9 @@ static void LdrpReclaimFile(PLOADER_MODULE File) MmFreePhysicalPage(Pfn); Address += PAGE_SIZE; } +#else + (void) File; +#endif } // NOTE: For now, selectively reclaim certain pages. At some point, we'll reclaim everything, and scrap this function diff --git a/boron/source/mm/i386/pt.c b/boron/source/mm/i386/pt.c new file mode 100644 index 00000000..c6812f64 --- /dev/null +++ b/boron/source/mm/i386/pt.c @@ -0,0 +1,448 @@ +/*** + The Boron Operating System + Copyright (C) 2023 iProgramInCpp + +Module name: + mm/amd64/pt.c + +Abstract: + This module implements page table management for + the AMD64 platform. + +Author: + iProgramInCpp - 8 September 2023 +***/ + +#include +#include +#include +#include +#include + +#define MI_PTE_LOC(Address) (MI_PML1_LOCATION + ((Address & MI_PML_ADDRMASK) >> 12) * sizeof(MMPTE)) + +PMMPTE MmGetPteLocation(uintptr_t Address) +{ + PMMPTE PtePtr = (PMMPTE)MI_PTE_LOC(Address); + + // HACK: Instead of just invalidating everything in the function + // MiFreeUnusedMappingLevelsInCurrentMap like I am supposed to, + // I will invalidate the TLB here. + // + // I know this is bad, but come on, when are we *ever* going to + // *not* go through this function? + KeInvalidatePage(PtePtr); + + return PtePtr; +} + +bool MmCheckPteLocation(uintptr_t Address, bool GenerateMissingLevels) +{ + ASSERT(Address < MI_PML1_LOCATION || (uint64_t)Address >= MI_PML1_LOC_END); + + // Check PML4, that's always accessible. + PMMPTE Pte; + + Pte = MmGetPteLocation(MI_PTE_LOC(MI_PTE_LOC(MI_PTE_LOC(Address)))); + if (~(*Pte) & MM_PTE_PRESENT) + goto Missing; + + // PML4 exists, check PML3 + Pte = MmGetPteLocation(MI_PTE_LOC(MI_PTE_LOC(Address))); + if (~(*Pte) & MM_PTE_PRESENT) + goto Missing; + + // PML3 exists, check PML2 + Pte = MmGetPteLocation(MI_PTE_LOC(Address)); + if (~(*Pte) & MM_PTE_PRESENT) + { + Missing: + if (GenerateMissingLevels) + { + PMMPTE Pte = MiGetPTEPointer(MiGetCurrentPageMap(), Address, GenerateMissingLevels); + return Pte != NULL; + } + + return false; + } + + return true; +} + +PMMPTE MmGetPteLocationCheck(uintptr_t Address, bool GenerateMissingLevels) +{ + if (!MmCheckPteLocation(Address, GenerateMissingLevels)) + return NULL; + + return MmGetPteLocation(Address); +} + +// Creates a page mapping. +HPAGEMAP MiCreatePageMapping(HPAGEMAP OldPageMapping) +{ + // Allocate the PML2. + int NewPageMappingPFN = MmAllocatePhysicalPage(); + if (NewPageMappingPFN == PFN_INVALID) + { + LogMsg("Error, can't create a new page mapping. Can't allocate PML4"); + return 0; + } + + uintptr_t NewPageMappingResult = MmPFNToPhysPage (NewPageMappingPFN); + PMMPTE NewPageMappingAccess = MmGetHHDMOffsetAddr (NewPageMappingResult), OldPageMappingAccess; + //PMMPTE OldPageMappingAccess = MmGetHHDMOffsetAddr (OldPageMapping); + + // copy the kernel's 256 entries, and zero out the first 256 + for (int i = 0; i < 256; i++) + { + NewPageMappingAccess[i] = 0; + } + + // Lock the kernel space's lock to not get any surprises. + MmLockKernelSpaceShared(); + + for (int i = 256; i < 512; i++) + { + //NewPageMappingAccess[i] = OldPageMappingAccess[i]; + + // We can't do it that easily because the new tree may be part of + // a different 8MB window. This is slow, but for now I don't really + // care! (In the best case, both PMLs are in the same 8MB section + // and no switching is performed) + MMPTE Temp; + + OldPageMappingAccess = MmGetHHDMOffsetAddr (OldPageMapping); + Temp = OldPageMappingAccess[i]; + + NewPageMappingAccess = MmGetHHDMOffsetAddr (NewPageMappingResult); + NewPageMappingAccess[i] = Temp; + } + + // For recursive paging + NewPageMappingAccess[MI_RECURSIVE_PAGING_START] = (uintptr_t)NewPageMappingResult | MM_PTE_PRESENT | MM_PTE_READWRITE | MM_PTE_NOEXEC; + + MmUnlockKernelSpace(); + + return (HPAGEMAP) NewPageMappingResult; +} + +PMMPTE MiGetPTEPointer(HPAGEMAP Mapping, uintptr_t Address, bool AllocateMissingPMLs) +{ + const uintptr_t TenTwentyThreeMask = 0x3FF; + + uintptr_t indices[] = { + 0, + (Address >> 12) & TenTwentyThreeMask, + (Address >> 22) & TenTwentyThreeMask, + 0, + }; + + int NumPfnsAllocated = 0; + int PfnsAllocated[3]; + MMPTE PtesOriginals[3]; + PMMPTE PtesModified [3]; + + MMPTE SupervisorBit; + if (Address >= MM_KERNEL_SPACE_BASE) + SupervisorBit = 0; + else + SupervisorBit = MM_PTE_USERACCESS; + + HPAGEMAP CurrentLevel = Mapping; + PMMPTE EntryPointer = NULL; + + for (int pml = 2; pml >= 1; pml--) + { + PMMPTE Entries = MmGetHHDMOffsetAddr(CurrentLevel); + + EntryPointer = &Entries[indices[pml]]; + + MMPTE Entry = *EntryPointer; + + if (pml > 1 && (~Entry & MM_PTE_PRESENT)) + { + // not present!! Do we allocate it? + if (!AllocateMissingPMLs) + return NULL; + + int pfn = MmAllocatePhysicalPage(); + if (pfn == PFN_INVALID) + { + DbgPrint("MiGetPTEPointer: Ran out of memory trying to allocate PTEs along the PML path"); + + // rollback + for (int i = 0; i < NumPfnsAllocated; i++) + { + *(PtesModified[i]) = PtesOriginals[i]; + MmFreePhysicalPage(PfnsAllocated[i]); + } + + return NULL; + } + + PtesModified [NumPfnsAllocated] = EntryPointer; + PtesOriginals[NumPfnsAllocated] = Entry; + PfnsAllocated[NumPfnsAllocated] = pfn; + NumPfnsAllocated++; + + memset(MmGetHHDMOffsetAddr(MmPFNToPhysPage(pfn)), 0, PAGE_SIZE); + + *EntryPointer = Entry = MM_PTE_PRESENT | MM_PTE_READWRITE | SupervisorBit | MmPFNToPhysPage(pfn); + } + + if (pml > 1 && (Entry & MM_PTE_PAGESIZE)) + { + // Higher page size, we can't allocate here. Probably HHDM or something - the kernel itself doesn't use this + DbgPrint("MiGetPTEPointer: Address %p contains a higher page size, we don't support that for now", Address); + return NULL; + } + + CurrentLevel = Entry & MM_PTE_ADDRESSMASK; + } + + return EntryPointer; +} + +static void MmpFreeVacantPMLsSub(HPAGEMAP Mapping, uintptr_t Address) +{ + // Lot of code was copied from MiGetPTEPointer. + const uintptr_t TenTwentyThreeMask = 0x3FF; + + uintptr_t indices[] = { + 0, + (Address >> 12) & TenTwentyThreeMask, + (Address >> 22) & TenTwentyThreeMask, + 0, + }; + + HPAGEMAP CurrentLevel = Mapping; + PMMPTE EntryPointer = NULL, ParentEntryPointer = NULL; + + for (int pml = 2; pml >= 1; pml--) + { + PMMPTE Entries = MmGetHHDMOffsetAddr(CurrentLevel); + + EntryPointer = &Entries[indices[pml]]; + + MMPTE Entry = *EntryPointer; + + if (pml > 1 && (~Entry & MM_PTE_PRESENT)) + { + // if we don't have a parent, return + if (!ParentEntryPointer) + return; + + // check if this entire page is vacant + for (int i = 0; i < 1024; i++) + { + if (Entries[i] != 0) + return; // isn't vacant + } + + // is vacant, so free it + *ParentEntryPointer = 0; + + int pfn = MmPhysPageToPFN(CurrentLevel); + MmFreePhysicalPage(pfn); + } + + if (pml > 1 && (Entry & MM_PTE_PAGESIZE)) + { + // Higher page size, we can't do that here. Probably HHDM or something - the kernel itself doesn't use this + return; + } + + CurrentLevel = Entry & MM_PTE_ADDRESSMASK; + ParentEntryPointer = EntryPointer; + } +} + +static void MmpFreeVacantPMLs(HPAGEMAP Mapping, uintptr_t Address) +{ + // Do this 2 times to ensure all levels are freed. Could be done better + for (int i = 0; i < 2; i++) + MmpFreeVacantPMLsSub(Mapping, Address); +} + +static bool MmpMapSingleAnonPageAtPte(PMMPTE Pte, uintptr_t Permissions, bool NonPaged) +{ + if (!Pte) + return false; + + if (MM_DBG_NO_DEMAND_PAGING || NonPaged) + { + int pfn = MmAllocatePhysicalPage(); + if (pfn == PFN_INVALID) + { + //DbgPrint("MiMapAnonPage(%p, %p) failed because we couldn't allocate physical memory", Mapping, Address); + return false; + } + + if (!Pte) + { + //DbgPrint("MiMapAnonPage(%p, %p) failed because PTE couldn't be retrieved", Mapping, Address); + return false; + } + + *Pte = MM_PTE_PRESENT | MM_PTE_ISFROMPMM | Permissions | MmPFNToPhysPage(pfn); + return true; + } + + *Pte = MM_DPTE_COMMITTED | Permissions; + + return true; +} + +bool MiMapAnonPage(HPAGEMAP Mapping, uintptr_t Address, uintptr_t Permissions, bool NonPaged) +{ + PMMPTE Pte = MiGetPTEPointer(Mapping, Address, true); + + return MmpMapSingleAnonPageAtPte(Pte, Permissions, NonPaged); +} + +bool MiMapPhysicalPage(HPAGEMAP Mapping, uintptr_t PhysicalPage, uintptr_t Address, uintptr_t Permissions) +{ + PMMPTE Pte = MiGetPTEPointer(Mapping, Address, true); + + if (!Pte) + return false; + + *Pte = (PhysicalPage & MM_PTE_ADDRESSMASK) | Permissions | MM_PTE_PRESENT; + + return true; +} + +void MiUnmapPages(HPAGEMAP Mapping, uintptr_t Address, size_t LengthPages) +{ + // Step 1. Unset the PRESENT bit on all pages in the range. + for (size_t i = 0; i < LengthPages; i++) + { + PMMPTE pPTE = MiGetPTEPointer(Mapping, Address + i * PAGE_SIZE, false); + + if (!pPTE) + continue; + + *pPTE &= ~MM_DPTE_COMMITTED; + + if (*pPTE & MM_PTE_PRESENT) + { + *pPTE &= ~MM_PTE_PRESENT; + *pPTE |= MM_DPTE_WASPRESENT; + } + else + { + *pPTE &= ~MM_DPTE_WASPRESENT; + } + } + + // Step 2. Issue a single TLB shootdown command to all CPUs to flush the TLB. + // TODO: This could be optimized, but eh, it's fine for now.. + MmIssueTLBShootDown(Address, LengthPages); + + // Step 3. If needed, free the PMM pages related to this page mapping. + for (size_t i = 0; i < LengthPages; i++) + { + PMMPTE pPTE = MiGetPTEPointer(Mapping, Address + i * PAGE_SIZE, false); + + if (!pPTE) + continue; + + uintptr_t Flags = MM_DPTE_WASPRESENT | MM_PTE_ISFROMPMM; + + if ((*pPTE & Flags) == Flags) + { + uintptr_t PhysPage = *pPTE & MM_PTE_ADDRESSMASK; + + MmFreePhysicalPage(MmPhysPageToPFN(PhysPage)); + + *pPTE = 0; + } + } + + return; + + // Step 4. Free higher PMLs if they're fully vacant + for (size_t i = 0; i < LengthPages; i++) + { + MmpFreeVacantPMLs(Mapping, Address + i * PAGE_SIZE); + } +} + +void MiPrepareGlobalAreaForPool(HPAGEMAP PageMap) +{ + PMMPTE Ptes = MmGetHHDMOffsetAddr(PageMap); + + int Pfn = MmAllocatePhysicalPage(); + if (Pfn == PFN_INVALID) + { + KeCrashBeforeSMPInit("MiPrepareGlobalAreaForPool: Can't allocate global pml4"); + } + + Ptes[MI_GLOBAL_AREA_START] = + MM_PTE_PRESENT | + MM_PTE_READWRITE | + MM_PTE_GLOBAL | + MM_PTE_ISFROMPMM | + MM_PTE_NOEXEC | + MmPFNToPhysPage(Pfn); +} + +uintptr_t MiGetTopOfPoolManagedArea() +{ + return MI_GLOBAL_AREA_START << 22; +} + +uintptr_t MiGetTopOfSecondPoolManagedArea() +{ + return MI_GLOBAL_AREA_START_2ND << 22; +} + +bool MiMapAnonPages(HPAGEMAP Mapping, uintptr_t Address, size_t SizePages, uintptr_t Permissions, bool NonPaged) +{ + // As an optimization, we'll wait until the PML1 index rolls over to zero before reloading the PTE pointer. + uint64_t CurrentPml1 = PML1_IDX(Address); + size_t DonePages = 0; + + PMMPTE PtePtr = MiGetPTEPointer(Mapping, Address, true); + + for (size_t i = 0; i < SizePages; i++) + { + // If one of these fails, then we should roll back. + if (!MmpMapSingleAnonPageAtPte(PtePtr, Permissions, NonPaged)) + goto ROLLBACK; + + // Increase the address size, get the next PTE pointer, update the current PML1, and + // increment the number of mapped pages (since this one was successfully mapped). + Address += PAGE_SIZE; + PtePtr++; + CurrentPml1++; + DonePages++; + + if (CurrentPml1 % (PAGE_SIZE / sizeof(MMPTE)) == 0) + { + // We have rolled over. + PtePtr = MiGetPTEPointer(Mapping, Address, true); + } + } + + // All allocations have succeeded! Let the caller know and don't undo our work. :) + return true; + +ROLLBACK: + // Unmap all the pages that we have mapped. + MiUnmapPages(Mapping, Address, DonePages); + return false; +} + +MMPTE MmGetPteBitsFromProtection(int Protection) +{ + MMPTE Pte = 0; + + if (Protection & PAGE_WRITE) + Pte |= MM_PTE_READWRITE; + + if (~Protection & PAGE_EXECUTE) + Pte |= MM_PTE_NOEXEC; + + return Pte; +} diff --git a/boron/source/mm/i386/ptfree.c b/boron/source/mm/i386/ptfree.c new file mode 100644 index 00000000..dfa890f3 --- /dev/null +++ b/boron/source/mm/i386/ptfree.c @@ -0,0 +1,153 @@ +/*** + The Boron Operating System + Copyright (C) 2025 iProgramInCpp + +Module name: + mm/i386/ptfree.c + +Abstract: + This module implements the function that frees unused + page table mapping levels. + +Author: + iProgramInCpp - 14 October 2025 +***/ +#include "../mi.h" + +#define PTES_PER_LEVEL (PAGE_SIZE / sizeof(MMPTE)) // 1024 +#define PAGE_MAP_LEVELS 8 +#define PTES_COVERED_BY_PML2 PTES_PER_LEVEL + +// Gets an address down the tree. +// +// For example, if you're in the PML2, this will give you +// the relevant PML1 address. +PMMPTE MiGetSubPteAddress(PMMPTE PteAddress) +{ + MMADDRESS_CONVERT Address; + Address.Long = (uintptr_t) PteAddress; + + Address.Level2Index = Address.Level1Index; + Address.Level1Index = Address.PageOffset / sizeof(PMMPTE); + Address.PageOffset = 0; + + return (PMMPTE) Address.Long; +} + +static bool MmpIsPteListCompletelyEmpty(PMMPTE Pte) +{ + bool AllZeroes = true; + + for (size_t PteIndex = 0; PteIndex < PTES_PER_LEVEL; PteIndex++) + { + if (Pte[PteIndex] != 0) + { + AllZeroes = false; + break; + } + } + + return AllZeroes; +} + +static bool MmpFreeUnusedMappingLevelsInCurrentMapPML(PMMPTE Pte, int RecursionCount); + +// NOTE: StartVa and SizePages are only roughly followed. +// +// NOTE: The address space lock of the process *must* be held. +// This also issues a TLB shootdown covering the affected area. +void MiFreeUnusedMappingLevelsInCurrentMap(uintptr_t StartVa, size_t SizePages) +{ + bool ShouldUnmapPML2; + MMADDRESS_CONVERT Address; + PMMPTE Pte; + + ShouldUnmapPML2 = true; + Address.Long = StartVa; + + Pte = (PMMPTE) MI_PML1_LOCATION + Address.Level2Index; + + // Currently, we can only operate in user space. This is because + // in kernel space, the PML2 pages may never be deallocated. + if (Address.Level2Index >= PTES_PER_LEVEL / 2) + ShouldUnmapPML2 = false; + + for (size_t PageNumber = 0; + PageNumber < SizePages; + PageNumber += PTES_COVERED_BY_PML2, + ++Pte) + { + if (~*Pte & MM_PTE_PRESENT) + continue; + + PMMPTE SubPte = MiGetSubPteAddress(Pte); + if (MmpIsPteListCompletelyEmpty(SubPte)) + { + MmFreePhysicalPage((*Pte & MI_PML_ADDRMASK) >> 12); + *Pte = 0; + continue; + } + + if (!MmpFreeUnusedMappingLevelsInCurrentMapPML(SubPte, PAGE_MAP_LEVELS - 1)) + continue; + + // Returned true, so this is now ready to free. + if (ShouldUnmapPML2) + { + MmFreePhysicalPage((*Pte & MI_PML_ADDRMASK) >> 12); + *Pte = 0; + } + } +} + +static bool MmpFreeUnusedMappingLevelsInCurrentMapPML(PMMPTE Pte, int MapLevel) +{ + if (MapLevel <= 1) + { + // We're on the last level. + for (size_t i = 0; i < PTES_PER_LEVEL; ++i, ++Pte) + { + if (*Pte) + { + // The PTE exists, check if it was decommitted though. + if ((~*Pte & MM_PTE_PRESENT) && (*Pte & MM_DPTE_DECOMMITTED)) + continue; + + // This mapping level is busy. + return false; + } + } + + return true; + } + + bool FreeParent = true; + + // Walk the PML. + for (size_t i = 0; i < PTES_PER_LEVEL; ++i, ++Pte) + { + if (~*Pte & MM_PTE_PRESENT) + continue; + + PMMPTE SubPte = MiGetSubPteAddress(Pte); + + if (MmpIsPteListCompletelyEmpty(SubPte)) + { + MmFreePhysicalPage((*Pte & MI_PML_ADDRMASK) >> 12); + *Pte = 0; + continue; + } + + if (!MmpFreeUnusedMappingLevelsInCurrentMapPML(SubPte, MapLevel - 1)) + { + FreeParent = false; + continue; + } + + // Returned true, so this is now ready to free. + MmFreePhysicalPage((*Pte & MI_PML_ADDRMASK) >> 12); + *Pte = 0; + } + + return FreeParent; +} diff --git a/drivers/CommonMakefile b/drivers/CommonMakefile index 468c3a24..d87f9315 100644 --- a/drivers/CommonMakefile +++ b/drivers/CommonMakefile @@ -19,14 +19,14 @@ LINKER_FILE = linker.ld DRIVER_ENTRY ?= DriverEntry -ARCHITECTURE = AMD64 +TARGET ?= AMD64 # This sucks. -ARCHITECTUREL=$(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$(ARCHITECTURE))))))))))))))))))))))))))) +TARGETL=$(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$(TARGET))))))))))))))))))))))))))) # This is the name that our final driver executable will have. # Change as needed. -override TARGET := $(BUILD_DIR)/out.$(ARCHITECTUREL).sys +override TARGET_DRIVER := $(BUILD_DIR)/out.$(TARGETL).sys # Convenience macro to reliably declare overridable command variables. define DEFAULT_VAR = @@ -50,20 +50,13 @@ ifeq ($(DEBUG2), yes) DEFINES += -DDEBUG2 endif -# It is highly recommended to use a custom built cross toolchain to build a driver. -# We are only using "cc" as a placeholder here. It may work by using -# the host system's toolchain, but this is not guaranteed. -$(eval $(call DEFAULT_VAR,CC,cc)) -$(eval $(call DEFAULT_VAR,CXX,c++)) - -# Same thing for "ld" (the linker). -$(eval $(call DEFAULT_VAR,LD,ld)) +include ../../tools/toolchain.mk # User controllable CFLAGS. -CFLAGS ?= $(OPT) -pipe -Wall -Wextra -DTARGET_$(ARCHITECTURE) -DIS_KERNEL_MODE $(DEFINES) $(USER_DEFINES) +CFLAGS ?= $(OPT) -pipe -Wall -Wextra -DTARGET_$(TARGET) -DIS_KERNEL_MODE $(DEFINES) $(USER_DEFINES) # User controllable CXXFLAGS. -CXXFLAGS ?= $(OPT) -pipe -Wall -Wextra -DTARGET_$(ARCHITECTURE) -DIS_KERNEL_MODE $(DEFINES) $(USER_DEFINES) +CXXFLAGS ?= $(OPT) -pipe -Wall -Wextra -DTARGET_$(TARGET) -DIS_KERNEL_MODE $(DEFINES) $(USER_DEFINES) # User controllable preprocessor flags. CPPFLAGS ?= -I $(INC_DIR) -I $(KE_DIR) -I $(DDK_DIR) @@ -137,7 +130,7 @@ override CFILES := $(shell find $(SRC_DIR) -not -path '*/.*' -type f -name '*.c' override CXXFILES := $(shell find $(SRC_DIR) -not -path '*/.*' -type f -name '*.cpp') override ASFILES := $(shell find $(SRC_DIR) -not -path '*/.*' -type f -name '*.S') override NASMFILES := $(shell find $(SRC_DIR) -not -path '*/.*' -type f -name '*.asm') -override OBJ := $(patsubst %.o,%.$(ARCHITECTUREL).o,$(patsubst $(SRC_DIR)/%,$(BUILD_DIR)/%,$(CFILES:.c=.o) $(CXXFILES:.cpp=.o) $(ASFILES:.S=.o) $(NASMFILES:.asm=.o))) +override OBJ := $(patsubst %.o,%.$(TARGETL).o,$(patsubst $(SRC_DIR)/%,$(BUILD_DIR)/%,$(CFILES:.c=.o) $(CXXFILES:.cpp=.o) $(ASFILES:.S=.o) $(NASMFILES:.asm=.o))) override HEADER_DEPS := $(patsubst %.o,%.d,$(OBJ)) # Default target. @@ -145,33 +138,33 @@ override HEADER_DEPS := $(patsubst %.o,%.d,$(OBJ)) all: driver # Link rules for the final driver executable. -$(TARGET): $(OBJ) - @echo "[LD]\tBuilding $(TARGET)" +$(TARGET_DRIVER): $(OBJ) + @echo "[LD]\tBuilding $(TARGET_DRIVER)" @$(LD) $(OBJ) $(LDFLAGS) -o $@ # Include header dependencies. -include $(HEADER_DEPS) # Compilation rules for *.c files. -$(BUILD_DIR)/%.$(ARCHITECTUREL).o: $(SRC_DIR)/%.c +$(BUILD_DIR)/%.$(TARGETL).o: $(SRC_DIR)/%.c @echo "[CC]\tCompiling $<" @mkdir -p $(dir $@) @$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ # Compilation rules for *.cpp files. -$(BUILD_DIR)/%.$(ARCHITECTUREL).o: $(SRC_DIR)/%.cpp +$(BUILD_DIR)/%.$(TARGETL).o: $(SRC_DIR)/%.cpp @echo "[CXX]\tCompiling $<" @mkdir -p $(dir $@) @$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@ # Compilation rules for *.S files. -$(BUILD_DIR)/%.$(ARCHITECTUREL).o: $(SRC_DIR)/%.S +$(BUILD_DIR)/%.$(TARGETL).o: $(SRC_DIR)/%.S @echo "[AS]\tCompiling $<" @mkdir -p $(dir $@) @$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ # Compilation rules for *.asm (nasm) files. -$(BUILD_DIR)/%.$(ARCHITECTUREL).o: $(SRC_DIR)/%.asm +$(BUILD_DIR)/%.$(TARGETL).o: $(SRC_DIR)/%.asm @echo "[AS]\tCompiling $<" @mkdir -p $(dir $@) @nasm $(NASMFLAGS) $< -o $@ @@ -180,6 +173,6 @@ $(BUILD_DIR)/%.$(ARCHITECTUREL).o: $(SRC_DIR)/%.asm .PHONY: clean clean: @echo "Cleaning" - @rm -rf $(TARGET) $(OBJ) $(HEADER_DEPS) + @rm -rf $(TARGET_DRIVER) $(OBJ) $(HEADER_DEPS) -driver: $(TARGET) +driver: $(TARGET_DRIVER) diff --git a/tools/toolchain.mk b/tools/toolchain.mk new file mode 100644 index 00000000..e7407119 --- /dev/null +++ b/tools/toolchain.mk @@ -0,0 +1,67 @@ + +# Determine the build tools used automatically. +ifeq ($(TARGETL), amd64) + # Compiler Toolchain + BCC ?= gcc + BCXX ?= g++ + BLD ?= ld + BASM ?= nasm + + # Compiler and linker flags + # + # NOTE 7.7.2024 -- No-reorder-functions was added because a certain functions + # was generating an "unlikely" section, which was placed at different addresses + # in kernel.elf and kernel2.elf, screwing up the symbol table... That's pretty + # bad. + # + # TODO: fix above ^^^ + ARCH_CFLAGS = \ + -m64 \ + -march=x86-64 \ + -mabi=sysv \ + -mno-80387 \ + -mno-mmx \ + -mno-sse \ + -mno-sse2 \ + -mno-red-zone \ + -mcmodel=kernel \ + -fno-reorder-functions + + ARCH_LDFLAGS = \ + -z max-page-size=0x1000 + + ARCH_ASFLAGS = \ + -f elf64 + + LINK_ARCH = elf_x86_64 + SMP = yes + +else ifeq ($(TARGETL), i386) + # Compiler Toolchain + BCC ?= clang + BCXX ?= clang++ + BLD ?= ld + BASM ?= nasm + + # Compiler and linker flags + ARCH_CFLAGS = \ + -target i686-elf \ + -mno-80387 \ + -mno-mmx \ + -mno-sse \ + -mno-sse2 + + ARCH_LDFLAGS = \ + -z max-page-size=0x1000 \ + -L. \ + -lgcc-i686 + + ARCH_ASFLAGS = \ + -f elf32 + + LINK_ARCH = elf_i386 + SMP = no + +else + $(error You cannot build for this architecture right now.) +endif From 31b903cf2b28e0684d3bb74d8da5ea4eca68bed6 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Wed, 15 Oct 2025 11:02:49 +0300 Subject: [PATCH 05/44] * X86: Userspace and test driver now build for i386! --- boron/Makefile | 7 +++-- boron/include/arch/amd64.h | 6 ++-- boron/include/arch/i386.h | 12 +++++--- boron/linker.i386.ld | 3 +- boron/source/build_number | 2 +- drivers/CommonMakefile | 50 ++++++++++++---------------------- drivers/test/source/fworktst.c | 4 +-- tools/toolchain.mk | 11 +++++--- user/CommonMakefile | 36 ++++++++++++------------ user/libboron/source/reloc.c | 24 +++++++++------- 10 files changed, 76 insertions(+), 79 deletions(-) diff --git a/boron/Makefile b/boron/Makefile index 2f4cd175..30196d03 100644 --- a/boron/Makefile +++ b/boron/Makefile @@ -32,7 +32,7 @@ TARGETL=$(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f, BUILD_DIR = build SRC_DIR = source INC_DIR = include -DDKI_DIR = ../common/include +DDK_DIR = ../common/include SCRIPTS_DIR = scripts LINKER_FILE = linker.$(TARGETL).ld ISO_DIR=$(BUILD_DIR)/iso_root @@ -75,6 +75,7 @@ else DEFINES += -O3 endif +IS_KERNEL = yes include ../tools/toolchain.mk ifeq ($(SMP), yes) @@ -88,10 +89,10 @@ CFLAGS ?= -pipe -Wall -Wextra -DTARGET_$(TARGET) -DKERNEL -DIS_KERNEL_MODE $(DEF CXXFLAGS ?= -pipe -Wall -Wextra -DTARGET_$(TARGET) -DKERNEL -DIS_KERNEL_MODE $(DEFINES) # User controllable preprocessor flags. -CPPFLAGS ?= -I $(INC_DIR) -I $(DDKI_DIR) +CPPFLAGS ?= -I $(INC_DIR) -I $(DDK_DIR) # User controllable nasm flags. -NASMFLAGS ?= -F dwarf -I$(SRC_DIR) -I$(INC_DIR) -I$(DDKI_DIR) +NASMFLAGS ?= -F dwarf -I$(SRC_DIR) -I$(INC_DIR) -I$(DDK_DIR) # User controllable linker flags. We set none by default. LDFLAGS ?= diff --git a/boron/include/arch/amd64.h b/boron/include/arch/amd64.h index e8dbf164..967fad78 100644 --- a/boron/include/arch/amd64.h +++ b/boron/include/arch/amd64.h @@ -5,6 +5,8 @@ #error "Don't include this if you aren't building for amd64!" #endif +#include + // Model specific registers uint64_t KeGetMSR(uint32_t msr); void KeSetMSR(uint32_t msr, uint64_t value); @@ -52,7 +54,7 @@ typedef union } MMADDRESS_CONVERT; -#endif +#endif // KERNEL // Note! Most of these are going to be present everywhere we'll port to. @@ -172,8 +174,6 @@ struct KREGISTERS_tag uint64_t ss; }; -#include - // IDT #define C_IDT_MAX_ENTRIES (0x100) diff --git a/boron/include/arch/i386.h b/boron/include/arch/i386.h index 0b632f44..d132d73d 100644 --- a/boron/include/arch/i386.h +++ b/boron/include/arch/i386.h @@ -14,6 +14,12 @@ Module name: ***/ #pragma once +#ifndef TARGET_I386 +#error "Don't include this if you aren't building for i386!" +#endif + +#include + // Model specific registers uint64_t KeGetMSR(uint32_t msr); void KeSetMSR(uint32_t msr, uint64_t value); @@ -59,6 +65,8 @@ typedef union } MMADDRESS_CONVERT; +#endif // KERNEL + #define MM_KERNEL_SPACE_BASE (0x80000000U) #define MM_USER_SPACE_END (0x7FFFFFFFU) @@ -165,8 +173,6 @@ struct KREGISTERS_tag uint32_t Ss; }; -#include - // IDT #define C_IDT_MAX_ENTRIES (0x100) @@ -291,5 +297,3 @@ KARCH_DATA, *PKARCH_DATA; #define MSI_LEVELASSERT (1 << 14) #include - -#endif diff --git a/boron/linker.i386.ld b/boron/linker.i386.ld index 193eb9a1..d95bfabf 100644 --- a/boron/linker.i386.ld +++ b/boron/linker.i386.ld @@ -14,7 +14,6 @@ ENTRY (KiBeforeSystemStartup) PHDRS { intext PT_LOAD FLAGS((1 << 0) | (1 << 2)) ; /* Execute + Read */ - indata PT_LOAD FLAGS((1 << 1) | (1 << 2)) ; /* Write + Read */ text PT_LOAD FLAGS((1 << 0) | (1 << 2)) ; /* Execute + Read */ rodata PT_LOAD FLAGS((1 << 2)) ; /* Read only */ data PT_LOAD FLAGS((1 << 1) | (1 << 2)) ; /* Write + Read */ @@ -32,7 +31,7 @@ SECTIONS .ipldata : { *(.ipldata .ipldata.*) - } :indata + } :intext .ipltext : { diff --git a/boron/source/build_number b/boron/source/build_number index 70e1a64c..bc768da7 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -144 \ No newline at end of file +146 \ No newline at end of file diff --git a/drivers/CommonMakefile b/drivers/CommonMakefile index d87f9315..41ab5b6b 100644 --- a/drivers/CommonMakefile +++ b/drivers/CommonMakefile @@ -68,28 +68,21 @@ NASMFLAGS ?= -F dwarf -I$(SRC_DIR) -I$(INC_DIR) -I $(KE_DIR) -I $(DDK_DIR) LDFLAGS ?= # Internal C flags that should not be changed by the user. -override CFLAGS += \ +CFLAGS += \ -fno-omit-frame-pointer \ - -std=c11 \ + -std=c2x \ -ffreestanding \ -fno-stack-protector \ -fno-stack-check \ -fno-lto \ -fPIC \ - -m64 \ - -march=x86-64 \ - -mabi=sysv \ - -mno-80387 \ - -mno-mmx \ - -mno-sse \ - -mno-sse2 \ - -mno-red-zone \ -MMD \ -MP \ - -I. + -I. \ + $(ARCH_CFLAGS) # Internal C++ flags that should not be changed by the user. -override CXXFLAGS += \ +CXXFLAGS += \ -fno-omit-frame-pointer \ -std=c++17 \ -ffreestanding \ @@ -97,33 +90,26 @@ override CXXFLAGS += \ -fno-stack-check \ -fno-lto \ -fPIC \ - -m64 \ - -march=x86-64 \ - -mabi=sysv \ - -mno-80387 \ - -mno-mmx \ - -mno-sse \ - -mno-sse2 \ - -mno-red-zone \ + -fno-reorder-functions \ -MMD \ -MP \ -fno-exceptions \ -fno-rtti \ - -I. + -I. \ + $(ARCH_CFLAGS) -override LDFLAGSBASE += \ +LDFLAGSBASE += \ -nostdlib -shared \ -e $(DRIVER_ENTRY) \ - -m elf_x86_64 \ - -z max-page-size=0x1000 + -m $(LINK_ARCH) \ + $(ARCH_LDFLAGS) # Internal linker flags that should not be changed by the user. -override LDFLAGS += \ +LDFLAGS += \ $(LDFLAGSBASE) # Internal nasm flags that should not be changed by the user. -override NASMFLAGS += \ - -f elf64 +NASMFLAGS += $(ARCH_ASFLAGS) # Use find to glob all *.c, *.S, and *.asm files in the directory and extract the object names. override CFILES := $(shell find $(SRC_DIR) -not -path '*/.*' -type f -name '*.c') @@ -140,7 +126,7 @@ all: driver # Link rules for the final driver executable. $(TARGET_DRIVER): $(OBJ) @echo "[LD]\tBuilding $(TARGET_DRIVER)" - @$(LD) $(OBJ) $(LDFLAGS) -o $@ + $(BLD) $(OBJ) $(LDFLAGS) -o $@ # Include header dependencies. -include $(HEADER_DEPS) @@ -149,25 +135,25 @@ $(TARGET_DRIVER): $(OBJ) $(BUILD_DIR)/%.$(TARGETL).o: $(SRC_DIR)/%.c @echo "[CC]\tCompiling $<" @mkdir -p $(dir $@) - @$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ + @$(BCC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ # Compilation rules for *.cpp files. $(BUILD_DIR)/%.$(TARGETL).o: $(SRC_DIR)/%.cpp @echo "[CXX]\tCompiling $<" @mkdir -p $(dir $@) - @$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@ + @$(BCXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@ # Compilation rules for *.S files. $(BUILD_DIR)/%.$(TARGETL).o: $(SRC_DIR)/%.S @echo "[AS]\tCompiling $<" @mkdir -p $(dir $@) - @$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ + @$(BCC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ # Compilation rules for *.asm (nasm) files. $(BUILD_DIR)/%.$(TARGETL).o: $(SRC_DIR)/%.asm @echo "[AS]\tCompiling $<" @mkdir -p $(dir $@) - @nasm $(NASMFLAGS) $< -o $@ + @$(BASM) $(NASMFLAGS) $< -o $@ # Remove object files and the final executable. .PHONY: clean diff --git a/drivers/test/source/fworktst.c b/drivers/test/source/fworktst.c index 8c14248a..7f035b67 100644 --- a/drivers/test/source/fworktst.c +++ b/drivers/test/source/fworktst.c @@ -131,13 +131,13 @@ void Init() uint64_t ReadTsc() { - uint64_t low, high; + uintptr_t low, high; // note: The rdtsc instruction is specified to zero out the top 32 bits of rax and rdx. ASM("rdtsc":"=a"(low), "=d"(high)); // So something like this is fine. - return high << 32 | low; + return ((uint64_t)high << 32) | low; } unsigned RandTscBased() diff --git a/tools/toolchain.mk b/tools/toolchain.mk index e7407119..84744fda 100644 --- a/tools/toolchain.mk +++ b/tools/toolchain.mk @@ -24,9 +24,8 @@ ifeq ($(TARGETL), amd64) -mno-sse \ -mno-sse2 \ -mno-red-zone \ - -mcmodel=kernel \ -fno-reorder-functions - + ARCH_LDFLAGS = \ -z max-page-size=0x1000 @@ -36,6 +35,10 @@ ifeq ($(TARGETL), amd64) LINK_ARCH = elf_x86_64 SMP = yes + ifeq ($(IS_KERNEL), yes) + ARCH_CFLAGS += -mcmodel=kernel + endif + else ifeq ($(TARGETL), i386) # Compiler Toolchain BCC ?= clang @@ -52,8 +55,8 @@ else ifeq ($(TARGETL), i386) -mno-sse2 ARCH_LDFLAGS = \ - -z max-page-size=0x1000 \ - -L. \ + -z max-page-size=0x1000 \ + -L$(DDK_DIR)/../../boron \ -lgcc-i686 ARCH_ASFLAGS = \ diff --git a/user/CommonMakefile b/user/CommonMakefile index 714108ef..72a96ff7 100644 --- a/user/CommonMakefile +++ b/user/CommonMakefile @@ -16,10 +16,10 @@ DDK_DIR = ../../common/include SDK_DIR = ../../user/include LINKER_FILE = linker.ld -ARCHITECTURE = AMD64 +TARGET ?= AMD64 # This sucks. -ARCHITECTUREL=$(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$(ARCHITECTURE))))))))))))))))))))))))))) +TARGETL=$(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$(TARGET))))))))))))))))))))))))))) ifeq ($(IS_LIBRARY),yes) SUFFIX = so @@ -32,8 +32,8 @@ INSTALL_DIR = ../../build # This is the name that our final application executable will have. # Change as needed. -override TARGET := $(BUILD_DIR)/$(APP_NAME).$(ARCHITECTUREL).$(SUFFIX) -override TARGET_INSTALL := $(INSTALL_DIR)/$(ARCHITECTUREL)/$(APP_NAME).$(SUFFIX) +override TARGET_FILE := $(BUILD_DIR)/$(APP_NAME).$(TARGETL).$(SUFFIX) +override TARGET_INSTALL := $(INSTALL_DIR)/$(TARGETL)/$(APP_NAME).$(SUFFIX) # Convenience macro to reliably declare overridable command variables. define DEFAULT_VAR = @@ -68,11 +68,11 @@ $(eval $(call DEFAULT_VAR,LD,ld)) # User controllable CFLAGS. CFLAGS ?= -CFLAGS += $(OPT) -pipe -Wall -Wextra -DTARGET_$(ARCHITECTURE) -DIS_USER_MODE $(DEFINES) $(USER_DEFINES) +CFLAGS += $(OPT) -pipe -Wall -Wextra -DTARGET_$(TARGET) -DIS_USER_MODE $(DEFINES) $(USER_DEFINES) # User controllable CXXFLAGS. CXXFLAGS ?= -CXXFLAGS += $(OPT) -pipe -Wall -Wextra -DTARGET_$(ARCHITECTURE) -DIS_USER_MODE $(DEFINES) $(USER_DEFINES) +CXXFLAGS += $(OPT) -pipe -Wall -Wextra -DTARGET_$(TARGET) -DIS_USER_MODE $(DEFINES) $(USER_DEFINES) # User controllable preprocessor flags. CPPFLAGS ?= @@ -152,7 +152,7 @@ ifeq ($(INCLUDE_LIBBORON),no) else LDFLAGS += \ -lboron \ - -L../../build/$(ARCHITECTUREL) + -L../../build/$(TARGETL) endif # Use find to glob all *.c, *.S, and *.asm files in the directory and extract the object names. @@ -160,7 +160,7 @@ override CFILES := $(shell find -L $(SRC_DIR) -not -path '*/.*' -type f -na override CXXFILES := $(shell find -L $(SRC_DIR) -not -path '*/.*' -type f -name '*.cpp') override ASFILES := $(shell find -L $(SRC_DIR) -not -path '*/.*' -type f -name '*.S') override NASMFILES := $(shell find -L $(SRC_DIR) -not -path '*/.*' -type f -name '*.asm') -override OBJ := $(patsubst %.o,%.$(ARCHITECTUREL).o,$(patsubst $(SRC_DIR)/%,$(BUILD_DIR)/%,$(CFILES:.c=.o) $(CXXFILES:.cpp=.o) $(ASFILES:.S=.o) $(NASMFILES:.asm=.o))) +override OBJ := $(patsubst %.o,%.$(TARGETL).o,$(patsubst $(SRC_DIR)/%,$(BUILD_DIR)/%,$(CFILES:.c=.o) $(CXXFILES:.cpp=.o) $(ASFILES:.S=.o) $(NASMFILES:.asm=.o))) override HEADER_DEPS := $(patsubst %.o,%.d,$(OBJ)) # Default target. @@ -168,33 +168,33 @@ override HEADER_DEPS := $(patsubst %.o,%.d,$(OBJ)) all: application # Link rules for the final driver executable. -$(TARGET): $(OBJ) - @echo "[LD]\tBuilding $(TARGET)" +$(TARGET_FILE): $(OBJ) + @echo "[LD]\tBuilding $(TARGET_FILE)" @$(LD) $(OBJ) $(LDFLAGS) -o $@ # Include header dependencies. -include $(HEADER_DEPS) # Compilation rules for *.c files. -$(BUILD_DIR)/%.$(ARCHITECTUREL).o: $(SRC_DIR)/%.c +$(BUILD_DIR)/%.$(TARGETL).o: $(SRC_DIR)/%.c @echo "[CC]\tCompiling $<" @mkdir -p $(dir $@) @$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ # Compilation rules for *.cpp files. -$(BUILD_DIR)/%.$(ARCHITECTUREL).o: $(SRC_DIR)/%.cpp +$(BUILD_DIR)/%.$(TARGETL).o: $(SRC_DIR)/%.cpp @echo "[CXX]\tCompiling $<" @mkdir -p $(dir $@) @$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@ # Compilation rules for *.S files. -$(BUILD_DIR)/%.$(ARCHITECTUREL).o: $(SRC_DIR)/%.S +$(BUILD_DIR)/%.$(TARGETL).o: $(SRC_DIR)/%.S @echo "[AS]\tCompiling $<" @mkdir -p $(dir $@) @$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ # Compilation rules for *.asm (nasm) files. -$(BUILD_DIR)/%.$(ARCHITECTUREL).o: $(SRC_DIR)/%.asm +$(BUILD_DIR)/%.$(TARGETL).o: $(SRC_DIR)/%.asm @echo "[AS]\tCompiling $<" @mkdir -p $(dir $@) @nasm $(NASMFLAGS) $< -o $@ @@ -203,10 +203,10 @@ $(BUILD_DIR)/%.$(ARCHITECTUREL).o: $(SRC_DIR)/%.asm .PHONY: clean clean: @echo "Cleaning" - @rm -rf $(TARGET) $(OBJ) $(HEADER_DEPS) + @rm -rf $(TARGET_FILE) $(OBJ) $(HEADER_DEPS) -application: $(TARGET) install_application +application: $(TARGET_FILE) install_application install_application: - @mkdir -p $(INSTALL_DIR)/$(ARCHITECTUREL) - @cp -f $(TARGET) $(TARGET_INSTALL) + @mkdir -p $(INSTALL_DIR)/$(TARGETL) + @cp -f $(TARGET_FILE) $(TARGET_INSTALL) diff --git a/user/libboron/source/reloc.c b/user/libboron/source/reloc.c index 3b22f85e..a9b02b7d 100644 --- a/user/libboron/source/reloc.c +++ b/user/libboron/source/reloc.c @@ -72,42 +72,46 @@ void RelocateSelf(PPEB Peb) { PELF_RELA Rela = (PELF_RELA) (ImageBase + RelaOffset + i); - #ifndef TARGET_AMD64 - #error TODO! - #else uint32_t RelType = (uint32_t) Rela->Info; + #if defined TARGET_AMD64 if (RelType != R_X86_64_RELATIVE) + #elif defined TARGET_I386 + if (RelType != R_386_RELATIVE) + #else + #error TODO! + #endif // Libboron.so, in addition to being restricted in so many // different ways already, also cannot import things from // other libraries. As such, the only relocation type you - // should see is R_X86_64_RELATIVE. + // should see is R_*_RELATIVE. __builtin_trap(); uintptr_t* Reloc = (uintptr_t*) (ImageBase + Rela->Offset); *Reloc = ImageBase + Rela->Addend; - #endif } for (size_t i = 0; i < RelSize; i += sizeof(ELF_REL)) { PELF_REL Rel = (PELF_REL) (ImageBase + RelOffset + i); - #ifndef TARGET_AMD64 - #error TODO! - #else uint32_t RelType = (uint32_t) Rel->Info; + #if defined TARGET_AMD64 if (RelType != R_X86_64_RELATIVE) + #elif defined TARGET_I386 + if (RelType != R_386_RELATIVE) + #else + #error TODO! + #endif // Libboron.so, in addition to being restricted in so many // different ways already, also cannot import things from // other libraries. As such, the only relocation type you - // should see is R_X86_64_RELATIVE. + // should see is R_*_RELATIVE. __builtin_trap(); uintptr_t* Reloc = (uintptr_t*) (ImageBase + Rel->Offset); *Reloc += ImageBase; - #endif } // Thanks to mlibc for this one. From ca17d4edd7309c1d3ea6246edce75ac53c58bd7f Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Wed, 15 Oct 2025 11:03:56 +0300 Subject: [PATCH 06/44] * I8042prt: Fix 32-bit specific bug --- drivers/i8042prt/source/kbd.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/i8042prt/source/kbd.c b/drivers/i8042prt/source/kbd.c index d73e6abc..04a76675 100644 --- a/drivers/i8042prt/source/kbd.c +++ b/drivers/i8042prt/source/kbd.c @@ -182,7 +182,7 @@ void KbdInitialize(int Vector, KIPL Ipl) BSTATUS KbdRead( PIO_STATUS_BLOCK Iosb, UNUSED PFCB Fcb, - UNUSED uintptr_t Offset, + UNUSED uint64_t Offset, PMDL Mdl, uint32_t Flags ) From 4464afd03cbf0df3cc5dc91332396b6a6f2918b3 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Wed, 15 Oct 2025 11:05:41 +0300 Subject: [PATCH 07/44] * Ext2fs: Fix compiler error --- drivers/ext2fs/source/inode.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/drivers/ext2fs/source/inode.c b/drivers/ext2fs/source/inode.c index 5e3faa29..e45c49bd 100644 --- a/drivers/ext2fs/source/inode.c +++ b/drivers/ext2fs/source/inode.c @@ -210,6 +210,7 @@ BSTATUS Ext2OpenInode(PEXT2_FILE_SYSTEM FileSystem, uint32_t InodeNumber, PFCB* if (InodeNumber >= FileSystem->SuperBlock.InodeCount) return STATUS_INVALID_PARAMETER; + PRBTREE_ENTRY Entry = NULL; PFCB Fcb = NULL; BSTATUS Status = STATUS_INVALID_PARAMETER; @@ -217,7 +218,7 @@ BSTATUS Ext2OpenInode(PEXT2_FILE_SYSTEM FileSystem, uint32_t InodeNumber, PFCB* AcquireInodeTreeMutex(FileSystem); Retry: - PRBTREE_ENTRY Entry = LookUpItemRbTree(&FileSystem->InodeTree, InodeNumber); + Entry = LookUpItemRbTree(&FileSystem->InodeTree, InodeNumber); if (Entry) { // Get the FCB and reference it. From bc3d63f658c98fa6b1bb430f37270c37668ec7a4 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Wed, 15 Oct 2025 11:07:05 +0300 Subject: [PATCH 08/44] * Test: Fix C2x warnings --- drivers/test/source/apctst.c | 2 +- drivers/test/source/balltst.c | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/test/source/apctst.c b/drivers/test/source/apctst.c index bffc3ea5..910a6912 100644 --- a/drivers/test/source/apctst.c +++ b/drivers/test/source/apctst.c @@ -28,7 +28,7 @@ static void ApcNormalRoutine(UNUSED void* Context, UNUSED void* SystemArgument1, KeSetEvent(&Event, 1); } -static NO_RETURN void ApcTestRoutine() +static NO_RETURN void ApcTestRoutine(UNUSED void* Parameter) { LogMsg("Hello from ApcTestRoutine! My thread ptr is %p", KeGetCurrentThread()); diff --git a/drivers/test/source/balltst.c b/drivers/test/source/balltst.c index 1043eab4..1d7bb68f 100644 --- a/drivers/test/source/balltst.c +++ b/drivers/test/source/balltst.c @@ -22,7 +22,7 @@ Module name: // Updates for each processor ID. int Updates[64]; -NO_RETURN void TestThread1() +NO_RETURN void TestThread1(UNUSED void* Parameter) { // Display a simple run timer for (int i = 0; ; i++) @@ -32,7 +32,7 @@ NO_RETURN void TestThread1() } } -NO_RETURN void TestThread2() +NO_RETURN void TestThread2(UNUSED void* Parameter) { for (int i = 0; i < 20; i++) { @@ -98,7 +98,7 @@ int RNGRange(int Min, int Max) return RNG() % (Max - Min) + Min; } -NO_RETURN void BallTest() +NO_RETURN void BallTest(UNUSED void* Parameter) { KDPC DrvDpc; KeInitializeDpc(&DrvDpc, BallTestDpc, KeGetCurrentThread()); From d2668eb94e3cc4928f44690c26687cb44270d40e Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Wed, 15 Oct 2025 14:01:02 +0300 Subject: [PATCH 09/44] * Move the Limine ISO step to a separate file. --- Makefile | 15 +++------------ boron/source/build_number | 2 +- drivers/CommonMakefile | 2 +- tools/build_iso_limine_amd64.mk | 13 +++++++++++++ 4 files changed, 18 insertions(+), 14 deletions(-) create mode 100644 tools/build_iso_limine_amd64.mk diff --git a/Makefile b/Makefile index a1695f45..e6b9b445 100644 --- a/Makefile +++ b/Makefile @@ -75,15 +75,9 @@ clean: image: limine $(IMAGE_TARGET) -$(IMAGE_TARGET): kernel drivers apps limine_config - @echo "Building iso..." - @rm -rf $(ISO_DIR) - @mkdir -p $(ISO_DIR) - @cp $(KERNEL_ELF) $(ISO_DIR)/$(KERNEL_NAME) - @cp -r $(BUILD_DIR)/*.exe $(BUILD_DIR)/*.sys $(BUILD_DIR)/*.so limine.cfg limine/limine-bios.sys limine/limine-bios-cd.bin $(ISO_DIR) - @xorriso -as mkisofs -b limine-bios-cd.bin -no-emul-boot -boot-load-size 4 -boot-info-table --protective-msdos-label $(ISO_DIR) -o $@ 2>/dev/null - @limine/limine-deploy $@ 2>/dev/null - @rm -rf $(ISO_DIR) +ifeq ($(TARGET),AMD64) +include tools/build_iso_limine_amd64.mk +endif run: image @echo "Running..." @@ -119,7 +113,4 @@ $(BUILD_DIR)/%.sys: FORCE @$(MAKE) -C $(patsubst $(BUILD_DIR)/%.sys,$(DRIVERS_DIR)/%,$@) @cp $(patsubst $(BUILD_DIR)/%.sys,$(DRIVERS_DIR)/%/build/out.$(TARGETL).sys,$@) $@ -limine_config: limine.cfg - @echo "[MK]\tlimine.cfg was updated" - FORCE: ; diff --git a/boron/source/build_number b/boron/source/build_number index bc768da7..5d1277e7 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -146 \ No newline at end of file +147 \ No newline at end of file diff --git a/drivers/CommonMakefile b/drivers/CommonMakefile index 41ab5b6b..ce66ab6e 100644 --- a/drivers/CommonMakefile +++ b/drivers/CommonMakefile @@ -126,7 +126,7 @@ all: driver # Link rules for the final driver executable. $(TARGET_DRIVER): $(OBJ) @echo "[LD]\tBuilding $(TARGET_DRIVER)" - $(BLD) $(OBJ) $(LDFLAGS) -o $@ + @$(BLD) $(OBJ) $(LDFLAGS) -o $@ # Include header dependencies. -include $(HEADER_DEPS) diff --git a/tools/build_iso_limine_amd64.mk b/tools/build_iso_limine_amd64.mk new file mode 100644 index 00000000..4f5c9b8a --- /dev/null +++ b/tools/build_iso_limine_amd64.mk @@ -0,0 +1,13 @@ + +$(IMAGE_TARGET): kernel drivers apps limine_config + @echo "[MK]\tBuilding iso..." + @rm -rf $(ISO_DIR) + @mkdir -p $(ISO_DIR) + @cp $(KERNEL_ELF) $(ISO_DIR)/$(KERNEL_NAME) + @cp -r $(BUILD_DIR)/*.exe $(BUILD_DIR)/*.sys $(BUILD_DIR)/*.so limine.cfg limine/limine-bios.sys limine/limine-bios-cd.bin $(ISO_DIR) + @xorriso -as mkisofs -b limine-bios-cd.bin -no-emul-boot -boot-load-size 4 -boot-info-table --protective-msdos-label $(ISO_DIR) -o $@ 2>/dev/null + @limine/limine-deploy $@ 2>/dev/null + @rm -rf $(ISO_DIR) + +limine_config: + @echo "[MK]\tlimine.cfg was updated" From 9345fc8e0d5c05e3fcb12e3efa4a01e631afd285 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Wed, 15 Oct 2025 14:21:18 +0300 Subject: [PATCH 10/44] * Use different limine configs for 32-bit and 64-bit. --- Makefile | 11 ++++++++++- boron/source/build_number | 2 +- limine.cfg => limine.amd64.cfg | 2 +- limine.i386.cfg | 19 +++++++++++++++++++ ...so_limine_amd64.mk => build_iso_limine.mk} | 7 ++++--- 5 files changed, 35 insertions(+), 6 deletions(-) rename limine.cfg => limine.amd64.cfg (93%) create mode 100644 limine.i386.cfg rename tools/{build_iso_limine_amd64.mk => build_iso_limine.mk} (67%) diff --git a/Makefile b/Makefile index e6b9b445..851bfea9 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,7 @@ TARGETL ?= $(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F KERNEL_NAME = kernel.elf SYSDLL_NAME = libboron.so +ifeq ($(TARGET),AMD64) DRIVERS_LIST = \ halx86 \ framebuf \ @@ -26,6 +27,12 @@ DRIVERS_LIST = \ stornvme \ ext2fs \ test +else ifeq ($(TARGET),I386) +DRIVERS_LIST = \ + i8042prt \ + ext2fs \ + test +endif # The build directory BUILD_DIR = build/$(TARGETL) @@ -76,7 +83,9 @@ clean: image: limine $(IMAGE_TARGET) ifeq ($(TARGET),AMD64) -include tools/build_iso_limine_amd64.mk +include tools/build_iso_limine.mk +else ifeq ($(TARGET),I386) +include tools/build_iso_limine.mk endif run: image diff --git a/boron/source/build_number b/boron/source/build_number index 5d1277e7..4b9bce48 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -147 \ No newline at end of file +148 \ No newline at end of file diff --git a/limine.cfg b/limine.amd64.cfg similarity index 93% rename from limine.cfg rename to limine.amd64.cfg index 19e302c4..b15996ff 100644 --- a/limine.cfg +++ b/limine.amd64.cfg @@ -1,7 +1,7 @@ TIMEOUT=3 VERBOSE=yes -:The Boron Operating System +:Boron (amd64) PROTOCOL=limine KERNEL_PATH=boot:///kernel.elf #CMDLINE=/noinit diff --git a/limine.i386.cfg b/limine.i386.cfg new file mode 100644 index 00000000..b1ffb7c4 --- /dev/null +++ b/limine.i386.cfg @@ -0,0 +1,19 @@ +TIMEOUT=3 +VERBOSE=yes + +:Boron (i386) + PROTOCOL=multiboot + KERNEL_PATH=boot:///kernel.elf + CMDLINE=/noinit + + #MODULE_PATH=boot:///hali386.sys + #MODULE_PATH=boot:///framebuf.sys + MODULE_PATH=boot:///i8042prt.sys + #MODULE_PATH=boot:///stornvme.sys + MODULE_PATH=boot:///ext2fs.sys + MODULE_PATH=boot:///test.sys + + #MODULE_PATH=boot:///libboron.so + #MODULE_PATH=boot:///libtest.so + #MODULE_PATH=boot:///init.exe + #MODULE_PATH=boot:///test.exe diff --git a/tools/build_iso_limine_amd64.mk b/tools/build_iso_limine.mk similarity index 67% rename from tools/build_iso_limine_amd64.mk rename to tools/build_iso_limine.mk index 4f5c9b8a..31969ad6 100644 --- a/tools/build_iso_limine_amd64.mk +++ b/tools/build_iso_limine.mk @@ -4,10 +4,11 @@ $(IMAGE_TARGET): kernel drivers apps limine_config @rm -rf $(ISO_DIR) @mkdir -p $(ISO_DIR) @cp $(KERNEL_ELF) $(ISO_DIR)/$(KERNEL_NAME) - @cp -r $(BUILD_DIR)/*.exe $(BUILD_DIR)/*.sys $(BUILD_DIR)/*.so limine.cfg limine/limine-bios.sys limine/limine-bios-cd.bin $(ISO_DIR) + @cp -r $(BUILD_DIR)/*.exe $(BUILD_DIR)/*.sys $(BUILD_DIR)/*.so limine/limine-bios.sys limine/limine-bios-cd.bin $(ISO_DIR) + @cp limine.$(TARGETL).cfg $(ISO_DIR)/limine.cfg @xorriso -as mkisofs -b limine-bios-cd.bin -no-emul-boot -boot-load-size 4 -boot-info-table --protective-msdos-label $(ISO_DIR) -o $@ 2>/dev/null @limine/limine-deploy $@ 2>/dev/null @rm -rf $(ISO_DIR) -limine_config: - @echo "[MK]\tlimine.cfg was updated" +limine_config: limine.$(TARGETL).cfg + @echo "[MK]\tlimine.$(TARGETL).cfg was updated" From 316f208b1c5a196fd2c83adadf70cfc44f28a7f5 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Wed, 15 Oct 2025 14:25:33 +0300 Subject: [PATCH 11/44] * Add i386 run and runw rules + move run scripts to tools/. --- Makefile | 10 ++---- boron/source/build_number | 2 +- run.sh | 3 -- run.bat => tools/run-amd64.bat | 0 tools/run-amd64.sh | 3 ++ tools/run-i386.bat | 49 ++++++++++++++++++++++++++ tools/run-i386.sh | 3 ++ run-unix.sh => tools/run-unix-amd64.sh | 0 tools/run-unix-i386.sh | 9 +++++ tools/run_rule_amd64.mk | 8 +++++ tools/run_rule_i386.mk | 8 +++++ 11 files changed, 83 insertions(+), 12 deletions(-) delete mode 100644 run.sh rename run.bat => tools/run-amd64.bat (100%) create mode 100644 tools/run-amd64.sh create mode 100644 tools/run-i386.bat create mode 100644 tools/run-i386.sh rename run-unix.sh => tools/run-unix-amd64.sh (100%) create mode 100644 tools/run-unix-i386.sh create mode 100644 tools/run_rule_amd64.mk create mode 100644 tools/run_rule_i386.mk diff --git a/Makefile b/Makefile index 851bfea9..93a493c5 100644 --- a/Makefile +++ b/Makefile @@ -84,18 +84,12 @@ image: limine $(IMAGE_TARGET) ifeq ($(TARGET),AMD64) include tools/build_iso_limine.mk +include tools/run_rule_amd64.mk else ifeq ($(TARGET),I386) include tools/build_iso_limine.mk +include tools/run_rule_i386.mk endif -run: image - @echo "Running..." - @./run-unix.sh - -runw: image - @echo "Invoking WSL to run the OS..." - @./run.sh - kernel: $(KERNEL_ELF) drivers: $(DRIVERS_TARGETS) diff --git a/boron/source/build_number b/boron/source/build_number index 4b9bce48..d7019ae2 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -148 \ No newline at end of file +149 \ No newline at end of file diff --git a/run.sh b/run.sh deleted file mode 100644 index 46755ad1..00000000 --- a/run.sh +++ /dev/null @@ -1,3 +0,0 @@ -# works only in wsl :) - -cmd.exe /k "run.bat && exit" diff --git a/run.bat b/tools/run-amd64.bat similarity index 100% rename from run.bat rename to tools/run-amd64.bat diff --git a/tools/run-amd64.sh b/tools/run-amd64.sh new file mode 100644 index 00000000..31ee3dfd --- /dev/null +++ b/tools/run-amd64.sh @@ -0,0 +1,3 @@ +# works only in wsl :) + +cmd.exe /k "tools\run-amd64.bat && exit" diff --git a/tools/run-i386.bat b/tools/run-i386.bat new file mode 100644 index 00000000..180b54b8 --- /dev/null +++ b/tools/run-i386.bat @@ -0,0 +1,49 @@ +@rem Run script + +@echo off + +set backupPath=%path% +set NSPath=%CD% +cd /d c:\Program Files\qemu +set path=%path%;%NSPath% + +if exist %nspath%\vdiske2.vdi ( + set DriveOptions=-cdrom %nspath%\build\image.i386.iso -drive id=nvm,file=%nspath%\vdiske2.vdi,if=none -device nvme,serial=deadbeef,drive=nvm +) else ( + set DriveOptions=-cdrom %nspath%\build\image.i386.iso +) + +qemu-system-i386.exe -no-reboot -no-shutdown -d int -M smm=off ^ +-M q35 ^ +-m 256M ^ +-smp 6 ^ +-boot d ^ +-display sdl ^ +-accel tcg ^ +-monitor telnet:127.0.0.1:56789,server,nowait ^ +-debugcon stdio ^ +-trace *nvme* -trace *msi* -D %nspath%\keep\nvmelog.txt ^ +-s ^ +%DriveOptions% + +:-debugcon stdio ^ +:-d cpu_reset ^ +: -s -S -- for debugging with GDB +: -serial COM7 -- to output the serial port to somewhere real +: -kernel %nspath%/kernel.bin +: -debugcon stdio +: -monitor telnet:127.0.0.1:55555,server,nowait -- to use the QEMU console +: +:-d int ^ +:-D %nspath%\keep/things.txt ^ +:qemu-system-i386 -m 16M -drive file=\\.\PHYSICALDRIVE1,format=raw +rem -s -S + +:-drive id=disk,file=%nspath%\vdisk.vdi,if=none ^ +:-device ahci,id=ahci ^ +:-device ide-hd,drive=disk,bus=ahci.0 ^ + +rem go back +cd /d %NSPath% + +set path=%backupPath% diff --git a/tools/run-i386.sh b/tools/run-i386.sh new file mode 100644 index 00000000..3a3bfb4a --- /dev/null +++ b/tools/run-i386.sh @@ -0,0 +1,3 @@ +# works only in wsl :) + +cmd.exe /k "tools\run-i386.bat && exit" diff --git a/run-unix.sh b/tools/run-unix-amd64.sh similarity index 100% rename from run-unix.sh rename to tools/run-unix-amd64.sh diff --git a/tools/run-unix-i386.sh b/tools/run-unix-i386.sh new file mode 100644 index 00000000..30a0ceb8 --- /dev/null +++ b/tools/run-unix-i386.sh @@ -0,0 +1,9 @@ +qemu-system-i386 \ + -no-reboot \ + -no-shutdown \ + -M q35 \ + -m 256M \ + -smp 4 \ + -boot d \ + -cdrom build/image.i386.iso \ + -debugcon stdio diff --git a/tools/run_rule_amd64.mk b/tools/run_rule_amd64.mk new file mode 100644 index 00000000..9bdec72b --- /dev/null +++ b/tools/run_rule_amd64.mk @@ -0,0 +1,8 @@ + +run: image + @echo "Running..." + @./tools/run-unix-amd64.sh + +runw: image + @echo "Invoking WSL to run the OS..." + @./tools/run-amd64.sh diff --git a/tools/run_rule_i386.mk b/tools/run_rule_i386.mk new file mode 100644 index 00000000..fc0dd397 --- /dev/null +++ b/tools/run_rule_i386.mk @@ -0,0 +1,8 @@ + +run: image + @echo "Running..." + @./tools/run-unix-i386.sh + +runw: image + @echo "Invoking WSL to run the OS..." + @./tools/run-i386.sh From b366a9af0d6e21be0c36eea9909c871f61a2ba42 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Wed, 15 Oct 2025 14:31:29 +0300 Subject: [PATCH 12/44] * Ke: Fix a bug in the foreinit code where the bootstrap page directory isn't aligned to a page boundary --- boron/source/build_number | 2 +- boron/source/ke/i386/debug.c | 2 +- boron/source/ke/i386/foreinit.asm | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/boron/source/build_number b/boron/source/build_number index d7019ae2..c663e4d0 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -149 \ No newline at end of file +151 \ No newline at end of file diff --git a/boron/source/ke/i386/debug.c b/boron/source/ke/i386/debug.c index 6035d24d..de7ba9ce 100644 --- a/boron/source/ke/i386/debug.c +++ b/boron/source/ke/i386/debug.c @@ -122,7 +122,7 @@ void DbgPrintStackTrace(uintptr_t Ebp) int Depth = 30; char Buffer[128]; - DbgPrintDouble("\tAddress \tName\n"); + DbgPrintDouble("\tAddress \tName\n"); #ifndef DISABLE_USER_MODE_PREVENTION if (Rbp <= MM_USER_SPACE_END) diff --git a/boron/source/ke/i386/foreinit.asm b/boron/source/ke/i386/foreinit.asm index 11e91a70..87476064 100644 --- a/boron/source/ke/i386/foreinit.asm +++ b/boron/source/ke/i386/foreinit.asm @@ -122,6 +122,8 @@ section .bss KiMultibootSignature: resd 1 KiMultibootPointer: resd 1 + +alignb 4096 KiInitialStack: resb 4096 KiBootstrapPageDirectory: resb 4096 KiBootstrapPageTables: resb 8192 From 65132362667cc672b405e38eafd816f632eb8642 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Wed, 15 Oct 2025 22:31:53 +0300 Subject: [PATCH 13/44] * Mm,Ke: Improve physical memory mapping on i386 * Fix some bugs. --- boron/address_space_i386.txt | 13 +- boron/include/arch/i386.h | 17 +- boron/source/build_number | 2 +- boron/source/ke/amd64/boot.c | 10 +- boron/source/ke/i386/boot.c | 95 ++++++++++++ boron/source/ke/i386/foreinit.asm | 13 +- boron/source/ke/i386/mboot.h | 250 ++++++++++++++++++++++++++++++ boron/source/mm/amd64/pt.c | 4 +- boron/source/mm/i386/idmap.c | 37 +++++ boron/source/mm/i386/pt.c | 12 +- boron/source/mm/mi.h | 11 ++ boron/source/mm/pmm.c | 10 +- 12 files changed, 440 insertions(+), 34 deletions(-) create mode 100644 boron/source/ke/i386/boot.c create mode 100644 boron/source/ke/i386/mboot.h create mode 100644 boron/source/mm/i386/idmap.c diff --git a/boron/address_space_i386.txt b/boron/address_space_i386.txt index e8662769..12c6819d 100644 --- a/boron/address_space_i386.txt +++ b/boron/address_space_i386.txt @@ -8,16 +8,15 @@ BORON Operating System Address Space (i386) | unused | +------------------------------+ - 0xF0000000 | more dynamic pool space | -+------------------------------+ - 0xD0000000 ++------------------------------+ - 0xE0000000 | page frame data base | -+------------------------------+ - 0xC2000000 ++------------------------------+ - 0xD4000000 | system module DLLs | -+------------------------------+ - 0xC1000000 -| fast mapping in 8MB windows | -+------------------------------+ - 0xC0800000 ++------------------------------+ - 0xD1000000 +| fast mapping in 16MB windows | ++------------------------------+ - 0xD0000000 +| map of first 256 MB of phys | | kernel code & data | -+------------------------------+ - 0xC0001000 -| fast page mapping | +------------------------------+ - 0xC0000000 | dynamic pool space | +------------------------------+ - 0x80000000 diff --git a/boron/include/arch/i386.h b/boron/include/arch/i386.h index d132d73d..c6828392 100644 --- a/boron/include/arch/i386.h +++ b/boron/include/arch/i386.h @@ -45,12 +45,17 @@ void KePortWriteDword(uint16_t portNo, uint32_t data); #define MI_PML1_LOC_END ((uint64_t)0x100000000U) #define MI_PML_ADDRMASK ((uintptr_t)0xFFFFF000U) -// MmGetHHDMOffsetAddr and other HHDM-related calls aren't implemented -// using an HHDM on 32-bit. Instead, they're implemented via 8MB windows -// that get spawned in everytime MmGetHHDMOffsetAddr is called with a -// different 8MB region. -#define MI_FASTMAP_START ((uintptr_t)0xC0800000U) -#define MI_FASTMAP_MASK ((uintptr_t)0xFF800000U) +// MmGetHHDMOffsetAddr and other HHDM-related calls are implemented two-fold: +// +// - The first 256 MB of RAM are mapped in an offset identity mapping +// +// - The rest of the address space is accessible via a 16 MB window fast mapping. +#define MI_IDENTMAP_START ((uintptr_t) 0xC0000000) +#define MI_IDENTMAP_SIZE ((uintptr_t) 0x10000000) + +#define MI_FASTMAP_START ((uintptr_t) 0xD0000000) +#define MI_FASTMAP_MASK ((uintptr_t) 0xFF000000) +#define MI_FASTMAP_SIZE (16 * 1024 * 1024) typedef union { diff --git a/boron/source/build_number b/boron/source/build_number index c663e4d0..b34c321e 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -151 \ No newline at end of file +171 diff --git a/boron/source/ke/amd64/boot.c b/boron/source/ke/amd64/boot.c index 13bdcc64..21d1d104 100644 --- a/boron/source/ke/amd64/boot.c +++ b/boron/source/ke/amd64/boot.c @@ -154,7 +154,7 @@ static void KiConvertLimineFramebufferToLoaderFramebuffer( // Allocate a contiguous area of memory from the memory map. INIT -static void* KiLimineAllocateMemoryFromMemMap(size_t Size) +static void* KiEarlyAllocateMemoryFromMemMap(size_t Size) { Size = (Size + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1); @@ -179,7 +179,7 @@ static void* KiLimineAllocateMemoryFromMemMap(size_t Size) return (void*) MmGetHHDMOffsetAddr(CurrAddr); } - KeCrashBeforeSMPInit("Error, out of memory in KiLimineAllocateMemoryFromMemMap"); + KeCrashBeforeSMPInit("Error, out of memory in KiEarlyAllocateMemoryFromMemMap"); } INIT @@ -237,7 +237,7 @@ void KiInitLoaderParameterBlock() // Initialize the other modules. Lpb->ModuleInfo.Count = KeLimineModuleRequest.response->module_count; - Lpb->ModuleInfo.List = KiLimineAllocateMemoryFromMemMap(Lpb->ModuleInfo.Count * sizeof(LOADER_MODULE)); + Lpb->ModuleInfo.List = KiEarlyAllocateMemoryFromMemMap(Lpb->ModuleInfo.Count * sizeof(LOADER_MODULE)); for (size_t i = 0; i < Lpb->ModuleInfo.Count; i++) KiConvertLimineFileToLoaderModule(&Lpb->ModuleInfo.List[i], KeLimineModuleRequest.response->modules[i]); @@ -245,14 +245,14 @@ void KiInitLoaderParameterBlock() // Initialize the CPUs. Lpb->Multiprocessor.BootstrapHardwareId = KeLimineSmpRequest.response->bsp_lapic_id; Lpb->Multiprocessor.Count = KeLimineSmpRequest.response->cpu_count; - Lpb->Multiprocessor.List = KiLimineAllocateMemoryFromMemMap(Lpb->Multiprocessor.Count * sizeof(LOADER_AP)); + Lpb->Multiprocessor.List = KiEarlyAllocateMemoryFromMemMap(Lpb->Multiprocessor.Count * sizeof(LOADER_AP)); for (size_t i = 0; i < Lpb->Multiprocessor.Count; i++) KiConvertLimineApToLoaderAp(&Lpb->Multiprocessor.List[i], KeLimineSmpRequest.response->cpus[i]); // Initialize the frame buffers. Lpb->FramebufferCount = KeLimineFramebufferRequest.response->framebuffer_count; - Lpb->Framebuffers = KiLimineAllocateMemoryFromMemMap(Lpb->FramebufferCount * sizeof(LOADER_FRAMEBUFFER)); + Lpb->Framebuffers = KiEarlyAllocateMemoryFromMemMap(Lpb->FramebufferCount * sizeof(LOADER_FRAMEBUFFER)); for (size_t i = 0; i < Lpb->FramebufferCount; i++) KiConvertLimineFramebufferToLoaderFramebuffer(&Lpb->Framebuffers[i], KeLimineFramebufferRequest.response->framebuffers[i]); diff --git a/boron/source/ke/i386/boot.c b/boron/source/ke/i386/boot.c new file mode 100644 index 00000000..9b5d99c8 --- /dev/null +++ b/boron/source/ke/i386/boot.c @@ -0,0 +1,95 @@ +/*** + The Boron Operating System + Copyright (C) 2025 iProgramInCpp + +Module name: + ke/i386/boot.c + +Abstract: + This module contains the bootstrap code that converts + Multiboot bootloader data into kernel specific definitions. + +Author: + iProgramInCpp - 15 October 2025 +***/ +#include "../ki.h" +#include "../../mm/mi.h" +#include "mboot.h" + +extern uint32_t KiMultibootSignature; +extern multiboot_info_t* KiMultibootPointer; + +#define P2V(address) ((void*)(MI_IDENTMAP_START + address)) + +LOADER_PARAMETER_BLOCK KeLoaderParameterBlock; + +INIT void KeMarkCrashedAp(UNUSED uint32_t ProcessorIndex) {} +INIT void KeJumpstartAp(UNUSED uint32_t ProcessorIndex) {} + +#define MAX_MEMORY_REGIONS 256 +static LOADER_MEMORY_REGION KiMemoryRegions[MAX_MEMORY_REGIONS]; + +// Allocate a contiguous area of memory from the memory map. +INIT +static void* KiEarlyAllocateMemoryFromMemMap(size_t Size) +{ + Size = (Size + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1); + + PLOADER_PARAMETER_BLOCK Lpb = &KeLoaderParameterBlock; + for (uint64_t i = 0; i < Lpb->MemoryRegionCount; i++) + { + // if the entry isn't usable, skip it + PLOADER_MEMORY_REGION Entry = &Lpb->MemoryRegions[i]; + + if (Entry->Type != LOADER_MEM_FREE) + continue; + + // Note: Usable entries are guaranteed to be aligned to page size, and + // not overlap any other entries. + if (Entry->Size < Size) + continue; + + uintptr_t CurrAddr = Entry->Base; + Entry->Base += Size; + Entry->Size -= Size; + + return (void*) MmGetHHDMOffsetAddr(CurrAddr); + } + + KeCrashBeforeSMPInit("Error, out of memory in KiEarlyAllocateMemoryFromMemMap"); +} + +INIT +static void KiRemoveAreaFromMemMap(uintptr_t StartAddress, size_t Size) +{ + // TODO + (void) StartAddress; + (void) Size; +} + + +INIT +void KiInitLoaderParameterBlock() +{ + // Initialize the base identity mapping. + MiInitializeBaseIdentityMapping(); + + PLOADER_PARAMETER_BLOCK Lpb = &KeLoaderParameterBlock; + + if (KiMultibootSignature != MULTIBOOT_BOOTLOADER_MAGIC) + KeCrashBeforeSMPInit("KiMultibootSignature is not %08x, it's %08x!", MULTIBOOT_BOOTLOADER_MAGIC, KiMultibootSignature); + + // Initialize the memory regions. + + // Initialize the kernel module. + + // Initialize the other modules. + + // Initialize the CPUs. + + // Initialize the frame buffers. + + // Initialize the bootloader's information as well as the command line. + + +} diff --git a/boron/source/ke/i386/foreinit.asm b/boron/source/ke/i386/foreinit.asm index 87476064..85113e93 100644 --- a/boron/source/ke/i386/foreinit.asm +++ b/boron/source/ke/i386/foreinit.asm @@ -28,7 +28,7 @@ section .ipldata ; Header dd 0x1BADB002 ; Signature - dd 7 ; Flags: + dd 7 ; Flags: MULTIBOOT_PAGE_ALIGN | MULTIBOOT_MEMORY_INFO | MULTIBOOT_VIDEO_MODE dd - (0x1BADB002 + 7) ; Check Sum ; A.out Kludge - blank because we're an ELF @@ -72,11 +72,13 @@ KiBeforeSystemStartup: add edi, 4 loop .LoopFill - ; map the two page tables to both 0x00000000 and 0xC0000000 + ; map the two page tables to both 0x00000000 and 0xC0000000, as well + ; as the page directory itself into the 1023rd entry mov dword [V2P(KiBootstrapPageDirectory) + 0 * 4], V2P(KiBootstrapPageTables + 0) + 0x03 mov dword [V2P(KiBootstrapPageDirectory) + 1 * 4], V2P(KiBootstrapPageTables + 4096) + 0x03 mov dword [V2P(KiBootstrapPageDirectory) + 768 * 4], V2P(KiBootstrapPageTables + 0) + 0x03 mov dword [V2P(KiBootstrapPageDirectory) + 769 * 4], V2P(KiBootstrapPageTables + 4096) + 0x03 + mov dword [V2P(KiBootstrapPageDirectory) +1023 * 4], V2P(KiBootstrapPageDirectory) + 0x03 ; set CR3 to the physical address of the page directory mov ecx, V2P(KiBootstrapPageDirectory) @@ -120,10 +122,15 @@ KiBeforeSystemStartupHigherHalf: section .bss +global KiMultibootSignature +global KiMultibootPointer +global KiBootstrapPageDirectory +global KiBootstrapPageTables + KiMultibootSignature: resd 1 KiMultibootPointer: resd 1 alignb 4096 KiInitialStack: resb 4096 KiBootstrapPageDirectory: resb 4096 -KiBootstrapPageTables: resb 8192 +KiBootstrapPageTables: resb 64 * 4096 diff --git a/boron/source/ke/i386/mboot.h b/boron/source/ke/i386/mboot.h new file mode 100644 index 00000000..e9a35cc0 --- /dev/null +++ b/boron/source/ke/i386/mboot.h @@ -0,0 +1,250 @@ +/* Copyright (C) 1999,2003,2007,2008,2009 Free Software Foundation, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL ANY + * DEVELOPER OR DISTRIBUTOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR + * IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + + +#ifndef MULTIBOOT_HEADER +#define MULTIBOOT_HEADER 1 + +#define MbCheckFlag(flags,bit) ((flags) & (1 << (bit))) + +/* How many bytes from the start of the file we search for the header. */ +#define MULTIBOOT_SEARCH 8192 + +/* The magic field should contain this. */ +#define MULTIBOOT_HEADER_MAGIC 0x1BADB002 + +/* This should be in %eax. */ +#define MULTIBOOT_BOOTLOADER_MAGIC 0x2BADB002 + +/* The bits in the required part of flags field we don't support. */ +#define MULTIBOOT_UNSUPPORTED 0x0000fffc + +/* Alignment of multiboot modules. */ +#define MULTIBOOT_MOD_ALIGN 0x00001000 + +/* Alignment of the multiboot info structure. */ +#define MULTIBOOT_INFO_ALIGN 0x00000004 + +/* Flags set in the 'flags' member of the multiboot header. */ + +/* Align all boot modules on i386 page (4KB) boundaries. */ +#define MULTIBOOT_PAGE_ALIGN 0x00000001 + +/* Must pass memory information to OS. */ +#define MULTIBOOT_MEMORY_INFO 0x00000002 + +/* Must pass video information to OS. */ +#define MULTIBOOT_VIDEO_MODE 0x00000004 + +/* This flag indicates the use of the address fields in the header. */ +#define MULTIBOOT_AOUT_KLUDGE 0x00010000 + +/* Flags to be set in the 'flags' member of the multiboot info structure. */ + +/* is there basic lower/upper memory information? */ +#define MULTIBOOT_INFO_MEMORY 0x00000001 +/* is there a boot device set? */ +#define MULTIBOOT_INFO_BOOTDEV 0x00000002 +/* is the command-line defined? */ +#define MULTIBOOT_INFO_CMDLINE 0x00000004 +/* are there modules to do something with? */ +#define MULTIBOOT_INFO_MODS 0x00000008 + +/* These next two are mutually exclusive */ + +/* is there a symbol table loaded? */ +#define MULTIBOOT_INFO_AOUT_SYMS 0x00000010 +/* is there an ELF section header table? */ +#define MULTIBOOT_INFO_ELF_SHDR 0X00000020 + +/* is there a full memory map? */ +#define MULTIBOOT_INFO_MEM_MAP 0x00000040 + +/* Is there drive info? */ +#define MULTIBOOT_INFO_DRIVE_INFO 0x00000080 + +/* Is there a config table? */ +#define MULTIBOOT_INFO_CONFIG_TABLE 0x00000100 + +/* Is there a boot loader name? */ +#define MULTIBOOT_INFO_BOOT_LOADER_NAME 0x00000200 + +/* Is there an APM table? */ +#define MULTIBOOT_INFO_APM_TABLE 0x00000400 + +/* Is there video information? */ +#define MULTIBOOT_INFO_VIDEO_INFO 0x00000800 + +//small jerry-rig +#define MULTIBOOT_INFO_FRAMEBUFFER_INFO 0x00001000 + +#ifndef ASM_FILE + +typedef unsigned char MultibootUInt8; +typedef unsigned short MultibootUInt16; +typedef unsigned int MultibootUInt32; +typedef unsigned long long MultibootUInt64; + +struct multiboot_header +{ + /* Must be MULTIBOOT_MAGIC - see above. */ + MultibootUInt32 magic; + + /* Feature flags. */ + MultibootUInt32 flags; + + /* The above fields plus this one must equal 0 mod 2^32. */ + MultibootUInt32 checksum; + + /* These are only valid if MULTIBOOT_AOUT_KLUDGE is set. */ + MultibootUInt32 header_addr; + MultibootUInt32 load_addr; + MultibootUInt32 load_end_addr; + MultibootUInt32 bss_end_addr; + MultibootUInt32 entry_addr; + + /* These are only valid if MULTIBOOT_VIDEO_MODE is set. */ + MultibootUInt32 mode_type; + MultibootUInt32 width; + MultibootUInt32 height; + MultibootUInt32 depth; +}; + +/* The symbol table for a.out. */ +struct multiboot_aout_symbol_table +{ + MultibootUInt32 tabsize; + MultibootUInt32 strsize; + MultibootUInt32 addr; + MultibootUInt32 reserved; +}; +typedef struct multiboot_aout_symbol_table multiboot_aout_symbol_table_t; + +/* The section header table for ELF. */ +struct multiboot_elf_section_header_table +{ + MultibootUInt32 num; + MultibootUInt32 size; + MultibootUInt32 addr; + MultibootUInt32 shndx; +}; +typedef struct multiboot_elf_section_header_table multiboot_elf_section_header_table_t; + +struct multiboot_info +{ + /* Multiboot info version number */ + MultibootUInt32 flags; + + /* Available memory from BIOS */ + MultibootUInt32 mem_lower; + MultibootUInt32 mem_upper; + + /* "root" partition */ + MultibootUInt32 boot_device; + + /* Kernel command line */ + MultibootUInt32 cmdline; + + /* Boot-Module list */ + MultibootUInt32 mods_count; + MultibootUInt32 mods_addr; + + union + { + multiboot_aout_symbol_table_t aout_sym; + multiboot_elf_section_header_table_t elf_sec; + } u; + + /* Memory Mapping buffer */ + MultibootUInt32 mmap_length; + MultibootUInt32 mmap_addr; + + /* Drive Info buffer */ + MultibootUInt32 drives_length; + MultibootUInt32 drives_addr; + + /* ROM configuration table */ + MultibootUInt32 config_table; + + /* Boot Loader Name */ + MultibootUInt32 boot_loader_name; + + /* APM table */ + MultibootUInt32 apm_table; + + /* Video */ + MultibootUInt32 vbe_control_info; + MultibootUInt32 vbe_mode_info; + MultibootUInt16 vbe_mode; + MultibootUInt16 vbe_interface_seg; + MultibootUInt16 vbe_interface_off; + MultibootUInt16 vbe_interface_len; + + MultibootUInt64 framebuffer_addr; + MultibootUInt32 framebuffer_pitch; + MultibootUInt32 framebuffer_width; + MultibootUInt32 framebuffer_height; + MultibootUInt8 framebuffer_bpp; + MultibootUInt8 framebuffer_type; + union { + struct { + MultibootUInt32 framebuffer_palette_addr; + MultibootUInt16 framebuffer_palette_num_colors; + }; + struct { + MultibootUInt8 framebuffer_red_field_position ; + MultibootUInt8 framebuffer_red_mask_size ; + MultibootUInt8 framebuffer_green_field_position ; + MultibootUInt8 framebuffer_green_mask_size ; + MultibootUInt8 framebuffer_blue_field_position ; + MultibootUInt8 framebuffer_blue_mask_size ; + }; + } u2; +}; +typedef struct multiboot_info multiboot_info_t; + +struct multiboot_mmap_entry +{ + MultibootUInt32 size; + MultibootUInt64 addr; + MultibootUInt64 len; +#define MULTIBOOT_MEMORY_AVAILABLE 1 +#define MULTIBOOT_MEMORY_RESERVED 2 + MultibootUInt32 type; +} __attribute__((packed)); +typedef struct multiboot_mmap_entry multiboot_memory_map_t; + +struct multiboot_mod_list +{ + /* the memory used goes from bytes 'mod_start' to 'mod_end-1' inclusive */ + MultibootUInt32 mod_start; + MultibootUInt32 mod_end; + + /* Module command line */ + MultibootUInt32 cmdline; + + /* padding to take it to 16 bytes (must be zero) */ + MultibootUInt32 pad; +}; +typedef struct multiboot_mod_list multiboot_module_t; + +#endif /* ! ASM_FILE */ + +#endif /* ! MULTIBOOT_HEADER */ \ No newline at end of file diff --git a/boron/source/mm/amd64/pt.c b/boron/source/mm/amd64/pt.c index ba5433ce..31703ea7 100644 --- a/boron/source/mm/amd64/pt.c +++ b/boron/source/mm/amd64/pt.c @@ -15,11 +15,9 @@ Module name: #include #include -#include #include #include - -#define MI_PTE_LOC(Address) (MI_PML1_LOCATION + ((Address & MI_PML_ADDRMASK) >> 12) * sizeof(MMPTE)) +#include "../mi.h" PMMPTE MmGetPteLocation(uintptr_t Address) { diff --git a/boron/source/mm/i386/idmap.c b/boron/source/mm/i386/idmap.c new file mode 100644 index 00000000..6fa50081 --- /dev/null +++ b/boron/source/mm/i386/idmap.c @@ -0,0 +1,37 @@ +/*** + The Boron Operating System + Copyright (C) 2025 iProgramInCpp + +Module name: + mm/i386/idmap.c + +Abstract: + This module implements identity mapping management for + the i386 platform. + +Author: + iProgramInCpp - 15 October 2025 +***/ +#include "../mi.h" + +#define P2V(n) ((void*)(MI_IDENTMAP_START + (n)) +#define V2P(p) ((uintptr_t)(p) - MI_IDENTMAP_START) + +extern uint8_t KiBootstrapPageTables[]; + +void MiInitializeBaseIdentityMapping() +{ + for (size_t i = 0; i < MI_IDENTMAP_SIZE; i += PAGE_SIZE) + { + uintptr_t Address = MI_IDENTMAP_START + i; + + PMMPTE Level2 = (PMMPTE) MI_PTE_LOC(MI_PTE_LOC(Address)); + PMMPTE Level1 = (PMMPTE) MI_PTE_LOC(Address); + + *Level2 = V2P(&KiBootstrapPageTables[(Address >> 22) * PAGE_SIZE]) + | MM_PTE_READWRITE + | MM_PTE_PRESENT; + + *Level1 = i | MM_PTE_READWRITE | MM_PTE_PRESENT; + } +} diff --git a/boron/source/mm/i386/pt.c b/boron/source/mm/i386/pt.c index c6812f64..19551613 100644 --- a/boron/source/mm/i386/pt.c +++ b/boron/source/mm/i386/pt.c @@ -1,25 +1,23 @@ /*** The Boron Operating System - Copyright (C) 2023 iProgramInCpp + Copyright (C) 2025 iProgramInCpp Module name: - mm/amd64/pt.c + mm/i386/pt.c Abstract: This module implements page table management for - the AMD64 platform. + the i386 platform. Author: - iProgramInCpp - 8 September 2023 + iProgramInCpp - 15 October 2025 ***/ #include #include -#include #include #include - -#define MI_PTE_LOC(Address) (MI_PML1_LOCATION + ((Address & MI_PML_ADDRMASK) >> 12) * sizeof(MMPTE)) +#include "../mi.h" PMMPTE MmGetPteLocation(uintptr_t Address) { diff --git a/boron/source/mm/mi.h b/boron/source/mm/mi.h index bfe7dbee..b39b81c2 100644 --- a/boron/source/mm/mi.h +++ b/boron/source/mm/mi.h @@ -336,4 +336,15 @@ void MiReleaseVad(PMMVAD Vad); // uncommitted and certain code paths are skipped. void MiDecommitVad(PMMVAD_LIST VadList, PMMVAD Vad, uintptr_t StartVa, size_t SizePages); +// ===== Memory Initialization ===== +#ifdef IS_32_BIT +void MiInitializeBaseIdentityMapping(); +#endif + +// ===== Hardware Specific ===== + +#if defined TARGET_I386 || defined TARGET_AMD64 +#define MI_PTE_LOC(Address) (MI_PML1_LOCATION + (((Address) & MI_PML_ADDRMASK) >> 12) * sizeof(MMPTE)) +#endif + #endif//NS64_MI_H diff --git a/boron/source/mm/pmm.c b/boron/source/mm/pmm.c index ba7cc1f4..176aa939 100644 --- a/boron/source/mm/pmm.c +++ b/boron/source/mm/pmm.c @@ -71,25 +71,29 @@ uintptr_t MmHHDMWindowBase; static void MiUpdateHHDMWindowBase(uintptr_t PhysAddr) { + const int PtesPerLevel = PAGE_SIZE / sizeof(MMPTE); PMMPTE Ptes = (PMMPTE)(MI_PML1_LOCATION); PhysAddr &= MI_FASTMAP_MASK; MmHHDMWindowBase = PhysAddr; - for (size_t i = 0; i < 8 * 1024 * 1024; i += 4096) + for (size_t i = 0; i < MI_FASTMAP_SIZE; i += PAGE_SIZE) { uintptr_t Address = MI_FASTMAP_START + i; MMADDRESS_CONVERT Convert; Convert.Long = Address; - Ptes[Convert.Level2Index * 1024 + Convert.Level1Index] = MM_PTE_PRESENT | MM_PTE_READWRITE | MM_PTE_NOEXEC | (PhysAddr + i); + Ptes[Convert.Level2Index * PtesPerLevel + Convert.Level1Index] = MM_PTE_PRESENT | MM_PTE_READWRITE | MM_PTE_NOEXEC | (PhysAddr + i); KeInvalidatePage((void*)Address); } } void* MmGetHHDMOffsetAddr(uintptr_t PhysAddr) { + if (PhysAddr < MI_IDENTMAP_SIZE) + return (void*)(MI_IDENTMAP_START + PhysAddr); + if ((PhysAddr & MI_FASTMAP_MASK) != MmHHDMWindowBase) MiUpdateHHDMWindowBase(PhysAddr); @@ -99,6 +103,8 @@ void* MmGetHHDMOffsetAddr(uintptr_t PhysAddr) uintptr_t MmGetHHDMOffsetFromAddr(void* Addr) { uintptr_t AddrInt = (uintptr_t) Addr; + if (AddrInt >= MI_IDENTMAP_START && AddrInt < MI_IDENTMAP_START + MI_IDENTMAP_SIZE) + return AddrInt - MI_IDENTMAP_START; if ((AddrInt & MI_FASTMAP_MASK) != MmHHDMWindowBase) { From 7c32f56996ca5bc4f3b0824f1e4ae8dfe480a285 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Thu, 16 Oct 2025 01:03:49 +0300 Subject: [PATCH 14/44] * Ke,Mm: Add support for Multiboot memory maps for i386. --- boron/linker.i386.ld | 2 + boron/source/build_number | 2 +- boron/source/ke/i386/boot.c | 214 +++++++++++++++++++++++++++++- boron/source/ke/i386/foreinit.asm | 8 +- tools/run-i386.bat | 1 - tools/run-unix-i386.sh | 1 - 6 files changed, 215 insertions(+), 13 deletions(-) diff --git a/boron/linker.i386.ld b/boron/linker.i386.ld index d95bfabf..66e6ad17 100644 --- a/boron/linker.i386.ld +++ b/boron/linker.i386.ld @@ -94,6 +94,8 @@ SECTIONS PROVIDE(PsSystemProcess = PspSystemProcessObject + 64); } :data + KiKernelEnd = . - 0xC0000000; + /* Discard .note.* and .eh_frame since they may cause issues on some hosts. */ /DISCARD/ : { *(.eh_frame) diff --git a/boron/source/build_number b/boron/source/build_number index b34c321e..aa34eab5 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -171 +199 diff --git a/boron/source/ke/i386/boot.c b/boron/source/ke/i386/boot.c index 9b5d99c8..a534627f 100644 --- a/boron/source/ke/i386/boot.c +++ b/boron/source/ke/i386/boot.c @@ -17,9 +17,9 @@ Module name: #include "mboot.h" extern uint32_t KiMultibootSignature; -extern multiboot_info_t* KiMultibootPointer; +extern multiboot_info_t* KiMultibootInfo; -#define P2V(address) ((void*)(MI_IDENTMAP_START + address)) +#define P2V(address) ((void*)(MI_IDENTMAP_START + (uintptr_t)(address))) LOADER_PARAMETER_BLOCK KeLoaderParameterBlock; @@ -62,24 +62,226 @@ static void* KiEarlyAllocateMemoryFromMemMap(size_t Size) INIT static void KiRemoveAreaFromMemMap(uintptr_t StartAddress, size_t Size) { - // TODO - (void) StartAddress; - (void) Size; + DbgPrint("KiRemoveAreaFromMemMap(0x%p, 0x%x)", StartAddress, Size); + PLOADER_PARAMETER_BLOCK Lpb = &KeLoaderParameterBlock; + + // Ensure the start address and size greedily cover page boundaries. + Size += StartAddress & (PAGE_SIZE - 1); + StartAddress &= ~(PAGE_SIZE - 1); + Size = (Size + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1); + + uintptr_t Start = StartAddress; + uintptr_t End = StartAddress + Size; + + for (size_t i = 0; i < Lpb->MemoryRegionCount; i++) + { + PLOADER_MEMORY_REGION OtherRegion = &KiMemoryRegions[i]; + if (OtherRegion->Type != LOADER_MEM_FREE) + continue; + + uintptr_t OrStart = OtherRegion->Base; + uintptr_t OrEnd = OtherRegion->Base + OtherRegion->Size; + + if (OrEnd <= Start || End <= OrStart) + { + // Not overlapping + continue; + } + + if (Start <= OrStart && OrEnd <= End) + { + // region completely swallowed, so nuke it + OtherRegion->Type = LOADER_MEM_RESERVED; + continue; + } + + if (OrStart <= Start && End <= OrEnd) + { + // The region we are trying to erase is completely within + // this memory region. + + // We need to create two memory regions: + // OrStart -- Start - End -- OrEnd + + // First, check the trivial cases + if (OrStart == Start) + { + // just set the start to the end + OtherRegion->Base = End; + OtherRegion->Size = OrEnd - End; + } + else if (OrEnd == End) + { + // just set the end to the start + OtherRegion->Size = Start - OrStart; + } + else + { + // need to create a separate region + OtherRegion->Size = Start - OrStart; + + if (Lpb->MemoryRegionCount >= MAX_MEMORY_REGIONS) + continue; + + DbgPrint("ADDING"); + PLOADER_MEMORY_REGION NewRegion = &KiMemoryRegions[Lpb->MemoryRegionCount++]; + NewRegion->Type = LOADER_MEM_FREE; + NewRegion->Base = End; + NewRegion->Size = OrEnd - End; + } + + continue; + } + + if (OrStart < End && End < OrEnd && Start < OrStart) + OrStart = End; + + if (Start < OrEnd && OrEnd < End && OrStart < Start) + OrEnd = Start; + + if (OrEnd < OrStart) + { + OtherRegion->Type = LOADER_MEM_RESERVED; + continue; + } + + OtherRegion->Base = OrStart; + OtherRegion->Size = OrEnd - OrStart; + } +} + +INIT +static void KiInitializeMemoryRegions() +{ + PLOADER_PARAMETER_BLOCK Lpb = &KeLoaderParameterBlock; + multiboot_memory_map_t + *Mmap = P2V(KiMultibootInfo->mmap_addr), + *MmapStart = Mmap, + *MmapEnd = (void*)((uintptr_t)Mmap + KiMultibootInfo->mmap_length); + + // First, find all the available entries, and insert them. During this loop, + // any overlapping regions are resized/merged/erased. + size_t Index = 0; + for (; Mmap < MmapEnd; Mmap = (void*)((uintptr_t)Mmap + Mmap->size + sizeof(Mmap->size))) + { + if (Mmap->type != MULTIBOOT_MEMORY_AVAILABLE) + continue; + + DbgPrint("Mmap->addr = %p, Mmap->len = %p", (void*)(unsigned)Mmap->addr, Mmap->len); + + // ignore ranges that start into 64-bit memory + if (Mmap->addr > 0x100000000) + continue; + + // cap ranges that start into 64-bit memory + if (Mmap->addr + Mmap->len > 0x100000000) + Mmap->len = 0x100000000 - Mmap->addr; + + PLOADER_MEMORY_REGION MemoryRegion = &KiMemoryRegions[Index]; + MemoryRegion->Base = Mmap->addr; + MemoryRegion->Size = Mmap->len; + + // Ensure the address and length are page size aligned. + uint32_t Bias = PAGE_SIZE - (MemoryRegion->Base & (PAGE_SIZE - 1)); + if (Bias != PAGE_SIZE) + { + MemoryRegion->Size -= Bias; + MemoryRegion->Base += Bias; // addr is now aligned + } + + MemoryRegion->Size = MemoryRegion->Size & ~(PAGE_SIZE - 1); + if (MemoryRegion->Size == 0) + continue; + + MemoryRegion->Type = LOADER_MEM_FREE; + + // Ensure this range doesn't overlap with anything else. + uintptr_t Start = MemoryRegion->Base; + uintptr_t End = MemoryRegion->Base + MemoryRegion->Size; + + for (size_t i = 0; i < Index; i++) + { + PLOADER_MEMORY_REGION OtherRegion = &KiMemoryRegions[i]; + + uintptr_t OrStart = OtherRegion->Base; + uintptr_t OrEnd = OtherRegion->Base + OtherRegion->Size; + + if (OrStart <= Start && End <= OrEnd) + { + // new region completely inside old, discard. + Start = End = 0; + break; + } + + if (Start <= OrStart && OrEnd <= End) + { + // the new segment completely swallows the old one. + OtherRegion->Base = Start; + OtherRegion->Size = End - Start; + Start = End = 0; + break; + } + + if (OrStart < End && Start <= OrStart && End <= OrEnd) + End = OrStart; + + if (Start < OrEnd && OrStart <= Start && OrEnd <= End) + Start = OrEnd; + } + + MemoryRegion->Base = Start; + + if (End < Start) + MemoryRegion->Size = 0; + else + MemoryRegion->Size = End - Start; + + if (MemoryRegion->Size != 0) + Index++; + else + MemoryRegion->Type = LOADER_MEM_RESERVED; + + if (Index >= MAX_MEMORY_REGIONS) + { + DbgPrint( + "BOOT WARNING: The bootloader provided %zu bytes of entries, but we have a maximum of %d " + "entries, and as such, some of the memory will be invisible to the OS.", + KiMultibootInfo->mmap_length, + MAX_MEMORY_REGIONS + ); + } + } + + Lpb->MemoryRegionCount = Index; + Lpb->MemoryRegions = KiMemoryRegions; + + Mmap = MmapStart; + for (; Mmap < MmapEnd; Mmap = (void*)((uintptr_t)Mmap + Mmap->size + sizeof(Mmap->size))) + { + if (Mmap->type == MULTIBOOT_MEMORY_AVAILABLE) + continue; + + KiRemoveAreaFromMemMap(Mmap->addr, Mmap->len); + } } +extern char KiKernelEnd[]; INIT void KiInitLoaderParameterBlock() { // Initialize the base identity mapping. MiInitializeBaseIdentityMapping(); - PLOADER_PARAMETER_BLOCK Lpb = &KeLoaderParameterBlock; + KiMultibootInfo = P2V(KiMultibootInfo); + if (KiMultibootSignature != MULTIBOOT_BOOTLOADER_MAGIC) KeCrashBeforeSMPInit("KiMultibootSignature is not %08x, it's %08x!", MULTIBOOT_BOOTLOADER_MAGIC, KiMultibootSignature); // Initialize the memory regions. + KiInitializeMemoryRegions(); + KiRemoveAreaFromMemMap(0, (size_t) KiKernelEnd); // Initialize the kernel module. diff --git a/boron/source/ke/i386/foreinit.asm b/boron/source/ke/i386/foreinit.asm index 85113e93..13ea2766 100644 --- a/boron/source/ke/i386/foreinit.asm +++ b/boron/source/ke/i386/foreinit.asm @@ -54,7 +54,7 @@ KiBeforeSystemStartup: ; store the provided multiboot data mov [V2P(KiMultibootSignature)], eax - mov [V2P(KiMultibootPointer)], ebx + mov [V2P(KiMultibootInfo)], ebx ; first address to map is 0x00000000. map 2048 pages xor esi, esi @@ -109,7 +109,7 @@ KiBeforeSystemStartupHigherHalf: mov cr3, ecx ; setup the initial stack - mov esp, KiInitialStack + mov esp, KiInitialStack + 4096 ; GDT will be set up later @@ -123,12 +123,12 @@ KiBeforeSystemStartupHigherHalf: section .bss global KiMultibootSignature -global KiMultibootPointer +global KiMultibootInfo global KiBootstrapPageDirectory global KiBootstrapPageTables KiMultibootSignature: resd 1 -KiMultibootPointer: resd 1 +KiMultibootInfo: resd 1 alignb 4096 KiInitialStack: resb 4096 diff --git a/tools/run-i386.bat b/tools/run-i386.bat index 180b54b8..e84437b6 100644 --- a/tools/run-i386.bat +++ b/tools/run-i386.bat @@ -16,7 +16,6 @@ if exist %nspath%\vdiske2.vdi ( qemu-system-i386.exe -no-reboot -no-shutdown -d int -M smm=off ^ -M q35 ^ -m 256M ^ --smp 6 ^ -boot d ^ -display sdl ^ -accel tcg ^ diff --git a/tools/run-unix-i386.sh b/tools/run-unix-i386.sh index 30a0ceb8..0d427ce0 100644 --- a/tools/run-unix-i386.sh +++ b/tools/run-unix-i386.sh @@ -3,7 +3,6 @@ qemu-system-i386 \ -no-shutdown \ -M q35 \ -m 256M \ - -smp 4 \ -boot d \ -cdrom build/image.i386.iso \ -debugcon stdio From db1afea036414e3801b88ce29e0cf226130ed864 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Fri, 17 Oct 2025 14:09:16 +0300 Subject: [PATCH 15/44] * Hali386: Begin * Ke: Making progress in the initialization process! --- Makefile | 1 + boron/include/hal.h | 2 +- boron/include/hal/init.h | 8 +- boron/include/ke/lpb.h | 1 + boron/include/ke/process.h | 2 +- boron/source/build_number | 2 +- boron/source/hal/hal.c | 4 +- boron/source/ke/amd64/boot.c | 1 + boron/source/ke/amd64/intobj.c | 2 +- boron/source/ke/amd64/tlbs.c | 2 +- boron/source/ke/i386/boot.c | 104 +++++++++- boron/source/ke/i386/cpu.c | 1 + boron/source/ke/i386/intobj.c | 2 +- boron/source/ke/process.c | 6 +- boron/source/ke/smp.c | 8 +- boron/source/ldr/loader.c | 2 +- boron/source/mm/i386/pt.c | 6 +- boron/source/ps/init.c | 5 +- boron/source/ps/process.c | 6 +- drivers/hali386/Makefile | 10 + drivers/hali386/linker.ld | 65 +++++++ drivers/hali386/source/crash.c | 26 +++ drivers/hali386/source/hali.h | 33 ++++ drivers/hali386/source/init.c | 65 +++++++ drivers/hali386/source/pci.c | 335 +++++++++++++++++++++++++++++++++ drivers/hali386/source/pci.h | 55 ++++++ drivers/hali386/source/pic.c | 28 +++ drivers/hali386/source/pio.h | 27 +++ drivers/hali386/source/term.c | 23 +++ drivers/hali386/source/timer.c | 68 +++++++ drivers/halx86/source/apic.c | 2 +- drivers/halx86/source/apic.h | 3 +- drivers/halx86/source/init.c | 2 +- limine.i386.cfg | 15 +- 34 files changed, 889 insertions(+), 33 deletions(-) create mode 100644 drivers/hali386/Makefile create mode 100644 drivers/hali386/linker.ld create mode 100644 drivers/hali386/source/crash.c create mode 100644 drivers/hali386/source/hali.h create mode 100644 drivers/hali386/source/init.c create mode 100644 drivers/hali386/source/pci.c create mode 100644 drivers/hali386/source/pci.h create mode 100644 drivers/hali386/source/pic.c create mode 100644 drivers/hali386/source/pio.h create mode 100644 drivers/hali386/source/term.c create mode 100644 drivers/hali386/source/timer.c diff --git a/Makefile b/Makefile index 93a493c5..49be3769 100644 --- a/Makefile +++ b/Makefile @@ -29,6 +29,7 @@ DRIVERS_LIST = \ test else ifeq ($(TARGET),I386) DRIVERS_LIST = \ + hali386 \ i8042prt \ ext2fs \ test diff --git a/boron/include/hal.h b/boron/include/hal.h index 1722e0ff..676b8f54 100644 --- a/boron/include/hal.h +++ b/boron/include/hal.h @@ -16,7 +16,7 @@ bool HalWasInitted(); // HAL API. See hal/init.h -void HalEndOfInterrupt(); +void HalEndOfInterrupt(int InterruptNumber); void HalRequestIpi(uint32_t LapicId, uint32_t Flags, int Vector); void HalInitSystemUP(); void HalInitSystemMP(); diff --git a/boron/include/hal/init.h b/boron/include/hal/init.h index 32d25631..9f81aad9 100644 --- a/boron/include/hal/init.h +++ b/boron/include/hal/init.h @@ -21,7 +21,7 @@ Module name: #define HAL_VFTABLE_LOADED (1 << 0) // Function pointer definitions -typedef void(*PFHAL_END_OF_INTERRUPT)(void); +typedef void(*PFHAL_END_OF_INTERRUPT)(int InterruptNumber); typedef void(*PFHAL_REQUEST_INTERRUPT_IN_TICKS)(uint64_t Ticks); typedef void(*PFHAL_REQUEST_IPI)(uint32_t LapicId, uint32_t Flags, int Vector); typedef void(*PFHAL_INIT_SYSTEM_UP)(void); @@ -36,11 +36,11 @@ typedef uint64_t(*PFHAL_GET_TICK_FREQUENCY)(void); typedef uint64_t(*PFHAL_GET_INT_TIMER_DELTA_TICKS)(void); #ifdef TARGET_AMD64 - typedef void(*PFHAL_IOAPIC_SET_IRQ_REDIRECT)(uint8_t Vector, uint8_t Irq, uint32_t LapicId, bool Status); +#endif +#if defined TARGET_AMD64 || defined TARGET_I386 #include "pci.h" - #endif typedef struct @@ -63,6 +63,8 @@ typedef struct #ifdef TARGET_AMD64 PFHAL_IOAPIC_SET_IRQ_REDIRECT IoApicSetIrqRedirect; +#endif +#if defined TARGET_AMD64 || defined TARGET_I386 PFHAL_PCI_ENUMERATE PciEnumerate; PFHAL_PCI_CONFIG_READ_DWORD PciConfigReadDword; PFHAL_PCI_CONFIG_READ_WORD PciConfigReadWord; diff --git a/boron/include/ke/lpb.h b/boron/include/ke/lpb.h index e20c2a72..d33dd4f1 100644 --- a/boron/include/ke/lpb.h +++ b/boron/include/ke/lpb.h @@ -32,6 +32,7 @@ typedef struct uint32_t Pitch; // width of a single row in bytes uint32_t Width; uint32_t Height; + bool IsPhysicalAddress; uint8_t BitDepth; uint8_t RedMaskSize; uint8_t RedMaskShift; diff --git a/boron/include/ke/process.h b/boron/include/ke/process.h index e0685d76..069ae0fc 100644 --- a/boron/include/ke/process.h +++ b/boron/include/ke/process.h @@ -53,7 +53,7 @@ PKPROCESS KeGetSystemProcess(); void KeDeallocateProcess(PKPROCESS Process); // Initialize the process. -void KeInitializeProcess(PKPROCESS Process, int BasePriority, KAFFINITY BaseAffinity); +BSTATUS KeInitializeProcess(PKPROCESS Process, int BasePriority, KAFFINITY BaseAffinity); // Attach to or detach from a process, on behalf of the current thread. // diff --git a/boron/source/build_number b/boron/source/build_number index aa34eab5..b4249c47 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -199 +239 diff --git a/boron/source/hal/hal.c b/boron/source/hal/hal.c index 457063f3..2430f45d 100644 --- a/boron/source/hal/hal.c +++ b/boron/source/hal/hal.c @@ -27,9 +27,9 @@ void HalSetVftable(const HAL_VFTABLE* Table) HalpVftable = *Table; } -void HalEndOfInterrupt() +void HalEndOfInterrupt(int InterruptNumber) { - HalpVftable.EndOfInterrupt(); + HalpVftable.EndOfInterrupt(InterruptNumber); } void HalRequestInterruptInTicks(uint64_t Ticks) diff --git a/boron/source/ke/amd64/boot.c b/boron/source/ke/amd64/boot.c index 21d1d104..71c2788b 100644 --- a/boron/source/ke/amd64/boot.c +++ b/boron/source/ke/amd64/boot.c @@ -148,6 +148,7 @@ static void KiConvertLimineFramebufferToLoaderFramebuffer( LoaderFb->GreenMaskShift = LimineFb->green_mask_shift; LoaderFb->BlueMaskSize = LimineFb->blue_mask_size; LoaderFb->BlueMaskShift = LimineFb->blue_mask_shift; + LoaderFb->IsPhysicalAddress = false; // TODO: transfer other details if needed. } diff --git a/boron/source/ke/amd64/intobj.c b/boron/source/ke/amd64/intobj.c index b875f42b..790cb777 100644 --- a/boron/source/ke/amd64/intobj.c +++ b/boron/source/ke/amd64/intobj.c @@ -55,7 +55,7 @@ static PKREGISTERS KiInterruptDispatch(PKREGISTERS Regs) KeReleaseSpinLock(&InterruptList->Lock, Ipl); // Acknowledge the interrupt. - HalEndOfInterrupt(); + HalEndOfInterrupt((int) Regs->IntNumber); // No change in registers. return Regs; diff --git a/boron/source/ke/amd64/tlbs.c b/boron/source/ke/amd64/tlbs.c index cf471b9c..c52ad58e 100644 --- a/boron/source/ke/amd64/tlbs.c +++ b/boron/source/ke/amd64/tlbs.c @@ -142,6 +142,6 @@ PKREGISTERS KiHandleTlbShootdownIpi(PKREGISTERS Regs) KeReleaseSpinLock(&Prcb->TlbsLock, IPL_NOINTS); - HalEndOfInterrupt(); + HalEndOfInterrupt((int) Regs->IntNumber); return Regs; } diff --git a/boron/source/ke/i386/boot.c b/boron/source/ke/i386/boot.c index a534627f..371d6356 100644 --- a/boron/source/ke/i386/boot.c +++ b/boron/source/ke/i386/boot.c @@ -62,7 +62,6 @@ static void* KiEarlyAllocateMemoryFromMemMap(size_t Size) INIT static void KiRemoveAreaFromMemMap(uintptr_t StartAddress, size_t Size) { - DbgPrint("KiRemoveAreaFromMemMap(0x%p, 0x%x)", StartAddress, Size); PLOADER_PARAMETER_BLOCK Lpb = &KeLoaderParameterBlock; // Ensure the start address and size greedily cover page boundaries. @@ -123,7 +122,6 @@ static void KiRemoveAreaFromMemMap(uintptr_t StartAddress, size_t Size) if (Lpb->MemoryRegionCount >= MAX_MEMORY_REGIONS) continue; - DbgPrint("ADDING"); PLOADER_MEMORY_REGION NewRegion = &KiMemoryRegions[Lpb->MemoryRegionCount++]; NewRegion->Type = LOADER_MEM_FREE; NewRegion->Base = End; @@ -153,6 +151,9 @@ static void KiRemoveAreaFromMemMap(uintptr_t StartAddress, size_t Size) INIT static void KiInitializeMemoryRegions() { + if (~KiMultibootInfo->flags & MULTIBOOT_INFO_MEM_MAP) + KeCrashBeforeSMPInit("ERROR: There is no memory map specified!"); + PLOADER_PARAMETER_BLOCK Lpb = &KeLoaderParameterBlock; multiboot_memory_map_t *Mmap = P2V(KiMultibootInfo->mmap_addr), @@ -167,8 +168,6 @@ static void KiInitializeMemoryRegions() if (Mmap->type != MULTIBOOT_MEMORY_AVAILABLE) continue; - DbgPrint("Mmap->addr = %p, Mmap->len = %p", (void*)(unsigned)Mmap->addr, Mmap->len); - // ignore ranges that start into 64-bit memory if (Mmap->addr > 0x100000000) continue; @@ -267,6 +266,10 @@ static void KiInitializeMemoryRegions() extern char KiKernelEnd[]; +static LOADER_AP KiLoaderAp; +static LOADER_FRAMEBUFFER KiLoaderFramebuffer; +static void* KiLoaderApDummy; + INIT void KiInitLoaderParameterBlock() { @@ -281,17 +284,106 @@ void KiInitLoaderParameterBlock() // Initialize the memory regions. KiInitializeMemoryRegions(); - KiRemoveAreaFromMemMap(0, (size_t) KiKernelEnd); + KiRemoveAreaFromMemMap(0x100000, (size_t) KiKernelEnd - 0x100000); // Initialize the kernel module. + Lpb->ModuleInfo.Kernel.Path = "kernel.elf"; + Lpb->ModuleInfo.Kernel.String = P2V(KiMultibootInfo->cmdline); + Lpb->ModuleInfo.Kernel.Address = (void*) 0xC0100000; // TODO: is the whole kernel (+ELF stuff) loaded here?? + Lpb->ModuleInfo.Kernel.Size = (size_t) KiKernelEnd - 0x100000; // Initialize the other modules. + if (KiMultibootInfo->flags & MULTIBOOT_INFO_MODS) + { + Lpb->ModuleInfo.Count = KiMultibootInfo->mods_count; + Lpb->ModuleInfo.List = KiEarlyAllocateMemoryFromMemMap(Lpb->ModuleInfo.Count * sizeof(LOADER_MODULE)); + + // QUIRK: Multiboot1 does *not* give you the name of modules! + // So their name has to be specified through the commandline. + // Wow, that sucks. Also Multiboot2 doesn't either. + multiboot_module_t* Module = P2V(KiMultibootInfo->mods_addr); + KiRemoveAreaFromMemMap(KiMultibootInfo->mods_addr, Lpb->ModuleInfo.Count * sizeof(multiboot_module_t)); + + for (size_t i = 0; i < Lpb->ModuleInfo.Count; i++) + { + PLOADER_MODULE Mod = &Lpb->ModuleInfo.List[i]; + Mod->Address = P2V(Module->mod_start); + Mod->Size = Module->mod_end - Module->mod_start; + Mod->String = ""; + + if (Module->cmdline == 0) + KeCrashBeforeSMPInit("ERROR: Cannot load module table. This module has no name."); + + Mod->Path = P2V(Module->cmdline); + + KiRemoveAreaFromMemMap(Module->mod_start, Module->mod_end - Module->mod_start); + + Module++; + } + } + else + { + DbgPrint("Booted without modules, the system WILL NOT boot!"); + Lpb->ModuleInfo.Count = 0; + Lpb->ModuleInfo.List = NULL; + } // Initialize the CPUs. + Lpb->Multiprocessor.Count = 1; + Lpb->Multiprocessor.List = &KiLoaderAp; + Lpb->Multiprocessor.BootstrapHardwareId = 1; + + KiLoaderAp.ProcessorId = 0; + KiLoaderAp.HardwareId = 0; + KiLoaderAp.TrampolineJumpAddress = &KiLoaderApDummy; + KiLoaderAp.ExtraArgument = NULL; // Initialize the frame buffers. + if (KiMultibootInfo->flags & MULTIBOOT_INFO_FRAMEBUFFER_INFO) + { + Lpb->FramebufferCount = 1; + Lpb->Framebuffers = &KiLoaderFramebuffer; + + PLOADER_FRAMEBUFFER Fb = &KiLoaderFramebuffer; + if (KiMultibootInfo->framebuffer_addr > 0x100000000) + { + KeCrash( + "KiMultibootInfo->framebuffer_addr is %08x%08x, which is larger than 32-bit!", + (uint32_t)KiMultibootInfo->framebuffer_addr, + (uint32_t)(KiMultibootInfo->framebuffer_addr >> 32) + ); + } + + Fb->Address = (void*) (uint32_t) KiMultibootInfo->framebuffer_addr; + Fb->Pitch = KiMultibootInfo->framebuffer_pitch; + Fb->Width = KiMultibootInfo->framebuffer_width; + Fb->Height = KiMultibootInfo->framebuffer_height; + Fb->BitDepth = KiMultibootInfo->framebuffer_bpp; + Fb->RedMaskSize = KiMultibootInfo->u2.framebuffer_red_mask_size; + Fb->RedMaskShift = KiMultibootInfo->u2.framebuffer_red_field_position; + Fb->GreenMaskSize = KiMultibootInfo->u2.framebuffer_green_mask_size; + Fb->GreenMaskShift = KiMultibootInfo->u2.framebuffer_green_field_position; + Fb->BlueMaskSize = KiMultibootInfo->u2.framebuffer_blue_mask_size; + Fb->BlueMaskShift = KiMultibootInfo->u2.framebuffer_blue_field_position; + } + else + { + DbgPrint("Booted without a framebuffer, things might go wrong!"); + Lpb->FramebufferCount = 0; + Lpb->Framebuffers = NULL; + } // Initialize the bootloader's information as well as the command line. + Lpb->CommandLine = P2V(KiMultibootInfo->cmdline); - + if (KiMultibootInfo->flags & MULTIBOOT_INFO_BOOT_LOADER_NAME) + { + Lpb->LoaderInfo.Name = P2V(KiMultibootInfo->boot_loader_name); + Lpb->LoaderInfo.Version = "v1.0"; + } + else + { + Lpb->LoaderInfo.Name = "Generic Multiboot1 compliant bootloader"; + Lpb->LoaderInfo.Version = "v1.0"; + } } diff --git a/boron/source/ke/i386/cpu.c b/boron/source/ke/i386/cpu.c index 74befe17..6104e588 100644 --- a/boron/source/ke/i386/cpu.c +++ b/boron/source/ke/i386/cpu.c @@ -66,6 +66,7 @@ static void* KiCPUPointer; void KeSetCPUPointer(void* pGS) { + DbgPrint("KeSetCPUPointer: %p", pGS); KiCPUPointer = pGS; } diff --git a/boron/source/ke/i386/intobj.c b/boron/source/ke/i386/intobj.c index 30c42f60..76e67441 100644 --- a/boron/source/ke/i386/intobj.c +++ b/boron/source/ke/i386/intobj.c @@ -54,7 +54,7 @@ static PKREGISTERS KiInterruptDispatch(PKREGISTERS Regs) KeReleaseSpinLock(&InterruptList->Lock, Ipl); // Acknowledge the interrupt. - HalEndOfInterrupt(); + HalEndOfInterrupt(Regs->IntNumber); // No change in registers. return Regs; diff --git a/boron/source/ke/process.c b/boron/source/ke/process.c index 4ab93403..668fa7fe 100644 --- a/boron/source/ke/process.c +++ b/boron/source/ke/process.c @@ -46,12 +46,15 @@ PKPROCESS KeGetSystemProcess() return &PsGetSystemProcess()->Pcb; } -void KeInitializeProcess(PKPROCESS Process, int BasePriority, KAFFINITY BaseAffinity) +BSTATUS KeInitializeProcess(PKPROCESS Process, int BasePriority, KAFFINITY BaseAffinity) { KeInitializeDispatchHeader(&Process->Header, DISPATCH_PROCESS); Process->PageMap = MiCreatePageMapping(KeGetCurrentPageTable()); + if (Process->PageMap == 0) + return STATUS_INSUFFICIENT_MEMORY; + InitializeListHead(&Process->ThreadList); Process->AccumulatedTime = 0; @@ -61,6 +64,7 @@ void KeInitializeProcess(PKPROCESS Process, int BasePriority, KAFFINITY BaseAffi Process->DefaultAffinity = BaseAffinity; Process->PebPointer = NULL; + return STATUS_SUCCESS; } PKPROCESS KeSetAttachedProcess(PKPROCESS Process) diff --git a/boron/source/ke/smp.c b/boron/source/ke/smp.c index 1f748745..03dcc0a5 100644 --- a/boron/source/ke/smp.c +++ b/boron/source/ke/smp.c @@ -162,6 +162,12 @@ void PsInitSystemProcess(); NO_RETURN INIT void KeInitSMP() { +#ifdef CONFIG_SMP + #define UNI_OR_MULTI "Multi" +#else + #define UNI_OR_MULTI "Uni" +#endif + PLOADER_MP_INFO MpInfo = &KeLoaderParameterBlock.Multiprocessor; PLOADER_AP BspAp = NULL; @@ -220,7 +226,7 @@ void KeInitSMP() int VersionNumber = KeGetVersionNumber(); LogMsg("Boron (TM), October 2025 - v%d.%d.%d", VER_MAJOR(VersionNumber), VER_MINOR(VersionNumber), VER_BUILD(VersionNumber)); - LogMsg("%u System Processors [%u Kb System Memory] MultiProcessor Kernel", MpInfo->Count, MmTotalAvailablePages * PAGE_SIZE / 1024); + LogMsg("%u System Processors [%u Kb System Memory] %sProcessor Kernel", MpInfo->Count, MmTotalAvailablePages * PAGE_SIZE / 1024, UNI_OR_MULTI); for (uint64_t i = 0; i < MpInfo->Count; i++) { diff --git a/boron/source/ldr/loader.c b/boron/source/ldr/loader.c index 0b425941..9d7a7aac 100644 --- a/boron/source/ldr/loader.c +++ b/boron/source/ldr/loader.c @@ -21,7 +21,7 @@ static const char* LdrpHalPath = "halx86.sys"; #elif defined TARGET_I386 -static uintptr_t LdrpCurrentBase = 0xC0800000; +static uintptr_t LdrpCurrentBase = 0xD1000000; static const char* LdrpHalPath = "hali386.sys"; // sorry bucko, halx86 is already taken #else diff --git a/boron/source/mm/i386/pt.c b/boron/source/mm/i386/pt.c index 19551613..e1043620 100644 --- a/boron/source/mm/i386/pt.c +++ b/boron/source/mm/i386/pt.c @@ -90,8 +90,8 @@ HPAGEMAP MiCreatePageMapping(HPAGEMAP OldPageMapping) PMMPTE NewPageMappingAccess = MmGetHHDMOffsetAddr (NewPageMappingResult), OldPageMappingAccess; //PMMPTE OldPageMappingAccess = MmGetHHDMOffsetAddr (OldPageMapping); - // copy the kernel's 256 entries, and zero out the first 256 - for (int i = 0; i < 256; i++) + // copy the kernel's 512 entries, and zero out the first 512 + for (int i = 0; i < 512; i++) { NewPageMappingAccess[i] = 0; } @@ -99,7 +99,7 @@ HPAGEMAP MiCreatePageMapping(HPAGEMAP OldPageMapping) // Lock the kernel space's lock to not get any surprises. MmLockKernelSpaceShared(); - for (int i = 256; i < 512; i++) + for (int i = 512; i < 1024; i++) { //NewPageMappingAccess[i] = OldPageMappingAccess[i]; diff --git a/boron/source/ps/init.c b/boron/source/ps/init.c index 14aa8c03..24da2618 100644 --- a/boron/source/ps/init.c +++ b/boron/source/ps/init.c @@ -67,12 +67,15 @@ void PsInitSystemProcess() PspSystemProcessNpHeader.NormalHeader = Hdr; // Initialize the kernel side process. - KeInitializeProcess( + BSTATUS Status = KeInitializeProcess( &PsSystemProcess.Pcb, PRIORITY_NORMAL, AFFINITY_ALL ); + if (FAILED(Status)) + KeCrashBeforeSMPInit("KeInitializeProcess failed!"); + // Use the new page mapping. KeSetCurrentPageTable(PsSystemProcess.Pcb.PageMap); diff --git a/boron/source/ps/process.c b/boron/source/ps/process.c index 83b78e3a..6db1bf1b 100644 --- a/boron/source/ps/process.c +++ b/boron/source/ps/process.c @@ -83,15 +83,15 @@ BSTATUS PspInitializeProcessObject(void* ProcessV, void* Context) Process->Pcb.PageMap = 0; // Initialize the kernel side process. - KeInitializeProcess( + Status = KeInitializeProcess( &Process->Pcb, PRIORITY_NORMAL, AFFINITY_ALL ); // If the initial page map couldn't be created, throw an out of memory error. - if (!Process->Pcb.PageMap) - return STATUS_INSUFFICIENT_MEMORY; + if (FAILED(Status)) + return Status; MmInitializeVadList(&Process->VadList); diff --git a/drivers/hali386/Makefile b/drivers/hali386/Makefile new file mode 100644 index 00000000..644ea9f4 --- /dev/null +++ b/drivers/hali386/Makefile @@ -0,0 +1,10 @@ +# The Boron Operating System +# Common makefile for all driver targets + +DRIVER_NAME = hali386 +# DRIVER_ENTRY = DriverEntry +# DEBUG = yes +# DEBUG2 = no +USER_DEFINES = -DIS_HAL -DFLANTERM_FB_DISABLE_CANVAS -DFLANTERM_FB_DISABLE_BUMP_ALLOC + +include ../CommonMakefile diff --git a/drivers/hali386/linker.ld b/drivers/hali386/linker.ld new file mode 100644 index 00000000..a2c76e0e --- /dev/null +++ b/drivers/hali386/linker.ld @@ -0,0 +1,65 @@ +/* Tell the linker that we want an x86_64 ELF64 output file */ +OUTPUT_FORMAT(elf64-x86-64) +OUTPUT_ARCH(i386:x86-64) + +/* We want the symbol DriverEntry to be our entry point */ +ENTRY(DriverEntry) + +/* Define the program headers we want so the bootloader gives us the right */ +/* MMU permissions */ +PHDRS +{ + text PT_LOAD FLAGS((1 << 0) | (1 << 2)) ; /* Execute + Read */ + rodata PT_LOAD FLAGS((1 << 2)) ; /* Read only */ + data PT_LOAD FLAGS((1 << 1) | (1 << 2)) ; /* Write + Read */ +} + +SECTIONS +{ + /* We want to be placed in the topmost 2GiB of the address space, for optimizations, and because that is what the Limine spec mandates. */ + /* Any address in this region will do, but often 0xffffffff80000000 is chosen as that is the beginning of the region. */ + . = 0xffffffff80000000; + + .text : { + *(.text .text.*) + } :text + + /* Move to the next memory page for .rodata */ + . = ALIGN(CONSTANT(MAXPAGESIZE)); + + .rodata : { + *(.rodata .rodata.*) + } :rodata + + /* Move to the next memory page for .data */ + . = ALIGN(CONSTANT(MAXPAGESIZE)); + + /* Global constructor array. */ + .init_array : { + g_init_array_start = .; + *(.init_array) + g_init_array_end = .; + } + + /* Global destructor array. */ + .fini_array : { + g_fini_array_start = .; + *(.fini_array) + g_fini_array_end = .; + } + + .data : { + *(.data .data.*) + } :data + + .bss : { + *(COMMON) + *(.bss .bss.*) + } :data + + /* Discard .note.* and .eh_frame since they may cause issues on some hosts. */ + /DISCARD/ : { + *(.eh_frame) + *(.note .note.*) + } +} diff --git a/drivers/hali386/source/crash.c b/drivers/hali386/source/crash.c new file mode 100644 index 00000000..9b5aac15 --- /dev/null +++ b/drivers/hali386/source/crash.c @@ -0,0 +1,26 @@ +/*** + The Boron Operating System + Copyright (C) 2025 iProgramInCpp + +Module name: + ha/crash.c + +Abstract: + This module contains the i386 platform's specific crash routine. + +Author: + iProgramInCpp - 17 October 2025 +***/ +#include +#include "hali.h" + +HAL_API void HalProcessorCrashed() +{ + KeStopCurrentCPU(); +} + +void HalCrashSystem(const char* Message) +{ + DISABLE_INTERRUPTS(); + KeCrashConclusion(Message); +} diff --git a/drivers/hali386/source/hali.h b/drivers/hali386/source/hali.h new file mode 100644 index 00000000..50a4cee0 --- /dev/null +++ b/drivers/hali386/source/hali.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +#define HAL_API // specify calling convention here if needed + +#define PIT_TICK_FREQUENCY (1193182) +#define PIT_CHANNEL_0_PORT (0x40) + +#define PIC_MAIN_COMMAND (0x20) +#define PIC_MAIN_DATA (0x21) +#define PIC_SUB_COMMAND (0xA0) +#define PIC_SUB_DATA (0xA1) + +#define PIC_CMD_EOI (0x20) + +// ====== Timer ====== +void HalUpdatePitClock(); + +HAL_API uint64_t HalGetTickFrequency(); + +HAL_API uint64_t HalGetTickCount(); + +HAL_API bool HalUseOneShotIntTimer(); + +HAL_API uint64_t HalGetIntTimerFrequency(); + +HAL_API void HalRequestInterruptInTicks(uint64_t ticks); + +HAL_API uint64_t HalGetIntTimerDeltaTicks(); + + + diff --git a/drivers/hali386/source/init.c b/drivers/hali386/source/init.c new file mode 100644 index 00000000..be73d773 --- /dev/null +++ b/drivers/hali386/source/init.c @@ -0,0 +1,65 @@ +/*** + The Boron Operating System + Copyright (C) 2025 iProgramInCpp + +Module name: + ha/init.c + +Abstract: + This module contains the two initialization functions + (UP-init and MP-init) + +Author: + iProgramInCpp - 16 October 2025 +***/ +#include +#include "hali.h" +#include "pci.h" + +static const HAL_VFTABLE HalpVfTable = +{ + .EndOfInterrupt = HalEndOfInterrupt, + .RequestInterruptInTicks = HalRequestInterruptInTicks, + .RequestIpi = HalRequestIpi, + .InitSystemUP = HalInitSystemUP, + .InitSystemMP = HalInitSystemMP, + .DisplayString = HalDisplayString, + .CrashSystem = HalCrashSystem, + .ProcessorCrashed = HalProcessorCrashed, + .UseOneShotIntTimer = HalUseOneShotIntTimer, + .GetIntTimerFrequency = HalGetIntTimerFrequency, + .GetTickCount = HalGetTickCount, + .GetTickFrequency = HalGetTickFrequency, + .GetIntTimerDeltaTicks = HalGetIntTimerDeltaTicks, + .PciEnumerate = HalPciEnumerate, + .PciConfigReadDword = HalPciConfigReadDword, + .PciConfigReadWord = HalPciConfigReadWord, + .PciConfigWriteDword = HalPciConfigWriteDword, + .PciReadDeviceIdentifier = HalPciReadDeviceIdentifier, + .PciReadBar = HalPciReadBar, + .PciReadBarAddress = HalPciReadBarAddress, + .PciReadBarIoAddress = HalPciReadBarIoAddress, + .Flags = HAL_VFTABLE_LOADED, +}; + +// Initialize the HAL on the BSP, for all processors. +HAL_API void HalInitSystemUP() +{ +} + +// Initialize the HAL separately for each processor. +// This function is run on ALL processors. +HAL_API void HalInitSystemMP() +{ +} + +BSTATUS DriverEntry(UNUSED PDRIVER_OBJECT Object) +{ + // Note! The HAL's driver object is kind of useless, it doesn't do anything actually. + + // Hook the HAL's functions. + HalSetVftable(&HalpVfTable); + + // And return, that's all we need. + return STATUS_SUCCESS; +} \ No newline at end of file diff --git a/drivers/hali386/source/pci.c b/drivers/hali386/source/pci.c new file mode 100644 index 00000000..cd52ea73 --- /dev/null +++ b/drivers/hali386/source/pci.c @@ -0,0 +1,335 @@ +/*** + The Boron Operating System + Copyright (C) 2023 iProgramInCpp + +Module name: + pci.c + +Abstract: + This module defines the base PCI functions. + +Author: + iProgramInCpp - 4 July 2024 +***/ +#include "pio.h" +#include "pci.h" + +static void PcipWriteAddress(PPCI_ADDRESS Address, uint8_t Offset) +{ + KePortWriteDword( + PCI_CONFIG_ADDRESS, + (Address->Bus << 16) | + (Address->Slot << 11) | + (Address->Function << 8) | + (Offset & 0xFC) | + 0x80000000 + ); +} + +uint32_t HalPciConfigReadDword(PPCI_ADDRESS Address, uint8_t Offset) +{ + PcipWriteAddress(Address, Offset); + return KePortReadDword(PCI_CONFIG_DATA); +} + +void HalPciConfigWriteDword(PPCI_ADDRESS Address, uint8_t Offset, uint32_t Data) +{ + PcipWriteAddress(Address, Offset); + KePortWriteDword(PCI_CONFIG_DATA, Data); +} + +uint16_t HalPciConfigReadWord(PPCI_ADDRESS Address, uint8_t Offset) +{ + PcipWriteAddress(Address, Offset); + return (uint16_t)(KePortReadDword(PCI_CONFIG_DATA) >> (8 * (Offset & 2))); +} + +void HalPciReadDeviceIdentifier(PPCI_ADDRESS Address, PPCI_IDENTIFIER OutIdentifier) +{ + OutIdentifier->VendorAndDeviceId = HalPciConfigReadDword(Address, PCI_OFFSET_DEVICE_IDENTIFIER); +} + +uint32_t HalPciReadBar(PPCI_ADDRESS Address, int BarIndex) +{ + return HalPciConfigReadDword(Address, PCI_OFFSET_BAR0 + 4 * BarIndex); +} + +uintptr_t HalPciReadBarAddress(PPCI_ADDRESS Address, int BarIndex) +{ + uint32_t LowBar = HalPciReadBar(Address, BarIndex); + + // Check if the BAR is actually for an I/O space device. + if (LowBar & 1) + return 0; + + // Check the type of BAR. + int Type = (int) (LowBar & 0b110) >> 1; + + // NOTE: This source file will probably be shared between the 32-bit and + // 64-bit x86 HALs, explaining this check. +#ifndef IS_64_BIT + if (Type == 2) + Type = 0; +#endif + + if (Type == 0) + // 32-bit BAR + return LowBar & ~0xF; + + if (Type != 2) + // Unknown type of BAR + return 0; + + return ((uint64_t)HalPciReadBar(Address, BarIndex + 1) << 32) | (LowBar & ~0xF); +} + +uint32_t HalPciReadBarIoAddress(PPCI_ADDRESS Address, int BarIndex) +{ + uint32_t Bar = HalPciReadBar(Address, BarIndex); + + // Check if the BAR is actually for a memory space device. + if (~Bar & 1) + return 0; + + return Bar & ~0x3; +} + +PPCI_DEVICE HalpPciDevices; +size_t HalpPciDeviceCount, HalpPciDeviceCapacity; +KSPIN_LOCK HalpPciDeviceSpinLock; + +static void HalpPciAddDevice(PPCI_DEVICE Device) +{ + // Process this device's capabilities list, if it exists. + uint16_t Status = HalPciConfigReadWord(&Device->Address, PCI_OFFSET_STATUS_COMMAND + 2); + + if (Status & PCI_STA_CAPABILITIESLIST) + { + // The capabilities pointer is stored at offset 0x34. + // Trim the upper 24 bits as the header is only 256 bytes in size, and also the lower 4 bits. + uint32_t CapabilityOffset = HalPciConfigReadDword(&Device->Address, PCI_OFFSET_CAPABILITIES_PTR); + CapabilityOffset &= 0xFC; + + // The capability offset linked list is terminated with a zero. + while (CapabilityOffset != 0) + { + uint32_t UpperDword = HalPciConfigReadDword(&Device->Address, CapabilityOffset); + uint8_t Capability = UpperDword & 0xFF; + + // Determine the capability's ID. + switch (Capability) + { + case PCI_CAP_MSI: + { + Device->MsiData.Exists = true; + Device->MsiData.CapabilityOffset = CapabilityOffset; + break; + } + + case PCI_CAP_MSI_X: + { + Device->MsixData.Exists = true; + Device->MsixData.CapabilityOffset = CapabilityOffset; + + // Disable interrupts on the capability, and read information from it. + UpperDword |= PCI_MSIX_MC_ENABLE << 16; + UpperDword |= PCI_MSIX_MC_FUNMASK << 16; + + HalPciConfigWriteDword(&Device->Address, CapabilityOffset, UpperDword); + + uint32_t Command = HalPciConfigReadDword(&Device->Address, PCI_OFFSET_STATUS_COMMAND); + Command |= PCI_CMD_INTERRUPTDISABLE; + HalPciConfigWriteDword(&Device->Address, PCI_OFFSET_STATUS_COMMAND, Command); + + Device->MsixData.TableSize = PCI_MSIX_MC_TABLESIZE(UpperDword >> 16); + + uint32_t Bir = HalPciConfigReadDword(&Device->Address, CapabilityOffset + 0x04); + Device->MsixData.Bir = Bir & 0x7; + Device->MsixData.TableOffset = Bir & ~0x7; + + uint32_t Pbir = HalPciConfigReadDword(&Device->Address, CapabilityOffset + 0x04); + Device->MsixData.Pbir = Pbir & 0x7; + Device->MsixData.PendingTableOffset = Pbir & ~0x7; + + break; + } + + default: + { + PciDbgPrint( + "PCI device %04x_%04x at %d.%d.%d has unrecognised capability %02x", + Device->Identifier.VendorId, + Device->Identifier.DeviceId, + Device->Address.Bus, + Device->Address.Slot, + Device->Address.Function, + Capability + ); + break; + } + } + + // Jump to the next capability offset. + CapabilityOffset = (UpperDword >> 8) & 0xFC; + } + } + + // Now, add it into the list of known PCI devices. + KIPL Ipl; + KeAcquireSpinLock(&HalpPciDeviceSpinLock, &Ipl); + + size_t Index; + if ((Index = HalpPciDeviceCount++) == HalpPciDeviceCapacity) + { + size_t OldCapacity = HalpPciDeviceCapacity; + + if (HalpPciDeviceCapacity < 16) + HalpPciDeviceCapacity = 16; + else + HalpPciDeviceCapacity *= 2; + + PPCI_DEVICE NewDevices = MmAllocatePool(POOL_NONPAGED, sizeof(PCI_DEVICE) * HalpPciDeviceCapacity); + if (!NewDevices) + { + KeCrash( + "WARNING: Could not add PCI device with address %d,%d,%d due to an out of memory condition", + Device->Address.Bus, + Device->Address.Slot, + Device->Address.Function + ); + } + + memset(NewDevices + OldCapacity, 0, sizeof(PCI_DEVICE) * (HalpPciDeviceCapacity - OldCapacity)); + + if (HalpPciDevices) + { + memcpy(NewDevices, HalpPciDevices, sizeof(PCI_DEVICE) * OldCapacity); + MmFreePool(HalpPciDevices); + } + HalpPciDevices = NewDevices; + } + + HalpPciDevices[Index] = *Device; + + KeReleaseSpinLock(&HalpPciDeviceSpinLock, Ipl); +} + +void HalPciProbe() +{ + PciDbgPrint("Probing PCI devices..."); + + // TODO: Better way to probe devices. + PCI_DEVICE Device; + + for (int Bus = 0; Bus < PCI_MAX_BUS; Bus++) + { + for (int Slot = 0; Slot < PCI_MAX_SLOT; Slot++) + { + for (int Function = 0; Function < PCI_MAX_FUNC; Function++) + { + memset(&Device, 0, sizeof Device); + + Device.Address.Bus = Bus; + Device.Address.Slot = Slot; + Device.Address.Function = Function; + + HalPciReadDeviceIdentifier(&Device.Address, &Device.Identifier); + + if (Device.Identifier.VendorId == 0xFFFF) + continue; + + //DbgPrint("Found PCI device. Vendor ID: %04x, Device ID: %04x", Device.Identifier.VendorId, Device.Identifier.DeviceId); + + // Also fetch its class register + Device.Class.Register = HalPciConfigReadDword(&Device.Address, PCI_OFFSET_REVISION_CLASS); + + HalpPciAddDevice(&Device); + } + } + } + + // List all devices. + KIPL Ipl; + KeAcquireSpinLock(&HalpPciDeviceSpinLock, &Ipl); + +#ifdef PCIDEBUG + for (size_t i = 0; i < HalpPciDeviceCount; i++) + { + PPCI_DEVICE Device = &HalpPciDevices[i]; + + PciDbgPrint( + "DEVICE: Bus %02x, Slot %02x, Function %1x, Vendor ID: %04x, Device ID: %04x, Class: %02x, SubClass: %02x, ProgIF: %02x, Rev: %02x. Ints: %s", + Device->Address.Bus, + Device->Address.Slot, + Device->Address.Function, + Device->Identifier.VendorId, + Device->Identifier.DeviceId, + Device->Class.Class, + Device->Class.SubClass, + Device->Class.ProgIF, + Device->Class.Revision, + Device->MsixData.Exists ? "MSI-X" : (Device->MsiData.Exists ? "MSI" : "No Interrupts") + ); + } +#endif + + KeReleaseSpinLock(&HalpPciDeviceSpinLock, Ipl); + + DbgPrint("PCI device probe complete."); +} + +BSTATUS +HalPciEnumerate( + bool LookUpByIds, + size_t IdCount, + PPCI_IDENTIFIER Identifiers, + uint8_t ClassCode, + uint8_t SubClassCode, + PHAL_PCI_ENUMERATE_CALLBACK Callback, + void* CallbackContext +) +{ + bool FoundDevices = false; + + for (size_t i = 0; i < HalpPciDeviceCount; i++) + { + PPCI_DEVICE Device = &HalpPciDevices[i]; + + // Check if the device matches. + if (LookUpByIds) + { + bool Is = false; + + for (size_t j = 0; !Is && j < IdCount; j++) + { + if (Device->Identifier.VendorAndDeviceId == Identifiers[j].VendorAndDeviceId) + Is = true; + } + + if (Is) + { + if (Callback(Device, CallbackContext)) + FoundDevices = true; + } + } + else + { + if (ClassCode == Device->Class.Class && + (SubClassCode == PCI_SUBCLASS_ANY || SubClassCode == Device->Class.SubClass)) + { + if (Callback(Device, CallbackContext)) + FoundDevices = true; + } + } + } + + if (FoundDevices) + return STATUS_SUCCESS; + else + return STATUS_NO_SUCH_DEVICES; +} + +void HalInitPci() +{ + HalPciProbe(); +} \ No newline at end of file diff --git a/drivers/hali386/source/pci.h b/drivers/hali386/source/pci.h new file mode 100644 index 00000000..a0eb3b7b --- /dev/null +++ b/drivers/hali386/source/pci.h @@ -0,0 +1,55 @@ +/*** + The Boron Operating System + Copyright (C) 2024 iProgramInCpp + +Module name: + pci.h + +Abstract: + This header defines PCI-related structures and + function prototypes. + +Author: + iProgramInCpp - 4 July 2024 +***/ +#pragma once + +#include +#include +#include +#include // includes hal/pci.h which defines certain types +#include + +#define PCI_CONFIG_ADDRESS (0xCF8) +#define PCI_CONFIG_DATA (0xCFC) + +#define PCI_VENDOR_INVALID (0xFFFF) + +// PCI capability IDs. +#define PCI_CAP_PCI_EXPRESS (0x01) +#define PCI_CAP_MSI (0x05) +#define PCI_CAP_MSI_X (0x11) + +#define PCI_MAX_BUS (256) +#define PCI_MAX_SLOT (32) +#define PCI_MAX_FUNC (8) + +#ifdef DEBUG2 +#define PCIDEBUG +#endif + +#ifdef PCIDEBUG +#define PciDbgPrint(...) DbgPrint(__VA_ARGS__) +#else +#define PciDbgPrint(...) do { } while (0) +#endif + +void HalInitPci(); + +uint32_t HalPciConfigReadDword(PPCI_ADDRESS Address, uint8_t Offset); +uint16_t HalPciConfigReadWord(PPCI_ADDRESS Address, uint8_t Offset); +void HalPciConfigWriteDword(PPCI_ADDRESS Address, uint8_t Offset, uint32_t Data); +void HalPciReadDeviceIdentifier(PPCI_ADDRESS Address, PPCI_IDENTIFIER OutIdentifier); +uint32_t HalPciReadBar(PPCI_ADDRESS Address, int BarIndex); +uint32_t HalPciReadBarIoAddress(PPCI_ADDRESS Address, int BarIndex); +uintptr_t HalPciReadBarAddress(PPCI_ADDRESS Address, int BarIndex); \ No newline at end of file diff --git a/drivers/hali386/source/pic.c b/drivers/hali386/source/pic.c new file mode 100644 index 00000000..f3cfadaa --- /dev/null +++ b/drivers/hali386/source/pic.c @@ -0,0 +1,28 @@ +/*** + The Boron Operating System + Copyright (C) 2025 iProgramInCpp + +Module name: + ha/pic.c + +Abstract: + This module contains support routines for the PIC. + +Author: + iProgramInCpp - 16 October 2025 +***/ +#include +#include "hali.h" + +HAL_API void HalRequestIpi(UNUSED uint32_t HardwareId, UNUSED uint32_t Flags, UNUSED int Vector) +{ + DbgPrint("HalRequestIpi not implemented"); +} + +void HalEndOfInterrupt(int InterruptNumber) +{ + if (InterruptNumber >= 0x28) + KePortWriteByte(PIC_SUB_COMMAND, PIC_CMD_EOI); + + KePortWriteByte(PIC_MAIN_COMMAND, PIC_CMD_EOI); +} diff --git a/drivers/hali386/source/pio.h b/drivers/hali386/source/pio.h new file mode 100644 index 00000000..95bd3dac --- /dev/null +++ b/drivers/hali386/source/pio.h @@ -0,0 +1,27 @@ +/*** + The Boron Operating System + Copyright (C) 2023 iProgramInCpp + +Module name: + arch/amd64/pio.h + +Abstract: + This module implements x86 specific Port I/O functions. + +Author: + iProgramInCpp - 7 September 2023 +***/ + +#ifndef NS64_PIO_H +#define NS64_PIO_H + +#include + +uint8_t KePortReadByte(uint16_t portNo); +void KePortWriteByte(uint16_t portNo, uint8_t data); +uint16_t KePortReadWord(uint16_t portNo); +void KePortWriteWord(uint16_t portNo, uint16_t data); +uint32_t KePortReadDword(uint16_t portNo); +void KePortWriteDword(uint16_t portNo, uint32_t data); + +#endif//NS64_PIO_H diff --git a/drivers/hali386/source/term.c b/drivers/hali386/source/term.c new file mode 100644 index 00000000..ab2dcc59 --- /dev/null +++ b/drivers/hali386/source/term.c @@ -0,0 +1,23 @@ +/*** + The Boron Operating System + Copyright (C) 2025 iProgramInCpp + +Module name: + ha/term.c + +Abstract: + This module implements the terminal functions for the i386 + platform. + +Author: + iProgramInCpp - 17 October 2025 +***/ +#include +#include +#include "hali.h" + +HAL_API void HalDisplayString(const char* Message) +{ + // TODO + DbgPrint("HalDisplayString(%zu): %s", strlen(Message), Message); +} diff --git a/drivers/hali386/source/timer.c b/drivers/hali386/source/timer.c new file mode 100644 index 00000000..694e6ca7 --- /dev/null +++ b/drivers/hali386/source/timer.c @@ -0,0 +1,68 @@ +/*** + The Boron Operating System + Copyright (C) 2025 iProgramInCpp + +Module name: + ha/timer.c + +Abstract: + This module contains the two initialization functions + (UP-init and MP-init) + +Author: + iProgramInCpp - 16 October 2025 +***/ +#include +#include "hali.h" + +#define TIMER_RELOAD_VALUE 4000 + +// This value is incremented by TIMER_RELOAD_VALUE on every timer interrupt. +static uint16_t PitTicksPassed; + +HAL_API uint64_t HalGetTickFrequency() +{ + return PIT_TICK_FREQUENCY; +} + +HAL_API uint64_t HalGetTickCount() +{ + // The PIT decrements from TIMER_RELOAD_VALUE + uint8_t Low, High; + Low = KePortReadByte(PIT_CHANNEL_0_PORT); + High = KePortReadByte(PIT_CHANNEL_0_PORT); + + uint16_t Timer = TIMER_RELOAD_VALUE - (Low | (High << 8)); + return PitTicksPassed + Timer; +} + +HAL_API bool HalUseOneShotIntTimer() +{ + return false; +} + +HAL_API uint64_t HalGetIntTimerFrequency() +{ + return PIT_TICK_FREQUENCY; +} + +HAL_API void HalRequestInterruptInTicks(UNUSED uint64_t Ticks) +{ + DbgPrint("HalRequestInterruptInTicks not implemented"); +} + +HAL_API uint64_t HalGetIntTimerDeltaTicks() +{ + return TIMER_RELOAD_VALUE; +} + +void HalUpdatePitClock() +{ + bool Restore = KeDisableInterrupts(); + + PitTicksPassed += TIMER_RELOAD_VALUE; + KePortWriteByte(PIT_CHANNEL_0_PORT, TIMER_RELOAD_VALUE & 0xFF); + KePortWriteByte(PIT_CHANNEL_0_PORT, TIMER_RELOAD_VALUE >> 8); + + KeRestoreInterrupts(Restore); +} diff --git a/drivers/halx86/source/apic.c b/drivers/halx86/source/apic.c index 85bc41c1..5fd78f4a 100644 --- a/drivers/halx86/source/apic.c +++ b/drivers/halx86/source/apic.c @@ -435,7 +435,7 @@ void HalInitApicMP() } -HAL_API void HalEndOfInterrupt() +HAL_API void HalEndOfInterrupt(UNUSED int InterruptNumber) { ApicEndOfInterrupt(); } diff --git a/drivers/halx86/source/apic.h b/drivers/halx86/source/apic.h index 13f1ac8d..ec474bba 100644 --- a/drivers/halx86/source/apic.h +++ b/drivers/halx86/source/apic.h @@ -15,8 +15,7 @@ Module name: #ifndef NS64_HAL_APIC_H #define NS64_HAL_APIC_H -void HalEndOfInterrupt(); - +void HalEndOfInterrupt(int InterruptNumber); void HalSendIpi(uint32_t Processor, int Vector); void HalBroadcastIpi(int Vector, bool IncludeSelf); void HalInitApicUP(); diff --git a/drivers/halx86/source/init.c b/drivers/halx86/source/init.c index abefb347..c3ba1691 100644 --- a/drivers/halx86/source/init.c +++ b/drivers/halx86/source/init.c @@ -21,7 +21,7 @@ Module name: #include "ioapic.h" #include "pci.h" -void HalEndOfInterrupt(); +void HalEndOfInterrupt(int InterruptNumber); void HalRequestInterruptInTicks(uint64_t Ticks); void HalRequestIpi(uint32_t LapicId, uint32_t Flags, int Vector); void HalInitSystemUP(); diff --git a/limine.i386.cfg b/limine.i386.cfg index b1ffb7c4..df56523d 100644 --- a/limine.i386.cfg +++ b/limine.i386.cfg @@ -6,12 +6,23 @@ VERBOSE=yes KERNEL_PATH=boot:///kernel.elf CMDLINE=/noinit - #MODULE_PATH=boot:///hali386.sys + MODULE_PATH=boot:///hali386.sys + MODULE_STRING=hali386.sys + #MODULE_PATH=boot:///framebuf.sys - MODULE_PATH=boot:///i8042prt.sys + #MODULE_STRING=framebuf.sys + + #MODULE_PATH=boot:///i8042prt.sys + #MODULE_STRING=i8042prt.sys + #MODULE_PATH=boot:///stornvme.sys + #MODULE_STRING=stornvme.sys + MODULE_PATH=boot:///ext2fs.sys + MODULE_STRING=ext2fs.sys + MODULE_PATH=boot:///test.sys + MODULE_STRING=test.sys #MODULE_PATH=boot:///libboron.so #MODULE_PATH=boot:///libtest.so From a291dcaa6ea1805e49bc1cafea6b369bab970495 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Fri, 17 Oct 2025 15:59:33 +0300 Subject: [PATCH 16/44] * X86: Now boots to the framebuffer terminal! Still a massive WIP behind the scenes though. --- boron/Makefile | 4 +- boron/include/mm/pool.h | 2 +- boron/source/build_number | 2 +- boron/source/ke/i386/boot.c | 31 ++++- boron/source/ke/i386/cpu.c | 1 - boron/source/ke/i386/mboot.h | 25 +++- boron/source/ke/smp.c | 6 +- boron/source/mm/pool.c | 12 +- drivers/CommonMakefile | 12 +- drivers/hali386/source/flanterm/backends/fb.c | 1 + drivers/hali386/source/flanterm/backends/fb.h | 1 + drivers/hali386/source/flanterm/flanterm.c | 1 + drivers/hali386/source/flanterm/flanterm.h | 1 + drivers/hali386/source/font.h | 1 + drivers/hali386/source/hali.h | 28 +++- drivers/hali386/source/init.c | 2 + drivers/hali386/source/pic.c | 35 ++++- drivers/hali386/source/term.c | 120 +++++++++++++++++- limine.i386.cfg | 8 +- user/CommonMakefile | 4 +- 20 files changed, 253 insertions(+), 44 deletions(-) create mode 120000 drivers/hali386/source/flanterm/backends/fb.c create mode 120000 drivers/hali386/source/flanterm/backends/fb.h create mode 120000 drivers/hali386/source/flanterm/flanterm.c create mode 120000 drivers/hali386/source/flanterm/flanterm.h create mode 120000 drivers/hali386/source/font.h diff --git a/boron/Makefile b/boron/Makefile index 30196d03..9e8a65fe 100644 --- a/boron/Makefile +++ b/boron/Makefile @@ -83,10 +83,10 @@ ifeq ($(SMP), yes) endif # User controllable CFLAGS. -CFLAGS ?= -pipe -Wall -Wextra -DTARGET_$(TARGET) -DKERNEL -DIS_KERNEL_MODE $(DEFINES) +CFLAGS ?= -pipe -Wall -Wextra -DTARGET_$(TARGET) -DBORON_TARGET=\"$(TARGETL)\" -DKERNEL -DIS_KERNEL_MODE $(DEFINES) # User controllable CXXFLAGS. -CXXFLAGS ?= -pipe -Wall -Wextra -DTARGET_$(TARGET) -DKERNEL -DIS_KERNEL_MODE $(DEFINES) +CXXFLAGS ?= -pipe -Wall -Wextra -DTARGET_$(TARGET) -DBORON_TARGET=\"$(TARGETL)\" -DKERNEL -DIS_KERNEL_MODE $(DEFINES) # User controllable preprocessor flags. CPPFLAGS ?= -I $(INC_DIR) -I $(DDK_DIR) diff --git a/boron/include/mm/pool.h b/boron/include/mm/pool.h index 41768954..fc305ba5 100644 --- a/boron/include/mm/pool.h +++ b/boron/include/mm/pool.h @@ -43,7 +43,7 @@ size_t MmGetSizeFromPoolAddress(void* Address); // simply call MmFreePoolBig. This function is thread-safe. // // The PermissionsAndCaching parameter is ORed onto the PTEs that will map this physical area. -void* MmMapIoSpace(uintptr_t PhysicalAddress, size_t NumberOfPages, uintptr_t PermissionsAndCaching, int Tag); +void* MmMapIoSpace(uintptr_t PhysicalAddress, size_t SizePages, uintptr_t PermissionsAndCaching, int Tag); // ******* Little Pool ******* // The little pool is a pool allocation system implemented on top diff --git a/boron/source/build_number b/boron/source/build_number index b4249c47..a700e799 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -239 +257 diff --git a/boron/source/ke/i386/boot.c b/boron/source/ke/i386/boot.c index 371d6356..4efde236 100644 --- a/boron/source/ke/i386/boot.c +++ b/boron/source/ke/i386/boot.c @@ -331,7 +331,7 @@ void KiInitLoaderParameterBlock() // Initialize the CPUs. Lpb->Multiprocessor.Count = 1; Lpb->Multiprocessor.List = &KiLoaderAp; - Lpb->Multiprocessor.BootstrapHardwareId = 1; + Lpb->Multiprocessor.BootstrapHardwareId = 0; KiLoaderAp.ProcessorId = 0; KiLoaderAp.HardwareId = 0; @@ -359,12 +359,41 @@ void KiInitLoaderParameterBlock() Fb->Width = KiMultibootInfo->framebuffer_width; Fb->Height = KiMultibootInfo->framebuffer_height; Fb->BitDepth = KiMultibootInfo->framebuffer_bpp; + + // @BROKEN: Limine RedMaskSize = KiMultibootInfo->u2.framebuffer_red_mask_size; Fb->RedMaskShift = KiMultibootInfo->u2.framebuffer_red_field_position; Fb->GreenMaskSize = KiMultibootInfo->u2.framebuffer_green_mask_size; Fb->GreenMaskShift = KiMultibootInfo->u2.framebuffer_green_field_position; Fb->BlueMaskSize = KiMultibootInfo->u2.framebuffer_blue_mask_size; Fb->BlueMaskShift = KiMultibootInfo->u2.framebuffer_blue_field_position; + + // Quirk detection: If the values make no sense, believe this is Limine v10.x (or + // earlier) loading us. + if (Fb->RedMaskShift == Fb->GreenMaskShift || + Fb->RedMaskShift == Fb->BlueMaskShift || + Fb->GreenMaskShift == Fb->BlueMaskShift || + !Fb->RedMaskSize || + !Fb->GreenMaskSize || + !Fb->BlueMaskShift) + { + DbgPrint("Limine v10.x (or earlier) booted us, so correcting color information"); + Fb->RedMaskSize = KiMultibootInfo->framebuffer_colorinfo_b; + Fb->RedMaskShift = KiMultibootInfo->framebuffer_colorinfo_a; + Fb->GreenMaskSize = KiMultibootInfo->u2.framebuffer_red_mask_size; + Fb->GreenMaskShift = KiMultibootInfo->u2.framebuffer_red_field_position; + Fb->BlueMaskSize = KiMultibootInfo->u2.framebuffer_green_mask_size; + Fb->BlueMaskShift = KiMultibootInfo->u2.framebuffer_green_field_position; + } } else { diff --git a/boron/source/ke/i386/cpu.c b/boron/source/ke/i386/cpu.c index 6104e588..74befe17 100644 --- a/boron/source/ke/i386/cpu.c +++ b/boron/source/ke/i386/cpu.c @@ -66,7 +66,6 @@ static void* KiCPUPointer; void KeSetCPUPointer(void* pGS) { - DbgPrint("KeSetCPUPointer: %p", pGS); KiCPUPointer = pGS; } diff --git a/boron/source/ke/i386/mboot.h b/boron/source/ke/i386/mboot.h index e9a35cc0..4c15cd2d 100644 --- a/boron/source/ke/i386/mboot.h +++ b/boron/source/ke/i386/mboot.h @@ -203,18 +203,31 @@ struct multiboot_info MultibootUInt32 framebuffer_height; MultibootUInt8 framebuffer_bpp; MultibootUInt8 framebuffer_type; + + // @BROKEN: The Multiboot specification mentions that the color_info + // struct should begin at offset 110, which is not divisible by 4. + // + // However, the specification's *own definition*, as well as GRUB's, + // places all of these fields in a union that will inevitably get + // 4 byte alignment due to the framebuffer_palette_addr member. + // + // Meanwhile, Limine respects the *offset* of 110, and places color + // information there. So, we'll have to do some quirk detection. + MultibootUInt8 framebuffer_colorinfo_a; + MultibootUInt8 framebuffer_colorinfo_b; + union { struct { MultibootUInt32 framebuffer_palette_addr; MultibootUInt16 framebuffer_palette_num_colors; }; struct { - MultibootUInt8 framebuffer_red_field_position ; - MultibootUInt8 framebuffer_red_mask_size ; - MultibootUInt8 framebuffer_green_field_position ; - MultibootUInt8 framebuffer_green_mask_size ; - MultibootUInt8 framebuffer_blue_field_position ; - MultibootUInt8 framebuffer_blue_mask_size ; + MultibootUInt8 framebuffer_red_field_position; + MultibootUInt8 framebuffer_red_mask_size; + MultibootUInt8 framebuffer_green_field_position; + MultibootUInt8 framebuffer_green_mask_size; + MultibootUInt8 framebuffer_blue_field_position; + MultibootUInt8 framebuffer_blue_mask_size; }; } u2; }; diff --git a/boron/source/ke/smp.c b/boron/source/ke/smp.c index 03dcc0a5..cb967871 100644 --- a/boron/source/ke/smp.c +++ b/boron/source/ke/smp.c @@ -167,7 +167,7 @@ void KeInitSMP() #else #define UNI_OR_MULTI "Uni" #endif - + PLOADER_MP_INFO MpInfo = &KeLoaderParameterBlock.Multiprocessor; PLOADER_AP BspAp = NULL; @@ -225,8 +225,8 @@ void KeInitSMP() PsInitSystemProcess(); int VersionNumber = KeGetVersionNumber(); - LogMsg("Boron (TM), October 2025 - v%d.%d.%d", VER_MAJOR(VersionNumber), VER_MINOR(VersionNumber), VER_BUILD(VersionNumber)); - LogMsg("%u System Processors [%u Kb System Memory] %sProcessor Kernel", MpInfo->Count, MmTotalAvailablePages * PAGE_SIZE / 1024, UNI_OR_MULTI); + LogMsg("Boron (TM), October 2025 - v%d.%d.%d (%s)", VER_MAJOR(VersionNumber), VER_MINOR(VersionNumber), VER_BUILD(VersionNumber), BORON_TARGET); + LogMsg("%u System Processors [%u Kb System Memory] " UNI_OR_MULTI "Processor Kernel", MpInfo->Count, MmTotalAvailablePages * PAGE_SIZE / 1024); for (uint64_t i = 0; i < MpInfo->Count; i++) { diff --git a/boron/source/mm/pool.c b/boron/source/mm/pool.c index baa071eb..08fcc745 100644 --- a/boron/source/mm/pool.c +++ b/boron/source/mm/pool.c @@ -91,12 +91,14 @@ void MmFreePool(void* Pointer) MiSlabFree(Pointer); } -void* MmMapIoSpace(uintptr_t PhysicalAddress, size_t NumberOfPages, uintptr_t PermissionsAndCaching, int Tag) +void* MmMapIoSpace(uintptr_t PhysicalAddress, size_t Size, uintptr_t PermissionsAndCaching, int Tag) { if (Tag == 0) Tag = POOL_TAG("MMIS"); + size_t SizePages = (Size + PAGE_SIZE - 1) / PAGE_SIZE; + // Allocate some pool space. - void* Space = MmAllocatePoolBig(POOL_FLAG_CALLER_CONTROLLED, NumberOfPages, Tag); + void* Space = MmAllocatePoolBig(POOL_FLAG_CALLER_CONTROLLED, SizePages, Tag); uintptr_t VirtualAddress = (uintptr_t) Space; if (!Space) @@ -106,11 +108,11 @@ void* MmMapIoSpace(uintptr_t PhysicalAddress, size_t NumberOfPages, uintptr_t Pe HPAGEMAP PageMap = MiGetCurrentPageMap(); - for (size_t i = 0; i < NumberOfPages; i++, PhysicalAddress += PAGE_SIZE, VirtualAddress += PAGE_SIZE) + for (size_t i = 0; i < SizePages; i++, PhysicalAddress += PAGE_SIZE, VirtualAddress += PAGE_SIZE) { if (!MiMapPhysicalPage(PageMap, PhysicalAddress, VirtualAddress, PermissionsAndCaching)) { - NumberOfPages = i; + SizePages = i; goto Rollback; } } @@ -119,7 +121,7 @@ void* MmMapIoSpace(uintptr_t PhysicalAddress, size_t NumberOfPages, uintptr_t Pe return Space; Rollback: - MiUnmapPages(PageMap, (uintptr_t) Space, NumberOfPages); + MiUnmapPages(PageMap, (uintptr_t) Space, SizePages); MmUnlockKernelSpace(); MmFreePoolBig(Space); return NULL; diff --git a/drivers/CommonMakefile b/drivers/CommonMakefile index ce66ab6e..cacd5cfc 100644 --- a/drivers/CommonMakefile +++ b/drivers/CommonMakefile @@ -53,10 +53,10 @@ endif include ../../tools/toolchain.mk # User controllable CFLAGS. -CFLAGS ?= $(OPT) -pipe -Wall -Wextra -DTARGET_$(TARGET) -DIS_KERNEL_MODE $(DEFINES) $(USER_DEFINES) +CFLAGS ?= $(OPT) -pipe -Wall -Wextra -DTARGET_$(TARGET) -DBORON_TARGET=\"$(TARGETL)\" -DIS_KERNEL_MODE $(DEFINES) $(USER_DEFINES) # User controllable CXXFLAGS. -CXXFLAGS ?= $(OPT) -pipe -Wall -Wextra -DTARGET_$(TARGET) -DIS_KERNEL_MODE $(DEFINES) $(USER_DEFINES) +CXXFLAGS ?= $(OPT) -pipe -Wall -Wextra -DTARGET_$(TARGET) -DBORON_TARGET=\"$(TARGETL)\" -DIS_KERNEL_MODE $(DEFINES) $(USER_DEFINES) # User controllable preprocessor flags. CPPFLAGS ?= -I $(INC_DIR) -I $(KE_DIR) -I $(DDK_DIR) @@ -112,10 +112,10 @@ LDFLAGS += \ NASMFLAGS += $(ARCH_ASFLAGS) # Use find to glob all *.c, *.S, and *.asm files in the directory and extract the object names. -override CFILES := $(shell find $(SRC_DIR) -not -path '*/.*' -type f -name '*.c') -override CXXFILES := $(shell find $(SRC_DIR) -not -path '*/.*' -type f -name '*.cpp') -override ASFILES := $(shell find $(SRC_DIR) -not -path '*/.*' -type f -name '*.S') -override NASMFILES := $(shell find $(SRC_DIR) -not -path '*/.*' -type f -name '*.asm') +override CFILES := $(shell find -L $(SRC_DIR) -not -path '*/.*' -type f -name '*.c') +override CXXFILES := $(shell find -L $(SRC_DIR) -not -path '*/.*' -type f -name '*.cpp') +override ASFILES := $(shell find -L $(SRC_DIR) -not -path '*/.*' -type f -name '*.S') +override NASMFILES := $(shell find -L $(SRC_DIR) -not -path '*/.*' -type f -name '*.asm') override OBJ := $(patsubst %.o,%.$(TARGETL).o,$(patsubst $(SRC_DIR)/%,$(BUILD_DIR)/%,$(CFILES:.c=.o) $(CXXFILES:.cpp=.o) $(ASFILES:.S=.o) $(NASMFILES:.asm=.o))) override HEADER_DEPS := $(patsubst %.o,%.d,$(OBJ)) diff --git a/drivers/hali386/source/flanterm/backends/fb.c b/drivers/hali386/source/flanterm/backends/fb.c new file mode 120000 index 00000000..27ed2932 --- /dev/null +++ b/drivers/hali386/source/flanterm/backends/fb.c @@ -0,0 +1 @@ +../../../../halx86/source/flanterm/backends/fb.c \ No newline at end of file diff --git a/drivers/hali386/source/flanterm/backends/fb.h b/drivers/hali386/source/flanterm/backends/fb.h new file mode 120000 index 00000000..e6425e4f --- /dev/null +++ b/drivers/hali386/source/flanterm/backends/fb.h @@ -0,0 +1 @@ +../../../../halx86/source/flanterm/backends/fb.h \ No newline at end of file diff --git a/drivers/hali386/source/flanterm/flanterm.c b/drivers/hali386/source/flanterm/flanterm.c new file mode 120000 index 00000000..91ceed44 --- /dev/null +++ b/drivers/hali386/source/flanterm/flanterm.c @@ -0,0 +1 @@ +../../../halx86/source/flanterm/flanterm.c \ No newline at end of file diff --git a/drivers/hali386/source/flanterm/flanterm.h b/drivers/hali386/source/flanterm/flanterm.h new file mode 120000 index 00000000..d1e636d5 --- /dev/null +++ b/drivers/hali386/source/flanterm/flanterm.h @@ -0,0 +1 @@ +../../../halx86/source/flanterm/flanterm.h \ No newline at end of file diff --git a/drivers/hali386/source/font.h b/drivers/hali386/source/font.h new file mode 120000 index 00000000..329925a8 --- /dev/null +++ b/drivers/hali386/source/font.h @@ -0,0 +1 @@ +../../halx86/source/font.h \ No newline at end of file diff --git a/drivers/hali386/source/hali.h b/drivers/hali386/source/hali.h index 50a4cee0..ee8be32f 100644 --- a/drivers/hali386/source/hali.h +++ b/drivers/hali386/source/hali.h @@ -4,15 +4,24 @@ #define HAL_API // specify calling convention here if needed -#define PIT_TICK_FREQUENCY (1193182) -#define PIT_CHANNEL_0_PORT (0x40) +#define PIT_TICK_FREQUENCY (1193182) +#define PIT_CHANNEL_0_PORT (0x40) -#define PIC_MAIN_COMMAND (0x20) -#define PIC_MAIN_DATA (0x21) -#define PIC_SUB_COMMAND (0xA0) -#define PIC_SUB_DATA (0xA1) +#define PIC_INTERRUPT_BASE (0x20) -#define PIC_CMD_EOI (0x20) +#define PIC1_COMMAND (0x20) +#define PIC1_DATA (0x21) +#define PIC2_COMMAND (0xA0) +#define PIC2_DATA (0xA1) + +#define PIC_CMD_EOI (0x20) + +// From the 8259A datasheet. +#define PIC_ICW1_ENABLE_ICW4 (1 << 0) +#define PIC_ICW1_INITIALIZE (1 << 4) // always 1 in the data sheet +#define PIC_ICW3_PRI_CONFIG (1 << 2) // there is a sub PIC and it's IRQ 2 +#define PIC_ICW3_SUB_CONFIG (2) // the sub PIC cascades at IRQ 2 +#define PIC_ICW4_8086_MODE (1 << 0) // ====== Timer ====== void HalUpdatePitClock(); @@ -29,5 +38,10 @@ HAL_API void HalRequestInterruptInTicks(uint64_t ticks); HAL_API uint64_t HalGetIntTimerDeltaTicks(); +// ====== Interrupt Controller ====== +HAL_API void HalInitPic(); + +HAL_API void HalEndOfInterrupt(int InterruptNumber); + diff --git a/drivers/hali386/source/init.c b/drivers/hali386/source/init.c index be73d773..26e539e0 100644 --- a/drivers/hali386/source/init.c +++ b/drivers/hali386/source/init.c @@ -45,6 +45,8 @@ static const HAL_VFTABLE HalpVfTable = // Initialize the HAL on the BSP, for all processors. HAL_API void HalInitSystemUP() { + HalInitPic(); + HalInitTerminal(); } // Initialize the HAL separately for each processor. diff --git a/drivers/hali386/source/pic.c b/drivers/hali386/source/pic.c index f3cfadaa..7fcaf709 100644 --- a/drivers/hali386/source/pic.c +++ b/drivers/hali386/source/pic.c @@ -14,15 +14,44 @@ Module name: #include #include "hali.h" +void KePortWriteByteWait(uint16_t Port, uint8_t Data) +{ + KePortWriteByte(Port, Data); + KeSpinningHint(); +} + HAL_API void HalRequestIpi(UNUSED uint32_t HardwareId, UNUSED uint32_t Flags, UNUSED int Vector) { DbgPrint("HalRequestIpi not implemented"); } -void HalEndOfInterrupt(int InterruptNumber) +HAL_API void HalEndOfInterrupt(int InterruptNumber) { if (InterruptNumber >= 0x28) - KePortWriteByte(PIC_SUB_COMMAND, PIC_CMD_EOI); + KePortWriteByte(PIC2_COMMAND, PIC_CMD_EOI); + + KePortWriteByte(PIC1_COMMAND, PIC_CMD_EOI); +} + +HAL_API void HalInitPic() +{ + // ICW1: Initialization Command + KePortWriteByteWait(PIC1_COMMAND, PIC_ICW1_INITIALIZE | PIC_ICW1_ENABLE_ICW4); + KePortWriteByteWait(PIC2_COMMAND, PIC_ICW1_INITIALIZE | PIC_ICW1_ENABLE_ICW4); + + // ICW2: Base offset of interrupts + KePortWriteByteWait(PIC1_DATA, PIC_INTERRUPT_BASE); + KePortWriteByteWait(PIC2_DATA, PIC_INTERRUPT_BASE + 8); + + // ICW3: Primary/secondary relationship + KePortWriteByteWait(PIC1_DATA, PIC_ICW3_PRI_CONFIG); + KePortWriteByteWait(PIC2_DATA, PIC_ICW3_SUB_CONFIG); + + // ICW4: Mode + KePortWriteByteWait(PIC1_DATA, PIC_ICW4_8086_MODE); + KePortWriteByteWait(PIC2_DATA, PIC_ICW4_8086_MODE); - KePortWriteByte(PIC_MAIN_COMMAND, PIC_CMD_EOI); + // Mask ALL interrupts + KePortWriteByteWait(PIC1_DATA, 0xFF); + KePortWriteByteWait(PIC2_DATA, 0xFF); } diff --git a/drivers/hali386/source/term.c b/drivers/hali386/source/term.c index ab2dcc59..3986ec50 100644 --- a/drivers/hali386/source/term.c +++ b/drivers/hali386/source/term.c @@ -15,9 +15,125 @@ Module name: #include #include #include "hali.h" +#include "flanterm/flanterm.h" +#include "flanterm/backends/fb.h" +#include "font.h" + +// NOTE: Initialization done on the BSP. So no need to sync anything +uint8_t* HalpTerminalMemory; +size_t HalpTerminalMemoryHead; +size_t HalpTerminalMemorySize; + +static struct flanterm_context* HalpTerminalContext; + +bool HalIsTerminalInitted() +{ + return HalpTerminalContext != NULL; +} + +static void* HalpTerminalMemAlloc(size_t sz) +{ + if (HalpTerminalMemoryHead + sz > HalpTerminalMemorySize) + { + DbgPrint("Error, running out of memory in the terminal heap"); + return NULL; + } + + uint8_t* pCurMem = &HalpTerminalMemory[HalpTerminalMemoryHead]; + HalpTerminalMemoryHead += sz; + return pCurMem; +} + +static void HalpTerminalFree(UNUSED void* pMem, UNUSED size_t sz) +{ +} + +bool HalpIsSerialAvailable; + +void HalInitTerminal() +{ + if (KeLoaderParameterBlock.FramebufferCount == 0) + KeCrashBeforeSMPInit("HAL: No framebuffers found"); + + PLOADER_FRAMEBUFFER Framebuffer = &KeLoaderParameterBlock.Framebuffers[0]; + uint32_t defaultBG = 0x0000007f; + uint32_t defaultFG = 0x00ffffff; + + // on a 1280x800 screen, the term will have a rez of 160x50 (8000 chars). + // 52 bytes per character. + + const int charWidth = 8, charHeight = 16; + int charScale = 1; + + int termBufWidth = Framebuffer->Width / charWidth / charScale; + int termBufHeight = Framebuffer->Height / charHeight / charScale; + + const int usagePerChar = 52; // I calculated it + const int fontBoolMemUsage = charWidth * charHeight * 256; // there are 256 chars + const int fontDataMemUsage = charWidth * charHeight * 256 / 8; + const int contextSize = sizeof(struct flanterm_fb_context); + + int totalMemUsage = contextSize + fontDataMemUsage + fontBoolMemUsage + termBufWidth * termBufHeight * usagePerChar; + size_t sizePages = (totalMemUsage + PAGE_SIZE - 1) / PAGE_SIZE; + + void* FramebufferMemory = MmMapIoSpace( + (uintptr_t)Framebuffer->Address, + Framebuffer->Pitch * Framebuffer->Height, + MM_PTE_READWRITE | MM_PTE_CDISABLE, + POOL_TAG("HAFB") + ); + + DbgPrint("Screen resolution: %d by %d. Will use %d Bytes. Framebuffer mapped at %p.", Framebuffer->Width, Framebuffer->Height, sizePages * PAGE_SIZE, FramebufferMemory); + + HalpTerminalMemory = MmAllocatePoolBig(POOL_FLAG_NON_PAGED, sizePages, POOL_TAG("Term")); + HalpTerminalMemoryHead = 0; + HalpTerminalMemorySize = sizePages * PAGE_SIZE; + + HalpTerminalContext = flanterm_fb_init( + &HalpTerminalMemAlloc, + &HalpTerminalFree, + FramebufferMemory, + Framebuffer->Width, + Framebuffer->Height, + Framebuffer->Pitch, + Framebuffer->RedMaskSize, Framebuffer->RedMaskShift, // red mask size and shift + Framebuffer->GreenMaskSize, Framebuffer->GreenMaskShift, // green mask size and shift + Framebuffer->BlueMaskSize, Framebuffer->BlueMaskShift, // blue mask size and shift + NULL, // canvas + NULL, // ansi colors + NULL, // ansi bright colors + &defaultBG, // default background + &defaultFG, // default foreground + NULL, // default background bright + NULL, // default fontground bright + HalpBuiltInFont, // font pointer + charWidth, // font width + charHeight, // font height + 0, // character spacing X + charScale, // character scale width + charScale, // character scale height + 0 // character spacing Y + ); + + if (!HalpTerminalContext) + { + KeCrashBeforeSMPInit("Error, no terminal context"); + } +} HAL_API void HalDisplayString(const char* Message) { - // TODO - DbgPrint("HalDisplayString(%zu): %s", strlen(Message), Message); + if (!HalpTerminalContext) + { + HalPrintStringDebug(Message); + return; + } + + size_t Length = strlen(Message); + + static KSPIN_LOCK SpinLock; + KIPL Ipl; + KeAcquireSpinLock(&SpinLock, &Ipl); + flanterm_write(HalpTerminalContext, Message, Length); + KeReleaseSpinLock(&SpinLock, Ipl); } diff --git a/limine.i386.cfg b/limine.i386.cfg index df56523d..6bebc0e5 100644 --- a/limine.i386.cfg +++ b/limine.i386.cfg @@ -18,11 +18,11 @@ VERBOSE=yes #MODULE_PATH=boot:///stornvme.sys #MODULE_STRING=stornvme.sys - MODULE_PATH=boot:///ext2fs.sys - MODULE_STRING=ext2fs.sys + #MODULE_PATH=boot:///ext2fs.sys + #MODULE_STRING=ext2fs.sys - MODULE_PATH=boot:///test.sys - MODULE_STRING=test.sys + #MODULE_PATH=boot:///test.sys + #MODULE_STRING=test.sys #MODULE_PATH=boot:///libboron.so #MODULE_PATH=boot:///libtest.so diff --git a/user/CommonMakefile b/user/CommonMakefile index 72a96ff7..bfc0fcef 100644 --- a/user/CommonMakefile +++ b/user/CommonMakefile @@ -68,11 +68,11 @@ $(eval $(call DEFAULT_VAR,LD,ld)) # User controllable CFLAGS. CFLAGS ?= -CFLAGS += $(OPT) -pipe -Wall -Wextra -DTARGET_$(TARGET) -DIS_USER_MODE $(DEFINES) $(USER_DEFINES) +CFLAGS += $(OPT) -pipe -Wall -Wextra -DTARGET_$(TARGET) -DBORON_TARGET=\"$(TARGETL)\" -DIS_USER_MODE $(DEFINES) $(USER_DEFINES) # User controllable CXXFLAGS. CXXFLAGS ?= -CXXFLAGS += $(OPT) -pipe -Wall -Wextra -DTARGET_$(TARGET) -DIS_USER_MODE $(DEFINES) $(USER_DEFINES) +CXXFLAGS += $(OPT) -pipe -Wall -Wextra -DTARGET_$(TARGET) -DBORON_TARGET=\"$(TARGETL)\" -DIS_USER_MODE $(DEFINES) $(USER_DEFINES) # User controllable preprocessor flags. CPPFLAGS ?= From 107350ebcae9d1f42bc5ec7be0af7e9ff2522fd8 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Fri, 17 Oct 2025 16:38:05 +0300 Subject: [PATCH 17/44] * Framebuf: Port to 32-bit --- Makefile | 1 + boron/source/build_number | 2 +- boron/source/ke/i386/boot.c | 1 - drivers/framebuf/source/main.c | 6 ++++++ limine.i386.cfg | 4 ++-- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 49be3769..fe420020 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,7 @@ DRIVERS_LIST = \ else ifeq ($(TARGET),I386) DRIVERS_LIST = \ hali386 \ + framebuf \ i8042prt \ ext2fs \ test diff --git a/boron/source/build_number b/boron/source/build_number index a700e799..4fc233b7 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -257 +261 diff --git a/boron/source/ke/i386/boot.c b/boron/source/ke/i386/boot.c index 4efde236..bbd7809b 100644 --- a/boron/source/ke/i386/boot.c +++ b/boron/source/ke/i386/boot.c @@ -317,7 +317,6 @@ void KiInitLoaderParameterBlock() Mod->Path = P2V(Module->cmdline); KiRemoveAreaFromMemMap(Module->mod_start, Module->mod_end - Module->mod_start); - Module++; } } diff --git a/drivers/framebuf/source/main.c b/drivers/framebuf/source/main.c index a77d1947..f546c105 100644 --- a/drivers/framebuf/source/main.c +++ b/drivers/framebuf/source/main.c @@ -173,7 +173,13 @@ BSTATUS CreateFrameBufferObject(int Index) snprintf(Name, sizeof Name, "FrameBuffer%d", Index); PLOADER_FRAMEBUFFER Framebuffer = &KeLoaderParameterBlock.Framebuffers[Index]; + +#ifdef IS_64_BIT FbAddress = MmGetHHDMOffsetFromAddr(Framebuffer->Address); +#else + FbAddress = (uintptr_t) Framebuffer->Address; +#endif + FbWidth = Framebuffer->Width; FbHeight = Framebuffer->Height; FbPitch = Framebuffer->Pitch; diff --git a/limine.i386.cfg b/limine.i386.cfg index 6bebc0e5..17cfce6d 100644 --- a/limine.i386.cfg +++ b/limine.i386.cfg @@ -9,8 +9,8 @@ VERBOSE=yes MODULE_PATH=boot:///hali386.sys MODULE_STRING=hali386.sys - #MODULE_PATH=boot:///framebuf.sys - #MODULE_STRING=framebuf.sys + MODULE_PATH=boot:///framebuf.sys + MODULE_STRING=framebuf.sys #MODULE_PATH=boot:///i8042prt.sys #MODULE_STRING=i8042prt.sys From 356029353d0192ff41f1ccd74ffc57fb07e86134 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Fri, 17 Oct 2025 18:23:34 +0300 Subject: [PATCH 18/44] * X86: More work --- boron/include/hal.h | 6 ++++++ boron/include/hal/init.h | 13 +++++++++++++ boron/source/build_number | 2 +- boron/source/hal/hal.c | 19 +++++++++++++++++++ boron/source/ke/i386/intobj.c | 11 ++++++++++- boron/source/ke/i386/ipl.c | 11 +++++------ drivers/hali386/source/hali.h | 6 ++++-- drivers/hali386/source/init.c | 2 ++ drivers/hali386/source/pic.c | 18 ++++++++++++++++++ drivers/i8042prt/source/main.c | 14 +++++++++++++- readme.md | 31 ++++++++++++++++++++++++------- 11 files changed, 115 insertions(+), 18 deletions(-) diff --git a/boron/include/hal.h b/boron/include/hal.h index 676b8f54..9588f27d 100644 --- a/boron/include/hal.h +++ b/boron/include/hal.h @@ -28,7 +28,13 @@ uint64_t HalGetIntTimerFrequency(); uint64_t HalGetTickCount(); uint64_t HalGetTickFrequency(); uint64_t HalGetIntTimerDeltaTicks(); +#ifdef TARGET_AMD64 void HalIoApicSetIrqRedirect(uint8_t Vector, uint8_t Irq, uint32_t LapicId, bool Status); +#endif +#ifdef TARGET_I386 +void HalPicRegisterInterrupt(uint8_t Vector, KIPL Ipl); +void HalPicDeregisterInterrupt(uint8_t Vector, KIPL Ipl); +#endif #ifdef IS_HAL void HalSetVftable(const HAL_VFTABLE* Table); diff --git a/boron/include/hal/init.h b/boron/include/hal/init.h index 9f81aad9..4454e2db 100644 --- a/boron/include/hal/init.h +++ b/boron/include/hal/init.h @@ -39,10 +39,19 @@ typedef uint64_t(*PFHAL_GET_INT_TIMER_DELTA_TICKS)(void); typedef void(*PFHAL_IOAPIC_SET_IRQ_REDIRECT)(uint8_t Vector, uint8_t Irq, uint32_t LapicId, bool Status); #endif +#ifdef TARGET_I386 +typedef void(*PFHAL_PIC_REGISTER_INTERRUPT)(uint8_t Vector, KIPL Ipl); +typedef void(*PFHAL_PIC_DEREGISTER_INTERRUPT)(uint8_t Vector, KIPL Ipl); +#endif + #if defined TARGET_AMD64 || defined TARGET_I386 #include "pci.h" #endif +#ifdef TARGET_I386 +#define PIC_INTERRUPT_BASE (0x20) +#endif + typedef struct { uint64_t Flags; @@ -64,6 +73,10 @@ typedef struct #ifdef TARGET_AMD64 PFHAL_IOAPIC_SET_IRQ_REDIRECT IoApicSetIrqRedirect; #endif +#ifdef TARGET_I386 + PFHAL_PIC_REGISTER_INTERRUPT PicRegisterInterrupt; + PFHAL_PIC_DEREGISTER_INTERRUPT PicDeregisterInterrupt; +#endif #if defined TARGET_AMD64 || defined TARGET_I386 PFHAL_PCI_ENUMERATE PciEnumerate; PFHAL_PCI_CONFIG_READ_DWORD PciConfigReadDword; diff --git a/boron/source/build_number b/boron/source/build_number index 4fc233b7..10b0c0db 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -261 +264 diff --git a/boron/source/hal/hal.c b/boron/source/hal/hal.c index 2430f45d..d41f9803 100644 --- a/boron/source/hal/hal.c +++ b/boron/source/hal/hal.c @@ -98,11 +98,30 @@ uint64_t HalGetIntTimerDeltaTicks() } #ifdef TARGET_AMD64 + void HalIoApicSetIrqRedirect(uint8_t Vector, uint8_t Irq, uint32_t LapicId, bool Status) { return HalpVftable.IoApicSetIrqRedirect(Vector, Irq, LapicId, Status); } +#endif // TARGET_AMD64 + +#ifdef TARGET_I386 + +void HalPicRegisterInterrupt(uint8_t Vector, KIPL Ipl) +{ + HalpVftable.PicDeregisterInterrupt(Vector, Ipl); +} + +void HalPicDeregisterInterrupt(uint8_t Vector, KIPL Ipl) +{ + HalpVftable.PicDeregisterInterrupt(Vector, Ipl); +} + +#endif // TARGET_I386 + +#if defined TARGET_AMD64 || defined TARGET_I386 + BSTATUS HalPciEnumerate( bool LookUpByIds, diff --git a/boron/source/ke/i386/intobj.c b/boron/source/ke/i386/intobj.c index 76e67441..c6cf9c20 100644 --- a/boron/source/ke/i386/intobj.c +++ b/boron/source/ke/i386/intobj.c @@ -79,7 +79,9 @@ void KeInitializeInterrupt( KIPL InterruptIpl, bool SharedVector) { - ASSERT((Vector >> 4) == InterruptIpl && "The interrupt vector must match its IPL on AMD64."); + if (Vector < PIC_INTERRUPT_BASE && Vector >= PIC_INTERRUPT_BASE + 16) + DbgPrint("WARNING: KeInitializeInterrupt -- interrupt vector %d may not be called", Vector); + ASSERT(InterruptIpl > IPL_DPC && "The caller may not override this IPL"); ASSERT(InterruptIpl < IPL_CLOCK && "The caller may not override this IPL"); @@ -115,6 +117,10 @@ bool KeConnectInterrupt(PKINTERRUPT Interrupt) return false; } } + else + { + HalPicRegisterInterrupt(Interrupt->Vector, Interrupt->Ipl); + } // Connect the interrupt now. InsertTailList(&InterruptList->List, &Interrupt->Entry); @@ -143,6 +149,9 @@ void KeDisconnectInterrupt(PKINTERRUPT Interrupt) RemoveEntryList(&Interrupt->Entry); Interrupt->Connected = false; + if (IsListEmpty(&InterruptList->List)) + HalPicDeregisterInterrupt(Interrupt->Vector, Interrupt->Ipl); + KeReleaseSpinLock(&InterruptList->Lock, IplUnused); KeLowerIPL(Ipl); } diff --git a/boron/source/ke/i386/ipl.c b/boron/source/ke/i386/ipl.c index e7011131..b28b90f4 100644 --- a/boron/source/ke/i386/ipl.c +++ b/boron/source/ke/i386/ipl.c @@ -16,10 +16,9 @@ Module name: void KeOnUpdateIPL(KIPL NewIpl, KIPL OldIpl) { - // TODO: lazy IPL - only program the interrupt controller if needed - - if (NewIpl == OldIpl) - return; - - // TODO: placeholder method here + // Well, there's nothing here, since this is useless. + // The beautiful code is in traps.c, dealing with hardware + // interrupts. + (void) NewIpl; + (void) OldIpl; } diff --git a/drivers/hali386/source/hali.h b/drivers/hali386/source/hali.h index ee8be32f..2a01816c 100644 --- a/drivers/hali386/source/hali.h +++ b/drivers/hali386/source/hali.h @@ -7,8 +7,6 @@ #define PIT_TICK_FREQUENCY (1193182) #define PIT_CHANNEL_0_PORT (0x40) -#define PIC_INTERRUPT_BASE (0x20) - #define PIC1_COMMAND (0x20) #define PIC1_DATA (0x21) #define PIC2_COMMAND (0xA0) @@ -43,5 +41,9 @@ HAL_API void HalInitPic(); HAL_API void HalEndOfInterrupt(int InterruptNumber); +HAL_API void HalRegisterInterrupt(uint8_t Vector, KIPL Ipl); + +HAL_API void HalDeregisterInterrupt(uint8_t Vector, KIPL Ipl); + diff --git a/drivers/hali386/source/init.c b/drivers/hali386/source/init.c index 26e539e0..097aa9ff 100644 --- a/drivers/hali386/source/init.c +++ b/drivers/hali386/source/init.c @@ -31,6 +31,8 @@ static const HAL_VFTABLE HalpVfTable = .GetTickCount = HalGetTickCount, .GetTickFrequency = HalGetTickFrequency, .GetIntTimerDeltaTicks = HalGetIntTimerDeltaTicks, + .PicRegisterInterrupt = HalPicRegisterInterrupt, + .PicDeregisterInterrupt = HalPicDeregisterInterrupt, .PciEnumerate = HalPciEnumerate, .PciConfigReadDword = HalPciConfigReadDword, .PciConfigReadWord = HalPciConfigReadWord, diff --git a/drivers/hali386/source/pic.c b/drivers/hali386/source/pic.c index 7fcaf709..280a0419 100644 --- a/drivers/hali386/source/pic.c +++ b/drivers/hali386/source/pic.c @@ -14,6 +14,10 @@ Module name: #include #include "hali.h" +uint16_t KiIplTable[] = { + +}; + void KePortWriteByteWait(uint16_t Port, uint8_t Data) { KePortWriteByte(Port, Data); @@ -33,6 +37,20 @@ HAL_API void HalEndOfInterrupt(int InterruptNumber) KePortWriteByte(PIC1_COMMAND, PIC_CMD_EOI); } +HAL_API void HalRegisterInterrupt(uint8_t Vector, KIPL Ipl) +{ + // TODO + (void) Vector; + (void) Ipl; +} + +HAL_API void HalDeregisterInterrupt(uint8_t Vector, KIPL Ipl) +{ + // TODO + (void) Vector; + (void) Ipl; +} + HAL_API void HalInitPic() { // ICW1: Initialization Command diff --git a/drivers/i8042prt/source/main.c b/drivers/i8042prt/source/main.c index f1a37c3f..775f9f06 100644 --- a/drivers/i8042prt/source/main.c +++ b/drivers/i8042prt/source/main.c @@ -65,6 +65,8 @@ BSTATUS InitializeDevice() (void) 0; // Allocate an interrupt for the keyboard. +#ifdef TARGET_AMD64 + KIPL IplKbd, IplMou; int VectorKbd = AllocateVector(&IplKbd, IPL_DEVICES0); int VectorMou = AllocateVector(&IplMou, IPL_DEVICES0); @@ -76,12 +78,22 @@ BSTATUS InitializeDevice() // Ok, now set the IRQ redirects. HalIoApicSetIrqRedirect(VectorKbd, I8042_IRQ_KBD, LapicId, true); HalIoApicSetIrqRedirect(VectorMou, I8042_IRQ_MOU, LapicId, true); + +#elif defined TARGET_I386 + + const KIPL IplKbd = IPL_DEVICES0; + const int VectorKbd = PIC_INTERRUPT_BASE; + + bool Restore = KeDisableInterrupts(); + +#else +#error If you're using the i8042prt driver for another architecture, please define the interrupt method for it. +#endif // Initialize the keyboard. KbdInitialize(VectorKbd, IplKbd); KeRestoreInterrupts(Restore); - return STATUS_SUCCESS; } diff --git a/readme.md b/readme.md index b984936d..4325cf7b 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,7 @@ Boron is a 64-bit operating system designed with SMP in mind. It borrows heavily from Windows NT, but does not aim to be a total clone. -Note! Boron is currently not even in a minimally usable state. Don't expect it to do anything. +NOTE: Boron is currently not even in a minimally usable state. Don't expect it to do anything. This project is licensed under the three clause BSD license, **except the following**: - Flanterm (drivers/halx86/source/flanterm): https://github.com/mintsuki/flanterm - Licensed under the 2 clause BSD license @@ -21,6 +21,21 @@ Currently this uses Limine v7.x. * The chemical element with the same name has the atomic number of 5. Coincidentally this is also my 5th operating system project. (three of them flopped, and the other one that didn't is [NanoShell](https://github.com/iProgramMC/NanoShellOS)) +### Supported platforms + +Currently, Boron supports running on: +- x86_64 +- x86 (32-bit) + +Note: The supporting code for 32-bit x86 is called `i386`, but there is a long way to actually running on the original 80386.) + +### Planned ports + +Boron is planned to be ported to: +- PowerPC (32-bit) +- ARMv6 (32-bit) +- AArch64 (64-bit) + ## Building In a terminal, run the following commands: ``` @@ -42,6 +57,8 @@ Currently, the OS's source is structured into the following: * `drivers/` - The actual drivers themselves. +* `user/` - Boron's native userspace distribution lives here. + Kernel DLL exports will use the prefix `OS`. I know this isn't the chemical symbol for boron (that being B), but it is what it is. @@ -78,7 +95,7 @@ all present related to it. * [x] Reclamation of pages occupied by initialization code * [x] Capture of physical regions for extended use * [x] Mapping and unmapping anonymous memory - * [ ] File backed memory + * [x] File backed memory * [ ] Swap file support * [ ] Swap out page tables * [ ] Swap out kernel code @@ -100,7 +117,7 @@ all present related to it. * [x] Dynamic linked library loader (`Ldr`) * [x] HAL separate from kernel * [x] Load additional kernel side modules - * [ ] Map the Boron system service DLL (BoronDLL.dll) + * [x] Map the Boron system service DLL (libboron.so) * [ ] File system manager * [ ] ... @@ -111,10 +128,10 @@ all present related to it. * [ ] Security subsystem (later) * [ ] ... -* [ ] Boron DLL / Librarian / System service handler - * [ ] Load fixed modules - * [ ] Load relocatable modules - * [ ] Link modules with each other by resolving undefined dependencies +* [x] Boron DLL / Librarian / System service handler + * [x] Load fixed modules + * [x] Load relocatable modules + * [x] Link modules with each other by resolving undefined dependencies * [ ] User space * [ ] Command line shell From 84de01b6c90665e7fa1ae5189e8f7b913b0c562a Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Fri, 17 Oct 2025 20:43:14 +0300 Subject: [PATCH 19/44] * X86: Even more work with interrupts and the PIT! --- boron/include/arch/i386.h | 18 ++-- boron/include/ke/irq.h | 10 +++ boron/linker.i386.ld | 2 +- boron/source/build_number | 2 +- boron/source/ke/crash.c | 5 ++ boron/source/ke/i386/boot.c | 4 +- boron/source/ke/i386/debug.c | 3 +- boron/source/ke/i386/ipl.c | 24 ------ boron/source/ke/i386/trap.asm | 137 ++++++++++++++++-------------- boron/source/ke/i386/traplist.asm | 2 +- boron/source/ke/i386/traps.c | 71 ++++++++++------ drivers/hali386/source/hali.h | 10 +++ drivers/hali386/source/init.c | 1 + drivers/hali386/source/pic.c | 59 +++++++++++-- drivers/hali386/source/timer.c | 39 +++++++-- drivers/i8042prt/source/main.c | 8 +- 16 files changed, 250 insertions(+), 145 deletions(-) delete mode 100644 boron/source/ke/i386/ipl.c diff --git a/boron/include/arch/i386.h b/boron/include/arch/i386.h index c6828392..e3588b1e 100644 --- a/boron/include/arch/i386.h +++ b/boron/include/arch/i386.h @@ -149,19 +149,14 @@ typedef uint32_t MMPTE, *PMMPTE; #define MSR_IA32_EFER_LMA (1 << 10) // Long Mode active #define MSR_IA32_EFER_NXE (1 << 11) // No Execute enable +// Registers pushed by KiTrapCommon. Pushed in reverse order from how they're laid out. struct KREGISTERS_tag { - // Old IPL - uint32_t OldIpl; - + uint32_t Cr2, Edi, Esi, Ebx; uint32_t Ebp; uint32_t Sfra; // stack frame return address - - // Registers pushed by KiTrapCommon. Pushed in reverse order from how they're laid out. - uint32_t Cr2; - uint32_t Edi, Esi; - uint32_t CsDupl; - uint32_t Edx, Ecx, Ebx, Eax; + uint32_t OldIpl; + uint32_t Edx, Ecx, Eax; // Registers pushed by each trap handler uint32_t IntNumber; @@ -284,10 +279,15 @@ typedef struct } KGDT; +#define KE_MAX_QUEUED_INTERRUPTS 32 + typedef struct { KGDT Gdt; KTSS Tss; + + uint8_t InterruptQueue[KE_MAX_QUEUED_INTERRUPTS]; + uint8_t InterruptQueuePlace; } KARCH_DATA, *PKARCH_DATA; diff --git a/boron/include/ke/irq.h b/boron/include/ke/irq.h index 8f4e80f3..43a2942b 100644 --- a/boron/include/ke/irq.h +++ b/boron/include/ke/irq.h @@ -19,9 +19,19 @@ Module name: #include #include +#ifdef TARGET_AMD64 + // Allocate an interrupt vector for the specified IPL. int KeAllocateInterruptVector(KIPL Ipl); +#endif + +#ifdef TARGET_I386 + +#define SYSTEM_IRQ(irqNo) (PIC_INTERRUPT_BASE + (irqNo)) + +#endif + // // WARNING! // diff --git a/boron/linker.i386.ld b/boron/linker.i386.ld index 66e6ad17..510ddfae 100644 --- a/boron/linker.i386.ld +++ b/boron/linker.i386.ld @@ -94,7 +94,7 @@ SECTIONS PROVIDE(PsSystemProcess = PspSystemProcessObject + 64); } :data - KiKernelEnd = . - 0xC0000000; + KiKernelEnd = .; /* Discard .note.* and .eh_frame since they may cause issues on some hosts. */ /DISCARD/ : { diff --git a/boron/source/build_number b/boron/source/build_number index 10b0c0db..1473a88f 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -264 +281 diff --git a/boron/source/ke/crash.c b/boron/source/ke/crash.c index 7f2323fa..761e3b65 100644 --- a/boron/source/ke/crash.c +++ b/boron/source/ke/crash.c @@ -58,7 +58,12 @@ void KeCrashConclusion(const char* Message) #endif // List each loaded DLL's base +#ifdef IS_64_BIT LogMsg("Dll Base Name"); +#else + LogMsg("Dll Base Name"); +#endif + for (int i = 0; i < KeLoadedDLLCount; i++) { PLOADED_DLL LoadedDll = &KeLoadedDLLs[i]; diff --git a/boron/source/ke/i386/boot.c b/boron/source/ke/i386/boot.c index bbd7809b..97a0eb16 100644 --- a/boron/source/ke/i386/boot.c +++ b/boron/source/ke/i386/boot.c @@ -284,13 +284,13 @@ void KiInitLoaderParameterBlock() // Initialize the memory regions. KiInitializeMemoryRegions(); - KiRemoveAreaFromMemMap(0x100000, (size_t) KiKernelEnd - 0x100000); + KiRemoveAreaFromMemMap(0x100000, (size_t) KiKernelEnd - 0xC0100000); // Initialize the kernel module. Lpb->ModuleInfo.Kernel.Path = "kernel.elf"; Lpb->ModuleInfo.Kernel.String = P2V(KiMultibootInfo->cmdline); Lpb->ModuleInfo.Kernel.Address = (void*) 0xC0100000; // TODO: is the whole kernel (+ELF stuff) loaded here?? - Lpb->ModuleInfo.Kernel.Size = (size_t) KiKernelEnd - 0x100000; + Lpb->ModuleInfo.Kernel.Size = (size_t) KiKernelEnd - 0xC0100000; // Initialize the other modules. if (KiMultibootInfo->flags & MULTIBOOT_INFO_MODS) diff --git a/boron/source/ke/i386/debug.c b/boron/source/ke/i386/debug.c index de7ba9ce..3291a5d8 100644 --- a/boron/source/ke/i386/debug.c +++ b/boron/source/ke/i386/debug.c @@ -20,6 +20,7 @@ Module name: #include #define KERNEL_IMAGE_BASE (0xC0100000) +extern char KiKernelEnd[]; #ifdef DEBUG @@ -57,7 +58,7 @@ static void DbgResolveAddress(uintptr_t Address, char *SymbolName, size_t Buffer uintptr_t BaseAddress = 0; // Determine where that address came from. - if (Address >= KERNEL_IMAGE_BASE) + if ((uintptr_t) KiKernelEnd > Address && Address >= KERNEL_IMAGE_BASE) { // Easy, it's in the kernel. // Determine the symbol's name diff --git a/boron/source/ke/i386/ipl.c b/boron/source/ke/i386/ipl.c deleted file mode 100644 index b28b90f4..00000000 --- a/boron/source/ke/i386/ipl.c +++ /dev/null @@ -1,24 +0,0 @@ -/*** - The Boron Operating System - Copyright (C) 2025 iProgramInCpp - -Module name: - ke/i386/ipl.c - -Abstract: - This header file implements support for the interrupt - priority system. - -Author: - iProgramInCpp - 14 October 2025 -***/ -#include - -void KeOnUpdateIPL(KIPL NewIpl, KIPL OldIpl) -{ - // Well, there's nothing here, since this is useless. - // The beautiful code is in traps.c, dealing with hardware - // interrupts. - (void) NewIpl; - (void) OldIpl; -} diff --git a/boron/source/ke/i386/trap.asm b/boron/source/ke/i386/trap.asm index e42f68b4..7fbe2334 100644 --- a/boron/source/ke/i386/trap.asm +++ b/boron/source/ke/i386/trap.asm @@ -23,9 +23,9 @@ global KiTrapCallList section .text -; int KiEnterHardwareInterrupt(int IntNo); -extern KiEnterHardwareInterrupt -; void KiExitHardwareInterrupt(int OldIpl); +; int KiTryEnterHardwareInterrupt(int IntNum); +extern KiTryEnterHardwareInterrupt +; void KiExitHardwareInterrupt(PKREGISTERS Registers); extern KiExitHardwareInterrupt ; Return Value: @@ -50,76 +50,71 @@ KeRestoreInterrupts: .ret: ret ; done -; Push the entire state except EAX, EBX, ECX ,and EDX -%macro PUSH_STATE 0 - push esi - push edi - mov eax, cr2 - push eax -%endmacro - -; Pop the entire state except EAX, EBX, ECX, and EDX -%macro POP_STATE 0 - add esp, 4 ; the space occupied by the cr2 register - pop edi - pop esi -%endmacro - global KiTrapCommon KiTrapCommon: push eax - push ebx push ecx push edx - lea ebx, [esp + 16] ; Get the pointer to the value after rbx and rax on the stack - lea ecx, [esp + 24] ; Get the pointer to the EIP from the interrupt frame. - lea edx, [esp + 28] ; Get the pointer to the CS from the interrupt frame. - mov eax, [edx] ; Load the CS. + + ; Check if we can enter the interrupt handler, based on the + ; current interrupt number. This interrupt number has an + ; IPL associated with it; if it's lower than the current one, + ; this function returns -1. + mov eax, [esp + 12] push eax - ; Note that LEA doesn't actually perform any memory accesses, all it - ; does it load the address of certain things into a register. We then - ; defer actually loading those until after DS was changed. - PUSH_STATE ; Push the state, except for the old ipl - mov ebx, [ebx] ; Retrieve the interrupt number and RIP from interrupt frame. These were deferred - mov ecx, [ecx] ; so that we wouldn't attempt to access the kernel stack using the user's data segment. - mov edx, [edx] ; Load CS, to determine the previous mode when entering a hardware interrupt - push ecx ; Enter a stack frame so that stack printing doesn't skip over anything + call KiTryEnterHardwareInterrupt + add esp, 4 + + cmp eax, 0 ; If it returned -1, it means we're not allowed to enter yet. + jl .returnEarly + + ; Push the old IPL. + push eax + + ; Get the pointer to the IP, because we want to construct a + ; fake stack frame that includes it. + mov ecx, [esp + 24] + push ecx push ebp mov ebp, esp - cld ; Clear direction flag, will be restored by iretq - movsx edi, byte [KiTrapIplList + ebx] ; Get the IPL for the respective interrupt vector - push edi - call KiEnterHardwareInterrupt ; Tell the kernel we entered a hardware interrupt - add esp, 4 - push eax ; Push the old IPL that we obtained from the function - mov edi, esp ; Retrieve the PKREGISTERS to call the trap handler - push edi - call [KiTrapCallList + 8 * ebx] ; Call the trap handler. It returns the new RSP. - ; add esp, 4 skipped because redundant - mov esp, eax ; Use the new PKREGISTERS instance as what to pull - mov edi, eax ; Get the pointer to the register context + + ; Push the rest of the state. + push ebx + push esi push edi - call KiExitHardwareInterrupt ; Tell the kernel we're exiting the hardware interrupt - add esp, 4 - pop edi ; Pop the old IPL because we don't need it any more - pop ebp ; Leave the stack frame - pop ecx ; Skip over the RIP duplicate that we pushed - POP_STATE ; Pop the state - pop eax ; Pop the RAX register - it has the old value of CS which we can check - pop edx ; Pop the EDX register - pop ecx ; Pop the ECX register - pop ebx ; Pop the EBX register - pop eax ; Pop the EAX register - add esp, 8 ; Pop the interrupt number and the error code + mov eax, cr2 + push eax + + ; Then, find the current interrupt number again and run it. + ; The interrupt handler will, if it returns, return the pointer + ; to the new stack, so we'll load that instead of restoring state + ; like usual. + + ; Note: this 40 is carefully counted! + mov ecx, [esp + 40] + push esp + call [KiTrapCallList + ecx * 4] + mov eax, esp + + ; Exit the hardware interrupt now. + push esp + call KiExitHardwareInterrupt + + ; Pop the state and return. + add esp, 8 ; pop the argument we pushed for KiExitHardwareInterrupt + the CR2 + pop edi + pop esi + pop ebx + pop ebp + add esp, 8 ; pop SFRA + old IPL + +.returnEarly: + pop edx + pop ecx + pop eax + add esp, 8 ; pop IntNum + ErrorCode iretd -%macro CLEAR_REGS 0 - xor ebx, ebx - xor ecx, ecx - xor edx, edx - xor esi, esi -%endmacro - global KeDescendIntoUserMode KeDescendIntoUserMode: mov edi, [esp + 4] @@ -134,17 +129,31 @@ KeDescendIntoUserMode: push dword 0x202 ; push RFLAGS push dword SEG_RING_3_CODE | 3 ; push CS push edi ; push RIP + mov edi, edx ; clear all the registers xor eax, eax xor ebp, ebp - mov edi, edx - CLEAR_REGS + xor ebx, ebx + xor ecx, ecx + xor edx, edx + xor esi, esi ; finally, swap gs and return to user mode. cli iretd +global KiCallSoftwareInterrupt +KiCallSoftwareInterrupt: + mov eax, [esp + 4] + mov ecx, [esp] + pushfd ; push eflags + push dword SEG_RING_0_CODE ; push cs + push ecx ; push eip - return value + cli ; ensure interrupts are disabled + jmp [KiTrapCallList + eax * 4] ; jump to the specific trap + ; note: the function will return directly to the return address + section .bss global KiTrapIplList KiTrapIplList: diff --git a/boron/source/ke/i386/traplist.asm b/boron/source/ke/i386/traplist.asm index 941eb560..969bd901 100644 --- a/boron/source/ke/i386/traplist.asm +++ b/boron/source/ke/i386/traplist.asm @@ -16,7 +16,7 @@ bits 32 section .text -; NOTE: This uses 5120 bytes of data. Is that bad? I don't know. +; NOTE: This uses however many bytes of data. Is that bad? I don't know. ; Arguments: ; 0 - Interrupt number in hexadecimal diff --git a/boron/source/ke/i386/traps.c b/boron/source/ke/i386/traps.c index 00a27583..cdeb6f98 100644 --- a/boron/source/ke/i386/traps.c +++ b/boron/source/ke/i386/traps.c @@ -19,6 +19,12 @@ Module name: #include #include "../../ke/ki.h" +extern void* const KiTrapList[]; // traplist.asm +extern int8_t KiTrapIplList[]; // trap.asm +extern void* KiTrapCallList[]; // trap.asm + +extern void KiCallSoftwareInterrupt(int Vector); // trap.asm + // The trap gate isn't likely to be used as it doesn't turn off // interrupts when entering the interrupt handler. enum KGATE_TYPE @@ -99,7 +105,7 @@ static void KiLoadInterruptVector(PKIDT Idt, int Vector, KiInterruptVector Handl Entry->Present = true; } -int KiEnterHardwareInterrupt(int NewIpl) +int KiTryEnterHardwareInterrupt(int IntNum) { PKPRCB Prcb = KeGetCurrentPRCB(); @@ -108,16 +114,32 @@ int KiEnterHardwareInterrupt(int NewIpl) // grab old IPL int OldIpl = (int) *IplPtr; - // set new IPL, if not marked as "don't override" + // figure out the new IPL + int NewIpl = KiTrapIplList[IntNum]; + + // Check if this is a lower priority interrupt than our current IPL. if (NewIpl != -1) { - *IplPtr = NewIpl; // specific to Amd64 + if (OldIpl >= NewIpl) + { + DbgPrint("Deferring int %d for later.", IntNum); + + // Trying to get interrupted by a masked interrupt? No problem, enqueue + // this interrupt onto our queue and simply return. + if (Prcb->ArchData.InterruptQueuePlace >= KE_MAX_QUEUED_INTERRUPTS) + { + DbgPrint( + "ERROR: Interrupt overflow. More than %d interrupts enqueued " + "at once. This interrupt will be dropped." + ); + return -1; + } + + Prcb->ArchData.InterruptQueue[Prcb->ArchData.InterruptQueuePlace++] = IntNum; + return -1; + } - if (OldIpl > NewIpl) - // uh oh! - KeCrash("KiEnterHardwareInterrupt: Old IPL of %d was higher than current IPL of %d.", OldIpl, NewIpl); - - KeOnUpdateIPL(NewIpl, OldIpl); + *IplPtr = NewIpl; // specific to Amd64 } // now that we've setup the hardware interrupt stuff, enable interrupts. @@ -131,14 +153,18 @@ int KiEnterHardwareInterrupt(int NewIpl) void KiExitHardwareInterrupt(PKREGISTERS Registers) { DISABLE_INTERRUPTS(); - - int OldIpl = Registers->OldIpl; - PKPRCB Prcb = KeGetCurrentPRCB(); - - KIPL PrevIpl = Prcb->Ipl; + int OldIpl = Registers->OldIpl; Prcb->Ipl = OldIpl; - KeOnUpdateIPL(OldIpl, PrevIpl); + + // Check if there are any interrupts we still need to de-queue. + // Note that we don't scan using a for loop, because this could change. + while (Prcb->ArchData.InterruptQueuePlace != 0) + { + int Vector = Prcb->ArchData.InterruptQueue[--Prcb->ArchData.InterruptQueuePlace]; + DbgPrint("Calling enqueued software interrupt %d", Vector); + KiCallSoftwareInterrupt(Vector); + } // Check if the current thread is terminated and we are about to // return to user mode. @@ -189,12 +215,7 @@ PKREGISTERS KiHandlePageFault(PKREGISTERS Regs) return Regs; } -extern void* const KiTrapList[]; // traplist.asm -extern int8_t KiTrapIplList[]; // trap.asm -extern void* KiTrapCallList[]; // trap.asm -static KSPIN_LOCK KiTrapLock; - -static int KepIplVectors[IPL_COUNT]; +static KSPIN_LOCK KiTrapLock; // Run on the BSP only. void KiSetupIdt() @@ -210,11 +231,6 @@ void KiSetupIdt() KiTrapCallList[i] = KiTrapUnknownHandler; } - for (int i = 0; i < IPL_COUNT; i++) - { - KepIplVectors[i] = i * 0x10; - } - KeRegisterInterrupt(INTV_DBL_FAULT, KiHandleDoubleFault); KeRegisterInterrupt(INTV_PROT_FAULT, KiHandleProtectionFault); KeRegisterInterrupt(INTV_PAGE_FAULT, KiHandlePageFault); @@ -225,6 +241,7 @@ void KiSetupIdt() void KeRegisterInterrupt(int Vector, PKINTERRUPT_HANDLER Handler) { + DbgPrint("Setting trap handler to %p", Handler); KIPL Ipl; KeAcquireSpinLock(&KiTrapLock, &Ipl); KiTrapCallList[Vector] = Handler; @@ -235,3 +252,7 @@ void KeSetInterruptIPL(int Vector, KIPL Ipl) { KiTrapIplList[Vector] = Ipl; } + +void KeOnUpdateIPL(UNUSED KIPL OldIpl, UNUSED KIPL NewIpl) +{ +} diff --git a/drivers/hali386/source/hali.h b/drivers/hali386/source/hali.h index 2a01816c..9d02a5c1 100644 --- a/drivers/hali386/source/hali.h +++ b/drivers/hali386/source/hali.h @@ -6,6 +6,7 @@ #define PIT_TICK_FREQUENCY (1193182) #define PIT_CHANNEL_0_PORT (0x40) +#define PIT_COMMAND_PORT (0x43) #define PIC1_COMMAND (0x20) #define PIC1_DATA (0x21) @@ -21,6 +22,13 @@ #define PIC_ICW3_SUB_CONFIG (2) // the sub PIC cascades at IRQ 2 #define PIC_ICW4_8086_MODE (1 << 0) +#define PIT_CMD_CHANNEL_0 (0 << 6) +#define PIT_CMD_ACCESS_2_BYTES (3 << 4) +#define PIT_CMD_RATE_GENERATOR (2 << 1) + +// ====== Port Utils ====== +void KePortWriteByteWait(uint16_t Port, uint8_t Data); + // ====== Timer ====== void HalUpdatePitClock(); @@ -36,6 +44,8 @@ HAL_API void HalRequestInterruptInTicks(uint64_t ticks); HAL_API uint64_t HalGetIntTimerDeltaTicks(); +void HalInitTimer(); + // ====== Interrupt Controller ====== HAL_API void HalInitPic(); diff --git a/drivers/hali386/source/init.c b/drivers/hali386/source/init.c index 097aa9ff..bf1060e4 100644 --- a/drivers/hali386/source/init.c +++ b/drivers/hali386/source/init.c @@ -55,6 +55,7 @@ HAL_API void HalInitSystemUP() // This function is run on ALL processors. HAL_API void HalInitSystemMP() { + HalInitTimer(); } BSTATUS DriverEntry(UNUSED PDRIVER_OBJECT Object) diff --git a/drivers/hali386/source/pic.c b/drivers/hali386/source/pic.c index 280a0419..6c28f751 100644 --- a/drivers/hali386/source/pic.c +++ b/drivers/hali386/source/pic.c @@ -15,8 +15,24 @@ Module name: #include "hali.h" uint16_t KiIplTable[] = { - + 0xFFFF, // IPL_NORMAL + 0xFFFF, // IPL_1 + 0xFFFF, // IPL_2 + 0xFFFF, // IPL_APC + 0xFFFF, // IPL_DPC + 0xFFFF, // IPL_DEVICES0 + 0xFFFF, // IPL_DEVICES1 + 0xFFFF, // IPL_DEVICES2 + 0xFFFF, // IPL_DEVICES3 + 0xFFFF, // IPL_DEVICES4 + 0xFFFF, // IPL_DEVICES5 + 0xFFFF, // IPL_DEVICES6 + 0xFFFF, // IPL_DEVICES7 + 0xFFFF, // IPL_DEVICES8 + 0xFFFF, // IPL_CLOCK + 0xFFFF, // IPL_NOINTS }; +static_assert(ARRAY_COUNT(KiIplTable) == IPL_COUNT); void KePortWriteByteWait(uint16_t Port, uint8_t Data) { @@ -37,18 +53,43 @@ HAL_API void HalEndOfInterrupt(int InterruptNumber) KePortWriteByte(PIC1_COMMAND, PIC_CMD_EOI); } -HAL_API void HalRegisterInterrupt(uint8_t Vector, KIPL Ipl) +HAL_API void HalPicRegisterInterrupt(uint8_t Vector, KIPL Ipl) { - // TODO - (void) Vector; - (void) Ipl; + if (Vector < PIC_INTERRUPT_BASE || Vector >= PIC_INTERRUPT_BASE + 16) + { + DbgPrint("HalRegisterInterrupt: Dropping vector %zu", Vector); + return; + } + + bool Restore = KeDisableInterrupts(); + + Vector -= PIC_INTERRUPT_BASE; + for (KIPL i = 0; i < Ipl; i++) + KiIplTable[i] &= ~(1 << Vector); + + KeRestoreInterrupts(Restore); + + // TODO: just unmask everything for now. Should we even use the other masks? + KePortWriteByteWait(PIC1_DATA, KiIplTable[0] & 0xFF); + KePortWriteByteWait(PIC2_DATA, KiIplTable[0] >> 8); } -HAL_API void HalDeregisterInterrupt(uint8_t Vector, KIPL Ipl) +// NOTE: There are no more uses of this interrupt once this function is called. I hope. +HAL_API void HalPicDeregisterInterrupt(uint8_t Vector, KIPL Ipl) { - // TODO - (void) Vector; - (void) Ipl; + if (Vector < PIC_INTERRUPT_BASE || Vector >= PIC_INTERRUPT_BASE + 16) + { + DbgPrint("HalRegisterInterrupt: Dropping vector %zu", Vector); + return; + } + + bool Restore = KeDisableInterrupts(); + + Vector -= PIC_INTERRUPT_BASE; + for (KIPL i = 0; i < Ipl; i++) + KiIplTable[i] |= 1 << Vector; + + KeRestoreInterrupts(Restore); } HAL_API void HalInitPic() diff --git a/drivers/hali386/source/timer.c b/drivers/hali386/source/timer.c index 694e6ca7..77487738 100644 --- a/drivers/hali386/source/timer.c +++ b/drivers/hali386/source/timer.c @@ -15,10 +15,13 @@ Module name: #include #include "hali.h" +// TODO: Decide on a good tick rate. For slower systems, increased +// time slice time would be better. #define TIMER_RELOAD_VALUE 4000 // This value is incremented by TIMER_RELOAD_VALUE on every timer interrupt. -static uint16_t PitTicksPassed; +static uint64_t PitTicksPassed; +static bool PitInitialized; HAL_API uint64_t HalGetTickFrequency() { @@ -27,6 +30,9 @@ HAL_API uint64_t HalGetTickFrequency() HAL_API uint64_t HalGetTickCount() { + if (!PitInitialized) + return 0; + // The PIT decrements from TIMER_RELOAD_VALUE uint8_t Low, High; Low = KePortReadByte(PIT_CHANNEL_0_PORT); @@ -36,14 +42,14 @@ HAL_API uint64_t HalGetTickCount() return PitTicksPassed + Timer; } -HAL_API bool HalUseOneShotIntTimer() +HAL_API uint64_t HalGetIntTimerFrequency() { - return false; + return PIT_TICK_FREQUENCY; } -HAL_API uint64_t HalGetIntTimerFrequency() +HAL_API bool HalUseOneShotIntTimer() { - return PIT_TICK_FREQUENCY; + return false; } HAL_API void HalRequestInterruptInTicks(UNUSED uint64_t Ticks) @@ -56,8 +62,9 @@ HAL_API uint64_t HalGetIntTimerDeltaTicks() return TIMER_RELOAD_VALUE; } -void HalUpdatePitClock() +PKREGISTERS HalTimerTick(PKREGISTERS Regs) { + DbgPrint("Tick!"); bool Restore = KeDisableInterrupts(); PitTicksPassed += TIMER_RELOAD_VALUE; @@ -65,4 +72,24 @@ void HalUpdatePitClock() KePortWriteByte(PIT_CHANNEL_0_PORT, TIMER_RELOAD_VALUE >> 8); KeRestoreInterrupts(Restore); + + KeTimerTick(); + + HalEndOfInterrupt((int) Regs->IntNumber); + return Regs; +} + +void HalInitTimer() +{ + KeRegisterInterrupt(SYSTEM_IRQ(0), HalTimerTick); + HalPicRegisterInterrupt(SYSTEM_IRQ(0), IPL_CLOCK); + KeSetInterruptIPL(SYSTEM_IRQ(0), IPL_CLOCK); + + bool Restore = KeDisableInterrupts(); + + KePortWriteByteWait(PIT_COMMAND_PORT, PIT_CMD_CHANNEL_0 | PIT_CMD_ACCESS_2_BYTES | PIT_CMD_RATE_GENERATOR); + KePortWriteByteWait(PIT_CHANNEL_0_PORT, TIMER_RELOAD_VALUE & 0xFF); + KePortWriteByteWait(PIT_CHANNEL_0_PORT, TIMER_RELOAD_VALUE >> 8); + + KeRestoreInterrupts(Restore); } diff --git a/drivers/i8042prt/source/main.c b/drivers/i8042prt/source/main.c index 775f9f06..8ead0aa6 100644 --- a/drivers/i8042prt/source/main.c +++ b/drivers/i8042prt/source/main.c @@ -21,6 +21,8 @@ Module name: PDRIVER_OBJECT I8042DriverObject; +#ifdef TARGET_AMD64 + int AllocateVector(PKIPL Ipl, KIPL Default) { int Vector = -1; @@ -32,6 +34,8 @@ int AllocateVector(PKIPL Ipl, KIPL Default) return Vector; } +#endif + BSTATUS InitializeDevice() { // Disable primary and secondary PS/2 ports. @@ -82,12 +86,12 @@ BSTATUS InitializeDevice() #elif defined TARGET_I386 const KIPL IplKbd = IPL_DEVICES0; - const int VectorKbd = PIC_INTERRUPT_BASE; + const int VectorKbd = SYSTEM_IRQ(1); bool Restore = KeDisableInterrupts(); #else -#error If you're using the i8042prt driver for another architecture, please define the interrupt method for it. +#error "If you're using the i8042prt driver for another architecture, please define the interrupt method for it." #endif // Initialize the keyboard. From 513e0476b4b564c62eba1ea4bf089b6d745381e5 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Fri, 17 Oct 2025 22:02:44 +0300 Subject: [PATCH 20/44] * X86: Get timers mostly working --- boron/source/build_number | 2 +- boron/source/ke/i386/debug.c | 10 ++++++++++ boron/source/ke/i386/misc.asm | 3 +-- boron/source/ke/i386/thredsup.c | 2 +- boron/source/ke/i386/trap.asm | 3 ++- boron/source/ke/i386/traps.c | 25 ++++++++++++++++++++----- boron/source/ke/ki.h | 4 ++++ boron/source/ke/sched.c | 3 +++ boron/source/rtl/print.c | 8 ++++++-- drivers/hali386/source/timer.c | 13 ++++--------- 10 files changed, 52 insertions(+), 21 deletions(-) diff --git a/boron/source/build_number b/boron/source/build_number index 1473a88f..ae4cf41b 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -281 +307 diff --git a/boron/source/ke/i386/debug.c b/boron/source/ke/i386/debug.c index 3291a5d8..b0d79cca 100644 --- a/boron/source/ke/i386/debug.c +++ b/boron/source/ke/i386/debug.c @@ -19,6 +19,10 @@ Module name: #include #include +#ifdef DEBUG2 +#define DONT_LOCK +#endif + #define KERNEL_IMAGE_BASE (0xC0100000) extern char KiKernelEnd[]; @@ -278,10 +282,16 @@ KSPIN_LOCK KiDebugPrintLock; void DbgPrintStringLocked(const char* str) { +#ifndef DONT_LOCK KIPL OldIpl; KeAcquireSpinLock(&KiDebugPrintLock, &OldIpl); +#endif + DbgPrintString(str); + +#ifndef DONT_LOCK KeReleaseSpinLock(&KiDebugPrintLock, OldIpl); +#endif } #endif diff --git a/boron/source/ke/i386/misc.asm b/boron/source/ke/i386/misc.asm index b9e8b10b..b18e983a 100644 --- a/boron/source/ke/i386/misc.asm +++ b/boron/source/ke/i386/misc.asm @@ -123,7 +123,6 @@ KiSwitchThreadStack: ; push System V ABI callee saved registers pushfd - push ebp push ebx push esi push edi @@ -139,11 +138,11 @@ KiPopEverythingAndReturn: pop edi pop esi pop ebx - pop ebp xor eax, eax ; Pop EFLAGS and return popfd + pop ebp ret ; Arguments: diff --git a/boron/source/ke/i386/thredsup.c b/boron/source/ke/i386/thredsup.c index 5f4c6cb8..f1f6381f 100644 --- a/boron/source/ke/i386/thredsup.c +++ b/boron/source/ke/i386/thredsup.c @@ -26,8 +26,8 @@ void KiSetupRegistersThread(PKTHREAD Thread) // KiPopEverythingAndReturn pops these *(--StackPointer) = (uint32_t) KiThreadEntryPoint; // Set return address - *(--StackPointer) = 0x200; // Set IF when entering the thread *(--StackPointer) = 0; // Set EBP + *(--StackPointer) = 0x200; // Set IF when entering the thread *(--StackPointer) = 0; // Set EBX *(--StackPointer) = (uint32_t) Thread->StartContext; // Set ESI *(--StackPointer) = (uint32_t) Thread->StartRoutine; // Set EDI diff --git a/boron/source/ke/i386/trap.asm b/boron/source/ke/i386/trap.asm index 7fbe2334..6e0fdea8 100644 --- a/boron/source/ke/i386/trap.asm +++ b/boron/source/ke/i386/trap.asm @@ -94,7 +94,7 @@ KiTrapCommon: mov ecx, [esp + 40] push esp call [KiTrapCallList + ecx * 4] - mov eax, esp + mov esp, eax ; Exit the hardware interrupt now. push esp @@ -113,6 +113,7 @@ KiTrapCommon: pop ecx pop eax add esp, 8 ; pop IntNum + ErrorCode +KeIretdBreakpoint: iretd global KeDescendIntoUserMode diff --git a/boron/source/ke/i386/traps.c b/boron/source/ke/i386/traps.c index cdeb6f98..ee063d8c 100644 --- a/boron/source/ke/i386/traps.c +++ b/boron/source/ke/i386/traps.c @@ -25,6 +25,8 @@ extern void* KiTrapCallList[]; // trap.asm extern void KiCallSoftwareInterrupt(int Vector); // trap.asm +bool KiTimerSuppression = true; // set to false in ../sched.c + // The trap gate isn't likely to be used as it doesn't turn off // interrupts when entering the interrupt handler. enum KGATE_TYPE @@ -120,17 +122,29 @@ int KiTryEnterHardwareInterrupt(int IntNum) // Check if this is a lower priority interrupt than our current IPL. if (NewIpl != -1) { - if (OldIpl >= NewIpl) + // HACK: Suppress timer interrupts until system is booted. + if (IntNum == PIC_INTERRUPT_BASE && KiTimerSuppression) + { + HalEndOfInterrupt(IntNum); + return -1; + } + + // If this is a hardware interrupt and it's lower in our priority, then defer it. + if (OldIpl >= NewIpl && IntNum >= 0x20) { - DbgPrint("Deferring int %d for later.", IntNum); + DbgPrint("Deferring int %d for later. TriedIpl: %d CurrIpl: %d", IntNum, NewIpl, OldIpl); + if (IntNum == PIC_INTERRUPT_BASE) + KeCrash("Timer tick triggered twice??"); + // Trying to get interrupted by a masked interrupt? No problem, enqueue // this interrupt onto our queue and simply return. if (Prcb->ArchData.InterruptQueuePlace >= KE_MAX_QUEUED_INTERRUPTS) { DbgPrint( - "ERROR: Interrupt overflow. More than %d interrupts enqueued " - "at once. This interrupt will be dropped." + "ERROR: Got interrupt %d. More than %d interrupts enqueued " + "at once, so this interrupt will be dropped.", + KE_MAX_QUEUED_INTERRUPTS ); return -1; } @@ -145,7 +159,8 @@ int KiTryEnterHardwareInterrupt(int IntNum) // now that we've setup the hardware interrupt stuff, enable interrupts. // we couldn't have done that before because the CPU would think that we're // in a low IPL thing meanwhile we're not.. - ENABLE_INTERRUPTS(); + if (NewIpl != -1 && NewIpl < IPL_CLOCK) + ENABLE_INTERRUPTS(); return OldIpl; } diff --git a/boron/source/ke/ki.h b/boron/source/ke/ki.h index 73cb4f7b..e6dfda8c 100644 --- a/boron/source/ke/ki.h +++ b/boron/source/ke/ki.h @@ -111,4 +111,8 @@ void KiInitializeThread(PKTHREAD Thread, void* KernelStack, size_t KernelStackSi bool KiCancelTimer(PKTIMER Timer); +#ifdef TARGET_I386 +extern bool KiTimerSuppression; +#endif + #endif//BORON_KE_KI_H diff --git a/boron/source/ke/sched.c b/boron/source/ke/sched.c index 1c4773e9..33382d64 100644 --- a/boron/source/ke/sched.c +++ b/boron/source/ke/sched.c @@ -275,6 +275,9 @@ NO_RETURN void KeSchedulerCommit() // the quantum-end interrupt. KIPL Ipl = KeRaiseIPL(IPL_DPC); KiSetPendingQuantumEnd(); +#ifdef TARGET_I386 + KiTimerSuppression = false; +#endif KeLowerIPL(Ipl); // Wait for the waves to pick us up... diff --git a/boron/source/rtl/print.c b/boron/source/rtl/print.c index 81560d36..d47d65a7 100644 --- a/boron/source/rtl/print.c +++ b/boron/source/rtl/print.c @@ -18,6 +18,10 @@ Module name: #include #include +#ifdef DEBUG2 +#define DONT_LOCK +#endif + #ifdef KERNEL #include #include @@ -65,13 +69,13 @@ void DbgPrint(const char* msg, ...) #ifdef KERNEL // This one goes to the debug log. // Debug2 turns off the spin locks associated with the debug prints. -#ifndef DEBUG2 +#ifndef DONT_LOCK KIPL OldIpl; KeAcquireSpinLock(&KiDebugPrintLock, &OldIpl); #endif HalPrintStringDebug(buffer); -#ifndef DEBUG2 +#ifndef DONT_LOCK KeReleaseSpinLock(&KiDebugPrintLock, OldIpl); #endif diff --git a/drivers/hali386/source/timer.c b/drivers/hali386/source/timer.c index 77487738..9c545e95 100644 --- a/drivers/hali386/source/timer.c +++ b/drivers/hali386/source/timer.c @@ -39,7 +39,8 @@ HAL_API uint64_t HalGetTickCount() High = KePortReadByte(PIT_CHANNEL_0_PORT); uint16_t Timer = TIMER_RELOAD_VALUE - (Low | (High << 8)); - return PitTicksPassed + Timer; + uint64_t Value = PitTicksPassed + Timer; + return Value; } HAL_API uint64_t HalGetIntTimerFrequency() @@ -64,17 +65,10 @@ HAL_API uint64_t HalGetIntTimerDeltaTicks() PKREGISTERS HalTimerTick(PKREGISTERS Regs) { - DbgPrint("Tick!"); - bool Restore = KeDisableInterrupts(); - PitTicksPassed += TIMER_RELOAD_VALUE; - KePortWriteByte(PIT_CHANNEL_0_PORT, TIMER_RELOAD_VALUE & 0xFF); - KePortWriteByte(PIT_CHANNEL_0_PORT, TIMER_RELOAD_VALUE >> 8); - - KeRestoreInterrupts(Restore); KeTimerTick(); - + HalEndOfInterrupt((int) Regs->IntNumber); return Regs; } @@ -92,4 +86,5 @@ void HalInitTimer() KePortWriteByteWait(PIT_CHANNEL_0_PORT, TIMER_RELOAD_VALUE >> 8); KeRestoreInterrupts(Restore); + PitInitialized = true; } From 65cc29c5cc584b1ff5d99a94bc2800d7e47282d0 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Fri, 17 Oct 2025 23:12:14 +0300 Subject: [PATCH 21/44] * Mm: Fix pool allocator for 32-bit systems --- boron/include/arch/i386.h | 5 +-- boron/source/mm/mi.h | 3 ++ boron/source/mm/poolhdr.c | 23 +++++++++---- boron/source/mm/poolsup.c | 72 +++++++++++++++++++++++++++++++++++++-- 4 files changed, 92 insertions(+), 11 deletions(-) diff --git a/boron/include/arch/i386.h b/boron/include/arch/i386.h index e3588b1e..87012d9c 100644 --- a/boron/include/arch/i386.h +++ b/boron/include/arch/i386.h @@ -113,11 +113,12 @@ MMADDRESS_CONVERT; // (3) - If the PTE is in transition, then the physical page is part of either the standby or the modified page list. #define MM_DPTE_DECOMMITTED (1 << 3) +#define MM_DPTE_WASPRESENT (1 << 7) #define MM_DPTE_COMMITTED (1 << 8) #define MM_DPTE_BACKEDBYFILE (1 << 9) #define MM_DPTE_SWAPPED (1 << 10) -#define MM_DPTE_WASPRESENT (1 << 30) -#define MM_PTE_ISPOOLHDR (1 << 31) // if the PTE actually contains the address of a pool entry (subtracted MM_KERNEL_SPACE_BASE from it) + +#define MM_PTE_ISPOOLHDR (1 << 11) // see src/mm/poolsup.c for an explanation // Page fault reasons #define MM_FAULT_PROTECTION (1 << 0) // 0: Page wasn't marked present; 1: Page protection violation (e.g. writing to a readonly page) diff --git a/boron/source/mm/mi.h b/boron/source/mm/mi.h index b39b81c2..516a2f53 100644 --- a/boron/source/mm/mi.h +++ b/boron/source/mm/mi.h @@ -176,6 +176,9 @@ typedef struct MIPOOL_ENTRY_tag uintptr_t UserData; // Qword 3 uintptr_t Address; // Qword 4 size_t Size; // Qword 5, size is in pages. +#ifdef IS_32_BIT + int Dummy; +#endif } MIPOOL_ENTRY, *PMIPOOL_ENTRY; diff --git a/boron/source/mm/poolhdr.c b/boron/source/mm/poolhdr.c index 78b30e58..c7274446 100644 --- a/boron/source/mm/poolhdr.c +++ b/boron/source/mm/poolhdr.c @@ -17,17 +17,28 @@ Module name: typedef struct { + // UPDATE MmpAllocateFromPoolEntrySlab if changing these. + +#ifdef IS_64_BIT + #define POOL_ENTRY_COUNT 84 // Size of a MIPOOL_ENTRY is 48. - // UPDATE MmpAllocateFromPoolEntrySlab if changing this. - MIPOOL_ENTRY Entries[84]; + // Size of additional data is 32 bytes. (2xPtr=16 + 2xU64=16) + // Note: Free space is 32 bytes +#else + #define POOL_ENTRY_COUNT 127 + // Size of a MIPOOL_ENTRY is 32 + // Size of additional data is 24 bytes (2xPtr=8 + 2xU64=16) + // Note: Free space is 16 bytes +#endif + + MIPOOL_ENTRY Entries[POOL_ENTRY_COUNT]; LIST_ENTRY ListEntry; uint64_t Bitmap[2]; - - // Note - Free space: 32 bytes } MIPOOL_ENTRY_SLAB, *PMIPOOL_ENTRY_SLAB; static_assert(sizeof(MIPOOL_ENTRY_SLAB) <= PAGE_SIZE); +static_assert((sizeof(MIPOOL_ENTRY) & 0x7) == 0); static LIST_ENTRY MmpPoolSlabList; static KSPIN_LOCK MmpPoolSlabListLock; @@ -47,10 +58,10 @@ PMIPOOL_ENTRY MmpAllocateFromPoolEntrySlab(PMIPOOL_ENTRY_SLAB Slab) } } - const uint64_t FirstTwentyBitsSet = (1ULL << 20) - 1; + const uint64_t FirstTwentyBitsSet = (1ULL << (POOL_ENTRY_COUNT - 64)) - 1; if ((Slab->Bitmap[1] & FirstTwentyBitsSet) != FirstTwentyBitsSet) { - for (int i = 0, j = 64; i < 20; i++, j++) + for (int i = 0, j = 64; i < POOL_ENTRY_COUNT - 64; i++, j++) { if (~Slab->Bitmap[1] & (1ULL << i)) { diff --git a/boron/source/mm/poolsup.c b/boron/source/mm/poolsup.c index 2cc9cff0..60dc8444 100644 --- a/boron/source/mm/poolsup.c +++ b/boron/source/mm/poolsup.c @@ -14,6 +14,59 @@ Module name: ***/ #include "mi.h" +#ifdef IS_32_BIT + +// the structure of the pool header PTE if this is set is as follows: +// +// [Bits 31..12] 1 [Bits 11..10] 0 [Bits 9..3] 0 +// +// - Bit 8 is cleared because MM_DPTE_COMMITTED shouldn't conflict +// with this scheme. +// +// - Bit 0 is cleared because MM_PTE_PRESENT conflicts +// +// - Bit 11 is set because that's MM_PTE_ISPOOLHDR + +typedef union +{ + MMPTE Pte; + + struct + { + uintptr_t Present : 1; // MUST be zero + uintptr_t B3to9 : 7; + uintptr_t Committed : 1; // MUST be zero + uintptr_t B10to11 : 2; + uintptr_t IsPoolHdr : 1; // MUST be ONE + uintptr_t B12to31 : 20; + } + PACKED; +} +MMPTE_POOLHEADER; + +static_assert(sizeof(MMPTE_POOLHEADER) == sizeof(uint32_t)); +static_assert(MM_DPTE_COMMITTED == (1 << 8)); +static_assert(MM_PTE_ISPOOLHDR == (1 << 11)); + +MMPTE MiCalculatePoolHeaderPte(uintptr_t Handle) +{ + MMPTE_POOLHEADER PteHeader; + PteHeader.Pte = 0; + + PteHeader.B3to9 = (Handle >> 3) & 0x1FF; + PteHeader.B10to11 = (Handle >> 10) & 0x3; + PteHeader.B12to31 = (Handle >> 12); + PteHeader.IsPoolHdr = true; + + return PteHeader.Pte; +} + +#else + +#define MiCalculatePoolHeaderPte(Handle) (((Handle) - MM_KERNEL_SPACE_BASE) | MM_PTE_ISPOOLHDR) + +#endif + // // TODO: This could be improved, however, it's probably OK for now. // @@ -219,7 +272,7 @@ void MiFreePoolSpace(MIPOOL_SPACE_HANDLE Handle) PMMPTE PtePtr = MiGetPTEPointer(MiGetCurrentPageMap(), Address, false); ASSERT(PtePtr); - ASSERT(*PtePtr == ((Handle - MM_KERNEL_SPACE_BASE) | MM_PTE_ISPOOLHDR)); + ASSERT(*PtePtr == MiCalculatePoolHeaderPte(Handle)); *PtePtr = 0; MmUnlockKernelSpace(); @@ -263,7 +316,8 @@ MIPOOL_SPACE_HANDLE MiReservePoolSpaceTagged(size_t SizeInPages, void** OutputAd ASSERT(*PtePtr == 0); ASSERT((Handle & MM_PTE_PRESENT) == 0); - *PtePtr = (Handle - MM_KERNEL_SPACE_BASE) | MM_PTE_ISPOOLHDR; + *PtePtr = MiCalculatePoolHeaderPte(Handle); + MmUnlockKernelSpace(); *OutputAddress = (void*) ((uintptr_t) OutputAddressSub + PAGE_SIZE); @@ -341,9 +395,21 @@ MIPOOL_SPACE_HANDLE MiGetPoolSpaceHandleFromAddress(void* AddressV) // the valid bit set. uintptr_t PAddress = *PtePtr; ASSERT(PAddress & MM_PTE_ISPOOLHDR); + +#ifdef IS_32_BIT + MMPTE_POOLHEADER PteHeader; + PteHeader.Pte = PAddress; + + PAddress = + PteHeader.B12to31 << 12 | + PteHeader.B10to11 << 10 | + PteHeader.B3to9 << 3; +#else PAddress &= ~MM_PTE_ISPOOLHDR; + PAddress += MM_KERNEL_SPACE_BASE; +#endif - MIPOOL_SPACE_HANDLE Handle = PAddress + MM_KERNEL_SPACE_BASE; + MIPOOL_SPACE_HANDLE Handle = PAddress; MmUnlockKernelSpace(); return Handle; } From eda333e629c6db6aa57b2ef442fbe9f995567d0f Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Fri, 17 Oct 2025 23:13:09 +0300 Subject: [PATCH 22/44] * X86,Ke: Fix a bug where kernel crashes if there is no active thread and the timer ticks --- boron/source/build_number | 2 +- boron/source/ke/i386/traps.c | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/boron/source/build_number b/boron/source/build_number index ae4cf41b..6489928e 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -307 +329 diff --git a/boron/source/ke/i386/traps.c b/boron/source/ke/i386/traps.c index ee063d8c..d24dd813 100644 --- a/boron/source/ke/i386/traps.c +++ b/boron/source/ke/i386/traps.c @@ -183,11 +183,13 @@ void KiExitHardwareInterrupt(PKREGISTERS Registers) // Check if the current thread is terminated and we are about to // return to user mode. - if (KeGetCurrentThread()->PendingTermination && + PKTHREAD CurrentThread = KeGetCurrentThread(); + if (CurrentThread && + CurrentThread->PendingTermination && Registers->OldIpl == IPL_NORMAL && Registers->Cs == SEG_RING_3_CODE) { - KiTerminateUserModeThread(KeGetCurrentThread()->IncrementTerminated); + KiTerminateUserModeThread(CurrentThread->IncrementTerminated); } // Note: safe to call here because KiDispatchSoftwareInterrupts From b8e897202cd504704afb05cf6218e2fbc31435ec Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Fri, 17 Oct 2025 23:13:41 +0300 Subject: [PATCH 23/44] * Test: Fix pipe test for 32-bit systems --- drivers/test/source/pipetst.c | 2 +- limine.i386.cfg | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/test/source/pipetst.c b/drivers/test/source/pipetst.c index 0f89d73a..5dc1becb 100644 --- a/drivers/test/source/pipetst.c +++ b/drivers/test/source/pipetst.c @@ -25,7 +25,7 @@ void PerformPipeTest() IO_STATUS_BLOCK Iosb; // memcpy from the start of the HHDM to get some data going - memcpy(SomeData, (void*) 0xFFFF800000000000, sizeof SomeData); + memcpy(SomeData, (void*) MmGetHHDMOffsetAddr(0), sizeof SomeData); const size_t BufferSize = 4096; diff --git a/limine.i386.cfg b/limine.i386.cfg index 17cfce6d..cf23afef 100644 --- a/limine.i386.cfg +++ b/limine.i386.cfg @@ -21,8 +21,8 @@ VERBOSE=yes #MODULE_PATH=boot:///ext2fs.sys #MODULE_STRING=ext2fs.sys - #MODULE_PATH=boot:///test.sys - #MODULE_STRING=test.sys + MODULE_PATH=boot:///test.sys + MODULE_STRING=test.sys #MODULE_PATH=boot:///libboron.so #MODULE_PATH=boot:///libtest.so From f8499d62425d7eae3026984ab108495975f7c329 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Fri, 17 Oct 2025 23:16:50 +0300 Subject: [PATCH 24/44] * Ke(i386): Turns out we don't need the timer suppression after all --- boron/source/build_number | 2 +- boron/source/ke/i386/traps.c | 9 --------- boron/source/ke/ki.h | 4 ---- boron/source/ke/sched.c | 6 +++--- 4 files changed, 4 insertions(+), 17 deletions(-) diff --git a/boron/source/build_number b/boron/source/build_number index 6489928e..6f96da66 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -329 +332 diff --git a/boron/source/ke/i386/traps.c b/boron/source/ke/i386/traps.c index d24dd813..265249c1 100644 --- a/boron/source/ke/i386/traps.c +++ b/boron/source/ke/i386/traps.c @@ -25,8 +25,6 @@ extern void* KiTrapCallList[]; // trap.asm extern void KiCallSoftwareInterrupt(int Vector); // trap.asm -bool KiTimerSuppression = true; // set to false in ../sched.c - // The trap gate isn't likely to be used as it doesn't turn off // interrupts when entering the interrupt handler. enum KGATE_TYPE @@ -122,13 +120,6 @@ int KiTryEnterHardwareInterrupt(int IntNum) // Check if this is a lower priority interrupt than our current IPL. if (NewIpl != -1) { - // HACK: Suppress timer interrupts until system is booted. - if (IntNum == PIC_INTERRUPT_BASE && KiTimerSuppression) - { - HalEndOfInterrupt(IntNum); - return -1; - } - // If this is a hardware interrupt and it's lower in our priority, then defer it. if (OldIpl >= NewIpl && IntNum >= 0x20) { diff --git a/boron/source/ke/ki.h b/boron/source/ke/ki.h index e6dfda8c..73cb4f7b 100644 --- a/boron/source/ke/ki.h +++ b/boron/source/ke/ki.h @@ -111,8 +111,4 @@ void KiInitializeThread(PKTHREAD Thread, void* KernelStack, size_t KernelStackSi bool KiCancelTimer(PKTIMER Timer); -#ifdef TARGET_I386 -extern bool KiTimerSuppression; -#endif - #endif//BORON_KE_KI_H diff --git a/boron/source/ke/sched.c b/boron/source/ke/sched.c index 33382d64..f56da8fd 100644 --- a/boron/source/ke/sched.c +++ b/boron/source/ke/sched.c @@ -275,9 +275,6 @@ NO_RETURN void KeSchedulerCommit() // the quantum-end interrupt. KIPL Ipl = KeRaiseIPL(IPL_DPC); KiSetPendingQuantumEnd(); -#ifdef TARGET_I386 - KiTimerSuppression = false; -#endif KeLowerIPL(Ipl); // Wait for the waves to pick us up... @@ -769,6 +766,9 @@ void KiHandleQuantumEnd() void KeTimerTick() { + if (!KeGetCurrentThread()) + return; + // Check if the current thread's quantum has expired. If it has, set // the PENDING_YIELD pending event and issue a software interrupt. From 83c6cecc9c318a04e50cefa4701d0925f3f6834e Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Fri, 17 Oct 2025 23:24:19 +0300 Subject: [PATCH 25/44] * Ke(i386): Fix bug where thread routine wasn't called correctly. --- boron/source/build_number | 2 +- boron/source/ke/i386/misc.asm | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/boron/source/build_number b/boron/source/build_number index 6f96da66..0ae9d1ef 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -332 +334 diff --git a/boron/source/ke/i386/misc.asm b/boron/source/ke/i386/misc.asm index b18e983a..3d72136b 100644 --- a/boron/source/ke/i386/misc.asm +++ b/boron/source/ke/i386/misc.asm @@ -159,7 +159,7 @@ KiThreadEntryPoint: ; ebx saved because KiUnlockDispatcher is SysV compliant ; eax still pushed-- will be used as the argument - jmp edi + call edi ; Used for the init phase of the scheduler, before a thread is scheduled in. global KiSwitchThreadStackForever From 7f439cc9473d29059ae4442444b912ed83de2a44 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Sat, 18 Oct 2025 00:02:08 +0300 Subject: [PATCH 26/44] * Mm(i386): Fix several page table bugs - The recursive mapping was accidentally placed at 511 instead of 1023 - The 0xC0000000-0xD0000000 mapping was initialized from a bad offset and those pages ended up being allocated by the PMM and then overwritten --- boron/include/arch/i386.h | 2 +- boron/source/build_number | 2 +- boron/source/mm/i386/idmap.c | 2 +- boron/source/mm/slab.c | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/boron/include/arch/i386.h b/boron/include/arch/i386.h index 87012d9c..1cc5b3b1 100644 --- a/boron/include/arch/i386.h +++ b/boron/include/arch/i386.h @@ -38,7 +38,7 @@ void KePortWriteDword(uint16_t portNo, uint32_t data); #define MI_GLOBAL_AREA_START (512) #define MI_GLOBAL_AREA_START_2ND (832) -#define MI_RECURSIVE_PAGING_START (511) +#define MI_RECURSIVE_PAGING_START (1023) #define MI_PML2_LOCATION ((uintptr_t)0xFFFFF000U) #define MI_PML1_LOCATION ((uintptr_t)0xFFC00000U) diff --git a/boron/source/build_number b/boron/source/build_number index 0ae9d1ef..cb35cf9f 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -334 +368 diff --git a/boron/source/mm/i386/idmap.c b/boron/source/mm/i386/idmap.c index 6fa50081..a0afa13a 100644 --- a/boron/source/mm/i386/idmap.c +++ b/boron/source/mm/i386/idmap.c @@ -28,7 +28,7 @@ void MiInitializeBaseIdentityMapping() PMMPTE Level2 = (PMMPTE) MI_PTE_LOC(MI_PTE_LOC(Address)); PMMPTE Level1 = (PMMPTE) MI_PTE_LOC(Address); - *Level2 = V2P(&KiBootstrapPageTables[(Address >> 22) * PAGE_SIZE]) + *Level2 = V2P(&KiBootstrapPageTables[(i >> 22) * PAGE_SIZE]) | MM_PTE_READWRITE | MM_PTE_PRESENT; diff --git a/boron/source/mm/slab.c b/boron/source/mm/slab.c index 5a0462ff..5d186401 100644 --- a/boron/source/mm/slab.c +++ b/boron/source/mm/slab.c @@ -55,7 +55,7 @@ static size_t MmpSlabItemDetermineLength(int ItemSize) return PAGE_SIZE; // Allow at least 4 items to fit. - return (ItemSize * 4 + PAGE_SIZE - 1) / PAGE_SIZE * PAGE_SIZE; + return (ItemSize * 4 + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1); } static bool MmpRequiresRbTreeEntry(int ItemSize) @@ -130,7 +130,7 @@ int MmGetSmallestSlabSizeThatFitsSize(size_t Size) void* MmpSlabItemTryAllocate(PMISLAB_ITEM Item, int EntrySize) { - int EntriesPerItem = (Item->Length - sizeof(Item)) / EntrySize; + int EntriesPerItem = (Item->Length - sizeof(*Item)) / EntrySize; int BitmapWrdsToCheck = (EntriesPerItem + 63) / 64; int LastBitmapBitCount = EntriesPerItem % 64; From 43f00c6ff0889f452b2bc21f94d56ccdb3cb0f8b Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Sat, 18 Oct 2025 00:24:20 +0300 Subject: [PATCH 27/44] * Mm(i386): Fix PFN DB location, make PFDBE power of 2 again because turns out non PO2 PFDBEs may cross page boundaries --- boron/include/arch/i386.h | 2 +- boron/include/mm/pfn.h | 2 ++ boron/source/build_number | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/boron/include/arch/i386.h b/boron/include/arch/i386.h index 1cc5b3b1..46f4d9da 100644 --- a/boron/include/arch/i386.h +++ b/boron/include/arch/i386.h @@ -75,7 +75,7 @@ MMADDRESS_CONVERT; #define MM_KERNEL_SPACE_BASE (0x80000000U) #define MM_USER_SPACE_END (0x7FFFFFFFU) -#define MM_PFNDB_BASE (0xC2000000U) +#define MM_PFNDB_BASE (0xD4000000U) #define MM_PTE_PRESENT (1 << 0) #define MM_PTE_READWRITE (1 << 1) diff --git a/boron/include/mm/pfn.h b/boron/include/mm/pfn.h index 77ec2ee0..2e150c0b 100644 --- a/boron/include/mm/pfn.h +++ b/boron/include/mm/pfn.h @@ -99,6 +99,8 @@ typedef struct PACKED FileCache; }; + + uint32_t Dummy; // to make this a power of 2 #endif } PACKED diff --git a/boron/source/build_number b/boron/source/build_number index cb35cf9f..446dfcc5 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -368 +369 From dd7b847e13ba5be629e0263b364f0a3cee219d3d Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Sat, 18 Oct 2025 00:24:53 +0300 Subject: [PATCH 28/44] * Test: Fix fireworks test on 32-bit --- drivers/test/source/fworktst.c | 10 ++++++++++ drivers/test/source/main.c | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/drivers/test/source/fworktst.c b/drivers/test/source/fworktst.c index 7f035b67..31ce4e57 100644 --- a/drivers/test/source/fworktst.c +++ b/drivers/test/source/fworktst.c @@ -120,7 +120,17 @@ void Init() { PLOADER_FRAMEBUFFER Framebuffer = &KeLoaderParameterBlock.Framebuffers[0]; +#ifdef IS_32_BIT + PixBuff = MmMapIoSpace( + (uintptr_t)Framebuffer->Address, + Framebuffer->Pitch * Framebuffer->Height, + MM_PTE_READWRITE | MM_PTE_CDISABLE, + POOL_TAG("FWFB") + ); +#else PixBuff = Framebuffer->Address; +#endif + PixWidth = Framebuffer->Width; PixHeight = Framebuffer->Height; PixPitch = Framebuffer->Pitch; diff --git a/drivers/test/source/main.c b/drivers/test/source/main.c index ec2f2d94..5a993333 100644 --- a/drivers/test/source/main.c +++ b/drivers/test/source/main.c @@ -81,7 +81,7 @@ NO_RETURN void DriverTestThread(UNUSED void* Parameter) //PerformProcessTest(); //PerformMutexTest(); //PerformBallTest(); - //PerformFireworksTest(); + PerformFireworksTest(); //PerformHandleTest(); //PerformApcTest(); //PerformRwlockTest(); @@ -98,7 +98,7 @@ NO_RETURN void DriverTestThread(UNUSED void* Parameter) //PerformMm4Test(); //PerformMm5Test(); //PerformFs1Test(); - PerformPipeTest(); + //PerformPipeTest(); LogMsg(ANSI_GREEN "*** All tests have concluded." ANSI_RESET); KeTerminateThread(0); From 96c1511c0fd074247dfe76b22344457891aebe65 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Sat, 18 Oct 2025 10:49:14 +0300 Subject: [PATCH 29/44] * Mm(i386): Some memory manager fixes Unfortunately, still not stable. --- boron/source/build_number | 2 +- boron/source/ke/i386/foreinit.asm | 2 ++ boron/source/mm/i386/idmap.c | 11 +++++++++++ boron/source/mm/mi.h | 11 ++++++++--- boron/source/mm/pmm.c | 4 ++++ boron/source/mm/poolsup.c | 4 ++++ boron/source/mm/slab.c | 4 ++++ tools/run-i386.bat | 2 +- 8 files changed, 35 insertions(+), 5 deletions(-) diff --git a/boron/source/build_number b/boron/source/build_number index 446dfcc5..c2f53117 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -369 +380 diff --git a/boron/source/ke/i386/foreinit.asm b/boron/source/ke/i386/foreinit.asm index 13ea2766..2fc71174 100644 --- a/boron/source/ke/i386/foreinit.asm +++ b/boron/source/ke/i386/foreinit.asm @@ -126,6 +126,7 @@ global KiMultibootSignature global KiMultibootInfo global KiBootstrapPageDirectory global KiBootstrapPageTables +global KiHhdmWindowPageTables KiMultibootSignature: resd 1 KiMultibootInfo: resd 1 @@ -134,3 +135,4 @@ alignb 4096 KiInitialStack: resb 4096 KiBootstrapPageDirectory: resb 4096 KiBootstrapPageTables: resb 64 * 4096 +KiHhdmWindowPageTables: resb 4 * 4096 diff --git a/boron/source/mm/i386/idmap.c b/boron/source/mm/i386/idmap.c index a0afa13a..bf17e381 100644 --- a/boron/source/mm/i386/idmap.c +++ b/boron/source/mm/i386/idmap.c @@ -18,6 +18,7 @@ Module name: #define V2P(p) ((uintptr_t)(p) - MI_IDENTMAP_START) extern uint8_t KiBootstrapPageTables[]; +extern uint8_t KiHhdmWindowPageTables[]; void MiInitializeBaseIdentityMapping() { @@ -34,4 +35,14 @@ void MiInitializeBaseIdentityMapping() *Level1 = i | MM_PTE_READWRITE | MM_PTE_PRESENT; } + + for (size_t i = 0; i < MI_FASTMAP_SIZE; i += PAGE_SIZE * PAGE_SIZE / sizeof(MMPTE)) + { + uintptr_t Address = MI_FASTMAP_START + i; + + PMMPTE Level2 = (PMMPTE) MI_PTE_LOC(MI_PTE_LOC(Address)); + *Level2 = V2P(&KiHhdmWindowPageTables[(i >> 22) * PAGE_SIZE]) + | MM_PTE_READWRITE + | MM_PTE_PRESENT; + } } diff --git a/boron/source/mm/mi.h b/boron/source/mm/mi.h index 516a2f53..57b53ed0 100644 --- a/boron/source/mm/mi.h +++ b/boron/source/mm/mi.h @@ -80,11 +80,16 @@ typedef struct MISLAB_ITEM_tag // into the rest. union { - uint64_t Bitmap[4]; // Supports down to 16 byte sized items + uint64_t Bitmap[4]; // 32 bytes - Supports down to 16 byte sized items struct { - uint64_t Bitmap2[1]; - RBTREE_ENTRY TreeEntry; + #ifdef IS_64_BIT + uint64_t Bitmap2[1]; // 8 + RBTREE_ENTRY TreeEntry; // 24 + #else + uint64_t Bitmap2[2]; // 16 + RBTREE_ENTRY TreeEntry; // 12 + #endif }; }; diff --git a/boron/source/mm/pmm.c b/boron/source/mm/pmm.c index 176aa939..92a06913 100644 --- a/boron/source/mm/pmm.c +++ b/boron/source/mm/pmm.c @@ -71,6 +71,8 @@ uintptr_t MmHHDMWindowBase; static void MiUpdateHHDMWindowBase(uintptr_t PhysAddr) { + KIPL Ipl = MiLockPfdb(); + const int PtesPerLevel = PAGE_SIZE / sizeof(MMPTE); PMMPTE Ptes = (PMMPTE)(MI_PML1_LOCATION); @@ -87,6 +89,8 @@ static void MiUpdateHHDMWindowBase(uintptr_t PhysAddr) Ptes[Convert.Level2Index * PtesPerLevel + Convert.Level1Index] = MM_PTE_PRESENT | MM_PTE_READWRITE | MM_PTE_NOEXEC | (PhysAddr + i); KeInvalidatePage((void*)Address); } + + MiUnlockPfdb(Ipl); } void* MmGetHHDMOffsetAddr(uintptr_t PhysAddr) diff --git a/boron/source/mm/poolsup.c b/boron/source/mm/poolsup.c index 60dc8444..7c6b20a6 100644 --- a/boron/source/mm/poolsup.c +++ b/boron/source/mm/poolsup.c @@ -157,6 +157,7 @@ MIPOOL_SPACE_HANDLE MmpSplitEntry(PMIPOOL_ENTRY PoolEntry, size_t SizeInPages, v // Link it such that: // PoolEntry ====> NewEntry ====> PoolEntry->Flink + ASSERT(NewEntry); InsertHeadList(&PoolEntry->ListEntry, &NewEntry->ListEntry); // Assign the other properties @@ -191,6 +192,9 @@ MIPOOL_SPACE_HANDLE MiReservePoolSpaceTaggedSub(size_t SizeInPages, void** Outpu while (CurrentEntry != &MmpPoolList) { + if (CurrentEntry == NULL) + KeCrash("HUH??! CurrentEntry is NULL"); + // Skip allocated entries. PMIPOOL_ENTRY Current = MIP_CURRENT(CurrentEntry); diff --git a/boron/source/mm/slab.c b/boron/source/mm/slab.c index 5d186401..a9decab3 100644 --- a/boron/source/mm/slab.c +++ b/boron/source/mm/slab.c @@ -266,7 +266,11 @@ void MmpSlabContainerFree(PMISLAB_CONTAINER Container, PMISLAB_ITEM Item, void* bool RequiresRbTreeEntry = MmpRequiresRbTreeEntry(Container->ItemSize); // Check if it's all zero: +#ifdef IS_64_BIT if (!Item->Bitmap[0] && (RequiresRbTreeEntry || (!Item->Bitmap[1] && !Item->Bitmap[2] && !Item->Bitmap[3]))) +#else + if (!Item->Bitmap[0] && !Item->Bitmap[1] && (RequiresRbTreeEntry || (!Item->Bitmap[2] && !Item->Bitmap[3]))) +#endif { RemoveEntryList(&Item->ListEntry); diff --git a/tools/run-i386.bat b/tools/run-i386.bat index e84437b6..a54df744 100644 --- a/tools/run-i386.bat +++ b/tools/run-i386.bat @@ -15,7 +15,7 @@ if exist %nspath%\vdiske2.vdi ( qemu-system-i386.exe -no-reboot -no-shutdown -d int -M smm=off ^ -M q35 ^ --m 256M ^ +-m 1024M ^ -boot d ^ -display sdl ^ -accel tcg ^ From 20ca16d6de0c14f8077d74ee1d1b89cecc04388f Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Sat, 18 Oct 2025 14:19:51 +0300 Subject: [PATCH 30/44] * X86: More fixes. I think I will need to completely rewrite the pool allocator, since I found some major flaws in regards to locking and things like that. --- boron/source/build_number | 2 +- boron/source/mm/mi.h | 10 +++---- boron/source/mm/poolhdr.c | 21 +++++++++++++-- boron/source/mm/poolsup.c | 48 +++++++++++++++++++++++++--------- boron/source/mm/slab.c | 4 +++ drivers/test/source/fworktst.c | 6 ----- drivers/test/source/main.c | 6 ++++- 7 files changed, 69 insertions(+), 28 deletions(-) diff --git a/boron/source/build_number b/boron/source/build_number index c2f53117..4391a334 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -380 +396 diff --git a/boron/source/mm/mi.h b/boron/source/mm/mi.h index 57b53ed0..1223ddc3 100644 --- a/boron/source/mm/mi.h +++ b/boron/source/mm/mi.h @@ -67,7 +67,6 @@ struct MISLAB_CONTAINER_tag; #define MI_SLAB_ITEM_CHECK (0x424C5342) // "BSLB" -#define MI_MIN_SIZE_SLAB (8) #define MI_MAX_SIZE_SLAB (32768) typedef struct MISLAB_ITEM_tag @@ -175,15 +174,16 @@ HUGE_MEMORY_BLOCK, *PHUGE_MEMORY_BLOCK; typedef struct MIPOOL_ENTRY_tag { - LIST_ENTRY ListEntry; // Qword 0, 1 +#ifdef IS_32_BIT + int Dummy; + #define MIPOOL_DUMMY_SIGNATURE 0x12345678 +#endif int Flags; // Qword 2 int Tag; uintptr_t UserData; // Qword 3 uintptr_t Address; // Qword 4 size_t Size; // Qword 5, size is in pages. -#ifdef IS_32_BIT - int Dummy; -#endif + LIST_ENTRY ListEntry; // Qword 0, 1 } MIPOOL_ENTRY, *PMIPOOL_ENTRY; diff --git a/boron/source/mm/poolhdr.c b/boron/source/mm/poolhdr.c index c7274446..9dea6c45 100644 --- a/boron/source/mm/poolhdr.c +++ b/boron/source/mm/poolhdr.c @@ -25,10 +25,10 @@ typedef struct // Size of additional data is 32 bytes. (2xPtr=16 + 2xU64=16) // Note: Free space is 32 bytes #else - #define POOL_ENTRY_COUNT 127 + #define POOL_ENTRY_COUNT 64 // Size of a MIPOOL_ENTRY is 32 // Size of additional data is 24 bytes (2xPtr=8 + 2xU64=16) - // Note: Free space is 16 bytes + // Note: Free space is 8 bytes #endif MIPOOL_ENTRY Entries[POOL_ENTRY_COUNT]; @@ -96,6 +96,9 @@ PMIPOOL_ENTRY MiCreatePoolEntry() if (PoolEntry) { memset(PoolEntry, 0, sizeof *PoolEntry); + #ifdef MIPOOL_DUMMY_SIGNATURE + PoolEntry->Dummy = MIPOOL_DUMMY_SIGNATURE; + #endif KeReleaseSpinLock(&MmpPoolSlabListLock, Ipl); return PoolEntry; } @@ -121,6 +124,13 @@ PMIPOOL_ENTRY MiCreatePoolEntry() // Attempt to allocate as usual. PMIPOOL_ENTRY PoolEntry = MmpAllocateFromPoolEntrySlab(Item); + if (PoolEntry) + { + memset(PoolEntry, 0, sizeof *PoolEntry); + #ifdef MIPOOL_DUMMY_SIGNATURE + PoolEntry->Dummy = MIPOOL_DUMMY_SIGNATURE; + #endif + } KeReleaseSpinLock(&MmpPoolSlabListLock, Ipl); @@ -129,6 +139,13 @@ PMIPOOL_ENTRY MiCreatePoolEntry() void MiDeletePoolEntry(PMIPOOL_ENTRY Entry) { +#ifdef MIPOOL_DUMMY_SIGNATURE + ASSERT(Entry->Dummy == MIPOOL_DUMMY_SIGNATURE); + + ASSERT(Entry->ListEntry.Flink == NULL); + ASSERT(Entry->ListEntry.Blink == NULL); +#endif + // Get the containing slab. PMIPOOL_ENTRY_SLAB Slab = (PMIPOOL_ENTRY_SLAB) ((uintptr_t) Entry & ~(PAGE_SIZE - 1)); diff --git a/boron/source/mm/poolsup.c b/boron/source/mm/poolsup.c index 7c6b20a6..0ab7047e 100644 --- a/boron/source/mm/poolsup.c +++ b/boron/source/mm/poolsup.c @@ -50,10 +50,11 @@ static_assert(MM_PTE_ISPOOLHDR == (1 << 11)); MMPTE MiCalculatePoolHeaderPte(uintptr_t Handle) { + ASSERT(!(Handle & 0x7)); MMPTE_POOLHEADER PteHeader; PteHeader.Pte = 0; - PteHeader.B3to9 = (Handle >> 3) & 0x1FF; + PteHeader.B3to9 = (Handle >> 3) & 0x7F; PteHeader.B10to11 = (Handle >> 10) & 0x3; PteHeader.B12to31 = (Handle >> 12); PteHeader.IsPoolHdr = true; @@ -61,6 +62,22 @@ MMPTE MiCalculatePoolHeaderPte(uintptr_t Handle) return PteHeader.Pte; } +FORCE_INLINE +uintptr_t MiReconstructPoolHandleFromPte(MMPTE Pte) +{ + MMPTE_POOLHEADER PteHeader; + PteHeader.Pte = Pte; + + ASSERT(!PteHeader.Present); + ASSERT(!PteHeader.Committed); + ASSERT(PteHeader.IsPoolHdr); + + return + PteHeader.B3to9 << 3 | + PteHeader.B10to11 << 10 | + PteHeader.B12to31 << 12; +} + #else #define MiCalculatePoolHeaderPte(Handle) (((Handle) - MM_KERNEL_SPACE_BASE) | MM_PTE_ISPOOLHDR) @@ -149,15 +166,15 @@ MIPOOL_SPACE_HANDLE MmpSplitEntry(PMIPOOL_ENTRY PoolEntry, size_t SizeInPages, v // This entry manages the area directly after the PoolEntry does. PMIPOOL_ENTRY NewEntry = MiCreatePoolEntry(); - // TODO: Debug the firework test driver. When removing this, it doesn't throw up a nice - // page fault, but rather hits some weird code which sets the stack pointer to 0x000000017FFFFFFF - // and triple faults the kernel. if (!NewEntry) return 0; // Link it such that: // PoolEntry ====> NewEntry ====> PoolEntry->Flink ASSERT(NewEntry); + ASSERT(PoolEntry); + ASSERT(PoolEntry->ListEntry.Flink); + ASSERT(PoolEntry->ListEntry.Blink); InsertHeadList(&PoolEntry->ListEntry, &NewEntry->ListEntry); // Assign the other properties @@ -211,6 +228,9 @@ MIPOOL_SPACE_HANDLE MiReservePoolSpaceTaggedSub(size_t SizeInPages, void** Outpu return Handle; } + if (CurrentEntry->Flink == NULL) + KeCrash("HUH??! CurrentEntry->Flink is NULL! CurrentEntry: %p"); + CurrentEntry = CurrentEntry->Flink; } @@ -239,7 +259,11 @@ static void MmpTryConnectEntryWithItsFlink(PMIPOOL_ENTRY Entry) Entry->Size += Flink->Size; // remove the 'flink' entry - RemoveHeadList(&Entry->ListEntry); + RemoveEntryList(&Flink->ListEntry); + ASSERT(Entry->ListEntry.Flink != NULL); + ASSERT(Entry->ListEntry.Blink != NULL); + ASSERT(Flink->ListEntry.Flink == NULL); + ASSERT(Flink->ListEntry.Blink == NULL); MiDeletePoolEntry(Flink); } @@ -252,6 +276,9 @@ void MiFreePoolSpaceSub(MIPOOL_SPACE_HANDLE Handle) // Get the handle to the pool entry. PMIPOOL_ENTRY Entry = (PMIPOOL_ENTRY) Handle; + ASSERT(!(Handle & 0x7)); + ASSERT(Handle >= MM_KERNEL_SPACE_BASE); + ASSERT(MiReconstructPoolHandleFromPte(MiGetPoolSpaceHandleFromAddress(Entry)) == Handle); if (~Entry->Flags & MI_POOL_ENTRY_ALLOCATED) { @@ -262,7 +289,8 @@ void MiFreePoolSpaceSub(MIPOOL_SPACE_HANDLE Handle) Entry->Tag = MI_EMPTY_TAG; MmpTryConnectEntryWithItsFlink(Entry); - MmpTryConnectEntryWithItsFlink(MIP_BLINK(&Entry->ListEntry)); + if (Entry->ListEntry.Blink != &MmpPoolList) + MmpTryConnectEntryWithItsFlink(MIP_BLINK(&Entry->ListEntry)); KeReleaseSpinLock(&MmpPoolLock, OldIpl); } @@ -401,13 +429,7 @@ MIPOOL_SPACE_HANDLE MiGetPoolSpaceHandleFromAddress(void* AddressV) ASSERT(PAddress & MM_PTE_ISPOOLHDR); #ifdef IS_32_BIT - MMPTE_POOLHEADER PteHeader; - PteHeader.Pte = PAddress; - - PAddress = - PteHeader.B12to31 << 12 | - PteHeader.B10to11 << 10 | - PteHeader.B3to9 << 3; + PAddress = MiReconstructPoolHandleFromPte(PAddress); #else PAddress &= ~MM_PTE_ISPOOLHDR; PAddress += MM_KERNEL_SPACE_BASE; diff --git a/boron/source/mm/slab.c b/boron/source/mm/slab.c index a9decab3..bbd844c9 100644 --- a/boron/source/mm/slab.c +++ b/boron/source/mm/slab.c @@ -336,6 +336,10 @@ void MiSlabFree(void* Ptr) PHUGE_MEMORY_BLOCK Hmb = PageAlignedPtr; if (Hmb->Check == MI_HUGE_MEMORY_CHECK) { + // FIX ASAP + // TODO: this is vulnerable to attacks where someone fills the memory with this + // TODO TODO TODO + // Free it as a huge memory block. MmpFreeHuge(Hmb); return; diff --git a/drivers/test/source/fworktst.c b/drivers/test/source/fworktst.c index 31ce4e57..3f6ac0de 100644 --- a/drivers/test/source/fworktst.c +++ b/drivers/test/source/fworktst.c @@ -279,9 +279,6 @@ NO_RETURN void T_Particle(void* Parameter) NO_RETURN void T_Explodeable(UNUSED void* Parameter) { - KTIMER Timer; - KeInitializeTimer(&Timer); - FIREWORK_DATA Data; memset(&Data, 0, sizeof Data); @@ -375,9 +372,6 @@ void PerformFireworksTest() // The main thread occupies itself with spawning explodeables // from time to time, to keep things interesting. - KTIMER Timer; - KeInitializeTimer(&Timer); - while (true) { int SpawnCount = Rand() % 20 + 1; diff --git a/drivers/test/source/main.c b/drivers/test/source/main.c index 5a993333..92e7cbee 100644 --- a/drivers/test/source/main.c +++ b/drivers/test/source/main.c @@ -35,7 +35,11 @@ void PerformDelay(int Ms, PKDPC Dpc) KeInitializeTimer(&Timer); KeSetTimer(&Timer, Ms, Dpc); - KeWaitForSingleObject(&Timer.Header, false, TIMEOUT_INFINITE, MODE_KERNEL); + BSTATUS Status = KeWaitForSingleObject(&Timer.Header, false, TIMEOUT_INFINITE, MODE_KERNEL); + if (FAILED(Status)) + DbgPrint("PerformDelay FAILED. %d (%s)", Status, RtlGetStatusString(Status)); + + KeCancelTimer(&Timer); } void DumpHex(void* DataV, size_t DataSize, bool LogScreen) From 1f2a2d1c02e07a76b18c5fa84524b470f46e2ce9 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Sat, 18 Oct 2025 15:46:16 +0300 Subject: [PATCH 31/44] * Mm(i386): Add a way for pool header slabs to be mapped in without the HHDM. This is because these pool header slabs could be allocated at >256MB, at which point MmGetHHDMOffsetAddr() relies on the window system, so the addresses are never permanent, thus it starts crashing when the PMM starts giving out addresses bigger than 256 MB. --- boron/address_space_i386.txt | 6 ++ boron/source/build_number | 2 +- boron/source/ke/i386/foreinit.asm | 2 + boron/source/ldr/loader.c | 2 +- boron/source/mm/i386/idmap.c | 13 +++ boron/source/mm/mi.h | 25 ++++-- boron/source/mm/poolhdr.c | 136 ++++++++++++++++++++++++++---- boron/source/mm/poolsup.c | 21 +++-- boron/source/rtl/string.c | 4 + 9 files changed, 173 insertions(+), 38 deletions(-) diff --git a/boron/address_space_i386.txt b/boron/address_space_i386.txt index 12c6819d..9bacf2ba 100644 --- a/boron/address_space_i386.txt +++ b/boron/address_space_i386.txt @@ -9,9 +9,15 @@ BORON Operating System Address Space (i386) +------------------------------+ - 0xF0000000 | more dynamic pool space | +------------------------------+ - 0xE0000000 +| unused | ++------------------------------+ - 0xD8000000 | page frame data base | +------------------------------+ - 0xD4000000 | system module DLLs | ++------------------------------+ - 0xD2000000 +| unused | ++------------------------------+ - 0xD1800000 +| pool header slab magazines | +------------------------------+ - 0xD1000000 | fast mapping in 16MB windows | +------------------------------+ - 0xD0000000 diff --git a/boron/source/build_number b/boron/source/build_number index 4391a334..8d73f938 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -396 +422 diff --git a/boron/source/ke/i386/foreinit.asm b/boron/source/ke/i386/foreinit.asm index 2fc71174..3a3c3c17 100644 --- a/boron/source/ke/i386/foreinit.asm +++ b/boron/source/ke/i386/foreinit.asm @@ -127,6 +127,7 @@ global KiMultibootInfo global KiBootstrapPageDirectory global KiBootstrapPageTables global KiHhdmWindowPageTables +global KiPoolHeadersPageTables KiMultibootSignature: resd 1 KiMultibootInfo: resd 1 @@ -136,3 +137,4 @@ KiInitialStack: resb 4096 KiBootstrapPageDirectory: resb 4096 KiBootstrapPageTables: resb 64 * 4096 KiHhdmWindowPageTables: resb 4 * 4096 +KiPoolHeadersPageTables: resb 2 * 4096 diff --git a/boron/source/ldr/loader.c b/boron/source/ldr/loader.c index 9d7a7aac..d4b27663 100644 --- a/boron/source/ldr/loader.c +++ b/boron/source/ldr/loader.c @@ -21,7 +21,7 @@ static const char* LdrpHalPath = "halx86.sys"; #elif defined TARGET_I386 -static uintptr_t LdrpCurrentBase = 0xD1000000; +static uintptr_t LdrpCurrentBase = 0xD2000000; static const char* LdrpHalPath = "hali386.sys"; // sorry bucko, halx86 is already taken #else diff --git a/boron/source/mm/i386/idmap.c b/boron/source/mm/i386/idmap.c index bf17e381..359c4bab 100644 --- a/boron/source/mm/i386/idmap.c +++ b/boron/source/mm/i386/idmap.c @@ -19,6 +19,7 @@ Module name: extern uint8_t KiBootstrapPageTables[]; extern uint8_t KiHhdmWindowPageTables[]; +extern uint8_t KiPoolHeadersPageTables[]; void MiInitializeBaseIdentityMapping() { @@ -36,6 +37,7 @@ void MiInitializeBaseIdentityMapping() *Level1 = i | MM_PTE_READWRITE | MM_PTE_PRESENT; } + // Map the level 2 PTs for the HHDM window. for (size_t i = 0; i < MI_FASTMAP_SIZE; i += PAGE_SIZE * PAGE_SIZE / sizeof(MMPTE)) { uintptr_t Address = MI_FASTMAP_START + i; @@ -45,4 +47,15 @@ void MiInitializeBaseIdentityMapping() | MM_PTE_READWRITE | MM_PTE_PRESENT; } + + // Map the level 2 PTs for the pool headers. + for (size_t i = 0; i < MI_POOL_HEADERS_SIZE; i += PAGE_SIZE * PAGE_SIZE / sizeof(MMPTE)) + { + uintptr_t Address = MI_POOL_HEADERS_START + i; + + PMMPTE Level2 = (PMMPTE) MI_PTE_LOC(MI_PTE_LOC(Address)); + *Level2 = V2P(&KiPoolHeadersPageTables[(i >> 22) * PAGE_SIZE]) + | MM_PTE_READWRITE + | MM_PTE_PRESENT; + } } diff --git a/boron/source/mm/mi.h b/boron/source/mm/mi.h index 1223ddc3..efd430ba 100644 --- a/boron/source/mm/mi.h +++ b/boron/source/mm/mi.h @@ -174,19 +174,25 @@ HUGE_MEMORY_BLOCK, *PHUGE_MEMORY_BLOCK; typedef struct MIPOOL_ENTRY_tag { + LIST_ENTRY ListEntry; + int Flags; + int Tag; + uintptr_t UserData; + uintptr_t Address; + size_t Size; #ifdef IS_32_BIT int Dummy; #define MIPOOL_DUMMY_SIGNATURE 0x12345678 #endif - int Flags; // Qword 2 - int Tag; - uintptr_t UserData; // Qword 3 - uintptr_t Address; // Qword 4 - size_t Size; // Qword 5, size is in pages. - LIST_ENTRY ListEntry; // Qword 0, 1 } MIPOOL_ENTRY, *PMIPOOL_ENTRY; +#ifdef IS_64_BIT +static_assert(sizeof(MIPOOL_ENTRY) == 48); +#else +static_assert(sizeof(MIPOOL_ENTRY) == 32); +#endif + typedef enum MIPOOL_ENTRY_FLAGS_tag { MI_POOL_ENTRY_ALLOCATED = (1 << 0), @@ -234,10 +240,15 @@ MIPOOL_SPACE_HANDLE MiGetPoolSpaceHandleFromAddress(void* Address); void MiDumpPoolInfo(); // ===== Pool entry allocator ===== -// Really simple allocator that dishes out pool entries. To get rid of the pool allocator's dependency on the slab allocator. +// Really simple allocator that dishes out pool entries. To get rid of the pool allocator's +// dependency on the slab allocator. // The dependency chart will now look like this: // [PoolEntryAllocator] <----- [PoolAllocator] <----- [SlabAllocator] +#ifdef IS_32_BIT +#define MI_POOL_HEADERS_START (0xD1000000U) +#define MI_POOL_HEADERS_SIZE (0x00800000U) +#endif PMIPOOL_ENTRY MiCreatePoolEntry(); diff --git a/boron/source/mm/poolhdr.c b/boron/source/mm/poolhdr.c index 9dea6c45..819a49cc 100644 --- a/boron/source/mm/poolhdr.c +++ b/boron/source/mm/poolhdr.c @@ -81,6 +81,113 @@ void MiInitPoolEntryAllocator() InitializeListHead(&MmpPoolSlabList); } +#ifdef IS_64_BIT + +PMIPOOL_ENTRY_SLAB MiAllocatePoolHeaderSlab() +{ + MMPFN Pfn = MmAllocatePhysicalPage(); + if (Pfn == PFN_INVALID) + { + DbgPrint("WARNING in MiAllocatePoolHeaderSlab: No physical memory left!"); + return NULL; + } + + // HHDM is supported on 64-bit, so just return this + return MmGetHHDMOffsetAddr(Pfn); +} + +void MiFreePoolHeaderSlab(PMIPOOL_ENTRY_SLAB Address) +{ + MMPFN Pfn = MmPhysPageToPFN(MmGetHHDMOffsetFromAddr(Address)); + MmFreePhysicalPage(Pfn); +} + +#else + +#define POOL_HDR_MAX_PFNS_MAPPED (MI_POOL_HEADERS_SIZE / PAGE_SIZE) + +static KSPIN_LOCK MiPoolHeaderMapLock; +static uint32_t MiPoolHeaderPfnsMappedBitmap[POOL_HDR_MAX_PFNS_MAPPED / 32]; +static_assert(ARRAY_COUNT(MiPoolHeaderPfnsMappedBitmap) == 64); + +PMIPOOL_ENTRY_SLAB MiAllocatePoolHeaderSlab() +{ + MMPFN Pfn = MmAllocatePhysicalPage(); + if (Pfn == PFN_INVALID) + { + DbgPrint("WARNING in MiAllocatePoolHeaderSlab: No physical memory left!"); + return NULL; + } + + KIPL Ipl; + KeAcquireSpinLock(&MiPoolHeaderMapLock, &Ipl); + + // Find a place in the bitmap + const size_t NotFound = 0xFFFFFFFF; + size_t IndexFound = NotFound; + + for (size_t i = 0; i < POOL_HDR_MAX_PFNS_MAPPED / 32 && IndexFound == NotFound; i++) + { + if (MiPoolHeaderPfnsMappedBitmap[i] == 0xFFFFFFFF) + continue; + + // claim the first clear bit + for (int k = 0; k < 32; k++) + { + if (~MiPoolHeaderPfnsMappedBitmap[i] & (1 << k)) + { + MiPoolHeaderPfnsMappedBitmap[i] |= 1 << k; + IndexFound = i * 32 + k; + break; + } + } + } + + if (IndexFound == NotFound) + { + DbgPrint("WARNING in MiAllocatePoolHeaderSlab: No more space left!"); + KeReleaseSpinLock(&MiPoolHeaderMapLock, Ipl); + MmFreePhysicalPage(Pfn); + return NULL; + } + + void* Address = (void*)(MI_POOL_HEADERS_START + IndexFound * PAGE_SIZE); + + PMMPTE Pte = (PMMPTE) MI_PTE_LOC((uintptr_t) Address); + *Pte = MmPFNToPhysPage(Pfn) | MM_PTE_PRESENT | MM_PTE_READWRITE; + KeInvalidatePage(Pte); + + KeReleaseSpinLock(&MiPoolHeaderMapLock, Ipl); + return Address; +} + +void MiFreePoolHeaderSlab(PMIPOOL_ENTRY_SLAB Address) +{ + KIPL Ipl; + KeAcquireSpinLock(&MiPoolHeaderMapLock, &Ipl); + + PMMPTE Pte = (PMMPTE) MI_PTE_LOC((uintptr_t) Address); + MMPFN Pfn = MmPhysPageToPFN(*Pte & MM_PTE_ADDRESSMASK); + + // clear the PTE + *Pte = 0; + KeInvalidatePage(Address); + + // then free + MmFreePhysicalPage(Pfn); + + // and mark as unmapped + uint32_t Index = ((uintptr_t)Address - MI_POOL_HEADERS_START) / PAGE_SIZE; + uint32_t BIndex = Index >> 5, BSubIndex = Index & 0x1F; + + ASSERT(MiPoolHeaderPfnsMappedBitmap[BIndex] & (1 << BSubIndex)); + MiPoolHeaderPfnsMappedBitmap[BIndex] &= ~(1 << BSubIndex); + + KeReleaseSpinLock(&MiPoolHeaderMapLock, Ipl); +} + +#endif + PMIPOOL_ENTRY MiCreatePoolEntry() { KIPL Ipl; @@ -107,33 +214,29 @@ PMIPOOL_ENTRY MiCreatePoolEntry() } // Have to allocate a new entry. - int Pfn = MmAllocatePhysicalPage(); - if (Pfn == PFN_INVALID) + PMIPOOL_ENTRY_SLAB Item = MiAllocatePoolHeaderSlab(); + if (!Item) { - DbgPrint("MmpSlabContainerAllocate: Run out of memory! What will we do?!"); - // TODO: invoke the out of memory handler here, then try again KeReleaseSpinLock(&MmpPoolSlabListLock, Ipl); return NULL; } - PMIPOOL_ENTRY_SLAB Item = MmGetHHDMOffsetAddr(MmPFNToPhysPage(Pfn)); memset(Item, 0, sizeof *Item); // Add it to the list. InsertHeadList(&MmpPoolSlabList, &Item->ListEntry); - // Attempt to allocate as usual. + // Attempt to allocate as usual. This should succeed, + // because we just freshly allocated a new entry. PMIPOOL_ENTRY PoolEntry = MmpAllocateFromPoolEntrySlab(Item); - if (PoolEntry) - { - memset(PoolEntry, 0, sizeof *PoolEntry); - #ifdef MIPOOL_DUMMY_SIGNATURE - PoolEntry->Dummy = MIPOOL_DUMMY_SIGNATURE; - #endif - } + ASSERT(PoolEntry); - KeReleaseSpinLock(&MmpPoolSlabListLock, Ipl); + memset(PoolEntry, 0, sizeof *PoolEntry); +#ifdef MIPOOL_DUMMY_SIGNATURE + PoolEntry->Dummy = MIPOOL_DUMMY_SIGNATURE; +#endif + KeReleaseSpinLock(&MmpPoolSlabListLock, Ipl); return PoolEntry; } @@ -162,9 +265,6 @@ void MiDeletePoolEntry(PMIPOOL_ENTRY Entry) if (Slab->Bitmap[0] == 0 && Slab->Bitmap[1] == 0) { RemoveEntryList(&Slab->ListEntry); - - int Pfn = MmPhysPageToPFN(MmGetHHDMOffsetFromAddr(Slab)); - - MmFreePhysicalPage(Pfn); + MiFreePoolHeaderSlab(Slab); } } diff --git a/boron/source/mm/poolsup.c b/boron/source/mm/poolsup.c index 0ab7047e..46ac5594 100644 --- a/boron/source/mm/poolsup.c +++ b/boron/source/mm/poolsup.c @@ -80,7 +80,9 @@ uintptr_t MiReconstructPoolHandleFromPte(MMPTE Pte) #else -#define MiCalculatePoolHeaderPte(Handle) (((Handle) - MM_KERNEL_SPACE_BASE) | MM_PTE_ISPOOLHDR) +#define MiCalculatePoolHeaderPte(Handle) (((uintptr_t)(Handle) - MM_KERNEL_SPACE_BASE) | MM_PTE_ISPOOLHDR) + +#define MiReconstructPoolHandleFromPte(Pte) ((MIPOOL_SPACE_HANDLE)(((Pte) & ~MM_PTE_ISPOOLHDR) + MM_KERNEL_SPACE_BASE)) #endif @@ -209,8 +211,10 @@ MIPOOL_SPACE_HANDLE MiReservePoolSpaceTaggedSub(size_t SizeInPages, void** Outpu while (CurrentEntry != &MmpPoolList) { +#ifdef DEBUG if (CurrentEntry == NULL) - KeCrash("HUH??! CurrentEntry is NULL"); + KeCrash("HUH?!? CurrentEntry is NULL"); +#endif // Skip allocated entries. PMIPOOL_ENTRY Current = MIP_CURRENT(CurrentEntry); @@ -228,8 +232,10 @@ MIPOOL_SPACE_HANDLE MiReservePoolSpaceTaggedSub(size_t SizeInPages, void** Outpu return Handle; } +#ifdef DEBUG if (CurrentEntry->Flink == NULL) - KeCrash("HUH??! CurrentEntry->Flink is NULL! CurrentEntry: %p"); + KeCrash("HUH?!? CurrentEntry->Flink is NULL! CurrentEntry: %p"); +#endif CurrentEntry = CurrentEntry->Flink; } @@ -278,7 +284,7 @@ void MiFreePoolSpaceSub(MIPOOL_SPACE_HANDLE Handle) PMIPOOL_ENTRY Entry = (PMIPOOL_ENTRY) Handle; ASSERT(!(Handle & 0x7)); ASSERT(Handle >= MM_KERNEL_SPACE_BASE); - ASSERT(MiReconstructPoolHandleFromPte(MiGetPoolSpaceHandleFromAddress(Entry)) == Handle); + ASSERT(MiReconstructPoolHandleFromPte(MiCalculatePoolHeaderPte(Handle)) == Handle); if (~Entry->Flags & MI_POOL_ENTRY_ALLOCATED) { @@ -427,14 +433,7 @@ MIPOOL_SPACE_HANDLE MiGetPoolSpaceHandleFromAddress(void* AddressV) // the valid bit set. uintptr_t PAddress = *PtePtr; ASSERT(PAddress & MM_PTE_ISPOOLHDR); - -#ifdef IS_32_BIT PAddress = MiReconstructPoolHandleFromPte(PAddress); -#else - PAddress &= ~MM_PTE_ISPOOLHDR; - PAddress += MM_KERNEL_SPACE_BASE; -#endif - MIPOOL_SPACE_HANDLE Handle = PAddress; MmUnlockKernelSpace(); return Handle; diff --git a/boron/source/rtl/string.c b/boron/source/rtl/string.c index 2ceb6d9d..a9d071a4 100644 --- a/boron/source/rtl/string.c +++ b/boron/source/rtl/string.c @@ -64,6 +64,10 @@ void* memset(void* dst, int c, size_t n) size_t strlen(const char * s) { +#if defined KERNEL && defined DEBUG + if (!s) + KeCrash("strlen(NULL)"); +#endif // Count the amount of non-zero characters in the null-terminated string. size_t sz = 0; while (*s++) sz++; From 6e029132bec30745dcd47f43ff08100f2bc55b6a Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Sat, 18 Oct 2025 18:19:52 +0300 Subject: [PATCH 32/44] * Mm: Add MmBeginUsingHHDM() and MmEndUsingHHDM() - for 32-bit, this is important. * Mm: Switch to MmGetPteLocation/MmGetPteLocationCheck. * Mm: Remove the `HPAGEMAP Mapping` parameter from several page table functions. * Mm: Actually allow MmMapIoSpace addresses to be freed properly using MmFreePoolBig. * Ke: Use a hardcoded PRCB list for non-SMP builds. * Ke(amd64): Make the boot page mapping have recursive page mappings setup. * Framebuffer: Use MmMapIoSpace for temporary mappings on 32-bit builds. * Halx86(amd64): Use MmMapIoSpace instead of doing it manually. --- boron/include/hal/pci.h | 4 + boron/include/mm/pmm.h | 12 ++ boron/include/mm/pool.h | 5 + boron/include/mm/pt.h | 16 +- boron/source/build_number | 2 +- boron/source/ke/amd64/boot.c | 5 + boron/source/ke/i386/boot.c | 3 +- boron/source/ke/process.c | 2 +- boron/source/ke/smp.c | 22 +++ boron/source/ldr/dll.c | 7 +- boron/source/mm/amd64/pt.c | 59 +++---- boron/source/mm/cache.c | 2 + boron/source/mm/fault.c | 2 + boron/source/mm/i386/pt.c | 277 +++++++-------------------------- boron/source/mm/mdl.c | 29 +++- boron/source/mm/pmm.c | 107 ++++++++----- boron/source/mm/pool.c | 27 ++-- boron/source/mm/poolhdr.c | 2 +- boron/source/mm/poolsup.c | 14 +- boron/source/mm/reclaim.c | 3 +- boron/source/mm/teardown.c | 16 +- drivers/framebuf/source/main.c | 40 +++++ drivers/halx86/source/acpi.c | 22 +-- drivers/halx86/source/hpet.c | 21 +-- drivers/test/source/mdltst.c | 4 +- drivers/test/source/mm1tst.c | 14 +- drivers/test/source/proctst.c | 6 +- 27 files changed, 338 insertions(+), 385 deletions(-) diff --git a/boron/include/hal/pci.h b/boron/include/hal/pci.h index ddb97809..32ca0996 100644 --- a/boron/include/hal/pci.h +++ b/boron/include/hal/pci.h @@ -316,10 +316,14 @@ void HalPciMsixSetInterrupt(PPCI_DEVICE Device, int Index, uint8_t ProcessorId, { uintptr_t Address = HalPciReadBarAddress(&Device->Address, Device->MsixData.Bir); + MmBeginUsingHHDM(); + PPCI_MSIX_TABLE_ENTRY Table = MmGetHHDMOffsetAddr(Address + Device->MsixData.TableOffset); Table += Index; Table->Address = HalPciMsiCreateAddress(ProcessorId); Table->MessageData = HalPciMsiCreateMessage(Vector, EdgeTrigger, Deassert); Table->VectorControl = 0; + + MmEndUsingHHDM(); } diff --git a/boron/include/mm/pmm.h b/boron/include/mm/pmm.h index 8084c77c..07c29b39 100644 --- a/boron/include/mm/pmm.h +++ b/boron/include/mm/pmm.h @@ -41,6 +41,18 @@ uintptr_t MmGetHHDMOffsetFromAddr(void* Addr); // Converts a physical address to a page frame number (PFN). MMPFN MmPhysPageToPFN(uintptr_t PhysAddr); +#ifdef IS_32_BIT + +void MmBeginUsingHHDM(void); +void MmEndUsingHHDM(void); + +#else + +#define MmBeginUsingHHDM() +#define MmEndUsingHHDM() + +#endif + // Converts a page frame number (PFN) to a physical page. uintptr_t MmPFNToPhysPage(MMPFN Pfn); diff --git a/boron/include/mm/pool.h b/boron/include/mm/pool.h index fc305ba5..ae84b01a 100644 --- a/boron/include/mm/pool.h +++ b/boron/include/mm/pool.h @@ -24,6 +24,10 @@ typedef int POOL_TYPE; // but the range itself is unmapped #define POOL_FLAG_CALLER_CONTROLLED (1 << 1) +// If this flag is set, then the PTEs will be unmapped automatically +// even if POOL_FLAG_CALLER_CONTROLLED is set. +#define POOL_FLAG_UNMAP_ANYWAY (1 << 2) + // Redundant, could just pass 0 #define POOL_PAGED (0) @@ -35,6 +39,7 @@ typedef int POOL_TYPE; void* MmAllocatePoolBig(int PoolFlags, size_t PageCount, int Tag); +// NOTE: This accepts any offset within the page. So even MmMapIoSpace mapped items can be freed with this. void MmFreePoolBig(void* Address); size_t MmGetSizeFromPoolAddress(void* Address); diff --git a/boron/include/mm/pt.h b/boron/include/mm/pt.h index 3a9d4c97..2926864c 100644 --- a/boron/include/mm/pt.h +++ b/boron/include/mm/pt.h @@ -51,17 +51,11 @@ void MmUnlockSpace(KIPL OldIpl, uintptr_t DecidingAddress); HPAGEMAP MiGetCurrentPageMap(); // Creates a page mapping. -HPAGEMAP MiCreatePageMapping(HPAGEMAP OldPageMapping); +HPAGEMAP MiCreatePageMapping(); // Deletes a page mapping. void MiFreePageMapping(HPAGEMAP OldPageMapping); -// Resolves a page table entry pointer (virtual address offset by HHDM) relative to an address. -// Can allocate the missing page mapping levels on its way if the flag is set. -// If on its way, it hits a higher page size, currently it will return null since it's not really -// designed for that. -PMMPTE MiGetPTEPointer(HPAGEMAP Mapping, uintptr_t Address, bool AllocateMissingPMLs); - // Check if the PTE for a certain VA exists at the recursive PTE address, in the // current page mapping. // @@ -88,16 +82,16 @@ PMMPTE MmGetPteLocation(uintptr_t Address); PMMPTE MmGetPteLocationCheck(uintptr_t Address, bool GenerateMissingLevels); // Attempts to map a physical page into the specified address space. -bool MiMapAnonPage(HPAGEMAP Mapping, uintptr_t Address, uintptr_t Permissions, bool NonPaged); +bool MiMapAnonPage(uintptr_t Address, uintptr_t Permissions, bool NonPaged); // Attempts to map several anonymous pages into the specified address space. -bool MiMapAnonPages(HPAGEMAP Mapping, uintptr_t Address, size_t SizePages, uintptr_t Permissions, bool NonPaged); +bool MiMapAnonPages(uintptr_t Address, size_t SizePages, uintptr_t Permissions, bool NonPaged); // Attempts to map a known physical page into the specified address space. -bool MiMapPhysicalPage(HPAGEMAP Mapping, uintptr_t PhysicalPage, uintptr_t Address, uintptr_t Permissions); +bool MiMapPhysicalPage(uintptr_t PhysicalPage, uintptr_t Address, uintptr_t Permissions); // Unmaps some memory. Automatically frees it if it is handled by the PMM. -void MiUnmapPages(HPAGEMAP Mapping, uintptr_t Address, size_t LengthPages); +void MiUnmapPages(uintptr_t Address, size_t LengthPages); // Handles a page fault. Returns whether or not the page fault was handled. // TODO make it MiPageFault and export it only to ke/except diff --git a/boron/source/build_number b/boron/source/build_number index 8d73f938..e5a135a5 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -422 +445 diff --git a/boron/source/ke/amd64/boot.c b/boron/source/ke/amd64/boot.c index 71c2788b..a5ea9dbd 100644 --- a/boron/source/ke/amd64/boot.c +++ b/boron/source/ke/amd64/boot.c @@ -203,6 +203,11 @@ void KiInitLoaderParameterBlock() CHECK_RESPONSE(KeLimineKernelFileRequest); CHECK_RESPONSE(KeLimineBootloaderInfoRequest); + // Initialize the recursive paging mechanism. + HPAGEMAP PageMap = MiGetCurrentPageMap(); + PMMPTE Pte = MmGetHHDMOffsetAddr(PageMap); + Pte[MI_RECURSIVE_PAGING_START] = PageMap | MM_PTE_PRESENT | MM_PTE_READWRITE | MM_PTE_NOEXEC; + // Initialize the memory regions. struct limine_memmap_response* MemMapResponse = KeLimineMemMapRequest.response; if (MemMapResponse->entry_count >= MAX_MEMORY_REGIONS) diff --git a/boron/source/ke/i386/boot.c b/boron/source/ke/i386/boot.c index 97a0eb16..eb95cc8d 100644 --- a/boron/source/ke/i386/boot.c +++ b/boron/source/ke/i386/boot.c @@ -53,7 +53,8 @@ static void* KiEarlyAllocateMemoryFromMemMap(size_t Size) Entry->Base += Size; Entry->Size -= Size; - return (void*) MmGetHHDMOffsetAddr(CurrAddr); + ASSERT(CurrAddr < MI_IDENTMAP_SIZE); + return (void*) (MI_IDENTMAP_START + CurrAddr); } KeCrashBeforeSMPInit("Error, out of memory in KiEarlyAllocateMemoryFromMemMap"); diff --git a/boron/source/ke/process.c b/boron/source/ke/process.c index 668fa7fe..5c295459 100644 --- a/boron/source/ke/process.c +++ b/boron/source/ke/process.c @@ -50,7 +50,7 @@ BSTATUS KeInitializeProcess(PKPROCESS Process, int BasePriority, KAFFINITY BaseA { KeInitializeDispatchHeader(&Process->Header, DISPATCH_PROCESS); - Process->PageMap = MiCreatePageMapping(KeGetCurrentPageTable()); + Process->PageMap = MiCreatePageMapping(); if (Process->PageMap == 0) return STATUS_INSUFFICIENT_MEMORY; diff --git a/boron/source/ke/smp.c b/boron/source/ke/smp.c index cb967871..a56bcb8c 100644 --- a/boron/source/ke/smp.c +++ b/boron/source/ke/smp.c @@ -159,6 +159,11 @@ NO_RETURN void KeCrashBeforeSMPInit(const char* message, ...) void PsInitSystemProcess(); +#ifndef CONFIG_SMP +static KPRCB KiPrcb; +static PKPRCB KiPrcbList[1]; +#endif + NO_RETURN INIT void KeInitSMP() { @@ -179,12 +184,24 @@ void KeInitSMP() if (MpInfo->Count > ProcessorLimit) KeCrashBeforeSMPInit("Error, unsupported amount of CPUs: %llu (limit is %llu)", MpInfo->Count, ProcessorLimit); +#ifdef CONFIG_SMP int cpuListPFN = MmAllocatePhysicalPage(); if (cpuListPFN == PFN_INVALID) KeCrashBeforeSMPInit("Error, can't initialize CPU list, we don't have enough memory"); + // TODO: don't use MmGetHHDMOffsetAddr on 32-bit builds? KeProcessorList = MmGetHHDMOffsetAddr(MmPFNToPhysPage(cpuListPFN)); KeProcessorCount = MpInfo->Count; +#else + KeProcessorList = KiPrcbList; + KeProcessorCount = 1; + + if (MpInfo->Count >= 1) + { + DbgPrint("%zu processors provided by the loader, but we only support one"); + MpInfo->Count = 1; + } +#endif // Initialize all the CPUs in series. for (uint64_t i = 0; i < MpInfo->Count; i++) @@ -199,7 +216,12 @@ void KeInitSMP() KeCrashBeforeSMPInit("Error, can't initialize CPUs, we don't have enough memory"); } + #ifdef CONFIG_SMP PKPRCB Prcb = MmGetHHDMOffsetAddr(MmPFNToPhysPage(PrcbPfn)); + #else + PKPRCB Prcb = &KiPrcb; + ASSERT(MpInfo->Count == 1); + #endif memset(Prcb, 0, sizeof *Prcb); // initialize the struct diff --git a/boron/source/ldr/dll.c b/boron/source/ldr/dll.c index 5c2773fd..5e48715a 100644 --- a/boron/source/ldr/dll.c +++ b/boron/source/ldr/dll.c @@ -56,8 +56,6 @@ static void LdriMapInProgramHeader(PLOADER_MODULE File, PELF_PROGRAM_HEADER Phdr Permissions |= MM_PTE_READWRITE; - HPAGEMAP PageMap = MiGetCurrentPageMap(); - // Now map it in. uintptr_t VirtAddrBackup = VirtAddr; for (size_t i = 0; i < SizePages; i++) @@ -70,7 +68,7 @@ static void LdriMapInProgramHeader(PLOADER_MODULE File, PELF_PROGRAM_HEADER Phdr // care of zero-filling everything. But not here. // Some entries overlap. Check if there's already a PTE beforehand. - PMMPTE Pte = MiGetPTEPointer(PageMap, VirtAddr, false); + PMMPTE Pte = MmGetPteLocationCheck(VirtAddr, false); if (Pte && (*Pte & MM_PTE_PRESENT)) { MMPTE OldPte = *Pte; @@ -97,8 +95,7 @@ static void LdriMapInProgramHeader(PLOADER_MODULE File, PELF_PROGRAM_HEADER Phdr uintptr_t Page = MmPFNToPhysPage(Pfn); - if (!MiMapPhysicalPage(PageMap, - Page, + if (!MiMapPhysicalPage(Page, VirtAddr, Permissions)) KeCrashBeforeSMPInit("Can't map in program header to virtual address %p!", VirtAddr); diff --git a/boron/source/mm/amd64/pt.c b/boron/source/mm/amd64/pt.c index 31703ea7..6253441a 100644 --- a/boron/source/mm/amd64/pt.c +++ b/boron/source/mm/amd64/pt.c @@ -19,6 +19,10 @@ Module name: #include #include "../mi.h" +// TODO: Rewrite this so that MmCheckPteLocation spawns the locations directly +// instead of relying on the ancient MiGetPTEPointer. +PMMPTE MiGetPTEPointer(HPAGEMAP Mapping, uintptr_t Address, bool AllocateMissingPMLs); + PMMPTE MmGetPteLocation(uintptr_t Address) { PMMPTE PtePtr = (PMMPTE)MI_PTE_LOC(Address); @@ -76,9 +80,10 @@ PMMPTE MmGetPteLocationCheck(uintptr_t Address, bool GenerateMissingLevels) } // Creates a page mapping. -HPAGEMAP MiCreatePageMapping(HPAGEMAP OldPageMapping) +HPAGEMAP MiCreatePageMapping() { // Allocate the PML4. + HPAGEMAP OldPageMapping = KeGetCurrentPageTable(); int NewPageMappingPFN = MmAllocatePhysicalPage(); if (NewPageMappingPFN == PFN_INVALID) { @@ -166,7 +171,7 @@ bool MmpCloneUserHalfLevel(int Level, PMMPTE New, PMMPTE Old, int Index) // Clones a user page mapping HPAGEMAP MmClonePageMapping(HPAGEMAP OldPageMapping) { - HPAGEMAP NewPageMapping = MiCreatePageMapping(OldPageMapping); + HPAGEMAP NewPageMapping = MiCreatePageMapping(); // If the new page mapping is zero, we can't proceed! if (NewPageMapping == 0) @@ -449,8 +454,9 @@ static bool MmpMapSingleAnonPageAtPte(PMMPTE Pte, uintptr_t Permissions, bool No Return value: Whether the mapping update was successful. ***/ -bool MiMapAnonPage(HPAGEMAP Mapping, uintptr_t Address, uintptr_t Permissions, bool NonPaged) +bool MiMapAnonPage(uintptr_t Address, uintptr_t Permissions, bool NonPaged) { + HPAGEMAP Mapping = MiGetCurrentPageMap(); PMMPTE Pte = MiGetPTEPointer(Mapping, Address, true); return MmpMapSingleAnonPageAtPte(Pte, Permissions, NonPaged); @@ -476,8 +482,9 @@ bool MiMapAnonPage(HPAGEMAP Mapping, uintptr_t Address, uintptr_t Permissions, b Return value: Whether the mapping update was successful. ***/ -bool MiMapPhysicalPage(HPAGEMAP Mapping, uintptr_t PhysicalPage, uintptr_t Address, uintptr_t Permissions) +bool MiMapPhysicalPage(uintptr_t PhysicalPage, uintptr_t Address, uintptr_t Permissions) { + HPAGEMAP Mapping = MiGetCurrentPageMap(); PMMPTE Pte = MiGetPTEPointer(Mapping, Address, true); if (!Pte) @@ -500,8 +507,10 @@ bool MiMapPhysicalPage(HPAGEMAP Mapping, uintptr_t PhysicalPage, uintptr_t Addre Return value: None. ***/ -void MiUnmapPages(HPAGEMAP Mapping, uintptr_t Address, size_t LengthPages) +void MiUnmapPages(uintptr_t Address, size_t LengthPages) { + HPAGEMAP Mapping = MiGetCurrentPageMap(); + // Step 1. Unset the PRESENT bit on all pages in the range. for (size_t i = 0; i < LengthPages; i++) { @@ -556,40 +565,6 @@ void MiUnmapPages(HPAGEMAP Mapping, uintptr_t Address, size_t LengthPages) } } -/*** - Function description: - Prepares the pool manager. Since this is hardware specific, - this was split away from the pool manager. - - Parameters: - The page map to be modified. Ideally, this function affects - ALL page maps. - - Return value: - None. - - Notes: - This function is run during uniprocessor system initialization. -***/ -void MiPrepareGlobalAreaForPool(HPAGEMAP PageMap) -{ - PMMPTE Ptes = MmGetHHDMOffsetAddr(PageMap); - - int Pfn = MmAllocatePhysicalPage(); - if (Pfn == PFN_INVALID) - { - KeCrashBeforeSMPInit("MiPrepareGlobalAreaForPool: Can't allocate global pml4"); - } - - Ptes[MI_GLOBAL_AREA_START] = - MM_PTE_PRESENT | - MM_PTE_READWRITE | - MM_PTE_GLOBAL | - MM_PTE_ISFROMPMM | - MM_PTE_NOEXEC | - MmPFNToPhysPage(Pfn); -} - /*** Function description: Gets the top of the pool managed area. This was split away @@ -637,8 +612,10 @@ uintptr_t MiGetTopOfPoolManagedArea() Return value: Whether the allocation was successful. ***/ -bool MiMapAnonPages(HPAGEMAP Mapping, uintptr_t Address, size_t SizePages, uintptr_t Permissions, bool NonPaged) +bool MiMapAnonPages(uintptr_t Address, size_t SizePages, uintptr_t Permissions, bool NonPaged) { + HPAGEMAP Mapping = MiGetCurrentPageMap(); + // As an optimization, we'll wait until the PML1 index rolls over to zero before reloading the PTE pointer. uint64_t CurrentPml1 = PML1_IDX(Address); size_t DonePages = 0; @@ -670,7 +647,7 @@ bool MiMapAnonPages(HPAGEMAP Mapping, uintptr_t Address, size_t SizePages, uintp ROLLBACK: // Unmap all the pages that we have mapped. - MiUnmapPages(Mapping, Address, DonePages); + MiUnmapPages(Address, DonePages); return false; } diff --git a/boron/source/mm/cache.c b/boron/source/mm/cache.c index 84824bd4..d37aab95 100644 --- a/boron/source/mm/cache.c +++ b/boron/source/mm/cache.c @@ -14,6 +14,8 @@ Module name: ***/ #include "mi.h" +// ***TODO: Rewrite this for 32-bit builds*** + void MmInitializeCcb(PCCB Ccb) { memset(Ccb, 0, sizeof *Ccb); diff --git a/boron/source/mm/fault.c b/boron/source/mm/fault.c index 6992603b..10d23f4c 100644 --- a/boron/source/mm/fault.c +++ b/boron/source/mm/fault.c @@ -538,8 +538,10 @@ BSTATUS MiWriteFault(UNUSED PEPROCESS Process, uintptr_t Va, PMMPTE PtePtr) return STATUS_REFAULT_SLEEP; } + MmBeginUsingHHDM(); void* Address = MmGetHHDMOffsetAddr(MmPFNToPhysPage(NewPfn)); memcpy(Address, (void*)(Va & ~(PAGE_SIZE - 1)), PAGE_SIZE); + MmEndUsingHHDM(); // Now assign the new PFN. *PtePtr = diff --git a/boron/source/mm/i386/pt.c b/boron/source/mm/i386/pt.c index e1043620..3768d601 100644 --- a/boron/source/mm/i386/pt.c +++ b/boron/source/mm/i386/pt.c @@ -36,34 +36,31 @@ PMMPTE MmGetPteLocation(uintptr_t Address) bool MmCheckPteLocation(uintptr_t Address, bool GenerateMissingLevels) { - ASSERT(Address < MI_PML1_LOCATION || (uint64_t)Address >= MI_PML1_LOC_END); - - // Check PML4, that's always accessible. PMMPTE Pte; + MMPTE SupervisorBit; - Pte = MmGetPteLocation(MI_PTE_LOC(MI_PTE_LOC(MI_PTE_LOC(Address)))); - if (~(*Pte) & MM_PTE_PRESENT) - goto Missing; + ASSERT(Address < MI_PML1_LOCATION || (uint64_t)Address >= MI_PML1_LOC_END); - // PML4 exists, check PML3 - Pte = MmGetPteLocation(MI_PTE_LOC(MI_PTE_LOC(Address))); - if (~(*Pte) & MM_PTE_PRESENT) - goto Missing; + if (Address >= MM_KERNEL_SPACE_BASE) + SupervisorBit = 0; + else + SupervisorBit = MM_PTE_USERACCESS; - // PML3 exists, check PML2 + // Check the presence of the PT Pte = MmGetPteLocation(MI_PTE_LOC(Address)); if (~(*Pte) & MM_PTE_PRESENT) { - Missing: - if (GenerateMissingLevels) - { - PMMPTE Pte = MiGetPTEPointer(MiGetCurrentPageMap(), Address, GenerateMissingLevels); - return Pte != NULL; - } + if (!GenerateMissingLevels) + return false; - return false; + MMPFN PtAllocated = MmAllocatePhysicalPage(); + if (PtAllocated == PFN_INVALID) + return false; + + *Pte = MmPFNToPhysPage(PtAllocated) | MM_PTE_PRESENT | MM_PTE_READWRITE | SupervisorBit; } + // Page table exists. return true; } @@ -76,7 +73,7 @@ PMMPTE MmGetPteLocationCheck(uintptr_t Address, bool GenerateMissingLevels) } // Creates a page mapping. -HPAGEMAP MiCreatePageMapping(HPAGEMAP OldPageMapping) +HPAGEMAP MiCreatePageMapping() { // Allocate the PML2. int NewPageMappingPFN = MmAllocatePhysicalPage(); @@ -87,179 +84,50 @@ HPAGEMAP MiCreatePageMapping(HPAGEMAP OldPageMapping) } uintptr_t NewPageMappingResult = MmPFNToPhysPage (NewPageMappingPFN); - PMMPTE NewPageMappingAccess = MmGetHHDMOffsetAddr (NewPageMappingResult), OldPageMappingAccess; - //PMMPTE OldPageMappingAccess = MmGetHHDMOffsetAddr (OldPageMapping); - - // copy the kernel's 512 entries, and zero out the first 512 - for (int i = 0; i < 512; i++) - { - NewPageMappingAccess[i] = 0; - } // Lock the kernel space's lock to not get any surprises. MmLockKernelSpaceShared(); + MmBeginUsingHHDM(); + PMMPTE NewPageMappingAccess = MmGetHHDMOffsetAddr (NewPageMappingResult); + PMMPTE OldPageDirectory = (PMMPTE) MI_PML2_LOCATION; + + // zero out the first 512 + for (int i = 0; i < 512; i++) + NewPageMappingAccess[i] = 0; + + // then copy out the kernel's latter 512 entries for (int i = 512; i < 1024; i++) - { - //NewPageMappingAccess[i] = OldPageMappingAccess[i]; - - // We can't do it that easily because the new tree may be part of - // a different 8MB window. This is slow, but for now I don't really - // care! (In the best case, both PMLs are in the same 8MB section - // and no switching is performed) - MMPTE Temp; - - OldPageMappingAccess = MmGetHHDMOffsetAddr (OldPageMapping); - Temp = OldPageMappingAccess[i]; - - NewPageMappingAccess = MmGetHHDMOffsetAddr (NewPageMappingResult); - NewPageMappingAccess[i] = Temp; - } + NewPageMappingAccess[i] = OldPageDirectory[i]; - // For recursive paging + // and replace that last entry with the pointer to this one. NewPageMappingAccess[MI_RECURSIVE_PAGING_START] = (uintptr_t)NewPageMappingResult | MM_PTE_PRESENT | MM_PTE_READWRITE | MM_PTE_NOEXEC; + MmEndUsingHHDM(); MmUnlockKernelSpace(); - return (HPAGEMAP) NewPageMappingResult; } -PMMPTE MiGetPTEPointer(HPAGEMAP Mapping, uintptr_t Address, bool AllocateMissingPMLs) -{ - const uintptr_t TenTwentyThreeMask = 0x3FF; - - uintptr_t indices[] = { - 0, - (Address >> 12) & TenTwentyThreeMask, - (Address >> 22) & TenTwentyThreeMask, - 0, - }; - - int NumPfnsAllocated = 0; - int PfnsAllocated[3]; - MMPTE PtesOriginals[3]; - PMMPTE PtesModified [3]; - - MMPTE SupervisorBit; - if (Address >= MM_KERNEL_SPACE_BASE) - SupervisorBit = 0; - else - SupervisorBit = MM_PTE_USERACCESS; - - HPAGEMAP CurrentLevel = Mapping; - PMMPTE EntryPointer = NULL; - - for (int pml = 2; pml >= 1; pml--) - { - PMMPTE Entries = MmGetHHDMOffsetAddr(CurrentLevel); - - EntryPointer = &Entries[indices[pml]]; - - MMPTE Entry = *EntryPointer; - - if (pml > 1 && (~Entry & MM_PTE_PRESENT)) - { - // not present!! Do we allocate it? - if (!AllocateMissingPMLs) - return NULL; - - int pfn = MmAllocatePhysicalPage(); - if (pfn == PFN_INVALID) - { - DbgPrint("MiGetPTEPointer: Ran out of memory trying to allocate PTEs along the PML path"); - - // rollback - for (int i = 0; i < NumPfnsAllocated; i++) - { - *(PtesModified[i]) = PtesOriginals[i]; - MmFreePhysicalPage(PfnsAllocated[i]); - } - - return NULL; - } - - PtesModified [NumPfnsAllocated] = EntryPointer; - PtesOriginals[NumPfnsAllocated] = Entry; - PfnsAllocated[NumPfnsAllocated] = pfn; - NumPfnsAllocated++; - - memset(MmGetHHDMOffsetAddr(MmPFNToPhysPage(pfn)), 0, PAGE_SIZE); - - *EntryPointer = Entry = MM_PTE_PRESENT | MM_PTE_READWRITE | SupervisorBit | MmPFNToPhysPage(pfn); - } - - if (pml > 1 && (Entry & MM_PTE_PAGESIZE)) - { - // Higher page size, we can't allocate here. Probably HHDM or something - the kernel itself doesn't use this - DbgPrint("MiGetPTEPointer: Address %p contains a higher page size, we don't support that for now", Address); - return NULL; - } - - CurrentLevel = Entry & MM_PTE_ADDRESSMASK; - } - - return EntryPointer; -} - -static void MmpFreeVacantPMLsSub(HPAGEMAP Mapping, uintptr_t Address) +static void MmpFreeVacantPageTables(uintptr_t Address) { - // Lot of code was copied from MiGetPTEPointer. - const uintptr_t TenTwentyThreeMask = 0x3FF; - - uintptr_t indices[] = { - 0, - (Address >> 12) & TenTwentyThreeMask, - (Address >> 22) & TenTwentyThreeMask, - 0, - }; - - HPAGEMAP CurrentLevel = Mapping; - PMMPTE EntryPointer = NULL, ParentEntryPointer = NULL; - - for (int pml = 2; pml >= 1; pml--) + if (!MmCheckPteLocation(Address, false)) + // already freed + return; + + // check if the page table this address' PTE is in is vacant + PMMPTE PtePT = MmGetPteLocation(Address); + PtePT = (PMMPTE)((uintptr_t)PtePT & ~(PAGE_SIZE - 1)); + for (int i = 0; i < 1024; i++) { - PMMPTE Entries = MmGetHHDMOffsetAddr(CurrentLevel); - - EntryPointer = &Entries[indices[pml]]; - - MMPTE Entry = *EntryPointer; - - if (pml > 1 && (~Entry & MM_PTE_PRESENT)) - { - // if we don't have a parent, return - if (!ParentEntryPointer) - return; - - // check if this entire page is vacant - for (int i = 0; i < 1024; i++) - { - if (Entries[i] != 0) - return; // isn't vacant - } - - // is vacant, so free it - *ParentEntryPointer = 0; - - int pfn = MmPhysPageToPFN(CurrentLevel); - MmFreePhysicalPage(pfn); - } - - if (pml > 1 && (Entry & MM_PTE_PAGESIZE)) - { - // Higher page size, we can't do that here. Probably HHDM or something - the kernel itself doesn't use this + if (PtePT[i] != 0) + // isn't vacant return; - } - - CurrentLevel = Entry & MM_PTE_ADDRESSMASK; - ParentEntryPointer = EntryPointer; } -} - -static void MmpFreeVacantPMLs(HPAGEMAP Mapping, uintptr_t Address) -{ - // Do this 2 times to ensure all levels are freed. Could be done better - for (int i = 0; i < 2; i++) - MmpFreeVacantPMLsSub(Mapping, Address); + + PMMPTE PtePD = MmGetPteLocation(MI_PTE_LOC(Address)); + MMPFN Pfn = MmPhysPageToPFN(*PtePD & MM_PTE_ADDRESSMASK); + *PtePD = 0; + MmFreePhysicalPage(Pfn); } static bool MmpMapSingleAnonPageAtPte(PMMPTE Pte, uintptr_t Permissions, bool NonPaged) @@ -287,36 +155,31 @@ static bool MmpMapSingleAnonPageAtPte(PMMPTE Pte, uintptr_t Permissions, bool No } *Pte = MM_DPTE_COMMITTED | Permissions; - return true; } -bool MiMapAnonPage(HPAGEMAP Mapping, uintptr_t Address, uintptr_t Permissions, bool NonPaged) +bool MiMapAnonPage(uintptr_t Address, uintptr_t Permissions, bool NonPaged) { - PMMPTE Pte = MiGetPTEPointer(Mapping, Address, true); - + PMMPTE Pte = MmGetPteLocationCheck(Address, true); return MmpMapSingleAnonPageAtPte(Pte, Permissions, NonPaged); } -bool MiMapPhysicalPage(HPAGEMAP Mapping, uintptr_t PhysicalPage, uintptr_t Address, uintptr_t Permissions) +bool MiMapPhysicalPage(uintptr_t PhysicalPage, uintptr_t Address, uintptr_t Permissions) { - PMMPTE Pte = MiGetPTEPointer(Mapping, Address, true); - + PMMPTE Pte = MmGetPteLocationCheck(Address, true); if (!Pte) return false; *Pte = (PhysicalPage & MM_PTE_ADDRESSMASK) | Permissions | MM_PTE_PRESENT; - return true; } -void MiUnmapPages(HPAGEMAP Mapping, uintptr_t Address, size_t LengthPages) +void MiUnmapPages(uintptr_t Address, size_t LengthPages) { // Step 1. Unset the PRESENT bit on all pages in the range. for (size_t i = 0; i < LengthPages; i++) { - PMMPTE pPTE = MiGetPTEPointer(Mapping, Address + i * PAGE_SIZE, false); - + PMMPTE pPTE = MmGetPteLocationCheck(Address + i * PAGE_SIZE, false); if (!pPTE) continue; @@ -340,8 +203,7 @@ void MiUnmapPages(HPAGEMAP Mapping, uintptr_t Address, size_t LengthPages) // Step 3. If needed, free the PMM pages related to this page mapping. for (size_t i = 0; i < LengthPages; i++) { - PMMPTE pPTE = MiGetPTEPointer(Mapping, Address + i * PAGE_SIZE, false); - + PMMPTE pPTE = MmGetPteLocationCheck(Address + i * PAGE_SIZE, false); if (!pPTE) continue; @@ -350,39 +212,10 @@ void MiUnmapPages(HPAGEMAP Mapping, uintptr_t Address, size_t LengthPages) if ((*pPTE & Flags) == Flags) { uintptr_t PhysPage = *pPTE & MM_PTE_ADDRESSMASK; - MmFreePhysicalPage(MmPhysPageToPFN(PhysPage)); - *pPTE = 0; } } - - return; - - // Step 4. Free higher PMLs if they're fully vacant - for (size_t i = 0; i < LengthPages; i++) - { - MmpFreeVacantPMLs(Mapping, Address + i * PAGE_SIZE); - } -} - -void MiPrepareGlobalAreaForPool(HPAGEMAP PageMap) -{ - PMMPTE Ptes = MmGetHHDMOffsetAddr(PageMap); - - int Pfn = MmAllocatePhysicalPage(); - if (Pfn == PFN_INVALID) - { - KeCrashBeforeSMPInit("MiPrepareGlobalAreaForPool: Can't allocate global pml4"); - } - - Ptes[MI_GLOBAL_AREA_START] = - MM_PTE_PRESENT | - MM_PTE_READWRITE | - MM_PTE_GLOBAL | - MM_PTE_ISFROMPMM | - MM_PTE_NOEXEC | - MmPFNToPhysPage(Pfn); } uintptr_t MiGetTopOfPoolManagedArea() @@ -395,13 +228,13 @@ uintptr_t MiGetTopOfSecondPoolManagedArea() return MI_GLOBAL_AREA_START_2ND << 22; } -bool MiMapAnonPages(HPAGEMAP Mapping, uintptr_t Address, size_t SizePages, uintptr_t Permissions, bool NonPaged) +bool MiMapAnonPages(uintptr_t Address, size_t SizePages, uintptr_t Permissions, bool NonPaged) { // As an optimization, we'll wait until the PML1 index rolls over to zero before reloading the PTE pointer. uint64_t CurrentPml1 = PML1_IDX(Address); size_t DonePages = 0; - PMMPTE PtePtr = MiGetPTEPointer(Mapping, Address, true); + PMMPTE PtePtr = MmGetPteLocationCheck(Address, true); for (size_t i = 0; i < SizePages; i++) { @@ -419,7 +252,7 @@ bool MiMapAnonPages(HPAGEMAP Mapping, uintptr_t Address, size_t SizePages, uintp if (CurrentPml1 % (PAGE_SIZE / sizeof(MMPTE)) == 0) { // We have rolled over. - PtePtr = MiGetPTEPointer(Mapping, Address, true); + PtePtr = MmGetPteLocationCheck(Address, true); } } @@ -428,7 +261,7 @@ bool MiMapAnonPages(HPAGEMAP Mapping, uintptr_t Address, size_t SizePages, uintp ROLLBACK: // Unmap all the pages that we have mapped. - MiUnmapPages(Mapping, Address, DonePages); + MiUnmapPages(Address, DonePages); return false; } diff --git a/boron/source/mm/mdl.c b/boron/source/mm/mdl.c index 99623171..05d3ea85 100644 --- a/boron/source/mm/mdl.c +++ b/boron/source/mm/mdl.c @@ -21,7 +21,7 @@ void MmUnmapPagesMdl(PMDL Mdl) if (!Mdl->MappedStartVA) return; - MiUnmapPages(Mdl->Process->Pcb.PageMap, Mdl->MappedStartVA, Mdl->NumberPages); + MiUnmapPages(Mdl->MappedStartVA, Mdl->NumberPages); Mdl->Flags &= ~MDL_FLAG_MAPPED; } @@ -77,8 +77,6 @@ BSTATUS MmMapPinnedPagesMdl(PMDL Mdl, void** OutAddress) uintptr_t Address = MapAddress; size_t Index = 0; - HPAGEMAP PageMap = Mdl->Process->Pcb.PageMap; - MmLockKernelSpaceExclusive(); for (; Index < Mdl->NumberPages; Address += PAGE_SIZE, Index++) @@ -86,10 +84,10 @@ BSTATUS MmMapPinnedPagesMdl(PMDL Mdl, void** OutAddress) // Add a reference to the page. MmPageAddReference(Mdl->Pages[Index]); - if (!MiMapPhysicalPage(PageMap, Mdl->Pages[Index] * PAGE_SIZE, Address, Permissions)) + if (!MiMapPhysicalPage(Mdl->Pages[Index] * PAGE_SIZE, Address, Permissions)) { // Unmap everything mapped so far. - MiUnmapPages(PageMap, MapAddress, Index); + MiUnmapPages(MapAddress, Index); MmUnlockKernelSpace(); @@ -158,6 +156,7 @@ BSTATUS MmProbeAndPinPagesMdl(PMDL Mdl, KPROCESSOR_MODE AccessMode, bool IsWrite uintptr_t EndPage = (VirtualAddress + Mdl->ByteOffset + Size + 0xFFF) & ~0xFFF; BSTATUS FailureReason = STATUS_SUCCESS; + // TODO: Arbitrary size limitation that we should remove! if (Size >= MDL_MAX_SIZE) return STATUS_INVALID_PARAMETER; @@ -167,7 +166,9 @@ BSTATUS MmProbeAndPinPagesMdl(PMDL Mdl, KPROCESSOR_MODE AccessMode, bool IsWrite if (AccessMode == MODE_USER && MM_USER_SPACE_END < EndPage) return STATUS_INVALID_PARAMETER; - HPAGEMAP PageMap = Mdl->Process->Pcb.PageMap; + PEPROCESS Restore = NULL; + if (Mdl->Process != PsGetAttachedProcess()) + Restore = PsSetAttachedProcess(Mdl->Process); // Fault all the pages in. for (uintptr_t Address = StartPage; Address < EndPage; Address += PAGE_SIZE) @@ -182,7 +183,12 @@ BSTATUS MmProbeAndPinPagesMdl(PMDL Mdl, KPROCESSOR_MODE AccessMode, bool IsWrite } if (FailureReason) + { + if (Restore) + PsSetAttachedProcess(Restore); + return FailureReason; + } // TODO(WORKINGSET): Check if the working set (when we add it) can even fit all of these pages. // TODO(possibly related to above): Ensure proper failure if the whole buffer doesn't fit in system memory! @@ -193,7 +199,7 @@ BSTATUS MmProbeAndPinPagesMdl(PMDL Mdl, KPROCESSOR_MODE AccessMode, bool IsWrite while (true) { KIPL OldIpl = MmLockSpaceShared(Address); - PMMPTE PtePtr = MiGetPTEPointer(PageMap, Address, false); + PMMPTE PtePtr = MmGetPteLocationCheck(Address, false); bool TryFault = false; if (!PtePtr) @@ -285,6 +291,9 @@ BSTATUS MmProbeAndPinPagesMdl(PMDL Mdl, KPROCESSOR_MODE AccessMode, bool IsWrite else Mdl->Flags &= ~MDL_FLAG_WRITE; + if (Restore) + PsSetAttachedProcess(Restore); + if (FailureReason) { // Unpin only up to the current index, the rest weren't filled in due to the failure. @@ -354,8 +363,10 @@ void MmCopyIntoMdl(PMDL Mdl, uintptr_t Offset, const void* SourceBuffer, size_t else CopyAmount = BytesTillNext; + MmBeginUsingHHDM(); char* PageDest = MmGetHHDMOffsetAddr(MmPFNToPhysPage(Mdl->Pages[PageIndex])); memcpy(PageDest + PageOffs, SourceBufferChr, CopyAmount); + MmEndUsingHHDM(); SourceBufferChr += CopyAmount; Offset += CopyAmount; @@ -385,8 +396,10 @@ void MmSetIntoMdl(PMDL Mdl, uintptr_t Offset, uint8_t ToSet, size_t Size) else CopyAmount = BytesTillNext; + MmBeginUsingHHDM(); char* PageDest = MmGetHHDMOffsetAddr(MmPFNToPhysPage(Mdl->Pages[PageIndex])); memset(PageDest + PageOffs, ToSet, CopyAmount); + MmEndUsingHHDM(); Offset += CopyAmount; Size -= CopyAmount; @@ -417,8 +430,10 @@ void MmCopyFromMdl(PMDL Mdl, uintptr_t Offset, void* DestinationBuffer, size_t S else CopyAmount = BytesTillNext; + MmBeginUsingHHDM(); char* PageDest = MmGetHHDMOffsetAddr(MmPFNToPhysPage(Mdl->Pages[PageIndex])); memcpy(DestBufferChr, PageDest + PageOffs, CopyAmount); + MmEndUsingHHDM(); DestBufferChr += CopyAmount; Offset += CopyAmount; diff --git a/boron/source/mm/pmm.c b/boron/source/mm/pmm.c index 92a06913..68c20785 100644 --- a/boron/source/mm/pmm.c +++ b/boron/source/mm/pmm.c @@ -25,8 +25,7 @@ Module name: #define PmmDbgPrint(...) do {} while (0) #endif -//extern volatile struct limine_hhdm_request KeLimineHhdmRequest; -//extern volatile struct limine_memmap_request KeLimineMemMapRequest; +static KSPIN_LOCK MmPfnLock; // Free page statistics size_t MmTotalAvailablePages; @@ -68,9 +67,25 @@ uintptr_t MmGetHHDMOffsetFromAddr(void* addr) #endif uintptr_t MmHHDMWindowBase; +static KSPIN_LOCK MiHHDMWindowLock; +static KIPL MiHHDMWindowIpl; + +void MmBeginUsingHHDM() +{ + KIPL Ipl; + KeAcquireSpinLock(&MiHHDMWindowLock, &Ipl); + MiHHDMWindowIpl = Ipl; +} + +void MmEndUsingHHDM() +{ + ASSERT(MiHHDMWindowLock.Locked); + KeReleaseSpinLock(&MiHHDMWindowLock, MiHHDMWindowIpl); +} static void MiUpdateHHDMWindowBase(uintptr_t PhysAddr) { + ASSERT(MiHHDMWindowLock.Locked); KIPL Ipl = MiLockPfdb(); const int PtesPerLevel = PAGE_SIZE / sizeof(MMPTE); @@ -95,6 +110,9 @@ static void MiUpdateHHDMWindowBase(uintptr_t PhysAddr) void* MmGetHHDMOffsetAddr(uintptr_t PhysAddr) { + ASSERT(!MmPfnLock.Locked); + ASSERT(MiHHDMWindowLock.Locked); + if (PhysAddr < MI_IDENTMAP_SIZE) return (void*)(MI_IDENTMAP_START + PhysAddr); @@ -106,6 +124,9 @@ void* MmGetHHDMOffsetAddr(uintptr_t PhysAddr) uintptr_t MmGetHHDMOffsetFromAddr(void* Addr) { + ASSERT(!MmPfnLock.Locked); + ASSERT(MiHHDMWindowLock.Locked); + uintptr_t AddrInt = (uintptr_t) Addr; if (AddrInt >= MI_IDENTMAP_START && AddrInt < MI_IDENTMAP_START + MI_IDENTMAP_SIZE) return AddrInt - MI_IDENTMAP_START; @@ -272,7 +293,10 @@ static bool MiMapNewPageAtAddressIfNeeded(uintptr_t pageTable, uintptr_t address return false; } + MmBeginUsingHHDM(); memset(MmGetHHDMOffsetAddr(Addr), 0, PAGE_SIZE); + MmEndUsingHHDM(); + Level1[Convert.Level1Index] = Addr | MM_PTE_PRESENT | MM_PTE_READWRITE; } @@ -325,7 +349,6 @@ static MMPFN MiFirstZeroPFN = PFN_INVALID, MiLastZeroPFN = PFN_INVALID; static MMPFN MiFirstFreePFN = PFN_INVALID, MiLastFreePFN = PFN_INVALID; static MMPFN MiFirstStandbyPFN = PFN_INVALID, MiLastStandbyPFN = PFN_INVALID; static MMPFN MiFirstModifiedPFN = PFN_INVALID, MiLastModifiedPFN = PFN_INVALID; -static KSPIN_LOCK MmPfnLock; KIPL MiLockPfdb() { @@ -696,7 +719,11 @@ MMPFN MmAllocatePhysicalPage() KeReleaseSpinLock(&MmPfnLock, OldIpl); if (!FromZero && currPFN != PFN_INVALID) + { + MmBeginUsingHHDM(); memset(MmGetHHDMOffsetAddr(MmPFNToPhysPage(currPFN)), 0, PAGE_SIZE); + MmEndUsingHHDM(); + } #ifdef PMMDEBUG DbgPrint("MmAllocatePhysicalPage() => %d (RA:%p)", currPFN, __builtin_return_address(0)); @@ -906,56 +933,50 @@ void MiReinsertIntoModifiedList(MMPFN Pfn) } } -// Zeroes out a free PFN, takes it off the free PFN list and adds it to -// the zero PFN list. -static void MmpZeroOutPFN(MMPFN pfn) +// Zeroes out the first free PFN, takes it off the free PFN list and +// adds it to the zero PFN list. +void MmZeroOutFirstPFN() { - ASSERT(pfn != PFN_INVALID); - PMMPFDBE pPF = MmGetPageFrameFromPFN(pfn); - if (pPF->Type == PF_TYPE_ZEROED) - return; + // step 1. find the first free PFN, if it exists. + KIPL OldIpl; + KeAcquireSpinLock(&MmPfnLock, &OldIpl); - if (pPF->Type != PF_TYPE_FREE) + if (MiFirstFreePFN == PFN_INVALID) { - DbgPrint("Error, attempting to zero out pfn %d which is used", pfn); + Return: + KeReleaseSpinLock(&MmPfnLock, OldIpl); return; } + + MMPFN pfn = MiFirstFreePFN; + PMMPFDBE pPF = MmGetPageFrameFromPFN(pfn); + + if (pPF->Type == PF_TYPE_ZEROED) + goto Return; + +#ifdef DEBUG + if (pPF->Type != PF_TYPE_FREE) + KeCrash("Error, attempting to zero out pfn %d which is used", pfn); +#endif pPF->Type = PF_TYPE_ZEROED; MmpRemovePfnFromList(&MiFirstFreePFN, &MiLastFreePFN, pfn); + KeReleaseSpinLock(&MmPfnLock, OldIpl); - // zero out the page itself + // step 2. free the PFN. + MmBeginUsingHHDM(); uint8_t* mem = MmGetHHDMOffsetAddr(MmPFNToPhysPage(pfn)); memset(mem, 0, PAGE_SIZE); + MmEndUsingHHDM(); - MmpAddPfnToList(&MiFirstZeroPFN, &MiLastZeroPFN, pfn); -} - -void MmZeroOutPFN(MMPFN pfn) -{ - ASSERT(pfn != PFN_INVALID); - KIPL OldIpl; + // step 3. add this PFN to the zero list KeAcquireSpinLock(&MmPfnLock, &OldIpl); - MmpZeroOutPFN(pfn); + MmpAddPfnToList(&MiFirstZeroPFN, &MiLastZeroPFN, pfn); KeReleaseSpinLock(&MmPfnLock, OldIpl); } -void MmZeroOutFirstPFN() -{ - KIPL OldIpl; - KeAcquireSpinLock(&MmPfnLock, &OldIpl); - - if (MiFirstFreePFN == PFN_INVALID) - { - KeReleaseSpinLock(&MmPfnLock, OldIpl); - return; - } - - MmpZeroOutPFN(MiFirstFreePFN); - - KeReleaseSpinLock(&MmPfnLock, OldIpl); -} +#ifdef IS_64_BIT void* MmAllocatePhysicalPageHHDM() { @@ -972,6 +993,20 @@ void MmFreePhysicalPageHHDM(void* page) return MmFreePhysicalPage(MmPhysPageToPFN(MmGetHHDMOffsetFromAddr(page))); } +#else + +void* MmAllocatePhysicalPageHHDM() +{ + KeCrash("NYI MmAllocatePhysicalPageHHDM"); +} + +void MmFreePhysicalPageHHDM(void* Page) +{ + KeCrash("NYI MmFreePhysicalPageHHDM(%p)", Page); +} + +#endif + void MiPageAddReferenceWithPfdbLocked(MMPFN Pfn) { ASSERT(Pfn != PFN_INVALID); diff --git a/boron/source/mm/pool.c b/boron/source/mm/pool.c index 08fcc745..90197c5a 100644 --- a/boron/source/mm/pool.c +++ b/boron/source/mm/pool.c @@ -43,7 +43,6 @@ void* MmAllocatePoolBig(int PoolFlags, size_t PageCount, int Tag) // Map the memory in! This will affect ALL page maps if (!MiMapAnonPages( - MiGetCurrentPageMap(), (uintptr_t) OutputAddress, PageCount, MM_PTE_READWRITE | MM_PTE_GLOBAL, @@ -65,14 +64,16 @@ void MmFreePoolBig(void* Address) MIPOOL_SPACE_HANDLE Handle = MiGetPoolSpaceHandleFromAddress(Address); int PoolFlags = (int) MiGetUserDataFromPoolSpaceHandle(Handle); - if (~PoolFlags & POOL_FLAG_CALLER_CONTROLLED) + if ((~PoolFlags & POOL_FLAG_CALLER_CONTROLLED) || + (PoolFlags & POOL_FLAG_UNMAP_ANYWAY)) { MmLockKernelSpaceExclusive(); // De-allocate the memory first. Ideally this will affect ALL page maps - MiUnmapPages(MiGetCurrentPageMap(), - (uintptr_t)Address, - MiGetSizeFromPoolSpaceHandle(Handle)); + MiUnmapPages( + (uintptr_t)Address, + MiGetSizeFromPoolSpaceHandle(Handle) + ); MmUnlockKernelSpace(); } @@ -95,10 +96,18 @@ void* MmMapIoSpace(uintptr_t PhysicalAddress, size_t Size, uintptr_t Permissions { if (Tag == 0) Tag = POOL_TAG("MMIS"); + // Ensure the starting address is page aligned. + Size += PhysicalAddress & (PAGE_SIZE - 1); + PhysicalAddress &= ~(PAGE_SIZE - 1); + size_t SizePages = (Size + PAGE_SIZE - 1) / PAGE_SIZE; // Allocate some pool space. - void* Space = MmAllocatePoolBig(POOL_FLAG_CALLER_CONTROLLED, SizePages, Tag); + void* Space = MmAllocatePoolBig( + POOL_FLAG_CALLER_CONTROLLED | POOL_FLAG_UNMAP_ANYWAY, + SizePages, + Tag + ); uintptr_t VirtualAddress = (uintptr_t) Space; if (!Space) @@ -106,11 +115,9 @@ void* MmMapIoSpace(uintptr_t PhysicalAddress, size_t Size, uintptr_t Permissions MmLockKernelSpaceExclusive(); - HPAGEMAP PageMap = MiGetCurrentPageMap(); - for (size_t i = 0; i < SizePages; i++, PhysicalAddress += PAGE_SIZE, VirtualAddress += PAGE_SIZE) { - if (!MiMapPhysicalPage(PageMap, PhysicalAddress, VirtualAddress, PermissionsAndCaching)) + if (!MiMapPhysicalPage(PhysicalAddress, VirtualAddress, PermissionsAndCaching)) { SizePages = i; goto Rollback; @@ -121,7 +128,7 @@ void* MmMapIoSpace(uintptr_t PhysicalAddress, size_t Size, uintptr_t Permissions return Space; Rollback: - MiUnmapPages(PageMap, (uintptr_t) Space, SizePages); + MiUnmapPages((uintptr_t) Space, SizePages); MmUnlockKernelSpace(); MmFreePoolBig(Space); return NULL; diff --git a/boron/source/mm/poolhdr.c b/boron/source/mm/poolhdr.c index 819a49cc..33a41b07 100644 --- a/boron/source/mm/poolhdr.c +++ b/boron/source/mm/poolhdr.c @@ -93,7 +93,7 @@ PMIPOOL_ENTRY_SLAB MiAllocatePoolHeaderSlab() } // HHDM is supported on 64-bit, so just return this - return MmGetHHDMOffsetAddr(Pfn); + return MmGetHHDMOffsetAddr(MmPFNToPhysPage(Pfn)); } void MiFreePoolHeaderSlab(PMIPOOL_ENTRY_SLAB Address) diff --git a/boron/source/mm/poolsup.c b/boron/source/mm/poolsup.c index 46ac5594..9cf75893 100644 --- a/boron/source/mm/poolsup.c +++ b/boron/source/mm/poolsup.c @@ -308,7 +308,7 @@ void MiFreePoolSpace(MIPOOL_SPACE_HANDLE Handle) // Acquire the kernel space lock and zero out its PTE. MmLockKernelSpaceExclusive(); - PMMPTE PtePtr = MiGetPTEPointer(MiGetCurrentPageMap(), Address, false); + PMMPTE PtePtr = MmGetPteLocationCheck(Address, false); ASSERT(PtePtr); ASSERT(*PtePtr == MiCalculatePoolHeaderPte(Handle)); *PtePtr = 0; @@ -341,7 +341,7 @@ MIPOOL_SPACE_HANDLE MiReservePoolSpaceTagged(size_t SizeInPages, void** OutputAd // Acquire the kernel space lock and place the address of the handle into the first part of the PTE. MmLockKernelSpaceExclusive(); - PMMPTE PtePtr = MiGetPTEPointer(MiGetCurrentPageMap(), (uintptr_t) OutputAddressSub, true); + PMMPTE PtePtr = MmGetPteLocationCheck((uintptr_t) OutputAddressSub, true); if (!PtePtr) { // TODO: Handle this in a nicer way. @@ -354,7 +354,10 @@ MIPOOL_SPACE_HANDLE MiReservePoolSpaceTagged(size_t SizeInPages, void** OutputAd ASSERT(*PtePtr == 0); ASSERT((Handle & MM_PTE_PRESENT) == 0); - *PtePtr = MiCalculatePoolHeaderPte(Handle); + MMPTE Pte = MiCalculatePoolHeaderPte(Handle); + ASSERT(MiReconstructPoolHandleFromPte(Pte) == Handle); + + *PtePtr = Pte; MmUnlockKernelSpace(); @@ -421,7 +424,7 @@ MIPOOL_SPACE_HANDLE MiGetPoolSpaceHandleFromAddress(void* AddressV) MmLockKernelSpaceExclusive(); - PMMPTE PtePtr = MiGetPTEPointer(MiGetCurrentPageMap(), Address - PAGE_SIZE, false); + PMMPTE PtePtr = MmGetPteLocationCheck(Address - PAGE_SIZE, false); if (!PtePtr) { MmUnlockKernelSpace(); @@ -432,6 +435,9 @@ MIPOOL_SPACE_HANDLE MiGetPoolSpaceHandleFromAddress(void* AddressV) // N.B. This kind of relies on the notion that the address doesn't have // the valid bit set. uintptr_t PAddress = *PtePtr; + if (~PAddress & MM_PTE_ISPOOLHDR) { + KeCrash("Trying to access pool space handle from address %p, but its PTE says %p", AddressV, PAddress); + } ASSERT(PAddress & MM_PTE_ISPOOLHDR); PAddress = MiReconstructPoolHandleFromPte(PAddress); MIPOOL_SPACE_HANDLE Handle = PAddress; diff --git a/boron/source/mm/reclaim.c b/boron/source/mm/reclaim.c index 575de4ea..d8445524 100644 --- a/boron/source/mm/reclaim.c +++ b/boron/source/mm/reclaim.c @@ -24,7 +24,7 @@ void MiReclaimInitText() MmLockKernelSpaceExclusive(); for (uintptr_t i = (uintptr_t) KiTextInitStart; i != (uintptr_t) KiTextInitEnd; i += PAGE_SIZE) { - PMMPTE PtePtr = MiGetPTEPointer(MiGetCurrentPageMap(), i, false); + PMMPTE PtePtr = MmGetPteLocationCheck(i, false); ASSERT(PtePtr); MMPTE Pte = *PtePtr; @@ -39,6 +39,7 @@ void MiReclaimInitText() MmFreePhysicalPage(Pfn); Reclaimed++; } + MmUnlockKernelSpace(); #ifdef DEBUG diff --git a/boron/source/mm/teardown.c b/boron/source/mm/teardown.c index 10347842..c46387fc 100644 --- a/boron/source/mm/teardown.c +++ b/boron/source/mm/teardown.c @@ -61,14 +61,26 @@ void MmTearDownProcess(PEPROCESS Process) MiFreeUnusedMappingLevelsInCurrentMap(0, (MM_USER_SPACE_END + 1) >> 12); -#if defined(DEBUG) && defined(TARGET_AMD64) +#ifdef DEBUG +#ifdef TARGET_AMD64 PMMPTE PteScan = MmGetHHDMOffsetAddr(Process->Pcb.PageMap); for (int i = 0; i < 256; i++) ASSERT(~PteScan[i] & MM_PTE_PRESENT); +#endif // TARGET_AMD64 + +#ifdef TARGET_I386 + MmBeginUsingHHDM(); + PMMPTE PteScan = MmGetHHDMOffsetAddr(Process->Pcb.PageMap); + + for (int i = 0; i < 256; i++) + ASSERT(~PteScan[i] & MM_PTE_PRESENT); + + MmEndUsingHHDM(); +#endif // TARGET_I386 -#endif +#endif // DEBUG if (Process->Pcb.PageMap != 0) MmFreePhysicalPage(MmPhysPageToPFN(Process->Pcb.PageMap)); diff --git a/drivers/framebuf/source/main.c b/drivers/framebuf/source/main.c index f546c105..e012f287 100644 --- a/drivers/framebuf/source/main.c +++ b/drivers/framebuf/source/main.c @@ -62,7 +62,27 @@ BSTATUS FramebufferRead(PIO_STATUS_BLOCK Iosb, UNUSED PFCB Fcb, uint64_t Offset, if (Size == 0) goto SuccessZero; +#ifdef IS_64_BIT + MmCopyIntoMdl(MdlBuffer, 0, MmGetHHDMOffsetAddr(Ext->Address + Offset), Size); + +#else + + void *TempMapping = MmMapIoSpace( + Ext->Address + Offset, + Size, + MM_PTE_READWRITE | MM_PTE_CDISABLE, + POOL_TAG("FBCP") + ); + + if (!TempMapping) + return IOSB_STATUS(Iosb, STATUS_INSUFFICIENT_MEMORY); + + MmCopyIntoMdl(MdlBuffer, 0, TempMapping, Size); + + MmFreePoolBig(TempMapping); + +#endif Iosb->BytesRead = Size; return IOSB_STATUS(Iosb, STATUS_SUCCESS); @@ -92,7 +112,27 @@ BSTATUS FramebufferWrite(PIO_STATUS_BLOCK Iosb, PFCB Fcb, uint64_t Offset, PMDL if (Size == 0) goto SuccessZero; +#ifdef IS_64_BIT + MmCopyFromMdl(MdlBuffer, 0, MmGetHHDMOffsetAddr(Ext->Address + Offset), Size); + +#else + + void *TempMapping = MmMapIoSpace( + Ext->Address + Offset, + Size, + MM_PTE_READWRITE | MM_PTE_CDISABLE, + POOL_TAG("FBCP") + ); + + if (!TempMapping) + return IOSB_STATUS(Iosb, STATUS_INSUFFICIENT_MEMORY); + + MmCopyFromMdl(MdlBuffer, 0, TempMapping, Size); + + MmFreePoolBig(TempMapping); + +#endif Iosb->BytesWritten = Size; return IOSB_STATUS(Iosb, STATUS_SUCCESS); diff --git a/drivers/halx86/source/acpi.c b/drivers/halx86/source/acpi.c index de04112f..f041fb19 100644 --- a/drivers/halx86/source/acpi.c +++ b/drivers/halx86/source/acpi.c @@ -18,8 +18,7 @@ Module name: #include "pio.h" // TODO: Add a way to do that without breaking into Mm's internal functions -HPAGEMAP MiGetCurrentPageMap(); -bool MiMapPhysicalPage(HPAGEMAP Mapping, uintptr_t PhysicalPage, uintptr_t Address, uintptr_t Permissions); +bool MiMapPhysicalPage(uintptr_t PhysicalPage, uintptr_t Address, uintptr_t Permissions); static PRSDP_DESCRIPTION HalpRsdp; static PRSDT_TABLE HalpRsdt; @@ -92,20 +91,15 @@ void AcpiInitPmt() uintptr_t Addr = Header->X_PMTimerBlock.Address; uintptr_t OffsetWithinPage = Addr & 0xFFF; - void* PageAddress = MmAllocatePoolBig(POOL_FLAG_CALLER_CONTROLLED, 1, POOL_TAG("APMT")); + void* PageAddress = MmMapIoSpace( + Addr, + 1, // SizePages + MM_PTE_READWRITE | MM_PTE_CDISABLE | MM_PTE_GLOBAL | MM_PTE_NOEXEC, + POOL_TAG("APMT") + ); + if (!PageAddress) - { - CRASH_BECAUSE_FAILURE_TO_MAP: KeCrashBeforeSMPInit("Could not map ACPI PMT timer as uncacheable"); - } - - if (!MiMapPhysicalPage(MiGetCurrentPageMap(), - Addr, - (uintptr_t) PageAddress, - MM_PTE_READWRITE | MM_PTE_CDISABLE | MM_PTE_GLOBAL | MM_PTE_NOEXEC)) - { - goto CRASH_BECAUSE_FAILURE_TO_MAP; - } uintptr_t PageAddress2 = (uintptr_t) PageAddress; PageAddress2 += OffsetWithinPage; diff --git a/drivers/halx86/source/hpet.c b/drivers/halx86/source/hpet.c index ee76317c..9dc4f9a4 100644 --- a/drivers/halx86/source/hpet.c +++ b/drivers/halx86/source/hpet.c @@ -49,21 +49,16 @@ void HpetInitialize() uintptr_t HpetAddress = Hpet->Address.Address; // Map the HPET as uncacheable. - void* Address = MmAllocatePoolBig(POOL_FLAG_CALLER_CONTROLLED, 1, POOL_TAG("HPET")); + void* Address = MmMapIoSpace( + HpetAddress, + 1, // SizePages + MM_PTE_READWRITE | MM_PTE_CDISABLE | MM_PTE_GLOBAL | MM_PTE_NOEXEC, + POOL_TAG("HPET") + ); + if (!Address) - { - CRASH_BECAUSE_FAILURE_TO_MAP: KeCrashBeforeSMPInit("Could not map HPET as uncacheable"); - } - - if (!MiMapPhysicalPage(MiGetCurrentPageMap(), - HpetAddress, - (uintptr_t) Address, - MM_PTE_READWRITE | MM_PTE_CDISABLE | MM_PTE_GLOBAL | MM_PTE_NOEXEC)) - { - goto CRASH_BECAUSE_FAILURE_TO_MAP; - } - + HpetpIsAvailable = true; HpetpRegisters = (PHPET_REGISTERS) ((uintptr_t) Address + (HpetAddress & 0xFFF)); diff --git a/drivers/test/source/mdltst.c b/drivers/test/source/mdltst.c index aae7c665..e3498dca 100644 --- a/drivers/test/source/mdltst.c +++ b/drivers/test/source/mdltst.c @@ -29,12 +29,10 @@ void PerformMdlTest() KeWaitForSingleObject(&Timer, false, TIMEOUT_INFINITE, MODE_KERNEL); */ - HPAGEMAP PageMap = MiGetCurrentPageMap(); uintptr_t FixedAddr = 0x40000000; size_t FixedSize = 0x10000; if (!MiMapAnonPages( - PageMap, FixedAddr, FixedSize / PAGE_SIZE, MM_PTE_READWRITE, @@ -94,7 +92,7 @@ void PerformMdlTest() LogMsg("Memory Pages Available Now: %zu", MmGetTotalFreePages()); - MiUnmapPages(PageMap, FixedAddr, FixedSize); + MiUnmapPages(FixedAddr, FixedSize); // note: the variation of 3 pages is actually normal at this point // the pages are allocated during the initial mapping diff --git a/drivers/test/source/mm1tst.c b/drivers/test/source/mm1tst.c index c58e5b13..0e6d0d38 100644 --- a/drivers/test/source/mm1tst.c +++ b/drivers/test/source/mm1tst.c @@ -46,7 +46,6 @@ void Reboot() { void PerformDemandPageTest() { LogMsg(">> Demand page test"); - HPAGEMAP Map = MiGetCurrentPageMap(); void* PoolAddr = MmAllocatePoolBig(POOL_FLAG_CALLER_CONTROLLED, 1, POOL_TAG("Mts1")); ASSERT(PoolAddr); @@ -56,7 +55,7 @@ void PerformDemandPageTest() // Make the PTE a demand page PTE. MmLockKernelSpaceExclusive(); - PMMPTE Pte = MiGetPTEPointer(Map, Va, true); + PMMPTE Pte = MmGetPteLocationCheck(Va, true); ASSERT(Pte); *Pte = MM_DPTE_COMMITTED | MM_PTE_READWRITE; @@ -68,7 +67,7 @@ void PerformDemandPageTest() LogMsg("Va read: %08x", *((uint32_t*)Va)); MmLockKernelSpaceExclusive(); - MiUnmapPages(Map, Va, 1); + MiUnmapPages(Va, 1); MmUnlockKernelSpace(); MmFreePoolBig(PoolAddr); @@ -77,8 +76,7 @@ void PerformDemandPageTest() void PerformCopyOnWriteTest() { LogMsg(">> Copy on write test"); - HPAGEMAP Map = MiGetCurrentPageMap(); - + void* PoolAddr = MmAllocatePoolBig(POOL_FLAG_CALLER_CONTROLLED, 2, POOL_TAG("Mts2")); ASSERT(PoolAddr); @@ -96,8 +94,8 @@ void PerformCopyOnWriteTest() // Now map them in. MmLockKernelSpaceExclusive(); - bool Res1 = MiMapPhysicalPage(Map, MmPFNToPhysPage(Pfn), Va1, MM_PTE_ISFROMPMM | MM_PTE_COW); - bool Res2 = MiMapPhysicalPage(Map, MmPFNToPhysPage(Pfn), Va2, MM_PTE_ISFROMPMM | MM_PTE_COW); + bool Res1 = MiMapPhysicalPage(MmPFNToPhysPage(Pfn), Va1, MM_PTE_ISFROMPMM | MM_PTE_COW); + bool Res2 = MiMapPhysicalPage(MmPFNToPhysPage(Pfn), Va2, MM_PTE_ISFROMPMM | MM_PTE_COW); ASSERT(Res1 && Res2); MmUnlockKernelSpace(); @@ -115,7 +113,7 @@ void PerformCopyOnWriteTest() // Unmap everything. MmLockKernelSpaceExclusive(); - MiUnmapPages(Map, Va1, 2); + MiUnmapPages(Va1, 2); MmUnlockKernelSpace(); // Free the pool space. diff --git a/drivers/test/source/proctst.c b/drivers/test/source/proctst.c index ed9813a7..a8f1c748 100644 --- a/drivers/test/source/proctst.c +++ b/drivers/test/source/proctst.c @@ -31,9 +31,7 @@ void ProcessTestRoutine(UNUSED void* Ptr) // TODO: locking? - HPAGEMAP Map = MiGetCurrentPageMap(); - - MiMapAnonPages(Map, (uintptr_t) TheMemory, SizeOfTheMemory / PAGE_SIZE, MM_PTE_READWRITE, true); + MiMapAnonPages((uintptr_t) TheMemory, SizeOfTheMemory / PAGE_SIZE, MM_PTE_READWRITE, true); // Probe the memory. int Status = MmProbeAddress(TheMemory, SizeOfTheMemory, true, MODE_KERNEL); @@ -50,7 +48,7 @@ void ProcessTestRoutine(UNUSED void* Ptr) KeWaitForSingleObject(&Evnt, false, TIMEOUT_INFINITE, MODE_KERNEL); // Unmap the memory. - MiUnmapPages(Map, (uintptr_t) TheMemory, SizeOfTheMemory / PAGE_SIZE); + MiUnmapPages((uintptr_t) TheMemory, SizeOfTheMemory / PAGE_SIZE); Status = MmProbeAddress(TheMemory, SizeOfTheMemory, true, MODE_KERNEL); LogMsg("Status In Process: %d (after unmapping memory)", Status); From aa7d998db5eb51f720bc286f9ff61cf7822f520d Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Mon, 20 Oct 2025 21:03:49 +0300 Subject: [PATCH 33/44] * Mm: Completely rework the Cache Control Block structure. This allows the implementation to also work on 32-bit builds. It might be slightly slower than the old version, but if it works, it works. I will need to do more testing, though. --- boron/include/mm/cache.h | 52 ++--- boron/include/mm/pfn.h | 14 +- boron/include/mm/pmm.h | 24 ++- boron/source/build_number | 2 +- boron/source/mm/cache.c | 361 ++++++++++++++++++++++++----------- boron/source/mm/fault.c | 207 +++++++------------- boron/source/mm/mi.h | 4 + boron/source/mm/pmm.c | 70 +++++-- drivers/test/source/ccbtst.c | 4 + 9 files changed, 455 insertions(+), 283 deletions(-) diff --git a/boron/include/mm/cache.h b/boron/include/mm/cache.h index 1a00c090..38d36fc9 100644 --- a/boron/include/mm/cache.h +++ b/boron/include/mm/cache.h @@ -32,24 +32,8 @@ Module name: // I will go for 4 levels of indirection for now. typedef struct _FCB FCB, *PFCB; -typedef struct _CCB_INDIRECTION CCB_INDIRECTION, *PCCB_INDIRECTION; -typedef union _CCB_ENTRY -{ - PCCB_INDIRECTION Indirection; - MMPFN Pfn; - uintptr_t Long; -} -CCB_ENTRY, *PCCB_ENTRY; - -static_assert(sizeof(CCB_ENTRY) == sizeof(uintptr_t)); - -struct _CCB_INDIRECTION -{ - CCB_ENTRY Entries[PAGE_SIZE / sizeof(CCB_ENTRY)]; -}; - -static_assert(sizeof(CCB_INDIRECTION) == PAGE_SIZE); +#define MM_INDIRECTION_COUNT (PAGE_SIZE / sizeof(MMPFN)) typedef struct _CCB { @@ -66,13 +50,13 @@ typedef struct _CCB uint64_t FirstModifiedPage; uint64_t LastModifiedPage; - CCB_ENTRY Direct[MM_DIRECT_PAGE_COUNT]; - PCCB_INDIRECTION Level1Indirect; - PCCB_INDIRECTION Level2Indirect; - PCCB_INDIRECTION Level3Indirect; - PCCB_INDIRECTION Level4Indirect; + MMPFN Direct[MM_DIRECT_PAGE_COUNT]; + MMPFN Level1Indirect; + MMPFN Level2Indirect; + MMPFN Level3Indirect; + MMPFN Level4Indirect; #if MM_INDIRECTION_LEVELS == 5 - PCCB_INDIRECTION Level5Indirect; + MMPFN Level5Indirect; #endif } CCB, *PCCB; @@ -94,6 +78,8 @@ void MmUnlockCcb(PCCB Ccb) KeReleaseMutex(&Ccb->Mutex); } +#if 0 + // Gets a pointer to an entry in the CCB. // If TryAllocateLowerLevels is true, it will attempt to allocate levels if they aren't // allocated. However, this might fail if out of memory, in which case NULL will be @@ -103,3 +89,23 @@ void MmUnlockCcb(PCCB Ccb) // // NOTE: The CCB must be locked. PCCB_ENTRY MmGetEntryPointerCcb(PCCB Ccb, uint64_t PageOffset, bool TryAllocateLowerLevels); + +#endif + +// Retrieves the page frame number at the specified page offset within the CCB. +// This returns a PFN whose reference count is incremented by one on retrieval. +// +// This is thread safe because the CCB mutex is used internally. +MMPFN MmGetEntryCcb(PCCB Ccb, uint64_t PageOffset); + +// Assigns a PFN to the specified page offset within the CCB. +// +// OutPrototypePtePointer is nullable. +// +// If Pfn is PFN_INVALID, then this serves to: +// 1) prepare the CCB for assignment in this slot (as a performance optimization), and +// 2) check if there is already a PFN assigned to this slot (to refault instead of doing +// a useless write) +// +// If the entry is already assigned, this returns STATUS_CONFLICTING_ADDRESSES. +BSTATUS MmSetEntryCcb(PCCB Ccb, uint64_t PageOffset, MMPFN Pfn, PMM_PROTOTYPE_PTE_PTR OutPrototypePtePointer); diff --git a/boron/include/mm/pfn.h b/boron/include/mm/pfn.h index 2e150c0b..0a66595f 100644 --- a/boron/include/mm/pfn.h +++ b/boron/include/mm/pfn.h @@ -90,6 +90,7 @@ typedef struct { struct { + // This address is a **physical** address. Therefore, uint32_t _PrototypePte; uint32_t _Fcb; @@ -108,13 +109,13 @@ MMPFDBE, *PMMPFDBE; #ifdef IS_64_BIT -#define PFDBE_PrototypePte(Pfdbe) ((uintptr_t*) (0xFFFF000000000000ULL | (Pfdbe)->FileCache._PrototypePte)) -#define PFDBE_Fcb(Pfdbe) ((PFCB) (0xFFFF000000000000ULL | (Pfdbe)->FileCache._Fcb)) +#define PFDBE_PrototypePte(Pfdbe) ((MMPFN*) (0xFFFF000000000000ULL | (Pfdbe)->FileCache._PrototypePte)) +#define PFDBE_Fcb(Pfdbe) ((PFCB) (0xFFFF000000000000ULL | (Pfdbe)->FileCache._Fcb)) #else -#define PFDBE_PrototypePte(Pfdbe) ((uintptr_t*) ((Pfdbe)->FileCache._PrototypePte)) -#define PFDBE_Fcb(Pfdbe) ((PFCB) ((Pfdbe)->FileCache._Fcb)) +#define PFDBE_PrototypePte(Pfdbe) ((MMPFN*) ((Pfdbe)->FileCache._PrototypePte)) +#define PFDBE_Fcb(Pfdbe) ((PFCB) ((Pfdbe)->FileCache._Fcb)) #endif @@ -134,6 +135,11 @@ enum #define PFN_INVALID ((MMPFN)-1) +// Returned by the page cache. Watch out! +#define MM_PFN_OUTOFMEMORY ((MMPFN) -2) + +#define IS_BAD_PFN(Pfn) ((Pfn) == PFN_INVALID || (Pfn) == MM_PFN_OUTOFMEMORY) + #ifdef IS_64_BIT static_assert((sizeof(MMPFDBE) & (sizeof(MMPFDBE) - 1)) == 0, "The page frame struct should be a power of two"); #endif diff --git a/boron/include/mm/pmm.h b/boron/include/mm/pmm.h index 07c29b39..b89d30bb 100644 --- a/boron/include/mm/pmm.h +++ b/boron/include/mm/pmm.h @@ -17,6 +17,22 @@ Module name: #include +#ifdef IS_64_BIT + +typedef MMPFN *MM_PROTOTYPE_PTE_PTR, **PMM_PROTOTYPE_PTE_PTR; +#define MM_PROTOTYPE_PTE_PTR_NONE (NULL) + +#else + +// 32-bit prototype PTE addresses should be OR'd with this value to +// make them virtual. +#define MM_PROTO_PTE_PTR_IS_VIRTUAL (1 << 0) +#define MM_VIRTUAL_PROTO_PTE_PTR(Ptr) ((uintptr_t)(Ptr) | 1) +#define MM_PROTOTYPE_PTE_PTR_NONE (0) +typedef uintptr_t MM_PROTOTYPE_PTE_PTR, *PMM_PROTOTYPE_PTE_PTR; + +#endif + typedef struct _FCB FCB, *PFCB; #ifdef KERNEL @@ -41,6 +57,8 @@ uintptr_t MmGetHHDMOffsetFromAddr(void* Addr); // Converts a physical address to a page frame number (PFN). MMPFN MmPhysPageToPFN(uintptr_t PhysAddr); +#define MmGetHHDMOffsetAddrPfn(Pfn) MmGetHHDMOffsetAddr(MmPFNToPhysPage(Pfn)) + #ifdef IS_32_BIT void MmBeginUsingHHDM(void); @@ -64,9 +82,9 @@ MMPFN MmAllocatePhysicalPage(void); void MmPageAddReference(MMPFN Pfn); // Assign a prototype PTE address to the page frame. -void MmSetPrototypePtePfn(MMPFN Pfn, uintptr_t* PrototypePte); +void MmSetPrototypePtePfn(MMPFN Pfn, MM_PROTOTYPE_PTE_PTR PrototypePte); -// Assign a prototype PTE address, FCB pointer and offset, to the page frame. +// Assign an FCB pointer and offset, to the page frame. // // Note that the reference to the FCB is weak, i.e. it does not count towards // the FCB's reference count. When the FCB is deleted, the entire page cache @@ -74,7 +92,7 @@ void MmSetPrototypePtePfn(MMPFN Pfn, uintptr_t* PrototypePte); // // The offset is saved in multiples of page size, but the passed in offset // is in bytes. -void MmSetCacheDetailsPfn(MMPFN Pfn, uintptr_t* PrototypePte, PFCB Fcb, uint64_t Offset); +void MmSetCacheDetailsPfn(MMPFN Pfn, PFCB Fcb, uint64_t Offset); // Set an allocated page as modified. void MmSetModifiedPfn(MMPFN Pfn); diff --git a/boron/source/build_number b/boron/source/build_number index e5a135a5..8410b8b8 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -445 +473 diff --git a/boron/source/mm/cache.c b/boron/source/mm/cache.c index d37aab95..5b322bee 100644 --- a/boron/source/mm/cache.c +++ b/boron/source/mm/cache.c @@ -14,13 +14,25 @@ Module name: ***/ #include "mi.h" -// ***TODO: Rewrite this for 32-bit builds*** +// TODO: There is probably going to be a lot of contention on the PFDB lock +// from here. Find a better way. void MmInitializeCcb(PCCB Ccb) { memset(Ccb, 0, sizeof *Ccb); KeInitializeMutex(&Ccb->Mutex, MM_CCB_MUTEX_LEVEL); + + for (int i = 0; i < MM_DIRECT_PAGE_COUNT; i++) + Ccb->Direct[i] = PFN_INVALID; + + Ccb->Level1Indirect = PFN_INVALID; + Ccb->Level2Indirect = PFN_INVALID; + Ccb->Level3Indirect = PFN_INVALID; + Ccb->Level4Indirect = PFN_INVALID; +#if MM_INDIRECTION_LEVELS == 5 + Ccb->Level5Indirect = PFN_INVALID; +#endif } void MmTearDownCcb(PCCB Ccb) @@ -33,141 +45,276 @@ void MmTearDownCcb(PCCB Ccb) DbgPrint("TODO: MmTearDownCcb(%p)", Ccb); } -PCCB_ENTRY MmGetEntryPointerCcb(PCCB Ccb, uint64_t PageOffset, bool TryAllocateLowerLevels) +static MMPFN MiNextEntryCache(MMPFN PfnIndirection, uint64_t* PageOffset, bool AllocateIndirection) { - ASSERT(Ccb->Mutex.Header.Signaled > 0 && Ccb->Mutex.OwnerThread == KeGetCurrentThread()); + if (IS_BAD_PFN(PfnIndirection)) + return PFN_INVALID; - if (PageOffset < MM_DIRECT_PAGE_COUNT) - return &Ccb->Direct[PageOffset]; - PageOffset -= MM_DIRECT_PAGE_COUNT; + size_t InPageOffset = *PageOffset & (MM_INDIRECTION_COUNT - 1); + *PageOffset /= MM_INDIRECTION_COUNT; - size_t EntriesPerPage = ARRAY_COUNT(Ccb->Level1Indirect[0].Entries); - size_t Range = EntriesPerPage; + // This is slower than it probably should be. + // + // However, we have to guard against the PFN getting reclaimed + // from under our nose before we get to add our own reference to it. + MmBeginUsingHHDM(); + PMMPFN Indirection = MmGetHHDMOffsetAddrPfn(PfnIndirection); - // Level 1 - if (PageOffset < EntriesPerPage) + KIPL Ipl = MiLockPfdb(); + MMPFN Pfn = Indirection[InPageOffset]; + + if (Pfn == PFN_INVALID && AllocateIndirection) { - //DbgPrint("L1: OffsetL0=%04x", PageOffset); - if (!Ccb->Level1Indirect) + bool IsZeroed; + Pfn = MiAllocatePhysicalPageWithPfdbLocked(&IsZeroed); + if (Pfn == PFN_INVALID) { - if (!TryAllocateLowerLevels) - return NULL; - - if (!(Ccb->Level1Indirect = MmAllocatePhysicalPageHHDM())) - { - DbgPrint("MmGetEntryPointerCcb(%p, %llu, %d): could not allocate 1st level indirection table", Ccb, PageOffset, TryAllocateLowerLevels); - return NULL; - } + // still invalid, so no dice. + MiUnlockPfdb(Ipl); + MmEndUsingHHDM(); + return MM_PFN_OUTOFMEMORY; } - return &Ccb->Level1Indirect->Entries[PageOffset]; + // PFN is valid, so clear it + memset(MmGetHHDMOffsetAddrPfn(Pfn), 0xFF, PAGE_SIZE); + Indirection[InPageOffset] = Pfn; } - PageOffset -= EntriesPerPage; - Range *= EntriesPerPage; - // Level 2 - if (PageOffset < Range) + if (Pfn != PFN_INVALID) + MiPageAddReferenceWithPfdbLocked(Pfn); + + MiUnlockPfdb(Ipl); + MmEndUsingHHDM(); + return Pfn; +} + +// Input: PfnIndirection - the PFN of the indirection to store to, PageOffset - the offset inside this indirection +static BSTATUS MiAssignEntryToCache( + PMMPFN Pointer, + MMPFN Pfn, + bool IsHhdm, + PMM_PROTOTYPE_PTE_PTR OutPrototypePtePointer +) +{ + // If we're trying to set a PFN, and a PFN is already set: + if (!IS_BAD_PFN(*Pointer)) + return STATUS_CONFLICTING_ADDRESSES; + + // Assign the entry now. + if (!IS_BAD_PFN(Pfn)) { - uint64_t OffsetL1 = PageOffset % EntriesPerPage; - uint64_t OffsetL0 = PageOffset / EntriesPerPage; - //DbgPrint("L2: OffsetL0=%04x OffsetL1=%04x", OffsetL0, OffsetL1); + *Pointer = Pfn; + + MM_PROTOTYPE_PTE_PTR ProtoPtePtr; - if (!Ccb->Level2Indirect) - { - if (!TryAllocateLowerLevels) - return NULL; - - if (!(Ccb->Level2Indirect = MmAllocatePhysicalPageHHDM())) - { - DbgPrint("MmGetEntryPointerCcb(%p, %llu, %d): could not allocate 2nd level (root) indirection table", Ccb, PageOffset, TryAllocateLowerLevels); - return NULL; - } - } - if (!Ccb->Level2Indirect->Entries[OffsetL0].Indirection) - { - if (!TryAllocateLowerLevels) - return NULL; - - if (!(Ccb->Level2Indirect->Entries[OffsetL0].Indirection = MmAllocatePhysicalPageHHDM())) - { - DbgPrint("MmGetEntryPointerCcb(%p, %llu, %d): could not allocate 2nd level (L1) indirection table", Ccb, PageOffset, TryAllocateLowerLevels); - return NULL; - } - } + #ifdef IS_32_BIT + if (IsHhdm) + ProtoPtePtr = MmGetHHDMOffsetFromAddr(Pointer); + else + ProtoPtePtr = MM_VIRTUAL_PROTO_PTE_PTR(Pointer); + #else + (void) IsHhdm; + ProtoPtePtr = Pointer; + #endif + + MmSetPrototypePtePfn(Pfn, ProtoPtePtr); - return &Ccb->Level2Indirect->Entries[OffsetL0].Indirection->Entries[OffsetL1]; + if (OutPrototypePtePointer) + *OutPrototypePtePointer = ProtoPtePtr; } - PageOffset -= Range; - Range *= EntriesPerPage; - // Level 3 - if (PageOffset < Range) + return STATUS_SUCCESS; +} + +static BSTATUS MiAssignEntryToCacheIndirect( + MMPFN PfnIndirection, + uint64_t PageOffset, + MMPFN Pfn, + PMM_PROTOTYPE_PTE_PTR OutPrototypePtePointer +) +{ + MmBeginUsingHHDM(); + + PMMPFN Indirection = MmGetHHDMOffsetAddrPfn(PfnIndirection); + BSTATUS Result = MiAssignEntryToCache(Indirection + PageOffset, Pfn, false, OutPrototypePtePointer); + + MmEndUsingHHDM(); + return Result; +} + +static bool MmpEnsureIndirectionExists(PMMPFN Indirection, bool AllocateIndirection) +{ + if (!IS_BAD_PFN(*Indirection)) + return true; + + if (!AllocateIndirection) + return false; + + *Indirection = MmAllocatePhysicalPage(); + + MmBeginUsingHHDM(); + PMMPFN Memory = MmGetHHDMOffsetAddrPfn(*Indirection); + memset(Memory, 0xFF, PAGE_SIZE); + MmEndUsingHHDM(); + + return !IS_BAD_PFN(*Indirection); +} + +// Takes in the page offset as passed into MmGetEntryCcb and MmSetEntryCcb. +// However, this does nothing if PageOffset < MM_DIRECT_PAGE_COUNT. +static MMPFN MiWalkCacheUntilIndirection(PCCB Ccb, uint64_t* PageOffset, bool AllocateIndirection) +{ +#ifdef DEBUG + uint64_t OriginalPageOffset = *PageOffset; +#endif + const MMPFN Failure = AllocateIndirection ? MM_PFN_OUTOFMEMORY : PFN_INVALID; + + ASSERT(*PageOffset >= MM_DIRECT_PAGE_COUNT); + *PageOffset -= MM_DIRECT_PAGE_COUNT; + + // Level 1 + if (*PageOffset < MM_DIRECT_PAGE_COUNT) { - uint64_t OffsetL2 = PageOffset % EntriesPerPage; - uint64_t OffsetL1 = PageOffset / EntriesPerPage % EntriesPerPage; - uint64_t OffsetL0 = PageOffset / EntriesPerPage / EntriesPerPage; - //DbgPrint("L3: OffsetL0=%04x OffsetL1=%04x OffsetL2=%04x", OffsetL0, OffsetL1, OffsetL2); + if (!MmpEnsureIndirectionExists(&Ccb->Level1Indirect, AllocateIndirection)) + return Failure; - if (!Ccb->Level3Indirect) - { - if (!TryAllocateLowerLevels) - return NULL; - - if (!(Ccb->Level3Indirect = MmAllocatePhysicalPageHHDM())) - { - DbgPrint("MmGetEntryPointerCcb(%p, %llu, %d): could not allocate 3rd level (root) indirection table", Ccb, PageOffset, TryAllocateLowerLevels); - return NULL; - } - } - if (!Ccb->Level3Indirect->Entries[OffsetL0].Indirection) - { - if (!TryAllocateLowerLevels) - return NULL; - - if (!(Ccb->Level3Indirect->Entries[OffsetL0].Indirection = MmAllocatePhysicalPageHHDM())) - { - DbgPrint("MmGetEntryPointerCcb(%p, %llu, %d): could not allocate 3rd level (L1) indirection table", Ccb, PageOffset, TryAllocateLowerLevels); - return NULL; - } - } + // Well, this is trivial. + MmPageAddReference(Ccb->Level1Indirect); + return Ccb->Level1Indirect; + } + + // Level 2 + *PageOffset -= MM_DIRECT_PAGE_COUNT; + if (*PageOffset < MM_INDIRECTION_COUNT * MM_INDIRECTION_COUNT) + { + if (!MmpEnsureIndirectionExists(&Ccb->Level2Indirect, AllocateIndirection)) + return Failure; - if (!Ccb->Level3Indirect->Entries[OffsetL0].Indirection->Entries[OffsetL1].Indirection) - { - if (!TryAllocateLowerLevels) - return NULL; - - if (!(Ccb->Level3Indirect->Entries[OffsetL0].Indirection->Entries[OffsetL1].Indirection = MmAllocatePhysicalPageHHDM())) - { - DbgPrint("MmGetEntryPointerCcb(%p, %llu, %d): could not allocate 3rd level (L1) indirection table", Ccb, PageOffset, TryAllocateLowerLevels); - return NULL; - } - } + // This is also kind of trivial although less so. + return MiNextEntryCache(Ccb->Level2Indirect, PageOffset, AllocateIndirection); + } + + // Level 3 + *PageOffset -= MM_INDIRECTION_COUNT; + if (*PageOffset < MM_INDIRECTION_COUNT * MM_INDIRECTION_COUNT) + { + if (!MmpEnsureIndirectionExists(&Ccb->Level3Indirect, AllocateIndirection)) + return Failure; + + MMPFN Pfn3 = MiNextEntryCache(Ccb->Level3Indirect, PageOffset, AllocateIndirection); + if (IS_BAD_PFN(Pfn3)) + return Pfn3; - return &Ccb->Level3Indirect->Entries[OffsetL0].Indirection->Entries[OffsetL1].Indirection->Entries[OffsetL2]; + MMPFN Pfn2 = MiNextEntryCache(Pfn3, PageOffset, AllocateIndirection); + MmFreePhysicalPage(Pfn3); + return Pfn2; } - PageOffset -= Range; - Range *= EntriesPerPage; // Level 4 - if (PageOffset < Range) + *PageOffset -= MM_INDIRECTION_COUNT * MM_INDIRECTION_COUNT; + if (*PageOffset < MM_INDIRECTION_COUNT * MM_INDIRECTION_COUNT * MM_INDIRECTION_COUNT) { - DbgPrint("MmGetEntryPointerCcb(%p, %llu, %d): TODO: level 4", Ccb, PageOffset, TryAllocateLowerLevels); - return NULL; + if (!MmpEnsureIndirectionExists(&Ccb->Level4Indirect, AllocateIndirection)) + return Failure; + + MMPFN Pfn4 = MiNextEntryCache(Ccb->Level4Indirect, PageOffset, AllocateIndirection); + if (IS_BAD_PFN(Pfn4)) + return Pfn4; + + MMPFN Pfn3 = MiNextEntryCache(Pfn4, PageOffset, AllocateIndirection); + MmFreePhysicalPage(Pfn4); + if (IS_BAD_PFN(Pfn3)) + return Pfn3; + + MMPFN Pfn2 = MiNextEntryCache(Pfn3, PageOffset, AllocateIndirection); + MmFreePhysicalPage(Pfn3); + return Pfn2; } - PageOffset -= Range; - Range *= EntriesPerPage; #if MM_INDIRECTION_LEVELS == 5 // Level 5 - if (PageOffset < Range) + *PageOffset -= MM_INDIRECTION_COUNT * MM_INDIRECTION_COUNT * MM_INDIRECTION_COUNT; + if (*PageOffset < MM_INDIRECTION_COUNT * MM_INDIRECTION_COUNT * MM_INDIRECTION_COUNT * MM_INDIRECTION_COUNT) { - DbgPrint("MmGetEntryPointerCcb(%p, %llu, %d): TODO: level 5", Ccb, PageOffset, TryAllocateLowerLevels); - return NULL; + if (!MmpEnsureIndirectionExists(&Ccb->Level5Indirect, AllocateIndirection)) + return Failure; + + MMPFN Pfn5 = MiNextEntryCache(Ccb->Level5Indirect, PageOffset, AllocateIndirection); + if (IS_BAD_PFN(Pfn4)) + return Pfn4; + + MMPFN Pfn4 = MiNextEntryCache(Pfn5, PageOffset, AllocateIndirection); + MmFreePhysicalPage(Pfn5); + if (IS_BAD_PFN(Pfn4)) + return Pfn4; + + MMPFN Pfn3 = MiNextEntryCache(Pfn4, PageOffset, AllocateIndirection); + MmFreePhysicalPage(Pfn4); + if (IS_BAD_PFN(Pfn3)) + return Pfn3; + + MMPFN Pfn2 = MiNextEntryCache(Pfn3, PageOffset, AllocateIndirection); + MmFreePhysicalPage(Pfn3); + return Pfn2; } - PageOffset -= Range; - Range *= EntriesPerPage; #endif + + DbgPrint("MiWalkCacheUntilIndirection: Page offset %zu is outside of the supported range.", OriginalPageOffset); + return PFN_INVALID; +} + +MMPFN MmGetEntryCcb(PCCB Ccb, uint64_t PageOffset) +{ + MMPFN Pfn = PFN_INVALID; + MmLockCcb(Ccb); + + if (PageOffset < MM_DIRECT_PAGE_COUNT) + { + KIPL Ipl = MiLockPfdb(); + Pfn = Ccb->Direct[PageOffset]; + + if (!IS_BAD_PFN(Pfn)) + MiPageAddReferenceWithPfdbLocked(Pfn); + + MiUnlockPfdb(Ipl); + goto Done; + } + + Pfn = MiWalkCacheUntilIndirection(Ccb, &PageOffset, false); + if (!IS_BAD_PFN(Pfn)) + { + MMPFN Pfn2 = Pfn; + Pfn = MiNextEntryCache(Pfn2, &PageOffset, false); + MmFreePhysicalPage(Pfn2); + } + + if (IS_BAD_PFN(Pfn)) + Pfn = PFN_INVALID; + +Done: + MmUnlockCcb(Ccb); + return Pfn; +} + +BSTATUS MmSetEntryCcb(PCCB Ccb, uint64_t PageOffset, MMPFN InPfn, PMM_PROTOTYPE_PTE_PTR OutPrototypePtePointer) +{ + BSTATUS Status = STATUS_INSUFFICIENT_MEMORY; + MmLockCcb(Ccb); + + if (PageOffset < MM_DIRECT_PAGE_COUNT) + { + Status = MiAssignEntryToCache(&Ccb->Direct[PageOffset], InPfn, false, OutPrototypePtePointer); + goto Exit; + } + + MMPFN Pfn = MiWalkCacheUntilIndirection(Ccb, &PageOffset, true); + if (!IS_BAD_PFN(Pfn)) + { + Status = MiAssignEntryToCacheIndirect(Pfn, PageOffset, InPfn, OutPrototypePtePointer); + MmFreePhysicalPage(Pfn); + } - DbgPrint("MmGetEntryPointerCcb(%p, %llu, %d): no more levels to check", Ccb, PageOffset, TryAllocateLowerLevels); - return NULL; +Exit: + MmUnlockCcb(Ccb); + return Status; } diff --git a/boron/source/mm/fault.c b/boron/source/mm/fault.c index 10d23f4c..26bcdbe2 100644 --- a/boron/source/mm/fault.c +++ b/boron/source/mm/fault.c @@ -64,6 +64,30 @@ static BSTATUS MmpHandleFaultCommittedPage(PMMPTE PtePtr, MMPTE SupervisorBit) return STATUS_SUCCESS; } +static BSTATUS MmpAssignPfnToAddress(uintptr_t Va, MMPFN Pfn, bool SetCacheDetails, uint32_t VadFlagsLong, PFILE_OBJECT FileObject, uint64_t FileOffset) +{ + MMPTE SupervisorBit = Va >= MM_KERNEL_SPACE_BASE ? 0 : MM_PTE_USERACCESS; + + PMMPTE PtePtr = MmGetPteLocationCheck(Va, true); + if (!PtePtr) + return STATUS_INSUFFICIENT_MEMORY; + + MMPTE NewPte = MM_PTE_PRESENT | MM_PTE_ISFROMPMM | SupervisorBit | MmPFNToPhysPage(Pfn); + + MMVAD_FLAGS VadFlags; + VadFlags.LongFlags = VadFlagsLong; + + if (VadFlags.Cow) + NewPte |= MM_PTE_COW; + + *PtePtr = NewPte; + + if (SetCacheDetails) + MmSetCacheDetailsPfn(Pfn, FileObject->Fcb, FileOffset); + + return STATUS_SUCCESS; +} + static BSTATUS MmpHandleFaultCommittedMappedPage( uintptr_t Va, uintptr_t VaBase, @@ -93,12 +117,6 @@ static BSTATUS MmpHandleFaultCommittedMappedPage( // Now, this is obviously just user error, but can this be exploited? // Probably not. When the page fault finishes, BSTATUS Status; - PMMPTE PtePtr = NULL; - - MMPTE SupervisorBit = Va >= MM_KERNEL_SPACE_BASE ? 0 : MM_PTE_USERACCESS; - - MMVAD_FLAGS VadFlags; - VadFlags.LongFlags = VadFlagsLong; const uintptr_t PageMask = ~(PAGE_SIZE - 1); uint64_t FileOffset = (Va & PageMask) - VaBase + MappedOffset; @@ -114,7 +132,6 @@ static BSTATUS MmpHandleFaultCommittedMappedPage( { MMPFN Pfn = PFN_INVALID; PFILE_OBJECT FileObject = (PFILE_OBJECT) Object; - PCCB_ENTRY PCcbEntry = NULL; // First, check if the BackingMemory call is supported. IO_STATUS_BLOCK Iosb; @@ -164,66 +181,24 @@ static BSTATUS MmpHandleFaultCommittedMappedPage( } else { - MmLockCcb(PageCache); - - PCcbEntry = MmGetEntryPointerCcb(PageCache, FileOffset / PAGE_SIZE, false); - if (PCcbEntry && AtLoad(PCcbEntry->Long)) - { - // Lock the physical memory lock because the page we're looking for - // might have been freed. - // - // This is, as far as I know, the only way. Sure, if the CCB entry - // doesn't exist and it is zero, then you are able to skip over the - // PFN lock at no extra cost (you'd just be reloading the same page - // twice potentially) - KIPL Ipl = MiLockPfdb(); - - // After this, the CCB entry cannot be changed. This is because - // when a page is reclaimed the PFDB lock is held. - CCB_ENTRY CcbEntry; - if (PCcbEntry) - CcbEntry = *PCcbEntry; - - if (CcbEntry.Long) - { - // We managed to find the PFN in one piece. Add a reference to it as we - // will just map it directly. - Pfn = CcbEntry.Pfn; - MiPageAddReferenceWithPfdbLocked(Pfn); - } - - MiUnlockPfdb(Ipl); - } - - MmUnlockCcb(PageCache); + Pfn = MmGetEntryCcb(PageCache, FileOffset / PAGE_SIZE); } // Did we find the PFN already? if (Pfn != PFN_INVALID) { - // As it turns out, yes! - PtePtr = MmGetPteLocationCheck(Va, true); - if (!PtePtr) + Status = MmpAssignPfnToAddress(Va, Pfn, BackingMemory == NULL, VadFlagsLong, FileObject, FileOffset); + if (FAILED(Status)) { + MmFreePhysicalPage(Pfn); + DbgPrint("%s: out of memory because PTE couldn't be allocated (1)", __func__); + ASSERT(Status == STATUS_INSUFFICIENT_MEMORY); Status = STATUS_REFAULT_SLEEP; goto Exit; } - MMPTE NewPte = MM_PTE_PRESENT | MM_PTE_ISFROMPMM | SupervisorBit | MmPFNToPhysPage(Pfn); - - if (VadFlags.Cow) - NewPte |= MM_PTE_COW; - - *PtePtr = NewPte; - - if (!BackingMemory) - { - MmSetCacheDetailsPfn(Pfn, &PCcbEntry->Long, FileObject->Fcb, FileOffset); - } - PFDbgPrint("%s: hooray! page fault fulfilled by cached fetch %p", __func__, PtePtr); - Status = STATUS_SUCCESS; goto Exit; } @@ -235,11 +210,8 @@ static BSTATUS MmpHandleFaultCommittedMappedPage( ASSERT(!BackingMemory); // First, ensure the CCB entry can be there. - MmLockCcb(PageCache); - PCcbEntry = MmGetEntryPointerCcb(PageCache, FileOffset / PAGE_SIZE, true); - MmUnlockCcb(PageCache); - - if (!PCcbEntry) + int SetResult = MmSetEntryCcb(PageCache, FileOffset / PAGE_SIZE, PFN_INVALID, NULL); + if (SetResult == STATUS_INSUFFICIENT_MEMORY) { // Out of memory. DbgPrint("%s: out of memory because CCB entry couldn't be allocated (1)", __func__); @@ -247,13 +219,20 @@ static BSTATUS MmpHandleFaultCommittedMappedPage( goto Exit; } + if (SetResult == STATUS_CONFLICTING_ADDRESSES) + { + // Already assigned. + DbgPrint("%s: CCB entry was found to be assigned, refaulting", __func__); + Status = STATUS_REFAULT; + goto Exit; + } + // Allocate a PFN now, do a read, and then put it into the CCB. Pfn = MmAllocatePhysicalPage(); if (Pfn == PFN_INVALID) { // Out of memory. DbgPrint("%s: out of memory because a page frame couldn't be allocated", __func__); - MmUnlockCcb(PageCache); Status = STATUS_REFAULT_SLEEP; goto Exit; } @@ -276,7 +255,6 @@ static BSTATUS MmpHandleFaultCommittedMappedPage( // Ran out of memory trying to read this file. DbgPrint("%s: cannot answer page fault because I/O failed with code %d", __func__, Status); MmFreePhysicalPage(Pfn); - MmUnlockCcb(PageCache); if (Status == STATUS_INSUFFICIENT_MEMORY) { @@ -288,87 +266,49 @@ static BSTATUS MmpHandleFaultCommittedMappedPage( } // The I/O operation has succeeded. - // Prepare the CCB entry. - CCB_ENTRY CcbEntryNew; - CcbEntryNew.Long = 0; - CcbEntryNew.Pfn = Pfn; - // It's time to put this in the CCB and then map. - MmLockCcb(PageCache); - PCcbEntry = MmGetEntryPointerCcb(PageCache, FileOffset / PAGE_SIZE, true); - if (!PCcbEntry) - { - // Out of memory. Did someone remove our prior allocation? - // Well, we've got almost no choice but to throw away our result. - // - // (We could instead beg the memory manager for more memory, but - // I don't feel like writing that right now.) - DbgPrint("%s: out of memory because CCB entry couldn't be allocated (2)", __func__); - MmFreePhysicalPage(Pfn); - MmUnlockCcb(PageCache); - Status = STATUS_REFAULT_SLEEP; - goto Exit; - } - - // There you are! - // - // If the CCB entry is zero, then it'll be populated with the - // new CCB entry. Otherwise, there's something already there - // and we have simply wasted our time and we need to refault. - MMPTE Expected = 0; - if (AtCompareExchange(&PCcbEntry->Long, &Expected, CcbEntryNew.Long)) + Status = MmSetEntryCcb(PageCache, FileOffset / PAGE_SIZE, Pfn, NULL); + if (FAILED(Status)) { - // Compare-exchange successful. Note: we are surrendering - // our reference of the physical page to the CCB. - MmUnlockCcb(PageCache); - - // Acquire the address space lock now. - SpaceUnlockIpl = MmLockSpaceExclusive(Va); - - // We need to refetch the PTE pointer because it may have been - // freed in the meantime. - PtePtr = MmGetPteLocationCheck(Va, true); - if (!PtePtr) + if (Status == STATUS_CONFLICTING_ADDRESSES) { - DbgPrint("%s: out of memory because PTE couldn't be allocated (2)", __func__); - MmFreePhysicalPage(Pfn); - MmUnlockCcb(PageCache); - Status = STATUS_REFAULT_SLEEP; - goto Exit; + // Need to throw away our hard work as someone finished it before us. + DbgPrint("%s: CCB entry was found to be assigned by the time IO was made (%p), refaulting (2)", __func__, Va); + Status = STATUS_REFAULT; } - - MMPTE NewPte = *PtePtr; - - // Are you already present?! - if (NewPte & MM_PTE_PRESENT) + else { - // Yeah, you seem to already be present. So we just - // wasted all of our time. - DbgPrint("%s: va already valid by the time IO was made (%p)", __func__, Va); - MmFreePhysicalPage(Pfn); - Status = STATUS_SUCCESS; - goto Exit; + // Out of memory. Did someone remove our prior allocation? + // Well, we've got almost no choice but to throw away our result. + // + // (We could instead beg the memory manager for more memory, but + // I don't feel like writing that right now.) + ASSERT(Status == STATUS_INSUFFICIENT_MEMORY); + DbgPrint("%s: out of memory because CCB entry couldn't be allocated (2)", __func__); + Status = STATUS_REFAULT_SLEEP; } - NewPte = MM_PTE_PRESENT | MM_PTE_ISFROMPMM | SupervisorBit | MmPFNToPhysPage(Pfn); - - if (VadFlags.Cow) - NewPte |= MM_PTE_COW; - - MmSetCacheDetailsPfn(Pfn, &PCcbEntry->Long, FileObject->Fcb, FileOffset); - - *PtePtr = NewPte; - - PFDbgPrint("%s: hooray! page fault fulfilled by I/O read", __func__); - Status = STATUS_SUCCESS; + MmFreePhysicalPage(Pfn); goto Exit; } - // Something's already there! - // It's most likely a good page, so let's just refault. - DbgPrint("%s: ccb already found by the time IO was made (%p), refaulting", __func__, Va); - Status = STATUS_REFAULT; - MmUnlockCcb(PageCache); + // Success! This is the right PFN, so assign it. + + // Acquire the address space lock now. + SpaceUnlockIpl = MmLockSpaceExclusive(Va); + + Status = MmpAssignPfnToAddress(Va, Pfn, true, VadFlagsLong, FileObject, FileOffset); + if (FAILED(Status)) + { + DbgPrint("%s: out of memory because PTE couldn't be allocated (2)", __func__); + MmFreePhysicalPage(Pfn); + Status = STATUS_REFAULT_SLEEP; + goto Exit; + } + + PFDbgPrint("%s: hooray! page fault fulfilled by I/O read", __func__); + Status = STATUS_SUCCESS; + goto Exit; } Exit: @@ -678,6 +618,7 @@ BSTATUS MmPageFault(UNUSED uintptr_t FaultPC, uintptr_t FaultAddress, uintptr_t BSTATUS Status = KeWaitForSingleObject(&Timer, true, TIMEOUT_INFINITE, KeGetPreviousMode()); if (FAILED(Status)) { + DbgPrint("MmPageFault: Failed to sleep?! %s (%d)", RtlGetStatusString(Status), Status); KeCancelTimer(&Timer); } else diff --git a/boron/source/mm/mi.h b/boron/source/mm/mi.h index efd430ba..773cdeef 100644 --- a/boron/source/mm/mi.h +++ b/boron/source/mm/mi.h @@ -35,6 +35,10 @@ KIPL MiLockPfdb(); // Unlocks the page frame database's spinlock. void MiUnlockPfdb(KIPL Ipl); +// This does the same as MmAllocatePhysicalPage, but expects the PFDB +// lock to be locked. +MMPFN MiAllocatePhysicalPageWithPfdbLocked(bool* IsZeroed); + // Gets the reference count of a page by PFN. // The PFN lock must be held. int MiGetReferenceCountPfn(MMPFN Pfn); diff --git a/boron/source/mm/pmm.c b/boron/source/mm/pmm.c index 68c20785..7139ab50 100644 --- a/boron/source/mm/pmm.c +++ b/boron/source/mm/pmm.c @@ -25,6 +25,26 @@ Module name: #define PmmDbgPrint(...) do {} while (0) #endif +// LOCKING: +// +// You might think that I would need to establish a locking +// order between the PFN lock and the HHDM lock. But I won't. +// Why? Well, let's take a look at the two cases: +// +// 64-bit: The HHDM lock doesn't exist. Like, at all. So, +// MmBeginUsingHHDM and MmEndUsingHHDM are no-ops, and as +// such, both orderings work. +// +// 32-bit: The HHDM lock exists. However, since we're on a +// non-SMP system (assert that later), that means that +// regardless of what lock we grab first, the other is +// guaranteed to be unlocked. So there is no locking- +// inversion-caused deadlock. + +#if defined IS_32_BIT && defined CONFIG_SMP +#error You should fix this locking inversion bug! It could result in nasty deadlocks! +#endif + static KSPIN_LOCK MmPfnLock; // Free page statistics @@ -673,10 +693,30 @@ static void MmpInitializePfn(PMMPFDBE Pfdbe) // MiDetransitionPfn(PFN(pte)) // release PTE lock // } + // + // ^^^^^ this is old as hell and probably outdated TODO: clarify or remove ASSERT(Pfdbe->FileCache._PrototypePte); - uintptr_t* Ptr = PFDBE_PrototypePte(Pfdbe); + MM_PROTOTYPE_PTE_PTR Ptr = PFDBE_PrototypePte(Pfdbe); + +#ifdef IS_64_BIT + AtStore(*Ptr, 0); + +#else + if (Ptr & MM_PROTO_PTE_PTR_IS_VIRTUAL) + { + // Virtual + AtStore(*(PMMPFN)(Ptr & ~MM_PROTO_PTE_PTR_IS_VIRTUAL), 0); + } + else + { + // Physical + MmBeginUsingHHDM(); + AtStore(*(PMMPFN)MmGetHHDMOffsetAddr(Ptr), 0); + MmEndUsingHHDM(); + } +#endif } Pfdbe->Type = PF_TYPE_USED; @@ -698,24 +738,31 @@ static MMPFN MmpAllocateFromFreeList(PMMPFN First, PMMPFN Last) MmpRemovePfnFromList(First, Last, *First); MmpInitializePfn(pPF); + ASSERT(currPFN != PFN_INVALID && currPFN != MM_PFN_OUTOFMEMORY); return currPFN; } -MMPFN MmAllocatePhysicalPage() +MMPFN MiAllocatePhysicalPageWithPfdbLocked(bool* IsZeroed) { - KIPL OldIpl; - KeAcquireSpinLock(&MmPfnLock, &OldIpl); - - bool FromZero = true; + *IsZeroed = true; MMPFN currPFN = MmpAllocateFromFreeList(&MiFirstZeroPFN, &MiLastZeroPFN); if (currPFN == PFN_INVALID) { - FromZero = false; + *IsZeroed = false; currPFN = MmpAllocateFromFreeList(&MiFirstFreePFN, &MiLastFreePFN); if (currPFN == PFN_INVALID) currPFN = MmpAllocateFromFreeList(&MiFirstStandbyPFN, &MiLastStandbyPFN); } + return currPFN; +} + +MMPFN MmAllocatePhysicalPage() +{ + bool FromZero; + KIPL OldIpl; + KeAcquireSpinLock(&MmPfnLock, &OldIpl); + MMPFN currPFN = MiAllocatePhysicalPageWithPfdbLocked(&FromZero); KeReleaseSpinLock(&MmPfnLock, OldIpl); if (!FromZero && currPFN != PFN_INVALID) @@ -750,7 +797,7 @@ MMPFN MiRemoveOneModifiedPfn() return Pfn; } -void MmSetPrototypePtePfn(MMPFN Pfn, uintptr_t* PrototypePte) +void MmSetPrototypePtePfn(MMPFN Pfn, MM_PROTOTYPE_PTE_PTR PrototypePte) { ASSERT(Pfn != PFN_INVALID); @@ -759,12 +806,12 @@ void MmSetPrototypePtePfn(MMPFN Pfn, uintptr_t* PrototypePte) // If this page is freed after this operation, then the prototype // PTE will be atomically set to zero when reclaimed. - MmGetPageFrameFromPFN(Pfn)->FileCache._PrototypePte = (uint64_t) PrototypePte; + MmGetPageFrameFromPFN(Pfn)->FileCache._PrototypePte = (uintptr_t) PrototypePte; KeReleaseSpinLock(&MmPfnLock, OldIpl); } -void MmSetCacheDetailsPfn(MMPFN Pfn, uintptr_t* PrototypePte, PFCB Fcb, uint64_t Offset) +void MmSetCacheDetailsPfn(MMPFN Pfn, PFCB Fcb, uint64_t Offset) { ASSERT(Pfn != PFN_INVALID); @@ -775,8 +822,7 @@ void MmSetCacheDetailsPfn(MMPFN Pfn, uintptr_t* PrototypePte, PFCB Fcb, uint64_t PMMPFDBE Pfdbe = MmGetPageFrameFromPFN(Pfn); - Pfdbe->FileCache._PrototypePte = (uint64_t) PrototypePte; - Pfdbe->FileCache._Fcb = (uint64_t) Fcb; + Pfdbe->FileCache._Fcb = (uintptr_t) Fcb; Pfdbe->FileCache._OffsetLower = Offset; Pfdbe->_OffsetUpper = (Offset >> 32); Pfdbe->IsFileCache = 1; diff --git a/drivers/test/source/ccbtst.c b/drivers/test/source/ccbtst.c index 976e4c43..7a56502e 100644 --- a/drivers/test/source/ccbtst.c +++ b/drivers/test/source/ccbtst.c @@ -16,6 +16,8 @@ Module name: #include #include "utils.h" +#if 0 + void PerformCcbTest() { LogMsg("Delaying half a second"); @@ -47,3 +49,5 @@ void PerformCcbTest() } + +#endif From 3aca4914f0c7961b6a6fb1714c912996d98d9e6b Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Mon, 20 Oct 2025 21:05:34 +0300 Subject: [PATCH 34/44] * Mm(i386): Well, we still have to make it build on 32-bit, don't we? --- boron/include/mm/pfn.h | 4 ++-- boron/source/build_number | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/boron/include/mm/pfn.h b/boron/include/mm/pfn.h index 0a66595f..26e8b300 100644 --- a/boron/include/mm/pfn.h +++ b/boron/include/mm/pfn.h @@ -114,8 +114,8 @@ MMPFDBE, *PMMPFDBE; #else -#define PFDBE_PrototypePte(Pfdbe) ((MMPFN*) ((Pfdbe)->FileCache._PrototypePte)) -#define PFDBE_Fcb(Pfdbe) ((PFCB) ((Pfdbe)->FileCache._Fcb)) +#define PFDBE_PrototypePte(Pfdbe) ((MM_PROTOTYPE_PTE_PTR) ((Pfdbe)->FileCache._PrototypePte)) +#define PFDBE_Fcb(Pfdbe) ((PFCB)((Pfdbe)->FileCache._Fcb)) #endif diff --git a/boron/source/build_number b/boron/source/build_number index 8410b8b8..5f3bb981 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -473 +474 From ad00b9b22f67407fc4198c1ee3b4f227264ae6f2 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Mon, 20 Oct 2025 23:31:33 +0300 Subject: [PATCH 35/44] * Mm: Fix a bug and a typo --- boron/source/build_number | 2 +- boron/source/mm/cache.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/boron/source/build_number b/boron/source/build_number index 5f3bb981..7573eff8 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -474 +475 diff --git a/boron/source/mm/cache.c b/boron/source/mm/cache.c index 5b322bee..f8c3096e 100644 --- a/boron/source/mm/cache.c +++ b/boron/source/mm/cache.c @@ -185,7 +185,7 @@ static MMPFN MiWalkCacheUntilIndirection(PCCB Ccb, uint64_t* PageOffset, bool Al // Level 2 *PageOffset -= MM_DIRECT_PAGE_COUNT; - if (*PageOffset < MM_INDIRECTION_COUNT * MM_INDIRECTION_COUNT) + if (*PageOffset < MM_INDIRECTION_COUNT) { if (!MmpEnsureIndirectionExists(&Ccb->Level2Indirect, AllocateIndirection)) return Failure; @@ -240,7 +240,7 @@ static MMPFN MiWalkCacheUntilIndirection(PCCB Ccb, uint64_t* PageOffset, bool Al return Failure; MMPFN Pfn5 = MiNextEntryCache(Ccb->Level5Indirect, PageOffset, AllocateIndirection); - if (IS_BAD_PFN(Pfn4)) + if (IS_BAD_PFN(Pfn5)) return Pfn4; MMPFN Pfn4 = MiNextEntryCache(Pfn5, PageOffset, AllocateIndirection); From 1fdb87c6f15d39e1dec65c7299435999412c3198 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Tue, 21 Oct 2025 09:19:08 +0300 Subject: [PATCH 36/44] * Hal(i386): Fix timer going backwards temporarily because interrupts are being missed. * Hal: Fix a typo in HalPicRegisterInterrupt * I8042prt: Get working * Ke: Fix a bug where the "more than 1 CPU" message is printed if you have one CPU --- boron/source/build_number | 2 +- boron/source/hal/hal.c | 29 +++++++++++++++++++++++++++-- boron/source/ke/smp.c | 4 ++-- drivers/hali386/source/pic.c | 1 + drivers/hali386/source/timer.c | 22 +++++++++++++++++++--- drivers/test/source/main.c | 4 ++-- limine.i386.cfg | 4 ++-- 7 files changed, 54 insertions(+), 12 deletions(-) diff --git a/boron/source/build_number b/boron/source/build_number index 7573eff8..1448fd85 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -475 +483 diff --git a/boron/source/hal/hal.c b/boron/source/hal/hal.c index d41f9803..32dbf944 100644 --- a/boron/source/hal/hal.c +++ b/boron/source/hal/hal.c @@ -15,6 +15,10 @@ Module name: #include #include +#if defined DEBUG && !defined CONFIG_SMP +#define TICK_DEBUG +#endif + HAL_VFTABLE HalpVftable; bool HalWasInitted() @@ -84,7 +88,28 @@ uint64_t HalGetIntTimerFrequency() uint64_t HalGetTickCount() { - return HalpVftable.GetTickCount(); +#ifdef TICK_DEBUG + static uint64_t LastTickCount = 0; +#endif + + uint64_t TickCount = HalpVftable.GetTickCount(); + +#ifdef TICK_DEBUG + bool Restore = KeDisableInterrupts(); + if (LastTickCount > TickCount) + { + KeCrash( + "ERROR: LastTickCount: %lld, TickCount: %lld. Timer went backwards?", + LastTickCount, + TickCount + ); + } + + LastTickCount = TickCount; + KeRestoreInterrupts(Restore); +#endif + + return TickCount; } uint64_t HalGetTickFrequency() @@ -110,7 +135,7 @@ void HalIoApicSetIrqRedirect(uint8_t Vector, uint8_t Irq, uint32_t LapicId, bool void HalPicRegisterInterrupt(uint8_t Vector, KIPL Ipl) { - HalpVftable.PicDeregisterInterrupt(Vector, Ipl); + HalpVftable.PicRegisterInterrupt(Vector, Ipl); } void HalPicDeregisterInterrupt(uint8_t Vector, KIPL Ipl) diff --git a/boron/source/ke/smp.c b/boron/source/ke/smp.c index a56bcb8c..49389a0c 100644 --- a/boron/source/ke/smp.c +++ b/boron/source/ke/smp.c @@ -196,9 +196,9 @@ void KeInitSMP() KeProcessorList = KiPrcbList; KeProcessorCount = 1; - if (MpInfo->Count >= 1) + if (MpInfo->Count > 1) { - DbgPrint("%zu processors provided by the loader, but we only support one"); + DbgPrint("%zu processors provided by the loader, but we only support one", MpInfo->Count); MpInfo->Count = 1; } #endif diff --git a/drivers/hali386/source/pic.c b/drivers/hali386/source/pic.c index 6c28f751..82014166 100644 --- a/drivers/hali386/source/pic.c +++ b/drivers/hali386/source/pic.c @@ -55,6 +55,7 @@ HAL_API void HalEndOfInterrupt(int InterruptNumber) HAL_API void HalPicRegisterInterrupt(uint8_t Vector, KIPL Ipl) { + DbgPrint("HalPicRegisterInterrupt: %d, %d", Vector, Ipl); if (Vector < PIC_INTERRUPT_BASE || Vector >= PIC_INTERRUPT_BASE + 16) { DbgPrint("HalRegisterInterrupt: Dropping vector %zu", Vector); diff --git a/drivers/hali386/source/timer.c b/drivers/hali386/source/timer.c index 9c545e95..8a1b5376 100644 --- a/drivers/hali386/source/timer.c +++ b/drivers/hali386/source/timer.c @@ -17,7 +17,7 @@ Module name: // TODO: Decide on a good tick rate. For slower systems, increased // time slice time would be better. -#define TIMER_RELOAD_VALUE 4000 +#define TIMER_RELOAD_VALUE 15000 // This value is incremented by TIMER_RELOAD_VALUE on every timer interrupt. static uint64_t PitTicksPassed; @@ -33,13 +33,29 @@ HAL_API uint64_t HalGetTickCount() if (!PitInitialized) return 0; - // The PIT decrements from TIMER_RELOAD_VALUE + KePortWriteByte(PIT_COMMAND_PORT, 0x00); // latch + + bool Restore = KeDisableInterrupts(); + static uint64_t LastValue = 0; + uint8_t Low, High; Low = KePortReadByte(PIT_CHANNEL_0_PORT); High = KePortReadByte(PIT_CHANNEL_0_PORT); - uint16_t Timer = TIMER_RELOAD_VALUE - (Low | (High << 8)); + uint16_t ReadIn = (Low | (High << 8)); + uint16_t Timer = TIMER_RELOAD_VALUE - ReadIn; uint64_t Value = PitTicksPassed + Timer; + + if (Value < LastValue) + { + // Seems like there's been an extended period of time where interrupts + // were disabled? Not sure. But what I know for sure is that timers + // can't go backwards. (unless they overflow) + Value += TIMER_RELOAD_VALUE; + } + + LastValue = Value; + KeRestoreInterrupts(Restore); return Value; } diff --git a/drivers/test/source/main.c b/drivers/test/source/main.c index 92e7cbee..afaf27df 100644 --- a/drivers/test/source/main.c +++ b/drivers/test/source/main.c @@ -85,14 +85,14 @@ NO_RETURN void DriverTestThread(UNUSED void* Parameter) //PerformProcessTest(); //PerformMutexTest(); //PerformBallTest(); - PerformFireworksTest(); + //PerformFireworksTest(); //PerformHandleTest(); //PerformApcTest(); //PerformRwlockTest(); //PerformObjectTest(); //PerformMdlTest(); //PerformIntTest(); - //PerformKeyboardTest(); + PerformKeyboardTest(); //PerformStorageTest(); //PerformExObTest(); //PerformCcbTest(); diff --git a/limine.i386.cfg b/limine.i386.cfg index cf23afef..9d2965f6 100644 --- a/limine.i386.cfg +++ b/limine.i386.cfg @@ -12,8 +12,8 @@ VERBOSE=yes MODULE_PATH=boot:///framebuf.sys MODULE_STRING=framebuf.sys - #MODULE_PATH=boot:///i8042prt.sys - #MODULE_STRING=i8042prt.sys + MODULE_PATH=boot:///i8042prt.sys + MODULE_STRING=i8042prt.sys #MODULE_PATH=boot:///stornvme.sys #MODULE_STRING=stornvme.sys From b41210b956eed55ff69041871a50c8e4f18f11ba Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Tue, 21 Oct 2025 10:29:45 +0300 Subject: [PATCH 37/44] * StorNvme: Make the initial semaphore wait alertable. --- boron/source/build_number | 2 +- drivers/stornvme/source/commands.c | 31 ++++++++++++++++-------------- drivers/stornvme/source/nvme.h | 2 +- drivers/stornvme/source/queue.c | 18 ++++++++++++++--- 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/boron/source/build_number b/boron/source/build_number index 1448fd85..f52aaac0 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -483 +484 diff --git a/drivers/stornvme/source/commands.c b/drivers/stornvme/source/commands.c index d1056726..2f0057b4 100644 --- a/drivers/stornvme/source/commands.c +++ b/drivers/stornvme/source/commands.c @@ -16,15 +16,20 @@ Module name: #include // NOTE: Here, EntryPair's Event field will be replaced with an address that'll be stale on exit. -BSTATUS NvmeSendAndWait(PQUEUE_CONTROL_BLOCK Qcb, PQUEUE_ENTRY_PAIR EntryPair) +// +// TODO: Make this event's wait cancelable. +BSTATUS NvmeSendAndWait(PQUEUE_CONTROL_BLOCK Qcb, PQUEUE_ENTRY_PAIR EntryPair, bool Alertable) { + BSTATUS Status; KEVENT Event; KeInitializeEvent(&Event, EVENT_NOTIFICATION, false); EntryPair->Event = &Event; - NvmeSend(Qcb, EntryPair); + Status = NvmeSend(Qcb, EntryPair, Alertable); + if (FAILED(Status)) + return Status; - BSTATUS Status = KeWaitForSingleObject(&Event, false, TIMEOUT_INFINITE, MODE_KERNEL); + Status = KeWaitForSingleObject(&Event, false, TIMEOUT_INFINITE, MODE_KERNEL); if (FAILED(Status)) return Status; @@ -55,11 +60,9 @@ BSTATUS NvmeIdentify(PCONTROLLER_EXTENSION ContExtension, void* IdentifyBuffer, QueueEntry.Sub.Dword10.Identify.Cns = Cns; - BSTATUS Status = NvmeSendAndWait(&ContExtension->AdminQueue, &QueueEntry); + BSTATUS Status = NvmeSendAndWait(&ContExtension->AdminQueue, &QueueEntry, false); if (!FAILED(Status)) - { memcpy(IdentifyBuffer, MmGetHHDMOffsetAddr(MmPFNToPhysPage(Page)), PAGE_SIZE); - } MmFreePhysicalPage(Page); return Status; @@ -76,7 +79,7 @@ BSTATUS NvmeSetFeature(PCONTROLLER_EXTENSION ContExtension, int FeatureIdentifie QueueEntry.Sub.DataPointer[0] = DataPointer; QueueEntry.Sub.Dword10.SetFeatures.FeatureIdentifier = FeatureIdentifier; - return NvmeSendAndWait(&ContExtension->AdminQueue, &QueueEntry); + return NvmeSendAndWait(&ContExtension->AdminQueue, &QueueEntry, false); } BSTATUS NvmeAllocateIoQueues(PCONTROLLER_EXTENSION ContExtension, size_t QueueCount, size_t* OutQueueCount) @@ -93,7 +96,7 @@ BSTATUS NvmeAllocateIoQueues(PCONTROLLER_EXTENSION ContExtension, size_t QueueCo QueueEntry.Sub.Dword11.SetFeatures.SubQueueCount = QueueCount; QueueEntry.Sub.Dword11.SetFeatures.ComQueueCount = QueueCount; - BSTATUS Status = NvmeSendAndWait(&ContExtension->AdminQueue, &QueueEntry); + BSTATUS Status = NvmeSendAndWait(&ContExtension->AdminQueue, &QueueEntry, false); if (FAILED(Status)) return Status; @@ -142,7 +145,7 @@ BSTATUS NvmeInitializeIoQueue(PCONTROLLER_EXTENSION ContExtension, PQUEUE_CONTRO QueueEntry.Sub.Dword11.CreateIoCompQueue.PhysicallyContiguous = 1; QueueEntry.Sub.Dword11.CreateIoCompQueue.InterruptVector = Id; - BSTATUS Status = NvmeSendAndWait(&ContExtension->AdminQueue, &QueueEntry); + BSTATUS Status = NvmeSendAndWait(&ContExtension->AdminQueue, &QueueEntry, false); if (FAILED(Status)) { DbgPrint("StorNvme: failed to create I/O completion queue %zu: status %d", Id, Status); @@ -162,7 +165,7 @@ BSTATUS NvmeInitializeIoQueue(PCONTROLLER_EXTENSION ContExtension, PQUEUE_CONTRO QueueEntry.Sub.Dword11.CreateIoSubQueue.CompletionQueueId = Id; QueueEntry.Sub.Dword11.CreateIoSubQueue.PhysicallyContiguous = 1; - Status = NvmeSendAndWait(&ContExtension->AdminQueue, &QueueEntry); + Status = NvmeSendAndWait(&ContExtension->AdminQueue, &QueueEntry, false); if (FAILED(Status)) { DbgPrint("StorNvme: failed to create I/O submission queue %zu: status %d", Id, Status); @@ -193,11 +196,11 @@ BSTATUS NvmeSendRead(PDEVICE_EXTENSION DeviceExtension, uint64_t Prp[2], uint64_ if (Wait) { - return NvmeSendAndWait(NvmeChooseIoQueue(ContExtension), QueueEntryPtr); + return NvmeSendAndWait(NvmeChooseIoQueue(ContExtension), QueueEntryPtr, true); } else { - NvmeSend(NvmeChooseIoQueue(ContExtension), QueueEntryPtr); + NvmeSend(NvmeChooseIoQueue(ContExtension), QueueEntryPtr, true); return STATUS_SUCCESS; } } @@ -217,11 +220,11 @@ BSTATUS NvmeSendWrite(PDEVICE_EXTENSION DeviceExtension, uint64_t Prp[2], uint64 if (Wait) { - return NvmeSendAndWait(NvmeChooseIoQueue(ContExtension), QueueEntryPtr); + return NvmeSendAndWait(NvmeChooseIoQueue(ContExtension), QueueEntryPtr, true); } else { - NvmeSend(NvmeChooseIoQueue(ContExtension), QueueEntryPtr); + NvmeSend(NvmeChooseIoQueue(ContExtension), QueueEntryPtr, true); return STATUS_SUCCESS; } } diff --git a/drivers/stornvme/source/nvme.h b/drivers/stornvme/source/nvme.h index 08c7f7ee..7d5aad83 100644 --- a/drivers/stornvme/source/nvme.h +++ b/drivers/stornvme/source/nvme.h @@ -506,7 +506,7 @@ extern IO_DISPATCH_TABLE NvmeDispatchTable; // // After sending, one may wait on the event passed into EntryPair. Once the operation is // complete, the event will be set. -void NvmeSend(PQUEUE_CONTROL_BLOCK Qcb, PQUEUE_ENTRY_PAIR EntryPair); +BSTATUS NvmeSend(PQUEUE_CONTROL_BLOCK Qcb, PQUEUE_ENTRY_PAIR EntryPair, bool Alertable); // NOTE: Ownership of the SubmissionQueuePhysical and CompletionQueuePhysical pages is transferred to the queue. void NvmeSetupQueue( diff --git a/drivers/stornvme/source/queue.c b/drivers/stornvme/source/queue.c index 00b4d6e3..0edf65f0 100644 --- a/drivers/stornvme/source/queue.c +++ b/drivers/stornvme/source/queue.c @@ -91,16 +91,27 @@ static void NvmeCreateInterruptForQueue(PQUEUE_CONTROL_BLOCK Qcb, int MsixIndex) HalPciMsixSetFunctionMask(Device, false); } -// Send a command to a queue and wait for it to finish. +// Send a command to a queue. // // NOTE: The PKEVENT Event field of EntryPair must point to a valid event. -void NvmeSend(PQUEUE_CONTROL_BLOCK Qcb, PQUEUE_ENTRY_PAIR EntryPair) +// The event will be signalled once the command finishes. +BSTATUS NvmeSend(PQUEUE_CONTROL_BLOCK Qcb, PQUEUE_ENTRY_PAIR EntryPair, bool Alertable) { // Wait on the semaphore to ensure there's space for our request. // // The semaphore will get released by the DPC routine when an operation // has completed. - KeWaitForSingleObject(&Qcb->Semaphore, false, TIMEOUT_INFINITE, MODE_KERNEL); + BSTATUS Status; + Status = KeWaitForSingleObject( + &Qcb->Semaphore, + Alertable, + TIMEOUT_INFINITE, + Alertable ? KeGetPreviousMode() : MODE_KERNEL + ); + + if (FAILED(Status)) + // Thread was alerted, abort this request. + return Status; KIPL Ipl; KeAcquireSpinLock(&Qcb->SpinLock, &Ipl); @@ -126,6 +137,7 @@ void NvmeSend(PQUEUE_CONTROL_BLOCK Qcb, PQUEUE_ENTRY_PAIR EntryPair) *Qcb->SubmissionQueue.DoorBell = Qcb->SubmissionQueue.Index; KeReleaseSpinLock(&Qcb->SpinLock, Ipl); + return Status; } void NvmeSetupQueue( From 88bbe37b0c9ee65b3e0f40d8b1d057e760ab3dd5 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Tue, 21 Oct 2025 22:14:50 +0300 Subject: [PATCH 38/44] * Rtl: Add validity checks for ELF loading * User: Actually make sure to compile with the correct toolchain --- boron/source/build_number | 2 +- boron/source/ps/initproc.c | 4 +++ boron/source/rtl/elf.c | 49 +++++++++++++++++++++++++++++++ common/include/elf.h | 28 ++++++++++++++++++ common/include/rtl/elf.h | 2 ++ limine.i386.cfg | 6 +++- user/CommonMakefile | 57 +++++++++++------------------------- user/libboron/source/calls.S | 22 ++++++++++++++ 8 files changed, 128 insertions(+), 42 deletions(-) diff --git a/boron/source/build_number b/boron/source/build_number index f52aaac0..73e7a666 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -484 +489 diff --git a/boron/source/ps/initproc.c b/boron/source/ps/initproc.c index 9b6a9f96..56e07e66 100644 --- a/boron/source/ps/initproc.c +++ b/boron/source/ps/initproc.c @@ -155,6 +155,10 @@ void PsStartInitialProcess(UNUSED void* ContextUnused) FirstAddr = (uintptr_t) ~0ULL; LargestAddr = 0; + Status = RtlCheckValidity(&ElfHeader); + if (FAILED(Status)) + KeCrash("%s: libboron.so has an invalid header: %s (%d)", Func, RtlGetStatusString(Status), Status); + // Find the dynamic program header; PELF_PROGRAM_HEADER DynamicPhdr = PspLdrFindDynamicPhdr( ProgramHeaders, diff --git a/boron/source/rtl/elf.c b/boron/source/rtl/elf.c index 1b17db2a..51c50089 100644 --- a/boron/source/rtl/elf.c +++ b/boron/source/rtl/elf.c @@ -421,6 +421,55 @@ void RtlRelocateRelrEntries(PELF_DYNAMIC_INFO DynInfo, uintptr_t ImageBase) } } +BSTATUS RtlCheckValidity(PELF_HEADER Header) +{ + if (memcmp(Header->Identifier, "\x7F" "ELF", 4) != 0) + { + DbgPrint("Rtl: Elf has invalid header."); + return STATUS_INVALID_EXECUTABLE; + } + +#ifdef IS_64_BIT + if (Header->Identifier[ELF_IDENT_CLASS] != ELF_MCLASS_64BIT) +#else + if (Header->Identifier[ELF_IDENT_CLASS] != ELF_MCLASS_32BIT) +#endif + { + DbgPrint("Rtl: Elf isn't %d bit", sizeof(uintptr_t) * 8); + return STATUS_INVALID_ARCHITECTURE; + } + + // N.B. we don't support big endian, and probably will never + if (Header->Identifier[ELF_IDENT_DATA] != ELF_MDATA_LSB) + { + DbgPrint("Rtl: Elf is opposite endianness"); + return STATUS_INVALID_ARCHITECTURE; + } + +#if defined TARGET_AMD64 + const int Arch = ELF_ARCH_AMD64; +#elif defined TARGET_I386 + const int Arch = ELF_ARCH_386; +#endif + + if (Header->Machine != Arch) + return STATUS_INVALID_ARCHITECTURE; + + if (Header->Type != ELF_TYPE_EXECUTABLE && Header->Type != ELF_TYPE_DYNAMIC) + { + DbgPrint("Rtl: Elf is type %d which we don't know about"); + return STATUS_INVALID_EXECUTABLE; + } + + if (Header->ProgramHeaderCount == 0) + { + DbgPrint("Rtl: Elf has no program headers, probably invalid for our purposes"); + return STATUS_INVALID_EXECUTABLE; + } + + return STATUS_SUCCESS; +} + uint32_t RtlElfHash(const char* Name) { const uint8_t* NameU = (const uint8_t*) Name; diff --git a/common/include/elf.h b/common/include/elf.h index bf4c2b6c..0da67348 100644 --- a/common/include/elf.h +++ b/common/include/elf.h @@ -32,6 +32,34 @@ enum ELF_IDENT_ABI_VERSION, }; +enum +{ + ELF_MCLASS_NONE, + ELF_MCLASS_32BIT, + ELF_MCLASS_64BIT, +}; + +enum +{ + ELF_MDATA_NONE, + ELF_MDATA_LSB, + ELF_MDATA_MSB, +}; + +enum +{ + ELF_ARCH_SPARC = 2, + ELF_ARCH_386 = 3, + ELF_ARCH_M68K = 4, + ELF_ARCH_MIPS = 8, + ELF_ARCH_MIPS_RS3_LE = 10, + ELF_ARCH_PPC = 20, + ELF_ARCH_PPC64 = 21, + ELF_ARCH_ARM = 40, + ELF_ARCH_IA64 = 50, + ELF_ARCH_AMD64 = 62, +}; + enum { ELF_TYPE_NONE, diff --git a/common/include/rtl/elf.h b/common/include/rtl/elf.h index 8aafc086..51870901 100644 --- a/common/include/rtl/elf.h +++ b/common/include/rtl/elf.h @@ -55,6 +55,8 @@ void RtlRelocateRelrEntries(PELF_DYNAMIC_INFO DynInfo, uintptr_t LoadBase); uint32_t RtlElfHash(const char* Name); +BSTATUS RtlCheckValidity(PELF_HEADER Header); + #ifdef __cplusplus } #endif diff --git a/limine.i386.cfg b/limine.i386.cfg index 9d2965f6..441c2176 100644 --- a/limine.i386.cfg +++ b/limine.i386.cfg @@ -24,7 +24,11 @@ VERBOSE=yes MODULE_PATH=boot:///test.sys MODULE_STRING=test.sys - #MODULE_PATH=boot:///libboron.so + MODULE_PATH=boot:///libboron.so + MODULE_STRING=libboron.so #MODULE_PATH=boot:///libtest.so + #MODULE_STRING=libtest.so #MODULE_PATH=boot:///init.exe + #MODULE_STRING=init.exe #MODULE_PATH=boot:///test.exe + #MODULE_STRING=test.exe diff --git a/user/CommonMakefile b/user/CommonMakefile index bfc0fcef..caa280af 100644 --- a/user/CommonMakefile +++ b/user/CommonMakefile @@ -57,14 +57,7 @@ ifeq ($(DEBUG2), yes) DEFINES += -DDEBUG2 endif -# It is highly recommended to use a custom built cross toolchain to build a driver. -# We are only using "cc" as a placeholder here. It may work by using -# the host system's toolchain, but this is not guaranteed. -$(eval $(call DEFAULT_VAR,CC,cc)) -$(eval $(call DEFAULT_VAR,CXX,c++)) - -# Same thing for "ld" (the linker). -$(eval $(call DEFAULT_VAR,LD,ld)) +include ../../tools/toolchain.mk # User controllable CFLAGS. CFLAGS ?= @@ -86,7 +79,7 @@ NASMFLAGS += -F dwarf -I$(SRC_DIR) -I$(INC_DIR) -I $(SDK_DIR) -I $(DDK_DIR) LDFLAGS ?= # Internal C flags that should not be changed by the user. -override CFLAGS += \ +CFLAGS += \ -fno-omit-frame-pointer \ -std=c11 \ -ffreestanding \ @@ -94,53 +87,37 @@ override CFLAGS += \ -fno-stack-check \ -fno-lto \ -fPIC \ - -m64 \ - -march=x86-64 \ - -mabi=sysv \ - -mno-80387 \ - -mno-mmx \ - -mno-sse \ - -mno-sse2 \ - -mno-red-zone \ -MMD \ -MP \ - -I. + -I. \ + $(ARCH_CFLAGS) # Internal C++ flags that should not be changed by the user. -override CXXFLAGS += \ +CXXFLAGS += \ -fno-omit-frame-pointer \ -std=c++17 \ -ffreestanding \ -fno-stack-protector \ -fno-stack-check \ -fno-lto \ - -m64 \ - -march=x86-64 \ - -mabi=sysv \ - -mno-80387 \ - -mno-mmx \ - -mno-sse \ - -mno-sse2 \ - -mno-red-zone \ - -MMD \ - -MP \ -fno-exceptions \ -fno-rtti \ - -I. + -I. \ + $(ARCH_CFLAGS) -override LDFLAGSBASE += \ +LDFLAGSBASE += \ + --dynamic-linker libboron.so \ -nostdlib \ - -m elf_x86_64 \ -z max-page-size=0x1000 \ - --dynamic-linker libboron.so + -m $(LINK_ARCH) \ + $(ARCH_LDFLAGS) # Internal linker flags that should not be changed by the user. LDFLAGS += \ $(LDFLAGSBASE) # Internal nasm flags that should not be changed by the user. -NASMFLAGS += \ - -f elf64 +NASMFLAGS += $(ARCH_ASFLAGS) ifeq ($(IS_LIBRARY),yes) LDFLAGS += \ @@ -170,7 +147,7 @@ all: application # Link rules for the final driver executable. $(TARGET_FILE): $(OBJ) @echo "[LD]\tBuilding $(TARGET_FILE)" - @$(LD) $(OBJ) $(LDFLAGS) -o $@ + @$(BLD) $(OBJ) $(LDFLAGS) -o $@ # Include header dependencies. -include $(HEADER_DEPS) @@ -179,25 +156,25 @@ $(TARGET_FILE): $(OBJ) $(BUILD_DIR)/%.$(TARGETL).o: $(SRC_DIR)/%.c @echo "[CC]\tCompiling $<" @mkdir -p $(dir $@) - @$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ + @$(BCC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ # Compilation rules for *.cpp files. $(BUILD_DIR)/%.$(TARGETL).o: $(SRC_DIR)/%.cpp @echo "[CXX]\tCompiling $<" @mkdir -p $(dir $@) - @$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@ + @$(BCXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@ # Compilation rules for *.S files. $(BUILD_DIR)/%.$(TARGETL).o: $(SRC_DIR)/%.S @echo "[AS]\tCompiling $<" @mkdir -p $(dir $@) - @$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ + @$(BCC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ # Compilation rules for *.asm (nasm) files. $(BUILD_DIR)/%.$(TARGETL).o: $(SRC_DIR)/%.asm @echo "[AS]\tCompiling $<" @mkdir -p $(dir $@) - @nasm $(NASMFLAGS) $< -o $@ + @$(BASM) $(NASMFLAGS) $< -o $@ # Remove object files and the final executable. .PHONY: clean diff --git a/user/libboron/source/calls.S b/user/libboron/source/calls.S index 438509c6..c181d14e 100644 --- a/user/libboron/source/calls.S +++ b/user/libboron/source/calls.S @@ -4,6 +4,8 @@ .section .text .att_syntax +#ifdef TARGET_AMD64 + .macro CALL number, name .globl \name .type \name, @function @@ -54,6 +56,26 @@ ret .endm +#elif defined TARGET_I386 + +.macro CALL number, name + .globl \name + .type \name, @function +\name: + // TODO + ret +.endmacro + +.macro CALLEX number, name + .globl \name + .type \name, @function +\name: + // TODO + ret +.endmacro + +#endif + CALL 0, OSAllocateVirtualMemory CALL 1, OSClose CALL 2, OSCreateEvent From 1178f4a9305e41246fe01554c43bd99b2ad734e3 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Wed, 22 Oct 2025 15:47:09 +0300 Subject: [PATCH 39/44] * X86: First run of libboron + first syscall! --- boron/include/arch/i386.h | 1 + boron/include/ex/object.h | 4 +- boron/include/ke.h | 1 + boron/include/ke/services.h | 22 ++++ boron/include/mm/services.h | 13 +++ boron/source/build_number | 2 +- boron/source/ke/amd64/syscall.asm | 2 + boron/source/ke/i386/cpu.c | 15 +-- boron/source/ke/i386/syscall.c | 174 ++++++++++++++++++++++++++++++ boron/source/ke/i386/traps.c | 7 +- boron/source/ke/sched.c | 13 ++- boron/source/ps/initproc.c | 1 + limine.i386.cfg | 2 +- user/include/svcs.h | 10 ++ user/libboron/source/calls.S | 143 ++++++++++-------------- user/libboron/source/calltbl.h | 58 ++++++++++ user/libboron/source/init.c | 1 + 17 files changed, 361 insertions(+), 108 deletions(-) create mode 100644 boron/include/ke/services.h create mode 100644 boron/source/ke/i386/syscall.c create mode 100644 user/libboron/source/calltbl.h diff --git a/boron/include/arch/i386.h b/boron/include/arch/i386.h index 46f4d9da..c173ca85 100644 --- a/boron/include/arch/i386.h +++ b/boron/include/arch/i386.h @@ -180,6 +180,7 @@ struct KREGISTERS_tag #define INTV_DBL_FAULT (0x08) #define INTV_PROT_FAULT (0x0D) #define INTV_PAGE_FAULT (0x0E) +#define INTV_SYSTEMCALL (0x80) typedef struct KIDT_ENTRY_tag { diff --git a/boron/include/ex/object.h b/boron/include/ex/object.h index ba504cfd..cae9109b 100644 --- a/boron/include/ex/object.h +++ b/boron/include/ex/object.h @@ -84,4 +84,6 @@ BSTATUS OSWaitForSingleObject( HANDLE Handle, bool Alertable, int TimeoutMS -); \ No newline at end of file +); + +BSTATUS OSDuplicateHandle(HANDLE SourceHandle, HANDLE DestinationProcessHandle, PHANDLE OutNewHandle, int OpenFlags); \ No newline at end of file diff --git a/boron/include/ke.h b/boron/include/ke.h index 1e081b0f..226dff7f 100644 --- a/boron/include/ke.h +++ b/boron/include/ke.h @@ -33,3 +33,4 @@ Module name: #include #include #include +#include diff --git a/boron/include/ke/services.h b/boron/include/ke/services.h new file mode 100644 index 00000000..ac12f3c7 --- /dev/null +++ b/boron/include/ke/services.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +BSTATUS OSSetPebProcess(HANDLE ProcessHandle, void* PebPtr); + +BSTATUS OSSetCurrentPeb(void* Ptr); + +BSTATUS OSSetCurrentTeb(void* Ptr); + +void* OSGetCurrentPeb(); + +void* OSGetCurrentTeb(); + +BSTATUS OSGetTickCount(uint64_t* TickCount); + +BSTATUS OSGetTickFrequency(uint64_t* TickFrequency); + +BSTATUS OSGetVersionNumber(int* VersionNumber); + +BSTATUS OSOutputDebugString(const char* String, size_t StringLength); diff --git a/boron/include/mm/services.h b/boron/include/mm/services.h index 3b04e3ed..870694cb 100644 --- a/boron/include/mm/services.h +++ b/boron/include/mm/services.h @@ -40,3 +40,16 @@ BSTATUS OSMapViewOfObject( uint64_t SectionOffset, int Protection ); + +BSTATUS OSWriteVirtualMemory( + HANDLE ProcessHandle, + void* TargetAddress, + const void* Source, + size_t ByteCount +); + +BSTATUS OSGetMappedFileHandle( + PHANDLE OutHandle, + HANDLE ProcessHandle, + uintptr_t Address +); diff --git a/boron/source/build_number b/boron/source/build_number index 73e7a666..1303c35c 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -489 +497 diff --git a/boron/source/ke/amd64/syscall.asm b/boron/source/ke/amd64/syscall.asm index cee2c27b..07ba1941 100644 --- a/boron/source/ke/amd64/syscall.asm +++ b/boron/source/ke/amd64/syscall.asm @@ -132,6 +132,8 @@ KiSystemServiceTableEnd: ; R12 - Argument 7 ; R13 - Argument 8 ; R14 - Argument 9 +; +; RBX, RCX, and R15 are used by this handler and can't be provided. extern KiCheckTerminatedUserMode extern KiSystemServices diff --git a/boron/source/ke/i386/cpu.c b/boron/source/ke/i386/cpu.c index 74befe17..122302bf 100644 --- a/boron/source/ke/i386/cpu.c +++ b/boron/source/ke/i386/cpu.c @@ -140,8 +140,8 @@ static void KepSetupGdt(KARCH_DATA* Data) INIT static void KepSetupTss(KTSS* Tss) { - // we'll set it up later.. memset(Tss, 0, sizeof * Tss); + Tss->Ss0 = SEG_RING_0_DATA; } extern void KiSystemServiceHandler(); @@ -184,23 +184,12 @@ void KeInitCPU() */ } -/* extern uintptr_t KiSystemServiceTable[]; -extern uintptr_t KiSystemServiceTableEnd[]; // If system call tracing is enabled, this shows all of the system calls happening. void KePrintSystemServiceDebug(size_t Syscall) { // Format: "[ThreadPointer] - Syscall [Number] ([FunctionName])" - size_t Size = KiSystemServiceTableEnd - KiSystemServiceTable; - - const char* FunctionName; - - if (Syscall >= Size) - FunctionName = "INVALID"; - else - FunctionName = DbgLookUpRoutineNameByAddressExact(KiSystemServiceTable[Syscall]); - + const char* FunctionName = DbgLookUpRoutineNameByAddressExact(KiSystemServiceTable[Syscall]); DbgPrint("SYSCALL: %p - %d %s", KeGetCurrentThread(), (int) Syscall, FunctionName); } -*/ diff --git a/boron/source/ke/i386/syscall.c b/boron/source/ke/i386/syscall.c new file mode 100644 index 00000000..606c3281 --- /dev/null +++ b/boron/source/ke/i386/syscall.c @@ -0,0 +1,174 @@ + +#include "../ki.h" + +#include +#include +#include +#include +#include + +typedef struct +{ + uint64_t SectionOffset; + int Protection; +} +MAP_VIEW_OF_OBJECT_PARAMS, *PMAP_VIEW_OF_OBJECT_PARAMS; + +typedef struct +{ + uint32_t Flags; +} +READ_FILE_PARAMS, *PREAD_FILE_PARAMS; + +typedef struct +{ + uint32_t Flags; + uint64_t* OutSize; +} +WRITE_FILE_PARAMS, *PWRITE_FILE_PARAMS; + +BSTATUS OSMapViewOfObject_Call( + HANDLE ProcessHandle, + HANDLE MappedObject, + void** BaseAddressInOut, + size_t ViewSize, + int AllocationType, + PMAP_VIEW_OF_OBJECT_PARAMS ExtraParamsPtr +) +{ + BSTATUS Status; + MAP_VIEW_OF_OBJECT_PARAMS ExtraParams; + + Status = MmSafeCopy(&ExtraParams, ExtraParamsPtr, sizeof(MAP_VIEW_OF_OBJECT_PARAMS), MODE_USER, false); + if (FAILED(Status)) + return Status; + + return OSMapViewOfObject( + ProcessHandle, + MappedObject, + BaseAddressInOut, + ViewSize, + AllocationType, + ExtraParams.SectionOffset, + ExtraParams.Protection + ); +} + +BSTATUS OSReadFile_Call( + PIO_STATUS_BLOCK Iosb, + HANDLE Handle, + uint64_t ByteOffset, // 2 parameters + void* Buffer, + size_t Length, + PREAD_FILE_PARAMS ExtraParamsPtr +) +{ + BSTATUS Status; + READ_FILE_PARAMS ExtraParams; + + Status = MmSafeCopy(&ExtraParams, ExtraParamsPtr, sizeof(READ_FILE_PARAMS), MODE_USER, false); + if (FAILED(Status)) + return Status; + + return OSReadFile(Iosb, Handle, ByteOffset, Buffer, Length, ExtraParams.Flags); +} + +BSTATUS OSWriteFile_Call( + PIO_STATUS_BLOCK Iosb, + HANDLE Handle, + uint64_t ByteOffset, // 2 parameters + const void* Buffer, + size_t Length, + PWRITE_FILE_PARAMS ExtraParamsPtr +) +{ + BSTATUS Status; + WRITE_FILE_PARAMS ExtraParams; + + Status = MmSafeCopy(&ExtraParams, ExtraParamsPtr, sizeof(WRITE_FILE_PARAMS), MODE_USER, false); + if (FAILED(Status)) + return Status; + + return OSWriteFile(Iosb, Handle, ByteOffset, Buffer, Length, ExtraParams.Flags, ExtraParams.OutSize); +} + +// N.B. extra arguments will be ignored by the called function. +typedef int(*KI_SYSCALL_HANDLER)(uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t); + +const void* const KiSystemServiceTable[] = +{ + OSAllocateVirtualMemory, + OSClose, + OSCreateEvent, + OSCreateMutex, + OSCreatePipe, + OSCreateProcess, + OSCreateThread, + OSDeviceIoControl, + OSDuplicateHandle, + OSExitProcess, + OSExitThread, + OSFreeVirtualMemory, + OSGetAlignmentFile, + OSGetCurrentPeb, + OSGetCurrentTeb, + OSGetExitCodeProcess, + OSGetLengthFile, + OSGetMappedFileHandle, + OSGetTickCount, + OSGetTickFrequency, + OSGetVersionNumber, + OSMapViewOfObject_Call, + OSOpenEvent, + OSOpenFile, + OSOpenMutex, + OSOutputDebugString, + OSPulseEvent, + OSQueryEvent, + OSQueryMutex, + OSReadDirectoryEntries, + OSReadFile_Call, + OSReleaseMutex, + OSResetDirectoryReadHead, + OSResetEvent, + OSSetCurrentPeb, + OSSetCurrentTeb, + OSSetEvent, + OSSetExitCode, + OSSetPebProcess, + OSSetSuspendedThread, + OSSleep, + OSTerminateThread, + OSTouchFile, + OSWaitForMultipleObjects, + OSWaitForSingleObject, + OSWriteFile_Call, + OSWriteVirtualMemory, +}; + +#define KI_SYSCALL_COUNT ARRAY_COUNT(KiSystemServiceTable) + +extern void KePrintSystemServiceDebug(size_t Call); // cpu.c +extern void KiCheckTerminatedUserMode(); // traps.c + +PKREGISTERS KiSystemServiceHandler(PKREGISTERS Regs) +{ + if (Regs->Eax >= KI_SYSCALL_COUNT) + { + Regs->Eax = STATUS_INVALID_PARAMETER; + return Regs; + } + +#ifdef ENABLE_SYSCALL_TRACE + KePrintSystemServiceDebug(Regs->Eax); +#endif + + // Now call the function. + KI_SYSCALL_HANDLER Handler = KiSystemServiceTable[Regs->Eax]; + int Return = Handler(Regs->Ebx, Regs->Ecx, Regs->Edx, Regs->Esi, Regs->Edi, Regs->Ebp); + Regs->Eax = Return; + + KiCheckTerminatedUserMode(); + + return Regs; +} diff --git a/boron/source/ke/i386/traps.c b/boron/source/ke/i386/traps.c index 265249c1..9e7d57a9 100644 --- a/boron/source/ke/i386/traps.c +++ b/boron/source/ke/i386/traps.c @@ -47,8 +47,6 @@ KiIdtDescriptor; // The interrupt vector function type. It's not a function you can actually call from C. typedef void(*KiInterruptVector)(); -// Probably not going to be used because SYSCALL and SYSENTER both bypass the interrupt system. -// These are going to be optimized out, and are also niceties when needed, so I will keep them. static UNUSED void KiSetInterruptDPL(PKIDT Idt, int Vector, int Ring) { Idt->Entries[Vector].DPL = Ring; @@ -225,6 +223,8 @@ PKREGISTERS KiHandlePageFault(PKREGISTERS Regs) static KSPIN_LOCK KiTrapLock; +extern PKREGISTERS KiSystemServiceHandler(PKREGISTERS Regs); + // Run on the BSP only. void KiSetupIdt() { @@ -242,9 +242,12 @@ void KiSetupIdt() KeRegisterInterrupt(INTV_DBL_FAULT, KiHandleDoubleFault); KeRegisterInterrupt(INTV_PROT_FAULT, KiHandleProtectionFault); KeRegisterInterrupt(INTV_PAGE_FAULT, KiHandlePageFault); + KeRegisterInterrupt(INTV_SYSTEMCALL, KiSystemServiceHandler); KeSetInterruptIPL(INTV_DBL_FAULT, IPL_NOINTS); KeSetInterruptIPL(INTV_PROT_FAULT, IPL_NOINTS); KeSetInterruptIPL(INTV_PAGE_FAULT, IPL_UNDEFINED); + KeSetInterruptIPL(INTV_SYSTEMCALL, IPL_UNDEFINED); + KiSetInterruptDPL(&KiIdt, INTV_SYSTEMCALL, 3); } void KeRegisterInterrupt(int Vector, PKINTERRUPT_HANDLER Handler) diff --git a/boron/source/ke/sched.c b/boron/source/ke/sched.c index f56da8fd..3b245186 100644 --- a/boron/source/ke/sched.c +++ b/boron/source/ke/sched.c @@ -715,16 +715,21 @@ void KiSwitchToNextThread() uintptr_t StackBottom = (uintptr_t) Thread->Stack.Top + Thread->Stack.Size; KeGetCurrentPRCB()->SysCallStack = StackBottom; -#ifdef TARGET_AMD64 +#if defined TARGET_AMD64 || defined TARGET_I386 + // When an interrupt or exception happens and the CPL is ring 3, // this is fetched for a transition to ring 0. +#if defined TARGET_AMD64 KeGetCurrentPRCB()->ArchData.Tss.RSP[0] = StackBottom; +#elif defined TARGET_I386 + KeGetCurrentPRCB()->ArchData.Tss.Esp0 = StackBottom; +#endif // Set the relevant MSRs. - KeSetMSR(MSR_GS_BASE_KERNEL, (uint64_t) Thread->Process->PebPointer); - KeSetMSR(MSR_FS_BASE, (uint64_t) Thread->TebPointer); + KeSetMSR(MSR_GS_BASE_KERNEL, (uintptr_t) Thread->Process->PebPointer); + KeSetMSR(MSR_FS_BASE, (uintptr_t) Thread->TebPointer); #endif - + if (OldThread == Thread) { // The old thread is the same as the new thread, there's no need to diff --git a/boron/source/ps/initproc.c b/boron/source/ps/initproc.c index 56e07e66..99b3611b 100644 --- a/boron/source/ps/initproc.c +++ b/boron/source/ps/initproc.c @@ -199,6 +199,7 @@ void PsStartInitialProcess(UNUSED void* ContextUnused) // TODO: not sure why I have to subtract 0x1000 BoronDllBase = MM_USER_SPACE_END + 1 - PebSize - Size - 0x1000; + DbgPrint("BoronDllBase is %p", BoronDllBase); bool IsDynamicLoaded = false; uintptr_t BoronDllBaseOld = BoronDllBase; diff --git a/limine.i386.cfg b/limine.i386.cfg index 441c2176..9e690e27 100644 --- a/limine.i386.cfg +++ b/limine.i386.cfg @@ -4,7 +4,7 @@ VERBOSE=yes :Boron (i386) PROTOCOL=multiboot KERNEL_PATH=boot:///kernel.elf - CMDLINE=/noinit + #CMDLINE=/noinit MODULE_PATH=boot:///hali386.sys MODULE_STRING=hali386.sys diff --git a/user/include/svcs.h b/user/include/svcs.h index 338b0436..2b09757c 100644 --- a/user/include/svcs.h +++ b/user/include/svcs.h @@ -20,6 +20,8 @@ BSTATUS OSCreateEvent(PHANDLE OutHandle, POBJECT_ATTRIBUTES ObjectAttributes, in BSTATUS OSCreateMutex(PHANDLE OutHandle, POBJECT_ATTRIBUTES ObjectAttributes); +BSTATUS OSCreatePipe(PHANDLE OutHandle, POBJECT_ATTRIBUTES ObjectAttributes, size_t BufferSize, bool NonBlock); + #ifdef IS_BORON_DLL BSTATUS OSCreateProcessInternal( @@ -64,8 +66,12 @@ BSTATUS OSFreeVirtualMemory( BSTATUS OSGetAlignmentFile(HANDLE Handle, size_t* AlignmentOut); +void* OSGetCurrentPeb(); + void* OSGetCurrentTeb(); +BSTATUS OSGetExitCodeProcess(HANDLE ProcessHandle, int* ExitCodeOut); + BSTATUS OSGetLengthFile(HANDLE FileHandle, uint64_t* Length); BSTATUS OSGetMappedFileHandle(PHANDLE OutHandle, HANDLE ProcessHandle, uintptr_t Address); @@ -110,10 +116,14 @@ BSTATUS OSResetDirectoryReadHead(HANDLE FileHandle); BSTATUS OSResetEvent(HANDLE EventHandle); +BSTATUS OSSetCurrentPeb(void* Ptr); + BSTATUS OSSetCurrentTeb(void* Ptr); BSTATUS OSSetEvent(HANDLE EventHandle); +BSTATUS OSSetExitCode(int ExitCode); + BSTATUS OSSetPebProcess(HANDLE ProcessHandle, void* PebPtr); BSTATUS OSSetSuspendedThread(HANDLE ThreadHandle, bool IsSuspended); diff --git a/user/libboron/source/calls.S b/user/libboron/source/calls.S index c181d14e..5c3e4db8 100644 --- a/user/libboron/source/calls.S +++ b/user/libboron/source/calls.S @@ -6,7 +6,7 @@ #ifdef TARGET_AMD64 -.macro CALL number, name +.macro CALL number, argcount, name .globl \name .type \name, @function \name: @@ -17,36 +17,23 @@ pushq %r13 pushq %r14 pushq %r15 - movq %rcx, %r10 - movq $\number, %rax - syscall - popq %r15 - popq %r14 - popq %r13 - popq %r12 - popq %rbx - popq %rbp - ret -.endm - -// Unlike the previous call macro, this loads 3 extra arguments on the stack into r12, r13, and r14. -.macro CALLEX number, name - .globl \name - .type \name, @function -\name: - pushq %rbp - movq %rsp, %rbp - pushq %rbx - pushq %r12 - pushq %r13 - pushq %r14 - pushq %r15 - movq 16(%rbp), %r12 - movq 24(%rbp), %r13 - movq 32(%rbp), %r14 - movq %rcx, %r10 + + .if \argcount > 6 + movq 16(%rbp), %r12 + .endif + .if \argcount > 7 + movq 24(%rbp), %r13 + .endif + .if \argcount > 8 + movq 32(%rbp), %r14 + .endif + .if \argcount > 3 + movq %rcx, %r10 + .endif + movq $\number, %rax syscall + popq %r15 popq %r14 popq %r13 @@ -58,68 +45,52 @@ #elif defined TARGET_I386 -.macro CALL number, name - .globl \name - .type \name, @function -\name: - // TODO - ret -.endmacro - -.macro CALLEX number, name +.macro CALL number, argcount, name .globl \name .type \name, @function \name: - // TODO + pushl %ebp + movl %esp, %ebp + pushl %ebx + pushl %esi + pushl %edi + + movl $\number, %eax + .if \argcount > 0 + movl 8(%ebp), %ebx + .endif + .if \argcount > 1 + movl 12(%ebp), %ecx + .endif + .if \argcount > 2 + movl 16(%ebp), %edx + .endif + .if \argcount > 3 + movl 20(%ebp), %esi + .endif + .if \argcount > 4 + movl 24(%ebp), %edi + .endif + .if \argcount == 6 + /* if we have *exactly* 6 arguments, then just pass the */ + /* last one into EBP */ + movl 28(%ebp), %ebp + .elseif \argcount > 6 + /* if we have *more* than 6 arguments, we need to pass a */ + /* pointer to the last arguments which we'll copy from */ + /* the stack */ + leal 28(%ebp), %ebp + .endif + + int $0x80 + + popl %edi + popl %esi + popl %ebx + popl %ebp ret .endmacro #endif -CALL 0, OSAllocateVirtualMemory -CALL 1, OSClose -CALL 2, OSCreateEvent -CALL 3, OSCreateMutex -CALL 4, OSCreatePipe -CALL 5, OSCreateProcessInternal -CALL 6, OSCreateThread -CALL 7, OSDeviceIoControl -CALL 8, OSDuplicateHandle -CALL 9, OSExitProcess -CALL 10, OSExitThread -CALL 11, OSFreeVirtualMemory -CALL 12, OSGetAlignmentFile -CALL 13, OSGetCurrentPeb -CALL 14, OSGetCurrentTeb -CALL 15, OSGetExitCodeProcess -CALL 16, OSGetLengthFile -CALL 17, OSGetMappedFileHandle -CALL 18, OSGetTickCount -CALL 19, OSGetTickFrequency -CALL 20, OSGetVersionNumber -CALLEX 21, OSMapViewOfObject -CALL 22, OSOpenEvent -CALL 23, OSOpenFile -CALL 24, OSOpenMutex -CALL 25, OSOutputDebugString -CALL 26, OSPulseEvent -CALL 27, OSQueryEvent -CALL 28, OSQueryMutex -CALL 29, OSReadDirectoryEntries -CALL 30, OSReadFile -CALL 31, OSReleaseMutex -CALL 32, OSResetDirectoryReadHead -CALL 33, OSResetEvent -CALL 34, OSSetCurrentPeb -CALL 35, OSSetCurrentTeb -CALL 36, OSSetEvent -CALL 37, OSSetExitCode -CALL 38, OSSetPebProcess -CALL 39, OSSetSuspendedThread -CALL 40, OSSleep -CALL 41, OSTerminateThread -CALL 42, OSTouchFile -CALL 43, OSWaitForMultipleObjects -CALL 44, OSWaitForSingleObject -CALLEX 45, OSWriteFile -CALL 46, OSWriteVirtualMemory +#include "calltbl.h" diff --git a/user/libboron/source/calltbl.h b/user/libboron/source/calltbl.h new file mode 100644 index 00000000..c8e7078b --- /dev/null +++ b/user/libboron/source/calltbl.h @@ -0,0 +1,58 @@ + +CALL 0, 5, OSAllocateVirtualMemory +CALL 1, 1, OSClose +CALL 2, 4, OSCreateEvent +CALL 3, 2, OSCreateMutex +CALL 4, 4, OSCreatePipe +CALL 5, 4, OSCreateProcessInternal +CALL 6, 6, OSCreateThread +CALL 7, 6, OSDeviceIoControl +CALL 8, 4, OSDuplicateHandle +CALL 9, 1, OSExitProcess +CALL 10, 0, OSExitThread +CALL 11, 4, OSFreeVirtualMemory +CALL 12, 2, OSGetAlignmentFile +CALL 13, 0, OSGetCurrentPeb +CALL 14, 0, OSGetCurrentTeb +CALL 15, 2, OSGetExitCodeProcess +CALL 16, 2, OSGetLengthFile +CALL 17, 3, OSGetMappedFileHandle +CALL 18, 1, OSGetTickCount +CALL 19, 1, OSGetTickFrequency +CALL 20, 1, OSGetVersionNumber +// 21 OSMapViewOfObject +CALL 22, 2, OSOpenEvent +CALL 23, 2, OSOpenFile +CALL 24, 2, OSOpenMutex +CALL 25, 2, OSOutputDebugString +CALL 26, 1, OSPulseEvent +CALL 27, 2, OSQueryEvent +CALL 28, 2, OSQueryMutex +CALL 29, 4, OSReadDirectoryEntries +// 30 OSReadFile +CALL 31, 1, OSReleaseMutex +CALL 32, 1, OSResetDirectoryReadHead +CALL 33, 1, OSResetEvent +CALL 34, 1, OSSetCurrentPeb +CALL 35, 1, OSSetCurrentTeb +CALL 36, 1, OSSetEvent +CALL 37, 1, OSSetExitCode +CALL 38, 2, OSSetPebProcess +CALL 39, 2, OSSetSuspendedThread +CALL 40, 1, OSSleep +CALL 41, 1, OSTerminateThread +CALL 42, 1, OSTouchFile +CALL 43, 5, OSWaitForMultipleObjects +CALL 44, 3, OSWaitForSingleObject +// 45 OSWriteFile +CALL 46, 4, OSWriteVirtualMemory + +#ifdef IS_64_BIT +CALL 21, 7, OSMapViewOfObject +CALL 30, 6, OSReadFile +CALL 45, 7, OSWriteFile +#else +CALL 21, 8, OSMapViewOfObject +CALL 30, 7, OSReadFile +CALL 45, 8, OSWriteFile +#endif diff --git a/user/libboron/source/init.c b/user/libboron/source/init.c index 7e68138c..9241e6cc 100644 --- a/user/libboron/source/init.c +++ b/user/libboron/source/init.c @@ -695,6 +695,7 @@ uintptr_t OSDLLGetProcedureAddress(const char* ProcName) HIDDEN void DLLEntryPoint(PPEB Peb) { + DbgPrint("OSDLL: DLLEntryPoint called!"); OSDLLInitializeGlobalHeap(); InitializeListHead(&OSDllLoadQueue); InitializeListHead(&OSDllsLoaded); From 1f4ba7b9922d15ef156b7fedcb823c2dbf95432e Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Sun, 26 Oct 2025 11:15:38 +0200 Subject: [PATCH 40/44] * Ke: Fix parameter passing in KeDescendIntoUserMode. * Ps: Fix a bug where BSS wasn't initialized to zero. --- boron/source/build_number | 2 +- boron/source/ke/i386/syscall.c | 2 ++ boron/source/ke/i386/trap.asm | 8 +++++++- boron/source/ps/initproc.c | 27 +++++++++++++++++++++------ limine.i386.cfg | 4 ++-- user/libboron/source/init.c | 1 - 6 files changed, 33 insertions(+), 11 deletions(-) diff --git a/boron/source/build_number b/boron/source/build_number index 1303c35c..a0879629 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -497 +514 diff --git a/boron/source/ke/i386/syscall.c b/boron/source/ke/i386/syscall.c index 606c3281..47227217 100644 --- a/boron/source/ke/i386/syscall.c +++ b/boron/source/ke/i386/syscall.c @@ -7,6 +7,8 @@ #include #include +//#define ENABLE_SYSCALL_TRACE + typedef struct { uint64_t SectionOffset; diff --git a/boron/source/ke/i386/trap.asm b/boron/source/ke/i386/trap.asm index 6e0fdea8..8e230afe 100644 --- a/boron/source/ke/i386/trap.asm +++ b/boron/source/ke/i386/trap.asm @@ -122,6 +122,12 @@ KeDescendIntoUserMode: mov esi, [esp + 8] mov edx, [esp + 12] + ; put edx onto the stack, as well as a fake return address + ; this is so that parameter passing through this function works + sub esi, 8 + mov dword [esi], 0 + mov dword [esi + 4], edx + ; EDI - Initial program counter ; ESI - Initial stack pointer ; EDX - User context @@ -130,7 +136,6 @@ KeDescendIntoUserMode: push dword 0x202 ; push RFLAGS push dword SEG_RING_3_CODE | 3 ; push CS push edi ; push RIP - mov edi, edx ; clear all the registers xor eax, eax @@ -139,6 +144,7 @@ KeDescendIntoUserMode: xor ecx, ecx xor edx, edx xor esi, esi + xor edi, edi ; finally, swap gs and return to user mode. cli diff --git a/boron/source/ps/initproc.c b/boron/source/ps/initproc.c index 99b3611b..7793e949 100644 --- a/boron/source/ps/initproc.c +++ b/boron/source/ps/initproc.c @@ -1,6 +1,6 @@ /*** The Boron Operating System - Copyright (C) 2023 iProgramInCpp + Copyright (C) 2025 iProgramInCpp Module name: ps/initproc.c @@ -99,6 +99,10 @@ void PsStartInitialProcess(UNUSED void* ContextUnused) if (FAILED(Status)) KeCrash("%s: Failed to create initial process: %d (%s)", Status, RtlGetStatusString(Status)); + PEPROCESS Process = NULL; + Status = ExReferenceObjectByHandle(ProcessHandle, PsProcessObjectType, (void**) &Process); + ASSERT(SUCCEEDED(Status)); + FileAttributes.RootDirectory = HANDLE_NONE; FileAttributes.ObjectName = BoronDllPath; FileAttributes.ObjectNameLength = strlen(BoronDllPath); @@ -199,7 +203,6 @@ void PsStartInitialProcess(UNUSED void* ContextUnused) // TODO: not sure why I have to subtract 0x1000 BoronDllBase = MM_USER_SPACE_END + 1 - PebSize - Size - 0x1000; - DbgPrint("BoronDllBase is %p", BoronDllBase); bool IsDynamicLoaded = false; uintptr_t BoronDllBaseOld = BoronDllBase; @@ -253,6 +256,22 @@ void PsStartInitialProcess(UNUSED void* ContextUnused) BaseAddress ); } + + // If the size in file doesn't match the in-memory size, clear the rest to zero. + // This will trigger CoW faults. + if (ProgramHeader->SizeInFile < ProgramHeader->SizeInMemory) + { + // Temporarily attach to the process. + // TODO: Optimize. Don't switch the address space every time. + size_t SizeInFile = ProgramHeader->SizeInFile; + size_t SizeInMemory = ProgramHeader->SizeInMemory; + + PEPROCESS Restore = PsSetAttachedProcess(Process); + + memset((char*)BaseAddress + SizeInFile, 0, SizeInMemory - SizeInFile); + + PsSetAttachedProcess(Restore); + } } // Program headers have been mapped. @@ -284,10 +303,6 @@ void PsStartInitialProcess(UNUSED void* ContextUnused) KeCrash("%s: Failed to allocate PEB: %d (%s)", Func, Status, RtlGetStatusString(Status)); // Attach to this process so that we can write to the PEB. - PEPROCESS Process = NULL; - Status = ExReferenceObjectByHandle(ProcessHandle, PsProcessObjectType, (void**) &Process); - ASSERT(SUCCEEDED(Status)); - PEPROCESS OldAttached = PsSetAttachedProcess(Process); PPEB PPeb = PebPtr; diff --git a/limine.i386.cfg b/limine.i386.cfg index 9e690e27..963416a5 100644 --- a/limine.i386.cfg +++ b/limine.i386.cfg @@ -21,8 +21,8 @@ VERBOSE=yes #MODULE_PATH=boot:///ext2fs.sys #MODULE_STRING=ext2fs.sys - MODULE_PATH=boot:///test.sys - MODULE_STRING=test.sys + #MODULE_PATH=boot:///test.sys + #MODULE_STRING=test.sys MODULE_PATH=boot:///libboron.so MODULE_STRING=libboron.so diff --git a/user/libboron/source/init.c b/user/libboron/source/init.c index 9241e6cc..7e68138c 100644 --- a/user/libboron/source/init.c +++ b/user/libboron/source/init.c @@ -695,7 +695,6 @@ uintptr_t OSDLLGetProcedureAddress(const char* ProcName) HIDDEN void DLLEntryPoint(PPEB Peb) { - DbgPrint("OSDLL: DLLEntryPoint called!"); OSDLLInitializeGlobalHeap(); InitializeListHead(&OSDllLoadQueue); InitializeListHead(&OSDllsLoaded); From 5a5bb5c1ca5117cd6646cc3d4dbc3ef2172d378f Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Sun, 26 Oct 2025 16:45:40 +0200 Subject: [PATCH 41/44] * Ke(i386): Fix definition of OSReadFile and OSWriteFile * Mm: Fix a bug with MmCopyIntoMdl which prevented the function from taking page faults on 32-bit * Libboron: Clear bss in loaded modules if it's part of the data phdr * Load all the userspace files now --- boron/source/build_number | 2 +- boron/source/ke/i386/debug.c | 8 +++--- boron/source/ke/i386/probe.c | 6 ++-- boron/source/ke/i386/syscall.c | 8 +++--- boron/source/mm/mdl.c | 52 ++++++++++++++++++++++++++++------ limine.i386.cfg | 12 ++++---- user/libboron/source/calls.S | 2 ++ user/libboron/source/init.c | 39 +++++++++++++++++++++++++ 8 files changed, 103 insertions(+), 26 deletions(-) diff --git a/boron/source/build_number b/boron/source/build_number index a0879629..3fa81504 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -514 +577 diff --git a/boron/source/ke/i386/debug.c b/boron/source/ke/i386/debug.c index b0d79cca..a2a7943b 100644 --- a/boron/source/ke/i386/debug.c +++ b/boron/source/ke/i386/debug.c @@ -130,9 +130,9 @@ void DbgPrintStackTrace(uintptr_t Ebp) DbgPrintDouble("\tAddress \tName\n"); #ifndef DISABLE_USER_MODE_PREVENTION - if (Rbp <= MM_USER_SPACE_END) + if (Ebp <= MM_USER_SPACE_END) { - snprintf(Buffer, sizeof(Buffer), "\t%p\tUser Mode Address\n", (void*) Rbp); + snprintf(Buffer, sizeof(Buffer), "\t%p\tUser Mode Address\n", (void*) Ebp); DbgPrintDouble(Buffer); return; } @@ -145,7 +145,7 @@ void DbgPrintStackTrace(uintptr_t Ebp) #ifndef DISABLE_USER_MODE_PREVENTION if (Address <= MM_USER_SPACE_END) { - snprintf(Buffer, sizeof(Buffer), "\t%p\tUser Mode Address\n", (void*) Rbp); + snprintf(Buffer, sizeof(Buffer), "\t%p\tUser Mode Address\n", (void*) Ebp); DbgPrintDouble(Buffer); return; } @@ -163,7 +163,7 @@ void DbgPrintStackTrace(uintptr_t Ebp) #ifndef DISABLE_USER_MODE_PREVENTION if ((uintptr_t)StackFrame <= MM_USER_SPACE_END) { - snprintf(Buffer, sizeof(Buffer), "\t%p\tUser Mode Address\n", (void*) Rbp); + snprintf(Buffer, sizeof(Buffer), "\t%p\tUser Mode Address\n", (void*) Ebp); DbgPrintDouble(Buffer); return; } diff --git a/boron/source/ke/i386/probe.c b/boron/source/ke/i386/probe.c index cca43f7e..7d5b7d47 100644 --- a/boron/source/ke/i386/probe.c +++ b/boron/source/ke/i386/probe.c @@ -24,19 +24,19 @@ bool MmIsAddressRangeValid(uintptr_t Address, size_t Size, KPROCESSOR_MODE Acces { // Size=0 is invalid. if (Size == 0) { - DbgPrint("Size 0"); + DbgPrint("MmIsAddressRangeValid FAILURE: Size 0"); return false; } // Check for overflow. uintptr_t AddressEnd = Address + Size; if (AddressEnd < Address) { - DbgPrint("AddressEnd %p < Address %p", AddressEnd, Address); + DbgPrint("MmIsAddressRangeValid FAILURE: AddressEnd %p < Address %p", AddressEnd, Address); return false; } if (AccessMode == MODE_USER && AddressEnd > MM_USER_SPACE_END) { - DbgPrint("AccessMode==MODEUSER AddressEnd %p Address %p Size: %zu RA:%p", AddressEnd, Address, Size, CallerAddress()); + DbgPrint("MmIsAddressRangeValid FAILURE: AccessMode==MODEUSER AddressEnd %p Address %p Size: %zu RA:%p", AddressEnd, Address, Size, CallerAddress()); return false; } diff --git a/boron/source/ke/i386/syscall.c b/boron/source/ke/i386/syscall.c index 47227217..731b5ca8 100644 --- a/boron/source/ke/i386/syscall.c +++ b/boron/source/ke/i386/syscall.c @@ -18,12 +18,14 @@ MAP_VIEW_OF_OBJECT_PARAMS, *PMAP_VIEW_OF_OBJECT_PARAMS; typedef struct { + size_t Length; uint32_t Flags; } READ_FILE_PARAMS, *PREAD_FILE_PARAMS; typedef struct { + size_t Length; uint32_t Flags; uint64_t* OutSize; } @@ -61,7 +63,6 @@ BSTATUS OSReadFile_Call( HANDLE Handle, uint64_t ByteOffset, // 2 parameters void* Buffer, - size_t Length, PREAD_FILE_PARAMS ExtraParamsPtr ) { @@ -72,7 +73,7 @@ BSTATUS OSReadFile_Call( if (FAILED(Status)) return Status; - return OSReadFile(Iosb, Handle, ByteOffset, Buffer, Length, ExtraParams.Flags); + return OSReadFile(Iosb, Handle, ByteOffset, Buffer, ExtraParams.Length, ExtraParams.Flags); } BSTATUS OSWriteFile_Call( @@ -80,7 +81,6 @@ BSTATUS OSWriteFile_Call( HANDLE Handle, uint64_t ByteOffset, // 2 parameters const void* Buffer, - size_t Length, PWRITE_FILE_PARAMS ExtraParamsPtr ) { @@ -91,7 +91,7 @@ BSTATUS OSWriteFile_Call( if (FAILED(Status)) return Status; - return OSWriteFile(Iosb, Handle, ByteOffset, Buffer, Length, ExtraParams.Flags, ExtraParams.OutSize); + return OSWriteFile(Iosb, Handle, ByteOffset, Buffer, ExtraParams.Length, ExtraParams.Flags, ExtraParams.OutSize); } // N.B. extra arguments will be ignored by the called function. diff --git a/boron/source/mm/mdl.c b/boron/source/mm/mdl.c index 05d3ea85..1cc0d525 100644 --- a/boron/source/mm/mdl.c +++ b/boron/source/mm/mdl.c @@ -348,6 +348,11 @@ void MmCopyIntoMdl(PMDL Mdl, uintptr_t Offset, const void* SourceBuffer, size_t // code below to pretend that the starting pointer is page aligned. Offset += Mdl->ByteOffset; +#ifdef IS_32_BIT + // TODO: Get rid of this entirely by re-engineering the HHDM system + char* Temporary = MmAllocatePool(POOL_NONPAGED, 4096); +#endif + while (Size) { size_t PageIndex = Offset / PAGE_SIZE; @@ -363,24 +368,43 @@ void MmCopyIntoMdl(PMDL Mdl, uintptr_t Offset, const void* SourceBuffer, size_t else CopyAmount = BytesTillNext; + // on 32-bit, MmBeginUsingHHDM and MmEndUsingHHDM raise and lower IPL, which + // means we CANNOT! take page faults. This is why I opted for this hack. +#ifdef IS_32_BIT + memcpy(Temporary, SourceBufferChr, CopyAmount); +#else + const char* Temporary = SourceBufferChr; +#endif + MmBeginUsingHHDM(); char* PageDest = MmGetHHDMOffsetAddr(MmPFNToPhysPage(Mdl->Pages[PageIndex])); - memcpy(PageDest + PageOffs, SourceBufferChr, CopyAmount); + memcpy(PageDest + PageOffs, Temporary, CopyAmount); MmEndUsingHHDM(); SourceBufferChr += CopyAmount; Offset += CopyAmount; Size -= CopyAmount; } + +#ifdef IS_32_BIT + MmFreePool(Temporary); +#endif } -void MmSetIntoMdl(PMDL Mdl, uintptr_t Offset, uint8_t ToSet, size_t Size) +void MmCopyFromMdl(PMDL Mdl, uintptr_t Offset, void* DestinationBuffer, size_t Size) { + char* DestBufferChr = (char*) DestinationBuffer; + // NOTE: The MDL's starting pointer isn't necessarily page aligned. // As such, push the offset forward by Mdl->ByteOffset to allow the // code below to pretend that the starting pointer is page aligned. Offset += Mdl->ByteOffset; +#ifdef IS_32_BIT + // TODO: Get rid of this entirely by re-engineering the HHDM system + char* Temporary = MmAllocatePool(POOL_NONPAGED, 4096); +#endif + while (Size) { size_t PageIndex = Offset / PAGE_SIZE; @@ -396,20 +420,33 @@ void MmSetIntoMdl(PMDL Mdl, uintptr_t Offset, uint8_t ToSet, size_t Size) else CopyAmount = BytesTillNext; + // on 32-bit, MmBeginUsingHHDM and MmEndUsingHHDM raise and lower IPL, which + // means we CANNOT! take page faults. This is why I opted for this hack. +#ifndef IS_32_BIT + char* Temporary = DestBufferChr; +#endif + MmBeginUsingHHDM(); char* PageDest = MmGetHHDMOffsetAddr(MmPFNToPhysPage(Mdl->Pages[PageIndex])); - memset(PageDest + PageOffs, ToSet, CopyAmount); + memcpy(DestBufferChr, PageDest + PageOffs, CopyAmount); MmEndUsingHHDM(); +#ifdef IS_32_BIT + memcpy(DestBufferChr, Temporary, CopyAmount); +#endif + + DestBufferChr += CopyAmount; Offset += CopyAmount; Size -= CopyAmount; } + +#ifdef IS_32_BIT + MmFreePool(Temporary); +#endif } -void MmCopyFromMdl(PMDL Mdl, uintptr_t Offset, void* DestinationBuffer, size_t Size) +void MmSetIntoMdl(PMDL Mdl, uintptr_t Offset, uint8_t ToSet, size_t Size) { - char* DestBufferChr = (char*) DestinationBuffer; - // NOTE: The MDL's starting pointer isn't necessarily page aligned. // As such, push the offset forward by Mdl->ByteOffset to allow the // code below to pretend that the starting pointer is page aligned. @@ -432,10 +469,9 @@ void MmCopyFromMdl(PMDL Mdl, uintptr_t Offset, void* DestinationBuffer, size_t S MmBeginUsingHHDM(); char* PageDest = MmGetHHDMOffsetAddr(MmPFNToPhysPage(Mdl->Pages[PageIndex])); - memcpy(DestBufferChr, PageDest + PageOffs, CopyAmount); + memset(PageDest + PageOffs, ToSet, CopyAmount); MmEndUsingHHDM(); - DestBufferChr += CopyAmount; Offset += CopyAmount; Size -= CopyAmount; } diff --git a/limine.i386.cfg b/limine.i386.cfg index 963416a5..68cc3cb5 100644 --- a/limine.i386.cfg +++ b/limine.i386.cfg @@ -26,9 +26,9 @@ VERBOSE=yes MODULE_PATH=boot:///libboron.so MODULE_STRING=libboron.so - #MODULE_PATH=boot:///libtest.so - #MODULE_STRING=libtest.so - #MODULE_PATH=boot:///init.exe - #MODULE_STRING=init.exe - #MODULE_PATH=boot:///test.exe - #MODULE_STRING=test.exe + MODULE_PATH=boot:///libtest.so + MODULE_STRING=libtest.so + MODULE_PATH=boot:///init.exe + MODULE_STRING=init.exe + MODULE_PATH=boot:///test.exe + MODULE_STRING=test.exe diff --git a/user/libboron/source/calls.S b/user/libboron/source/calls.S index 5c3e4db8..7e901c9f 100644 --- a/user/libboron/source/calls.S +++ b/user/libboron/source/calls.S @@ -56,6 +56,7 @@ pushl %edi movl $\number, %eax + .if \argcount > 0 movl 8(%ebp), %ebx .endif @@ -71,6 +72,7 @@ .if \argcount > 4 movl 24(%ebp), %edi .endif + .if \argcount == 6 /* if we have *exactly* 6 arguments, then just pass the */ /* last one into EBP */ diff --git a/user/libboron/source/init.c b/user/libboron/source/init.c index 7e68138c..56ae19c7 100644 --- a/user/libboron/source/init.c +++ b/user/libboron/source/init.c @@ -138,6 +138,15 @@ BSTATUS OSDLLMapElfFile( if (NeedReadAndMapFile) { + DbgPrint( + "Iosb: %p, Handle: %d, Offset: %lld, Buffer: %p, Size: %zu", + (void*) &Iosb, + (int) Handle, + (uint64_t) 0, + (void*) &ElfHeader, + (size_t) sizeof ElfHeader + ); + Status = OSReadFile(&Iosb, Handle, 0, &ElfHeader, sizeof ElfHeader, 0); if (FAILED(Status)) { @@ -383,6 +392,36 @@ BSTATUS OSDLLMapElfFile( if (IsMainExecutable && ElfProgramHeader.Offset == 0) Peb->Loader.FileHeader = Address; + if (ElfProgramHeader.SizeInFile < ElfProgramHeader.SizeInMemory) + { + // TODO: add a system call for this instead! + size_t Offset = ElfProgramHeader.SizeInFile; + size_t Size = ElfProgramHeader.SizeInMemory - ElfProgramHeader.SizeInFile; + + uint8_t* Data = OSAllocate(Size); + if (!Data) + { + DbgPrint("OSDLL: Failed to map part of uninitialized data for the program due to insufficient memory"); + Status = STATUS_INSUFFICIENT_MEMORY; + goto EarlyExit; + } + + memset(Data, 0, Size); + + Status = OSWriteVirtualMemory(ProcessHandle, (char*) Address + Offset, Data, Size); + OSFree(Data); + + if (FAILED(Status)) + { + DbgPrint( + "OSDLL: Failed to map part of uninitialized data for the program: %s (%d)", + RtlGetStatusString(Status), + Status + ); + goto EarlyExit; + } + } + break; } case PROG_DYNAMIC: From fcbe1aea4c2f5ad32265184bdc0895d24ba9c1cf Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Sun, 26 Oct 2025 20:15:12 +0200 Subject: [PATCH 42/44] * Libboron: Do not relocate the interpreter, it already relocated itself. --- user/libboron/source/dll.h | 10 +++++++++- user/libboron/source/init.c | 35 ++++++++++++++++++++-------------- user/libboron/source/process.c | 4 ++-- 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/user/libboron/source/dll.h b/user/libboron/source/dll.h index 4f434ea2..13ee6425 100644 --- a/user/libboron/source/dll.h +++ b/user/libboron/source/dll.h @@ -12,11 +12,19 @@ #define LdrDbgPrint(...) do {} while (0) #endif +enum +{ + FILE_KIND_MAIN_EXECUTABLE, + FILE_KIND_DYNAMIC_LIBRARY, + FILE_KIND_INTERPRETER, +}; + typedef struct { LIST_ENTRY ListEntry; char Name[32]; + int FileKind; uintptr_t ImageBase; PELF_DYNAMIC_ITEM DynamicTable; ELF_DYNAMIC_INFO DynamicInfo; @@ -43,7 +51,7 @@ BSTATUS OSDLLMapElfFile( HANDLE FileHandle, const char* Name, ELF_ENTRY_POINT2* OutEntryPoint, - bool IsMainExecutable + int FileKind ); // Opens a file by name, scanning the PATH environment variable. diff --git a/user/libboron/source/init.c b/user/libboron/source/init.c index 56ae19c7..47959ee7 100644 --- a/user/libboron/source/init.c +++ b/user/libboron/source/init.c @@ -93,7 +93,7 @@ BSTATUS OSDLLMapElfFile( HANDLE Handle, const char* Name, ELF_ENTRY_POINT2* OutEntryPoint, - bool IsMainExecutable + int FileKind ) { BSTATUS Status; @@ -107,6 +107,7 @@ BSTATUS OSDLLMapElfFile( bool NeedFreeProgramHeaders = false; bool NeedReadAndMapFile = true; bool IsSeparateProcess = ProcessHandle != CURRENT_PROCESS_HANDLE; + bool IsMainExecutable = FileKind == FILE_KIND_MAIN_EXECUTABLE; Name = OSDLLOffsetPathAway(Name); LdrDbgPrint("OSDLL: Mapping ELF file %s.", Name); @@ -468,6 +469,7 @@ BSTATUS OSDLLMapElfFile( } LoadedImage = OSAllocate(sizeof(LOADED_IMAGE)); + LoadedImage->FileKind = FileKind; StringCopySafe(LoadedImage->Name, Name, sizeof(LoadedImage->Name)); // Parse the dynamic segment, if it exists. @@ -512,7 +514,7 @@ BSTATUS OSDLLLoadDynamicLibrary(PPEB Peb, const char* FileName) } ELF_ENTRY_POINT2 EntryPoint; - Status = OSDLLMapElfFile(Peb, CURRENT_PROCESS_HANDLE, Handle, FileName, &EntryPoint, false); + Status = OSDLLMapElfFile(Peb, CURRENT_PROCESS_HANDLE, Handle, FileName, &EntryPoint, FILE_KIND_DYNAMIC_LIBRARY); OSClose(Handle); if (FAILED(Status)) return Status; @@ -528,6 +530,7 @@ void OSDLLAddSelfToDllList() LoadedImage->ImageBase = RtlGetImageBase(); LoadedImage->DynamicTable = _DYNAMIC; + LoadedImage->FileKind = FILE_KIND_INTERPRETER; #ifdef DEBUG // Assert that DYN_NEEDED is not specified. @@ -587,7 +590,7 @@ BSTATUS OSDLLRunImage(PPEB Peb, ELF_ENTRY_POINT2* OutEntryPoint) return Status; } - Status = OSDLLMapElfFile(Peb, CURRENT_PROCESS_HANDLE, FileHandle, Peb->ImageName, OutEntryPoint, true); + Status = OSDLLMapElfFile(Peb, CURRENT_PROCESS_HANDLE, FileHandle, Peb->ImageName, OutEntryPoint, FILE_KIND_MAIN_EXECUTABLE); OSClose(FileHandle); if (FAILED(Status)) @@ -650,18 +653,22 @@ BSTATUS OSDLLRunImage(PPEB Peb, ELF_ENTRY_POINT2* OutEntryPoint) { PLOADED_IMAGE LoadedImage = CONTAINING_RECORD(DllEntry, LOADED_IMAGE, ListEntry); - if (!RtlPerformRelocations(&LoadedImage->DynamicInfo, LoadedImage->ImageBase)) + // The interpreter knows how to relocate themselves and has already done so. + if (LoadedImage->FileKind != FILE_KIND_INTERPRETER) { - DbgPrint("OSDLL: Cannot perform relocations on module %s.", LoadedImage->Name); - return STATUS_INVALID_EXECUTABLE; - } - - RtlRelocateRelrEntries(&LoadedImage->DynamicInfo, LoadedImage->ImageBase); - - if (!RtlLinkPlt(&LoadedImage->DynamicInfo, LoadedImage->ImageBase, LoadedImage->Name)) - { - DbgPrint("OSDLL: Module %s could not be linked.", LoadedImage->Name); - return STATUS_INVALID_EXECUTABLE; + if (!RtlPerformRelocations(&LoadedImage->DynamicInfo, LoadedImage->ImageBase)) + { + DbgPrint("OSDLL: Cannot perform relocations on module %s.", LoadedImage->Name); + return STATUS_INVALID_EXECUTABLE; + } + + RtlRelocateRelrEntries(&LoadedImage->DynamicInfo, LoadedImage->ImageBase); + + if (!RtlLinkPlt(&LoadedImage->DynamicInfo, LoadedImage->ImageBase, LoadedImage->Name)) + { + DbgPrint("OSDLL: Module %s could not be linked.", LoadedImage->Name); + return STATUS_INVALID_EXECUTABLE; + } } // TODO: Change permissions so that the code segments cannot be written to anymore. diff --git a/user/libboron/source/process.c b/user/libboron/source/process.c index f037eeff..057baf3a 100644 --- a/user/libboron/source/process.c +++ b/user/libboron/source/process.c @@ -220,7 +220,7 @@ BSTATUS OSCreateProcess( // Now map the main image inside. LdrDbgPrint("OSCreateProcess: Mapping main image %s.", ImageName); - Status = OSDLLMapElfFile(Peb, ProcessHandle, FileHandle, ImageName, &EntryPoint, true); + Status = OSDLLMapElfFile(Peb, ProcessHandle, FileHandle, ImageName, &EntryPoint, FILE_KIND_MAIN_EXECUTABLE); OSClose(FileHandle); if (FAILED(Status)) @@ -254,7 +254,7 @@ BSTATUS OSCreateProcess( //Status = OSDLLMapSelfIntoProcess(ProcessHandle, InterpreterFileHandle, PebSize, &EntryPoint); LdrDbgPrint("OSCreateProcess: Mapping interpreter %s.", Interpreter); - Status = OSDLLMapElfFile(Peb, ProcessHandle, InterpreterFileHandle, Interpreter, &EntryPoint, false); + Status = OSDLLMapElfFile(Peb, ProcessHandle, InterpreterFileHandle, Interpreter, &EntryPoint, FILE_KIND_INTERPRETER); OSClose(InterpreterFileHandle); if (FAILED(Status)) From 0a1b3ca111f0383516e2d1ac35a59710b681a75f Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Sun, 26 Oct 2025 21:15:59 +0200 Subject: [PATCH 43/44] * User: Running a second process now works! * Mm(i386): Relocate second part of pool. * Mm(i386): Initialize page tables for pool to keep them shared across address spaces. * Mm(i386): Fix bug where MmProbeAddressSubEarlyReturn doesn't return properly. * Mm(i386): Rewrite unused page table freeing context. * Mm: Fix bug where MmCopyFromMdl wasn't using the correct pointer. * Mm: Fix bug where the VA offset was applied twice. --- boron/include/arch/i386.h | 2 +- boron/source/build_number | 2 +- boron/source/ex/servsup.c | 20 ++++++-- boron/source/ke/i386/misc.asm | 14 +++--- boron/source/mm/amd64/ptfree.c | 2 +- boron/source/mm/fault.c | 2 +- boron/source/mm/heap.c | 4 ++ boron/source/mm/i386/ptfree.c | 92 ++++------------------------------ boron/source/mm/mdl.c | 2 +- boron/source/mm/poolsup.c | 33 +++++++++++- boron/source/mm/view.c | 4 -- user/init/source/main.c | 4 +- user/libboron/source/init.c | 21 +++----- 13 files changed, 84 insertions(+), 118 deletions(-) diff --git a/boron/include/arch/i386.h b/boron/include/arch/i386.h index c173ca85..909fad1c 100644 --- a/boron/include/arch/i386.h +++ b/boron/include/arch/i386.h @@ -36,7 +36,7 @@ void KePortWriteDword(uint16_t portNo, uint32_t data); // start PML2 index will be 512. The PFN database's is 776 #define MI_GLOBAL_AREA_START (512) -#define MI_GLOBAL_AREA_START_2ND (832) +#define MI_GLOBAL_AREA_START_2ND (896) #define MI_RECURSIVE_PAGING_START (1023) diff --git a/boron/source/build_number b/boron/source/build_number index 3fa81504..8026d9d6 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -577 +647 diff --git a/boron/source/ex/servsup.c b/boron/source/ex/servsup.c index 8b1416fd..7e270432 100644 --- a/boron/source/ex/servsup.c +++ b/boron/source/ex/servsup.c @@ -289,20 +289,34 @@ BSTATUS OSDummy() return 0; } +//#define SHOW_PID_IN_DEBUG_LOGS + // Prints a string to the debug console. BSTATUS OSOutputDebugString(const char* String, size_t StringLength) { BSTATUS Status; char* Memory; - Memory = MmAllocatePool(POOL_PAGED, StringLength + 1); +#ifdef SHOW_PID_IN_DEBUG_LOGS + const int OFFSET = 2 + 2 * sizeof(uintptr_t); +#else + const int OFFSET = 0; +#endif + Memory = MmAllocatePool(POOL_PAGED, StringLength + 1 + OFFSET); if (!Memory) return STATUS_INSUFFICIENT_MEMORY; - Memory[StringLength] = 0; - Status = MmSafeCopy(Memory, String, StringLength, KeGetPreviousMode(), false); + Memory[StringLength + OFFSET] = 0; + Status = MmSafeCopy(Memory + OFFSET, String, StringLength, KeGetPreviousMode(), false); if (SUCCEEDED(Status)) + { + #ifdef SHOW_PID_IN_DEBUG_LOGS + char PidBuffer[36]; + snprintf(PidBuffer, sizeof PidBuffer, "%p: ", KeGetCurrentProcess()); + memcpy(Memory, PidBuffer, OFFSET); + #endif DbgPrintStringLocked(Memory); + } MmFreePool(Memory); return Status; diff --git a/boron/source/ke/i386/misc.asm b/boron/source/ke/i386/misc.asm index 3d72136b..afef4c83 100644 --- a/boron/source/ke/i386/misc.asm +++ b/boron/source/ke/i386/misc.asm @@ -86,14 +86,8 @@ MmProbeAddressSub: pop edi pop ebp ret - -; void MmProbeAddressSubEarlyReturn() -; Returns early from MmProbeAddressSub and MmSafeCopySub. Called by the invalid page fault handler. -global MmProbeAddressSubEarlyReturn -MmProbeAddressSubEarlyReturn: - ret -; int MmSafeCopySub(void* Address, void* Source, size_t Length); +; int MmSafeCopySub(void* Destination, void* Source, size_t Length); global MmSafeCopySub MmSafeCopySub: push ebp @@ -107,6 +101,12 @@ MmSafeCopySub: mov ecx, [ebp + 16] rep movsb xor eax, eax + ; fall through + +; void MmProbeAddressSubEarlyReturn() +; Returns early from MmProbeAddressSub and MmSafeCopySub. Called by the invalid page fault handler. +global MmProbeAddressSubEarlyReturn +MmProbeAddressSubEarlyReturn: pop esi pop edi pop ebp diff --git a/boron/source/mm/amd64/ptfree.c b/boron/source/mm/amd64/ptfree.c index 42f09247..f91b927e 100644 --- a/boron/source/mm/amd64/ptfree.c +++ b/boron/source/mm/amd64/ptfree.c @@ -84,7 +84,7 @@ void MiFreeUnusedMappingLevelsInCurrentMap(uintptr_t StartVa, size_t SizePages) continue; PMMPTE SubPte = MiGetSubPteAddress(Pte); - if (MmpIsPteListCompletelyEmpty(SubPte)) + if (MmpIsPteListCompletelyEmpty(SubPte) && ShouldUnmapPML4) { MmFreePhysicalPage((*Pte & MI_PML_ADDRMASK) >> 12); *Pte = 0; diff --git a/boron/source/mm/fault.c b/boron/source/mm/fault.c index 26bcdbe2..9f744781 100644 --- a/boron/source/mm/fault.c +++ b/boron/source/mm/fault.c @@ -198,7 +198,7 @@ static BSTATUS MmpHandleFaultCommittedMappedPage( goto Exit; } - PFDbgPrint("%s: hooray! page fault fulfilled by cached fetch %p", __func__, PtePtr); + PFDbgPrint("%s: hooray! page fault fulfilled by cached fetch %p", __func__, Va); goto Exit; } diff --git a/boron/source/mm/heap.c b/boron/source/mm/heap.c index b43d29a3..56b80c11 100644 --- a/boron/source/mm/heap.c +++ b/boron/source/mm/heap.c @@ -310,7 +310,11 @@ void MmDebugDumpHeap() PRBTREE_ENTRY Entry = GetFirstEntryRbTree(&Heap->Tree); +#ifdef IS_64_BIT DbgPrint("HeapPtr Start End"); +#else + DbgPrint("HeapPtr Start End"); +#endif if (!Entry) DbgPrint("There are no heap entries."); diff --git a/boron/source/mm/i386/ptfree.c b/boron/source/mm/i386/ptfree.c index dfa890f3..9ea36888 100644 --- a/boron/source/mm/i386/ptfree.c +++ b/boron/source/mm/i386/ptfree.c @@ -14,9 +14,8 @@ Module name: ***/ #include "../mi.h" -#define PTES_PER_LEVEL (PAGE_SIZE / sizeof(MMPTE)) // 1024 -#define PAGE_MAP_LEVELS 8 -#define PTES_COVERED_BY_PML2 PTES_PER_LEVEL +#define PTES_PER_LEVEL (PAGE_SIZE / sizeof(MMPTE)) // 1024 +#define PTES_COVERED_BY_ONE_PT (PTES_PER_LEVEL * PTES_PER_LEVEL) // 1048576 // Gets an address down the tree. // @@ -50,104 +49,31 @@ static bool MmpIsPteListCompletelyEmpty(PMMPTE Pte) return AllZeroes; } -static bool MmpFreeUnusedMappingLevelsInCurrentMapPML(PMMPTE Pte, int RecursionCount); - // NOTE: StartVa and SizePages are only roughly followed. // // NOTE: The address space lock of the process *must* be held. -// This also issues a TLB shootdown covering the affected area. void MiFreeUnusedMappingLevelsInCurrentMap(uintptr_t StartVa, size_t SizePages) { - bool ShouldUnmapPML2; + if (StartVa >= MM_KERNEL_SPACE_BASE) + return; + MMADDRESS_CONVERT Address; PMMPTE Pte; - ShouldUnmapPML2 = true; Address.Long = StartVa; + Pte = (PMMPTE) MI_PML2_LOCATION + Address.Level2Index; - Pte = (PMMPTE) MI_PML1_LOCATION + Address.Level2Index; - - // Currently, we can only operate in user space. This is because - // in kernel space, the PML2 pages may never be deallocated. - if (Address.Level2Index >= PTES_PER_LEVEL / 2) - ShouldUnmapPML2 = false; - - for (size_t PageNumber = 0; - PageNumber < SizePages; - PageNumber += PTES_COVERED_BY_PML2, - ++Pte) - { - if (~*Pte & MM_PTE_PRESENT) - continue; - - PMMPTE SubPte = MiGetSubPteAddress(Pte); - if (MmpIsPteListCompletelyEmpty(SubPte)) - { - MmFreePhysicalPage((*Pte & MI_PML_ADDRMASK) >> 12); - *Pte = 0; - continue; - } - - if (!MmpFreeUnusedMappingLevelsInCurrentMapPML(SubPte, PAGE_MAP_LEVELS - 1)) - continue; - - // Returned true, so this is now ready to free. - if (ShouldUnmapPML2) - { - MmFreePhysicalPage((*Pte & MI_PML_ADDRMASK) >> 12); - *Pte = 0; - } - } -} - -static bool MmpFreeUnusedMappingLevelsInCurrentMapPML(PMMPTE Pte, int MapLevel) -{ - if (MapLevel <= 1) - { - // We're on the last level. - for (size_t i = 0; i < PTES_PER_LEVEL; ++i, ++Pte) - { - if (*Pte) - { - // The PTE exists, check if it was decommitted though. - if ((~*Pte & MM_PTE_PRESENT) && (*Pte & MM_DPTE_DECOMMITTED)) - continue; - - // This mapping level is busy. - return false; - } - } - - return true; - } - - bool FreeParent = true; - - // Walk the PML. - for (size_t i = 0; i < PTES_PER_LEVEL; ++i, ++Pte) + // Scan each page table in the range. + for (size_t i = 0; i < SizePages; i += PTES_COVERED_BY_ONE_PT, ++Pte) { - if (~*Pte & MM_PTE_PRESENT) + if (~(*Pte) & MM_PTE_PRESENT) continue; PMMPTE SubPte = MiGetSubPteAddress(Pte); - if (MmpIsPteListCompletelyEmpty(SubPte)) { MmFreePhysicalPage((*Pte & MI_PML_ADDRMASK) >> 12); *Pte = 0; - continue; - } - - if (!MmpFreeUnusedMappingLevelsInCurrentMapPML(SubPte, MapLevel - 1)) - { - FreeParent = false; - continue; } - - // Returned true, so this is now ready to free. - MmFreePhysicalPage((*Pte & MI_PML_ADDRMASK) >> 12); - *Pte = 0; } - - return FreeParent; } diff --git a/boron/source/mm/mdl.c b/boron/source/mm/mdl.c index 1cc0d525..9da4694d 100644 --- a/boron/source/mm/mdl.c +++ b/boron/source/mm/mdl.c @@ -428,7 +428,7 @@ void MmCopyFromMdl(PMDL Mdl, uintptr_t Offset, void* DestinationBuffer, size_t S MmBeginUsingHHDM(); char* PageDest = MmGetHHDMOffsetAddr(MmPFNToPhysPage(Mdl->Pages[PageIndex])); - memcpy(DestBufferChr, PageDest + PageOffs, CopyAmount); + memcpy(Temporary, PageDest + PageOffs, CopyAmount); MmEndUsingHHDM(); #ifdef IS_32_BIT diff --git a/boron/source/mm/poolsup.c b/boron/source/mm/poolsup.c index 9cf75893..df6d6958 100644 --- a/boron/source/mm/poolsup.c +++ b/boron/source/mm/poolsup.c @@ -116,7 +116,7 @@ static LIST_ENTRY MmpPoolList; // 0x80000000 - 0xC0000000 and 0xD0000000 - 0xF0000000 #define MI_POOL_LOG2_SIZE (30) -#define MI_POOL_LOG2_SIZE_2ND (29) +#define MI_POOL_LOG2_SIZE_2ND (28) #else @@ -124,9 +124,40 @@ static LIST_ENTRY MmpPoolList; #endif +#ifdef IS_32_BIT + +void MiInitializeRootPageTable(int Idx) +{ + PMMPTE Pte = (PMMPTE)MI_PML2_LOCATION + Idx; + MMPFN Pfn = MmAllocatePhysicalPage(); + + if (Pfn == PFN_INVALID) + KeCrashBeforeSMPInit("MiCalculatePoolHeaderPte ERROR: Out of memory!"); + + *Pte = MmPFNToPhysPage(Pfn) | MM_PTE_PRESENT | MM_PTE_READWRITE; +} + +void MiInitializePoolPageTables() +{ + int Size1 = 1 << (MI_POOL_LOG2_SIZE - 22); + int Size2 = 1 << (MI_POOL_LOG2_SIZE_2ND - 22); + + for (int i = MI_GLOBAL_AREA_START; i < MI_GLOBAL_AREA_START + Size1; i++) + MiInitializeRootPageTable(i); + + for (int i = MI_GLOBAL_AREA_START_2ND; i < MI_GLOBAL_AREA_START_2ND + Size2; i++) + MiInitializeRootPageTable(i); +} + +#endif + INIT void MiInitPool() { +#ifdef IS_32_BIT + MiInitializePoolPageTables(); +#endif + InitializeListHead(&MmpPoolList); PMIPOOL_ENTRY Entry = MiCreatePoolEntry(); diff --git a/boron/source/mm/view.c b/boron/source/mm/view.c index 4a4f83dc..dd949b9c 100644 --- a/boron/source/mm/view.c +++ b/boron/source/mm/view.c @@ -35,11 +35,7 @@ BSTATUS MmMapViewOfFile( PMMVAD_LIST VadList; void* BaseAddress = *BaseAddressInOut; - - // Remove the base address' offset inside a page and add it to the region size. - size_t VaOffset = (size_t)((uintptr_t)BaseAddress & (PAGE_SIZE - 1)); BaseAddress = (void*)((uintptr_t)BaseAddress & ~(PAGE_SIZE - 1)); - ViewSize += VaOffset; size_t PageOffset = SectionOffset & (PAGE_SIZE - 1); size_t ViewSizePages = (ViewSize + PageOffset + PAGE_SIZE - 1) / PAGE_SIZE; diff --git a/user/init/source/main.c b/user/init/source/main.c index 20162004..7c77f8ac 100644 --- a/user/init/source/main.c +++ b/user/init/source/main.c @@ -16,8 +16,8 @@ int _start() //RunTest2(); //RunTest3(); //RunTest4(); - //RunTest5(); - RunTest6(); + RunTest5(); + //RunTest6(); OSExitProcess(0); } diff --git a/user/libboron/source/init.c b/user/libboron/source/init.c index 47959ee7..c93da6e9 100644 --- a/user/libboron/source/init.c +++ b/user/libboron/source/init.c @@ -96,7 +96,7 @@ BSTATUS OSDLLMapElfFile( int FileKind ) { - BSTATUS Status; + BSTATUS Status = STATUS_SUCCESS; IO_STATUS_BLOCK Iosb; ELF_HEADER ElfHeader; ELF_PROGRAM_HEADER ElfProgramHeader; @@ -139,15 +139,6 @@ BSTATUS OSDLLMapElfFile( if (NeedReadAndMapFile) { - DbgPrint( - "Iosb: %p, Handle: %d, Offset: %lld, Buffer: %p, Size: %zu", - (void*) &Iosb, - (int) Handle, - (uint64_t) 0, - (void*) &ElfHeader, - (size_t) sizeof ElfHeader - ); - Status = OSReadFile(&Iosb, Handle, 0, &ElfHeader, sizeof ElfHeader, 0); if (FAILED(Status)) { @@ -240,7 +231,7 @@ BSTATUS OSDLLMapElfFile( } // Found it. - LdrDbgPrint("OSDLL: %s's image base is %p", Name, BaseAddress); + LdrDbgPrint("OSDLL: %s's image base is %p (size is %zu)", Name, BaseAddress, RegionSize); ImageBase = (uintptr_t) BaseAddress; // TODO: Now, free the reserved memory. But we may need to avoid a race condition @@ -365,7 +356,7 @@ BSTATUS OSDLLMapElfFile( void* OldAddress = Address; #endif - LdrDbgPrint("OSDLL: Initialized data at offset %p.", OldAddress); + LdrDbgPrint("OSDLL: Initialized data at offset %p, size %zu.", OldAddress, ElfProgramHeader.SizeInMemory); // Initialized data. Status = OSMapViewOfObject( @@ -383,7 +374,9 @@ BSTATUS OSDLLMapElfFile( if (FAILED(Status)) { DbgPrint( - "OSDLL: Failed to map initialized data for the program: %s (%d)", + "OSDLL: Failed to map initialized data for the program: %p %zu %s (%d)", + OldAddress, + ElfProgramHeader.SizeInMemory, RtlGetStatusString(Status), Status ); @@ -601,6 +594,7 @@ BSTATUS OSDLLRunImage(PPEB Peb, ELF_ENTRY_POINT2* OutEntryPoint) Status, RtlGetStatusString(Status) ); + return Status; } // Load the DLLs in the queue. @@ -656,6 +650,7 @@ BSTATUS OSDLLRunImage(PPEB Peb, ELF_ENTRY_POINT2* OutEntryPoint) // The interpreter knows how to relocate themselves and has already done so. if (LoadedImage->FileKind != FILE_KIND_INTERPRETER) { + LdrDbgPrint("OSDLL: Linking %s, file kind: %d...", LoadedImage->Name, LoadedImage->FileKind); if (!RtlPerformRelocations(&LoadedImage->DynamicInfo, LoadedImage->ImageBase)) { DbgPrint("OSDLL: Cannot perform relocations on module %s.", LoadedImage->Name); From e612aecc2b3904f2cf8e5ea46331c823565c5d27 Mon Sep 17 00:00:00 2001 From: iProgramInCpp Date: Sun, 26 Oct 2025 21:17:45 +0200 Subject: [PATCH 44/44] * Ke,Hal: Clean up logs --- boron/source/build_number | 2 +- boron/source/ke/i386/traps.c | 1 - drivers/hali386/source/pic.c | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/boron/source/build_number b/boron/source/build_number index 8026d9d6..0e332519 100644 --- a/boron/source/build_number +++ b/boron/source/build_number @@ -1 +1 @@ -647 +650 diff --git a/boron/source/ke/i386/traps.c b/boron/source/ke/i386/traps.c index 9e7d57a9..2f66725a 100644 --- a/boron/source/ke/i386/traps.c +++ b/boron/source/ke/i386/traps.c @@ -252,7 +252,6 @@ void KiSetupIdt() void KeRegisterInterrupt(int Vector, PKINTERRUPT_HANDLER Handler) { - DbgPrint("Setting trap handler to %p", Handler); KIPL Ipl; KeAcquireSpinLock(&KiTrapLock, &Ipl); KiTrapCallList[Vector] = Handler; diff --git a/drivers/hali386/source/pic.c b/drivers/hali386/source/pic.c index 82014166..6c28f751 100644 --- a/drivers/hali386/source/pic.c +++ b/drivers/hali386/source/pic.c @@ -55,7 +55,6 @@ HAL_API void HalEndOfInterrupt(int InterruptNumber) HAL_API void HalPicRegisterInterrupt(uint8_t Vector, KIPL Ipl) { - DbgPrint("HalPicRegisterInterrupt: %d, %d", Vector, Ipl); if (Vector < PIC_INTERRUPT_BASE || Vector >= PIC_INTERRUPT_BASE + 16) { DbgPrint("HalRegisterInterrupt: Dropping vector %zu", Vector);