Write OS kernel from scratch – load and enter the kernel

Time:2021-10-14

Series catalog

Kernel disk mirroring

NextOn virtual memory, this article will officially load and start the kernel, that is, the green part in the figure:

Write OS kernel from scratch - load and enter the kernel

Of course, the kernel image needs to be read and loaded from the disk, so here’s an old picture, yesdiskandmemoryData correspondence of (physical memory):

Write OS kernel from scratch - load and enter the kernel

By the way, the question mark in the slash shadow in the figure above is what we talked about in the previous chapterkernel page tables, that is, the orange part of the first figure, 256 in total, covering an area of 1MB.

Write kernel

go back tokernel, that is, the green part in the figure, does not actually exist yet, so first we need to implement and compile a simple demo kernel. If you have no idea what the kernel is, you may ask: what does the kernel look like?

The answer is very simple: the kernel is almost no different from the executable program you usually write in C. It also starts with a main function.

Let’s implement our first kernel:

void main() {
  while (1) {}
}

It’s that simple, except for onewhileLoop, nothing else, but it’s enough for our demo here.

Compiling kernel

There are many compilation parameters, such as 32-bit encoding, disabling the C standard library, etc. (this is our own customized OS, which is not compatible with the C Standard Library).

gcc -m32 -nostdlib -nostdinc -fno-builtin -fno-stack-protector -no-pie -fno-pic -c main.c -o main.o

Link kernel:

ld -m elf_i386 -Tlink.ld -o kernel main.o

A link configuration file will be used herelink.ld

ENTRY(main)
SECTIONS
{
  .text 0xC0800000:
  {
    code = .; _code = .; __code = .;
    *(.text)
  }

  .data ALIGN(4096):
  {
     data = .; _data = .; __data = .;
     *(.data)
     *(.rodata)
  }

  .bss ALIGN(4096):
  {
    bss = .; _bss = .; __bss = .;
    *(.bss)
    . = ALIGN(4096);
  }

  end = .; _end = .; __end = .;
}

The most important thing here is the definitiontextStart address of segment0xC0800000, which is also the beginning of the whole kernel addressing. If you rememberLastWe plan the virtual memory distribution of the kernel space:

Write OS kernel from scratch - load and enter the kernel

0xC0800000Will be the entry address of the kernel becausetextSegments will be loaded here, followed bydatabssWait.loaderWhen finished, it will jump to the address.

In addition, the above also defines the entry function of the entire executable file asmain

The compiled linked kernel is a binary in ELF format. We might as well disassemble it and dump it:

objdump -dsx kernel

Write OS kernel from scratch - load and enter the kernel

Can seemainThe address of the function is0xC080000, this is the first instruction after entering the kernel.

Create kernel image

dd if=kernel of=scroll.img bs=512 count=2048 seek=9 conv=notrunc

seek=9Because of the frontmbrandloaderIt has occupied the first 9 sectors on the disk. The kernel size here is 2048 sectors, a total of 1MB, which is large enough for our project.

Now disk mirroring has finally become like this:

Write OS kernel from scratch - load and enter the kernel

Read and load kernel

After the image is ready, the kernel can be read and loaded. First, give the code linkinit_kernel, for your reference.

And beforembrandloaderofloadDifferent, here will bereadandloadThe two words are separated because they are two steps:

  • Read: the kernel disk is mirroredOriginal binaryCopy to a free place in memory, where the binary is elf format;
  • Load: parse the elf executable binary obtained in the previous step, andsectionCopy to where they areAddressingA place of;

First, let’s look at the first step “read”. We chose 1MB at the top of the virtual memory, i.e(0xFFFFFFFF - 1MB) ~0xFFFFFFFF1MB of space is used as the storage address of binary images. Of course, this is entirely a personal choice. I chose here because no one will disturb here at present; Of course, the corresponding physical pages should also be allocated to itframes, inpage tableSo I also found 1MB free space from the remaining physical memory space to map it; Then the kernel image can be read in just like the MBR and loader before.

The next step is “load”. This involves parsing according to the specification of ELF file format. You need to take some time to understand the relevant documents, mainly fromprogram header tableGet eachsectionAnd the loaded memory address (virtual address of course), and then copy the data. The memory address loaded this time is0xC0800000Start position. Of course, before copying, you should pre allocate frames for them andpage tableEstablish memory mapping in. All this work is going onallocate_pages_for_kernelThis function is completed ahead of time.

Enter the kernel

When everything is ready, you can really enter the kernel:

init_kernel:
  call allocate_pages_for_kernel
  call load_hd_kernel_image
  call do_load_kernel
  
  ; init floating point unit before entering the kernel
  finit

  ; move stack to 0xF0000000
  mov esp, KERNEL_STACK_TOP - 16
  mov ebp, esp

  ; let's jump to kernel entry :)
  jmp eax
  ret

First, initialize the floating-point unit of the CPU to prevent exceptions behind it.

Then I willstackMoved to a higher address0xF0000000Location, of course, is not necessary. It’s entirely my personal choice. The current stack position is also very good (about0xC0007B00Near the following locations, where0x7B00This is inmbrIn the past, and after opening paging, we use0xC0000000 + 0x7B00Visit, if you remember). I just hope that the stack position after entering the kernel can be moved to a new place, so I took so many steps. The position of stack is relatively flexible, as long as it is an idle place that will not be disturbed.


Then it’s very simple,jmp eaxAn instruction jumped tokernelentrance.

Whyeax? This is the function abovedo_load_kernelThis function is the function that we parse the elf binary of the loaded kernel, which will return the entry address of the value kernel, i.emainFunction address. This address is from ELF fileELF Headerofe_entryField. The entry address of ELF executable binary is determined in the link phase, which is actually determined by the previouslink.ldInsideENTRY(main)designated.

If successful, the operation results are as follows:

Write OS kernel from scratch - load and enter the kernel

The program has successfully entered the kernel and run to0xC0800003Location is the location of the while loop, which will be the real beginning of the kernel journey:)