diff --git a/Makefile b/Makefile index b965aaa..7c5585d 100644 --- a/Makefile +++ b/Makefile @@ -47,14 +47,12 @@ OS_BIN := mOS.bin C_FILES = $(shell find ./ -name '*.[ch]') -OBJ_NAMES := src/os/main.o src/os/test.o os_entry.o src/lib/video/VGA_text.o \ +OBJ_NAMES := src/os/main.o src/os/test.o os_entry.o src/os/paging.o \ src/os/hard/idt.o src/os/hard/except.o src/os/hard/pic.o \ src/lib/device/serial.o src/lib/device/ps2.o src/lib/device/keyboard.o \ src/lib/container/ring_buffer.o \ src/lib/stdlib/stdio.o src/lib/stdlib/stdlib.o src/lib/stdlib/string.o \ - src/lib/pit/pit.o src/lib/device/key_handlers.o - - + src/lib/pit/pit.o src/lib/device/key_handlers.o src/lib/video/VGA_text.o .PHONY: clean qemu test all: $(OS_BIN) diff --git a/docs/os/paging.md b/docs/os/paging.md new file mode 100644 index 0000000..420c46d --- /dev/null +++ b/docs/os/paging.md @@ -0,0 +1,50 @@ +# Paging + +## MMU and You + +The Memory Management Unit (MMU) is responsible for translating "virtual" addresses to "physical" addresses. A "physical" address is an address that maps directly to a system's memory. On the other hand, a "virtual" address is an address that indirectly maps to a system's memory. When addresses are identity mapped, a virtual address is equivalent to a physical address (in that context). + +The MMU will always translate addresses once it is enabled (via `cr0`). However, this creates overhead for one of the most frequent operations. To minimize this overhead, the MMU maintains a cache referred to as the Translation Lookaside Buffer (TLB). Since the TLB doesn't constantly monitor if the translation tables are modified, it must be invalidated whenever a change is made. This takes the form of either `mov eax, cr3; mov cr3, eax` or `invlpg eax`. The first essentially sets the active translation context to itself, while `invlpg` is a dedicated instruction for invalidating just 1 page (although it may invalidate more). + +## Page Directory + +A Page Directory is a translation context. There can be many, and multiprocess operating systems may give each process its own page directory. However, only one is active at a time, the address of which can be found in `cr3`. The structure itself is 4KiB and consists of 1024 4-byte entries (for 32-bit x86). Each entry consists of 20-bits of address pointing to a page aligned Page Table, several flags, and some available bits. The important flags are Present, Accessed, Cache Disable, Write Through, Read/Write, User/Supervisor. + +- Present signifies that the table exists, if this is false, when the MMU tries to access this entry it will cause a page fault. +- Accessed is set to 1 whenever the MMU accesses that entry, this value should be reset by the OS if the OS intends to use it. +- Cache Disable, as the name implies, prevents the entry from being cached into the TLB. +- Write Through is when data is written to both cache and main memory at the same time, when this flag is 0 write back is used instead. Write back is when cache is used until the cache is invalidated where it then writes to main memory. +- Read/Write and User/Supervisor are permission flags that control who and what is allowed to interact with certain pages. + +## Page Table + +A Page Table is what determines which physical address is mapped to a virtual address. There are 1024 per page directory, and similarly the structure itself is 4KiB. The structure MUST be page aligned (since the page directory only stores the top 20-bits). The structure consists of 1024 4-byte entries (for 32-bit x86). Each entry consists of 20-bits of address pointing to a 4KiB page in physical memory, several flags, and some available bits. The important flags are the same as the Page Directory, with the addition of the Dirty bit. The dirty bit is set whenever the page is written to, the OS should reset this bit if it wishes to use it. + +### Virtual Address Decomposition + +A virtual address is comprised of 3 parts, a table index, a entry index, and an offset. The table and entry indexes are 10-bits giving them a range of [0, 1024). The offset is 12-bits, which is [0, 4096). + +The following diagram shows a decomposition. The construction of a physical address requires having a page directory populated with tables. + +![Decomposition](vaddr_decomp.png) + +### Mappings + +(In the context of a single directory) + +Identity mapping is when virtual addresses are the same as physical addresses. + +1:1 mapping is when each virtual address maps uniquely to a physical address. Identity mapping is a 1:1 mapping. + +N:1 mapping is when multiple virtual addresses map to a phsyical address. + +1:N mapping can only occur with multiple directories. + +#### Further Reading + +[Paging](https://wiki.osdev.org/Paging) +[Page Tables](https://wiki.osdev.org/Page_Tables) +[Identity Paging](https://wiki.osdev.org/Identity_Paging) +[MMU](https://wiki.osdev.org/Memory_Management_Unit) +[TLB](https://wiki.osdev.org/TLB) +[invlpg](https://www.felixcloutier.com/x86/invlpg) diff --git a/docs/os/vaddr_decomp.png b/docs/os/vaddr_decomp.png new file mode 100644 index 0000000..38b6ff1 Binary files /dev/null and b/docs/os/vaddr_decomp.png differ diff --git a/src/os/main.c b/src/os/main.c index 8172a40..6bbedf0 100644 --- a/src/os/main.c +++ b/src/os/main.c @@ -1,12 +1,14 @@ #include "device/ps2.h" #include "device/serial.h" #include "hard/idt.h" +#include "paging.h" #include "pit/pit.h" #include "test.h" #include "video/VGA_text.h" int os_main() { makeInterruptTable(); + initPaging(); init_pit(); serialInit(); ps2Init(); diff --git a/src/os/paging.c b/src/os/paging.c new file mode 100644 index 0000000..1fdda44 --- /dev/null +++ b/src/os/paging.c @@ -0,0 +1,156 @@ +#include "paging.h" + +#include "stdlib/string.h" + +// room for 3 page tables (12 MiB of mapped memory) +#define IDENTITY_PT_BASE 0x92000 +#define IDENTITY_PT_LIMIT 0x95000 +#define TABLE_COUNT ((IDENTITY_PT_LIMIT - IDENTITY_PT_BASE) / 0x1000) + +#define MiB4 0x400000 + +PageDirectory *idendirectory = (PageDirectory *)(ID_PAGE_DIRECTORY_BASE); + +bool pageTablePresent(PageDirectoryEntry tableEntry) { + return tableEntry & ENTRY_PRESENT; +} + +bool pageEntryPresent(PageTableEntry entry) { + // mask out all but first bit + return entry & ENTRY_PRESENT; +} + +void setEntryAddr(PageTableEntry *entry, const void *addr) { + if (entry == NULL) + return; + + *entry = ((uint32_t)(addr)&ENTRY_ADDR) | (*entry & ~(ENTRY_ADDR)); +} + +void setActivePageDir(PageDirectory *dir) { + if (dir == NULL) + dir = idendirectory; + + __asm__ volatile("mov %0, %%cr3" : : "a"(dir)); +} + +PageDirectory *getActivePageDir(void) { + PageDirectory *dir = NULL; + + __asm__ volatile("mov %%cr3, %0" : "=r"(dir)); + + return dir; +} + +void resetTLB(void) { + // setting the active directory to the current updates the TLB + setActivePageDir(getActivePageDir()); +} + +#define PAGE_TABLE_OFFSET 22 +#define PAGE_ENTRY_OFFSET 12 + +// highest 10 bits +uint16_t vaddrDirectoryIdx(const void *vaddr) { + return (uint32_t)(vaddr) >> PAGE_TABLE_OFFSET; +} + +// middle 10 bits +uint16_t vaddrEntryIdx(const void *vaddr) { + // shifted right 12 then 10-bit mask + return ((uint32_t)(vaddr) >> PAGE_ENTRY_OFFSET) & 0x3ff; +} + +// low 12 bits +uint16_t vaddrOffset(const void *vaddr) { + // 12-bit mask + return (uint32_t)(vaddr)&0xfff; +} + +void *toVaddr(uint16_t dirIdx, uint16_t entryIdx, uint16_t offset) { + uint32_t vaddr = offset; + vaddr |= (uint32_t)(entryIdx) << PAGE_ENTRY_OFFSET; + vaddr |= (uint32_t)(dirIdx) << PAGE_TABLE_OFFSET; + return (void *)(vaddr); +} + +PageDirectoryEntry *vaddrDirEntry(PageDirectory *directory, const void *vaddr) { + if (directory == NULL) + directory = getActivePageDir(); + + uint16_t tableidx = vaddrDirectoryIdx(vaddr); + return &directory->entries[tableidx]; +} + +PageTableEntry *vaddrTableEntry(PageDirectory *directory, const void *vaddr) { + // this will never be null (unless something really bad happened) + PageDirectoryEntry *dirEntry = vaddrDirEntry(directory, vaddr); + PageTable *table = (PageTable *)((*dirEntry) & ENTRY_ADDR); + + if (table == NULL) + return NULL; + + uint16_t entryidx = vaddrEntryIdx(vaddr); + return &table->entries[entryidx]; +} + +void *vaddrToPaddr(PageDirectory *dir, const void *vaddr) { + + if (dir == NULL) + dir = getActivePageDir(); + + // get and verify page entry + PageTableEntry *entry = vaddrTableEntry(dir, vaddr); + if (entry == NULL) + return NULL; + + uint32_t paddr = vaddrOffset(vaddr); + + // apply offset + return (void *)(paddr + ((*entry) & ENTRY_ADDR)); +} + +// identity maps the entire table at directory entry idx +void identityMapTable(PageDirectory *directory, uint16_t idx, uint32_t flags) { + PageTable *table = (PageTable *)(directory->entries[idx] & ENTRY_ADDR); + + // 4GiB per directory + // 4MiB per table + uint32_t baseAddr = idx * MiB4; + + for (uint32_t page_idx = 0; page_idx < PAGE_ENTRY_COUNT; ++page_idx) { + PageTableEntry entry = flags & ~(ENTRY_ADDR); + + // 4KiB per entry + entry |= (baseAddr + page_idx * PAGE_SIZE) & ENTRY_ADDR; + table->entries[page_idx] = entry; + } +} + +// preconditions, idx < PAGE_ENTRY_COUNT, table is 4KiB aligned +void addTableToDirectory(PageDirectory *directory, uint16_t idx, + PageTable *table, uint32_t flags) { + PageDirectoryEntry entry = flags & ~(ENTRY_ADDR); + entry |= (uint32_t)(table)&ENTRY_ADDR; + directory->entries[idx] = entry; +} + +void initPaging(void) { + // clear the memory (essentially say no page tables exist) + memset(idendirectory, 0, PAGE_ENTRY_COUNT * sizeof(PageDirectoryEntry)); + + // identity map 12MiB and setup directory + for (uint16_t idx = 0; idx < TABLE_COUNT; ++idx) { + PageTable *addr = (PageTable *)((idx * PAGE_SIZE) + IDENTITY_PT_BASE); + memset(addr, 0, PAGE_ENTRY_COUNT * sizeof(PageTableEntry)); + addTableToDirectory(idendirectory, idx, addr, DEFAULT_ENTRY_FLAGS); + identityMapTable(idendirectory, idx, DEFAULT_ENTRY_FLAGS); + } + + setActivePageDir(idendirectory); + + // enable paging flags in cr0 + __asm__ volatile("mov %cr0, %eax \n\t" + "or $0x80000001, %eax\n\t" + "mov %eax, %cr0"); +} \ No newline at end of file diff --git a/src/os/paging.h b/src/os/paging.h new file mode 100644 index 0000000..5ea1598 --- /dev/null +++ b/src/os/paging.h @@ -0,0 +1,114 @@ +#ifndef PAGING_H +#define PAGING_H + +#include +#include + +// unfortunately C's bitfields can't provide the packing we need +typedef uint32_t PageDirectoryEntry; +typedef uint32_t PageTableEntry; + +/*The page directory and it's tables must be 4KiB aligned (0x1000) + * 0x90000 is the start of the stack, in other words, + * the areas from 0x1000 to 0x90000 are in use! + * But there is a nice open region we can use from 0x91000 - 0x9f000 + * (We technically have until 0x9fc00 before we enter ExBIOS data) + */ + +#define ID_PAGE_DIRECTORY_BASE 0x91000 + +#define PAGE_ENTRY_COUNT 1024 +#define PAGE_SIZE 0x1000 + +// The entries share flags! +#define ENTRY_PRESENT 0b000000001 +#define ENTRY_RW 0b000000010 +#define ENTRY_US 0b000000100 +#define ENTRY_PWT 0b000001000 +#define ENTRY_PCD 0b000010000 +#define ENTRY_ACESSED 0b000100000 + +// keep this flag should always be 0 +#define ENTRY_PS 0b010000000 + +// only for page table entries, available for OS use in page directory +#define ENTRY_GLOBAL 0b100000000 +#define ENTRY_DIRTY 0b001000000 + +// these bits are also available for the OS +#define ENTRY_AVL 0b111000000000 + +// bits 12-31 +#define ENTRY_ADDR 0xfffff000 + +// default entry is present, read/writable, and accessable by user and +// supervisor +#define DEFAULT_ENTRY_FLAGS (ENTRY_PRESENT | ENTRY_RW | ENTRY_US) + +// note: these structures are lacking attrib packed +typedef struct { + PageDirectoryEntry entries[PAGE_ENTRY_COUNT]; +} PageDirectory; + +// note, this structure must be 4KiB aligned +typedef struct { + PageTableEntry entries[PAGE_ENTRY_COUNT]; +} PageTable; + +bool pageTablePresent(PageDirectoryEntry tableEntry); +bool pageEntryPresent(PageTableEntry entry); + +// sets the entry's physical page to that of addr +void setEntryAddr(PageTableEntry *entry, const void *addr); + +// NOTE: IF PageDirectory* IS NULL IT USES THE CURRENT DIRECTORY (unless +// otherwise specified) + +// sets the active page directory, if NULL uses the identity directory +void setActivePageDir(PageDirectory *dir); + +// gets the current page dir from cr3 +PageDirectory *getActivePageDir(void); + +/* + * resets the translation lookaside buffer + * the TLB needs to be reset whenever an entry is modified + */ +void resetTLB(void); + +// adds a table to a directory, TLB must be reset manually if directory is the +// current page directory +void addTableToDirectory(PageDirectory *directory, uint16_t idx, + PageTable *table, uint32_t flags); + +// translation helpers +uint16_t vaddrDirectoryIdx(const void *vaddr); +uint16_t vaddrEntryIdx(const void *vaddr); +uint16_t vaddrOffset(const void *vaddr); + +// translates table indexes and offset to virtual address +void *toVaddr(uint16_t dirIdx, uint16_t tableIdx, uint16_t offset); + +// returns the associated directory entry of vaddr, never null +PageDirectoryEntry *vaddrDirEntry(PageDirectory *directory, const void *vaddr); + +// returns the associated table entry of vaddr, null if invalid/unmapped address +PageTableEntry *vaddrTableEntry(PageDirectory *directory, const void *vaddr); + +// identity maps the PageTable at directory index idx +void identityMapTable(PageDirectory *directory, uint16_t idx, uint32_t flags); + +/* + * Converts virtual address to physical address + * (according to the current page table/directory) + * returns NULL when the address is invalid/unmapped + */ +void *vaddrToPaddr(PageDirectory *dir, const void *vaddr); + +/* + * enables paging and identity maps the kernel (1st MiB) + * as well as identity mapping 1MiB - 12MiB + */ +void initPaging(void); + +#endif \ No newline at end of file diff --git a/tests/expected/os/paging.expect b/tests/expected/os/paging.expect new file mode 100644 index 0000000..bedff05 --- /dev/null +++ b/tests/expected/os/paging.expect @@ -0,0 +1,5 @@ +test_composition done +test_identity done +test_swap done +test_modify_in_place done +test_entry_not_present done diff --git a/tests/src/os/paging.c b/tests/src/os/paging.c new file mode 100644 index 0000000..9915d9f --- /dev/null +++ b/tests/src/os/paging.c @@ -0,0 +1,232 @@ +#include "../test_helper.h" + +#include <../os/hard/idt.h> +#include <../os/paging.h> + +// 1st megabyte is a safe place to read from, but not to write to +#define MiB1 0x100000 + +// 3rd megabyte is safe to both read and write to +#define MiB3 (MiB1 * 3) + +// placed so that the test goes over the table 0 and table 1 boundary +#define IDENT_LOCATION ((MiB1 * 4) - PAGE_SIZE) + +// 4MiB +#define BOUND ((void *)(MiB1 * 4)) + +void assert_identity(PageDirectory *dir, void *addr) { + ASSERT(vaddrToPaddr(dir, addr) == addr); +} + +void test_identity(PageDirectory *idDir) { + + // test 2 whole pages (and first byte of 3rd) + for (int i = 0; i <= PAGE_SIZE * 2; ++i) { + assert_identity(idDir, (void *)(IDENT_LOCATION + i)); + } + + char done[] = "test_identity done\n"; + serialWrite(COM1, (uint8_t *)(done), sizeof(done) - 1); +} + +void assert_compose(uint16_t didx, uint16_t tidx, uint16_t off) { + void *addr = toVaddr(didx, tidx, off); + + ASSERT(addr == (void *)((didx * 0x400000) + (tidx * 0x1000) + off)); + + ASSERT(vaddrOffset(addr) == off); + ASSERT(vaddrEntryIdx(addr) == tidx); + ASSERT(vaddrDirectoryIdx(addr) == didx); +} + +void test_composition(void) { + + assert_compose(0, 0, 0); + assert_compose(0, 0, 0xfff); + assert_compose(1023, 0, 0); + assert_compose(0, 1023, 0); + assert_compose(1023, 1023, 0xfff); + assert_compose(123, 456, 789); + + char done[] = "test_composition done\n"; + serialWrite(COM1, (uint8_t *)(done), sizeof(done) - 1); +} + +void test_swap_page(PageDirectory *base) { + // steal the 3MiB page and 3 more + PageDirectory *newDir = (PageDirectory *)(MiB3); + PageTable *tables = (PageTable *)(MiB3 + sizeof(PageDirectory)); + + // setup identity paged dir + addTableToDirectory(newDir, 0, tables, DEFAULT_ENTRY_FLAGS); + addTableToDirectory(newDir, 1, tables + 1, DEFAULT_ENTRY_FLAGS); + addTableToDirectory(newDir, 2, tables + 2, DEFAULT_ENTRY_FLAGS); + + identityMapTable(newDir, 0, DEFAULT_ENTRY_FLAGS); + identityMapTable(newDir, 1, DEFAULT_ENTRY_FLAGS); + identityMapTable(newDir, 2, DEFAULT_ENTRY_FLAGS); + + // first and second entry of page table at index 1 + PageTableEntry *entry1 = vaddrTableEntry(newDir, BOUND); + PageTableEntry *entry2 = vaddrTableEntry(newDir, BOUND + PAGE_SIZE); + ASSERT(entry1 == &tables[1].entries[0]); + ASSERT(entry2 == &tables[1].entries[1]); + + // map 4th MiB to 4th MiB + 1 page and vice versa + setEntryAddr(entry1, BOUND + PAGE_SIZE); + setEntryAddr(entry2, BOUND); + ASSERT(vaddrToPaddr(newDir, BOUND) == BOUND + PAGE_SIZE); + ASSERT(vaddrToPaddr(newDir, BOUND + PAGE_SIZE) == BOUND); + + uint32_t *magicLocation = (uint32_t *)(BOUND); + uint32_t *boringLocation = (uint32_t *)(BOUND + PAGE_SIZE); + + // setup data + magicLocation[0] = 0xdeadbeef; + boringLocation[0] = 0xc0decafe; + + // swap to new directory + setActivePageDir(newDir); + + // currently the two locations should be flipped + ASSERT(magicLocation[0] == 0xc0decafe); + ASSERT(boringLocation[0] == 0xdeadbeef); + magicLocation[0] = 0xdeadbeef; + boringLocation[0] = 0xc0decafe; + + // swap back + setActivePageDir(base); + + // they swapped!!! + ASSERT(magicLocation[0] == 0xc0decafe); + ASSERT(boringLocation[0] == 0xdeadbeef); + + char done[] = "test_swap done\n"; + serialWrite(COM1, (uint8_t *)(done), sizeof(done) - 1); +} + +void test_modify_in_place(PageDirectory *base) { + // steal the 3MiB page and 3 more + PageDirectory *newDir = (PageDirectory *)(MiB3); + PageTable *tables = (PageTable *)(MiB3 + sizeof(PageDirectory)); + + // setup identity paged dir + addTableToDirectory(newDir, 0, tables, DEFAULT_ENTRY_FLAGS); + addTableToDirectory(newDir, 1, tables + 1, DEFAULT_ENTRY_FLAGS); + addTableToDirectory(newDir, 2, tables + 2, DEFAULT_ENTRY_FLAGS); + + identityMapTable(newDir, 0, DEFAULT_ENTRY_FLAGS); + identityMapTable(newDir, 1, DEFAULT_ENTRY_FLAGS); + identityMapTable(newDir, 2, DEFAULT_ENTRY_FLAGS); + + setActivePageDir(newDir); + + // first and second entry of page table at index 1 + PageTableEntry *entry1 = vaddrTableEntry(newDir, BOUND); + PageTableEntry *entry2 = vaddrTableEntry(newDir, BOUND + PAGE_SIZE); + ASSERT(entry1 == &tables[1].entries[0]); + ASSERT(entry2 == &tables[1].entries[1]); + + // volatile is extremely important here, or else gcc will optimize our tests + // to fail + volatile uint32_t *baseLocation = (uint32_t *)(BOUND); + volatile uint32_t *magicLocation = (uint32_t *)(BOUND + PAGE_SIZE); + + // setup values + *baseLocation = 0xdeadbeef; + *magicLocation = 123; + ASSERT(*baseLocation == 0xdeadbeef); + ASSERT(*magicLocation == 123); + + // map 4th MiB + 1 page to 4th MiB page + setEntryAddr(entry2, BOUND); + resetTLB(); // ensure change is seen by MMU + + *magicLocation = 0x1337; + ASSERT(*baseLocation == 0x1337); + + // map back + setEntryAddr(entry2, BOUND + PAGE_SIZE); + resetTLB(); + + ASSERT(*magicLocation == 123); + + // swap back + setActivePageDir(base); + + char done[] = "test_modify_in_place done\n"; + serialWrite(COM1, (uint8_t *)(done), sizeof(done) - 1); +} + +volatile uint32_t errCode = -1; + +void test_handler(isr_registers_t *regs) { + errCode = regs->err_code; + + // We are manually incrementing the instruction pointer. + // This is to avoid faulting on the same instruction forever. + // But it can cause some unexpected things to happen. + regs->eip++; +} + +void assert_on_fault_code(uint32_t expected) { + InterruptState iprev = disableInterrupts(); + uint32_t got = errCode; + errCode = -1; + setInterrupts(iprev); + + // ASSERT must be called with interrupts enabled! + ASSERT_M(expected == got, "Expected %i but got %i as fault error code.", + expected, got); +} + +void test_entry_not_present(PageDirectory *base) { + // steal the 3MiB page and 2 more + PageDirectory *newDir = (PageDirectory *)(MiB3); + PageTable *tables = (PageTable *)(MiB3 + sizeof(PageDirectory)); + + // setup identity paged dir + addTableToDirectory(newDir, 0, tables, DEFAULT_ENTRY_FLAGS); + addTableToDirectory(newDir, 1, tables + 1, DEFAULT_ENTRY_FLAGS); + + identityMapTable(newDir, 0, DEFAULT_ENTRY_FLAGS); + identityMapTable(newDir, 1, ENTRY_RW | ENTRY_US); + + setActivePageDir(newDir); + + volatile uint32_t *faulter = (uint32_t *)(BOUND); + + // cause a page fault + *faulter; + + // this nop helps eip++ not do bad things + __asm__ volatile("nop"); + + // confusingly, the not present error code is 0. + assert_on_fault_code(0); + + // swap back + setActivePageDir(base); + + char done[] = "test_entry_not_present done\n"; + serialWrite(COM1, (uint8_t *)(done), sizeof(done) - 1); +} + +void test_main() { + test_composition(); + + PageDirectory *idDir = getActivePageDir(); + ASSERT(idDir != NULL); + + test_identity(idDir); + + test_swap_page(idDir); + + test_modify_in_place(idDir); + + // beware the page fault handler is overriden from this point on. + isrSetHandler(14, test_handler); + + test_entry_not_present(idDir); +} \ No newline at end of file