Multiboot1 Bare Bones: Difference between revisions

From OSDev.wiki
Jump to navigation Jump to search
[unchecked revision][unchecked revision]
Content added Content deleted
(→‎All in one makefile (Bochs): cleaned up the Makefile. Needs -Wall -Wextra etc adding but having not built the bare bones tutorial myself don't want to add these as they may break something)
(→‎All in one makefile (Bochs): Some touches to Makefile. -Wall -Wextra deemed more important than -Werror.)
Line 262: Line 262:
===All in one makefile (Bochs)===
===All in one makefile (Bochs)===
<source lang="make">
<source lang="make">
CC =i586-elf-gcc
CC = i586-elf-gcc
CFLAGS =-Werror -nostdlib -fno-builtin -nostartfiles -nodefaultlibs
CFLAGS = -Wall -Wextra -nostdlib -fno-builtin -nostartfiles -nodefaultlibs
LD =i586-elf-ld
LD = i586-elf-ld

OBJFILES = loader.o kernel.o


all: kernel.img
all: kernel.img


.s.o:
loader.o: loader.s
nasm -f elf -o loader.o loader.s
nasm -f elf -o $@ $<


.c.o:
kernel.o: kernel.c
$(CC) $(CFLAGS) -o kernel.o -c kernel.c
$(CC) $(CFLAGS) -o $@ -c $<


kernel.bin: kernel.o loader.o
kernel.bin: $(OBJFILES)
$(LD) -T linker.ld -o kernel.bin loader.o kernel.o
$(LD) -T linker.ld -o $@ $^


kernel.img: kernel.bin
kernel.img: kernel.bin
dd if=/dev/zero of=pad bs=1 count=750
dd if=/dev/zero of=pad bs=1 count=750
cat stage1 stage2 pad kernel.bin > kernel.img
cat stage1 stage2 pad $< > $@


clean:
clean:
rm -f loader.o kernel.o kernel.bin kernel.img
$(RM) $(OBJFILES) kernel.bin kernel.img


install:
install:
rm -f loader.o kernel.o kernel.bin
$(RM) $(OBJFILES) kernel.bin
</source>
</source>



Revision as of 07:17, 11 June 2012

Difficulty level

Beginner
Kernel Designs
Models
Other Concepts

In this tutorial we will compile a simple C kernel and boot it.

WAIT! Have you read Getting Started, Beginner Mistakes, and some of the related OS theory?

Preface

This tutorial assumes you have a compiler / assembler / linker toolchain capable of handling ELF files. On a Windows machine, you are strongly encouraged to set up a GCC Cross-Compiler, as it removes all the various toolchain-specific issues you might have ("PE operation on a non-PE file", "unsupported file format", and a number of others). While technically a Linux machine already has an ELF-capable toolchain, you are still encouraged to build a cross-compiler, as it is the first step to do, and it keeps you from relying on things you shouldn't (header files, for example).

To make starting an OS easy, we will be using a lot of existing parts, GRUB will be the bootloader, and the kernel will be in ELF format. GRUB (a Multiboot compliant boot loader) puts the system in to the correct state for your kernel to start executing. This includes enabling the A20 line (to give you access to all available memory addresses) and putting the system in to 32-bit Protected Mode, giving you access to a theoretical 4GiB of memory. We will not use a flat binary but a kernel in ELF format, so that we have a lot of control to tell GRUB where to load which part in memory.

Overview

Even when using GRUB, some setup is required before entering a main() type function. The most basic setup to get an ELF format kernel to be booted by GRUB consists of three files:

  • loader.s - assembler "glue" between bootloader and kernel
  • kernel.c - your actual kernel routines
  • linker.ld - for linking the above files

The second part of this tutorial briefly describes how to boot the compiled kernel.

loader.s

loader.s takes over control from the Multiboot bootloader, and jumps into the kernel proper.

NASM

global loader                           ; making entry point visible to linker
global magic                            ; we will use this in kmain
global mbd                              ; we will use this in kmain

extern kmain                            ; kmain is defined in kmain.cpp

; setting up the Multiboot header - see GRUB docs for details
MODULEALIGN equ  1<<0                   ; align loaded modules on page boundaries
MEMINFO     equ  1<<1                   ; provide memory map
FLAGS       equ  MODULEALIGN | MEMINFO  ; this is the Multiboot 'flag' field
MAGIC       equ  0x1BADB002             ; 'magic number' lets bootloader find the header
CHECKSUM    equ -(MAGIC + FLAGS)        ; checksum required

section .text

align 4
    dd MAGIC
    dd FLAGS
    dd CHECKSUM

; reserve initial kernel stack space
STACKSIZE equ 0x4000                    ; that's 16k.

loader:
    mov  esp, stack + STACKSIZE         ; set up the stack
    mov  [magic], eax                   ; Multiboot magic number
    mov  [mbd], ebx                     ; Multiboot info structure

    call kmain                          ; call kernel proper

    cli
.hang:
    hlt                                 ; halt machine should kernel return
    jmp  .hang

section .bss

align 4
stack: resb STACKSIZE                   ; reserve 16k stack on a doubleword boundary
magic: resd 1
mbd:   resd 1

Assemble using:

nasm -f elf -o loader.o loader.s

GAS

.global loader                          # making entry point visible to linker

# setting up the Multiboot header - see GRUB docs for details
.set ALIGN,    1<<0                     # align loaded modules on page boundaries
.set MEMINFO,  1<<1                     # provide memory map
.set FLAGS,    ALIGN | MEMINFO          # this is the Multiboot 'flag' field
.set MAGIC,    0x1BADB002               # 'magic number' lets bootloader find the header
.set CHECKSUM, -(MAGIC + FLAGS)         # checksum required

.align 4
.long MAGIC
.long FLAGS
.long CHECKSUM

# reserve initial kernel stack space
.set STACKSIZE, 0x4000                  # that is, 16k.
.lcomm stack, STACKSIZE, 32             # reserve 16k stack on a doubleword boundary
.comm  mbd, 4                           # we will use this in kmain
.comm  magic, 4                         # we will use this in kmain

loader:
    movl  $(stack + STACKSIZE), %esp    # set up the stack
    movl  %eax, magic                   # Multiboot magic number
    movl  %ebx, mbd                     # Multiboot data structure

    call  kmain                         # call kernel proper

    cli
hang:
    hlt                                 # halt machine should kernel return
    jmp   hang

Assemble using:

as -o loader.o loader.s

kernel.c

This is not exactly your average int main() (which is one of the reasons why you should not call it like that). Most notably, you only have the freestanding library available, not the hosted version (i.e., <float.h>, <iso646.h>, <limits.h>, <stdalign.h>, <stdarg.h>, <stdbool.h>, <stddef.h>, <stdint.h>, and <stdnoreturn.h>). Welcome to kernel land.

#include <stdint.h>

void kmain(void)
{
   extern uint32_t magic;
   extern void *mbd;

   if ( magic != 0x2BADB002 )
   {
      /* Something went not according to specs. Print an error */
      /* message and halt, but do *not* rely on the multiboot */
      /* data structure. */
   }

   /* You could either use multiboot.h */
   /* (http://www.gnu.org/software/grub/manual/multiboot/multiboot.html#multiboot_002eh) */
   /* or do your offsets yourself. The following is merely an example. */ 
   //char * boot_loader_name =(char*) ((long*)mbd)[16];

   /* Print a letter to screen to see everything is working: */
   unsigned char *videoram = (char *)0xB8000;
   videoram[0] = 65; /* character 'A' */
   videoram[1] = 0x07; /* light grey (7) on black (0). */
}

Compile using:

i586-elf-gcc -o kernel.o -c kernel.c -Wall -Wextra -Werror \
    -nostdlib -fno-builtin -nostartfiles -nodefaultlibs

Note: the flags -Wall -Wextra -Werror are not exactly required, but using them will certainly help you later on. They might seem to be a pest, but remember: The compiler is your friend!

Note #2: You may be able to use gcc instead of the i586-elf-gcc from the cross-compiler, but this does not work out of the box on at least Windows and 64-bit linux.

linker.ld

ENTRY (loader)

SECTIONS
{
    . = 0x00100000;

    .text ALIGN (0x1000) :
    {
        *(.text)
    }

    .rodata ALIGN (0x1000) :
    {
        *(.rodata*)
    }

    .data ALIGN (0x1000) :
    {
        *(.data)
    }

    .bss :
    {
        sbss = .;
        *(COMMON)
        *(.bss)
        ebss = .;
    }
}

Link using:

i586-elf-ld -T linker.ld -o kernel.bin loader.o kernel.o

Note: again using ld instead of i585-elf-ld may occasionally work, but in most configurations you get an error or an unbootable image.

The file kernel.bin is now your kernel (all other files are no longer needed).

Booting the kernel

In a few minutes, you will see your kernel in action.

Preparing a virtual computer (Bochs/QEMU) and grub

Main articles: Bochs, GRUB

In short: bochs is a virtual computer (so you won't damage your real computer), and grub is a boot loader. Another option is to use QEMU, an alternative virtual computer.

For bochs, prepare a configuration file named bochsrc.txt in your working directory. It should contain

boot: floppy
floppya: 1_44="floppy.img", status=inserted

which means that we will boot from a virtual floppy (actually a file). We will create this floppy file below.

Windows users can download QEMU binaries from here; if the latest version has not yet been made available there, it can probably be found on the forum somewhere. Linux users can install it using their distro's package manager (e.g., apt, yum). For example, under Ubuntu:

sudo apt-get install qemu

From the GRUB project download the binaries. For a first success, only the files stage1 and stage2 are needed. They are in a package named grub-0.97-pc386.

Also create a file named pad with exactly 750 bytes (content does not matter; more precisely, the 750 is the result of 102400 - len(stage1)- len(stage2)). eg: 102400 - (512 + 101138) = 750

To create this pad file under UNIX, type:

dd if=/dev/zero of=pad bs=1 count=750

To create this pad file under Windows, type:

fsutil file createnew pad 750

Creating the floppy image

Create the floppy image as follows:

cat  stage1 stage2 pad kernel.bin > floppy.img # linux, netbsd, ...
rem Windows, DOS
copy /b stage1+stage2+pad+kernel.bin   floppy.img

As a result, the kernel begins exactly in block #200 (remember, a block equals 512 bytes). Without padding, the kernel would start in the middle of a block, making it impossible to boot.

Note: bochs may complain about the size of the floppy image. If this happens, it means bochs wants a floppy image file that is 1474560 bytes long (or any other accepted size, but 1.44MB is the canonical size for a floppy). Simply create another pad file of the appropriate size to use in your cat statement. For example:

cat stage1 stage2 pad kernel.bin pad2 > floppy.img

with the resulting floppy.img being 1474560 bytes.

Starting the virtual computer (Bochs/QEMU)

This step is equivalent to turning on a computer with a bootable floppy inserted.

Important: first check the size of your kernel.bin in blocks. This is the length in bytes, divided by 512 and rounded up to the next integer. You will need this number. Let us assume that the kernel size is 18 blocks.

ToDo: Quote from Solar: The whole Bochs section is a bit fishy, as it makes assumptions on the sizes of stage1, stage2, and kernel.bin, but doesn't elaborate on how to determine them, so I guess it's easy to fall into the trap of merely copying the values without checking them.

If you are using bochs, Type bochs. If you are using QEMU, Type qemu -fda floppy.img

It will display a "monitor" with some messages from GRUB, ending in a command promt. Type:

kernel 200+18
boot

The first command means that the kernel starts at block #200 (see above) and its size is 18 blocks. This just loads the kernel into the memory, but does not start it. The boot command really starts the kernel.

All in one makefile (Bochs)

CC	= i586-elf-gcc
CFLAGS	= -Wall -Wextra -nostdlib -fno-builtin -nostartfiles -nodefaultlibs
LD	= i586-elf-ld

OBJFILES = loader.o kernel.o

all: kernel.img

.s.o:
	nasm -f elf -o $@ $<

.c.o:
	$(CC) $(CFLAGS) -o $@ -c $<

kernel.bin: $(OBJFILES)
	$(LD) -T linker.ld -o $@ $^

kernel.img: kernel.bin
	dd if=/dev/zero of=pad bs=1 count=750
	cat stage1 stage2 pad $< > $@

clean:
	$(RM) $(OBJFILES) kernel.bin kernel.img

install:
	$(RM) $(OBJFILES) kernel.bin

Then you can just launch bochs with the kernel.img.

Using QEMU's Built-in Bootloader

You can boot the kernel.bin file directly with QEMU, without the need for another bootloader, like this:

qemu -kernel kernel.bin

All in one script (QEMU)

The following script is a fast and convenient way of testing out MultiBoot-compliant kernels within QEMU. You can run it by simply typing ./run.sh in the same directory as your kernel.bin.

#!/bin/bash

# This script can be used to quickly test MultiBoot-compliant
# kernels.

# ---- begin config params ----

harddisk_image_size=$((4*1024*1024)) # 4 megabytes
harddisk_image="harddisk.img"
qemu_cmdline="qemu -monitor stdio"
kernel_args=""
kernel_binary="kernel.bin"

# ----  end config params  ----


function fail() { echo "$1"; exit 1; }
function prereq() {
	local c x
	if [ "$1" = "f" ]; then c=stat;x=file; else c=which;x=program; fi
	if [ -z "$3" ]; then
		$c "$2" >/dev/null || fail "$x $2 not found"
	else
		$c "$2" >/dev/null || fail "$x $2 (from package $3) not found"
	fi
}

# check prerequisites
prereq x mkfs.vfat dosfstools
prereq x mcopy mtools
prereq x syslinux
prereq f /usr/lib/syslinux/mboot.c32 syslinux


# create image
dd if=/dev/zero of="$harddisk_image" bs=4k count=$((harddisk_image_size/4096)) 2>/dev/null

# format image
mkfs.vfat "$harddisk_image" || fail "could not format harddisk.img"

# install syslinux
syslinux "$harddisk_image" || fail "could not install syslinux"

# copy over mboot.c32 (required for Multiboot)
mcopy -i "$harddisk_image" /usr/lib/syslinux/mboot.c32 ::mboot.c32 || fail "could not copy over mboot.c32"

# copy over kernel
mcopy -i "$harddisk_image" "$kernel_binary" ::kernel.bin || fail "could not copy over kernel"

# create syslinux.cfg
echo '
TIMEOUT 1
DEFAULT mboot.c32 kernel.bin '$kernel_args'
' | mcopy -i "$harddisk_image" - ::syslinux.cfg

## if you have any other files/directories to add, add them here

# run QEMU
$qemu_cmdline -hda "$harddisk_image"

echo

(Frequently asked) Questions

Why the multiboot header? Wouldn't a pure ELF file be loadable by GRUB anyway?
GRUB is capable of loading a variety of formats. However, in this tutorial we are creating a Multiboot compliant kernel that could be loaded by any other compliant bootloader. To achieve this, the multiboot header is mandatory.
Is the AOUT kludge required for my kernel ?
The AOUT kludge is not necessary for kernels in ELF format: a multiboot-compliant loader will recognize an ELF executable as such and use the program header to load things in their proper place. You can provide an AOUT kludge with your ELF kernel, in which case the headers of the ELF file are ignored. With any other format, such as AOUT, COFF or PE kernels, the AOUT kludge it is required, however.
Can the multiboot header be anywhere in the kernel file, or does it have to be in a specific offset?
The multiboot header must be in the first 8kb of the kernel file and must be aligned to a DWORD (4 byte) boundary for GRUB to find it. You can ensure that this is the case by putting the header in its own source code file and passing that as the first object file to LD.
Will GRUB wipe the BSS section before loading the kernel?
Yes. For ELF kernels, the .bss section is automatically identified and cleared (despite the Multiboot specification being a bit vague about it). For other formats, if you ask it politely to do so, that is if you use the 'address override' information from the multiboot header (flag #16) and give a non-zero value to the bss_end_addr field. Note that using "address override" with an ELF kernel will disable the default behavior and do what is described by the "address override" header instead.
What is the state of registers / memory / etc. when GRUB calls my kernel?
GRUB is an implementation of the Multiboot specification. Anything not specified there is "undefined behavior", which should ring a bell (not only) with C/C++ programmers... Better check the Machine State section of multiboot documentation, and assume nothing else.
I get unresolved references to kmain, or others because of missing / superfluous underscores in the object files !?
As an immediate resolve, you can use -fno-leading-underscores or -fleading-underscores to get the object file variant you need; in the long run, you might want to set up a GCC Cross-Compiler with the correct default behavior.
I get Error: junk at end of line, first unrecognized character is ',' ...
Chances are the GNU as you use is not targeted at ELF - it chokes on the .comm stack, STACKSIZE, 32 line, as three arguments to .comm are only supported for ELF targets. Try setting up a GCC Cross-Compiler.
I still get Error 13: Invalid or unsupported executable format from GRUB ...
Chances are the multiboot header is missing from the final executable, or it is not at the right location.
If you are using some other format than ELF (such as PE), you should specify the AOUT kludge in the multiboot header. The mbchk program (coming with GRUB) and "objdump -h" should give you more hints about what is going on.
It may also happen if you use an ELF object file instead of an executable (e.g. you have an ELF file with unresolved symbols or unfixable relocations). Try to link your ELF file to a binary executable to get more accurate error messages.
A common problem when your kernel size increases, is that the multiboot header does no longer appear at the start of the output binary. The common solutions is to put the multiboot header in a separate section and make sure that section is first in the output binary, or to include the multiboot header itself in the linker script.
How can I build this if my GCC port targets x86-64?
You should start with building a GCC Cross-Compiler if you don't have one yet. If you're trying to build using a GCC compiler that targets x86-64, you can add the '-m32' option to gcc and the '-melf_i386' option to ld to produce 32-bit code.