My Tutorials/How to program your own OS

From ThorstensHome
Jump to: navigation, search


First start

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:

; 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
    jmp .ende

4. Have nasm installed
5. Compile kernel.s to machine language:

nasm kernel.s

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

  1. The computer starts
  2. The computer's bios stores BIOS-Interrupts into the memory
  3. The computer loads the first sector of your floppy (bootsector) and executes it
  4. In your bootsector is your kernel. It loads some numbers into the processor's registers and calls the BIOS-Interrupt $0x10.
  5. int $0x10 decides upon the register's content that it has to print an "H"
  6. The program goes on with E, L and O
  7. The program terminates by entering an endless loop
  8. 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
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).

Second step

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
fdisk qemu.img

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

    ; 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, [0001]
    mov bx, 7
    int 0x10

    jmp .ende

We compile the loader using the net assembler to the file load:

nasm load.s  

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.

See also