|
[ / main / writing / grubKernel ] |
Creating a Multi-Boot Compatible Kernel |
©2002 neuraldk
|
|||||||
The Multi-Boot Standard |
|
|||||||
The multi-boot standard came about from a lack of general-purpose, boot loaders, and a lack of standardized kernels. It states a collection of rules that a kernel must comply with in order to be multi-boot compliant. The advantage, of course, is that your kernel can be booted, out of the box, by any multi-boot compliant boot-loader, such as GRUB. |
|
|||||||
The rules are simple, and easy to incorporate into your kernel. Some developers, however, have experienced troubles getting their kernel booted properly by GRUB, and so I've decided to create this tutorial in the hopes that it'll help some independant operating system developers out. |
|
|||||||
I intend to use Linux withen this tutorial as a development system simply because it has an excellent programming interface, and many different virtual machines to aid in development (vmware, bochs, plex86, etc). |
|
|||||||
Getting and Installing GRUB |
|
|||||||
The latest version of GRUB can be downloaded from:
|
|
|||||||
When I last downloaded, there was also a pre-installed GRUB floppy image (grub-0.92-i386-pc.ext2fs), which can speed up the installation process quite nicely. |
|
|||||||
After downloading, you can compile like any standard unix program (./configure, make), and now you're ready to setup a floppy with GRUB. If you managed to get a copy of the pre-installed image, you can skip the next step. |
|
|||||||
Creating an unformatted GRUB Image |
|
|||||||
After compiling grub, you will be left with two files of interest: stage1/stage1 and stage2/stage2. To create an unformatted GRUB image, you're going to want to concatenate these two files together: |
|
|||||||
|
|
|||||||
If you're testing your operating system with a virtual environment, you should be able to use this image as a floppy drive. If, however, you are using a non-virtual environment you'll want to write this image to a floppy disk:
|
|
|||||||
Creating a bootable GRUB floppy |
|
|||||||
Now you're going to want to create a disk that will be your operating system's boot disk. This disk must be formatted with a file system that GRUB supports (such as ext2fs, reiserfs, fat, etc). If you're using a virtual machine, you can create a floppy image with dd, and then format it like any device. We're also going to need to mount this floppy for our next step. |
|
|||||||
dd if=/dev/zero of=floppy.img bs=512 count=2880 mke2fs floppy.img mount floppy.img /mnt/floppy -o loop real environment: mke2fs /dev/fd0 mount /dev/fd0 /mnt/floppy |
|
|||||||
This formatted disk must also have the directory /boot/grub, which must contain stage1 and stage2. The location, on the floppy, of stage2 must also never change, or else GRUB will not be able to boot, so it's a good idea to make the file immutable:
cp stage1/stage1 stage2/stage2 /mnt/floppy/boot/grub chmod a-w /boot/grub/stage2 |
|
|||||||
Setting up GRUB |
|
|||||||
Now that we've been through all the preliminary steps, it's time to get GRUB up and running. First, boot your computer or virtual machine with your unformatted GRUB disk, or image. |
|
|||||||
At this point you'll be presented with the GRUB shell, and you'll want to remove the current floppy disk, or image, and insert the formatted GRUB floppy disk or image, and issue the command setup (fd0). This will setup your formatted floppy to boot GRUB properly, which will the basic building block we'll use when developing our operating system. |
|
|||||||
If you now boot with this floppy, you'll get the same result as with the previous unformatted floppy disk, or image, and so you may be asking, "What's the point? Why make two of the same disk?" |
|
|||||||
Well, the two aren't the same, actually. One has a formatted file system, while the other doesn't. We're going to need some form of a file system in order to copy our kernel to the GRUB floppy, which the first floppy doesn't afford us. It's purpose was simply to setup the second floppy, which is much more useful to us. |
|
|||||||
Creating a Mini-Kernel |
|
|||||||
In order for our kernel to be loadable by GRUB, we must provide it a multi-boot header, withen the first 8k of our kernel. |
|
|||||||
|
|
|||||||
Because of the 8k limitation, it's usually a good idea to define this header in assembly language so that you have complete control over where it appears. Certain C compilers may relocate your data to well beyond the 8kb mark, at which point GRUB will no longer recognize your kernel as a multi-boot kernel and will not boot it. |
|
|||||||
It should be noted that, if you're compiling your kernel into the ELF format, you will only need to define the first three elements. The ELF format itself will define the next five withen it's header information. The last four are only useful if you want GRUB to change the video mode for you. |
|
|||||||
The Multi-Boot Header Fields |
|
|||||||
|
|
|||||||
A Mini-Kernel |
|
|||||||
With this information, we can write a simple kernel that is loadable by GRUB: |
|
|||||||
; ; (c) 2002, NeuralDK section .text bits 32 ; multi boot header defines MBOOT_PAGE_ALIGN equ 1 << 0 MBOOT_MEM_INFO equ 1 << 1 MBOOT_AOUT_KLUDGE equ 1 << 16 MBOOT_MAGIC equ 0x1BADB002 MBOOT_FLAGS equ MBOOT_PAGE_ALIGN | MBOOT_MEM_INFO | MBOOT_AOUT_KLUDGE CHECKSUM equ -(MBOOT_MAGIC + MBOOT_FLAGS) STACK_SIZE equ 0x1000 ; defined in the linker script extern textEnd extern dataEnd extern bssEnd global start global _start entry: jmp start ; The Multiboot header align 4, db 0 mBootHeader: dd MBOOT_MAGIC dd MBOOT_FLAGS dd CHECKSUM ; fields used if MBOOT_AOUT_KLUDGE is set in ; MBOOT_HEADER_FLAGS dd mBootHeader ; these are PHYSICAL addresses dd entry ; start of kernel .text (code) section dd dataEnd ; end of kernel .data section dd bssEnd ; end of kernel BSS dd entry ; kernel entry point (initial EIP) start: _start: mov edi, 0xB8000 mov esi, string mov ah, 0x0F .charLoop: lodsb stosw or al, al jnz .charLoop jmp short $ section .data string db "ndk is alive!", 0 section .bss align 4, db 0 common stack 0x1000 resb 0x4000 |
|
|||||||
Hopefully most of this will be self explanatory given the above descriptions. This assembly source simply includes the multi-boot header, and a string displaying routine. In fact, this code is based, almost-exclusively, on the example located in the multi-boot specifications, with a few changes; I've rewritten the code for Nasm, and encorporated sections. |
|
|||||||
Why include sections? Well, very few kernels are 100% assembly. Most include C or C++ source, which is going to want, at least, the .text, .data and .bss sections. Also, with these sections defined, we're able to include entries 4 to 8 of the multi-boot header. They aren't required for ELF kernels, but if you intend to produce a kernel in any other format, you'll need these, and so I always provide them. |
|
|||||||
If anybody's gotten ahead of me now, they'll notice that this will not assemble correctly, and will complain about missing symbols. This is because I've written a linker script for this source code. Why? Simply because, with a linker script, I can have complete control over where sections lie, which is important, because the multi-boot header must be withen the first 8kb of our binary. Also, with a linker script I can define symbols for the starting and ending of each section (which is required for fields 4 to 8 of the multi-boot header). |
|
|||||||
.text 0x00100000 :{ *(.text) } textEnd = .; .data :{ *(.data) *(.rodata) } dataEnd = .; .bss :{ *(.common) *(.bss) } bssEnd = .; } |
|
|||||||
Looking above you'll probably spot a few oddities in the above script. Firstly, I've decided to put the read-only data in the data section which, by default, is read/write. Most programmers would include the read only section in the text section, which is read-only. The only reason I don't is because, once your kernel has been booted by GRUB you're left with two defined GDT entries; one code, and one data. Both start at 0x0, and extend to 0xFFFFFFFF. See what I'm getting at here? Even if you put the read-only data in a read-only section, it can still be read by the read/write data descriptor! They both span the same memory area. It makes no difference. |
|
|||||||
Secondly, you'll notice I've set the kernel to be loaded at the 1MB address. This is important. A multi-boot compatible kernel cannot be loaded before the 1MB mark, it must be loaded at or above 1MB. |
|
|||||||
Lastly, note that I've defined the ending of each section in this linker script. These are imported in the above assembly source, and used to define the multi-boot header. |
|
|||||||
After saving the above script as ./ldscript, you should be able to assemble and link this multi-boot kernel: |
|
|||||||
gcc -Xlinker -T -Xlinker ldscript -ffreestanding -fno-builtin -nostdlib -nostartfiles -s start.o -o miniKernel |
|
|||||||
All the switches to GCC are simply to tell it to link just our object. We don't want crt0.o or any other standard linker dependancies linked into our operating system. Not only is it wasted, operating system dependant code, but it will also result in duplicated symbols. You'll notice we defined the start and _start symbols in our assembly source. There was a reason for that. GCC has it's own start function which is called before the main function and sets up the Linux environment for the C or C++ program. We, obviously, don't want this. We aren't rewritting Linux, we're writting a completely new operating system, and so we should redefine this function. |
|
|||||||
Booting this Kernel with GRUB |
|
|||||||
Okay, with the above code out of the way, lets try booting it. All we have to do is copy our newly created kernel to our formatted GRUB floppy disk, or image. |
|
|||||||
mount floppy.img /mnt/floppy -o loop cp miniKernel /mnt/floppy real machine: mount /dev/fd0 /mnt/floppy cp miniKernel /mnt/floppy |
|
|||||||
And now reboot your environment with this floppy inserted, and you'll get the same GRUB shell again. Now try issuing the following commands to GRUB: |
|
|||||||
kernel /miniKernel boot |
|
|||||||
You're kernel should boot, display a message at the top left hand corner, and then loop endlessly. |
|
|||||||
A lot of work for all that, right? Perhaps, but the way we've coded our operating system, it's incredibly easy to extend. Before we do anything, though, we should define an IDT and GDT. Yes, GRUB does create a GDT for us, but it's size and location are undefined, and it's generally a good idea to create your own. You must create an IDT, seeing as though GRUB doesn't provide us with one. |
|
|||||||
Interestingly enough, with an IDT and GDT in place, our kernel is advanced enough to run the multi-boot example kernel, and now provides the basic elements of any kernel. What took me many hundreds of lines when developing polyOS can now be completed in less than 300, and our resulting kernel is only about 6kb. |
|
|||||||
Let's take a look at the revised start.asm code: |
|
|||||||
; ; (c) 2002, NeuralDK section .text bits 32 ; multi boot header defines MBOOT_PAGE_ALIGN equ 1 << 0 MBOOT_MEM_INFO equ 1 << 1 MBOOT_AOUT_KLUDGE equ 1 << 16 MBOOT_MAGIC equ 0x1BADB002 MBOOT_FLAGS equ MBOOT_PAGE_ALIGN | MBOOT_MEM_INFO | MBOOT_AOUT_KLUDGE CHECKSUM equ -(MBOOT_MAGIC + MBOOT_FLAGS) STACK_SIZE equ 0x1000 ; defined in the linker script extern textEnd extern dataEnd extern bssEnd extern cmain global start global _start entry: jmp start ; The Multiboot header align 4, db 0 mBootHeader: dd MBOOT_MAGIC dd MBOOT_FLAGS dd CHECKSUM ; fields used if MBOOT_AOUT_KLUDGE is set in ; MBOOT_HEADER_FLAGS dd mBootHeader ; these are PHYSICAL addresses dd entry ; start of kernel .text (code) section dd dataEnd ; end of kernel .data section dd bssEnd ; end of kernel BSS dd entry ; kernel entry point (initial EIP) start: _start: ; clear the idt + gdt @ 0x0 xor edi, edi mov ecx, 0x800 + 0x800 rep stosb ; setup a bare bones GDT mov esi, my_gdt mov edi, 0x0800 mov ecx, 8 * 4 rep movsb lgdt [pGDT] ; load the GDT lidt [pIDT] ; load the IDT mov dx, 0x08 ; 0x08 - kernel data segment mov ds, dx mov es, dx mov fs, dx mov gs, dx mov dx, 0x18 ; 0x18 - kernel stack segment mov ss, dx mov esp, (stack + STACK_SIZE) ; push the multiboot info structure, and magic push ebx push eax ; load cs with new selector jmp 0x10:new_gdt new_gdt: ; time for some C! call cmain jmp short $ section .data string db "ndk is alive!", 0 pIDT dw 800h ; limit of 256 IDT slots dd 00000000h ; starting at 0 pGDT dw 800h ; 256 GDT slots dd 00000800h ; starting at 800h (after IDT) my_gdt: ; Null descriptor ; base : 0x00000000 ; limit: 0x00000000 ( 0.0 MB) dd 0 dd 0 ; 0x08 descriptor - Kernel data segment ; base : 0x00000000 ; limit: 0xfffff pages (4 GB) ; DPL : 0 ; 32 bit, present, system, expand-up, writeable dd 0x0000ffff dd 0x00cf9200 ; 0x10 descriptor - Kernel code segment ; base : 0x00000000 ; limit: 0xfffff (4 GB) ; DPL : 0 ; 32 bit, present, system, non-conforming, readable dd 0x0000ffff dd 0x00cf9A00 ; 0x18 descriptor - Kernel stack segment ; base : 0x00000000 //0x00080000 ; limit: 0xfffff (4 GB) ; DPL : 0 ; 32 bit, present, system, expand-up, writeable dd 0x0000ffff dd 0x00cb9200 section .bss align 4, db 0 common stack 0x1000 resb 0x4000 |
|
|||||||
And the kernel.c file, provided in the multi-boot specifications: |
|
|||||||
/* Copyright (C) 1999 Free Software Foundation, Inc. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ #include "multiboot.h" /* Macros. */ /* Check if the bit BIT in FLAGS is set. */ #define CHECK_FLAG(flags,bit) ((flags) & (1 << (bit))) /* Some screen stuff. */ /* The number of columns. */ #define COLUMNS 80 /* The number of lines. */ #define LINES 24 /* The attribute of an character. */ #define ATTRIBUTE 7 /* The video memory address. */ #define VIDEO 0xB8000 /* Variables. */ /* Save the X position. */ static int xpos; /* Save the Y position. */ static int ypos; /* Point to the video memory. */ static volatile unsigned char *video; /* Forward declarations. */ void cmain (unsigned long magic, unsigned long addr); static void cls (void); static void itoa (char *buf, int base, int d); static void putchar (int c); void printf (const char *format, ...); /* Check if MAGIC is valid and print the Multiboot information structure pointed by ADDR. */ void cmain (unsigned long magic, unsigned long addr) { multiboot_info_t *mbi; /* Clear the screen. */ cls (); /* Am I booted by a Multiboot-compliant boot loader? */ if (magic != MULTIBOOT_BOOTLOADER_MAGIC) { printf ("Invalid magic number: 0x%x\n", (unsigned) magic); return; } /* Set MBI to the address of the Multiboot information structure. */ mbi = (multiboot_info_t *) addr; /* Print out the flags. */ printf ("flags = 0x%x\n", (unsigned) mbi->flags); /* Are mem_* valid? */ if (CHECK_FLAG (mbi->flags, 0)) printf ("mem_lower = %uKB, mem_upper = %uKB\n", (unsigned) mbi->mem_lower, (unsigned) mbi->mem_upper); /* Is boot_device valid? */ if (CHECK_FLAG (mbi->flags, 1)) printf ("boot_device = 0x%x\n", (unsigned) mbi->boot_device); /* Is the command line passed? */ if (CHECK_FLAG (mbi->flags, 2)) printf ("cmdline = %s\n", (char *) mbi->cmdline); /* Are mods_* valid? */ if (CHECK_FLAG (mbi->flags, 3)) { module_t *mod; int i; printf ("mods_count = %d, mods_addr = 0x%x\n", (int) mbi->mods_count, (int) mbi->mods_addr); for (i = 0, mod = (module_t *) mbi->mods_addr; i < mbi->mods_count; i++, mod += sizeof (module_t)) printf (" mod_start = 0x%x, mod_end = 0x%x, string = %s\n", (unsigned) mod->mod_start, (unsigned) mod->mod_end, (char *) mod->string); } /* Bits 4 and 5 are mutually exclusive! */ if (CHECK_FLAG (mbi->flags, 4) && CHECK_FLAG (mbi->flags, 5)) { printf ("Both bits 4 and 5 are set.\n"); return; } /* Is the symbol table of a.out valid? */ if (CHECK_FLAG (mbi->flags, 4)) { aout_symbol_table_t *aout_sym = &(mbi->u.aout_sym); printf ("aout_symbol_table: tabsize = 0x%0x, " "strsize = 0x%x, addr = 0x%x\n", (unsigned) aout_sym->tabsize, (unsigned) aout_sym->strsize, (unsigned) aout_sym->addr); } /* Is the section header table of ELF valid? */ if (CHECK_FLAG (mbi->flags, 5)) { elf_section_header_table_t *elf_sec = &(mbi->u.elf_sec); printf ("elf_sec: num = %u, size = 0x%x," " addr = 0x%x, shndx = 0x%x\n", (unsigned) elf_sec->num, (unsigned) elf_sec->size, (unsigned) elf_sec->addr, (unsigned) elf_sec->shndx); } /* Are mmap_* valid? */ if (CHECK_FLAG (mbi->flags, 6)) { memory_map_t *mmap; printf ("mmap_addr = 0x%x, mmap_length = 0x%x\n", (unsigned) mbi->mmap_addr, (unsigned) mbi->mmap_length); for (mmap = (memory_map_t *) mbi->mmap_addr; (unsigned long) mmap < mbi->mmap_addr + mbi->mmap_length; mmap = (memory_map_t *) ((unsigned long) mmap + mmap->size + sizeof (mmap->size))) printf (" size = 0x%x, base_addr = 0x%x%x," " length = 0x%x%x, type = 0x%x\n", (unsigned) mmap->size, (unsigned) mmap->base_addr_high, (unsigned) mmap->base_addr_low, (unsigned) mmap->length_high, (unsigned) mmap->length_low, (unsigned) mmap->type); } } /* Clear the screen and initialize VIDEO, XPOS and YPOS. */ static void cls (void) { int i; video = (unsigned char *) VIDEO; for (i = 0; i < COLUMNS * LINES * 2; i++) *(video + i) = 0; xpos = 0; ypos = 0; } /* Convert the integer D to a string and save the string in BUF. If BASE is equal to 'd', interpret that D is decimal, and if BASE is equal to 'x', interpret that D is hexadecimal. */ static void itoa (char *buf, int base, int d) { char *p = buf; char *p1, *p2; unsigned long ud = d; int divisor = 10; /* If %d is specified and D is minus, put `-' in the head. */ if (base == 'd' && d < 0) { *p++ = '-'; buf++; ud = -d; } else if (base == 'x') divisor = 16; /* Divide UD by DIVISOR until UD == 0. */ do { int remainder = ud % divisor; *p++ = (remainder < 10) ? remainder + '0' : remainder + 'a' - 10; } while (ud /= divisor); /* Terminate BUF. */ *p = 0; /* Reverse BUF. */ p1 = buf; p2 = p - 1; while (p1 < p2) { char tmp = *p1; *p1 = *p2; *p2 = tmp; p1++; p2--; } } /* Put the character C on the screen. */ static void putchar (int c) { if (c == '\n' || c == '\r') { newline: xpos = 0; ypos++; if (ypos >= LINES) ypos = 0; return; } *(video + (xpos + ypos * COLUMNS) * 2) = c & 0xFF; *(video + (xpos + ypos * COLUMNS) * 2 + 1) = ATTRIBUTE; xpos++; if (xpos >= COLUMNS) goto newline; } /* Format a string and print it on the screen, just like the libc function printf. */ void printf (const char *format, ...) { char **arg = (char **) &format; int c; char buf[20]; arg++; while ((c = *format++) != 0) { if (c != '%') putchar (c); else { char *p; c = *format++; switch (c) { case 'd': case 'u': case 'x': itoa (buf, c, *((int *) arg++)); p = buf; goto string; break; case 's': p = *arg++; if (! p) p = "(null)"; string: while (*p) putchar (*p++); break; default: putchar (*((int *) arg++)); break; } } } } |
|
|||||||
Along with a slightly modified multiboot.h header file provided by the multi-boot documentation (the GAS assembly language references have been removed): |
|
|||||||
/* Copyright (C) 1999, 2001 Free Software Foundation, Inc. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ /* The magic number passed by a Multiboot-compliant boot loader. */ #define MULTIBOOT_BOOTLOADER_MAGIC 0x2BADB002 /* Types. */ /* The Multiboot header. */ typedef struct multiboot_header { unsigned long magic; unsigned long flags; unsigned long checksum; unsigned long header_addr; unsigned long load_addr; unsigned long load_end_addr; unsigned long bss_end_addr; unsigned long entry_addr; } multiboot_header_t; /* The symbol table for a.out. */ typedef struct aout_symbol_table { unsigned long tabsize; unsigned long strsize; unsigned long addr; unsigned long reserved; } aout_symbol_table_t; /* The section header table for ELF. */ typedef struct elf_section_header_table { unsigned long num; unsigned long size; unsigned long addr; unsigned long shndx; } elf_section_header_table_t; /* The Multiboot information. */ typedef struct multiboot_info { unsigned long flags; unsigned long mem_lower; unsigned long mem_upper; unsigned long boot_device; unsigned long cmdline; unsigned long mods_count; unsigned long mods_addr; union { aout_symbol_table_t aout_sym; elf_section_header_table_t elf_sec; } u; unsigned long mmap_length; unsigned long mmap_addr; } multiboot_info_t; /* The module structure. */ typedef struct module { unsigned long mod_start; unsigned long mod_end; unsigned long string; unsigned long reserved; } module_t; /* The memory map. Be careful that the offset 0 is base_addr_low but no size. */ typedef struct memory_map { unsigned long size; unsigned long base_addr_low; unsigned long base_addr_high; unsigned long length_low; unsigned long length_high; unsigned long type; } memory_map_t; |
|
|||||||
That's it! You now have a fully functioning kernel. After compiling and linking it, you can overwrite our previous miniKernel on your boot disk and reboot to see it in action. |
|
|||||||
gcc -c kernel.c gcc -Xlinker -T -Xlinker ldscript -ffreestanding -fno-builtin -nostdlib -nostartfiles -s start.o kernel.o -o miniKernel |
|
|||||||