I'm learning OS development and have written a single stage bootloader that loads the kernel, enters to protected mode and finally jumps to the kernel.
I'll very appreciate any feedback and suggestion about the code. I'll be definitely glad to see any suggestions on my build system (Makefile
and the linker scripts) and how I can improve it. Should I write two separate Makefile
s for both the bootloader and the kernel?
You can take a look on the files in the Github repository if it will be more comfortable.
Thanks for your time!
src/bootloader/bootloader.s
:
/*
===============================================================================
SECTION .text.bootentry
===============================================================================
*/
.section .text.bootentry
.code16
.global bootentry
bootentry:
/* When BIOS finds a boot sector, BIOS loads it into memory at
* 0x0000:0x7C00, but some BIOSes loads it into memory at 0x07C0:0x0000,
* so it is a good practice to enforce CS:IP at the very first part of
* a boot sector.
*
* We can not manually set CS or IP, but we can long jump to the
* memory address we need: 0x0000:$bootentry.start.
*/
jmp $0x0000,$bootentry.start
bootentry.start:
/* This early execution environment is highly implemetation defined,
* meaning the implementation of your particular BIOS.
*
* So we do not ever make any assumptions on the content of the
* registers, so we need to initialize at least the data segment
* register because we use it anytime when we access the data in
* our code.
*/
xor %ax, %ax
mov %ax, %ds
/* We also have to initialize stack since we want to use
* procedure calls.
*
* BIOS memory map description found on the OSDev:
*
* <https://wiki.osdev.org/Memory_Map_(x86)>.
*
* states that memory area from 0x500 to 0x7BFF is guarranteed
* free to use. So we will use it for our stack.
*
* Note that we have to set the SS:SP pair back-to-back (no other
* instructions in between) because CPU's interruptions can harm
* this pair. All x86 processors protect the instruction following
* a write to SS from interruption, but some old 8086's had an error
* in them requiring us to use CLI/STI as a protection.
*
* We do not target on old infamous 8086's, so we rely that the
* processor does the protection itself.
*/
mov %ax, %ss
mov $0x7C00, %sp
/* The only thing that is certain after the boot sector is loaded is
* that the DL registers holds the drive code from where the boot
* sector was loaded. We need to keep it for later usage.
*/
mov %dl, boot_drive
/* Read the second sector from the floopy disk that contains our
* boot sector and place it into the memory address 0x0000:0x8000.
*
* TODO: check if reading failed.
*/
xor %ax, %ax
mov %ax, %es
mov $0x8000, %bx
mov (boot_drive), %dl
mov $0x0, %dh
mov $0x2, %cl
mov $0x0, %ch
mov $0x3, %al
call read_sectors_from_floppy
/* Check whenever A20 line is already enabled or not.
*
* TODO: enable A20 line if it is disabled.
*/
call check_a20
test %al, %al
jne bootentry.a20_is_disabled
/* Clear interrupts, load the Global Descriptor Table (GDT) and
* switch to the protected mode by setting the first bit of the CR0 register.
*/
cli
lgdt (gdtr)
mov %cr0, %eax
or $0x1, %eax
mov %eax, %cr0
/* We are in protected mode now. We need to clear the instructions pipeline
* that contains garbage 16-bit instructions. To clear the pipeline we
* need to only make a far jump.
*
* $0x8 is the offset of our second descriptor in the GDT since the first
* descriptor is the reserved null-descriptor. Thus, it is our code segment.
*/
jmp $0x08, $bootentry.clear_pipe
.code32
bootentry.clear_pipe:
/* We have to setup the segment register with our data segment (the third
* GDT's entry, in our case, with offset 0x10). */
mov $0x10, %ax
mov %ax, %ds
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
/* Setup the stack: */
mov %ax, %ss
mov $0x7C000, %esp
/* And jump to the kernel. We will never return from there. */
call 0x8000
/* But… if we actually somehow return from the kernel
* halt the CPU.
*/
bootentry.hang32:
hlt
jmp bootentry.hang32
.code16
/* If the A20 line is disabled, print a message about it and
* halt the CPU.
*/
bootentry.a20_is_disabled:
mov $a20_is_disabled, %si
call print_string_in_teletype_mode
bootentry.hang:
hlt
jmp bootentry.hang
/*
===============================================================================
SECTION .data
===============================================================================
*/
.section .data
.global a20_is_disabled
.global boot_drive
.global gdt
.global gdt_null_segment_descriptor
.global gdt_code_segment_descriptor
.global gdt_data_segment_descriptor
.global gdt_end
a20_is_disabled:
.asciz "A20 is disabled. Halt the CPU."
boot_drive:
.byte 0x00
/* All the GDT descriptors was generated by a tool found at the OSDev Wiki:
*
* <https://wiki.osdev.org/GDT_Tutorial>.
*
* The GDT_CODE_PL0 and GDT_DATA_PL0 presets was used.
*
* This is the simplest, «Basic Flat Model», memory model. Both segment
* descriptors have the same base address value of 0 and the same segment
* limit of 4 GByte space.
*
* This memory model is detaily described in Intel's manual: Volume 3,
* Chapter 3.
*/
gdt:
gdt_null_segment_descriptor:
.quad 0x0000000000000000
gdt_code_segment_descriptor:
.quad 0x00CF9A000000FFFF
gdt_data_segment_descriptor:
.quad 0x00CF92000000FFFF
gdt_end:
gdtr:
.word gdt_end - gdt - 1
.long gdt
/*
===============================================================================
SECTION .text
===============================================================================
*/
.section .text
.code16
.global print_string_in_teletype_mode
.global read_sectors_from_floppy
.global check_a20
.equ BIOS_VIDEO_TELETYPE_OUTPUT, 0x0E
.equ BIOS_DISK_READ_SECTORS_INTO_MEMORY, 0x02
/*
=======================================
PROCEDURE print_string_in_teletype_mode(str)
Description:
Prints a null-terminated string that is pointed to by str
in BIOS teletype mode to the page #0.
Implementation details:
The BIOS teletype output has the function code 0xE. It also
requires the page number (BH), the color of the character (BL,
only in graphic mode) and the character itself (AL).
The function is called by calling the 0x13 interrupt while
AH contains the function code (0xE).
The documentation about this BIOS interrupt can be found
at the address
<http://www.ctyme.com/intr/rb-0106.htm>.
Inputs:
- SI = pointer to the C-style string.
Returns:
Nothing.
=======================================
*/
print_string_in_teletype_mode:
push %ax
push %bx
cld
xor %bh, %bh
mov $BIOS_VIDEO_TELETYPE_OUTPUT, %ah
print_string_in_teletype_mode.loop:
lodsb
test %al, %al
je print_string_in_teletype_mode.done
int $0x10
jmp print_string_in_teletype_mode.loop
print_string_in_teletype_mode.done:
pop %bx
pop %ax
ret
/*
=======================================
PROCEDURE
read_sectors_from_floppy(segment, offset, number_of_sectors,
cylinder, sector, head, drive)
Description:
Read sectors from floopy disk to data buffer.
Implementation details:
BIOS fuction 0x13/0x02 (read sectors into memory) allow to read
from the hard drive, but this function can read sectors only from
the floppy disks.
The documentation for this BIOS function can be found at
<http://www.ctyme.com/intr/rb-0607.htm>.
This function also doesn't returns additional information if failed.
You can use the 0x13/0x01 BIOS function (get status of the last operation)
to see what caused the problem. Status codes can be found at
<http://www.ctyme.com/intr/rb-0606.htm#Table234>.
Inputs:
- ES:BX = data buffer.
- AL = number_of_sectors (must be nonzero).
- CH = cylinder number (0-based).
- CL = sector to start reading from (1-based, in range from 1 to 63).
- DH = head number (0-based).
- DL = drive number.
Returns:
Returns 0 in AX if successfull and a nonzero value otherwise.
=======================================
*/
read_sectors_from_floppy:
mov $BIOS_DISK_READ_SECTORS_INTO_MEMORY, %ah
int $0x13
jc read_sectors_from_floppy.read_error
xor %ax, %ax
read_sectors_from_floppy.read_error:
/* We don't have to explicitly store a nonzero value into AX since
* BIOS cares that AH contains the status of the last operation and
* it is already nonzero on failure.
*/
ret
/*
=======================================
PROCEDURE
check_a20()
Description:
The function tests whether the A20 line is enabled or not.
Implementations details:
We can check if A20 line is enabled using a simple method:
the logical addresses 0x0000:0x0510 and 0xFFFF:0x0500 points
to the same 20-bit physical address. So if we place a magic
byte into one of the address and get the same magic byte
from the other address then A20 line is disable because
memory wraps around. Otherwise A20 line is enabled.
Additional information about A20 line can be found at
the address:
<https://wiki.osdev.org/A20>.
Inputs:
Nothing.
Returns:
Returns 0 (AX) if A20 is enabled and 1 (AX) otherwise.
=======================================
*/
check_a20:
pushf
push %ds
push %di
push %es
push %si
cli
/* DS:DI = 0x0000:0x0510. */
xor %ax, %ax
mov %ax, %ds
mov $0x510, %di
/* ES:SI = 0xFFFF:0x500. */
not %ax
mov %ax, %es
mov $0x500, %si
/* Move bytes at the memory location 0xFFFF:0x500 and 0x0000:0x510
* in order to restore it later.
*/
movb %ds:(%di), %al
push %ax
movb %es:(%si), %al
push %ax
/* And now actually check if memory wraps around: */
movb $0x00, %es:(%si)
movb $0xBE, %ds:(%di)
cmpb $0xBE, %es:(%si)
/* Restore the original memory state: */
pop %ax
movb %al, %es:(%si)
pop %ax
movb %al, %ds:(%di)
/* Return the result: */
xor %ax, %ax
je check_a20.done
inc %ax
check_a20.done:
pop %si
pop %es
pop %di
pop %ds
popf
ret
src/bootloader/bootloader.ld
:
OUTPUT_FORMAT("binary");
ENTRY(bootentry);
SECTIONS {
/* BIOS loads the boot sector to the memory address 0x7C00,
* so the beginning of our code should be at this position.
*/
. = 0x7C00;
/* The .text.bootentry section will be placed as the first
* code of our bootloader.
*/
.text : SUBALIGN(0) {
*(.text.bootentry);
*(.text)
}
.data : SUBALIGN(0) {
*(.data)
}
/* The special bootloader signature is required by some old
* BIOS, so we have to place the special 0xAA55 word at the
* end of the boot sector. */
.sig : AT(0x7DFE) {
SHORT(0xAA55);
}
}
src/kernel/kernel_entry.s
:
.section .text.kernel_entry
.code32
.global kernel_entry
.extern kmain
kernel_entry:
jmp kmain
src/kernel/kernel.c
:
#include <stdint.h>
#include <stddef.h>
#define VGA_WIDTH 80
#define VGA_HEIGHT 25
#define VGA_COLOR(fg, bg) ((bg << 4) | fg)
#define VGA_CHAR(ch, fg, bg) ((uint16_t) ((VGA_COLOR(fg, bg) << 8) | ch))
enum {
VGA_COLOR_BLACK = 0,
VGA_COLOR_BLUE = 1,
VGA_COLOR_GREEN = 2,
VGA_COLOR_CYAN = 3,
VGA_COLOR_RED = 4,
VGA_COLOR_MAGENTA = 5,
VGA_COLOR_BROWN = 6,
VGA_COLOR_LIGHT_GREY = 7,
VGA_COLOR_DARK_GREY = 8,
VGA_COLOR_LIGHT_BLUE = 9,
VGA_COLOR_LIGHT_GREEN = 10,
VGA_COLOR_LIGHT_CYAN = 11,
VGA_COLOR_LIGHT_RED = 12,
VGA_COLOR_LIGHT_MAGENTA = 13,
VGA_COLOR_LIGHT_BROWN = 14,
VGA_COLOR_WHITE = 15,
};
volatile uint16_t * const VGA_VIDEO_MEMORY = (volatile uint16_t *) 0xB8000;
static void clear_screen(void);
void
kmain(void)
{
clear_screen();
const char greeting[] = "Hello, World!";
for (size_t i = 0; i < sizeof(greeting) - 1; ++i)
VGA_VIDEO_MEMORY[i] = VGA_CHAR(greeting[i], VGA_COLOR_WHITE, VGA_COLOR_BLACK);
}
void
clear_screen(void)
{
for (size_t i = 0; i < VGA_WIDTH * VGA_HEIGHT; ++i)
VGA_VIDEO_MEMORY[i] = VGA_CHAR(' ', VGA_COLOR_WHITE, VGA_COLOR_BLACK);
}
src/kernel/kernel.ld
:
OUTPUT_FORMAT("binary");
ENTRY(kernel_entry);
SECTIONS {
/* Our kernel will be loaded by the bootloader into the
* memory address 0x8000.
*/
. = 0x8000;
.text : SUBALIGN(0) {
*(.text.kernel_entry);
*(.text)
}
.data : {
*(.data)
}
.rodata : {
*(.rodata)
}
.bss : {
*(.bss)
}
}
Makefile
:
BUILDDIR = build
$(BUILDDIR)/os.bin: $(BUILDDIR)/bootloader.bin $(BUILDDIR)/kernel.bin
cat $^ > $@
$(BUILDDIR)/kernel.bin: $(BUILDDIR)/kernel_entry.o $(BUILDDIR)/kernel.o
ld -T src/kernel/kernel.ld -melf_i386 -o $@ $^
$(BUILDDIR)/kernel_entry.o:
as --32 -o $@ src/kernel/kernel_entry.s
$(BUILDDIR)/kernel.o:
gcc -ffreestanding -c -m32 -fno-pie -fno-stack-protector -o $@ src/kernel/kernel.c
$(BUILDDIR)/bootloader.bin: $(BUILDDIR)/bootloader.o
ld -T src/bootloader/bootloader.ld -melf_i386 -o $@ $<
$(BUILDDIR)/bootloader.o:
as --32 -o $@ src/bootloader/bootloader.s
.PHONY = run clean
run: $(BUILDDIR)/os.bin
qemu-system-i386 -boot order=a -drive file=$<,index=0,if=floppy,format=raw
clean:
rm -f $(BUILDDIR)/kernel.o $(BUILDDIR)/bootloader.o