├── .gitignore ├── Makefile ├── README.md ├── bin └── .gitkeep ├── config └── linker.ld ├── doc ├── CODE.md ├── CONFIG.md ├── DOCUMENTATION.md ├── code │ ├── Utils │ │ ├── UTILS.md │ │ ├── VGA.md │ │ └── string.md │ ├── boot │ │ ├── boot.md │ │ ├── boot_sector.md │ │ ├── disk.md │ │ ├── gdt.md │ │ ├── kernel_entry.md │ │ └── switch_pm.md │ ├── drivers │ │ ├── drivers.md │ │ └── keyboard.md │ ├── interrupts │ │ ├── idt.md │ │ ├── interrupts.md │ │ ├── interrupts_asm.md │ │ ├── isr.md │ │ ├── pic.md │ │ ├── port.md │ │ └── timer.md │ └── memory │ │ └── segmentation.md └── img │ └── hard_disk_structure.png ├── docker └── Dockerfile ├── iso └── .gitkeep └── src ├── boot_sector ├── boot_sector.asm ├── disk.asm ├── gdt.asm └── switch_pm.asm ├── entry ├── entry_point.asm └── kernel_entry.c ├── interrupts ├── pic.c ├── pic.h ├── port.c ├── port.h ├── timer.c └── timer.h └── utils ├── VGA.h ├── VGA ├── VGA.h ├── clear.c ├── clear.h ├── print.c └── print.h ├── string.h ├── string ├── itoa.c ├── itoa.h ├── revstr.c ├── revstr.h ├── strlen.c └── strlen.h └── types.h /.gitignore: -------------------------------------------------------------------------------- 1 | bin/* 2 | !bin/.gitkeep 3 | iso/* 4 | !iso/.gitkeep 5 | *.o 6 | *.bin 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SRC = ./src 2 | CONFIG = ./config 3 | BIN = ./bin 4 | ISO = ./iso 5 | 6 | # Main folders path 7 | ENTRY = $(SRC)/entry 8 | UTILS = $(SRC)/utils 9 | BOOT = $(SRC)/boot_sector 10 | INTERRUPTS = $(SRC)/interrupts 11 | 12 | # Kernel needed file(s) 13 | KERNEL_BIN = $(BIN)/kernel.bin 14 | KERNEL_BUILD = $(BIN)/kernelfull.o 15 | OS_BIN = $(ISO)/epi-os.iso 16 | 17 | # Compilation tools (compiler, linker, etc..) 18 | NASM = nasm 19 | CC = i686-elf-gcc 20 | LD = i686-elf-ld 21 | 22 | # Boot sector 23 | BOOT_SRC = $(BOOT)/boot_sector.asm 24 | BOOT_BIN = $(BIN)/boot.bin 25 | BOOT_FLAGS = -f bin 26 | 27 | # Includes 28 | INCLUDES = -I $(SRC) -I $(UTILS) 29 | 30 | # Flags 31 | ASM_FLAGS = -f elf 32 | CFLAGS = -g -ffreestanding $(INCLUDES) 33 | LDFLAGS = -Ttext 0x1000 --oformat binary 34 | 35 | # Sources 36 | ASM_SRC = $(ENTRY)/entry_point.asm 37 | C_SRC = $(ENTRY)/kernel_entry.c \ 38 | $(UTILS)/VGA/clear.c \ 39 | $(UTILS)/VGA/print.c \ 40 | $(UTILS)/string/revstr.c \ 41 | $(UTILS)/string/itoa.c \ 42 | $(UTILS)/string/strlen.c 43 | 44 | # Objects 45 | C_OBJ = $(C_SRC:.c=.o) 46 | ASM_OBJ = $(ASM_SRC:.asm=.o) 47 | KERNEL_OBJS = $(ASM_OBJ) $(C_OBJ) 48 | 49 | 50 | # Targets 51 | all: build 52 | 53 | build: boot_bin kernel_bin 54 | dd if=$(BOOT_BIN) >> $(OS_BIN) 55 | dd if=$(KERNEL_BIN) >> $(OS_BIN) 56 | dd if=/dev/zero bs=1048576 count=16 >> $(OS_BIN) 57 | 58 | # Compile and launch QEMU 59 | run: 60 | qemu-system-x86_64 -d int -no-reboot $(OS_BIN) 61 | 62 | build_and_run: build run 63 | 64 | boot_bin: 65 | $(NASM) $(BOOT_FLAGS) $(BOOT_SRC) -o $(BOOT_BIN) 66 | 67 | kernel_bin: $(KERNEL_OBJS) 68 | $(LD) $(LDFLAGS) $(KERNEL_OBJS) -o $(KERNEL_BIN) 69 | 70 | clean: 71 | $(RM) $(C_OBJ) 72 | $(RM) $(ASM_OBJ) 73 | $(RM) $(KERNEL_BIN) 74 | $(RM) $(BOOT_BIN) 75 | $(RM) $(KERNEL_BUILD) 76 | 77 | fclean: clean 78 | $(RM) $(OS_BIN) 79 | 80 | re: fclean all 81 | 82 | # Compilations rules 83 | %.o: %.c 84 | $(CC) $(CFLAGS) -c $< -o $@ 85 | 86 | %.o: %.asm 87 | $(NASM) $(ASM_FLAGS) $< -o $@ 88 | 89 | .PHONY: build run build_and_run boot_bin kernel_bin clean fclean re 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OS workshops 2 | 3 | ## Cross Compiler 4 | 5 | To compile the OS for the CPU, we need a cross compiler, a cross linker, etc.. It allows us to compile for another target than your current OS. 6 | 7 | We have made a Docker image, to build and run it, run the next instructions: 8 | 9 | #### Docker 10 | ```bash 11 | docker build -t osdev . 12 | ``` 13 | 14 | If you want to run qemu in graphical mode from the container (Forwarding X socket), 15 | 16 | ``` 17 | docker run -it -e DISPLAY=$DISPLAY \ 18 | -v /tmp/.X11-unix:/tmp/.X11-unix \ 19 | -v $PWD:/osdev osdev 20 | ``` 21 | 22 | if you just want to compile, 23 | 24 | ```bash 25 | docker run -it -v $PWD:/osdev osdev 26 | ``` 27 | 28 | #### From sources 29 | You can build it by hand from this source https://wiki.osdev.org/GCC_Cross-Compiler. 30 | 31 | ## Documentation 32 | 33 | To read the documentaion [**Doc**](doc/DOCUMENTATION.md). 34 | -------------------------------------------------------------------------------- /bin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epi-osdev/workshop-student/50ebc664c652d10992c75ef0d35312b767a8cce7/bin/.gitkeep -------------------------------------------------------------------------------- /config/linker.ld: -------------------------------------------------------------------------------- 1 | ENTRY(_start) 2 | OUTPUT_FORMAT(binary) 3 | SECTIONS 4 | { 5 | . = 1M; 6 | .text : ALIGN(4096) 7 | { 8 | *(.text) 9 | } 10 | 11 | .asm : ALIGN(4096) 12 | { 13 | *(.asm) 14 | } 15 | 16 | .rodata : ALIGN(4096) 17 | { 18 | *(.rodata) 19 | } 20 | 21 | .data : ALIGN(4096) 22 | { 23 | *(.data) 24 | } 25 | 26 | .bss : ALIGN(4096) 27 | { 28 | *(COMMON) 29 | *(.bss) 30 | } 31 | } -------------------------------------------------------------------------------- /doc/CODE.md: -------------------------------------------------------------------------------- 1 | # Code documentation 2 | 3 | ## Introduction 4 | 5 | this is the code documentation of the project. you can find all the explainations about the differents parts of the code, and the differents theorical parts of the project. all the documentation is store in the `doc` folder. and splitted in differents parts: 6 | 7 | - [Boot](code/boot/boot.md) \ 8 | this part explains all the differents parts of the boot process of the project (boot_sector, kernel entry, etc...). -------------------------------------------------------------------------------- /doc/CONFIG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epi-osdev/workshop-student/50ebc664c652d10992c75ef0d35312b767a8cce7/doc/CONFIG.md -------------------------------------------------------------------------------- /doc/DOCUMENTATION.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | Here you can find the project documentation. It explains the following differents parts: 4 | 5 | - [Config](CONFIG.md) 6 | 7 | - [Code](CODE.md) -------------------------------------------------------------------------------- /doc/code/Utils/UTILS.md: -------------------------------------------------------------------------------- 1 | # INTRODUCTION 2 | 3 | This is the introduction of all utils, it's all the useful functions that you can use to access some datas and some drivers. 4 | For now it's only including some folders with functions but in the future it will be more complex and more useful. So it will be a good idea to compile it with other makefiles. 5 | An important things about the utils is the fact that only one file is needed to be included in your other projects to use all the functions. So it's **FORBIDDEN** to include any other files. 6 | 7 | # TABLE OF CONTENTS 8 | 9 | - [VGA](#vga) 10 | - [string](#string) 11 | 12 | # VGA 13 | 14 | The vga is the first displaying mode of the PM, it's a `80` x `25` matrix starting from 0xB80000, each characters is 2 bytes long, the first one is the character (`ASCII`) and the second one is the `color` of the character. The color is a 4 bits value, the first 2 bits are the background color and the last 2 bits are the foreground color. The color is defined in the [`vga.h`](../../../src/utils/VGA/VGA.h) file. 15 | All the functions are described [HERE](VGA.md) 16 | 17 | # string 18 | 19 | The string utils is a string library, it contains all the functions to manipulate strings. All the functions are described [HERE](string.md) 20 | -------------------------------------------------------------------------------- /doc/code/Utils/VGA.md: -------------------------------------------------------------------------------- 1 | # INTRODUCTION 2 | 3 | Here is the VGA (Video Graphics Array) driver, it's a driver that allows you to display text on the screen. In the project, it's stored it the `src/utils/VGA` folder and can be used with the [`VGA.h`](../../../src/utils/VGA.h) include. 4 | 5 | # TABLE OF CONTENTS 6 | 7 | - [How it works ?](#how-it-works) 8 | - [How to use it ?](#how-to-use-it) 9 | - [Functions list](#functions-list) 10 | - [Want to contribute ?](#want-to-contribute) 11 | 12 | # How it works ? 13 | 14 | The VGA is provided by the BIOS, so we have to follow some rules, the first things to know is that the VGA is a buffer of `80 x 25` characters starting from the adress `0xB8000`, each characters is 2 bytes long, the first one is the character (`ASCII`) and the second one is the `color` of the character. A color is a 4 bits values (0-15) and the first 4 bits are the background color and the last 4 bits are the foreground color. The color is defined in the [`vga.h`](../../../src/utils/VGA/VGA.h) file. 15 | 16 | # How to use it ? 17 | 18 | The VGA folder provides an API that can be included to use all the functions. Only the `VGA.h` file is needed to use the VGA driver. It's made this way to have a cleaner code 19 | 20 | 21 | # Functions list 22 | 23 | - [vga_clear_screen](#func-vga-clear-screen) 24 | - [vga_putchar_at](#func-vga-putchar-at) 25 | - [vga_putstr_at](#func-vga-putstr-at) 26 | - [vga_print_int_at](#func-vga-print-int-at) 27 | - [vga_printf_at](#func-vga-printf-at) 28 | 29 | ## vga_clear_screen 30 | 31 | This function clear fill all the screen with the character ' ' and the color 0x00. 32 | Here is the prototype of the function: 33 | 34 | ```c 35 | void vga_clear_screen(); 36 | ``` 37 | 38 | ## vga_putchar_at 39 | 40 | This function display a character at the given position with the given color. 41 | It's the basic function to display a character on the screen. 42 | It will return 1 if the character is displayed and will return a negative value in case of error. 43 | Errors: 44 | - -1: The position is out of the screen (x > 80 or y > 25) 45 | Here is the prototype of the function: 46 | 47 | ```c 48 | int vga_putchar_at(char c, uint8_t color, uint8_t x, uint8_t y); 49 | ``` 50 | 51 | ## vga_putstr_at 52 | 53 | This function display a string at the given position with the given color. 54 | It's a basic function that display a string on the screen. 55 | It will return the string length if the string is displayed and will return a negative value in case of error. 56 | Errors: 57 | - -1: The position is out of the screen (x > 80 or y > 25) 58 | Here is the prototype of the function: 59 | 60 | ```c 61 | int vga_putstr_at(const char *str, uint8_t color, uint8_t x, uint8_t y); 62 | ``` 63 | 64 | ## vga_print_int_at 65 | 66 | This function display an integer at the given position with the given color. 67 | It's a basic function that display an integer on the screen. 68 | It will return the string length if the integer is displayed and will return a negative value in case of error. 69 | Errors: 70 | - -1: The position is out of the screen (x > 80 or y > 25) 71 | Here is the prototype of the function: 72 | 73 | ```c 74 | int vga_print_int_at(int num, uint8_t color, uint8_t x, uint8_t y); 75 | ``` 76 | 77 | ## vga_printf_at 78 | 79 | This function display a formatted string at the given position with the given color. 80 | It's a basic function that display a formatted string on the screen. 81 | It will return the string length if the string is displayed and will return a negative value in case of error. 82 | Errors: 83 | - -1: The position is out of the screen (x > 80 or y > 25) 84 | Here is the prototype of the function: 85 | 86 | ```c 87 | int vga_printf_at(const char *format, uint8_t color, uint8_t x, uint8_t y, ...); 88 | ``` 89 | 90 | # Want to contribute ? 91 | 92 | If you want to add a function in the VGA driver, you can do it by creating a new file in the `src/utils/VGA` folder and add the function in the [`VGA.h`](../../../src/utils/VGA.h) file. Pay attention to add the documentation of the function in this folder. And please make clean commented code with a proof of working code. 93 | 94 | -------------------------------------------------------------------------------- /doc/code/Utils/string.md: -------------------------------------------------------------------------------- 1 | # INTRODUCTION 2 | 3 | String utils is a library that contains all the functions to manipulate strings. It's just basic functions as you may know, but it's a good start to manipulate strings and it will be useful in the future. 4 | 5 | # TABLE OF CONTENTS 6 | 7 | - [How it works ?](#how-it-works) 8 | - [How to use it ?](#how-to-use-it) 9 | - [Functions list](#functions-list) 10 | - [Want to contribute ?](#want-to-contribute) 11 | 12 | # How it works ? 13 | 14 | This is containing all the functions to manipulate strings. It's just basic functions as you may know, but it's a good start to manipulate strings and it will be useful in the future. All the functions are stored in the `src/utils/string/` folder. 15 | 16 | # How to use it ? 17 | 18 | The string utils is a library that contains all the functions to manipulate strings. It's just basic functions as you may know, but it's a good start to manipulate strings and it will be useful in the future. It's providing an API named [string.h](../../../src/utils/string.h) that contains all the functions. Only this file must be included to use the string utils. 19 | 20 | # Functions list 21 | 22 | - [itoa](#func-itoa) 23 | - [revstr](#func-revstr) 24 | 25 | ## itoa 26 | 27 | This function convert an integer to a string into the given base. It will return nothing but the final string will be stored in the buffer given in parameter. The buffer must be big enough to store the string. 28 | 29 | Here is the prototype of the function: 30 | 31 | ```c 32 | void itoa(int num, char *str, uint8_t base); 33 | ``` 34 | 35 | ## revstr 36 | 37 | This function reverse a string. It will return nothing but the final string will be stored in the buffer given in parameter. 38 | 39 | Here is the prototype of the function: 40 | 41 | ```c 42 | void revstr(char *str); 43 | ``` 44 | 45 | # Want to contribute ? 46 | 47 | If you want to contribute to this utils, you can add your own function, please make sure that the code is clean and that it's working. If you want to add a function, please add it in the [string.h](../../../src/utils/string.h) file and add the documentation in the [string.md](string.md) file. 48 | -------------------------------------------------------------------------------- /doc/code/boot/boot.md: -------------------------------------------------------------------------------- 1 | # Boot documentation 2 | 3 | ## Introduction 4 | 5 | this is the documentation of all the booting things, it's splitted into differents parts that explain all the differents parts of the booting process. You have several parts: 6 | 7 | ## Table of content 8 | 9 | - [Boot sector](boot_sector.md) 10 | - [Kernel entry](kernel_entry.md) 11 | 12 | ## Boot sector 13 | 14 | The MD doc: 15 | - [Boot sector](boot_sector.md) 16 | 17 | The boot sector is the first part of the booting process. It's serve to load the kernel and give kernel precious informations about the hardware and how the processor should be configured. 18 | 19 | ## Kernel entry 20 | 21 | The MD doc: 22 | - [Kernel entry](kernel_entry.md) 23 | 24 | The kernel entry is the entry point of the kernel. It's the first function that is called when the kernel is loaded. It's serve to initialize the kernel and to call the kernel main function. It's the equivalent of the C main function. 25 | -------------------------------------------------------------------------------- /doc/code/boot/boot_sector.md: -------------------------------------------------------------------------------- 1 | # Boot sector documentation 2 | 3 | ## Introduction 4 | 5 | This document explains the boot sector stuff. It is splitted into different parts that explain the booting process. Every file that we will refers to are in `src/boot_sector/`. 6 | 7 | ## Table of content 8 | 9 | - [Processor modes](#processor-modes) 10 | - [Bios routines](#bios-routines) 11 | 12 | ## The existing modes 13 | 14 | This is the theorical documentation for the processor modes. In the Intel architecture, there are many different modes that the CPU can use. The CPU can boot in differents modes, **16 bits**, **32 bits** or **64 bits**, respectively named **Real mode**, **Protected mode** and **Long mode**. Every mode has differents features and limitations. More details below: 15 | 16 | - [Real mode](#real-mode) 17 | - [Protected mode](#protected-mode) 18 | - [Long mode](#long-mode) 19 | 20 | ### Real mode (16 bits) 21 | 22 | The real mode is the firt mode that we have access when booting the processor. It's a 16 bits mode that have a lot of limitations. The main limitation is that the processor can only access 64KB of memory (because the max amount of addresses is 65,536 bytes (216) that represents 64KB. But with some tricky things you can upgrade to less that 1MB of memory 23 | 24 | Here is a list of the quality and limitations of the real mode: 25 | 26 | - Cons: 27 | - Less than 1 MB of RAM is available for use. 28 | - There is no hardware-based memory protection (GDT), nor virtual memory. 29 | - There is no built in security mechanisms to protect against buggy or malicious applications. 30 | - The default CPU operand length is only 16 bits. 31 | - The memory addressing modes provided are more restrictive than other CPU modes. 32 | - Accessing more than 64k requires the use of segment register that are difficult to work with. 33 | - Pros 34 | - The BIOS installs device drivers to control devices and handle interrupt. 35 | - BIOS functions provide operating systems with a advanced collection of low level API functions. 36 | - Memory access is faster due to the lack of descriptor tables to check and smaller registers. 37 | 38 | ### Protected mode (32 bits) 39 | 40 | the protected mode is the second mode that we have access when booting the processor. It's a 32 bits mode that have a lot of limitations. The main limitation is that the processor can only access 4GB of memory (because the max amount of addresses is 4,294,967,296 bytes (232) that represents 4GB. But with some tricky things you can upgrade to less that 4GB of memory. \ 41 | It's a bit old now but it was for a long time the main mode of the processor. It's still used in some cases like windows 32 bits. The main advantage of the protected mode is that it's a lot more secure than the real mode. It's also a lot more powerful than the real mode. It's also a lot more easy to use than the real mode. But the main cons of the protected mode is that you cannot access to the BIOS interrupts and it's more complex to use but it's not a big deal. 42 | 43 | ### Long mode (64 bits) 44 | 45 | the long mode is the third mode that we have access when booting the processor. It's a 64 bits mode that have a lot of limitations. The main limitation is that the processor can only access 256TB of memory (because the max amount of addresses is 256, terabytes (248) that represents 256TB. But with some tricky things you can upgrade to less that 256TB of memory. \ 46 | Don't be affraid of this because you will not have to use this for now :). 47 | 48 | ## BIOS routine 49 | 50 | When you are starting your computer, the bios is doing some routine to check if in the current disk there is a bootable partition. To check this, it's checking if the address `0x7C00 + 510` has the two bytes `0x55` and `0xAA`. If it's the case, it's loading the first 512 bytes of the disk in the memory at the address `0x7C00` and running the code. 51 | 52 | ## [boot_sector.asm](../../../src/boot_sector/boot_sector.asm) explaination 53 | 54 | Explaination of all the code in the file `src/boot_sector/boot_sector.asm` 55 | 56 | ```nasm 57 | [org 0x7C00] 58 | [bits 16] 59 | ``` 60 | 61 | Here you have the two first lines that you must understand. The first line is telling the assembler that the code will be at the address `0x7C00`. The second line is telling the assembler that the code will be in 16 bits mode. If you read the [BIOS Routine](#bios-routine) section, you will understand that the bios is loading the first 512 bytes of the disk in the memory at the address `0x7C00`. So we have to put our code at this address. 62 |
63 | After this you have the first label 64 | 65 | ```nasm 66 | start: 67 | mov ax, 0x00 68 | mov ds, ax 69 | mov es, ax 70 | mov ss, ax 71 | mov bp, 0x7c00 72 | mov sp, bp 73 | ``` 74 | 75 | This label is initializing every segment register to 0 (if you don't know what a segment register is, please see the [**Doc**](../memory/segmentation.md)) \ 76 | The `mov ax, 0x00` is moving the value `0x00` in the register `ax`. It's useful for initializing the registers ds, es and ss to a default value 77 | 78 | `bp` and `sp` are the stack register, (sp = stack pointer, bp = base pointer). The stack is the local memory of your program, it's where all the local variables are stored. By definition the stack is growing downward (from high address to low address). The base pointer is the beginning of the stack and the stack pointer is the current position of the stack. We are setting both to `0x7c00` because we are beginning all the kernel code at this address so when the stack is growing it will not overlapping the kernel code. 79 | 80 | After the start label that will init all the useful register we need to load the disk and switch to the protected mode. To do this we have a label in 16bits mode that manage all of this. 81 | 82 | ```nasm 83 | [bits 16] 84 | run_16: 85 | mov [BOOT_DRIVE], dl 86 | call load_kernel 87 | call switch_to_pm 88 | ``` 89 | So as you can see it's a 16 bits label that load our kernel and switch to the protected mode. The hardest line in this label is the `mov [BOOT_DRIVE], dl`. When you are launching the kernel, BIOS store the id of the boot drive in the register `dl`. So we are moving the value of `dl` in the address `BOOT_DRIVE` to save the data. 90 | The two other lines are just calling the two functions that we will see later. 91 | 92 | The first function that will be called in our `run_16` label is the `load_kernel` function. This function is loading the entire kernel in the memory it's a 16 bits label that read the disk and store the content into the memory. Here is the actual code of the function: 93 | 94 | ```nasm 95 | [bits 16] 96 | load_kernel: 97 | mov bx, KERNEL_OFFSET 98 | mov dh, 31 99 | mov dl, [BOOT_DRIVE] 100 | call disk_load 101 | ret 102 | ``` 103 | If you want to see what's doing the `disk_load` function, you can see it in the [disk.md](disk.md) file. this function ask to set some registers before using it: 104 | - `bx`: it's the offset in the memory where the kernel will be loaded 105 | - `dh`: it's the number of sectors that will be loaded 106 | - `dl`: it's the id of the disk that will be loaded 107 | once the function is called, it will load `dh` sector from the disk `dl` and store the result into the memory at the offset `bx`. 108 | 109 | Once the kernel is loaded in memory, we have to switch to the protected mode. To do this we have to call the `switch_to_pm` function. This function is doing all the routine that we need to switch from RM to PM, if you want to see what is doing exactly this function you can see it in the [switch_pm.md](switch_pm.md) file. 110 | 111 | When all is done we are in protected mode (finally), so we want to jump to the C kernel code. to do this we have a label called `entry_point`. 112 | 113 | ```nasm 114 | entry_point: 115 | call KERNEL_OFFSET 116 | jmp $ 117 | ``` 118 | Previously, we have loaded the kernel in the memory at the offset `KERNEL_OFFSET`. So we are calling the function at this offset. The `jmp $` is just a infinite loop. 119 | 120 | The end of the file is for loading all the other files and writting the magic bytes at the end of the file. 121 | 122 | ```nasm 123 | %include "src/boot_sector/disk.asm" 124 | %include "src/boot_sector/gdt.asm" 125 | 126 | times 510-($ - $$) db 0 127 | dw 0xAA55 128 | ``` 129 | %include instruction is obviously for including the other files. 130 | - [gdt.asm](gdt.md) 131 | - [disk.asm](disk.md) 132 | - [switch_pm.asm](switch_to_pm.md) 133 | 134 | the `times 510-($ - $$) db 0` is writing 510 - (current address - start address) bytes of 0. It's just padding the file with 0 to have a size of 510 bytes. The last two bytes are the magic bytes `0xAA55` that are telling the bios that the file is bootable (necessary for the [BIOS routine](#bios-routine)). -------------------------------------------------------------------------------- /doc/code/boot/disk.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | This document explains all the code in the file `src/boot_sector/disk.asm`. 4 | This file manage all the hard disk related stuff. 5 | 6 | ## Table of content 7 | 8 | - [Introduction](#introduction) 9 | - [Table of content](#table-of-content) 10 | - [Disk structure](#disk-structure) 11 | - [Head](#head) 12 | - [Cylinder](#cylinder) 13 | - [Sector](#sector) 14 | - [Disk routines](#disk-routines) 15 | - [Code explaination](#code-explaination) 16 | 17 | ## Disk structure 18 | 19 | The structure of a hard disk is quite simple, it inherits the real structure of hard disks of that time. 20 | Here is a simple schema of the structure of a hard disk: 21 | Hard disk structure 22 | You can see that the hard disk is divided in 4 parts, the biggest part is the head, an head contains many cylinders and a cylinder contains many sectors. 23 | Here is a description of what is a head, a cylinder and a sector: 24 | 25 | #### Head 26 | 27 | The head is the needle that is used to read and write on a disk, so a head represents on which disk you want to write. In asm, the head is represented by the `DH` register. 28 | 29 | #### Cylinder 30 | 31 | The cylinder is a division of the disk in a vertical way. It's used to represent the position of the head on the disk. In asm, the cylinder is represented by the `CH` register. 32 | 33 | #### Sector 34 | 35 | The sector is a division of the disk in a horizontal way. It's used to represent the position of the head on the disk. In asm, the sector is represented by the `CL` register. 36 | 37 | ## Disk routines 38 | 39 | To access the hard disk, we need to use some routines, but first we need to know on which mode we want to access the hard disk. It's important because the hard disk can only be directly accessed by the CPU in RM and not in PM or above. In the file [boot_sector/disk.asm](../../../src/boot_sector/disk.asm) there is only the routine for RM, but if you want to access the hard disk in PM or above you will need to use the BIOS to communicate with the hard disk. 40 | 41 | To read the disk we need to call the interrupts 0x13 `ah` set to 0x02. All thoses register needs to be set before calling the interrupt: 42 | - `al`: number of sectors to read 43 | - `ch`: cylinder 44 | - `cl`: sector 45 | - `dh`: head 46 | - `dl`: drive 47 | 48 | Once all of this is done, the interrupts will read the sectors and put them in the memory at the address `es:bx`. 49 | 50 | ## Code explaination 51 | 52 | So, if you read all the previous part, you should know how to read the hard disk. In this file, we created a function named `disk_load` that will load the sectors from the hard disk to the memory. This function takes only two parameters: 53 | - `bx` for the address in memory where the sectors will be loaded 54 | - `dl` for the drive number 55 | - `dh` for the number of sectors to read (yes if you already read [Disk routines](#disk-routines) you must know that it's the `al` register but if we are using `dh` instead of `al` it's because we are using only one register (`dx`) so it's cleaner) 56 | 57 | Once all of this is set we can call this function: 58 | ```nasm 59 | disk_load: 60 | push dx 61 | mov ah, 0x02 62 | mov al, dh 63 | mov ch, 0x00 64 | mov dh, 0x00 65 | mov cl, 0x02 66 | int 0x13 67 | jc disk_error 68 | pop dx 69 | cmp dh, al 70 | jne disk_error 71 | ret 72 | ``` 73 | 74 | It's not a pretty hard function, it's just a wrapper around the interrupt 0x13. The only thing that is a bit tricky is the `jc disk_error`. 75 | The first line is `push dx`, it's storing the register dx to the stack so we can use it later. The register dx is containing both of the register dh and dl. dl is the low bytes of dx (the first byte) and dh is the high bytes of dx (the second byte). So we are storing the register dx to the stack so we can use it later. 76 | After we have a couple of line: 77 | ```nasm 78 | mov ah, 0x02 79 | mov al, dh 80 | mov ch, 0x00 81 | mov dh, 0x00 82 | mov cl, 0x02 83 | ``` 84 | Those line is just setting all the register needed to call the interrupts 0x13 and read the sectors. Remember that we have `dh` for the number of sectors to read and `dl` for the drive number (dl is already set). 85 | After that we just need to call the interrupt 0x13 with the command `int 0x13`. A particularity of the interrupts is that if it fails it will set the carry flag (CF) to 1. So we need to check if the carry flag is set to 1, to do this the keyword `jc` is used. If the carry flag is set to 1, it will jump to the label `disk_error` and if it's not set to 1 it will continue the execution of the function. 86 | Finally, we need to check if the number of sectors read is the same as the number of sectors we wanted to read. If it's not the same, it means that there was an error and we need to jump to the label `disk_error`. If it's the same, we just need to return from the function. 87 | ```nasm 88 | pop dx ; Restore DX from the stack 89 | cmp dh, al ; if AL ( sectors read ) != DH ( sectors expected ) 90 | jne disk_error ; display error message 91 | ret 92 | 93 | disk_error: 94 | jmp $ 95 | ``` 96 | Theses lines are just restoring the register `dx` from the stack (previously pushed) and checking if the number of sectors read is the same as the number of sectors we wanted to read. If it's not the same, it means that there was an error and we need to jump to the label `disk_error`. If it's the same, we just need to return from the function. -------------------------------------------------------------------------------- /doc/code/boot/gdt.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | This document explains all the code in the file `src/boot_sector/gdt.asm`. 4 | 5 | ## What does it mean? 6 | 7 | GDT stands for `Global Descriptor Table`, basically it is a binary structure. It contains entries telling the CPU about memory segments. A similar Interrupt Descriptor Table exists containing task and interrupt descriptors. 8 | 9 | 10 | ## What does it do ? 11 | 12 | As we remember, in real mode (16 bits), the segmentation is managed by the segment registers (`cs`, `ds`, `es`, `fs` and `gs`). Now it is managed by the segment stored in the GDT. 13 | 14 | The GDT should be seen as a large table with segment descriptors as entries. 15 | 16 | Each entry has a size of 8 bytes, remember that the first one is always filled with zeros, it looks like this 17 | 18 | ```nasm 19 | first_segment: 20 | dd 0x0 21 | dd 0x0 22 | ``` 23 | 24 | Every other entries should look like this: 25 | 26 | ```nasm 27 | entry_example: 28 | dw 0xffff ; Segment limit first 0-15 bits 29 | dw 0 ; Base first 0-15 bits 30 | db 0 ; Base 16-23 bits 31 | db 0x9a ; Access byte 32 | db 11001111b ; High 4 bit flags and the low 4 bit flags 33 | db 0 ; Base 24-31 bits 34 | ``` 35 | 36 | ## Loading the GDT 37 | 38 | To load the GDT, there is an instruction named `lgdt`, it means `Load Global Descriptor Table`. `lgdt` requires a 6 bytes structure, in this code we named it `gdt_descriptor`, as we can see below: 39 | ```nasm 40 | lgdt [gdt_descriptor] 41 | ``` 42 | 43 | ## Usage example 44 | 45 | As example you can see in `src/boot_sector/switch_pm.asm`: 46 | ```nasm 47 | jmp CODE_SEG:init_pm 48 | ``` 49 | 50 | Here, `CODE_SEG` is not a reference to the segment itself. Actually, it refers to the segment descriptor entry in the GDT. Moreover, in this part of code we were not in the segment `CODE`, it will make us using a different segment. It is called a `far jump`. -------------------------------------------------------------------------------- /doc/code/boot/kernel_entry.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epi-osdev/workshop-student/50ebc664c652d10992c75ef0d35312b767a8cce7/doc/code/boot/kernel_entry.md -------------------------------------------------------------------------------- /doc/code/boot/switch_pm.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | This document explains all the code in the file `src/boot_sector/switch_pm.md`. It's containing the code to switch from RM to PM. It's called in the [boot_sector.asm](../../../src/boot_sector/boot_sector.asm) file. It's important to switch to PM because it's allowing us to use all the 32 bits features of the CPU, like more 4 | memory, more registers, etc. To do the switch you must make a routine that will modify the behavior and switch the CPU into 32 bits mode. 5 | 6 | ## Table of content 7 | 8 | - [BIOS Routine](#bios-routine) 9 | - [Routine explaination](#routine-explaination) 10 | - [Code explaination](#code-explaination) 11 | 12 | ## BIOS Routine 13 | 14 | We need to do a routine to do the switch, this routine is provided by the Intel doc 15 | Here is the routine to switch from RM to PM: 16 | 17 | - Clear the interrupts 18 | - Load the GDT 19 | - Set the first bit of the CR0 register to 1 20 | - Make a far jump to the first 32 bits label in the code segment 21 | - Clear all the old segment registers 22 | - Update the stack pointer 23 | 24 | All theses instructions must be done to switch from RM to PM. If one is missing, the switch can fail. 25 | 26 | ## Routine explaination 27 | 28 | The first task of the routine is to clear the interrupts, you have to know that in PM it's not possible to use the interrupts because it's made radically different. So we need to disable all the interrupts to avoid conflicts with the new mode and the switching routine. 29 | 30 | Next, we need to load the GDT, the memory management is different between RM (basic flat memory) and PM (memory segmentation). The memory segmentation ask first to create a complex data structure that named [GDT](gdt.md) 31 | 32 | Next, we are setting the first bit of the CR0 register to 1, it's done by this code: 33 | ```nasm 34 | mov eax, cr0 35 | or eax, 1 36 | mov cr0, eax 37 | ``` 38 | No explication for this (for now), it's juste asked by the Intel doc. 39 | 40 | Next, we are making a far jump to the first 32 bits label in the code segment, it's done by this code: 41 | ```nasm 42 | jmp CODE_SEG:init_pm 43 | ``` 44 | You probably now how this type of jump works but there is an important feature to know. The far jump clear the CPU pipeline, the CPU pipeline in modern CPU is used to run tasks in a parallel way. It's very useful but for us when we are changing the mode some tasks can crash because of the differents mode (like 16 bits segmentation, interrupts, etc..). To avoid theses crashes we need to clear the CPU pipeline, it's done by the far jump. 45 | 46 | Now we are entering the first PM label. like said previously (#routine-explaination) we need to clear all the old segment because it's useless so we are setting all the segment registers (ds, ss, es, fs, gs) to the data segment. So when using theses, it will refer to the data segment. 47 | 48 | Finally, we are updating the stack pointer, before, the stack pointer was in 16 bits mode, now we are in 32 bits mode so we have more space, more memory adress, and new registers to the stack pointer. So we need to update the stack pointers to the new 32 bits mode. 49 | 50 | ## Code explaination 51 | 52 | we have two different labels in this file, one for RM and the other one for PM. Here is the first one 53 | 54 | ```nasm 55 | [bits 16] 56 | switch_to_pm: 57 | cli ; 1. disable interrupts 58 | lgdt [gdt_descriptor] ; 2. load the GDT descriptor 59 | mov eax, cr0 60 | or eax, 0x1 ; 3. set 32-bit mode bit in cr0 61 | mov cr0, eax 62 | jmp CODE_SEG:init_pm ; 4. far jump by using a different segment 63 | ``` 64 | this is our 16 bits mode, we are still in RM and as you can see, it's the first steps of the [bios routine](#bios-routine). Once we have done this, we are jumping to the PM label. 65 | 66 | ```nasm 67 | [bits 32] ; protected mode 68 | init_pm: ; we are now using 32-bit instructions 69 | mov ax, DATA_SEG ; 5. update the segment registers 70 | mov ds, ax 71 | mov ss, ax 72 | mov es, ax 73 | mov fs, ax 74 | mov gs, ax 75 | 76 | mov ebp, 0x90000 ; 6. update the stack right at the top of the free space 77 | mov esp, ebp 78 | 79 | call entry_point ; 7. Call a well-known label with useful code 80 | 81 | ``` 82 | No more to say here, it's just the second part of the [bios routine](#bios-routine). Once we have done this, we are jumping to the entry_point label. It's were we can finally start to use the 32 bits features of the CPU. 83 | -------------------------------------------------------------------------------- /doc/code/drivers/drivers.md: -------------------------------------------------------------------------------- 1 | # INTRODUCTION 2 | 3 | This document describes the drivers and how to use them. 4 | All the drivers provides an interface to the hardware, and are used by the kernel to access the hardware. 5 | 6 | # TABLE OF CONTENTS 7 | 8 | - [DRIVERS LIST](#drivers-list) 9 | 10 | # DRIVERS LIST 11 | 12 | - [keyboard](keyboard.md): keyboard management 13 | -------------------------------------------------------------------------------- /doc/code/drivers/keyboard.md: -------------------------------------------------------------------------------- 1 | # INTRODUCTION 2 | 3 | This file explains you the code in the [keyboard.c](../../../src/drivers/keyboard.c) file. 4 | All this code takes the data from the `PIC` and manages it to be able to use it later in the kernel. 5 | After initializing all the routines, `IDT` and `PIC`, we can now use the keyboard. We can get the key pressed with the `port_byte_in` function. This function is in the [port.c](../../../src/interrupts/port.c) file. See the [doc](port.md) for more details. When a key is pressed it will check the port `0x60` and return the value of the key pressed. We can then use this value to do what we want. If no key is pressed, it will return `0`. 6 | It will not return the ASCII value but it will return the `scancode` of the key pressed. The `scancode` is the value of the key pressed (similar to an index). To transform this code into an ASCII value we are using a layout it will make the transfert between the `scancode` and the ASCII value. You already hear about layouts: `AZERTY`, `QWERTY` is existing layouts. 7 | 8 | # TABLE OF CONTENTS 9 | 10 | - [INTRODUCTION](#introduction) 11 | - [CODE EXPLANATION](#code-explanation) 12 | 13 | # CODE EXPLANATION 14 | 15 | We can see in this file the global that represents the current layout: 16 | ```c 17 | static KEYBOARD_LAYOUT current_layout = FR; 18 | ``` 19 | It's a static variable to makes it private to this file. It's the current layout used by the keyboard. It's initialized to `FR` (French layout). the type `KEYBOARD_LAYOUT` is an enum that contains all the layouts available. It's defined in the [keyboard.h](../../../src/drivers/keyboard/keyboard.h) file. For now it only contains this: 20 | ```c 21 | typedef enum { 22 | FR, 23 | US 24 | } KEYBOARD_LAYOUT; 25 | ``` 26 | This enum is showing us all the existing layouts. But the definition of the layouts are in another global that looks like this: 27 | ```c 28 | static char layouts[LAYOUTS_NUM][KEYS_NUM] = { 29 | #include "layouts/fr.txt" 30 | #include "layouts/us.txt" 31 | }; 32 | ``` 33 | It's another static that contains an array of string (string is an array of char). To be more explicit, We are creating an array of `LAYOUTS_NUM` lines and each line contains `KEYS_NUM` char. `LAYOUTS_NUM` and `KEYS_NUM` are both macro defined in the [keyboard.h](../../../src/drivers/keyboard/keyboard.h). `LAYOUTS_NUM` is equal to `2` and `KEYS_NUM` is equal to `58`. They are arbitrary values. 34 | 35 | In this file we also have getter and setter to get and set the current layout 36 | ```c 37 | void set_layout(KEYBOARD_LAYOUT layout) 38 | { 39 | current_layout = layout; 40 | } 41 | 42 | KEYBOARD_LAYOUT get_layout() 43 | { 44 | return current_layout; 45 | } 46 | ``` 47 | Nothing special to say here. 48 | The interesting things are here: 49 | ```c 50 | static uint8_t resolve_scancode(uint8_t scancode) 51 | { 52 | if (scancode > KEYS_NUM) { 53 | return 0; 54 | } 55 | 56 | return layouts[current_layout][scancode]; 57 | } 58 | 59 | static void callback(registers_t *regs) 60 | { 61 | uint8_t scancode = port_byte_in(0x60); 62 | uint8_t pressed_char = resolve_scancode(scancode); 63 | 64 | // TODO 65 | vga_putchar_at(pressed_char, 0x0f, 20, 20); 66 | } 67 | void init_keyboard() 68 | { 69 | isr_register_handler(33, callback); 70 | } 71 | ``` 72 | The `init_keyboard` function is called once in our program, in the [kernel.c](../../../src/kernel.c) file. It registers the `callback` function to the `33` interrupt. The `33` interrupt is the interrupt that is called when a key is pressed. The `callback` function is called when a key is pressed. It will get the `scancode` of the key pressed (The scancode is provided in the `PIC` port `0x60`) and then it will resolve it to get the ASCII value of the key pressed. It will then print the ASCII value on the screen. It's a very simple example of what we can do with the keyboard. We can do a lot of things with it. We can create a shell, a text editor, a game, etc. It's up to you to do what you want with it. The function `resolve_scancode` is here to check if the scancode exists in our arrays 73 | -------------------------------------------------------------------------------- /doc/code/interrupts/idt.md: -------------------------------------------------------------------------------- 1 | # INTRODUCTION 2 | 3 | This file is explaining how is working the [idt.c](../../../src/interrupts/idt.c). This file is holding our `IDT` and it's providing functions to manipulate it. 4 | 5 | # TABLE OF CONTENTS 6 | 7 | - [INTRODUCTION](#introduction) 8 | - [HEADERS](#headers) 9 | - [DATA STRUCTURES](#data-structures) 10 | - [FUNCTIONS](#functions) 11 | - [CODE EXPLANATION](#code-explanation) 12 | - [idt_init](#idt_init) 13 | - [idt_set_gate](#idt_set_gate) 14 | - [idt_load](#idt_load) 15 | 16 | # HEADERS 17 | 18 | ## DATA STRUCTURES 19 | 20 | In the header we are defining some useful datas. The first one that we can see is the: 21 | ```c 22 | #define MAX_IDT_ENTRIES 256 23 | ``` 24 | it defines the max size of our `IDT`. The second interesting thing is the `idt_entry_t` structure. It's defining how an entry of our `IDT` will look like. Here is the structure: 25 | ```c 26 | typedef struct idt_entry_s { 27 | uint16_t base_low; 28 | uint16_t segment; 29 | uint8_t always_null; 30 | uint8_t flags; 31 | uint16_t base_high; 32 | } __attribute__((packed)) idt_entry_t; 33 | ``` 34 | It describes an entry of our `IDT`. All the explaination about the fields of this structure can be found in the [interrupts.md](../../../doc/code/interrupts/interrupts.md) file. 35 | The other interesting structure is the `idt_descriptor_t` structure. It's defining how our `IDT` descriptor will look like. Here is the structure: 36 | ```c 37 | typedef struct idt_descriptor_s { 38 | uint16_t size; 39 | idt_entry_t *idt_start; 40 | } __attribute__((packed)) idt_descriptor_t; 41 | ``` 42 | It's the `IDT` descriptor, you already know what a descriptor is with the [GDT](../boot/gdt.md) descriptor, it's the same things. The `size` field is the size of our `IDT` and the `idt_start` field is the address of the first entry of our `IDT`. 43 | Those two structures have already been explained in the [interrupts.md](interrupts.md) file. But it's a good thing to have a quick look at them. 44 | 45 | There is another data that useful. It's an enum named `IDT_FLAGS`. This enum is containing all the flags that we can use in our `IDT` entries. Here is the enum: 46 | ```c 47 | enum IDT_FLAGS { 48 | // Gate 49 | IDT_FLAG_GATE_TASK = 0X5, 50 | IDT_FLAG_GATE_16BIT_INT = 0x6, 51 | IDT_FLAG_GATE_16BIT_TRAP = 0x7, 52 | IDT_FLAG_GATE_32BIT_INT = 0xe, 53 | IDT_FLAG_GATE_32BIT_TRAP = 0xf, 54 | // Ring 55 | IDT_FLAG_RING0 = (0 << 5), 56 | IDT_FLAG_RING1 = (1 << 5), 57 | IDT_FLAG_RING2 = (2 << 5), 58 | IDT_FLAG_RING3 = (3 << 5), 59 | IDT_FLAG_PRESENT = 0x80 60 | }; 61 | ``` 62 | 63 | ## FUNCTIONS 64 | 65 | The rest of the header is just function prototypes. Here is the prototypes list: 66 | 67 | - [idt_init](#func-idt-init) 68 | - [set_idt_gate](#func-set-idt-gate) 69 | - [set_gate_flag](#func-set-gate-flag) 70 | - [idt_enable_gates](#func-idt-enable-gates) 71 | - [unset_gate_flag](#func-unset-gate-flag) 72 | - [lidt](#func-lidt) 73 | 74 | ### idt_init 75 | 76 | This function is initialzing our `IDT`, this function is loading all the `IDT` with the `lidt` instruction. Here is the function prototype: 77 | ```c 78 | void idt_init(); 79 | ``` 80 | 81 | ### set_idt_gate 82 | 83 | This function is setting an entry of our `IDT`. Here is the function prototype: 84 | It takes 4 parameters: 85 | - `uint8_t index`: the index of the entry that we want to set (0 to 255) 86 | - `void *base`: the address of the function that we want to call when the interrupt will be triggered 87 | - `uint16_t segment`: the segment selector of the code segment that we want to call (0x08 for kernel code segment by default) 88 | - `uint8_t flags`: the flags that we want to set for this entry (you can use the `IDT_FLAGS` enum to set the flags) 89 | Here is the function prototype: 90 | ```c 91 | void set_idt_gate(uint8_t index, void *base, uint16_t segment, uint8_t flags); 92 | ``` 93 | 94 | ### set_gate_flag 95 | 96 | The set_gate_flag function is setting a flag for an entry of our `IDT`. It's taking 2 parameters: 97 | - `uint8_t index`: the index of the entry that we want to set the flag (0 to 255) 98 | - `uint8_t flag`: the flag that we want to set (you can use the `IDT_FLAGS` enum to set the flags) 99 | Here is the function prototype: 100 | ```c 101 | void set_gate_flag(uint8_t index, uint8_t flag); 102 | ``` 103 | 104 | ### idt_enable_gates 105 | 106 | This function enables all the interrupts of our `IDT`. It's taking no parameters but it changes all the flags of our `IDT` entries. Here is the function prototype: 107 | ```c 108 | void idt_enable_gates(); 109 | ``` 110 | 111 | ### unset_gate_flag 112 | 113 | This function unsets a flag for an entry of our `IDT`. It's taking 2 parameters: 114 | - `uint8_t index`: the index of the entry that we want to unset the flag (0 to 255) 115 | - `uint8_t flag`: the flag that we want to unset (you can use the `IDT_FLAGS` enum to set the flags) 116 | Here is the function prototype: 117 | ```c 118 | void unset_gate_flag(uint8_t index, uint8_t flag); 119 | ``` 120 | 121 | ### lidt 122 | 123 | This function is a wrapper for the `lidt` instruction. The `lidt` instruction doesn't have an equivalent function in C. So we are using this function to load our `IDT` descriptor. 124 | Here is the function prototype: 125 | ```c 126 | void lidt(); 127 | ``` -------------------------------------------------------------------------------- /doc/code/interrupts/interrupts.md: -------------------------------------------------------------------------------- 1 | # INTRODUCTION 2 | 3 | The interrupts are the main parts of the communication between the CPU and the kernel. But what are they? How do they work? How can we use them? This document will try to answer these questions. 4 | 5 | What are interrupts ? 6 | When you are doing something wrong like dividing by zero, the CPU will send an interrupt to the kernel. The kernel will be processed in priority order. This is the main usage of the interrupts, but you can also program your own interrupts. `ISR` (Interrupts Sub Routine) is the interrupts used by the CPU. `IRQ` (Interrupt Request) is the interrupts used by the hardware (programmable interrupts). `ISR` and `IRQ` are managed by the `IDT` (Interrupt Descriptor Table). It's two names for the same thing. 7 | 8 | How do they work ? 9 | In PM there are `256` possibles interrupts. The first `32` are reserved for the CPU (to handle basic things as the division by zero). The other `224` are free to use. We cannot program the behaving of the interrupts but we can define `callbacks`, a callback is a function that will be called when the interrupt is triggered. With this, when the interrupt x is triggered, the callback x will be called. 10 | 11 | How can we use them ? 12 | To use the interrupts, you need to define a datastructure named `IDT`, it's an array of entries describing each 256 possible entries. Each entry is composed of a pointer to the callback and some flags. When your `IDT` is ready, you need to load it with the `lidt` instruction. After that, you can enable the interrupts with the `sti` instruction. Now, when an interrupt is triggered, the callback will be called. 13 | 14 | # TABLE OF CONTENTS 15 | 16 | - [INTRODUCTION](#introduction) 17 | - [IMPLEMENTATION](#implementation) 18 | - [FILES](#files) 19 | 20 | # IMPLEMENTATION 21 | 22 | To be easier to understand we are implementing the `IDT` in C. Like I said previously, the IDT is just a table of entries. An entry in the `IDT` is shaped like this: 23 | - `base_low`: The lower 16 bits of the address to jump to when this interrupt fires. (2 bytes) 24 | - `selector`: Kernel segment selector. (2 bytes) 25 | - `zero`: This must always be zero. (1 byte) 26 | - `flags`: More information about the entry. (1 byte) 27 | - `base_high`: The upper 16 bits of the address to jump to. (2 bytes) 28 | 29 | In total an entry is 8 bytes. The C implementation can be this (Actually defined [here](../../../src/interrupts/idt.h)): 30 | ```c 31 | typedef struct idt_entry_s { 32 | uint16_t base_low; 33 | uint16_t selector; 34 | uint8_t zero; 35 | uint8_t flags; 36 | uint16_t base_high; 37 | } __attribute__((packed)) idt_entry_t; 38 | ``` 39 | If you don't know what `__attribute__((packed))` does, it's just to tell the compiler to not add padding between the fields of the structure. It's really useful here because we want to have a structure of 8 bytes and not more. More information about this can be found [here](https://gcc.gnu.org/onlinedocs/gcc-4.8.1/gcc/Type-Attributes.html). 40 | 41 | the `idt_entry_t` is just a structure of one entry, but we need a table of entries. It also must be used by many functions (for example to set an entry, a flag, remove an entry, etc...). So we need to make it global. In the file [idt.c](../../../src/interrupts/idt.c) we can see this: 42 | ```c 43 | static idt_entry_t idt_entries[MAX_IDT_ENTRIES]; 44 | ``` 45 | We are creating a global named `idt_entries` that is an array of `idt_entry_t` of size `MAX_IDT_ENTRIES`. `MAX_IDT_ENTRIES` is defined in the file [idt.h](../../../src/interrupts/idt.h) and is equal to `256`. 46 | ```c 47 | #define MAX_IDT_ENTRIES 256 48 | ``` 49 | We are using the `static` keyword to make it private to the file. It's not really necessary but it's a good practice to do it. 50 | 51 | Also like the `GDT`, we need a structure to describe the `IDT`, it must contains: 52 | - `limit`: The size of the `IDT` minus one. (2 bytes) 53 | - `base`: The address of the first element of the `IDT`. (4 bytes) 54 | 55 | This descriptor must be passed to the `lidt` instruction. In C, the implementation can be this: 56 | ```c 57 | typedef struct idt_descriptor_s { 58 | uint16_t limit; 59 | idt_entry_t *idt_start; 60 | } __attribute__((packed)) idt_descriptor_t; 61 | ``` 62 | The `idt_entry_t *idt_start` is a pointer to the `IDT`, a pointer is an address of 4 bytes. We can change the type of the pointer to `uint32_t` if we want to be more precise, but it's not really necessary and it's more understandable like this. 63 | 64 | Once we have our `IDT` we can create functions to set an entry, remove an entry, set a flag, etc... In the file [idt.c](../../../src/interrupts/idt.c). All the documentation can be found in the file [idt.md](idt.md) 65 | 66 | Once we had set the shape of our `IDT`, we can fill it with our callbacks, the definitions of the callbacks can be found in the file [interrupts.asm](../../../src/interrupts/interrupts.asm). The documentation can be found in the file [interrupts.md](interrupts.md). Those callbacks are given to the `IDT` in the file [isr.c](../../../src/interrupts/isr.c). The documentation can be found in the file [isr.md](isr.md). 67 | 68 | # FILES 69 | 70 | - [idt.c](../../../src/interrupts/idt.c): It's describing the `IDT` and it's providing functions to manipulate it. 71 | - [isr.c](../../../src/interrupts/isr.c): This is the file that contains all the initialization of our `IDT`. So it will call the functions to set the entries, set the flags, etc... (It's called `ISR` but in fact it contains all the interrupts, not only the `ISR`). 72 | - [interrupts.asm](../../../src/interrupts/interrupts.asm): This file contains all the callbacks that will be called when an interrupt is triggered. It must be in assembly because we have data that are not accessible directly in C, like the code errors or the interrupt number. 73 | -------------------------------------------------------------------------------- /doc/code/interrupts/interrupts_asm.md: -------------------------------------------------------------------------------- 1 | # INTRODUCTION 2 | 3 | This document describes all the theory and code in the [interrupts.asm](../../../src/interrupts/interrupts.asm) file. 4 | 5 | This file is handling all `ISR` callback declarations. Like explained [here](interrupts.md) each `ISR` needs a callback. When the CPU is triggering an interrupt, it will call push information in some asm register and then call a callback. To make things easier, we are first declaring the callback in assembler to make sure we have all the data provided by the CPU. Then we are pushing the data in the stack before calling C functions. 6 | 7 | To declare all the callbacks we are using a label and we are using the keyword `global` to make sure the C code can access it. (In the C code, it can be accessed with the `extern` keyword). 8 | We must define 256 callbacks, and many of them are identical. So to make things easier, we are using a macro to define all the callbacks. 9 | 10 | Each callback is basically doing the same things: 11 | - clearing the interrupts (with the `cli` instruction) 12 | - pushing the data in the stack 13 | - calling the label `isr_common_stub` (for `ISR`) or `irq_common_stub` (for `IRQ`) 14 | 15 | The `isr_common_stub` and `irq_common_stub` are defined in the [interrupts.asm](../../../src/interrupts/interrupts.asm) file. These functions are called by all the callbacks that we have defined. They are doing they are doing the transition between the assembler code and the C code. It's pushing the datas in the stack and then calling the C function. 16 | 17 | # TABLE OF CONTENTS 18 | 19 | - [INTRODUCTION](#introduction) 20 | - [CODE EXPLAINATION](#code-explaination) 21 | - [ISR CALLBACKS](#isr-callbacks) 22 | - [IRQ CALLBACKS](#irq-callbacks) 23 | - [COMMON STUBS](#common-stubs) 24 | 25 | 26 | # CODE EXPLAINATION 27 | 28 | We need to set 256 `ISR` and 16 `IRQ` callbacks. Barely all of them are identical, the only difference is the number of the interrupt. So we are using a macro to define all the callbacks. 29 | 30 | ## ISR CALLBACKS 31 | 32 | Here is the macro that we are using to define all the `ISR` callbacks: 33 | ```nasm 34 | %macro ISR 1 35 | global isr%1 36 | 37 | isr%1: 38 | cli 39 | push 0 40 | push %1 41 | jmp isr_common_stub 42 | %endmacro 43 | ``` 44 | with the keyword `%macro` we are defining a macro, and with the keyword `%endmacro` we are closing the macro. This macro is named `ISR` and takes `1` parameter. This parameter is the number of the interrupt. We can use this parameter with the keyword `%1`. 45 | So, we are declaring a global label named `isr%1` (with the `%1` we are using the parameter of the macro). Then we are clearing the interrupts, pushing `0` and the number of the interrupt in the stack. Finally we are calling the `isr_common_stub` function. It's pushing 0 because the CPU push the error code in the stack only for some interrupts. 46 | 47 | Also, specific `ISR` push some data in the stack (error code). So we are using a specific macro to define these callbacks: 48 | ```nasm 49 | %macro ISR_ERROR 1 50 | global isr%1 51 | 52 | isr%1: 53 | cli 54 | push %1 55 | jmp isr_common_stub 56 | %endmacro 57 | ``` 58 | Globally, it's the same as the previous macro, but we are not pushing `0` in the stack. Because the CPU is pushing the error code in the stack. 59 | 60 | ## IRQ CALLBACKS 61 | 62 | The difference between the `ISR` and the `IRQ` is that the `IRQ` are not triggered by the CPU. They are triggered by the PIC. So we are using a macro to define all the `IRQ` callbacks: 63 | ```nasm 64 | %macro IRQ 2 65 | global irq%1 66 | 67 | irq%1: 68 | cli 69 | push %1; dummy error 70 | push %2 ; interrupt number 71 | jmp irq_common_stub 72 | %endmacro 73 | ``` 74 | It's a macro taking 2 parameters. The first one is the number of the interrupt, and the second one is the number of the IRQ. We are using the second one to know which PIC is triggering the interrupt. We are using this information to send an `EOI` to the PIC (End Of Interrupt). 75 | 76 | ## COMMON STUBS 77 | 78 | A common stub is a function that is called by all the callbacks. It's doing the transition between the assembler code and the C code. It's pushing the data in the stack and then calling the C function. 79 | 80 | The `isr_common_stub` and the `irq_common_stub` are the same function. The only difference is the name of the function that is called. We are not using macro for these functions because it's easier when you want to add some code in the function. 81 | 82 | ```nasm 83 | isr_common_stub: ; or irq_common_stub 84 | pusha 85 | 86 | xor eax, eax 87 | mov ax, ds 88 | push eax 89 | 90 | mov ax, 0x10 91 | mov ds, ax 92 | mov es, ax 93 | mov fs, ax 94 | mov gs, ax 95 | 96 | push esp 97 | call isr_handler ; or irq_handler 98 | add esp, 4 99 | 100 | pop eax 101 | mov ds, ax 102 | mov es, ax 103 | mov fs, ax 104 | mov gs, ax 105 | 106 | popa 107 | add esp, 8 108 | sti 109 | iret 110 | ``` 111 | Here is all the things done by the common stub: 112 | - push all the registers in the stack (with the `pusha` instruction). Pushing into the stack permits to save the registers values and to use them in the C code. 113 | - saving the data segment register (`ds`) in the stack. We will pop it later, it's avoiding C code to modify our data segment register. 114 | - Initializing all the data segment registers (`ds`, `es`, `fs`, `gs`) with the data segment register in our `GDT` (0x10). This is to make sure that the C code can access the data segment register. 115 | - push the stack pointer in the stack, it's very useful because it's a pointer to all the registers that we pushed before. 116 | - call the C function that will manage all the interrupts (`ISR` or `IRQ` are handle in different functions). 117 | - add 4 to the stack pointer, it's because we pushed the stack pointer in the stack before and we want to remove it. 118 | - pop the data segment register that we saved before. 119 | - reset all the data segment registers (`ds`, `es`, `fs`, `gs`) with the data segment register that we saved before. 120 | - pop all the registers in the stack (with the `popa` instruction). Popping from the stack permits to restore the registers values. 121 | - add 8 to the stack pointer, it's because we pushed 2 values in the stack before (the dummy error and the interrupt number). 122 | - enable the interrupts (with the `sti` instruction). 123 | - return from the interrupt (with the `iret` instruction). -------------------------------------------------------------------------------- /doc/code/interrupts/isr.md: -------------------------------------------------------------------------------- 1 | # INTRODUCTION 2 | 3 | This document explains the [isr.c](../../../src/interrupts/isr.c) file. 4 | This file is currently initializing and filling the `IDT` with the callbacks defined in the [interrupts.asm](../../../src/interrupts/interrupts.asm) file (doc [here](interrupts_asm.md)). The meaning of this file is to setup all the information and routines to handle interrupts and keyboard inputs. 5 | 6 | The code in this file is handling a function `isr_init` that do the following things: 7 | - fill the `IDT` with the callbacks defined in the [interrupts.asm](../../../src/interrupts/interrupts.asm) file 8 | - remap the PIC (Programmable Interrupt Controller), see [pic.md](pic.md) for more information 9 | - enable all the interrupts in the `IDT` 10 | - unset the syscall interrupt 11 | 12 | There is also an array of function pointers named `isr_handler` that will contains all the functions that we want to call when an interrupt is triggered. For example, if we want to call a function when the keyboard is pressed, we will set the function pointer at the index `33` (the index of the keyboard interrupt). 13 | 14 | # TABLE OF CONTENTS 15 | 16 | - [INTRODUCTION](#introduction) 17 | - [HEADER FILES](#header-files) 18 | - [CODE EXPLAINATION](#code-explaination) 19 | 20 | # HEADER FILES 21 | 22 | The header file [isr.h](../../../src/interrupts/isr.h) contains interesting datas. 23 | The first thing that you can see is the macro 24 | ```c 25 | #define KERNEL_CODE_SEG 0x08 26 | ``` 27 | This macro defines the code segment offset of the `GDT`. (see [gdt.md](../boot/gdt.md) for more information). 28 | 29 | There is many cursed lines in the header file, they are all shaped like this: 30 | ```c 31 | extern void isr0(); 32 | extern void isr1(); 33 | // ... 34 | extern void isr255(); 35 | extern void irq0(); 36 | extern void irq1(); 37 | // ... 38 | extern void irq15(); 39 | ``` 40 | all those lines are basic function prototypes, but they have a special keyword `extern`. This keyword is used to tell the compiler that the function is defined somewhere else. In this case, the functions are defined in the [interrupts.asm](../../../src/interrupts/interrupts.asm) file (doc [here](interrupts_asm.md)). 41 | All the functions will fill the `IDT` 42 | 43 | After all the function prototypes, there is a structure named registers_t, they will contains all the registers of the CPU that we pushed in the file [interrupts.asm](../../../src/interrupts/interrupts.asm) with the instruction `lidt` (doc [here](interrupts_asm.md)). 44 | The structure that will handle all the registers is the following: 45 | ```c 46 | typedef struct registers_s { 47 | uint32_t ds; 48 | uint32_t edi, esi, ebp, useless, ebx, edx, ecx, eax; 49 | uint32_t interrupt, error; 50 | uint32_t eip, cs, eflags, esp, ss; 51 | } __attribute__((packed)) registers_t; 52 | ``` 53 | 54 | The last thing that we can see in the header file is the following typedef: 55 | ```c 56 | typedef void (*isr_callback)(registers_t *regs); 57 | ``` 58 | It's creating a type named isr_callback that is a pointer to a function that takes a pointer to a registers_t structure as parameter and returns nothing. 59 | 60 | # CODE EXPLAINATION 61 | 62 | We saw the header file with all the declarations, now we will see the code of the file [isr.c](../../../src/interrupts/isr.c). 63 | 64 | The first line that we can see is the following: 65 | ```c 66 | static isr_callback isr_handlers[MAX_IDT_ENTRIES]; 67 | ``` 68 | This is a global variable that has the keyword `static` (see [here](https://www.geeksforgeeks.org/static-keyword-c/)). 69 | This variable is handling all the functions that we want to call when an interrupt is triggered. When an interrupts is called, all the functions named `isr` or `irq` (defined in [interrupts.asm](../../../src/interrupts/interrupts.asm) (doc [here](interrupts_asm.md))) will call the function `isr_handler` (or `irq_handler`) with the index of the interrupt as parameter. And the handler will call the function at the index of the interrupt in the `isr_handlers` array. 70 | The `MAX_IDT_ENTRIES` is defined in the header file [isr.h](../../../src/interrupts/isr.h) and is equal to `256` that is the maximum number of interrupts that we can handle. 71 | 72 | The first function that we see is: 73 | ```c 74 | static void isr_init_idt_gates(); 75 | ``` 76 | This function is filling all the 256 entries of the `IDT` with the callbacks defined in the [interrupts.asm](../../../src/interrupts/interrupts.asm) file that we externalized in the [isr.h](../../../src/interrupts/isr.h) (doc [here](interrupts_asm.md)). 77 | This is typically what we can see in the function: 78 | ```c 79 | // ... 80 | set_idt_gate(34, irq2, KERNEL_CODE_SEG, IDT_FLAG_RING0 | IDT_FLAG_GATE_32BIT_INT); 81 | set_idt_gate(35, irq3, KERNEL_CODE_SEG, IDT_FLAG_RING0 | IDT_FLAG_GATE_32BIT_INT); 82 | // ... 83 | set_idt_gate(56, isr56, KERNEL_CODE_SEG, IDT_FLAG_RING0 | IDT_FLAG_GATE_32BIT_INT); 84 | set_idt_gate(57, isr57, KERNEL_CODE_SEG, IDT_FLAG_RING0 | IDT_FLAG_GATE_32BIT_INT); 85 | // ... 86 | ``` 87 | 88 | Next we have the function `isr_init` 89 | ```c 90 | void isr_init() 91 | { 92 | isr_init_idt_gates(); 93 | pic_remap(); 94 | idt_enable_gates(); 95 | 96 | unset_gate_flag(0x80, IDT_FLAG_PRESENT); 97 | } 98 | ``` 99 | it's doing the following things: 100 | - call the function `isr_init_idt_gates` to fill the `IDT` 101 | - remap the PIC (Programmable Interrupt Controller), see [pic.md](pic.md) for more information 102 | - enable all the interrupts in the `IDT` 103 | - unset the syscall interrupt, We are unsetting the syscall interrupt because we do not have syscall implemented yet, so we don't want to trigger an interrupt when the syscall is called. 104 | 105 | The next function is the `isr_handler` it's a function that is called by the `isr_common_stub` function. Here is the implementation: 106 | ```c 107 | void isr_handler(registers_t *regs) 108 | { 109 | uint32_t interrupt = regs->interrupt; 110 | 111 | if (isr_handlers[interrupt] != 0) { 112 | isr_handlers[interrupt](regs); 113 | } 114 | } 115 | ``` 116 | it's just checking is an handler is set in the global `isr_handlers`, and if it is, it's calling it. 117 | 118 | The next function is just a setter for the `isr_handlers` array: 119 | ```c 120 | void isr_register_handler(int interrupt, isr_callback callback) 121 | { 122 | isr_handlers[interrupt] = callback; 123 | set_gate_flag(interrupt, IDT_FLAG_PRESENT); 124 | } 125 | ``` 126 | 127 | And the last one is a bit more complicated, it's the `irq_handler` function: 128 | ```c 129 | void irq_handler(registers_t *regs) 130 | { 131 | if (regs->interrupt >= 40) { 132 | port_byte_out(0xa0, 0x20); 133 | } 134 | port_byte_out(0x20, 0x20); 135 | if (isr_handlers[regs->interrupt] != 0) { 136 | isr_handlers[regs->interrupt](regs); 137 | } 138 | } 139 | ``` 140 | This function is called by the `irq_common_stub` function. It's doing the following things: 141 | - if the interrupt is greater than 40, it's sending an EOI (End Of Interrupt) to the slave PIC (Programmable Interrupt Controller), see [pic.md](pic.md) for more information 142 | - it's sending an EOI to the master PIC (Programmable Interrupt Controller), see [pic.md](pic.md) for more information 143 | - it's calling the handler if it's set 144 | -------------------------------------------------------------------------------- /doc/code/interrupts/pic.md: -------------------------------------------------------------------------------- 1 | # INTRODUCTION 2 | 3 | This document describes the code in the [pic.c](../../../src/interrupts/pic.c) file. 4 | In this file you will have the pic remaping. They are existing questions about the remaping of the pic. So I will try to explain it here. 5 | 6 | What is the PIC ? 7 | The PIC (or 8259_PIC) is one of the most important chips making up the x86 architecture. It holds communication between hardware and software with a thing that we call bus, a bus is just a collection of memory adresses we can read or write in. Due to growing complexity during the time, we need more than one PIC to hold all our hardware devices. We call the addresses of the PICs "ports" 8 | 9 | Why do we need to remap it ? 10 | We need to remap the PIC because in the past, the PIC was designed to be used with a 16-bit architecture. Things evolved and we now have a 32-bit architecture. So we need to remap the PIC to avoid conflicts with the memory adresses. 11 | 12 | Why was it not fixed with the time ? 13 | Because of the backward compatibility, it's important to keep the old architecture working to avoid breaking old software. So we need to keep this mistake. 14 | 15 | # TABLE OF CONTENTS 16 | 17 | - [INTRODUCTION](#introduction) 18 | - [CODE EXPLANATION](#code-explanation) 19 | 20 | # CODE EXPLANATION 21 | 22 | In this file you will find only one function named `pic_remap`, it will do all the remap routine before loading the `IDT`. 23 | ```c 24 | void pic_remap() 25 | { 26 | port_byte_out(0x20, 0x11); 27 | port_byte_out(0xA0, 0x11); 28 | port_byte_out(0x21, 0x20); 29 | port_byte_out(0xA1, 0x28); 30 | port_byte_out(0x21, 0x04); 31 | port_byte_out(0xA1, 0x02); 32 | port_byte_out(0x21, 0x01); 33 | port_byte_out(0xA1, 0x01); 34 | port_byte_out(0x21, 0x0); 35 | port_byte_out(0xA1, 0x0); 36 | } 37 | ``` 38 | You can see that we are using the `port_byte_out` function to write in the ports. The first argument is the port number and the second is the value we want to write in the port. You can see more details in the [port.c](../../../src/interrupts/pic.c) file see the [doc](port.md) for more details. 39 | 40 | -------------------------------------------------------------------------------- /doc/code/interrupts/port.md: -------------------------------------------------------------------------------- 1 | # INTRODUCTION 2 | 3 | This file describes the code in the [port.c](../../../src/interrupts/port.c) file. 4 | You can see in this file two functions that communicate with the PIC. 5 | See the `PIC` [doc](pic.md) for more details. 6 | We have two functions, one to write in a port and one to read from a port. Both of them are using the `out` and `in` asm instructions. So we need to call in C the `asm` function to use them (If you want more about the `asm` function, see the this link: [asm](https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html)). 7 | 8 | Here is the reading function: 9 | ```c 10 | unsigned char port_byte_in(unsigned short port) 11 | { 12 | unsigned char result = 0; 13 | 14 | __asm__("in %%dx, %%al" : "=a" (result) : "d" (port)); 15 | return (result); 16 | } 17 | ``` 18 | It launches the `in` instruction with the `port` as the first argument and the `result` as the second argument. The `result` is the value we want to read from the port. 19 | 20 | The writing function is similar: 21 | ```c 22 | void port_byte_out(unsigned short port, unsigned char data) 23 | { 24 | __asm__("out %%al, %%dx" : : "a" (data), "d" (port)); 25 | } 26 | ``` 27 | It launches the `out` instruction with the `data` as the first argument and the `port` as the second argument. The `data` is the value we want to write in the port. 28 | 29 | If you do not understand all this asm bullshit (it's normal), please see the `asm` keyword documentation: [asm](https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html) -------------------------------------------------------------------------------- /doc/code/interrupts/timer.md: -------------------------------------------------------------------------------- 1 | # INTRODUCTION 2 | 3 | This file explains the code in the [timer.c](../../../src/interrupts/timer.c) file. 4 | The timer is a hardware device that sends an interrupt to the CPU at a regular interval. It's used to schedule tasks and to create a time reference. It can be usefull for us to create a time reference for things like a sleep function. or to schedule tasks in general. 5 | 6 | # TABLE OF CONTENTS 7 | 8 | - [INTRODUCTION](#introduction) 9 | - [CODE EXPLANATION](#code-explanation) 10 | 11 | # CODE EXPLANATION 12 | 13 | This file for now only contains this: 14 | ```c 15 | static unsigned int tick = 0; 16 | 17 | static void timer_callback(__attribute__((unused))registers_t *regs) 18 | { 19 | tick++; 20 | } 21 | 22 | void init_timer(unsigned int freq) 23 | { 24 | isr_register_handler(32, timer_callback); 25 | 26 | uint32_t divisor = 1193180 / freq; 27 | uint8_t low = (uint8_t)(divisor & 0xff); 28 | uint8_t high = (uint8_t)((divisor >> 8) & 0xff); 29 | 30 | port_byte_out(0x43, 0x36); 31 | port_byte_out(0x40, low); 32 | port_byte_out(0x40, high); 33 | } 34 | ``` 35 | We have two functions: 36 | - One for the initialization of the timer 37 | - One for the callback of the timer 38 | 39 | The timer is called each time by the interrupts `32`. We can set a frequency for the timer. The frequency is the number of times the timer is called per second. The higher the frequency, the more precise the time reference will be. But the higher the frequency, the more CPU time it will take to call the timer. So we have to find a balance between the two. 40 | To set the frequency we have to set the divisor of the timer. The divisor is the number of ticks per second. The higher the divisor, the lower the frequency. The lower the divisor, the higher the frequency. The formula to calculate the divisor is `1193180 / freq`. `1193180` is the base frequency of the timer. `freq` is the frequency we want to set. The result of the formula is the divisor. We have to split the divisor in two parts. The lower part and the higher part. The lower part is the first 8 bits of the divisor. The higher part is the last 8 bits of the divisor. We have to send the lower part to the port `0x40` and the higher part to the port `0x40` too. But before sending the lower part we have to send the command `0x36` to the port `0x43`. This command is used to set the divisor of the timer. The lower part is sent first and then the higher part. The order is important. 41 | 42 | The `timer_callback` function is just a function that increments the static `tick` variable. This variable is used to create a time reference. We can use this variable to create a sleep function for example. 43 | -------------------------------------------------------------------------------- /doc/code/memory/segmentation.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epi-osdev/workshop-student/50ebc664c652d10992c75ef0d35312b767a8cce7/doc/code/memory/segmentation.md -------------------------------------------------------------------------------- /doc/img/hard_disk_structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epi-osdev/workshop-student/50ebc664c652d10992c75ef0d35312b767a8cce7/doc/img/hard_disk_structure.png -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM i686/ubuntu:latest 2 | 3 | # Update from apt sources files and upgrade packages 4 | # + download needed dependencies 5 | RUN apt-get update 6 | RUN apt-get install -y make \ 7 | wget \ 8 | unzip \ 9 | nasm \ 10 | lbzip2 \ 11 | grub-common \ 12 | qemu 13 | 14 | # Re-used environment variables 15 | ENV DIR="/osdev" 16 | ENV I686="i686-elf-tools-linux" 17 | ENV I686OUT="/opt/${I686}" 18 | 19 | # Working directory of the container 20 | WORKDIR ${DIR} 21 | 22 | # Install the i686 toolchain 23 | 24 | # Download pre-built i686 binaries (GCC + linker) 25 | RUN wget -P /tmp "https://github.com/lordmilko/i686-elf-tools/releases/download/7.1.0/${I686}.zip" 26 | # Extract the files to the i686 tool directory 27 | RUN unzip -d "${I686OUT}" "/tmp/${I686}" 28 | 29 | # Create an user (avoid root) 30 | RUN useradd -ms /bin/bash theo 31 | USER theo 32 | 33 | # Update PATH 34 | ENV PATH="${PATH}:${I686OUT}/bin" 35 | 36 | ENTRYPOINT ["/bin/bash"] 37 | 38 | # Usage example: 39 | # 40 | # docker build -t osdev . 41 | # 42 | # docker run -it -e DISPLAY=$DISPLAY \ 43 | # -v /tmp/.X11-unix:/tmp/.X11-unix \ 44 | # -v $PWD:/osdev osdev 45 | # 46 | # In this case $PWD point to the project root folder 47 | # 48 | # xhost manages X server control, it filters the authorized clients 49 | # perms: family:user 50 | # xhost +si:localuser:root 51 | # ^ allow a single loacl user root 52 | -------------------------------------------------------------------------------- /iso/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epi-osdev/workshop-student/50ebc664c652d10992c75ef0d35312b767a8cce7/iso/.gitkeep -------------------------------------------------------------------------------- /src/boot_sector/boot_sector.asm: -------------------------------------------------------------------------------- 1 | [org 0x7c00] 2 | 3 | [bits 16] 4 | start: 5 | mov ax, 0x00 6 | mov ds, ax 7 | mov es, ax 8 | mov ss, ax 9 | mov bp, 0x7c00 10 | mov sp, bp 11 | 12 | [bits 16] 13 | run_16: 14 | mov [BOOT_DRIVE], dl 15 | call load_kernel 16 | call switch_to_pm 17 | 18 | %include "src/boot_sector/disk.asm" 19 | %include "src/boot_sector/switch_pm.asm" 20 | %include "src/boot_sector/gdt.asm" 21 | 22 | [bits 16] 23 | load_kernel: 24 | mov bx, KERNEL_OFFSET 25 | mov dh, 31 26 | mov dl, [BOOT_DRIVE] 27 | call disk_load 28 | ret 29 | 30 | [bits 32] 31 | entry_point: 32 | call KERNEL_OFFSET 33 | jmp $ 34 | 35 | BOOT_DRIVE db 0 36 | KERNEL_OFFSET equ 0x1000 37 | 38 | times 510 - ($-$$) db 0 39 | dw 0xaa55 40 | -------------------------------------------------------------------------------- /src/boot_sector/disk.asm: -------------------------------------------------------------------------------- 1 | ; load DH sectors to ES:BX from drive DL 2 | ; parameters: 3 | ; DL = drive number 4 | ; ES:BX = buffer to load sectors to 5 | ; DH = number of sectors 6 | 7 | disk_load: 8 | push dx 9 | mov ah, 0x02 10 | mov al, dh 11 | mov ch, 0x00 12 | mov dh, 0x00 13 | mov cl, 0x02 14 | int 0x13 15 | jc disk_error 16 | pop dx 17 | cmp dh, al 18 | jne disk_error 19 | ret 20 | 21 | disk_error: 22 | jmp $ 23 | -------------------------------------------------------------------------------- /src/boot_sector/gdt.asm: -------------------------------------------------------------------------------- 1 | ; Offset for the segment descriptor 2 | ; It doesnt point on the segment itself 3 | CODE_SEG equ gdt_code - gdt_start 4 | DATA_SEG equ gdt_data - gdt_start 5 | 6 | gdt_start: 7 | 8 | gdt_null: 9 | dd 0x0 10 | dd 0x0 11 | ; offset 0x8 12 | gdt_code: ; CS SHOULD POINT TO THIS 13 | dw 0xffff ; Segment limit first 0-15 bits 14 | dw 0 ; Base first 0-15 bits 15 | db 0 ; Base 16-23 bits 16 | db 0x9a ; Access byte 17 | db 11001111b ; High 4 bit flags and the low 4 bit flags 18 | db 0 ; Base 24-31 bits 19 | 20 | ; offset 0x10 21 | gdt_data: ; DS, SS, ES, FS, GS 22 | dw 0xffff ; Segment limit first 0-15 bits 23 | dw 0 ; Base first 0-15 bits 24 | db 0 ; Base 16-23 bits 25 | db 0x92 ; Access byte 26 | db 11001111b ; High 4 bit flags and the low 4 bit flags 27 | db 0 ; Base 24-31 bits 28 | 29 | gdt_end: 30 | 31 | gdt_descriptor: 32 | dw gdt_end - gdt_start-1 33 | dd gdt_start 34 | -------------------------------------------------------------------------------- /src/boot_sector/switch_pm.asm: -------------------------------------------------------------------------------- 1 | ; Switching from real mode to protected mode 2 | 3 | [bits 16] 4 | switch_to_pm: 5 | cli 6 | lgdt [gdt_descriptor] 7 | mov eax, cr0 8 | or eax, 0x1 9 | mov cr0, eax 10 | jmp CODE_SEG:init_pm 11 | 12 | [bits 32] 13 | init_pm: 14 | mov ax, DATA_SEG 15 | mov ds, ax 16 | mov ss, ax 17 | mov es, ax 18 | mov fs, ax 19 | mov gs, ax 20 | 21 | mov ebp, 0x90000 22 | mov esp, ebp 23 | 24 | call entry_point 25 | -------------------------------------------------------------------------------- /src/entry/entry_point.asm: -------------------------------------------------------------------------------- 1 | [bits 32] 2 | [extern kernel_main] ; Define calling point. Must have same name as kernel.c 'main' function 3 | call kernel_main ; Calls the C function. The linker will know where it is placed in memory 4 | jmp $ 5 | -------------------------------------------------------------------------------- /src/entry/kernel_entry.c: -------------------------------------------------------------------------------- 1 | #include "VGA.h" 2 | 3 | void init() 4 | { 5 | vga_clear_screen(); 6 | } 7 | 8 | void kernel_main() 9 | { 10 | init(); 11 | char str[] = "Hello, World!"; 12 | vga_printf_at(str, 0x0F, 0, 0); 13 | } 14 | -------------------------------------------------------------------------------- /src/interrupts/pic.c: -------------------------------------------------------------------------------- 1 | #include "interrupts/port.h" 2 | 3 | void pic_remap() 4 | { 5 | port_byte_out(0x20, 0x11); 6 | port_byte_out(0xA0, 0x11); 7 | port_byte_out(0x21, 0x20); 8 | port_byte_out(0xA1, 0x28); 9 | port_byte_out(0x21, 0x04); 10 | port_byte_out(0xA1, 0x02); 11 | port_byte_out(0x21, 0x01); 12 | port_byte_out(0xA1, 0x01); 13 | port_byte_out(0x21, 0x0); 14 | port_byte_out(0xA1, 0x0); 15 | } 16 | -------------------------------------------------------------------------------- /src/interrupts/pic.h: -------------------------------------------------------------------------------- 1 | #ifndef __PIC_H__ 2 | #define __PIC_H__ 3 | 4 | void pic_remap(); 5 | 6 | #endif 7 | -------------------------------------------------------------------------------- /src/interrupts/port.c: -------------------------------------------------------------------------------- 1 | #include "port.h" 2 | 3 | /** 4 | * @brief 5 | * 6 | * Cathcing the reponse of the request (port_byte_out) 7 | * 8 | * @param port : the port to read from 9 | * @return unsigned char : the byte read from the port 10 | */ 11 | unsigned char port_byte_in(unsigned short port) 12 | { 13 | unsigned char result = 0; 14 | 15 | __asm__("in %%dx, %%al" : "=a" (result) : "d" (port)); 16 | return (result); 17 | } 18 | 19 | /** 20 | * @brief 21 | * 22 | * Requesting the port with the data 23 | * 24 | * @param port : port number 25 | * @param data : data to write 26 | */ 27 | void port_byte_out(unsigned short port, unsigned char data) 28 | { 29 | __asm__("out %%al, %%dx" : : "a" (data), "d" (port)); 30 | } 31 | -------------------------------------------------------------------------------- /src/interrupts/port.h: -------------------------------------------------------------------------------- 1 | #ifndef __PORT_H__ 2 | #define __PORT_H__ 3 | 4 | unsigned char port_byte_in(unsigned short port); 5 | void port_byte_out(unsigned short port, unsigned char data); 6 | 7 | #endif 8 | -------------------------------------------------------------------------------- /src/interrupts/timer.c: -------------------------------------------------------------------------------- 1 | #include "timer.h" 2 | 3 | static unsigned int tick = 0; 4 | 5 | static void timer_callback(__attribute__((unused))registers_t *regs) 6 | { 7 | tick++; 8 | } 9 | 10 | void init_timer(unsigned int freq) 11 | { 12 | isr_register_handler(32, timer_callback); 13 | 14 | uint32_t divisor = 1193180 / freq; 15 | uint8_t low = (uint8_t)(divisor & 0xff); 16 | uint8_t high = (uint8_t)((divisor >> 8) & 0xff); 17 | 18 | port_byte_out(0x43, 0x36); 19 | port_byte_out(0x40, low); 20 | port_byte_out(0x40, high); 21 | } 22 | -------------------------------------------------------------------------------- /src/interrupts/timer.h: -------------------------------------------------------------------------------- 1 | #ifndef __TIMER_H__ 2 | #define __TIMER_H__ 3 | 4 | #include "interrupts/isr.h" 5 | #include "interrupts/port.h" 6 | #include "utils/types.h" 7 | 8 | void init_timer(uint32_t freq); 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /src/utils/VGA.h: -------------------------------------------------------------------------------- 1 | #ifndef __EOS_UTILS_VGA_H__ 2 | #define __EOS_UTILS_VGA_H__ 3 | 4 | /** 5 | * @brief This is the global API for the VGA driver. 6 | * all the functions that could be called by other files must be included here. 7 | * And this file must be the only one to be included when using the VGA driver. 8 | */ 9 | 10 | #include "VGA/clear.h" 11 | #include "VGA/print.h" 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /src/utils/VGA/VGA.h: -------------------------------------------------------------------------------- 1 | #ifndef __EOS_UTILS_VGA_VGA_H__ 2 | #define __EOS_UTILS_VGA_VGA_H__ 3 | 4 | #include "types.h" 5 | 6 | #define VGA_WIDTH 80 7 | #define VGA_HEIGHT 25 8 | #define VGA_MEMORY ((char *)0xb8000) 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /src/utils/VGA/clear.c: -------------------------------------------------------------------------------- 1 | #include "clear.h" 2 | #include "VGA.h" 3 | 4 | void vga_clear_screen() 5 | { 6 | for (int i = 0; i < VGA_WIDTH * VGA_HEIGHT * 2; i += 2) { 7 | VGA_MEMORY[i] = '0'; 8 | VGA_MEMORY[i + 1] = 0x00; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/VGA/clear.h: -------------------------------------------------------------------------------- 1 | #ifndef __EOS_UTILS_VGA_VGA_CLEAR_H__ 2 | #define __EOS_UTILS_VGA_VGA_CLEAR_H__ 3 | 4 | /** 5 | * @brief clear the entire VGA screen with spaces 6 | * 7 | */ 8 | void vga_clear_screen(); 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /src/utils/VGA/print.c: -------------------------------------------------------------------------------- 1 | #include "VGA.h" 2 | #include "print.h" 3 | #include "string.h" 4 | #include 5 | 6 | static size_t get_index(uint8_t x, uint8_t y) 7 | { 8 | return y * 2 * VGA_WIDTH + x * 2; 9 | } 10 | 11 | int vga_putchar_at(char c, uint8_t color, uint8_t x, uint8_t y) 12 | { 13 | if (x >= VGA_WIDTH || y >= VGA_HEIGHT) 14 | return -1; 15 | const size_t index = get_index(x, y); 16 | VGA_MEMORY[index] = c; 17 | VGA_MEMORY[index + 1] = color; 18 | return 1; 19 | } 20 | 21 | int vga_putstr_at(const char *str, uint8_t color, uint8_t x, uint8_t y) 22 | { 23 | size_t size_printed = 0; 24 | 25 | if (x >= VGA_WIDTH || y >= VGA_HEIGHT) 26 | return -1; 27 | for (size_t i = 0; str[i]; ++i, ++size_printed) 28 | vga_putchar_at(str[i], color, x + i, y); 29 | return size_printed; 30 | } 31 | 32 | int vga_print_int_at(int num, uint8_t color, uint8_t x, uint8_t y) 33 | { 34 | char str[16] = {0}; 35 | 36 | if (x >= VGA_WIDTH || y >= VGA_HEIGHT) 37 | return -1; 38 | itoa(num, str, 10); 39 | return vga_putstr_at(str, color, x, y); 40 | } 41 | 42 | int vga_printf_at(const char *format, uint8_t color, uint8_t x, uint8_t y, ...) 43 | { 44 | va_list args; 45 | int size_printed = 0; 46 | 47 | if (x >= VGA_WIDTH || y >= VGA_HEIGHT) 48 | return -1; 49 | va_start(args, y); 50 | for (size_t i = 0; format[i]; ++i) { 51 | if (format[i] == '%') { 52 | switch (format[++i]) { 53 | case 'd': 54 | size_printed += vga_print_int_at(va_arg(args, int), color, x + size_printed, y); 55 | break; 56 | case 's': 57 | size_printed += vga_putstr_at(va_arg(args, char *), color, x + size_printed, y); 58 | break; 59 | case 'c': 60 | size_printed += vga_putchar_at(va_arg(args, int), color, x + size_printed, y); 61 | break; 62 | default: 63 | size_printed += vga_putchar_at(format[i], color, x + size_printed, y); 64 | break; 65 | } 66 | } else { 67 | size_printed += vga_putchar_at(format[i], color, x + size_printed, y); 68 | } 69 | } 70 | va_end(args); 71 | return size_printed; 72 | } 73 | -------------------------------------------------------------------------------- /src/utils/VGA/print.h: -------------------------------------------------------------------------------- 1 | #ifndef __EOS_VGA_PRINT_H__ 2 | #define __EOS_VGA_PRINT_H__ 3 | 4 | #include "types.h" 5 | 6 | /** 7 | * @brief Prints a string to the VGA buffer with the given color and position 8 | * @param c: the char to print 9 | * @param color: the color to print the char with 10 | * @param x: the x position to print the char at 11 | * @param y: the y position to print the char at 12 | * @return 1 if success 13 | * @return -1 if position error 14 | */ 15 | int vga_putchar_at(char c, uint8_t color, uint8_t x, uint8_t y); 16 | 17 | /** 18 | * @brief Prints a string to the VGA buffer with the given colors at the given position 19 | * @param str: the string to print 20 | * @param color: the color to print the string with 21 | * @param x: the x position to print the string at 22 | * @param y: the y position to print the string at 23 | * @return length of the string printed if success 24 | * @return -1 if position error 25 | */ 26 | int vga_putstr_at(const char *str, uint8_t color, uint8_t x, uint8_t y); 27 | 28 | /** 29 | * @brief Prints a number to the VGA buffer with the given colors at the given position 30 | * @param num: the number to print 31 | * @param color: the color to print the number with 32 | * @param x: the x position to print the number at 33 | * @param y: the y position to print the number at 34 | * @return length of the number printed if success 35 | * @return -1 if position error 36 | */ 37 | int vga_print_int_at(int num, uint8_t color, uint8_t x, uint8_t y); 38 | 39 | /** 40 | * @brief Prints a formatted string to the VGA buffer with the given colors at the given position 41 | * @param fmt: the formatted string to print 42 | * @param color: the color to print the formatted string with 43 | * @param x: the x position to print the formatted string at 44 | * @param y: the y position to print the formatted string at 45 | * @param ...: the arguments to the formatted string 46 | * @return length of the formatted string printed if success 47 | * @return -1 if position error 48 | */ 49 | int vga_printf_at(const char *format, uint8_t color, uint8_t x, uint8_t y, ...); 50 | 51 | #endif 52 | -------------------------------------------------------------------------------- /src/utils/string.h: -------------------------------------------------------------------------------- 1 | #ifndef __EOS_UTILS_STRING_H__ 2 | #define __EOS_UTILS_STRING_H__ 3 | 4 | /** 5 | * @brief This is the global API for the string utils. 6 | * all the functions that could be called by other files must be included here. 7 | * And this file must be the only one to be included when using the string utils. 8 | */ 9 | 10 | #include "string/itoa.h" 11 | #include "string/revstr.h" 12 | #include "string/strlen.h" 13 | 14 | #endif -------------------------------------------------------------------------------- /src/utils/string/itoa.c: -------------------------------------------------------------------------------- 1 | #include "itoa.h" 2 | #include "revstr.h" 3 | 4 | void itoa(int num, char *str, uint8_t base) 5 | { 6 | int i = 0; 7 | int is_negative = 0; 8 | 9 | if (num == 0) { 10 | str[i++] = '0'; 11 | str[i] = '\0'; 12 | return; 13 | } 14 | if (num < 0 && base == 10) { 15 | is_negative = 1; 16 | num = -num; 17 | } 18 | while (num != 0) { 19 | int rem = num % base; 20 | str[i++] = (rem > 9) ? (rem - 10) + 'a' : rem + '0'; 21 | num /= base; 22 | } 23 | if (is_negative) { 24 | str[i++] = '-'; 25 | } 26 | str[i] = '\0'; 27 | revstr(str); 28 | } -------------------------------------------------------------------------------- /src/utils/string/itoa.h: -------------------------------------------------------------------------------- 1 | #ifndef __EOS_UTILS_STRING_ITOA_H__ 2 | #define __EOS_UTILS_STRING_ITOA_H__ 3 | 4 | #include "types.h" 5 | 6 | /** 7 | * @brief This function converts an integer to a string and stores it in a buffer. 8 | * @param num The number to be converted. 9 | * @param str The buffer to store the string. 10 | * @param base The base of the number. 11 | */ 12 | void itoa(int num, char *str, uint8_t base); 13 | 14 | #endif -------------------------------------------------------------------------------- /src/utils/string/revstr.c: -------------------------------------------------------------------------------- 1 | #include "revstr.h" 2 | 3 | void revstr(char *str) 4 | { 5 | char *end = str; 6 | char tmp; 7 | 8 | if (str) { 9 | while (*end) { 10 | ++end; 11 | } 12 | --end; 13 | while (str < end) { 14 | tmp = *str; 15 | *str++ = *end; 16 | *end-- = tmp; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/string/revstr.h: -------------------------------------------------------------------------------- 1 | #ifndef __EOS_UTILS_STRING_REVSTR_H__ 2 | #define __EOS_UTILS_STRING_REVSTR_H__ 3 | 4 | /** 5 | * @brief This function reverses a string itself. 6 | * @param str The string to be reversed. 7 | */ 8 | void revstr(char *str); 9 | 10 | #endif -------------------------------------------------------------------------------- /src/utils/string/strlen.c: -------------------------------------------------------------------------------- 1 | #include "strlen.h" 2 | 3 | size_t strlen(const char *str) 4 | { 5 | if (str == NULL) { 6 | return 0; 7 | } 8 | size_t size; 9 | for (size = 0; str[size] != '\0'; size++); 10 | return size; 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/string/strlen.h: -------------------------------------------------------------------------------- 1 | #ifndef __EOS_UTILS_STRING_STRLEN_H__ 2 | #define __EOS_UTILS_STRING_STRLEN_H__ 3 | 4 | #include "types.h" 5 | 6 | /* 7 | * @brief: a function to get the lenght of a string 8 | * @param str: the string that will recover the size 9 | * @return: the value of the lenght of the string 10 | */ 11 | size_t strlen(const char *str); 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /src/utils/types.h: -------------------------------------------------------------------------------- 1 | #ifndef __EOS_UTILS_TYPES_H__ 2 | #define __EOS_UTILS_TYPES_H__ 3 | 4 | typedef unsigned char uint8_t; 5 | typedef unsigned short uint16_t; 6 | typedef unsigned int uint32_t; 7 | typedef uint32_t size_t; 8 | #define NULL 0 9 | 10 | #endif 11 | --------------------------------------------------------------------------------