Tic Tac Toe Kernel is a project I created with Valyreon in June of 2018, that has helped us gain better understanding of Kernel development. It provides users with an opportunity to engage in a game of Tic Tac Toe against the Kernel at two different difficulty levels: easy and hard. At the hard level, the Kernel is impossible to beat, making the best case scenario a draw game, while the easy level is made in such a way that the Kernel is playing the first free board space, thus making it easy to gain a win over the Kernel.
Tic Tac Toe Kernel easy gameplay running on Oracle VM VirtualBox
- Tic Tac Toe Kernel
The idea behind this project was to develop a simple, beginner-friendly, UNIX-clone operating system for the x86 architecture. The created OS is monolithic as this was the path of the least resistance while programming. Because of the aforementioned reasons, this is a very simple kernel because the used algorithms are not optimal or uttermost space efficient. The goal with this project was to gain basic knowledge on the kernel development and not to write the most efficient kernel possible. By writing this, a hope I will inspire someone to write their own kernel. Because of the extensible nature of this kernel, you could use the provided code and easily build your kernel on top of this one.
Note
-
The first part of this README will focus on the explanation of the kernel development, while the second part will focus on the Tic Tac Toe game and the Decision Tree.
-
The provided code contains a lot of comments, so if something isn't explained in README it is probably explained in the source code.
To compile and run this code you will need Linux, GCC, LD, GNU MAKE, NASM and QEMU. In order to fully understand everything that is written in this project, you will need to have a very good knowledge of C and a pretty good understanding of assembly (Intel syntax) as well as some basic knowledge of registers. Knowledge of Linux will be very helpful as the scripts and tutorial is tailor for it.
...
This file tells LD (GNU Linker) how to set up our kernel image. For more information about the LD check out this link. Firstly, it tells that the start location of our binary should be the symbol start. The .text section, the place where our code goes, should be first and should start at 0x100000 or 1 MB. The .data section should be next, followed by the .bss section, while each should be page-aligned with ALIGN(4K)
.
Note: The linker script specifies start as the entry point to the kernel and the bootloader will jump to this position once the kernel has been loaded.
To start your OS we will an existing piece of software to load it. This is called bootloader and we have used GRUB as the goal of this project wasn't to develop our own bootloader. Unless you really want to develop a bootloader, I recommend using one of the already available bootloaders. The boot.s is a Kernel start location which also defines multiboot header. Multiboot Standard describes an interface between the bootloader and the OS kernel, so we don't have to worry about that. It works by putting some magic values in some global variables inside the multiboot header.
Lines 10 to 14 in 1e8b7d2
When the bootloader sees these values, it recognizes the kernel as multiboot compatible, and it knows how to load us, and it can even forward us important information. Since there is no stack yet, we need to make sure the global variables are set correctly.
Lines 2 to 6 in 1e8b7d2
The Multiboot Standard doesn't define the value of the stack pointer register ESP, and it is up to the kernel to provide a stack. We allocate room for a small stack by creating a symbol at the bottom of it (stack_bottom
), allocating x amount of bytes and finally creating a symbol at the top (stack_top
).
mov esp, stack_top
To set up a stack, we set the ESP register to point to the top of the stack. This is very important, as a program written in C cannot function without a stack. After which we put the computer into an infinite loop. We achieve this by:
- Disabling interrupts with cli (clear interrupt enable in eflags).
- Waiting for the next interrupt to arrive with hlt.
- Jumping to the hlt instruction if it ever wakes up due to a non-maskable interrupt occurring or due to system management mode.
Warning
-
The stack on x86 must be 16-byte aligned.
-
The stack grows downwards on x86.
-
The compiler will assume the stack is properly aligned and failure to align the stack will result in undefined behavior.
-
CHECKSUM field is defined such that when the magic number, the flags and this are added together, the total must be zero. It is for error checking.
We have defined few commonly-used global functions inside the common.c and common.h. They contain function for writing to and reading from the I/O bus, strcmp, strcpy and strcat functions.
First we calculate a linear offset of the
The GDT and the IDT are descriptor tables. They are arrays of flags and bit values describing the operation of either the segmentation system (in the case of the GDT), or the interrupt vector table (IDT).
The Global Descriptor Table (GDT) is a table in memory that defines the processor's memory segments. The GDT sets the behavior of the segment registers and helps to ensure that protected mode operates smoothly. Segmentation is built into the x86 architecture, and it's impossible to get around it. With segmentation, every memory access is evaluated with respect to a segment. That is, the memory address is added to the segment's base address, and checked against the segment's length. The GDT is pointed to by a special register in the x86 chip, the GDT Register, or simply the GDTR. The GDTR is 48 bits long. The lower 16 bits tell the size of the GDT, and the upper 32 bits tell the location of the GDT in memory.
Lines 3 to 15 in 1e8b7d2
Keep in mind that the GRUB sets a GDT up for you. The problem is that you don't know where that GDT is, or what's in it. So you could accidentally overwrite it. There are total of 6 segmentation registers (cs, ds, es, fs, gs and ss). Each holds an offset into the GDT. The GDT table contains a number of entries called Segment Descriptors. Each is 8 bytes long and contains information on the starting point of the segment, the length of the segment, and the access rights of the segment.
tic-tac-toe-kernel/include/descriptor_tables.h
Lines 9 to 17 in 1e8b7d2
The Interrupt Descriptor Table (IDT) is a data structure used by the x86 architecture to implement an interrupt vector table. The IDT is used by the processor to determine the correct response to interrupts and exceptions.
Three general interrupt and exceptions sources:
- Exceptions
- Software interrupts
- External interrupts
Types of Exceptions:
- Faults - are precise exceptions reported on the boundary before the instruction causing the exception.
- Traps - are precise exceptions reported on the boundary following the instruction causing the exception.
- Aborts - are imprecise exceptions. Because they are imprecise, aborts typically do not allow reliable program restart.
Like the GDT the IDT is an array of 8-byte descriptors. Unlike the GDT the first entry of the IDT may contain a descriptor. It is just an array of entries, each one corresponding to an interrupt number. There are 256 possible interrupt numbers, so 256 must be defined. If an interrupt occurs and there is no entry for it (even a NULL entry is fine), the processor will panic and reset. The processor will sometimes need to signal your kernel. Something major may have happened, such as a divide-by-zero, or a page fault. To do this, it uses the first 32 interrupts. The special CPU-dedicated interrupts are shown below.
0 | Division by zero exception |
1 | Debug exception |
2 | Non maskable interrupt |
3 | Breakpoint exception |
4 | Overflow |
5 | Out of bounds exception |
6 | Invalid opcode exception |
7 | No coprocessor exception |
8 | Double fault |
9 | Coprocessor segment overrun |
10 | Invalid Task State Segment |
11 | Segment not present |
12 | Stack Segment Fault |
13 | General Protection Fault |
14 | Page fault |
15 | Reserved |
16 | x87 Floating Point Exception |
17 | Alignment check exception |
18 | Machine check exception |
19-31 | Reserved |
If you don't want to build your own bootable image, download the latest release of the Kernel and head over to OS testing section.
Move the build.sh
file to a src/
directory and then simply run the build script as:
sh ./src/build.sh
This will produce a binary file kernel.bin
.
You can create a bootable image containing the GRUB bootloader and your kernel using the program grub-mkrescue.
To create a bootable image, run in the following commands:
mv kernel.bin isodir/boot/kernel.bin
grub-mkrescue -o KernelXO.iso isodir
After installing QEMU use the following command to start the KernelXO.iso:
qemu-system-x86_64 -cdrom KernelXO.iso
Alternatively, you could mount the ISO image to a Virtual Machine.
Here are some kernel screenshots:
Tic Tac Toe Kernel running using QEMU
- Complete the
README.md
- Implement shutdown
- Implement score restart
- Implement gameplay restart