-
Notifications
You must be signed in to change notification settings - Fork 4
Final Report
LuaOS is a project started on a whim one day, with the idea of programming lower level parts of an operating system in a higher level language, Lua. I had hoped to achieve a very early, but functioning operating system that could be run on real hardware and would let the user create Lua scripts and have them be run.
My hypothesis was that if the act of creating low level parts of the kernel (device drivers, APIs, kernel additions) were abstracted away and under a easy to use and understand programming language (Lua), then the time and skill it would take to make these components would be greatly decreased.
- Computer running MacOS or Linux
- Git
- XMake
- Xorriso
- Lua >= 5.3
- LuaRocks
- RapidJSON rock
- The GNU Compiler Collection (Version >= 9)
- GNU Binutils (Version >= 9)
First, make sure you have all of the materials installed, and clone the repository.
git clone https://www.github.com/Frityet/LuaOS.git
Now, run to enable cross compilation mode
xmake f -p cross
Now that we have our build system setup, we must get out cross compiler working. I recommend this tutorial.
After that is setup, try running
lua build.lua run
And watch QEMU start a new VM instance of LuaOS!
For creating LuaOS, I must first be able to get code I write to be recognised by the BIOS (piece of software which runs when your computer starts up, that initialises all the devices in your computer and loads the operating system). Previously, I had actually written my own bootloader (piece of code put onto a specific part of the "boot drive" of the computer that tells the computer where the operating system is and does initialisation and loading), but it was poorly documented and quite unstable, which are 2 very big issues when working with x86 Assembly code. I asked other operating system developers on an alternative, and was pointed towards Limine. With the bootloader in place, I need to now start writing the kernel, which is the core of the operating system. The kernel has complete control over everything the operating system, and is the most critical part of the operating system as all programs, apps, scripts all interact with the kernel in some way (figure 1.0). The kernel facilitates interactions between every part of your system.
figure 1.0
There are many different types of kernels, which describe how much work the kernel does. Monolithic kernels have most to all of the services built in, ready for use at any time. This comes at the cost of speed, time, and modularity, where having all of the components built in means that new updates or additions to the kernel would require for a re-compilation. Microkernels on the other hand has minimal components in itself, relying on the services to be on other parts of the operating system. Micro-kernel designs are faster, and much more modular, but also tricky to get right.
The LuaOS kernel uses a micro-kernel design, as the modularity is required for the scripts.
The first part of the kernel I wrote was a wrapper over the bootloader's console interface. Basically, the bootloader provided my kernel with a function which would let me write one character (8 bit number, signed) to the screen. Because strings in C are simply an array of characters stringed together, I created functions which let me write text to the console easily and nicely.
static void kprintfln(string fmt, ...)
{
va_list arglist;
va_start(arglist, fmt);
for (; *fmt != '\0'; fmt++) {
if(*fmt == '%') {
kprint(va_arg(arglist, string));
continue;
}
stivale_print(fmt, 1);
}
va_end(arglist);
stivale_print("\n", 1);
}
The console also supported ANSI escape codes which let me use colours and styling on my text from the get go, which was very useful.
After creating this part, I moved on to the framebuffer. The framebuffer represents all of the pixels on the screen, and setting the value of that pixel allows for crude images and shapes. The bootloader gave the kernel the memory address to the framebuffer, and to write to the framebuffer I would have to specify an offset in bytes of which would calculate to the position of the pixel using the equation
uint8_t *pixel = vram //Address of the framebuffer
+ y //Y position you want to get
* pixelpitch //How many bytes of memory you skip to go one pixel down
+ x //X position you want to get
* pixelwidth; //How many bytes of memory you skip to go one pixel right
Using this equation, and some iteration, I was able to draw a rectangle to the screen with great inefficiency.
void draw_rect(point_t position, point_t size, uint32_t colour)
{
screen.cursor_position = position;
for (int height = 0; height < size.y; ++height) {
for (int width = 0; width < size.x; ++width) {
screen.framebuffer[screen.cursor_position.y * (screen.pitch / 4)] = colour;
screen.cursor_position.x++;
}
screen.cursor_position.x = position.x;
screen.cursor_position.y++;
}
screen.cursor_position.x = 0;
screen.cursor_position.y = 0;
}
Now it was time for more technical, and much less fun parts of the operating system.
The emulator I was using, QEMU, supported writing a log which would let me send data from my operating system straight to my PC, just by writing bytes to a specific port on the computer.
For writing to ports, I created a function in assembly for the input and output.
GLOBAL IN_PORT
IN_PORT:
IN AL, DX
RET
GLOBAL OUT_PORT
OUT_PORT:
OUT DX, AL2
RET
yeah this is pseudocode, cry about it
Then, I used these functions to write to the log
void lwrite(string message)
{
for (int i = 0; i < strlen(message); ++i) {
OUT_PORT(LOGGING_PORT, message[i]);
}
}
and I could not magically send data from my OS to my PC, allowing for easy logging.
With the logging implemented, its time to get interrupted. Specifically, system interrupts. System interrupts are signals emitted by devices in your computer being sent to the CPU with the instructions to stop the current action, and to do a different action. The interrupt "interrupts" the CPUs current process, to get a different process to be executed. Interrupts are just numbers, which the CPU uses to find the applicant interrupt in the Interrupt Descriptor Table, which is a registry of all valid interrupts.
For example, interrupt 0x0
, once sent to the CPU, has the instruction to write a "divide by 0" error message to the screen
register_interrupt_handler(0x0,
(uint64_t) &ASM_DIV_BY_ZERO,
0x8E,
0);
void div_by_zero_i(struct interrupt_frame *iframe)
{
console.println("\x1b[1;31mERROR: DIVISION BY ZERO");
HANG();
}
For registering interrupts, the bootloader cannot help this time, it must be done all by ourselves.