diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..a1307b3 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,25 @@ +{ + "configurations": [ + { + "name": "Mac", + "includePath": ["${workspaceFolder}/**"], + "defines": [], + "compilerPath": "/opt/homebrew/bin/i686-elf-gcc", + "compilerArgs": [ + "-ffreestanding", + "-nostdlib", + "-nostartfiles", + "-nodefaultlibs", + "-Iinc" + ], + "cStandard": "gnu99", + "intelliSenseMode": "macos-gcc-x86", + "configurationProvider": "ms-vscode.makefile-tools", + "browse": { + "path": ["${workspaceFolder}"], + "limitSymbolsToIncludedHeaders": true + } + } + ], + "version": 4 +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} diff --git a/Makefile b/Makefile index 75b2fbb..7244a04 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -FILES = ./build/kernel.asm.o ./build/kernel.o +FILES = ./build/kernel.asm.o ./build/kernel.o ./build/idt/idt.asm.o ./build/idt/idt.o ./build/memory/memory.o INCLUDES = -I./src FLAGS = -g -ffreestanding -falign-jumps -falign-functions -falign-labels -falign-loops -fstrength-reduce -fomit-frame-pointer -finline-functions -Wno-unused-function -fno-builtin -Werror -Wno-unused-label -Wno-cpp -Wno-unused-parammeter -nostdlib -nostartfiles -nodefaultlibs -Wall -O0 -Iinc @@ -21,6 +21,15 @@ all: ./bin/boot.bin ./bin/kernel.bin ./build/kernel.o: ./src/kernel.c i686-elf-gcc $(INCLUDES) $(FLAGS) -std=gnu99 -c ./src/kernel.c -o ./build/kernel.o +./build/idt/idt.asm.o: ./src/idt/idt.asm + nasm -f elf -g ./src/idt/idt.asm -o ./build/idt/idt.asm.o + +./build/idt/idt.o: ./src/idt/idt.c + i686-elf-gcc $(INCLUDES) -I./src/idt $(FLAGS) -std=gnu99 -c ./src/idt/idt.c -o ./build/idt/idt.o + +./build/memory/memory.o: ./src/memory/memory.c + i686-elf-gcc $(INCLUDES) -I./src/memory $(FLAGS) -std=gnu99 -c ./src/memory/memory.c -o ./build/memory/memory.o + run: qemu-system-x86_64 -hda ./bin/os.bin diff --git a/README.md b/README.md index 1d1ba5d..33490f5 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Developing a multi-threaded kernel from scratch Documents in this repo assumes macOS on Apple M1 chip. -``` +```shell > brew install nasm > brew install qemu ``` diff --git a/doc/protected_mode_development.md b/doc/protected_mode_development.md index 43919b1..51f96d5 100644 --- a/doc/protected_mode_development.md +++ b/doc/protected_mode_development.md @@ -30,7 +30,7 @@ We create entries for Global Descriptor Table (GDT) and load its address into GD This repo assumes you are using a Mac on M1 Chip. For that, we need to use LLDB instead of GDB. Here's how: -``` +```shell # Launch QEMU with gdb server enabled # '-s' shorthand for -gdb tcp::1234 # '-S' freeze CPU at startup (use 'c' to start execution) diff --git a/doc/protected_mode_development_2.md b/doc/protected_mode_development_2.md index 9bd2456..8749293 100644 --- a/doc/protected_mode_development_2.md +++ b/doc/protected_mode_development_2.md @@ -4,7 +4,7 @@ Installing the i686-elf toolchain on M1 Mac is very easy. -``` +```shell > brew install i686-elf-binutils i686-elf-gcc i386-elf-gdb ``` @@ -49,7 +49,7 @@ Now, we can use `i386-elf-gdb` instead of `lldb`. Load the symbol file `kernelfu Now you can debug the kernel just like you would do with any executables! -``` +```shell > make clean > make ❯ qemu-system-x86_64 -hda ./bin/os.bin -s -S & diff --git a/doc/protected_mode_development_3.md b/doc/protected_mode_development_3.md index 7271645..daaeed5 100644 --- a/doc/protected_mode_development_3.md +++ b/doc/protected_mode_development_3.md @@ -28,7 +28,7 @@ Each character will take 2 bytes. Example: Display "AB" in white -``` +```x86asm 0xB8000 = 'A' 0xB8001 = 0x0F 0xB8002 = 'B' @@ -37,7 +37,7 @@ Example: Display "AB" in white ### Writing to video memory in C -``` +```c char *video_mem = (char *)0xB8000; video_mem[0] = 'A'; video_mem[1] = '2'; // Green @@ -45,7 +45,7 @@ video_mem[1] = '2'; // Green but, we can optimize this by using `uint16_t` -``` +```c #include ... @@ -57,3 +57,64 @@ video_mem[0] = 0x0241; // 'A' (65 decimal or 0x41 hex) + Green (0x02) in the lit and with bit more helper functions, we can call `print` and write "Hello, World!". [commit](https://github.com/taikiy/kernel/commit/fa0fbabaf9c9cd93bcaff966fb54164fa3da3df6) + +### Interrupt Descriptor Table + +IDT describes how interrupts are invoked in Protected Mode, and can be mapped anywhere in memory (as opposed to IVT must be loaded at 0x00). This is equivalent of IVT (Interrupt Vector Table) in Real Mode, but a bit harder to setup. ([osdev wiki](https://wiki.osdev.org/Interrupt_descriptor_table)) + +We'll use the C structure to represent IDT entries. + +```c +struct idt_desc +{ + uint16_t offset_1; // offset bits 0..15 + uint16_t selector; // a code segment selector in GDT or LDT + uint8_t zero; // unused bits, set to 0 + uint8_t type_attr; // type and attributes + uint16_t offset_2; // offset bits 16..31 +} __attribute__((packed)); // ensures there's no unexpected padding +``` + +We can create an array to store interrupt descriptors. Index 0 defines interrupt 0 `int 0`, index 1 defines `int 1`, and so on. + +```c +struct idt_desc idt_descriptors[MAX_INTERRUPTS]; +``` + +The location of the IDT is kept in the IDTR (IDT Register). This is loaded using the `lidt` assembly instruction, whose argument is a pointer to an IDTR. + +```c +struct idtr_desc +{ + uint16_t limit; // The length of the Interrupt Descriptor Table minus one + uint32_t base; // The address of the Interrupt Descriptor Table +} __attribute__((packed)); +``` + +![IDT](https://pdos.csail.mit.edu/6.828/2008/readings/i386/fig9-1.gif) + +### Gate Types + +- 32-bit Task Gate (0x05 / 0b0101) +- 16-bit Interrupt Gate (0x06 / 0b0110) +- 16-bit Trap Gate (0x07 / 0b0111) +- 32-bit Interrupt Gate (0x0E / 0b1110) +- 32-bit Trap Gate (0x0F / 0b1111) + +#### Task Gate + +Task gates reference TSS descriptors and can assist in multi-tasking when exceptions occur. + +#### Interrupt Gate + +Interrupt gates are to be used for interrupts that we want to invoke ourselves in our code. + +#### Trap Gate + +Trap gates are like interrupt gates, but used for exceptions. They disable interrupts on entry and re-enable on an `iret` instruction. + +### Loading IDT + +Use `lidt` instruction. + +([commit]()) diff --git a/doc/real_mode_development.md b/doc/real_mode_development.md index 17c33a2..dfe02eb 100644 --- a/doc/real_mode_development.md +++ b/doc/real_mode_development.md @@ -20,11 +20,11 @@ For this section, we'll print "Hello, World!" on the screen, and for that we'll --- -1. Edit [`boot.asm`](../boot.asm) ([git](https://github.com/taikiy/kernel/commit/fa5ced2e4e5b3dab0105ed001ef021cc7759e329#diff-ef96aa02ede6928fc12bc906ab8b222af1250dde26bb066466d339e48ab4e658)) +1. Edit [`boot.asm`](../boot.asm) ([commit](https://github.com/taikiy/kernel/commit/fa5ced2e4e5b3dab0105ed001ef021cc7759e329#diff-ef96aa02ede6928fc12bc906ab8b222af1250dde26bb066466d339e48ab4e658)) 2. Assemble -``` +```shell > nasm -f bin ./boot.asm -o ./boot.bin ``` @@ -32,7 +32,7 @@ For this section, we'll print "Hello, World!" on the screen, and for that we'll Output has no header information. Just raw code output. -``` +```shell ❯ ll total 32 -rw-r--r-- 1 taiki staff 501B Jan 29 20:38 boot.asm @@ -41,7 +41,7 @@ total 32 You can disassemble the bin file to see the contents. -``` +```shell > ndisasm ./boot.bin` 00000000 B40E mov ah,0xe 00000002 B041 mov al,0x41 @@ -56,7 +56,7 @@ You can disassemble the bin file to see the contents. 4. Run -``` +```shell > qemu-system-x86_64 -hda ./boot.bin ``` @@ -66,11 +66,11 @@ You can disassemble the bin file to see the contents. In the previous section, `ORG 0x7c00` worked fine. This is because QEMU BIOS starts with DS set to 0. On other BIOS, however, this might not work if it initializes the data segment to, for example, `0x7c0`. In that case, our DS will be `0x7c00 + 0x7c0 * 16` which does not point to `message`. -To prevent this, we set DS ourselves in the assembly. ([git](https://github.com/taikiy/kernel/commit/6b08bf6ba316d4bcc16c7f214151aca9cfdcfab7#diff-ef96aa02ede6928fc12bc906ab8b222af1250dde26bb066466d339e48ab4e658)) +To prevent this, we set DS ourselves in the assembly. ([commit](https://github.com/taikiy/kernel/commit/6b08bf6ba316d4bcc16c7f214151aca9cfdcfab7#diff-ef96aa02ede6928fc12bc906ab8b222af1250dde26bb066466d339e48ab4e658)) ## 3. BIOS Parameter Block (BPB) -Usually, the binary file so far will work fine on real machines, but some BIOS expect what's known as [BPB](https://wiki.osdev.org/FAT#BPB_.28BIOS_Parameter_Block.29). For a maximum compatibility, we should reserve the block if BIOS decides to write some data in this data block. ([git](https://github.com/taikiy/kernel/commit/ec33f9a20982be55a0caf5eb59890048b4cfd064#diff-ef96aa02ede6928fc12bc906ab8b222af1250dde26bb066466d339e48ab4e658)) +Usually, the binary file so far will work fine on real machines, but some BIOS expect what's known as [BPB](https://wiki.osdev.org/FAT#BPB_.28BIOS_Parameter_Block.29). For a maximum compatibility, we should reserve the block if BIOS decides to write some data in this data block. ([commit](https://github.com/taikiy/kernel/commit/ec33f9a20982be55a0caf5eb59890048b4cfd064#diff-ef96aa02ede6928fc12bc906ab8b222af1250dde26bb066466d339e48ab4e658)) ## 4. Writing the bootloader to a USB stick @@ -82,22 +82,22 @@ On Mac, just like Linux, you can use `dd` to copy our binary bootloader file to 2. Check the interface path -``` -diskutil list +```shell +> diskutil list ``` This is equivalent to `fdisk -l` on Linux. 3. Unmount the USB stick -``` -diskutil unmountDisk /dev/disk6 +```shell +> diskutil unmountDisk /dev/disk6 ``` 4. Create the disk image. ❗ _Backup the USB stick before executing this command_ ❗ -``` -sudo dd if=./boot.bin of=/dev/disk6 +```shell +> sudo dd if=./boot.bin of=/dev/disk6 ``` 5. Unplug the stick, plug it into a PC, boot! @@ -119,7 +119,7 @@ In Real Mode, Interrupt Vector Table (IVT) is loaded at 0x00. IVT is a table tha 4 2 0 ``` -To define a custom interrupt handler, we first define a routine with a label, and then write the segment/offset of the routine to IVT. ([git](https://github.com/taikiy/kernel/commit/8a5fb00bc8bdaf47af6cb9de8ac107e8a6655db6#diff-ef96aa02ede6928fc12bc906ab8b222af1250dde26bb066466d339e48ab4e658)) +To define a custom interrupt handler, we first define a routine with a label, and then write the segment/offset of the routine to IVT. ([commit](https://github.com/taikiy/kernel/commit/8a5fb00bc8bdaf47af6cb9de8ac107e8a6655db6#diff-ef96aa02ede6928fc12bc906ab8b222af1250dde26bb066466d339e48ab4e658)) ## 6. Reading from the disk diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..09119f3 --- /dev/null +++ b/src/config.h @@ -0,0 +1,9 @@ +#ifndef CONFIG_H +#define CONFIG_H + +#define KERNEL_CODE_SELECTOR 0x08 +#define KERNEL_DATA_SELECTOR 0x10 + +#define TOTAL_INTERRUPTS 512 + +#endif diff --git a/src/idt/idt.asm b/src/idt/idt.asm new file mode 100644 index 0000000..f46adbf --- /dev/null +++ b/src/idt/idt.asm @@ -0,0 +1,13 @@ +section .asm + +global idt_load + +idt_load: + push ebp + mov ebp, esp + + mov ebx, [ebp+8] ; first argument passed to this function + lidt [edx] + + pop ebp + ret \ No newline at end of file diff --git a/src/idt/idt.c b/src/idt/idt.c new file mode 100644 index 0000000..95532c6 --- /dev/null +++ b/src/idt/idt.c @@ -0,0 +1,41 @@ +#include "idt.h" +#include "config.h" +#include "kernel.h" +#include "memory/memory.h" + +struct idt_desc idt_descriptors[TOTAL_INTERRUPTS]; +struct idtr_desc idtr_descriptor; + +extern void idt_load(struct idtr_desc *ptr); + +void idt_zero() +{ + print("Divide by zero error\n"); +} + +void idt_set(int interrupt_number, void *address) +{ + struct idt_desc *desc = &idt_descriptors[interrupt_number]; + + desc->offset_1 = (uint32_t)address & 0xffff; + desc->selector = KERNEL_CODE_SELECTOR; + desc->zero = 0x00; + // Type: 0x0e/0b1110 = 32-bit interrupt gate + // Attribute: Storage Segment = 0 (interrupt) + // DPL (Descriptor Privilege Level) = 0b11 or 3 (Ring 3) + // Present = 1 (0 for unused interrupt) + desc->type_attr = 0x0e | 0xe0; + desc->offset_2 = (uint32_t)address >> 16; +} + +void idt_init() +{ + memset(idt_descriptors, 0, sizeof(idt_descriptors)); + idtr_descriptor.limit = sizeof(idt_descriptors) - 1; + idtr_descriptor.base = (uint32_t)idt_descriptors; + + idt_set(0, idt_zero); + + // Load the IDT + idt_load(&idtr_descriptor); +} diff --git a/src/idt/idt.h b/src/idt/idt.h new file mode 100644 index 0000000..3cfd823 --- /dev/null +++ b/src/idt/idt.h @@ -0,0 +1,23 @@ +#ifndef IDT_H +#define IDT_H + +#include + +struct idt_desc +{ + uint16_t offset_1; // offset bits 0..15 + uint16_t selector; // a code segment selector in GDT or LDT + uint8_t zero; // unused bits, set to 0 + uint8_t type_attr; // type and attributes + uint16_t offset_2; // offset bits 16..31 +} __attribute__((packed)); // ensures there's no unexpected padding + +struct idtr_desc +{ + uint16_t limit; // The length of the Interrupt Descriptor Table minus one + uint32_t base; // The address of the Interrupt Descriptor Table +} __attribute__((packed)); + +void idt_init(); + +#endif diff --git a/src/kernel.asm b/src/kernel.asm index 9000cdf..51e7668 100644 --- a/src/kernel.asm +++ b/src/kernel.asm @@ -1,6 +1,7 @@ [BITS 32] global _start +global divide_by_zero extern kernel_main @@ -28,6 +29,9 @@ _start: cli ; Disables interrupts hlt ; This hangs the computer +divide_by_zero: + int 0 + times 512 - ($ - $$) db 0 ; Pad the kernel code sector to 512 bytes ; This ensures that any object files written in C and linked with this assembly ; will be correctly aligned. diff --git a/src/kernel.c b/src/kernel.c index e72786b..43dce37 100644 --- a/src/kernel.c +++ b/src/kernel.c @@ -1,6 +1,7 @@ #include "kernel.h" #include #include +#include "idt/idt.h" uint16_t *video_mem = 0; uint16_t terminal_row = 0; @@ -68,8 +69,14 @@ void print(const char *str) } } +extern void divide_by_zero(); + void kernel_main() { terminal_initialize(); print("Hello, World!\nYou are in Protected Mode!"); + + // Initialize the Interrupt Descriptor Table + idt_init(); + divide_by_zero(); } diff --git a/src/kernel.h b/src/kernel.h index 9cff07c..07acc82 100644 --- a/src/kernel.h +++ b/src/kernel.h @@ -5,5 +5,6 @@ #define VGA_HEIGHT 20 void kernel_main(); +void print(const char *str); -#endif \ No newline at end of file +#endif diff --git a/src/memory/memory.c b/src/memory/memory.c new file mode 100644 index 0000000..b5c2b30 --- /dev/null +++ b/src/memory/memory.c @@ -0,0 +1,13 @@ +#include "memory.h" + +void *memset(void *ptr, int c, size_t size) +{ + char *c_ptr = (char *)ptr; + + for (int i = 0; i < size; i++) + { + c_ptr[i] = (char)c; + } + + return ptr; +} \ No newline at end of file diff --git a/src/memory/memory.h b/src/memory/memory.h new file mode 100644 index 0000000..488824c --- /dev/null +++ b/src/memory/memory.h @@ -0,0 +1,8 @@ +#ifndef MEMORY_H +#define MEMORY_H + +#include + +void *memset(void *ptr, int c, size_t size); + +#endif