Ever since I started wondering how stuff on my computer worked, I had the dream of writing my very own operating system from scratch. And this is what I've learned in the last 8 months:
For starters, you need to know how a computer works on a low-level to understand what you're doing, so let's start with the very basic:
A transistor is an electronic component that acts as a switch, it uses semiconductors (most commonly silicon) to turn or on off electrical circuits. We can use these to create the so-called logic gates.
A common transistor has three ends: the collector, the base and the emitter. Each does what it's name describes, but I'm going to explain it anyways.
Collector: It's the pin that collects the voltage from the electrical source of the circuit, this is should be always on.
Base: This pin collects an input that could be either on or off, it doesn't have to be necessarily the same voltage as the collector, but it must come from the same circuit, otherwise, these wouldn't be connected and the electrons wouldn't have a path to travel in the circuit. (tl;dr: base and collector must have a common ground).
Emitter: This pin outputs voltage if the base input is HIGH (on).
Now that you know the basics on transistors, we get to explore what are these magical (and complex) logic gates.
Logic gates are pretty much the electronic circuitry of the propositions you learnt in your philosophy class, but I'm going to explain the basics of them.
As we learnt before, computers work with the principle of conditions, thanks to the nature of the transistors present in the CPU, and thus, we need to know more about these conditions.
Through a complex series of arrangements, you can make a transistor "think" logically, using these logic gates:
Not: This is the equivalent to the negation in philosophy, it inverts the input, so for instance, if you input HIGH to this logic gate, the output will be LOW (off).
Or: This logic gate takes two (or more, as all of the gates we're going to explore from now on) inputs, and it's output will be HIGH only if one or more inputs are HIGH.
And: This logic gate's output will be HIGH only if all of it's inputs are HIGH.
Xor: This logic gate's output will be HIGH only if it's input are different from each other. And now that we're exploring this concept, you should know that logic gates can only take two inputs at once, so what you do when there are more than two inputs is concatenate them, so, a xor with three inputs would be
((p ⊻ q) ⊻ r).
Nor: This logic gate is the conjunction of the
or gates, meaning, the output will be like if you passed a NOT gate through a OR condition (every possible input returns LOW except for the one where all it's values are LOW).
Now with this knowledge, we can pass on to the CPU.
The Central Processing Unit (a.k.a. CPU, or simply, processor) is the circuit that processes all the signals in your computer, it is generally divided into more units, like the ALU (Arithmetic Processing Unit), and Cores (you can think of these as tiny processors in which the jobs are divided into).
Each of these smaller units uses logic gates and transistors in them, and yes, you can make math using "Yes" and "No".
The CPU has also a set of instructions, which we'll use to program what it's doing at the time of our code's execution, as you'll discover later, these are General-Purpose and very intuitive.
The CPU also has these sections of memory (in which you store data) built-in called registers, in the x86 architecture (the one most PC's run on) has general purpose registers, but also some that are for specific use cases, such as storage, video, etc.
With this, let's pass to the memory.
The memory (not to be confused with storage) is the component in your computer used to load the programs you run on your computer. The information the program needs to store is temporarily loaded onto your computer's memory. This component is also known as RAM (random-access memory), and it's volatile, meaning it's contents are erased when it's powered off.
Memory divides into addresses, which are tiny sections of it that can contain data. These are commonly represented in hexadecimal, speaking of which, is represented by numbers that start with "0x", just like "0b" for binary.
Now, let's pass over to some action.
Assembly is a general-purpose low-level programming language designed to communicate directly with a computer's hardware (mainly CPU and memory). It's fairly simple compared manually writing binary. Assembly uses CPU instructions, and has to be compiled ("assembled") to run.
As this post is getting bigger and bigger without getting to code, I'm going to end it with the code for a "Hello world" message in screen written in assembly, but don't worry, I'll explain how it works.
org 0x7C00 bits 16 xor ax, ax mov ds, ax start: mov si, msg mov ah, 0x0E .loop lodsb or al, al jz halt int 0x10 jmp .loop halt: hlt msg: db "Hello world!", 0 times 510 - ($ - $$) db 0 dw 0xAA55
The first line defines the origin in memory of the program,
0x7c00 is the first address value in which the program is going to be stored. There is a reason of to why this number, that I'll explain in another post.
bits 16 defines that the program will run on 16 bits (even though our CPU could be 32 or 64 bits. This is a x86 architecture convention).
xor ax, ax means the next line will run if the condition is true. It compares the
ax register against itself (again, there's a reason for this that I'll explain later).
mov ds, ax moves the contents of
ax over to
ds, both are CPU registers.
msg: (seen later) defines sections of code (you can think of these as functions), though, the
msg: section is more like a variable, as it immediately returns "Hello world" and a null termination character.
mov si, msg moves the contents of the
msg variable over to the
si register. This will be later used to display the value of msg.
.loop section is a recursive function that calls itself over and over until the character to evaluate is the null termination character (
\0). It calls the
0x10 interrupt (used to display text stored in the
al register if the value on the register
ah is 0x0E (teletype)), and then, passes to the next character to evaluate it.
times 510 - ($ - $$) db 0 calculates the size of the code minus 510 (so that we have two bytes left) and then fills the space between the two last bytes of the sector and our code with zeroes.
dw 0xAA55 fills the two last bytes with the
55 AA hex values, note that I inverted the order of these two, this was on purpose, as the x86 architecture is little-endian.
We need to have these
55 AA bytes at the end so that the BIOS knows we're trying to boot off of the drive that contains our code.
Creating an operating system is a process for which you need low-level computing knowledge, which you should have acquired if you read the full post. Mainly, you'll need to know Assembly, and what are the components in your computer, how they work and what they do.
By the end, you'll have a working bootable "Hello world".
That's all for today, and thanks a lot for your attention!