5
\$\begingroup\$

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 Makefiles 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 0ドルx0000,$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 0ドルx7C00, %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 0ドルx8000, %bx
 mov (boot_drive), %dl
 mov 0ドルx0, %dh
 mov 0ドルx2, %cl
 mov 0ドルx0, %ch
 mov 0ドルx3, %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 0ドルx1, %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.
 *
 * 0ドルx8 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 0ドルx08, $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 0ドルx10, %ax
 mov %ax, %ds
 mov %ax, %es
 mov %ax, %fs
 mov %ax, %gs
 /* Setup the stack: */
 mov %ax, %ss
 mov 0ドルx7C000, %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 0ドルx10
 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 0ドルx13
 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 0ドルx510, %di
 /* ES:SI = 0xFFFF:0x500. */
 not %ax
 mov %ax, %es
 mov 0ドルx500, %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 0ドルx00, %es:(%si)
 movb 0ドルxBE, %ds:(%di)
 cmpb 0ドルxBE, %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
asked Feb 29, 2020 at 7:41
\$\endgroup\$

2 Answers 2

4
+100
\$\begingroup\$

check_a20 has two bugs in it. First, you not comparing with the correct memory addresses. The physical address for the real mode FFFF:0500 address is

 FFFF0
 0500
 -----
1004F0

which, if A20 is off, will wrap around to 0000:04F0, not 0000:0510. The correct addresses to use are 0000:0500 and FFFF:0510.

The second error is with how you get the result of the test back to the caller. You have a comparison instruction, cmpb 0ドルxBE, %es:(%si), which will set the Z flag (among others). You have several other instructions (pop, mov, pop, mov) then an xor %ax, %ax, which will set the Z flag. The je on the next line will then always jump, so your check_a20 will always return 0.

answered Mar 2, 2020 at 20:34
\$\endgroup\$
4
\$\begingroup\$

Review of the Makefile:

  • There's no .DELETE_ON_ERROR target. I know of no good reason to write a Makefile without that.

  • Put the makefile in the target directory (build) and find sources using VPATH. Then we don't need to hand-write rules.

  • We can make better use of built-in rules: adding ASFLAGS := --32 means we don't need to write rules for bootloader.o and kernel_entry.o. Similarly, add CFLAGS += -ffreestanding -c -m32 -fno-pie -fno-stack-protector to let us default kernel.o.

  • With suitable LDFLAGS, we can write the *.bin commands as $(LINK.c) $^ $(LDLIBS) -o $@.

  • rm -f can be written more portably as $(RM)

  • clean target leaves some *.bin files lying around.

answered Mar 2, 2020 at 17:08
\$\endgroup\$
0

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.