My Tutorials/How to program your own OS
This tells you how you write a short program and how you boot into it.
1. Use your intel-architecture (Intel/AMD) machine
2. Start your Linux
3. Write kernel.s:
start: ; this should print H mov ax, 0xe48 mov bx, 7 int 0x10 ; E mov ax, 0xe45 int 0x10 ; L mov ax, 0xe4C int 0x10 ; L mov ax, 0xe4C int 0x10 ; O mov ax, 0xe4F int 0x10 .ende jmp .ende
4. Have nasm installed
5. Compile kernel.s to machine language:
6. The result is the file kernel
7. Put your kernel.bin onto a floppy disk:
dd if=kernel of=/dev/fd0
8. boot from this floppy disk
The result is HELLO being printed on the screen.
How does the boot process work with my OS
- The computer starts
- The computer's bios stores BIOS-Interrupts into the memory
- The computer loads the first sector of your floppy (bootsector) and executes it
- In your bootsector is your kernel. It loads some numbers into the processor's registers and calls the BIOS-Interrupt $0x10.
- int $0x10 decides upon the register's content that it has to print an "H"
- The program goes on with E, L and O
- The program terminates by entering an endless loop
If you do not enter the endless loop, the rest of your bootsector would be executed, leading to unpredictable results.
Can I be independent from int $0x10 ?
- Yes! You have to print the characters on your own. I will show you in the next chapter.
Can I program this in C, too ?
- definitively yes. But you may not use any libraries that are tailored for your OS. For example, you may not use printf with stdio.h, as printf calls your linux-kernel.
Can I run this program under Linux ?
- No! Linux has special measures protecting the resources (that is, that nobody can e.g. reset the printer while printing without Linux knowing it). So, programming without protection is much more interesting than programming "under" Linux. Now have a look at what we made as machine code:
gil:~ # cat hello.bin ?H»Í?EÍ?LÍ?LÍ?OÍëâgil:~ #
beautiful, isn't it ? And every letter stands for a processor command or an argument to that. Look at it with hex (to be installed), and you will get two signs for each letter, identifying it exactly:
gil:~ # hex hello.bin 0000 b8 48 0e bb 07 00 cd 10 b8 45 0e cd 10 b8 4c 0e ?H.»..Í. ?E.Í.?L. 0010 cd 10 b8 4c 0e cd 10 b8 4f 0e cd 10 eb e2 Í.?L.Í.? O.Í.ëâ gil:~ #
Our kernel starts with a move-command, b8. Later, we find the command cd which means "Interrupt call", argument is 10. So, here the int $0x10 is called. With this example, you see how important the processor is for assembler, an alpha processor would perhaps interpret the commands (b8, cd, ...) totally different.
Writing directly to the screen As you see, we use int $0x10 as printf. But how does it do its work ? The answer is, it writes to the memory of the graphic card. Depending on if you have a color or monochrome, graphic or textual display, your graphic memory segment may be at $0xB800, $0xB000 or $0xA000. In text mode, it is mostly at $0xB800. So, let's change kernel.s:
mov $0xB000, %eax mov %eax, %ds mov %eax, %es mov $0x41, %al mov %al,%ds:0x2a6 mov %al,%ds:0x0001 mov %al,%ds:0x0002 mov %al,%ds:0x0003 ende: jmp ende
The result of this is an A in a funny color in the upper left of your text screen after boot.
- The processor stores data in its registers or in memory.
- mov $0x41, %al stores data in the register al
- mov %al,%ds:0x0003
- stores the data from al in the memory, in the segment determined by ds, at the offset 0x0003 a segment of memory is as a container for several offsets
- In text mode, the graphic card stores two values for each letter: First the letter and as second byte the color (determines background, foreground and attributes).
Use qemu virtualization
Rebooting your computer for every change in the OS is tedious. So, we use QEMU virtualization. Make an image from your floppy:
dd if=/dev/fd0 of=qemu.img
If you do not have /dev/fd0, then
qemu-img create qemu.img 1440k
dd if=/dev/zero of=qemu.img bs=512 count=2880
press a (make active), w (write) Boot a virtual machine from this image:
qemu -fda qemu.img
Now you can change your OS and test your changes without having to reboot your physical compi. You dd command now looks like this:
dd conv=notrunc if=kernel of=qemu.img
You can boot from a floppy, a CD or a harddisk. If you do, your computer reads the first sector (512 bytes) of the respective data carrier and executes it. This is the so-called boot-sector. Its job is to contain code that loads the rest of the operating system. Your computer only boots from a CD if its first sector contains the "fingerprint" of a boot-sector, the "magic number". The last two bytes must be 55AA. We are writing a boot loader now that will lodge in your data carrier's first sector. First, let's make an image of it, and fill the first sector completely with 55AA and the second sector with 42 (what else ?):
kolossus:~ # (for i in $(seq 1 1 256); do echo -en "\x55\xAA"; done; for i in $(seq 1 1 512); do echo -en "\x42"; done) | dd conv=notrunc of=qemu.img 0+729 records in 2+0 records out 1024 bytes (1.0 kB) copied, 0.0289144 s, 35.4 kB/s kolossus:~ #
We now have an 1.44 MB image of our boot disk, the first 512 bytes 55AA and the second 512 bytes 42:
kolossus:~ # ll qemu.img -rw-r--r-- 1 root root 1474752 Nov 11 21:36 qemu.img kolossus:~ #
Now we write the boot loader load.s
LoadKern: ; print an H to show you started mov ah, 0xe mov al, 0x48 mov bx, 7 int 0x10 mov bx, 0x2000 mov es, bx mov ds, bx mov bx, 0 ; Offset mov ah, 02 ; function# mov al, 01 ; sector count mov ch, 00 ; Track# mov cl, 02 ; Sector# mov dh, 00 ; Head# mov dl, 00 ; Drive# int 0x13 ; print the second sign from the sector mov ah, 0xe mov al,  mov bx, 7 int 0x10 .ende jmp .ende
We compile the loader using the net assembler to the file load:
And dump load to the beginning of our disk image:
dd conv=notrunc if=load of=qemu.img qemu -fda qemu.img
Now you can show the first data on the second sector. Use khexedit to verify your result.