diff --git a/.vscode/settings.json b/.vscode/settings.json index 2cd9ff5..d264713 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,7 @@ "files.associations": { "*.h": "c", "*.c": "c", - "*.asm": "nasm" + "*.asm": "nasm", + "Makefrag": "makefile" } } \ No newline at end of file diff --git a/2020301918-李懋良-试点班第4个报告.docx b/2020301918-李懋良-试点班第4个报告.docx deleted file mode 100644 index f4b56f8..0000000 Binary files a/2020301918-李懋良-试点班第4个报告.docx and /dev/null differ diff --git a/Makefile b/Makefile index 66e7731..d74dfff 100644 --- a/Makefile +++ b/Makefile @@ -65,27 +65,36 @@ OBJDIRS := all: # xv6黑科技,获取编译命令,如果命令较新则重新编译所有文件 -.PRECIOUS: $(OBJDIR)/.vars.% +.PRECIOUS: $(OBJDIR)/.vars.% \ + $(OBJDIR)/kern/%.o $(OBJDIR)/kern/%.d \ + $(OBJDIR)/user/%.o $(OBJDIR)/user/%.d \ + $(OBJDIR)/lib/%.o $(OBJDIR)/lib/kern/%.o $(OBJDIR)/lib/user/%.o \ + $(OBJDIR)/lib/%.d $(OBJDIR)/lib/kern/%.d $(OBJDIR)/lib/user/%.d $(OBJDIR)/.vars.%: FORCE @echo "$($*)" | cmp -s $@ || echo "$($*)" > $@ .PHONY: FORCE # 导入两个文件,两个文件分别编写,方便管理,也让主makefile更加舒服 include boot/Makefrag +include lib/Makefrag include kern/Makefrag +include user/Makefrag # FAT32镜像文件 -IMAGE = $(OBJDIR)/kern/a.img +IMAGE = $(OBJDIR)/a.img -$(IMAGE): $(OBJDIR)/boot/boot.bin $(OBJDIR)/boot/loader.bin $(OBJDIR)/kern/kernel.bin +$(IMAGE): $(OBJDIR)/boot/boot.bin $(OBJDIR)/boot/loader.bin $(OBJDIR)/kern/kernel.bin $(USER_BINS) @stat $@ 1>/dev/null 2>/dev/null || dd if=/dev/zero of=$@ bs=512 count=102400 @mkfs.vfat -F 32 -s8 $@ - @dd if=$< of=$@ bs=1 count=423 seek=90 conv=notrunc + @dd if=$< of=$@ bs=1 count=422 seek=90 conv=notrunc @sudo mount -o loop $@ /mnt @sudo cp $(OBJDIR)/boot/loader.bin /mnt -v @sudo cp $(OBJDIR)/kern/kernel.bin /mnt -v + @sudo cp $(USER_BINS) /mnt -v @sudo umount /mnt +all: $(IMAGE) + clean: @rm -rf $(OBJDIR) diff --git a/before_memory.png b/before_memory.png new file mode 100644 index 0000000..818413a Binary files /dev/null and b/before_memory.png differ diff --git a/boot/loader.asm b/boot/loader.asm index ec87b07..8da78f2 100644 --- a/boot/loader.asm +++ b/boot/loader.asm @@ -11,9 +11,6 @@ LoaderSegPhyAddr equ 10000h ; LOADER.BIN 被加载到的位置 ---- BaseOfStack equ OffsetOfLoader -PageDirBase equ 100000h ; 页目录开始地址: 1M -PageTblBase equ 101000h ; 页表开始地址: 1M + 4K - [SECTION .text.s16] ALIGN 16 [BITS 16] @@ -163,9 +160,12 @@ LABEL_PM_START: ; ; 启动分页机制 -------------------------------------------------------------- +PageDirBase equ 100000h ; 页目录开始地址: 1M +PageTblBase equ 101000h ; 页表开始地址: 1M + 4K +VirtAddrBase1 equ 0h ; 线性地址的0~128MB映射到0~128MB的物理地址 +VirtAddrBase2 equ 0c0000000h; 线性地址的3GB~3GB+128MB映射到0~128MB的物理地址 SetupPaging: ; 根据内存大小计算应初始化多少PDE以及多少页表 - xor edx, edx mov eax, 8000000h ; qemu虚拟机默认内存128MB,这里简化实现省去了实模式代码里中断向BIOS询问可用内存大小的步骤 mov ebx, 400000h ; 400000h = 4M = 4096 * 1024, 一个页表对应的内存大小 div ebx @@ -178,29 +178,45 @@ SetupPaging: ; 为简化处理, 所有线性地址对应相等的物理地址. 并且不考虑内存空洞. - ; 首先初始化页目录 - mov ax, SelectorFlatRW - mov es, ax +InitDirPages1: + ; 初始化0~128MB的根目录页表 mov edi, PageDirBase ; 此段首地址为 PageDirBase - xor eax, eax mov eax, PageTblBase | PG_P | PG_USU | PG_RWW .1: stosd add eax, 4096 ; 为了简化, 所有页表在内存中是连续的. loop .1 - ; 再初始化所有页表 +InitDirPages2: + ; 初始化3GB + 0 ~ 3GB + 128MB的根目录页表 + mov ecx, [esp] + mov edi, PageDirBase + 0c00h +.1: + stosd + add eax, 4096 + loop .1 + +InitTblPages1: + ; 再初始化所有0~128MB页表 pop eax ; 页表个数 mov ebx, 1024 ; 每个页表 1024 个 PTE mul ebx mov ecx, eax ; PTE个数 = 页表个数 * 1024 + push ecx mov edi, PageTblBase ; 此段首地址为 PageTblBase - xor eax, eax mov eax, PG_P | PG_USU | PG_RWW -.2: +.1: stosd add eax, 4096 ; 每一页指向 4K 的空间 - loop .2 + loop .1 + +InitTblPages2: + pop ecx + mov eax, PG_P | PG_USU | PG_RWW +.1: + stosd + add eax, 4096 ; 每一页指向 4K 的空间 + loop .1 mov eax, PageDirBase mov cr3, eax diff --git a/boot/loadkernel.c b/boot/loadkernel.c index e2b912a..3c3ad68 100644 --- a/boot/loadkernel.c +++ b/boot/loadkernel.c @@ -1,7 +1,7 @@ -#include "type.h" -#include "x86.h" -#include "elf.h" -#include "fat32.h" +#include +#include +#include +#include /* * 显示相关 diff --git a/inc/assert.h b/inc/assert.h index e3a3ab0..5a9ebbb 100644 --- a/inc/assert.h +++ b/inc/assert.h @@ -1,8 +1,6 @@ #ifndef MINIOS_ASSERT_H #define MINIOS_ASSERT_H -#include "stdio.h" - void _warn(const char*, int, const char*, ...); void _panic(const char*, int, const char*, ...) __attribute__((noreturn)); diff --git a/inc/elf.h b/inc/elf.h index 6371596..365d7f2 100644 --- a/inc/elf.h +++ b/inc/elf.h @@ -1,7 +1,7 @@ #ifndef MINIOS_ELF_H #define MINIOS_ELF_H -#include "type.h" +#include #define ELF_MAGIC 0x464C457FU /* "\x7FELF" in little endian */ diff --git a/inc/fat32.h b/inc/fat32.h index 4d6665e..24154d8 100644 --- a/inc/fat32.h +++ b/inc/fat32.h @@ -1,7 +1,7 @@ #ifndef MINIOS_FAT32_H #define MINIOS_FAT32_H -#include "type.h" +#include struct BPB { u8 BS_jmpBoot[3]; diff --git a/inc/kern/fs.h b/inc/kern/fs.h new file mode 100644 index 0000000..cb781a7 --- /dev/null +++ b/inc/kern/fs.h @@ -0,0 +1,11 @@ +#ifndef MINIOS_KERN_FS_H +#define MINIOS_KERN_FS_H + +#include + +ssize_t kern_read(int fd, void *buf, size_t count); +ssize_t kern_write(int fd, const void *buf, size_t count); + +void read_file(const char *filename, void *dst); + +#endif /* MINIOS_KERN_FS_H */ \ No newline at end of file diff --git a/inc/kern/keyboard.h b/inc/kern/keyboard.h new file mode 100644 index 0000000..d1a34db --- /dev/null +++ b/inc/kern/keyboard.h @@ -0,0 +1,9 @@ +#ifndef MINIOS_KERN_KEYBOARD_H +#define MINIOS_KERN_KEYBOARD_H + +#include + +void add_keyboard_buf(u8 ch); +u8 read_keyboard_buf(void); + +#endif /* MINIOS_KERN_KEYBOARD_H */ \ No newline at end of file diff --git a/inc/keymap.h b/inc/kern/keymap.h similarity index 99% rename from inc/keymap.h rename to inc/kern/keymap.h index 03e72a5..c1b5ed1 100644 --- a/inc/keymap.h +++ b/inc/kern/keymap.h @@ -1,7 +1,7 @@ -#ifndef MINIOS_KEYMAP_H -#define MINIOS_KEYMAP_H +#ifndef MINIOS_KERN_KEYMAP_H +#define MINIOS_KERN_KEYMAP_H -#include "type.h" +#include #define NR_SCAN_CODES 128 @@ -320,4 +320,4 @@ WWW Refresh E0, 67 E0, E7 WWW Favorites E0, 66 E0, E6 *=====================================================================================*/ -#endif /* MINIOS_KEYMAP_H */ +#endif /* MINIOS_KERN_KEYMAP_H */ diff --git a/inc/kern/kmalloc.h b/inc/kern/kmalloc.h new file mode 100644 index 0000000..4b1dbf2 --- /dev/null +++ b/inc/kern/kmalloc.h @@ -0,0 +1,6 @@ +#ifndef MINIOS_KERN_KMALLOC_H +#define MINIOS_KERN_KMALLOC_H + +phyaddr_t phy_malloc_4k(void); + +#endif /* MINIOS_KERN_KMALLOC_H */ \ No newline at end of file diff --git a/inc/kern/pmap.h b/inc/kern/pmap.h new file mode 100644 index 0000000..64f9149 --- /dev/null +++ b/inc/kern/pmap.h @@ -0,0 +1,8 @@ +#ifndef MINIOS_KERN_PMAP_H +#define MINIOS_KERN_PMAP_H + +#include + +void map_kern(phyaddr_t cr3); + +#endif \ No newline at end of file diff --git a/inc/kern/process.h b/inc/kern/process.h new file mode 100644 index 0000000..97c2207 --- /dev/null +++ b/inc/kern/process.h @@ -0,0 +1,75 @@ +#ifndef MINIOS_KERN_PROCESS_H +#define MINIOS_KERN_PROCESS_H + +#include +#include + +/* pcb中存储用户态进程的寄存器信息 */ +struct user_context { /* proc_ptr points here ↑ Low */ + u32 gs; /* ┓ │ */ + u32 fs; /* ┃ │ */ + u32 es; /* ┃ │ */ + u32 ds; /* ┃ │ */ + u32 edi; /* ┃ │ */ + u32 esi; /* ┣ pushed by save() │ */ + u32 ebp; /* ┃ │ */ + u32 kernel_esp; /* <- 'popad' will ignore it │ */ + u32 ebx; /* ┃ ↑栈从高地址往低地址增长 */ + u32 edx; /* ┃ │ */ + u32 ecx; /* ┃ │ */ + u32 eax; /* ┛ │ */ + u32 retaddr; /* return address for assembly code save() │ */ + u32 eip; /* ┓ │ */ + u32 cs; /* ┃ │ */ + u32 eflags; /* ┣ these are pushed by CPU during interrupt │ */ + u32 esp; /* ┃ │ */ + u32 ss; /* ┛ ┷High */ +}; + +struct kern_context { + u32 eflags; + u32 edi; + u32 esi; + u32 ebp; + u32 ebx; + u32 edx; + u32 ecx; + u32 eax; + u32 esp; +}; + +/* pcb */ +typedef struct s_proc { + struct user_context user_regs; + struct kern_context kern_regs; + u32 pid; + phyaddr_t cr3; + int priority; + int ticks; +}PROCESS_0; + +#define KERN_STACKSIZE (8 * KB) + +typedef union u_proc { + PROCESS_0 pcb; // 内核pcb + char _pad[KERN_STACKSIZE]; // pcb往上剩下的空间用于内核栈 +}PROCESS; // 通过union控制每个进程占8kb空间 + +/* 指向当前进程pcb的指针 */ +// kern/main.c +extern PROCESS *p_proc_ready; +/* pcb表 */ +#define PCB_SIZE 2 +// kern/main.c +extern PROCESS proc_table[]; + +// 内核栈切换上下文函数(汇编接口) +void switch_kern_context( + struct kern_context *current_context, + struct kern_context *next_context +); + +// 处理函数 +void schedule(void); +u32 kern_get_pid(PROCESS *p_proc); +#endif /* MINIOS_KERN_PROCESS_H */ \ No newline at end of file diff --git a/inc/protect.h b/inc/kern/protect.h similarity index 96% rename from inc/protect.h rename to inc/kern/protect.h index 73830fe..ed9a9ec 100644 --- a/inc/protect.h +++ b/inc/kern/protect.h @@ -1,7 +1,7 @@ -#ifndef MINIOS_PROTECT_H -#define MINIOS_PROTECT_H +#ifndef MINIOS_KERN_PROTECT_H +#define MINIOS_KERN_PROTECT_H -#include "type.h" +#include /* 存储段描述符/系统段描述符 */ typedef struct s_descriptor { /* 共 8 个字节 */ @@ -89,6 +89,9 @@ typedef struct s_gate #define INT_VECTOR_IRQ0 0x20 #define INT_VECTOR_IRQ8 0x28 +/* 系统调用 */ +#define INT_VECTOR_SYSCALL 0x80 + /* 权限 */ #define PRIVILEGE_KRNL 0 #define PRIVILEGE_TASK 1 @@ -126,6 +129,7 @@ init_gate(GATE *p_gate, u8 desc_type, void *handler, u8 privilage) #define LDT_SIZE GDT_SIZE #define IDT_SIZE 256 +// kern/start.c extern DESCRIPTOR gdt[GDT_SIZE]; extern DESCRIPTOR ldt[LDT_SIZE]; extern GATE idt[IDT_SIZE]; @@ -163,6 +167,7 @@ typedef struct s_tss { }TSS; /* 全局的tss数据结构 */ +// kern/start.c extern TSS tss; /* 描述符表 */ @@ -170,7 +175,7 @@ struct Pseudodesc { u16 pd_lim; // Limit u32 pd_base; // Base address } __attribute__ ((packed)); - +// kern/start.c extern struct Pseudodesc gdt_ptr, idt_ptr; -#endif /* MINIOS_PROTECT_H */ +#endif /* MINIOS_KERN_PROTECT_H */ diff --git a/inc/kern/stdio.h b/inc/kern/stdio.h new file mode 100644 index 0000000..a41f378 --- /dev/null +++ b/inc/kern/stdio.h @@ -0,0 +1,10 @@ +#ifndef MINIOS_KERN_STDIO_H +#define MINIOS_KERN_STDIO_H + +#include + +// lib/kern/terminal.c +int kprintf(const char *fmt, ...); +int vkprintf(const char *fmt, va_list); + +#endif /* MINIOS_KERN_STDIO_H */ \ No newline at end of file diff --git a/inc/kern/syscall.h b/inc/kern/syscall.h new file mode 100644 index 0000000..31ded36 --- /dev/null +++ b/inc/kern/syscall.h @@ -0,0 +1,25 @@ +#ifndef MINIOS_KERN_SYSCALL_H +#define MINIOS_KERN_SYSCALL_H + +#include +#include + +// 由于会出现系统底层函数复用的情况 +// miniOS 系统调用分成三层函数调用 +// sys_* 外层函数,放进系统调用表中 +// do_* 中间函数,留着用于参数处理 +// kern_* 底层函数,实际处理函数 + +// 系统调用处理函数表 +// kern/syscall.c +extern ssize_t (*syscall_table[])(void); + +// kern/time.c +ssize_t do_get_ticks(void); +// kern/process.c +ssize_t do_get_pid(void); +// kern/fs.c +ssize_t do_read(int fd, void *buf, size_t count); +ssize_t do_write(int fd, const void *buf, size_t count); + +#endif /* MINIOS_KERN_SYSCALL_H */ \ No newline at end of file diff --git a/inc/kern/time.h b/inc/kern/time.h new file mode 100644 index 0000000..0188a91 --- /dev/null +++ b/inc/kern/time.h @@ -0,0 +1,9 @@ +#ifndef MINIOS_KERN_TIME_H +#define MINIOS_KERN_TIME_H + +#include + +void timecounter_inc(void); +size_t kern_get_ticks(void); + +#endif /* MINIOS_KERN_TIME_H */ \ No newline at end of file diff --git a/inc/trap.h b/inc/kern/trap.h similarity index 81% rename from inc/trap.h rename to inc/kern/trap.h index 938752f..e371f17 100644 --- a/inc/trap.h +++ b/inc/kern/trap.h @@ -1,5 +1,5 @@ -#ifndef MINIOS_TRAP_H -#define MINIOS_TRAP_H +#ifndef MINIOS_KERN_TRAP_H +#define MINIOS_KERN_TRAP_H #define INT_M_CTL 0x20 /* I/O port for interrupt controller */ #define INT_M_CTLMASK 0x21 /* setting bits in this port disables ints */ @@ -19,13 +19,12 @@ #define PRINTER_IRQ 7 #define AT_WINI_IRQ 14 /* at winchester */ -/* 一个用于标记是否有重入内核的标志 */ -extern int k_reenter; - /* 执行用户进程的入口(汇编接口) */ void restart(); /* 导入中断处理函数(汇编接口) */ +// 系统调用 +void int_syscall(); // 异常 void divide_error(); void single_step_exception(); @@ -65,6 +64,26 @@ void hwint15(); void enable_irq(int irq); void disable_irq(int irq); +// kern/main.c +/* 标志着内核是否初始化完成 */ +extern bool init_kernel; + +static inline void +disable_int() +{ + asm volatile("cli"); +} + +static inline void +enable_int() +{ + if (init_kernel) + asm volatile("sti"); +} + +/* 系统调用实际处理函数(C接口) */ +void syscall_handler(void); + /* 异常中断实际处理函数(C接口) */ void exception_handler(int vec_no, int err_code, int eip, int cs, int eflags); @@ -72,8 +91,9 @@ void exception_handler(int vec_no, int err_code, int eip, /* 外设中断实际处理函数(C接口) */ void default_interrupt_handler(int irq); void clock_interrupt_handler(int irq); -void keyboard_interrupt_handler(int irq); +void kb_interrupt_handler(int irq); /* 外设中断实际处理函数表 */ +// kern/trap.c extern void (*irq_table[])(int); -#endif /* MINIOS_TRAP_H */ \ No newline at end of file +#endif /* MINIOS_KERN_TRAP_H */ \ No newline at end of file diff --git a/inc/keyboard.h b/inc/keyboard.h deleted file mode 100644 index 6ba0ac4..0000000 --- a/inc/keyboard.h +++ /dev/null @@ -1,9 +0,0 @@ -#ifndef MINIOS_KEYBOARD_H -#define MINIOS_KEYBOARD_H - -#include "type.h" - -#define KEYBOARD_BUF_REG 0x60 -void add_keyboard_buf(u8 ch); - -#endif /* MINIOS_KEYBOARD_H */ \ No newline at end of file diff --git a/inc/mmu.h b/inc/mmu.h new file mode 100644 index 0000000..2fbd208 --- /dev/null +++ b/inc/mmu.h @@ -0,0 +1,123 @@ +#ifndef MINIOS_MMU_H +#define MINIOS_MMU_H + +#include + +// 内核并不是恒等映射 +// 3GB~3GB+128MB的线性地址被映射到0~128MB的物理地址上 +// kernel程序的运行在3GB~3GB+128MB的线性地址上 +// 低3GB线性地址用于存放用户程序 +#define KERNBASE 0xC0000000 +#define K_PHY2LIN(x) ((x)+KERNBASE) +#define K_LIN2PHY(x) ((x)-KERNBASE) + +#define KB 1024u +#define MB (1024u * KB) +#define GB (1024u * MB) + +// A linear address 'la' has a three-part structure as follows: +// +// +--------10------+-------10-------+---------12----------+ +// | Page Directory | Page Table | Offset within Page | +// | Index | Index | | +// +----------------+----------------+---------------------+ +// \--- PDX(la) --/ \--- PTX(la) --/ \---- PGOFF(la) ----/ +// \---------- PGNUM(la) ----------/ +// +// The PDX, PTX, PGOFF, and PGNUM macros decompose linear addresses as shown. +// To construct a linear address la from PDX(la), PTX(la), and PGOFF(la), +// use PGADDR(PDX(la), PTX(la), PGOFF(la)). + +// page number field of address +#define PGNUM(la) (((uintptr_t) (la)) >> PTXSHIFT) + +// page directory index +#define PDX(la) ((((uintptr_t) (la)) >> PDXSHIFT) & 0x3FF) + +// page table index +#define PTX(la) ((((uintptr_t) (la)) >> PTXSHIFT) & 0x3FF) + +// offset in page +#define PGOFF(la) (((uintptr_t) (la)) & 0xFFF) + +// construct linear address from indexes and offset +#define PGADDR(d, t, o) ((void*) ((d) << PDXSHIFT | (t) << PTXSHIFT | (o))) + +// Page directory and page table constants. +#define NPDENTRIES 1024 // page directory entries per page directory +#define NPTENTRIES 1024 // page table entries per page table + +#define PGSIZE 4096 // bytes mapped by a page +#define PGSHIFT 12 // log2(PGSIZE) + +#define PTSIZE (PGSIZE*NPTENTRIES) // bytes mapped by a page directory entry +#define PTSHIFT 22 // log2(PTSIZE) + +#define PTXSHIFT 12 // offset of PTX in a linear address +#define PDXSHIFT 22 // offset of PDX in a linear address + +// Page table/directory entry flags. +#define PTE_P 0x001 // Present +#define PTE_W 0x002 // Writeable +#define PTE_U 0x004 // User +#define PTE_PWT 0x008 // Write-Through +#define PTE_PCD 0x010 // Cache-Disable +#define PTE_A 0x020 // Accessed +#define PTE_D 0x040 // Dirty +#define PTE_PS 0x080 // Page Size +#define PTE_G 0x100 // Global + +// Address in page table or page directory entry +#define PTE_ADDR(pte) ((phyaddr_t) (pte) & ~0xFFF) + +// Control Register flags +#define CR0_PE 0x00000001 // Protection Enable +#define CR0_MP 0x00000002 // Monitor coProcessor +#define CR0_EM 0x00000004 // Emulation +#define CR0_TS 0x00000008 // Task Switched +#define CR0_ET 0x00000010 // Extension Type +#define CR0_NE 0x00000020 // Numeric Errror +#define CR0_WP 0x00010000 // Write Protect +#define CR0_AM 0x00040000 // Alignment Mask +#define CR0_NW 0x20000000 // Not Writethrough +#define CR0_CD 0x40000000 // Cache Disable +#define CR0_PG 0x80000000 // Paging + +#define CR4_PCE 0x00000100 // Performance counter enable +#define CR4_MCE 0x00000040 // Machine Check Enable +#define CR4_PSE 0x00000010 // Page Size Extensions +#define CR4_DE 0x00000008 // Debugging Extensions +#define CR4_TSD 0x00000004 // Time Stamp Disable +#define CR4_PVI 0x00000002 // Protected-Mode Virtual Interrupts +#define CR4_VME 0x00000001 // V86 Mode Extensions + +// Eflags register +#define FL_CF 0x00000001 // Carry Flag +#define FL_PF 0x00000004 // Parity Flag +#define FL_AF 0x00000010 // Auxiliary carry Flag +#define FL_ZF 0x00000040 // Zero Flag +#define FL_SF 0x00000080 // Sign Flag +#define FL_TF 0x00000100 // Trap Flag +#define FL_IF 0x00000200 // Interrupt Flag +#define FL_DF 0x00000400 // Direction Flag +#define FL_OF 0x00000800 // Overflow Flag +#define FL_IOPL_MASK 0x00003000 // I/O Privilege Level bitmask +#define FL_IOPL_0 0x00000000 // IOPL == 0 +#define FL_IOPL_1 0x00001000 // IOPL == 1 +#define FL_IOPL_2 0x00002000 // IOPL == 2 +#define FL_IOPL_3 0x00003000 // IOPL == 3 +#define FL_NT 0x00004000 // Nested Task +#define FL_RF 0x00010000 // Resume Flag +#define FL_VM 0x00020000 // Virtual 8086 mode +#define FL_AC 0x00040000 // Alignment Check +#define FL_VIF 0x00080000 // Virtual Interrupt Flag +#define FL_VIP 0x00100000 // Virtual Interrupt Pending +#define FL_ID 0x00200000 // ID flag + +// Page fault error codes +#define FEC_PR 0x1 // Page fault caused by protection violation +#define FEC_WR 0x2 // Page fault caused by a write +#define FEC_U 0x4 // Page fault occured while in user mode + + +#endif /* MINIOS_MMU_H */ \ No newline at end of file diff --git a/inc/process.h b/inc/process.h deleted file mode 100644 index 7d4d668..0000000 --- a/inc/process.h +++ /dev/null @@ -1,44 +0,0 @@ -#ifndef MINIOS_PROCESS_H -#define MINIOS_PROCESS_H - -#include "type.h" - -/* pcb中存储用户态进程的寄存器信息 */ -typedef struct s_stackframe { /* proc_ptr points here ↑ Low */ - u32 gs; /* ┓ │ */ - u32 fs; /* ┃ │ */ - u32 es; /* ┃ │ */ - u32 ds; /* ┃ │ */ - u32 edi; /* ┃ │ */ - u32 esi; /* ┣ pushed by save() │ */ - u32 ebp; /* ┃ │ */ - u32 kernel_esp; /* <- 'popad' will ignore it │ */ - u32 ebx; /* ┃ ↑栈从高地址往低地址增长 */ - u32 edx; /* ┃ │ */ - u32 ecx; /* ┃ │ */ - u32 eax; /* ┛ │ */ - u32 retaddr; /* return address for assembly code save() │ */ - u32 eip; /* ┓ │ */ - u32 cs; /* ┃ │ */ - u32 eflags; /* ┣ these are pushed by CPU during interrupt │ */ - u32 esp; /* ┃ │ */ - u32 ss; /* ┛ ┷High */ -}STACK_FRAME; - -/* pcb */ -typedef struct s_proc { - STACK_FRAME regs; /* process' registers saved in stack frame */ - u32 pid; /* process id passed in from MM */ - char p_name[16]; /* name of the process */ -}PROCESS; -#ifdef HAPPY_SNAKE -#define PCB_SIZE 1 -#else -#define PCB_SIZE 3 -#endif -/* 指向当前进程pcb的指针 */ -extern PROCESS *p_proc_ready; -/* pcb表 */ -extern PROCESS proc_table[PCB_SIZE]; - -#endif \ No newline at end of file diff --git a/inc/stdio.h b/inc/stdio.h index 56416a6..15fb932 100644 --- a/inc/stdio.h +++ b/inc/stdio.h @@ -1,24 +1,17 @@ #ifndef MINIOS_STDIO_H #define MINIOS_STDIO_H -#include "type.h" -#include "stdarg.h" +#include +#include #ifndef NULL #define NULL ((void *) 0) #endif /* NULL */ -// lib/printfmt.c +// lib/kern/printfmt.c void printfmt(void (*putch)(int, void*), void *putdat, const char *fmt, ...); void vprintfmt(void (*putch)(int, void*), void *putdat, const char *fmt, va_list); int snprintf(char *str, int size, const char *fmt, ...); int vsnprintf(char *str, int size, const char *fmt, va_list); -// lib/terminal.c -int kprintf(const char *fmt, ...); -int vkprintf(const char *fmt, va_list); - -// kern/keyboard.c -u8 getch(void); - #endif /* MINIOS_STDIO_H */ \ No newline at end of file diff --git a/inc/string.h b/inc/string.h index 6e3c91d..c852b5e 100644 --- a/inc/string.h +++ b/inc/string.h @@ -1,7 +1,7 @@ #ifndef MINIOS_STRING_H #define MINIOS_STRING_H -#include "type.h" +#include int strlen(const char *s); int strnlen(const char *s, size_t size); diff --git a/inc/syscall.h b/inc/syscall.h new file mode 100644 index 0000000..ca6230a --- /dev/null +++ b/inc/syscall.h @@ -0,0 +1,10 @@ +#ifndef MINIOS_SYSCALL_H +#define MINIOS_SYSCALL_H + +// 系统调用号 +#define _NR_get_ticks 0 +#define _NR_get_pid 1 +#define _NR_read 2 +#define _NR_write 3 + +#endif /* MINIOS_SYSCALL_H */ diff --git a/inc/terminal.h b/inc/terminal.h index 512032b..a1b3522 100644 --- a/inc/terminal.h +++ b/inc/terminal.h @@ -1,12 +1,12 @@ #ifndef MINIOS_TERMINAL_H #define MINIOS_TERMINAL_H -#include "type.h" +#include /* * 终端大小参数 */ -#define TERMINAL_ADDR 0xB8000 +#define TERMINAL_ADDR 0xC0B8000 #define TERMINAL_COLUMN 80 #define TERMINAL_ROW 25 #define TERMINAL_SIZE ((TERMINAL_ROW) * (TERMINAL_COLUMN)) diff --git a/inc/time.h b/inc/time.h deleted file mode 100644 index e1c9ff8..0000000 --- a/inc/time.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef MINIOS_TIME_H -#define MINIOS_TIME_H - -#include "type.h" - -void timecounter_inc(); -size_t clock(); - -void change_8253Counter0(int hertz); -#define TIMER0 0x40 -#define TIMER_MODEREG 0x43 -#define TIMER_MODE 0x34 -#define TIMER_FREQ 1193182L - -#endif \ No newline at end of file diff --git a/inc/type.h b/inc/type.h index 895dcb7..5698449 100644 --- a/inc/type.h +++ b/inc/type.h @@ -4,17 +4,18 @@ #ifndef NULL #define NULL ((void*) 0) #endif -// #define HAPPY_SNAKE typedef _Bool bool; enum { false, true }; +typedef long long i64; +typedef unsigned long long u64; typedef int i32; -typedef unsigned int u32; +typedef unsigned int u32; typedef short i16; -typedef unsigned short u16; +typedef unsigned short u16; typedef char i8; -typedef unsigned char u8; +typedef unsigned char u8; typedef i32 intptr_t; typedef u32 uintptr_t; diff --git a/inc/user/stdio.h b/inc/user/stdio.h new file mode 100644 index 0000000..e0a553a --- /dev/null +++ b/inc/user/stdio.h @@ -0,0 +1,17 @@ +#ifndef MINIOS_USER_STDIO_H +#define MINIOS_USER_STDIO_H + +#include + +#define STDIN 0 +#define STDOUT 1 + +// lib/user/stdio.c +int printf(const char *fmt, ...); +int vprintf(const char *fmt, va_list); +u8 getch(); + +void fflush(); + + +#endif /* MINIOS_USER_STDIO_H */ \ No newline at end of file diff --git a/inc/user/syscall.h b/inc/user/syscall.h new file mode 100644 index 0000000..ee37810 --- /dev/null +++ b/inc/user/syscall.h @@ -0,0 +1,19 @@ +#ifndef MINIOS_USER_SYSCALL_H +#define MINIOS_USER_SYSCALL_H + +#include +#include + +ssize_t syscall0(size_t NR_syscall); +ssize_t syscall1(size_t NR_syscall, size_t p1); +ssize_t syscall2(size_t NR_syscall, size_t p1, size_t p2); +ssize_t syscall3(size_t NR_syscall, size_t p1, size_t p2, size_t p3); +ssize_t syscall4(size_t NR_syscall, size_t p1, size_t p2, size_t p3, size_t p4); +ssize_t syscall5(size_t NR_syscall, size_t p1, size_t p2, size_t p3, size_t p4, size_t p5); + +ssize_t get_ticks(); +ssize_t get_pid(); +ssize_t read(int fd, void *buf, size_t count); +ssize_t write(int fd, const void *buf, size_t count); + +#endif \ No newline at end of file diff --git a/inc/x86.h b/inc/x86.h index e52f001..a7b0924 100644 --- a/inc/x86.h +++ b/inc/x86.h @@ -1,7 +1,7 @@ #ifndef MINIOS_X86_H #define MINIOS_X86_H -#include "type.h" +#include static inline u8 inb(int port) @@ -100,15 +100,112 @@ outl(int port, u32 data) } static inline void -disable_int() +lcr0(u32 val) { - asm volatile("cli"); + asm volatile("movl %0,%%cr0" : : "r" (val)); +} + +static inline u32 +rcr0(void) +{ + u32 val; + asm volatile("movl %%cr0,%0" : "=r" (val)); + return val; +} + +static inline u32 +rcr2(void) +{ + u32 val; + asm volatile("movl %%cr2,%0" : "=r" (val)); + return val; } static inline void -enable_int() +lcr3(u32 val) { - asm volatile("sti"); + asm volatile("movl %0,%%cr3" : : "r" (val)); } -#endif \ No newline at end of file +static inline u32 +rcr3(void) +{ + u32 val; + asm volatile("movl %%cr3,%0" : "=r" (val)); + return val; +} + +static inline void +lcr4(u32 val) +{ + asm volatile("movl %0,%%cr4" : : "r" (val)); +} + +static inline u32 +rcr4(void) +{ + u32 cr4; + asm volatile("movl %%cr4,%0" : "=r" (cr4)); + return cr4; +} + +static inline void +tlbflush(void) +{ + u32 cr3; + asm volatile("movl %%cr3,%0" : "=r" (cr3)); + asm volatile("movl %0,%%cr3" : : "r" (cr3)); +} + +static inline u32 +read_eflags(void) +{ + u32 eflags; + asm volatile("pushfl; popl %0" : "=r" (eflags)); + return eflags; +} + +static inline void +write_eflags(u32 eflags) +{ + asm volatile("pushl %0; popfl" : : "r" (eflags)); +} + +static inline u32 +read_ebp(void) +{ + u32 ebp; + asm volatile("movl %%ebp,%0" : "=r" (ebp)); + return ebp; +} + +static inline u32 +read_esp(void) +{ + u32 esp; + asm volatile("movl %%esp,%0" : "=r" (esp)); + return esp; +} + +static inline u64 +read_tsc(void) +{ + u64 tsc; + asm volatile("rdtsc" : "=A" (tsc)); + return tsc; +} + +static inline u32 +xchg(volatile u32 *addr, u32 newval) +{ + u32 result; + + // The + in "+m" denotes a read-modify-write operand. + asm volatile("lock; xchgl %0, %1" + : "+m" (*addr), "=a" (result) + : "1" (newval) + : "cc"); + return result; +} + +#endif /* MINIOS_X86_H */ \ No newline at end of file diff --git a/kern/Makefrag b/kern/Makefrag index bd042a8..0151a0a 100644 --- a/kern/Makefrag +++ b/kern/Makefrag @@ -5,46 +5,38 @@ OBJDIRS += kern -KERN_ENTRY_ADDR := 0x200000 +KERN_ENTRY_ADDR := 0xC0200000 KERN_SRCFILES:= kern/astart.asm \ kern/atrap.asm \ + kern/aswitch.asm \ + kern/fs.c \ kern/keyboard.c \ + kern/kmalloc.c \ kern/main.c \ + kern/pmap.c \ + kern/process.c \ kern/start.c \ + kern/syscall.c \ kern/time.c \ kern/trap.c \ - lib/libch4Core.a\ - lib/terminal.c \ + lib/kern/terminal.c \ lib/printfmt.c \ lib/string.c KERN_OBJFILES := $(patsubst %.c, $(OBJDIR)/%.o, $(KERN_SRCFILES)) KERN_OBJFILES := $(patsubst %.asm, $(OBJDIR)/%.o, $(KERN_OBJFILES)) -KERN_OBJFILES := $(patsubst $(OBJDIR)/lib/%, $(OBJDIR)/kern/%, $(KERN_OBJFILES)) $(OBJDIR)/kern/%.o: kern/%.c $(OBJDIR)/.vars.CFLAGS @echo + cc $< @mkdir -p $(@D) @$(CC) $(CFLAGS) -c -o $@ $< -$(OBJDIR)/kern/%.o: lib/%.c $(OBJDIR)/.vars.CFLAGS - @echo + cc $< - @mkdir -p $(@D) - @$(CC) $(CFLAGS) -c -o $@ $< - $(OBJDIR)/kern/%.o: kern/%.asm $(OBJDIR)/.vars.CFLAGS @echo + as obj $< @mkdir -p $(@D) @$(AS) -f elf -o $@ $< -$(OBJDIR)/kern/%.a: lib/%.a $(OBJDIR)/.vars.CFLAGS - @echo + cp lib $< - @mkdir -p $(@D) - @cp $< $(@D) - $(OBJDIR)/kern/kernel.bin: $(KERN_OBJFILES) $(OBJDIR)/.vars.LDFLAGS @echo + ld $@ @$(LD) $(LDFLAGS) -s -Ttext $(KERN_ENTRY_ADDR) -o $@ $(KERN_OBJFILES) $(GCC_LIB) - @$(LD) $(LDFLAGS) -Ttext $(KERN_ENTRY_ADDR) -o $(OBJDIR)/kern/kernel.dbg $(KERN_OBJFILES) $(GCC_LIB) - -all: $(OBJDIR)/kern/a.img + @$(LD) $(LDFLAGS) -Ttext $(KERN_ENTRY_ADDR) -o $(OBJDIR)/kern/kernel.dbg $(KERN_OBJFILES) $(GCC_LIB) \ No newline at end of file diff --git a/kern/astart.asm b/kern/astart.asm index 4e8170a..0a2dd97 100644 --- a/kern/astart.asm +++ b/kern/astart.asm @@ -45,10 +45,13 @@ SELECTOR_TSS equ 020h SELECTOR_LDT equ 028h ; 导入函数 +; kern/start.c extern cstart +; kern/main.c extern kernel_main ; 导入全局变量 +; kern/start.c extern gdt_ptr extern idt_ptr @@ -68,6 +71,8 @@ _start: call cstart ; 在此函数中改变了gdt_ptr,让它指向新的GDT lgdt [gdt_ptr] ; 装载GDT + mov ax, SELECTOR_KERNEL_GS + mov gs, ax ; 刷新gs缓存,因为在cstart中发生了更新 mov ax, SELECTOR_LDT; 装载LDT lldt ax mov ax, SELECTOR_TSS; 装载TSS diff --git a/kern/aswitch.asm b/kern/aswitch.asm new file mode 100644 index 0000000..6be9fc7 --- /dev/null +++ b/kern/aswitch.asm @@ -0,0 +1,59 @@ +[BITS 32] + +[section .text] + +P_STACKBASE equ 0 +EFLAGSREG equ P_STACKBASE +EDIREG equ EFLAGSREG + 4 +ESIREG equ EDIREG + 4 +EBPREG equ ESIREG + 4 +EBXREG equ EBPREG + 4 +EDXREG equ EBXREG + 4 +ECXREG equ EDXREG + 4 +EAXREG equ ECXREG + 4 +ESPREG equ EAXREG + 4 + + +global switch_kern_context +;void switch_kern_context( +; struct kern_context *current_context, +; struct kern_context *next_context +;) +; 程序语义在kern/process.c的schedule函数有讲 + +switch_kern_context: + push ebp + mov ebp, esp + push eax + push ebx + mov eax, [ebp + 8] + mov ebx, [ebp + 12] + call .inner_switch + pop ebx + pop eax + pop ebp + ret +.inner_switch: + mov [eax + ESPREG], esp + lea esp, [eax + ESPREG] + push eax + push ecx + push edx + push ebx + push ebp + push esi + push edi + pushf + + mov esp, ebx + + popf + pop edi + pop esi + pop ebp + pop ebx + pop edx + pop ecx + pop eax + pop esp + ret \ No newline at end of file diff --git a/kern/atrap.asm b/kern/atrap.asm index 5ea61eb..912c18b 100644 --- a/kern/atrap.asm +++ b/kern/atrap.asm @@ -2,13 +2,13 @@ ; 导入 extern StackTop ; kern/astart.asm -extern tss ; inc/protect.h -extern k_reenter ; inc/trap.h -extern exception_handler ; inc/trap.h -extern irq_table ; inc/trap.h -extern p_proc_ready ; inc/process.h +extern tss ; inc/kern/protect.h +extern exception_handler ; inc/kern/trap.h +extern irq_table ; inc/kern/trap.h +extern syscall_handler ; inc/kern/trap.h +extern p_proc_ready ; inc/kern/process.h -; 具体看inc/process.h pcb维护的寄存器表 +; 具体看inc/kern/process.h pcb维护的寄存器表 P_STACKBASE equ 0 GSREG equ P_STACKBASE FSREG equ GSREG + 4 @@ -30,39 +30,32 @@ ESPREG equ EFLAGSREG + 4 SSREG equ ESPREG + 4 P_STACKTOP equ SSREG + 4 +extern to_kern_stack ; kern/process.c + save: pushad ; `. - push ds ; | - push es ; | 保存原寄存器值 - push fs ; | - push gs ; / - mov dx, ss - mov ds, dx - mov es, dx - - mov eax, esp ;eax = 进程表起始地址 - - inc dword [k_reenter] ;k_reenter++; - cmp dword [k_reenter], 0 ;if(k_reenter ==0) - jne .1 ;{ - mov esp, StackTop ; mov esp, StackTop <--切换到内核栈 - push restart ; push restart - jmp [eax + RETADR - P_STACKBASE]; return; -.1: ;} else { 已经在内核栈,不需要再切换 - push restart_reenter ; push restart_reenter - jmp [eax + RETADR - P_STACKBASE]; return; - ;} -; 具体看inc/protect.h TSS维护的寄存器表 -TSS3_S_SP0 equ 4 + push ds ; | + push es ; | 保存原寄存器值 + push fs ; | + push gs ; / + mov dx, ss + mov ds, dx + mov es, dx + mov eax, esp + test dword [eax + CSREG - P_STACKBASE], 3 + jz .1 ; 根据段寄存器的状态信息判断是否发生内核重入 + mov esp, StackTop ; 如果没有发生内核重入就意味着要到用户栈了,先将esp移入临时内核栈 + push eax ; 传入进程表首地址信息 + call to_kern_stack ; 这个函数之后esp变为进程内核栈 + pop eax ; 恢复进程表首地址信息(这个是restart第一句话要用的) +.1: + push eax ; 将restart要用的,如果在内核栈重入,pop esp不会有任何变化 + push restart ; 否则eax在进程表首地址,pop esp会使esp移动到用户栈栈底 + jmp [eax + RETADR - P_STACKBASE] restart: - mov esp, [p_proc_ready] - lea eax, [esp + P_STACKTOP] - mov dword [tss + TSS3_S_SP0], eax -restart_reenter: - cli - dec dword [k_reenter] + pop esp ; 获悉当前的esp该到哪,不用管现在是否要回用户态,语义在save中有解释 pop gs pop fs pop es @@ -71,6 +64,14 @@ restart_reenter: add esp, 4 iretd +; 系统调用 +int_syscall: + call save + sti + call syscall_handler + cli + ret + EOI equ 0x20 INT_M_CTL equ 0x20 ; I/O port for interrupt controller INT_M_CTLMASK equ 0x21 ; setting bits in this port disables ints @@ -97,10 +98,19 @@ INT_S_CTLMASK equ 0xA1 ; setting bits in this port disables ints ret %endmacro +; 时钟中断采取全程关中断的模式,时钟中断相当重要,不允许被打扰 ALIGN 16 hwint00: ; Interrupt routine for irq 0 (the clock). - hwint_master 0 + call save + mov al, EOI + out INT_M_CTL, al + push 0 + call [irq_table + 0] + pop ecx + + ret + ALIGN 16 hwint01: ; Interrupt routine for irq 1 (keyboard) hwint_master 1 @@ -234,6 +244,8 @@ exception: ; 一堆符号导出,没别的 global restart +; 系统调用 +global int_syscall ; 异常处理 global divide_error global single_step_exception diff --git a/kern/fs.c b/kern/fs.c new file mode 100644 index 0000000..a0f9970 --- /dev/null +++ b/kern/fs.c @@ -0,0 +1,180 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +/* + * 内核读入函数,由于没有文件描述符概念, + * 所以强制assert让fd为0,平时的读入流也默认是0的 + * 现在只有从键盘缓冲区获取字符的能力 + */ +ssize_t +kern_read(int fd, void *buf, size_t count) +{ + assert(fd == 0); + + char *s = buf; + + for (size_t i = 0 ; i < count ; i++) { + char c = read_keyboard_buf(); + if (c == -1) + return i; + *s++ = c; + } + + return count; +} + +ssize_t +do_read(int fd, void *buf, size_t count) +{ + assert(buf < (void *)KERNBASE); + assert(buf + count < (void *)KERNBASE); + return kern_read(fd, buf, count); +} + +/* + * 内核写入函数,由于没有文件描述符概念, + * 所以强制assert让fd为1,平时的输出流也默认是1的 + * 现在只有输出字符到终端的能力 + */ +ssize_t +kern_write(int fd, const void *buf, size_t count) +{ + assert(fd == 1); + + const char *s = buf; + for (size_t i = 0 ; i < count ; i++) + kprintf("%c", *s++); + + return count; +} + +ssize_t +do_write(int fd, const void *buf, size_t count) +{ + assert(buf < (void *)KERNBASE); + assert(buf + count < (void *)KERNBASE); + return kern_write(fd, buf, count); +} + +#define SECTSIZE 512 + +static void +waitdisk(void) +{ + // wait for disk reaady + while ((inb(0x1F7) & 0xC0) != 0x40) + /* do nothing */; +} + +static void +readsect(void *dst, u32 offset) +{ + // wait for disk to be ready + waitdisk(); + + outb(0x1F2, 1); // count = 1 + outb(0x1F3, offset); + outb(0x1F4, offset >> 8); + outb(0x1F5, offset >> 16); + outb(0x1F6, (offset >> 24) | 0xE0); + outb(0x1F7, 0x20); // cmd 0x20 - read sectors + + // wait for disk to be ready + waitdisk(); + + // read a sector + insl(0x1F0, dst, SECTSIZE/4); +} + +u32 fat_start_sec; +u32 data_start_sec; +u32 fat_now_sec; +struct BPB bpb; + +/* + * 获取下一个簇号 + */ +static u32 +get_next_clus(u32 current_clus) +{ + u32 sec = current_clus * 4 / SECTSIZE; + u32 off = current_clus * 4 % SECTSIZE; + + static u32 buf[SECTSIZE / 4]; + if (fat_now_sec != fat_start_sec + sec) { + readsect(buf, fat_start_sec + sec); + fat_now_sec = fat_start_sec + sec; + } + return buf[off / 4]; +} + +/* + * 读入簇号对应的数据区的所有数据 + * 读到dst开头的位置,返回读入的尾指针 + */ +static void * +read_data_sec(void *dst, u32 current_clus) +{ + current_clus -= 2; + current_clus *= bpb.BPB_SecPerClus; + current_clus += data_start_sec; + + for (int i = 0 ; i < bpb.BPB_SecPerClus ; i++, dst += SECTSIZE) + readsect(dst, current_clus + i); + return dst; +} + +/* + * 根据filename读取文件到dst处 + * filename要求是短目录项名(11个字节) + * dst推荐是3GB + 48MB + */ +void +read_file(const char *filename, void *dst) +{ + assert(strlen(filename) == 11); + + readsect(&bpb, 0); + + fat_start_sec = bpb.BPB_RsvdSecCnt; + data_start_sec = fat_start_sec + bpb.BPB_FATSz32 * bpb.BPB_NumFATs; + + u32 root_clus = bpb.BPB_RootClus; + u32 file_clus = 0; + + assert(bpb.BPB_BytsPerSec == SECTSIZE && bpb.BPB_SecPerClus == 8); + + static char buf[SECTSIZE * 8]; + // 遍历目录项获取文件起始簇号 + while (root_clus < 0x0FFFFFF8) { + void *read_end = read_data_sec((void *)buf, root_clus); + for (struct Directory_Entry *p = (void *)buf + ; (void *)p < read_end ; p++) { + if (strncmp(p->DIR_Name, filename, 11) == 0) { + assert(p->DIR_FileSize <= 16 * MB); + file_clus = (u32)p->DIR_FstClusHI << 16 | + p->DIR_FstClusLO; + break; + } + } + if (file_clus != 0) + break; + root_clus = get_next_clus(root_clus); + } + + if (file_clus == 0) + panic("file can't found! filename: %s", filename); + // 读入文件 + while (file_clus < 0x0FFFFFF8) { + dst = read_data_sec(dst, file_clus); + file_clus = get_next_clus(file_clus); + } +} \ No newline at end of file diff --git a/kern/game.h b/kern/game.h deleted file mode 100644 index d476e47..0000000 --- a/kern/game.h +++ /dev/null @@ -1,10 +0,0 @@ -// -// Created by ASUS on 2022/10/3. -// - -#ifndef CH3_GAME_H -#define CH3_GAME_H - -void startGame(); - -#endif //CH3_GAME_H diff --git a/kern/keyboard.c b/kern/keyboard.c index 8736226..634436d 100644 --- a/kern/keyboard.c +++ b/kern/keyboard.c @@ -1,11 +1,13 @@ -#include "keymap.h" -#include "stdio.h" -#include "type.h" -#include "x86.h" +#include +#include -#define KB_INBUF_SIZE 4 +#include +#include + +#define KB_INBUF_SIZE 1024 typedef struct kb_inbuf { + u32 lock; u8* p_head; u8* p_tail; int count; @@ -15,7 +17,6 @@ typedef struct kb_inbuf { static KB_INPUT kb_input = { .p_head = kb_input.buf, .p_tail = kb_input.buf, - .count = 0, }; /* @@ -24,14 +25,19 @@ static KB_INPUT kb_input = { void add_keyboard_buf(u8 ch) { - if (kb_input.count < KB_INBUF_SIZE) { - *(kb_input.p_head) = ch; - kb_input.p_head ++; - if (kb_input.p_head == kb_input.buf + KB_INBUF_SIZE) { - kb_input.p_head = kb_input.buf; - } - kb_input.count ++; - } + // 上个自旋锁,保证线程安全(不建议用中断)计组课上讲过,相信大家的记忆力 + while (xchg(&kb_input.lock, 1) == 1); + + if (kb_input.count == KB_INBUF_SIZE) + goto free; + + *kb_input.p_tail++ = ch; + if (kb_input.p_tail == kb_input.buf + KB_INBUF_SIZE) + kb_input.p_tail = kb_input.buf; + kb_input.count++; + +free: + xchg(&kb_input.lock, 0); } /* @@ -39,20 +45,24 @@ add_keyboard_buf(u8 ch) * 否则返回缓冲区队头的字符并弹出队头 */ u8 -getch(void) +read_keyboard_buf(void) { - if (kb_input.count == 0) - return -1; - else { - // disable_int(); - // in user mode, cli and sti will only cause protection exception - u8 val = *(kb_input.p_tail); - kb_input.p_tail ++; - if (kb_input.p_tail == kb_input.buf + KB_INBUF_SIZE) { - kb_input.p_tail = kb_input.buf; - } - kb_input.count --; - // enable_int(); - return val; + u8 res; + + // 上个自旋锁,保证线程安全(不建议用中断)计组课上讲过,相信大家的记忆力 + while (xchg(&kb_input.lock, 1) == 1); + + if (kb_input.count == 0) { + res = -1; + goto free; } + + res = *kb_input.p_head++; + if (kb_input.p_head == kb_input.buf + KB_INBUF_SIZE) + kb_input.p_head = kb_input.buf; + kb_input.count--; + +free: + xchg(&kb_input.lock, 0); + return res; } \ No newline at end of file diff --git a/kern/kmalloc.c b/kern/kmalloc.c new file mode 100644 index 0000000..33d2c3b --- /dev/null +++ b/kern/kmalloc.c @@ -0,0 +1,22 @@ +#include +#include + +#include +#include + +static phyaddr_t malloc_4k_p = 64 * MB; + +/* + * 分配物理内存,每次分配4kb,一页 + * 分配的物理内存区间为64MB~128MB + */ +phyaddr_t +phy_malloc_4k(void) +{ + assert(malloc_4k_p < 128 * MB); + + phyaddr_t addr = malloc_4k_p; + malloc_4k_p += PGSIZE; + + return addr; +} \ No newline at end of file diff --git a/kern/main.c b/kern/main.c index a60c7cd..d1b924c 100644 --- a/kern/main.c +++ b/kern/main.c @@ -1,117 +1,115 @@ -#include "assert.h" -#include "stdio.h" -#include "string.h" -#include "process.h" -#include "protect.h" -#include "type.h" -#include "trap.h" -#include "x86.h" -#include "time.h" -#include "keyboard.h" -#include "game.h" +#include +#include +#include +#include +#include -/* - * 三个测试函数,用户进程的执行流 - */ -void TestA() -{ - int i = 0; - u8 ch; - while(1){ - if ((ch = getch()) != 255) { - kprintf("%c", ch); - } - // kprintf("A%d.",i++); - for (int j = 0 ; j < 5e7 ; j++) - ;//do nothing - } -} +#include +#include +#include +#include +#include +#include +#include -void TestB() -{ - int i = 0; - while(1){ - // kprintf("B%d.",i++); - for (int j = 0 ; j < 5e7 ; j++) - ;//do nothing - } -} +// 标志着内核是否处理完成 +bool init_kernel; -void TestC() -{ - int i = 0; - while(1){ - // kprintf("C%d.",i++); - for (int j = 0 ; j < 5e7 ; j++) - ;//do nothing - } -} - -// 每个栈空间有4kb大 -#define STACK_PREPROCESS 0x1000 -#define STACK_TOTSIZE STACK_PREPROCESS * PCB_SIZE -// 用户栈(直接在内核开一个临时数组充当) -char process_stack[STACK_TOTSIZE]; // 指向当前进程pcb的指针 PROCESS *p_proc_ready; // pcb表 PROCESS proc_table[PCB_SIZE]; -#ifdef HAPPY_SNAKE -void (*entry[]) = { - startGame -}; -char pcb_name[][16] = { - "SNAKE" -}; -#else -void (*entry[]) = { - TestA, - TestB, - TestC, -}; -char pcb_name[][16] = { - "TestA", - "TestB", - "TestC", -}; -#endif + +static inline void +init_segment_regs(PROCESS *p_proc) +{ + p_proc->pcb.user_regs.cs = (SELECTOR_FLAT_C & SA_RPL_MASK & SA_TI_MASK) + | SA_TIL | RPL_USER; + p_proc->pcb.user_regs.ds = (SELECTOR_FLAT_RW & SA_RPL_MASK & SA_TI_MASK) + | SA_TIL | RPL_USER; + p_proc->pcb.user_regs.es = (SELECTOR_FLAT_RW & SA_RPL_MASK & SA_TI_MASK) + | SA_TIL | RPL_USER; + p_proc->pcb.user_regs.fs = (SELECTOR_FLAT_RW & SA_RPL_MASK & SA_TI_MASK) + | SA_TIL | RPL_USER; + p_proc->pcb.user_regs.ss = (SELECTOR_FLAT_RW & SA_RPL_MASK & SA_TI_MASK) + | SA_TIL | RPL_USER; + p_proc->pcb.user_regs.gs = (SELECTOR_VIDEO & SA_RPL_MASK & SA_TI_MASK) + | RPL_USER; +} + /* * 内核的main函数 * 用于初始化用户进程,然后将执行流交给用户进程 */ -void kernel_main() +void kernel_main(void) { kprintf("---start kernel main---\n"); PROCESS *p_proc = proc_table; - char *p_stack = process_stack; for (int i = 0 ; i < PCB_SIZE ; i++, p_proc++) { - strcpy(p_proc->p_name, pcb_name[i]); - p_proc->regs.cs = (SELECTOR_FLAT_C & SA_RPL_MASK & SA_TI_MASK) - | SA_TIL | RPL_USER; - p_proc->regs.ds = (SELECTOR_FLAT_RW & SA_RPL_MASK & SA_TI_MASK) - | SA_TIL | RPL_USER; - p_proc->regs.es = (SELECTOR_FLAT_RW & SA_RPL_MASK & SA_TI_MASK) - | SA_TIL | RPL_USER; - p_proc->regs.fs = (SELECTOR_FLAT_RW & SA_RPL_MASK & SA_TI_MASK) - | SA_TIL | RPL_USER; - p_proc->regs.ss = (SELECTOR_FLAT_RW & SA_RPL_MASK & SA_TI_MASK) - | SA_TIL | RPL_USER; - p_proc->regs.gs = (SELECTOR_VIDEO & SA_RPL_MASK & SA_TI_MASK) - | RPL_USER; + // 初始化进程段寄存器 + init_segment_regs(p_proc); + // 为进程分配cr3物理内存 + p_proc->pcb.cr3 = phy_malloc_4k(); + // 将3GB~3GB+128MB的线性地址映射到0~128MB的物理地址 + map_kern(p_proc->pcb.cr3); + // 在map_kern之后,就内核程序对应的页表已经被映射了 + // 就可以直接lcr3,于此同时执行流不会触发page fault + // 如果不先map_kern,执行流会发现执行的代码的线性地址不存在爆出Page Fault + // 当然选不选择看个人的想法,评价是都行,各有各的优缺点 + // lcr3(p_proc->pcb.cr3); - p_proc->regs.eip = (u32)entry[i]; - p_stack += STACK_PREPROCESS; - p_proc->regs.esp = (u32)p_stack; - p_proc->regs.eflags = 0x1202; /* IF=1, IOPL=1 */ + static char filename[PCB_SIZE][12] = { + "TESTPID BIN", + "TESTKEY BIN", + }; + // 从磁盘中将文件读出,需要注意的是要满足短目录项的文件名长度11, + // 前八个为文件名,后三个为后缀名,跟BootLoader做法一致 + // 推荐将文件加载到3GB + 48MB处,应用程序保证不会有16MB那么大 + read_file(filename[i], (void *)K_PHY2LIN(48 * MB)); + // 现在你就需要将从磁盘中读出的ELF文件解析到用户进程的地址空间中 + panic("unimplement! load elf file"); + + // 上一个实验中,我们开栈是用内核中的一个数组临时充当栈 + // 但是现在就不行了,用户是无法访问内核的地址空间(3GB ~ 3GB + 128MB) + // 需要你自行处理给用户分配用户栈。 + panic("unimplement! init user stack and esp"); + // 初始化用户寄存器 + p_proc->pcb.user_regs.eflags = 0x1202; /* IF=1, IOPL=1 */ + + // 接下来初始化内核寄存器, + // 为什么需要初始化内核寄存器原因是加入了系统调用后 + // 非常有可能出现系统调用执行过程中插入其余中断的情况, + // 如果跟之前一样所有进程共享一个内核栈会发生不可想象的结果, + // 为了避免这种情况,就需要给每个进程分配一个进程栈。 + // 当触发时钟中断发生调度的时候,不再是简单的切换p_proc_ready, + // 而是需要将内核栈进行切换,而且需要切换执行流到另一个进程的内核栈。 + // 所以需要一个地方存放当前进程的寄存器上下文,这是一个富有技巧性的活,深入研究会觉得很妙, + // 如果需要深入了解,去查看kern/process.c中的schedule函数了解切换细节。 + p_proc->pcb.kern_regs.esp = (u32)(p_proc + 1) - 8; + // 保证切换内核栈后执行流进入的是restart函数。 + *(u32 *)(p_proc->pcb.kern_regs.esp + 0) = (u32)restart; + // 这里是因为restart要用`pop esp`确认esp该往哪里跳。 + *(u32 *)(p_proc->pcb.kern_regs.esp + 4) = (u32)&p_proc->pcb; + + // 初始化其余量 + p_proc->pcb.pid = i; + static int priority_table[PCB_SIZE] = {1, 2}; + // priority 预计给每个进程分配的时间片 + // ticks 进程剩余的进程片 + p_proc->pcb.priority = p_proc->pcb.ticks = priority_table[i]; } p_proc_ready = proc_table; - - change_8253Counter0(1000); - enable_irq(CLOCK_IRQ); - enable_irq(KEYBOARD_IRQ); - restart(); + // 切换进程页表和tss + lcr3(p_proc_ready->pcb.cr3); + tss.esp0 = (u32)(&p_proc_ready->pcb.user_regs + 1); + + init_kernel = true; + + // 开个无用的kern_context存当前执行流的寄存器上下文(之后就没用了,直接放在临时变量中) + struct kern_context idle; + switch_kern_context(&idle, &p_proc_ready->pcb.kern_regs); assert(0); } \ No newline at end of file diff --git a/kern/pmap.c b/kern/pmap.c new file mode 100644 index 0000000..d1d5946 --- /dev/null +++ b/kern/pmap.c @@ -0,0 +1,46 @@ +#include +#include + +#include +#include + +/* + * 初始化进程页表的内核部分 + * 将3GB ~ 3GB + 128MB的线性地址映射到0 ~ 128MB的物理地址 + */ +void +map_kern(phyaddr_t cr3) +{ + // 初始化pde(页目录表) + // 由于cr3是物理地址,需要进行一步转化转化到线性地址才能访存 + uintptr_t *pde_ptr = (uintptr_t *)K_PHY2LIN(cr3); + // 基地址在3GB处 + pde_ptr += PDX(3 * GB); + // 一个页目录项映射4MB的内存,计算需要初始化的页目录项数目 + int pde_num = (128 * MB) / (4 * MB); + // 被映射的物理地址 + phyaddr_t phy_addr = 0; + while (pde_num--) { + // 对于每一个页目录项申请一页用于页表 + phyaddr_t pte_phy = phy_malloc_4k(); + // 保证申请出来的物理地址是4k对齐的 + assert(PGOFF(pte_phy) == 0); + // 初始化页目录项,权限是存在(P)|写(W)|用户能够访问(U) + *pde_ptr++ = pte_phy | PTE_P | PTE_W | PTE_U; + + // 接下来对新申请的页表初始化页表项 + // 由于申请的pte_phy是物理地址,需要进行一步转化转化到线性地址才能访存 + uintptr_t *pte_ptr = (uintptr_t *)K_PHY2LIN(pte_phy); + // 初始化页表的所有页表项 + int pte_num = NPTENTRIES; + while (pte_num--) { + // 初始化页表项,权限是存在(P)|写(W),用户不能访问 + // 直接线性映射物理页 + *pte_ptr++ = phy_addr | PTE_P | PTE_W; + // 换下一个物理页 + phy_addr += PGSIZE; + } + } + // 经常assert是个好习惯 + assert(phy_addr == 128 * MB); +} \ No newline at end of file diff --git a/kern/process.c b/kern/process.c new file mode 100644 index 0000000..eeb116e --- /dev/null +++ b/kern/process.c @@ -0,0 +1,104 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +/* + * 这个函数是写给汇编函数用, + * 在汇编函数调用save函数保存完用户寄存器上下文后, + * 需要切到进程内核栈中,minios的进程栈是与pcb是绑定的。 + * 相当于每个进程分配了一个大空间,前面一小部分给了pcb, + * 后面的全可以给进程栈使用,目前进程占8KB,进程栈占7KB多。 + * + * 这个函数的目的就是切换到进程栈处,由于每个进程的空间只有C语言这知道, + * 所以直接在C语言里面写一个内联汇编实现,这样更方便动态开内核栈空间不用顾忌到汇编。 + */ +void +to_kern_stack(u32 ret_esp) +{ + asm volatile ( + "add %1, %0\n\t" + "mov %0, %%esp\n\t" + "push %2\n\t" + "jmp *4(%%ebp)\n\t": + : "a"((u32)p_proc_ready->_pad) + , "b"((u32)sizeof(p_proc_ready->_pad)) + , "c"(ret_esp) + ); +} + +/* + * 调度函数 + */ +void +schedule(void) +{ + // 获取当前的eflags的中断位 + u32 IF_BIT = read_eflags() & FL_IF; + // 如果中断没关,一定!一定!一定!要关中断保证当前执行流操作的原子性 + if (IF_BIT != 0) + disable_int(); + + PROCESS *p_cur_proc = p_proc_ready; + PROCESS *p_next_proc = p_proc_ready + 1; + if (p_next_proc >= proc_table + PCB_SIZE) { + p_next_proc = proc_table; + } + + p_proc_ready = p_next_proc; + // 切换进程页表和tss + lcr3(p_proc_ready->pcb.cr3); + tss.esp0 = (u32)(&p_proc_ready->pcb.user_regs + 1); + + // 调度函数最精髓的部分,非常的有意思。 + // 对于目前的os来说,eax,ebx,ecx,edx,esi,edi,esp,ebp,eflags这九个寄存器就可以表达当前执行流的上下文了, + // 所以这个函数干了个什么事呢,它先将当前的esp保存到kern_regs中保存当前的栈信息, + // 之后将esp移到当前kern_context栈顶然后按顺序依次将eax,ecx,edx,ebx,ebp,esi,edi,eflags压栈, + // 这样就把当前进程寄存器上下文都保存下来了,再之后esp移到目标kern_context栈底, + // 再依次恢复eflags,edi,esi,ebp,ebx,edx,ecx,eax, + // 最后一步恢复esp,这个esp本身被push时保留的是进程栈中的esp,这样就完成了执行流的切换。 + // 从当前进程的角度上这就很有意思,相当于执行流进入了一个函数之后突然就被送往其他次元了,而且当前次元的时间静止了。 + // 然后再之后突然又回来了,还是之前的那些寄存器信息,感觉这个函数啥都没干。 + // 由于涉及到一大堆跟执行流切换相关的操作,所以一定!一定!一定!要关中断。 + switch_kern_context( + &p_cur_proc->pcb.kern_regs, + &p_next_proc->pcb.kern_regs); + // 在传送回之后需要做的第一件事就是忘掉所有的全局变量的缓存 + // 防止编译器优化导致某些变量被优化掉,比如说某个全局变量a + // 你拿eax去获取a的值,在发生调度之后a的值发生变化,下面用到了a + // 然后编译器想都不想就直接用eax接着用了,一个bug就这么来了 + + // 这玩意学术名词叫内存屏障,这还只是单核,如果多核就更加恐怖了, + // 你会看到内存屏障遍地插,这也是为什么os难的一部分原因,跟缓存斗,跟硬件斗,跟编译器斗,跟并发斗,其乐无穷。 + // 要敬佩那些写系统软件的人,斗都要斗麻了的。 + asm volatile ("":::"memory"); + + // 这里可能你会比较疑惑为什么还要开中断, + // 这个时候你需要要以单线程的思路去想, + // 上面的switch_kern_context在执行后执行流就交给了其他进程了, + // 但是其他进程总是会通过调度调度回来,对于当前这个进程来说, + // 它相当于突然被送到其他次元了,包还掉在地上(关中断),而且那个次元的时间就静止了(The World!) + // 当在其他次元兜兜转转回来了之后,还得把包捡回来(开中断)。 + // 不用去关心其他次元是怎么干的,但是对于这个次元来说还是得捡包的。 + // 如果每个次元都遵守这个规则,就可以保证执行不会出问题。 + if (IF_BIT != 0) + enable_int(); +} + +u32 +kern_get_pid(PROCESS *p_proc) +{ + return p_proc->pcb.pid; +} + +ssize_t +do_get_pid(void) +{ + return (ssize_t)kern_get_pid(p_proc_ready); +} \ No newline at end of file diff --git a/kern/start.c b/kern/start.c index 6327e00..ea32558 100644 --- a/kern/start.c +++ b/kern/start.c @@ -1,17 +1,17 @@ -#include "type.h" -#include "protect.h" -#include "stdio.h" -#include "assert.h" -#include "trap.h" -#include "x86.h" +#include +#include +#include + +#include +#include +#include struct Pseudodesc gdt_ptr, idt_ptr; DESCRIPTOR gdt[GDT_SIZE]; DESCRIPTOR ldt[LDT_SIZE]; GATE idt[IDT_SIZE]; - -TSS tss; +TSS tss; /* * 当发生不可挽回的错误时就打印错误信息并使CPU核休眠 @@ -55,7 +55,7 @@ _warn(const char *file, int line, const char *fmt,...) * 新增了tss和ldt的两个的全局描述符 */ static void -init_gdt() +init_gdt(void) { init_segment(&gdt[0], 0, 0, 0); // 代码段(cs) @@ -63,7 +63,8 @@ init_gdt() // 数据段(ds,es,fs,ss) init_segment(&gdt[2], 0, 0xfffff, DA_DRW | DA_32 | DA_LIMIT_4K); // 显存段(gs) - init_segment(&gdt[3], 0xb8000, 0xffff, DA_DRW | DA_DPL3); + init_segment(&gdt[3], K_PHY2LIN(0xb8000), 0x8, DA_DRW | DA_DPL3 | + DA_32 | DA_LIMIT_4K); tss.ss0 = SELECTOR_FLAT_RW; tss.iobase = sizeof(tss); /* 没有I/O许可位图 */ // tss段 @@ -79,7 +80,7 @@ init_gdt() * ,而是使用用户权限的ldt */ static void -init_ldt() +init_ldt(void) { init_segment(&ldt[0], 0, 0, 0); // 代码段(cs) @@ -95,7 +96,7 @@ init_ldt() * 创建idt表,看起来挺吓人的,实际上全是按照手册抄的。 */ static void -init_idt() +init_idt(void) { // 异常处理 init_gate(idt + INT_VECTOR_DIVIDE, DA_386IGate, @@ -163,6 +164,9 @@ init_idt() hwint14, PRIVILEGE_KRNL); init_gate(idt + INT_VECTOR_IRQ8 + 7, DA_386IGate, hwint15, PRIVILEGE_KRNL); + // 系统调用 + init_gate(idt + INT_VECTOR_SYSCALL, DA_386IGate, + int_syscall, PRIVILEGE_USER); idt_ptr.pd_base = (u32)idt; idt_ptr.pd_lim = sizeof(idt) - 1; @@ -172,7 +176,7 @@ init_idt() * 初始化8259A,设置外设中断,看起来挺吓人的,实际上全是按照手册抄的。 */ static void -init_8259A() +init_8259A(void) { outb(INT_M_CTL, 0x11); // Master 8259, ICW1. outb(INT_S_CTL, 0x11); // Slave 8259, ICW1. @@ -193,10 +197,23 @@ init_8259A() outb(INT_S_CTLMASK, 0xFF); // Slave 8259, OCW1. } +static void +init_irq(void) +{ + // 开启时钟中断 + enable_irq(CLOCK_IRQ); + // 调整时钟频率至1000Hz + outb(0x43, 0x34); + outb(0x40, 1193&0xff); + outb(0x40, 1193>>8); + // 开启键盘中断 + enable_irq(KEYBOARD_IRQ); +} + /* * 内核初始化函数 */ -void cstart() +void cstart(void) { // 清屏并将光标置为开头 // ANSI转义序列,由\x1b(ascii码27,为Esc)开头的序列 @@ -214,5 +231,7 @@ void cstart() init_8259A(); kprintf("idt "); init_idt(); + kprintf("irq"); + init_irq(); kprintf("---\n"); } diff --git a/kern/syscall.c b/kern/syscall.c new file mode 100644 index 0000000..891cc4a --- /dev/null +++ b/kern/syscall.c @@ -0,0 +1,86 @@ +#include + +#include +#include +#include +#include + +static ssize_t sys_get_ticks(void); +static ssize_t sys_get_pid(void); +static ssize_t sys_read(void); +static ssize_t sys_write(void); + +ssize_t (*syscall_table[])(void) = { +[_NR_get_ticks] sys_get_ticks, +[_NR_get_pid] sys_get_pid, +[_NR_read] sys_read, +[_NR_write] sys_write, +}; + +/* + * 获取系统调用参数 + * 系统调用最多会使用6个参数 + * 0 1 2 3 4 5 + * ebx ecx edx esi edi ebp + */ +static u32 +get_arg(int order) +{ + switch (order) { + case 0: + return p_proc_ready->pcb.user_regs.ebx; + case 1: + return p_proc_ready->pcb.user_regs.ecx; + case 2: + return p_proc_ready->pcb.user_regs.edx; + case 3: + return p_proc_ready->pcb.user_regs.esi; + case 4: + return p_proc_ready->pcb.user_regs.edi; + case 5: + return p_proc_ready->pcb.user_regs.ebp; + default: + panic("invalid order! order: %d", order); + } +} +/* + * ssize_t get_ticks(void) + * 获取当前的时间戳 + */ +static ssize_t +sys_get_ticks(void) +{ + return do_get_ticks(); +} + +/* + * int get_pid(void) + * 获取当前进程的进程号 + */ +static ssize_t +sys_get_pid(void) +{ + return do_get_pid(); +} + +/* + * ssize_t read(int fd, void *buf, size_t count) + * 目前在这个实验中这个系统调用的作用是从键盘中得到输入 + * 写入用户进程中以buf为开头的长度为count字节的缓冲区,返回值为写入的字节数 + */ +static ssize_t +sys_read(void) +{ + return do_read(get_arg(0), (void *)get_arg(1), get_arg(2)); +} + +/* + * ssize_t write(int fd, const void *buf, size_t count) + * 目前在这个实验中这个系统调用的作用是往终端输出字符 + * 输出用户进程中以buf为开头的长度为count字节的缓冲区,返回值为写出去的字节数 + */ +static ssize_t +sys_write(void) +{ + return do_write(get_arg(0), (const void *)get_arg(1), get_arg(2)); +} \ No newline at end of file diff --git a/kern/time.c b/kern/time.c index 385eea4..a100769 100644 --- a/kern/time.c +++ b/kern/time.c @@ -1,6 +1,7 @@ -#include "type.h" -#include "time.h" -#include "x86.h" +#include + +#include +#include static size_t timecounter; @@ -8,7 +9,7 @@ static size_t timecounter; * 时间戳加一 */ void -timecounter_inc() +timecounter_inc(void) { timecounter++; } @@ -17,20 +18,13 @@ timecounter_inc() * 获取内核当前的时间戳 */ size_t -clock() +kern_get_ticks(void) { return timecounter; } -void -change_8253Counter0(int hertz) { - outb(TIMER_MODEREG, TIMER_MODE); - outb(TIMER0, (u8)(TIMER_FREQ / hertz)); - outb(TIMER0, (u8)((TIMER_FREQ / hertz) >> 8)); - /* - 8253's counter is 2bytes long - port 0x43 be the Mode Control Register - Orange book writes 00_11_010_0 into this reg, which means - SelectCounter0 | RW low byte first and then high byte | rate generator mode | binary counter - */ +ssize_t +do_get_ticks(void) +{ + return (ssize_t)kern_get_ticks(); } \ No newline at end of file diff --git a/kern/trap.c b/kern/trap.c index fdd6c01..d2c373b 100644 --- a/kern/trap.c +++ b/kern/trap.c @@ -1,11 +1,15 @@ -#include "assert.h" -#include "process.h" -#include "stdio.h" -#include "time.h" -#include "trap.h" -#include "x86.h" -#include "keymap.h" -#include "keyboard.h" +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include /* * 当前内核需要处理中断的数量 @@ -14,7 +18,7 @@ int k_reenter; void (*irq_table[16])(int) = { clock_interrupt_handler, - keyboard_interrupt_handler, + kb_interrupt_handler, default_interrupt_handler, default_interrupt_handler, default_interrupt_handler, @@ -73,59 +77,108 @@ default_interrupt_handler(int irq) void exception_handler(int vec_no, int err_code, int eip, int cs, int eflags) { - char err_description[][64] = { "#DE Divide Error", - "#DB RESERVED", - "— NMI Interrupt", - "#BP Breakpoint", - "#OF Overflow", - "#BR BOUND Range Exceeded", - "#UD Invalid Opcode (Undefined Opcode)", - "#NM Device Not Available (No Math Coprocessor)", - "#DF Double Fault", - " Coprocessor Segment Overrun (reserved)", - "#TS Invalid TSS", - "#NP Segment Not Present", - "#SS Stack-Segment Fault", - "#GP General Protection", - "#PF Page Fault", - "— (Intel reserved. Do not use.)", - "#MF x87 FPU Floating-Point Error (Math Fault)", - "#AC Alignment Check", - "#MC Machine Check", - "#XF SIMD Floating-Point Exception" - }; - panic("\x1b[H\x1b[2JException! --> %s\nEFLAGS: %x CS: %x EIP: %x\nError code: %x", - err_description[vec_no], eflags, cs, eip, err_code); + static char err_description[][64] = { + "#DE Divide Error", + "#DB RESERVED", + "— NMI Interrupt", + "#BP Breakpoint", + "#OF Overflow", + "#BR BOUND Range Exceeded", + "#UD Invalid Opcode (Undefined Opcode)", + "#NM Device Not Available (No Math Coprocessor)", + "#DF Double Fault", + " Coprocessor Segment Overrun (reserved)", + "#TS Invalid TSS", + "#NP Segment Not Present", + "#SS Stack-Segment Fault", + "#GP General Protection", + "#PF Page Fault", + "— (Intel reserved. Do not use.)", + "#MF x87 FPU Floating-Point Error (Math Fault)", + "#AC Alignment Check", + "#MC Machine Check", + "#XF SIMD Floating-Point Exception" + }; + #define PANIC_STR_SIZE 256 + static char fmtstr[PANIC_STR_SIZE]; + + if (vec_no == INT_VECTOR_PAGE_FAULT) { + char *p_str = fmtstr; + p_str += snprintf( + p_str, PANIC_STR_SIZE - (p_str - fmtstr) - 1, + "\x1b[H\x1b[2JOh, you receive a page fault!\n"); + p_str += snprintf( + p_str, PANIC_STR_SIZE - (p_str - fmtstr) - 1, + "CS: %%x EIP: %%x You tried to access the address: %%x\n"); + if ((err_code & FEC_PR) == 0) { + p_str += snprintf( + p_str, PANIC_STR_SIZE - (p_str - fmtstr) - 1, + "You tried to access a nonexistent page!\n"); + } + if ((err_code & FEC_WR) != 0) { + p_str += snprintf( + p_str, PANIC_STR_SIZE - (p_str - fmtstr) - 1, + "You tried to write in this page!\n"); + } else { + p_str += snprintf( + p_str, PANIC_STR_SIZE - (p_str - fmtstr) - 1, + "You tried to read in this page!\n"); + } + if ((err_code & FEC_U) != 0) { + p_str += snprintf( + p_str, PANIC_STR_SIZE - (p_str - fmtstr) - 1, + "You tried to access a page in user mode!\n"); + } + panic(fmtstr, cs, eip, rcr2()); + } else { + snprintf(fmtstr, PANIC_STR_SIZE - 1, + "\x1b[H\x1b[2JException! --> " + "%%s\nEFLAGS: %%x CS: %%x EIP: %%x\nError code: %%x"); + panic(fmtstr, err_description[vec_no], + eflags, cs, eip, err_code); + } } /* - * 时钟中断处理函数 + * 时钟中断处理函数, + * 需要注意的是,在这次实验中时钟中断全程是 + * 关!中!断! + * 关!中!断! + * 关!中!断! + * 不允许任何其他中断打扰。 */ void clock_interrupt_handler(int irq) { - static unsigned int _sched_count = 0; - // kprintf("i%d", clock()); timecounter_inc(); - _sched_count ++; - if (p_proc_ready == proc_table) { - _sched_count ++; - if (_sched_count >= 20) { - p_proc_ready ++; - _sched_count = 0; - } - } else { - p_proc_ready ++; - } - if (p_proc_ready >= proc_table + PCB_SIZE) { - p_proc_ready = proc_table; + // 当一个函数的时间片全用完时 + if (--p_proc_ready->pcb.ticks == 0) { + p_proc_ready->pcb.ticks = p_proc_ready->pcb.priority; + schedule(); } } -void keyboard_interrupt_handler(int irq) { - // kprintf("K"); - u8 scode = inb(KEYBOARD_BUF_REG); - if (scode < sizeof(keymap) && keymap[scode] >= 'a' && keymap[scode] <= 'z') { - add_keyboard_buf(keymap[scode]); //only keep a-z MAKE CODE - // kprintf("%c", keymap[scode]); - } -} \ No newline at end of file +/* + * 键盘中断处理函数 + */ +void +kb_interrupt_handler(int irq) +{ + u8 ch = inb(0x60); + if ((ch & 0x80) != 0) + return; + ch = keymap[ch]; + if ('a' <= ch && ch <= 'z') + add_keyboard_buf(ch); +} + +/* + * 系统调用处理函数 + */ +void +syscall_handler(void) +{ + int syscall_id = p_proc_ready->pcb.user_regs.eax; + ssize_t ret = (*syscall_table[syscall_id])(); + p_proc_ready->pcb.user_regs.eax = ret; +} + diff --git a/lab5-finish.png b/lab5-finish.png new file mode 100644 index 0000000..dcccf69 Binary files /dev/null and b/lab5-finish.png differ diff --git a/lib/Makefrag b/lib/Makefrag new file mode 100644 index 0000000..3b91893 --- /dev/null +++ b/lib/Makefrag @@ -0,0 +1,31 @@ +OBJDIRS += lib + +$(OBJDIR)/lib/%.o: lib/%.c $(OBJDIR)/.vars.CFLAGS + @echo + cc $< + @mkdir -p $(@D) + @$(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR)/lib/kern/%.o: lib/kern/%.c $(OBJDIR)/.vars.CFLAGS + @echo + cc $< + @mkdir -p $(@D) + @$(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR)/lib/user/%.o: lib/user/%.c $(OBJDIR)/.vars.CFLAGS + @echo + cc $< + @mkdir -p $(@D) + @$(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR)/lib/%.o: lib/%.asm + @echo + as obj $< + @mkdir -p $(@D) + @$(AS) -f elf -o $@ $< + +$(OBJDIR)/lib/kern/%.o: lib/kern/%.asm + @echo + as obj $< + @mkdir -p $(@D) + @$(AS) -f elf -o $@ $< + +$(OBJDIR)/lib/user/%.o: lib/user/%.asm + @echo + as obj $< + @mkdir -p $(@D) + @$(AS) -f elf -o $@ $< diff --git a/lib/terminal.c b/lib/kern/terminal.c similarity index 97% rename from lib/terminal.c rename to lib/kern/terminal.c index 007e8b8..5cc4896 100644 --- a/lib/terminal.c +++ b/lib/kern/terminal.c @@ -1,10 +1,12 @@ -#include "assert.h" -#include "stdio.h" -#include "string.h" -#include "stdarg.h" -#include "terminal.h" -#include "type.h" -#include "x86.h" +#include +#include +#include +#include +#include + +#include +#include +#include inline static void write_to_terminal(u16 disp_pos, u16 content) @@ -15,6 +17,8 @@ write_to_terminal(u16 disp_pos, u16 content) } struct kprintfbuf { + // 锁 + u32 lock; // 通用 u16 color; i16 cursor_row; @@ -353,11 +357,13 @@ kprintfputch(int ch, struct kprintfbuf *b) int vkprintf(const char *fmt, va_list ap) { + while (xchg(&TTY.lock, 1) == 1); + TTY.cnt = 0; - vprintfmt((void*)kprintfputch, &TTY, fmt, ap); - + int rc = TTY.cnt; + xchg(&TTY.lock, 0); return rc; } diff --git a/lib/libch4Core.a b/lib/libch4Core.a deleted file mode 100644 index 1b7e0d1..0000000 Binary files a/lib/libch4Core.a and /dev/null differ diff --git a/lib/printfmt.c b/lib/printfmt.c index 45c708d..dd259cd 100644 --- a/lib/printfmt.c +++ b/lib/printfmt.c @@ -1,7 +1,7 @@ -#include "type.h" -#include "stdio.h" -#include "string.h" -#include "stdarg.h" +#include +#include +#include +#include /* * 从xv6中抄过来的printf,感觉非常好用。 diff --git a/lib/user/assert.c b/lib/user/assert.c new file mode 100644 index 0000000..8a074c8 --- /dev/null +++ b/lib/user/assert.c @@ -0,0 +1,40 @@ +#include + +#include + +/* + * 当发生不可挽回的错误时就打印错误信息并使进程死循环 + */ +void +_panic(const char *file, int line, const char *fmt,...) +{ + va_list ap; + + va_start(ap, fmt); + printf("\x1b[0m\x1b[91muser panic at %s:%d: ", file, line); + vprintf(fmt, ap); + printf("\n\x1b[0m"); + va_end(ap); + + fflush(); + // 休眠CPU核,直接罢工 + while(1) + /* do nothing */; +} + +/* + * 很像panic,但是不会休眠进程,就是正常打印信息 + */ +void +_warn(const char *file, int line, const char *fmt,...) +{ + va_list ap; + + va_start(ap, fmt); + printf("\x1b[0m\x1b[93muser warning at %s:%d: ", file, line); + vprintf(fmt, ap); + printf("\n\x1b[0m"); + va_end(ap); + + fflush(); +} \ No newline at end of file diff --git a/lib/user/astart.asm b/lib/user/astart.asm new file mode 100644 index 0000000..888cecf --- /dev/null +++ b/lib/user/astart.asm @@ -0,0 +1,8 @@ +[section .text] + +extern main +global _start + +_start: ; 第一条指令 + call main + jmp $ \ No newline at end of file diff --git a/lib/user/stdio.c b/lib/user/stdio.c new file mode 100644 index 0000000..7b46ccb --- /dev/null +++ b/lib/user/stdio.c @@ -0,0 +1,102 @@ +#include + +#include +#include + +#define PRINTFBUF_SIZE 4096 + +struct printfbuf { + u32 lock; + char buf[PRINTFBUF_SIZE]; + char *buf_p; + int cnt; +}; + +static struct printfbuf printfb = { + .buf_p = printfb.buf +}; + +static void +printfputch(int ch, struct printfbuf *b) +{ + b->cnt++; + *b->buf_p++ = (char)ch; + if (b->buf_p == b->buf + PRINTFBUF_SIZE) { + write(STDOUT, b->buf, sizeof(b->buf)); + b->buf_p = b->buf; + } +} + +int +vprintf(const char *fmt, va_list ap) +{ + struct printfbuf *b = &printfb; + + // 上个自旋锁,保证线程安全(不建议用中断)计组课上讲过,相信大家的记忆力 + while (xchg(&b->lock, 1) == 1); + + b->cnt = 0; + vprintfmt((void *)printfputch, b, fmt, ap); + + int rc = b->cnt; + xchg(&b->lock, 0); + + return rc; +} + +int +printf(const char *fmt, ...) +{ + va_list ap; + int rc; + + va_start(ap, fmt); + rc = vprintf(fmt, ap); + va_end(ap); + + return rc; +} + +/* + * 将printf的缓冲区全写出去 + */ +void +fflush() +{ + struct printfbuf *b = &printfb; + write(STDOUT, b->buf, b->buf_p - b->buf); + b->buf_p = b->buf; +} + + +#define GETCHBUG_SIZE 1024 +struct getchbuf { + u32 lock; + u8 buf[GETCHBUG_SIZE]; + u8 *st, *en; +}; + +static struct getchbuf getchb = { + .st = getchb.buf, + .en = getchb.buf, +}; + +u8 +getch() +{ + struct getchbuf *b = &getchb; + + // 上个自旋锁,保证线程安全(不建议用中断)计组课上讲过,相信大家的记忆力 + while (xchg(&b->lock, 1) == 1); + + if (b->st == b->en) { + b->st = b->en = b->buf; + b->en += read(STDIN, b->buf, sizeof(b->buf)); + } + + u8 rc = b->st == b->en ? -1 : *b->st++; + + xchg(&b->lock, 0); + + return rc; +} \ No newline at end of file diff --git a/lib/user/syscall.c b/lib/user/syscall.c new file mode 100644 index 0000000..6162b13 --- /dev/null +++ b/lib/user/syscall.c @@ -0,0 +1,116 @@ +#include + +// 第一个冒号区间:汇编命令 +// 第二个冒号区间:输出操作符扩展 +// "=a" 这条命令结束后将eax寄存器的值赋给ret变量 +// 第三个冒号区间:输入操作符扩展 +// "a" 将变量赋给eax寄存器 +// "b" 将变量赋给ebx寄存器 +// "c" 将变量赋给ecx寄存器 +// "d" 将变量赋给edx寄存器 +// "S" 将变量赋给esi寄存器 +// "D" 将变量赋给edi寄存器 +// 第四个冒号区间:指令约束,当汇编指令会修改相关操作时需要修改 +// 最主要是防被gcc编译器优化 +// "a/b/c/d/S/D" 提醒编译器这些寄存器的值会在汇编指令执行中被修改 +// "cc" 提醒编译器eflags寄存器的值在汇编指令执行中会被修改 +// "memory" 提醒编译器内存中的数据在汇编指令执行中会被修改 + +ssize_t +syscall0(size_t NR_syscall) +{ + ssize_t ret; + asm volatile("int $0x80" + : "=a"(ret) + : "a" (NR_syscall) + : "cc", "memory"); + return ret; +} + +ssize_t +syscall1(size_t NR_syscall, size_t p1) +{ + ssize_t ret; + asm volatile("int $0x80" + : "=a"(ret) + : "a" (NR_syscall), + "b" (p1) + : "cc", "memory"); + return ret; +} +ssize_t syscall2(size_t NR_syscall, size_t p1, size_t p2) +{ + ssize_t ret; + asm volatile("int $0x80" + : "=a"(ret) + : "a" (NR_syscall), + "b" (p1), + "c" (p2) + : "cc", "memory"); + return ret; +} +ssize_t syscall3(size_t NR_syscall, size_t p1, size_t p2, size_t p3) +{ + ssize_t ret; + asm volatile("int $0x80" + : "=a"(ret) + : "a" (NR_syscall), + "b" (p1), + "c" (p2), + "d" (p3) + : "cc", "memory"); + return ret; +} +ssize_t syscall4(size_t NR_syscall, size_t p1, size_t p2, + size_t p3, size_t p4) +{ + ssize_t ret; + asm volatile("int $0x80" + : "=a"(ret) + : "a" (NR_syscall), + "b" (p1), + "c" (p2), + "d" (p3), + "S" (p4) + : "cc", "memory"); + return ret; +} +ssize_t syscall5(size_t NR_syscall, size_t p1, size_t p2, + size_t p3, size_t p4, size_t p5) +{ + ssize_t ret; + asm volatile("int $0x80" + : "=a"(ret) + : "a" (NR_syscall), + "b" (p1), + "c" (p2), + "d" (p3), + "S" (p4), + "D" (p5) + : "cc", "memory"); + return ret; +} + +ssize_t +get_ticks() +{ + return syscall0(_NR_get_ticks); +} + +ssize_t +get_pid() +{ + return syscall0(_NR_get_pid); +} + +ssize_t +read(int fd, void *buf, size_t count) +{ + return syscall3(_NR_read, fd, (size_t)buf, count); +} + +ssize_t +write(int fd, const void *buf, size_t count) +{ + return syscall3(_NR_write, fd, (size_t)buf, count); +} \ No newline at end of file diff --git a/memory_in_kernel.png b/memory_in_kernel.png new file mode 100644 index 0000000..6912166 Binary files /dev/null and b/memory_in_kernel.png differ diff --git a/now_memory.png b/now_memory.png new file mode 100644 index 0000000..a3784b6 Binary files /dev/null and b/now_memory.png differ diff --git a/user/Makefrag b/user/Makefrag new file mode 100644 index 0000000..e12a7ac --- /dev/null +++ b/user/Makefrag @@ -0,0 +1,37 @@ +OBJDIRS += user + +USER_LIBSRCS:= lib/printfmt.c \ + lib/string.c \ + lib/user/assert.c \ + lib/user/astart.asm \ + lib/user/stdio.c \ + lib/user/syscall.c \ + +USER_LIBOBJS := $(patsubst %.c, $(OBJDIR)/%.o, $(USER_LIBSRCS)) +USER_LIBOBJS := $(patsubst %.asm, $(OBJDIR)/%.o, $(USER_LIBOBJS)) + +USER_SRCS := user/testpid.c \ + user/testkey.c \ + user/delay.c \ + user/snake.bin \ + +USER_OBJS := $(patsubst %.c, $(OBJDIR)/%.o, $(USER_SRCS)) + +USER_BINS := $(patsubst %.bin, $(OBJDIR)/%.bin, $(USER_SRCS)) +USER_BINS := $(patsubst %.c, $(OBJDIR)/%.bin, $(USER_SRCS)) + +$(OBJDIR)/user/%.o: user/%.c $(OBJDIR)/.vars.CFLAGS + @echo + cc $< + @mkdir -p $(@D) + @$(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR)/user/%.bin: user/%.bin + @echo + cp $< + @echo 'd2dcd918806dfd71bb55e52a4081e95e' > $(OBJDIR)/.snakemd5 + @md5sum $< | awk '{print $$1}' | cmp -s $(OBJDIR)/.snakemd5 || (echo 'you are a cheater!!!'; exit 1;) + @mkdir -p $(@D) + @cp $< $(@D) + +$(OBJDIR)/user/%.bin: $(OBJDIR)/user/%.o $(USER_LIBOBJS) $(OBJDIR)/.vars.LDFLAGS + @echo + ld $@ + @$(LD) $(LDFLAGS) -o $@ $< $(USER_LIBOBJS) $(GCC_LIB) \ No newline at end of file diff --git a/user/delay.c b/user/delay.c new file mode 100644 index 0000000..c177b72 --- /dev/null +++ b/user/delay.c @@ -0,0 +1,22 @@ +#include + +#include +#include + +int main() +{ + int pid = get_pid(); + int target_ticks = (pid + 1) * 1000; + + while(1) { + printf("\x1b[%dmI'm %d!\x1b[0m", 90 + pid + 1, pid); + fflush(); + int before_ticks = get_ticks(); + //delay_ticks(target_ticks); + int after_ticks = get_ticks(); + + int real_ticks = after_ticks - before_ticks; + int delta_ticks = real_ticks - target_ticks; + assert(-5 <= delta_ticks && delta_ticks <= 5); + } +} \ No newline at end of file diff --git a/user/snake.bin b/user/snake.bin new file mode 100644 index 0000000..30d3127 Binary files /dev/null and b/user/snake.bin differ diff --git a/user/testkey.c b/user/testkey.c new file mode 100644 index 0000000..e1dbacd --- /dev/null +++ b/user/testkey.c @@ -0,0 +1,12 @@ +#include + +int main() +{ + while(1) { + u8 ch = getch(); + if (ch == 255) + continue; + printf("%c", ch); + fflush(); + } +} \ No newline at end of file diff --git a/user/testpid.c b/user/testpid.c new file mode 100644 index 0000000..ab6b052 --- /dev/null +++ b/user/testpid.c @@ -0,0 +1,13 @@ +#include +#include + +int main() +{ + while (1) { + printf("pid: %d!", get_pid()); + fflush(); + for (int i = 0 ; i < (int)1e8 ; i++) + ;//do nothing + } + return 0; +} \ No newline at end of file diff --git a/实验五-v0.3.md b/实验五-v0.3.md new file mode 100644 index 0000000..a5cc059 --- /dev/null +++ b/实验五-v0.3.md @@ -0,0 +1,359 @@ +
+ 实验五 系统调用、页表 +
+ +
+ 谷建华 +
+ +
+ 2022-10-28 v0.3 +
+ +#### 实验目的 + +1. 学习用户态执行系统调用到内核态处理的整个过程. +2. 学习页表,理解如何从线性地址经过页表映射到物理地址实现进程间的内存资源隔离. +3. 学习页表项相关位的功能,如何维护页表数据结构. + +#### 实验预习内容 + +1. 系统调用规范. +2. 页表数据结构. +3. 页表项的组成. + +#### 实验内容 + +1. 补全`main.c`中的代码,为每个用户进程加载在磁盘中的ELF程序文件到用户地址空间. + (1) `main.c`中用户态的程序加载方式由原先加载kernel中的函数变成加载用户ELF文件的方式,你需要添加相应代码实现用户态程序加载功能. + (2) 我们将文件系统读取函数`read_file`准备好,给定短目录项名和加载的地址该函数会将指定文件加载到地址中. + (3) 我们将内核段地址映射函数`map_kern`准备好,它会将`3GB + 0MB ~ 3GB + 128MB`的线性地址(内存分布模型见实验参考最后一节)映射到`0MB ~ 128MB`的物理地址,供大家学习页表的初始化过程. + (4) `phy_malloc_4k`是一个非常简单的内存分配函数,它会分配物理地址`64MB ~ 128MB`区间的的内存,每次分配4KB,一个页面大小(暂时可以不用考虑内存回收的情况,只需要光分配就行了). + (5) 你需要做的是将用户ELF文件从文件系统中读取,并解析ELF文件将程序段正确加载到每个进程中的地址空间,使得能够运行用户态程序.我们准备了两个用户测例文件`testpid.bin`和`testkey.bin`,第一个测试`get_pid`系统调用,第二个测试键盘输入,**需要验证两个程序能够并发地执行**. + (6) 如果你在实现中遇到了`Page Fault`,那么基本上意味着你加载错了,这个时候请根据报错的`eip`信息(pc程序计数器)然后对ELF文件进行反汇编将触发错误的指令找到,必要的时候需要通过`gdb`一条汇编一条汇编地执行直到找到问题.跟页表打交道非常容易出现这种问题,请耐心调试. +2. 添加`delay_ticks`系统调用. + (1) `delay_ticks`的系统调用的C语言语义如下: + ```C + // 进程休眠ticks个时钟中断 + // 系统调用默认返回0 + ssize_t delay_ticks(u32 ticks); + ``` + (2) `delay_ticks`的系统调用号自行定义,你需要写在内核`kern/syscall.c`和用户`lib/user/syscall.c`的两部分的接口. + (3) 在`user/delay.c`存放着测试该系统调用的代码,而`delay_ticks`函数被注释掉了(我们并没有给出函数接口),你需要在实现对应接口后取消注释测试用户程序`delay.bin`. + (4) 你需要开三个用户进程同时执行`delay.bin`,这三个用户进程的pid号分别为`0,1,2`,`delay.bin`会获取进程pid号来给出休眠的时长.预计效果图如下: + + ![](./lab5-finish.png) + + (5) 你需要注意一个边界情况:当三个进程都休眠了,你怎么解决?谁接替执行流?或是干脆休眠核让执行流干脆不执行直到下个中断? +3. (自我提高内容,自己想做就做,不用写进报告,**禁止内卷**)添加`mmap`系统调用对贪吃蛇程序`snake.bin`进行数据注入. + (1) 你还记得实验四的贪吃蛇吗?还是这个程序,不过这次这个程序抽风了,它的食物出现在了地图外,那岂不是永远无法游玩了?不过好在天无绝人之路,善良的设计者设计食物结构体结构如下: + ```c + struct Food { + char hint[16]; + int x; + int y; + int score; + }; + + struct Food food = { + .hint = "This is a food", + //... + }; + ``` + food的值会放在elf的data段中,相信根据elf文件的信息和上面的初始化内容你能够找到food在内存中的地址. + (2) 编写`mmap`系统调用,其C语言语义如下(非官方语义,也可以自行设计其他语义解释): + ```c + // 将pid号进程的线性地址[src, src + length)中的页面共享到该进程的线性地址[dst, dst + length) + // src,length和dst需要4096对齐 + // 由于是自行实现的,没有过多的要求. + ssize_t mmap(int pid, void *src, size_t length, void *dst); + ``` + 自行写一个hack程序,在`main.c`中创建两个用户进程`snake.bin`和自己的hack程序,hack程序通过调用`mmap`系统调用跟`snake.bin`的部分内存共享,然后hack程序修改`food`的数据让食物回到地图内(每帧会重新绘制地图).这样就可以重新游玩贪吃蛇. + +#### 实验总结 + +1. 系统调用的入口参数是如何传入内核的?返回值又是如何返回给进程的? +2. 时钟中断和系统调用对IDT的初始化有什么不同? +3. 该实验中初始化内核页表和用户页表有啥区别? + +#### 实验参考 + +##### 系统调用 + +在上一个实验中,我们可以调用`kprintf`函数实现输出的功能,我们仅仅实现了一个“虚假”的用户态,虽然段寄存器的权限在用户态,但是我们依然能够调用kernel中的函数,但是这么做有一个弊端,用户态下能够轻易调用内核的函数,这是我们所不希望的,所以我们通过页表将进程资源隔离,通过页表给用户进程戴上一个VR眼镜,这样用户进程在执行的时候就看不到内核的函数. + +但是`printf`是需要向终端输出字符的,但是终端显示被内核所掌控,这个时候用户就需要使用系统调用向内核请求服务.简单理解就是用户做不了的需要向内核申请服务. + +系统调用类似函数调用,不过它是跨越权限的,系统调用也有它的一套规范,这套规范可以阅读`man syscall`手册查看,下面是调用规范的部分截取. + +``` +Architecture calling conventions + Every architecture has its own way of invoking and passing arguments to + the kernel. The details for various architectures are listed in the + two tables below. + + The first table lists the instruction used to transition to kernel mode + (which might not be the fastest or best way to transition to the ker‐ + nel, so you might have to refer to vdso(7)), the register used to indi‐ + cate the system call number, the register(s) used to return the system + call result, and the register used to signal an error. + + Arch/ABI Instruction System Ret Ret Error Notes + call # val val2 + ─────────────────────────────────────────────────────────────────── + alpha callsys v0 v0 a4 a3 1, 6 + arc trap0 r8 r0 - - + arm/OABI swi NR - a1 - - 2 + arm/EABI swi 0x0 r7 r0 r1 - + arm64 svc #0 x8 x0 x1 - + blackfin excpt 0x0 P0 R0 - - + i386 int $0x80 eax eax edx - +``` + +###### 系统调用汇编命令 + +看最后一行,我们可以获悉linux对i386架构是调用`int $0x80`汇编指令进行系统调用,调用这条指令后就可以直接进内核态进行系统调用处理. + +在之前的学习中,我们知道`int`的命令实际上是触发中断,后面跟着的数字是中断号,所以在i386架构中,系统调用本质上跟中断类似,不过不是由硬件触发是由用户自己调用触发.如果愿意,我们可以修改系统调用的中断号,只要不发生冲突就行. + +```c +// 在start.c中系统调用初始化 +// INT_VECTOR_SYSCALL值可以任意,这次的实验用的是linux的0x80 +init_gate(idt + INT_VECTOR_SYSCALL, DA_386IGate, + int_syscall, PRIVILEGE_USER); +``` + +###### 系统调用号 + +但是要知道内核可不只有一种功能,如何获悉用户想要哪种功能就看下一列`System call #`,可以知道内核是根据`eax`寄存器的值获悉用户需要哪种功能,这个值就叫系统调用号. + +系统调用号可以有很多种规范,相同的系统调用号在不同的规范下内核会实现不同的功能,我们的实验用是自创教学用的规范,专业标准的是叫POSIX(可移植操作系统接口)规范,它的一部分是对于系统调用号对应的功能进行定义,这样只要遵循的相同的系统调用规范就能让一个相同的用户程序在不同的操作系统上运行.在`man syscalls`手册中有写到:linux的系统调用号在64位机器上系统调用号在`/usr/include/asm/unistd_64.h`文件中,数了数有四百多个,量可以说是相当大了. + +###### 系统调用其余参数 + +系统调用可不光有一个系统调用号,它还可以附带一些参数,我们继续在`man syscall`中阅读可以看到这么一段: + +``` +The second table shows the registers used to pass the system call argu‐ +ments. + + Arch/ABI arg1 arg2 arg3 arg4 arg5 arg6 arg7 Notes + ────────────────────────────────────────────────────────────── + alpha a0 a1 a2 a3 a4 a5 - + arc r0 r1 r2 r3 r4 r5 - + arm/OABI a1 a2 a3 a4 v1 v2 v3 + arm/EABI r0 r1 r2 r3 r4 r5 r6 + arm64 x0 x1 x2 x3 x4 x5 - + blackfin R0 R1 R2 R3 R4 R5 - + i386 ebx ecx edx esi edi ebp - +``` + +看最后一行,可以知道系统调用是由`ebx`, `ecx`, `edx`, `esi`, `edi`, `ebp`这六个寄存器传递参数.不像正常的函数调用传参靠栈,而是靠寄存器传递参数,而且最多只能传递6个,如果要传递复杂的结构可以考虑传入结构体指针的方式解决. + +虽然上面的规范如此,但是没有定死,我们完全可以将`ebx`作为系统调用号,`eax`作为参数,不过这么做有点吃饱了撑着不去兼容规范. + +###### 系统调用返回值以及错误码 + +再看第一张表的`Retval`,可以获悉到系统调用也是可以有返回值的,返回值一般放在`eax`寄存器,当`eax`寄存器放不下的时候会将第二部分放`edx`,但是这种情况比较罕见,所以暂且不用去理会. + +系统调用的返回值与普通函数的返回值中不太一样的一点是它会返回错误号,错误号是一类特殊的返回值,当用户请求的系统调用正确时会返回0或系统调用规定的值,但有错误(常见的比如文件路径不存在,用户越权写系统文件)时系统调用会返回错误号,错误号值的范围从-1,-2,-3...以此类推,大概有一百来个.通过错误号,用户可以知道自己调用系统调用触发了什么问题,可以阅读`man errno`手册可以获得错误号对应的语义.在终端中输入错误命令或在平常C语言代码中调用`perror`函数输出的都是根据上一个系统调用的错误码信息输出相应的字符串. + +``` +$ ls +cstat cstat.c +$ stat 114514 +stat: cannot stat '114514': No such file or directory +$ cat cstat.c +// 你如果要问我怎么知道要引入这些头文件的,直接man翻手册啊 +// 正经人谁背的住那么多头文件,不如直接翻手册 +#include +#include +#include +#include + +int main() +{ + struct stat statbuf; + stat("1919810", &statbuf); + perror(""); +} +$ ./cstat +No such file or directory +``` + +就如上面的例子,两个都是访问的不存在的文件,获取到的错误码信息为ENOENT,具体值在POSIX中值被规定成了2,相当于系统调用返回-2,它的语义就是`No such file or directory`,所以两个命令都输出了这句话. + +由于错误码有限多个,所以同一个错误码在不同系统调用中有不同的语义,你如果会去翻系统调用的手册的时候会经常发现ERRORS这一栏,这里定义了一系列可能出现的问题以及会返回的对应错误码. + +###### 如何获取程序用到的系统调用 + +当你在终端中输入一行命令后发现它报错了,你肯定会很疑惑它为什么会这么报错,这个时候一种可行的做法就是使用`strace`命令,它会截获用户程序对应的系统调用. + +```shell +$ strace stat ./114514 +# strace 输出的东西太多,就截取跟114514相关的 +execve("/usr/bin/stat", ["stat", "./114514"], 0x7ffc75313828 /* 66 vars */) = 0 +lstat("./114514", 0x7ffca6294210) = -1 ENOENT (No such file or directory) +write(2, "cannot stat './114514'", 22) = 22 +``` + +根据strace的结果可以知道哪些系统调用发生了异常,上面可以知道`lstat`这个系统调用发生了异常,返回了ENOENT,用户程序可以通过其系统调用了解其实际干了什么,这也是一种猜测用户行为debug的一种方式. + +###### MINIOS系统调用处理逻辑 + +对于我们实验的代码,系统调用分成三步处理: + ++ `sys_*` 第一层处理函数,获取系统调用中需要的寄存器参数丢给下一层处理 ++ `do_*` 第二层处理函数,会将参数做一些转换和边界判断丢给下一层处理. ++ `kern_*` 第三层处理函数,也是实际处理函数,因为学生开发总是稀里糊涂的乱用处理函数,规定在内核中如果只能调用`kern_*`处理语义相同的功能. + +这三层在代码中可以体现,整个中断的处理流程跟时钟中断非常类似,还要简单. + +```nasm +; atrap.asm中的系统调用处理函数 +; 相比于硬件中断这段代码可太简单了 +int_syscall: + call save + sti + call syscall_handler + cli + ret +``` + +进内核态后先保存用户程序上下文(寄存器),然后调用`syscall_handler`函数进行处理,处理完毕后返回用户态,就这么简单. + +`syscall_handler`仅仅是根据系统调用号判断该用什么处理函数. + +```c +static ssize_t sys_get_ticks(void); +static ssize_t sys_get_pid(void); +static ssize_t sys_read(void); +static ssize_t sys_write(void); +// 这是种有趣的写法,举个例子 +// 这种写法钦定了syscall_table的第_NR_get_ticks项与sys_get_ticks函数绑定 +ssize_t (*syscall_table[])(void) = { +[_NR_get_ticks] sys_get_ticks, +[_NR_get_pid] sys_get_pid, +[_NR_read] sys_read, +[_NR_write] sys_write, +}; + +/* + * 系统调用处理函数 + */ +void +syscall_handler() +{ + int syscall_id = p_proc_ready->pcb.user_reg.eax; + ssize_t ret = (*syscall_table[syscall_id])(); + // 将用户态的寄存器值修改,在返回用户态时返回值就自然被写到了eax上 + p_proc_ready->pcb.regs.eax = ret; +} +``` + +##### 页表 + +在实验三,我们实际上已经引入了页表用于扩展地址空间,将由实模式的段偏移寻址模式变为页表寻址增加寻址能力,但是页表映射是恒等映射,让你们觉得好像内存哪里都能访问. + +###### 线性地址和物理地址 + +我们平时写的程序访问地址都是访问线性地址,线性地址是不用关心物理内存的约束,也就是意味着我们内存128MB的虚拟机,你可以尝试访问1GB处的线性地址,如果换成物理地址,你只能访问128MB以内的地址. + +我们访问线性地址,实际上也是在访问内存中的某处的物理地址,通过页表这个数据结构将线性地址转化为物理地址,就仿佛带了个VR眼镜,你觉得你能够看到某处线性地址,但是它实际上被VR眼镜转化到某一物理地址去了,同理你带不同的VR眼镜,相同的线性地址被解析成了不同的物理地址,每个进程能够访问到的物理内存地址范围互不相同,除非通过特殊手段,一个进程是不能访问到另一个进程的地址空间,这样就实现了进程资源之间的地址空间隔离. + +###### VR眼镜(cr3) + +在i386架构采用三级页表模式:页目录、页表和物理页(这个下文会慢慢解释),cr3中存放了页目录的地址.而这个cr3寄存器,它的意义就如上文提到的VR眼镜,每一个进程会维护一个cr3的值,代表进程的地址空间,每当进程发生调度的时候,新的进程会使用`lcr3`函数将cr3寄存器值更新,相当于将cr3的值mov过去,具体见执行代码,相当于换VR眼镜的过程. + +###### 线性地址转换到物理地址 + +好了,到了最麻烦的一部分,这一小节逻辑可以说是非常绕,根据经验,页表这一部分相当容易出现理论觉得懂了看代码觉得这不简单但是实操发现自己还是啥都不懂的情况,为了能够让大家顺利做实验,我们会写的尽量详细,写代码的时候建议多读几遍这一小节. + +线性地址一共有32位,硬件工程师将这32位划分成了三个部分: + +``` +A linear address 'la' has a three-part structure as follows: + ++--------10------+-------10-------+---------12----------+ +| Page Directory | Page Table | Offset within Page | +| Index | Index | | ++----------------+----------------+---------------------+ + \--- PDX(la) --/ \--- PTX(la) --/ \---- PGOFF(la) ----/ + \---------- PGNUM(la) ----------/ +``` + +硬件工程师规定一个页面的大小为4kb,4096bytes.将一个$ 2^{32} $的区间划分成$ 2^{20} $个页面,通过32位的高10位和中10位确定线性地址对应是哪一个**物理页面**,然后根据低12位确认在页面中的偏移从而确定线性地址对应的**实际物理地址**,对线性地址内存的修改最终会映射到对物理内存的修改. + +接下来要研究怎么根据高10位和中10位确认线性地址对应的是哪个物理页面.上文中的cr3寄存器就在这派上了用场,这个寄存器里面存着的值语义是一个**物理页面**的首地址,相当于对物理内存按照4kb为一块进行划分,其中任意一块的**首地址**(物理地址,低12位为0)可以成为一个合法的cr3值,cr3寄存器的值的意义如下,其中高20位就能表达物理页面的首地址,低12位全部保留为0. + +``` ++-----------20-----------+-----12-----+ +| Physical Address of | all 0 | +| Page | | ++------------------------+------------+ +``` + +一个页面4kb大,对于32位机器来说一个指针的大小为4字节,一个页面一共能够存放$ 2^{10} $个指向下一个物理页面的“指针”,高十位和中十位两个10位的数据,分别是页目录表和页表的下标,所以以高10位作为偏移量能够在cr3对应的物理页面找到指向(假设为)物理页面1首地址的“指针”,然后再以中10位作为偏移量在物理页面1找到指向(假设为)物理页面2首地址的“指针”,物理页面2就是线性地址对应的物理页面.cr3对应的物理页面官方命名为**页目录表**,物理页面1官方命名为**页表**.如果你对字典树(Trie树)这个数据结构比较熟悉的话,你就会发现这就是一个字符集大小为$ 2^{10} $的字典树. + +指向下一物理页面的“指针”在页目录表中被命名为**页目录项**在页表中被命名为**页表项**,线性地址的高10位又称**页目录索引**,中10位又称**页表索引**.不过觉得页目录项和页表项区分开来非常别扭,它们的结构和功能都完全一致,所以我觉得可以统称为**页表项**. + +由于物理页面的首地址的**低12位全是0**,所以硬件工程师将低12位重新利用起来.所以一个页表项的组成如下: + +``` ++-----------20-----------+-----12-----+ +| Physical Address of | Flag bit | +| Page | | ++------------------------+------------+ +``` + +页表项的高20位就能表示下一个物理页面的物理地址首地址,低12位是页表项的标志位,我们写实验的时候只需要关心这几位: + ++ `#define PTE_P 0x001` 存在位(第0位),标志着这一页表项指向的物理页面是否存在,如果没有被置位就视为不存在. ++ `#define PTE_W 0x002` 读写位(第1位),标志着这一页是否可写,如果被置位意味着是读写的,如果没有被置位意味着是只读的. ++ `#define PTE_U 0x004` 用户位(第2位),标志着这一页的权限段,如果被置位意味着是用户和内核都是可以访问的,如果没有被置位意味着只有内核有权限能够访问. + +最后梳理一下,线性地址转化为物理地址的过程: + +1. 根据cr3寄存器找到页目录表. +2. 根据线性地址的高10位找到对应的页目录项. +3. 根据页目录项的标志位判断是否有该页,做权限的检测等,如果非法就直接触发页面异常`Page Fault`. +4. 如果合法,根据页目录项找到页表. +5. 根据线性地址的中10位找到对应的页表项. +6. 根据页表项的标志位判断是否有该页,做权限的检测等,如果非法就直接触发页面异常`Page Fault`. +7. 如果合法,根据页表项找到物理页. +8. 根据线性地址的低12位找到对应的偏移,至此一次访存完成. + +可以看到其实页表本质是经过多次`找页表-找页表项`的循环,对于32位指令集架构是重复了2次,对于64位指令集架构就没32位那么漂亮了,它转换一个线性地址可能要重复3次、4次. + +###### tlb缓存(科普) + +根据上面的流程,实际上为了完成线性地址一次访存,需要访存物理内存三次,这会极大增大运行时的开销,所以硬件工程师将页表信息缓存下来,这在大家的计组课上讲过叫快表tlb,下次访问相同物理页时会使用tlb的信息加速访存. + +tlb加速了访存,但是于此同时增大的维护难度,会出现你修改了内存中的页表项后尝试访问页表项对应的线性地址发现修改的内容没有起作用的情况.这个时候就需要将页表进行刷新保证下次访存不会出问题. + +刷新命令其实很简单: + +```nasm +mov eax, cr3 +mov cr3, eax +``` + +相当于对cr3做一次重载硬件就会重新刷新tlb信息. + +###### MINIOS内存模型 + +之前我们实验的内存模型是恒等映射的. + +![](./before_memory.png) + +在这个实验由于用户地址空间隔离的需要,需要将内核程序迁移到`3GB ~ 3GB + 128MB`处运行,所以在很多方面如果需要访问物理内存地址需要访问**物理地址 + 3GB**的线性地址,低3GB用于给用户程序用. + +![](./now_memory.png) + +针对这次实验,内核的内存模型如下: + +![](memory_in_kernel.png) + ++ 3GB \~ ?: 内核程序使用(会随着开发动态变化,一般不会很大,顶天3MB). ++ 3GB + 48MB \~ 3GB + 64MB: `read_file`函数将文件从磁盘读出后会将文件数据加载在此处. ++ 3GB + 64MB \~ 3GB + 128MB:这段内存是空闲的,可以用于分配给用户进程,`phy_malloc_4k`会使用此段内存用于分配页面,相当于分配`64MB ~ 128MB`的物理内存. \ No newline at end of file diff --git a/实验四-v0.3.md b/实验四-v0.3.md deleted file mode 100644 index 6c954d5..0000000 --- a/实验四-v0.3.md +++ /dev/null @@ -1,305 +0,0 @@ -
- 实验四 中断 -
- -
- 谷建华 -
- -
- 2022-10-14 v0.3 -
- -#### 实验目的 - -1. 学习中断描述符,中断处理全流程(特别是执行流、堆栈、权限的切换),包括中断描述符表IDT的功能和设置 -2. 学习时钟中断和键盘中断的处理过程 -3. 学习分时任务调度 - -#### 实验预习内容 - -1. 中断描述表IDT -2. 8259A设置外设中断 -3. 分时任务调度原理 -4. 键盘中断的字模的获取和处理 - -#### 实验内容 - -1. 验证时钟中断的发生 - (1) 编译运行实验默认给的源码,观察并分析现象 - (2) 修改时钟中断处理程序,使之从输出`#`变成输出`i*(*为第几次中断)`,编译运行后观察并分析现象 -2. 修改时钟中断触发时的调度机制,让某个进程被分配到更少的时间片达成进程饥饿的目的(进程必须得活着的能够输出,饥饿不等于删掉进程),编译运行后观察并分析现象. -3. 阅读Orange第6章内容,修改硬件时钟频率,使之大约以1000Hz的频率触发时钟中断,编译运行后观察证实你新修改的频率要比原来的快(这个频率很难测量,只能通过对比的方式估计) -4. 阅读Orange第7章内容并结合实验参考,添加键盘中断,要求如下: - (1) 使之能够正确识别`a`\~`z`(不需要结合shift键区分大小写,就算是shift+`a`也识别为`a`而不是`A`),其余字符一概丢掉.将识别出来的字符通过调用`keyboard.c`中的`add_keyboard_buf`函数将字符添加到字符缓冲区中(需学生自行实现该函数). - (2) 修改`keyboard.c`中的`getch`函数,这是一个非阻塞的函数,即当缓冲区中有字符未输出时,输出缓冲区队头该字符,并将缓冲区队头字符弹出,当缓冲区中没有字符时输出255(u8意义下的-1). - (3) 我们准备了一个贪吃蛇(没错,有了时钟中断和键盘中断就能做一个小游戏了!),修改时钟频率至1000Hz,删除时钟中断处理函数中的所有`kprintf`,在`kernel_main`中仅创建一个单进程,进程入口函数会在`game.h`中给出.最后重新编译kernel就可以游玩了(不用将这一步写进报告). - -#### 实验总结 - -1. 操作系统内核的初始化阶段都完成哪些主要功能? -2. 刚刚进入时钟中断处理程序时系统用的是哪个堆栈,中断号是多少?执行流在哪个位置?在执行中断服务程序的过程中堆栈发生哪些变化?`call [irq_tabel + 4 * %1]`的功能是什么? -3. 外设(时钟、键盘等)中断的中断号是由谁决定的?在哪里决定的? - -#### 实验参考 - -在前几个实验,我们一直是在内核态,而我们的进程一般都是在用户态下执行的,这样进程做出出格的事情也不会伤到内核.那么接下来需要研究`kernel_main`函数是怎么进入用户态.这次实验的重点是从`restart`函数出发进入到用户态,然后又因为中断回到内核态这一整个过程. - -##### 1. 中断初始化 - -###### 如何区分当前执行流为用户态和内核态 - -平时都说用户态,内核态,但是怎么区分执行流目前的状态靠的就是段寄存器,可以发现段描述符的大小刚好是8字节,所以存储在段寄存器中的段选择子值假设是$ x $,那么$ \lfloor\frac{x}{8}\rfloor $就能够描述选择的是第几个段,即在二进制角度看段选择子的低三位就没有被用上,所以硬件工程师就考虑把这些位利用上,第0~1位用于权限,第2位用于标识段是全局段还是局部段. - -对于权限的划分各位需要阅读Orange教材,这里不细展开,总之靠着段选择子的第0~1位可以划分当前段的权限,当权限为用户态时执行流(CS)就是用户态. - -###### LDT初始化 - -LDT(local descriptor table)全称局部描述符表,跟GDT很类似,为什么需要LDT是因为在之前可能不同的任务它们的段寄存器可能会不同,为了区分,每个任务有它自己的独一套LDT,这样切换不同任务时标识更容易些.虽然理论上每个任务都有自己的独一套LDT,但是都是$ 2^{32} $寻址,限制都是靠分页做的(这个我们下个实验再说).所以我们只需要加载一次ldt就能满足所有用户态进程的需求. - -为了方便大家理解段之间的区别,这里我们约定GDT里面全是存储内核态会用到的段描述符(除了显存段),LDT里面存储用户态会用到的段描述符. - -```C -// 这句话初始化了ldt的段描述符 -init_segment(&gdt[5], (u32)ldt, sizeof(ldt) - 1, DA_LDT); -// 在加载了gdt后,ldt就可以通过传入段选择子的方法加载 -lgdt [gdt_ptr] ; 使用新的GDT -lldt [SELECTOR_LDT] ; SELECTOR_LDT = 0x28 = 40 = 5 * 8 -``` - -###### 中断 - -这一次实验我们要开始处理中断了,平时课上也讲过,执行流在用户态的时候肯定不能放心一直将执行流交给用户态(用户不能关中断IF位,要不然就永远无法响应中断了).比如时钟中断,每次执行固定时长后硬件会向内核发送一次中断,在触发中断异常/用户请求系统调用的时候能够回到内核态,不同的触发方式会执行不同的处理函数,那么如何区分这些不同的触发方式就需要IDT了. - -###### IDT初始化 - -IDT(Interrupt Descriptor Table)中断描述符表会根据中断的形式决定进入内核的中断处理函数,而区分形式是靠中断号判断,根据中断号找到对应的门描述符,根据中断描述符找到对应的中断处理函数入口和加载对应的CS段寄存器,将执行流交给内核. - -类似gdt, idt需要通过`lidt`命令将idt表的数据结构载入,该数据结构与上一个实验的`gdt_ptr`一致. - -```nasm - lidt [idt_ptr] -``` - -###### 8259A初始化 - -8259A简单来说是外设中断的实际处理硬件,时钟中断,键盘中断,鼠标中断等都是靠它给内核发送信号触发中断,它也需要初始化与中断描述符之间的联系. - -##### 2. 中断处理过程 - -###### 进入用户态 - -在内核中,每个进程需要维护一个用于存放进程用户态当前状态的寄存器表,当用户态因为某些原因陷入内核态时用户态的当前所有寄存器信息就存放在寄存器表中,当从内核态又回到用户态时就根据寄存器表恢复用户态当前的状态. - -```C -typedef struct s_stackframe { - u32 gs; - u32 fs; - u32 es; - u32 ds; - u32 edi; - u32 esi; - u32 ebp; - u32 kernel_esp; - u32 ebx; - u32 edx; - u32 ecx; - u32 eax; - u32 retaddr; - u32 eip; - u32 cs; - u32 eflags; - u32 esp; - u32 ss; -}STACK_FRAME; -``` - -这个就是我们这个实验会用到寄存器表,`gs`在低地址,`ss`在高地址,接下来结合源码分析进入用户态这个过程中寄存器的变化. - -在`kernel_main`中,我们需要对寄存器做一次初始化: - -```c -p_proc->regs.cs = (SELECTOR_FLAT_C & SA_RPL_MASK & SA_TI_MASK) - | SA_TIL | RPL_USER; -p_proc->regs.ds = (SELECTOR_FLAT_RW & SA_RPL_MASK & SA_TI_MASK) - | SA_TIL | RPL_USER; -p_proc->regs.es = (SELECTOR_FLAT_RW & SA_RPL_MASK & SA_TI_MASK) - | SA_TIL | RPL_USER; -p_proc->regs.fs = (SELECTOR_FLAT_RW & SA_RPL_MASK & SA_TI_MASK) - | SA_TIL | RPL_USER; -p_proc->regs.ss = (SELECTOR_FLAT_RW & SA_RPL_MASK & SA_TI_MASK) - | SA_TIL | RPL_USER; -p_proc->regs.gs = (SELECTOR_VIDEO & SA_RPL_MASK & SA_TI_MASK) - | RPL_USER; - -p_proc->regs.eip = (u32)entry[i]; -p_stack += STACK_PREPROCESS; -p_proc->regs.esp = (u32)p_stack; -p_proc->regs.eflags = 0x1202; /* IF=1, IOPL=1 */ -``` - -这里可以看到初始化的段寄存器中除了`gs`都有`SA_TIL`标志位,它的实际值是4,即二进制意义下的第2位,标志着这个段是选择的是ldt中的段,而ldt中的段都是用户态权限的,所以在进入到用户态时执行流权限就自动切换到用户态. - -再之后就是eip,这是执行流的寄存器,esp用于分配栈,eflags用于初始化flags信息. - -在`kernel_main`初始化完后会调用`restart`函数进入用户态,这是一个汇编接口函数,关键的代码如下: - -```nasm -restart: - mov esp, [p_proc_ready] - lea eax, [esp + P_STACKTOP] - mov dword [tss + TSS3_S_SP0], eax -restart_reenter: ; 我们的代码从这里开始分析,上面的等下会讲 - cli - dec dword [k_reenter] - pop gs - pop fs - pop es - pop ds - popad - add esp, 4 - iretd -``` - -先是关中断,我们肯定不希望在恢复用户态寄存器信息时被意外的中断干扰,再接下来是`k_reenter`减1(这个本参考不讲,自行阅读源码),之后开始恢复寄存器信息.先是恢复`gs`\~`ds`一共四个段寄存器信息.再是恢复`edi`\~`eax`这八个寄存器,需要注意的是`kernel_esp`比较特殊,它实际上不起恢复作用,因为现在的`esp`还不是用户态`esp`,而且`popad`指令会略过`esp`的恢复.再是`esp`加4跳过`retaddr`(这个变量是用于`save`这个函数,它存储的是`call save`时`ret`的地址),最后调用`iret`将`eip`\~`ss`这五个寄存器恢复(为什么让这五个寄存器单独用特殊指令恢复原因是这五个与执行流密切相关),由于eflags中IF位被置1中断被重新打开. - -###### 返回内核态 - -执行流肯定不能一直留在用户态,在接受中断的时候需要再次陷入内核态.再次陷入内核态后,硬件保证了在进入中断时eflags的中断IF位为0,不会受到其余中断的影响,这个时候内核调用了`call`函数保存`eax`\~`gs`寄存器(为什么不保存`ss`\~`eip`这五个寄存器在第3部分会讲到): - -```nasm -save: - pushad ; `. - push ds ; | - push es ; | 保存原寄存器值 - push fs ; | - push gs ; / - mov dx, ss - mov ds, dx - mov es, dx - - mov eax, esp ;eax = 进程表起始地址 - - inc dword [k_reenter] ;k_reenter++; - cmp dword [k_reenter], 0 ;if(k_reenter ==0) - jne .1 ;{ - mov esp, StackTop ; mov esp, StackTop <--切换到内核栈 - push restart ; push restart - jmp [eax + RETADR - P_STACKBASE]; return; -.1: ;} else { 已经在内核栈,不需要再切换 - push restart_reenter ; push restart_reenter - jmp [eax + RETADR - P_STACKBASE]; return; - ;} -``` - -在进入`call`函数中,`ret`的返回地址被存入了寄存器表中`retaddr`的位置,然后调用了`pushad`将`eax`\~`edi`存入寄存器表中,最后将其余段寄存器存入表中,这段代码最后的两个jmp是值得讲的,这个时候别傻乎乎用`ret`指令,返回地址实际上`retaddr`中存着,`ret`的话会把`restart`或`restart_reenter`当返回地址了. - -###### 屏蔽中断和置EOI - -```nasm -in al, INT_M_CTLMASK ; `. -or al, (1 << %1) ; | 屏蔽当前中断 -out INT_M_CTLMASK, al ; / -mov al, EOI ; `. 置EOI位 -out INT_M_CTL, al ; / -sti ; CPU在响应中断的过程中会自动关中断,这句之后就允许响应新的中断 -``` - -在保存完寄存器后,需要修改中断掩码使得不再相应相同类型的中断,保证在内核中不会被同类中断干扰.然后还得向`INT_M_CTL`端口发送EOI信号,告诉硬件已经做好准备了,可以接受下一个中断了(有可能在`sti`之后马上又被下一个中断打扰). - -###### 重新进入用户态 - -再接下来就是中断处理程序的调用了,在处理完中断后就可以准备返回用户态了: - -```nasm -cli -in al, INT_M_CTLMASK ; `. -and al, ~(1 << %1) ; | 恢复接受当前中断 -out INT_M_CTLMASK, al ; / -ret -``` - -跟上节做的事情相反,将目标中断恢复接受,然后使用ret指令,还记得`save`函数里面的`push restart`和`push restart_reenter`两个指令吗?这个push的地址值就是为了`ret`准备的,`ret`过后会重新回到`restart`,然后最终回到用户态. - -##### 3. TSS机制 - -TSS书上写的很玄乎,很难理解,但是实际上TSS没有那么难,举个实例就可以很清晰的知道TSS的作用,假设我们在用户态执行的程序突然受到一个中断要返回内核态,那么这个时候肯定不能就着用户态的esp存储寄存器信息,需要切换到一个特定的栈(内核栈)存储寄存器信息,那么这个内核栈的ss和esp需要预先存储到一个特定地方用于进入内核态时切换(没错,需要段寄存器,因为用户的段寄存器是低权限的,如果访问内核栈会违反保护模式qemu直接重开,你会看到终端不断闪现boot信息),而这个存放的位置就是TSS,TSS里面存放很多数据,看起来很吓人,但是实际上现在我们只会使用其中的`ss0`和`esp0`(0是内核权限级),当从用户态进入到内核态时,ss和esp会切换内核态的对应寄存器,这个时候就能正常执行内核程序. - -TSS是一个段,存放在gdt表中(标识为`DA_386TSS`).下面是gdt表中TSS段的初始化: - -```c -tss.ss0 = SELECTOR_FLAT_RW; //ss0的初始化在这里完成 -tss.iobase = sizeof(tss); /* 没有I/O许可位图 */ -init_segment(&gdt[4], (u32)&tss, sizeof(tss) - 1, DA_386TSS); -``` - -在初始化完TSS段之后需要通过`ltr`加载TSS选择子让硬件知晓TSS段. - -```nasm -_start: - ; 把 esp 从 LOADER 挪到 KERNEL - mov esp, StackTop ; 堆栈在 bss 段中 - - call cstart ; 在此函数中改变了gdt_ptr,让它指向新的GDT - - lgdt [gdt_ptr] ; 使用新的GDT - lldt [SELECTOR_LDT] - lidt [idt_ptr] - - jmp SELECTOR_KERNEL_CS:csinit -csinit: ; “这个跳转指令强制使用刚刚初始化的结构”——<> P90. - - xor eax, eax - mov ax, SELECTOR_TSS ; 选择TSS选择子 - ltr ax - - jmp kernel_main -``` - -在每次调用`restart`函数的时候,TSS中的`sp0`寄存器赋值为进程存储的寄存器表的顶部地址,这样保证进入内核态之后第一个压入的寄存器的值对应的是寄存器表中的`ss`. - -```nasm -restart: - mov esp, [p_proc_ready] - lea eax, [esp + P_STACKTOP] - mov dword [tss + TSS3_S_SP0], eax -restart_reenter: - ... -``` - -这样当再次陷入内核态时,首先将TSS中的`esp0`和`ss0`赋值到`esp`和`ss`寄存器,再之后`eip`\~`ss`这五个寄存器(其中`esp`和`ss`是用户态下的,虽然从逻辑上感觉不可思议,但是硬件总是可行的)会被压入栈中. - -##### 4. 时钟中断 - -时钟中断反正也挺简单的,Orange书上也有写,也就一个固定频率的晶振电路,触发指定次后向OS发送一个中断信号,这个时候执行流需要陷入内核然后处理时钟中断处理程序. - -##### 5. 键盘中断 - -这又是书上讲的很玄乎的一部分,但是实际上没那么玄乎,实验用不到那么多,在接受到键盘中断后,我们实际上需要解决两个问题:如何获取键盘输入的扫描码?如何将扫描码解析成正常ASCII码字符?解析后的ASCII码字符怎么用? - -从键盘输入上获取扫描码这个问题比较简单书上也讲了,存储在标号为`0x60`的端口里,可以通过inb(in_byte)函数将端口的值读出来.如果不及时读出来,你再怎么摁键盘也不会触发键盘中断,可以理解为第一次输入的扫描码直接把端口霸占住了,不让其他扫描码进来. - -但是实际上我们读入的是扫描码,是键盘上的一种编码,而我们需要将这种编码进行一步映射将扫描码映射成我们熟悉的ASCII字符,我们从端口里读入的是一个字节的数据,能表达0\~255之间的数,而ASCII字符仅能表达0\~127之间的数.最高位没有被用到,所以被硬件工程师重新利用,要知道我们摁下键盘实际上分摁下和弹起两个操作,对于同一个字符,它摁下与弹起的扫描码的区别在于摁下是最高位置0,而弹起是最高位置1,如果一直摁下会一直发送摁下的扫描码.对于`a`\~`z`这些字符,我们摁一次键会收到两个扫描码(摁下和弹起),但是在我们的日常理解里我们只关心摁下这个操作,所以在这次实验中,我们需要忽略掉弹起的扫描码,只关心摁下的扫描码,接下来需要解决的就是一个映射问题,`inc/keymap.h`里存放着一张扫描码到ASCII码的转换表,通过这张转换表就可以直接将扫描码转化为ASCII字符,这里并不需要考虑shift,ctrl这种特殊的控制字符,只需要实现实验要求的功能即可. - -在获取完ASCII码字符后,我们肯定不能把辛苦得来的字符丢掉,但是我们并不知道用户程序什么时候会来索取字符,所以需要一个缓冲区存储字符,在`kern/keyboard.c`放着一个简单的缓冲区: - -```c -#define KB_INBUF_SIZE 4 - -typedef struct kb_inbuf { - u8* p_head; - u8* p_tail; - int count; - u8 buf[KB_INBUF_SIZE]; -} KB_INPUT; - -static KB_INPUT kb_input = { - .p_head = kb_input.buf, - .p_tail = kb_input.buf, - .count = 0, -}; -``` - -这个数据结构本质上是一个队列,其中`p_head`指的是缓冲区的队首字符,`p_tail`指的是缓冲区的队尾字符,`count`是当前存储的字符数量,`buf`是缓冲区.需要注意的是让缓冲区满的时候所有添加的字符需要丢弃. - -当有这么一个缓冲区,触发键盘中断将解析来的字符存放到缓冲区中,当有用户程序需要索取时将缓冲区的队首字符弹出交给用户程序,就完成了整个交互. \ No newline at end of file