Simple Multiboot Bootloader

This time we will try to write a basic simple multi boot bootloader that will print something on the screen.

Let's start creating a Multiboot-compliant kernel

start.asm:

[BITS 32]
global start
start:
	mov esp, _sys_stack
	jmp stublet

ALIGN 4
mboot:
	MULTIBOOT_PAGE_ALIGN	equ 1<<0
	MULTIBOOT_MEMORY_INFO	equ 1<<1
	MULTIBOOT_AOUT_KLUDGE	equ 1<<16
	MULTIBOOT_HEADER_MAGIC	equ 0x1BADB002
	MULTIBOOT_HEADER_FLAGS	equ MULTIBOOT_PAGE_ALIGN | MULTIBOOT_MEMORY_INFO | MULTIBOOT_AOUT_KLUDGE
	MULTIBOOT_CHECKSUM		equ -(MULTIBOOT_HEADER_MAGIC + MULTIBOOT_HEADER_FLAGS)
	EXTERN code, bss, end
	; GRUB Multiboot header, boot signature
	dd MULTIBOOT_HEADER_MAGIC
	dd MULTIBOOT_HEADER_FLAGS
	dd MULTIBOOT_CHECKSUM
	; AOUT kludge (must be physical addresses)
	; Linker script fills these in
	dd mboot
	dd code
	dd bss
	dd end
	dd start

; Main entrypoint
stublet:
	extern	main
	call	main
	jmp		$

; GDT

; Interrupt Service Routines

; BSS Section
SECTION .bss
	resb 8192 ; 8KB of memory reserved
_sys_stack:
; This line intentionally left blank

Explanation:

1 Setting Up 32-bit Mode

  • The code begins with setting up 32-bit mode by specifying [BITS 32]

2 Defining the Start Label

  • The start label is defined as the entry point of the kernel. It initializes the stack pointer and jumps to the stublet label.
global start
start:
    mov esp, _sys_stack
    jmp stublet
  • mov esp, _sys_stack: Initializes the stack pointer to the top of the stack.
  • jmp stublet: Jumps to the stublet label where the main routine will be called.

3 Defining the Multiboot Header:

The Multiboot header is defined next. This header is essential for the bootloader (e.g., GRUB) to recognize and load the kernel:

ALIGN 4
mboot:
    MULTIBOOT_PAGE_ALIGN    equ 1<<0
    MULTIBOOT_MEMORY_INFO   equ 1<<1
    MULTIBOOT_AOUT_KLUDGE   equ 1<<16
    MULTIBOOT_HEADER_MAGIC  equ 0x1BADB002
    MULTIBOOT_HEADER_FLAGS  equ MULTIBOOT_PAGE_ALIGN | MULTIBOOT_MEMORY_INFO | MULTIBOOT_AOUT_KLUDGE
    MULTIBOOT_CHECKSUM      equ -(MULTIBOOT_HEADER_MAGIC + MULTIBOOT_HEADER_FLAGS)
    EXTERN code, bss, end
    ; GRUB Multiboot header, boot signature
    dd MULTIBOOT_HEADER_MAGIC
    dd MULTIBOOT_HEADER_FLAGS
    dd MULTIBOOT_CHECKSUM
    ; AOUT kludge (must be physical addresses)
    ; Linker script fills these in
    dd mboot
    dd code
    dd bss
    dd end
    dd start
  • Constants:
    • MULTIBOOT_PAGE_ALIGN: Indicates that the bootloader should load the modules aligned on page (4KB) boundaries.
    • MULTIBOOT_MEMORY_INFO: Requests memory information from the bootloader.
    • MULTIBOOT_AOUT_KLUDGE: Indicates that the kernel image is in a.out format.
    • MULTIBOOT_HEADER_MAGIC: The magic number that identifies the header (0x1BADB002).
    • MULTIBOOT_HEADER_FLAGS: Flags indicating the requirements of the kernel.
    • MULTIBOOT_CHECKSUM: A checksum that, when added to the magic number and flags, results in zero.
  • Multiboot Header Fields:
    • dd MULTIBOOT_HEADER_MAGIC: The magic number.
    • dd MULTIBOOT_HEADER_FLAGS: The flags.
    • dd MULTIBOOT_CHECKSUM: The checksum.
    • dd mboot, code, bss, end: Addresses for AOUT kludge.
    • dd start: The entry point of the kernel.

4 Main Entry Point

The stublet label calls the main function and then enters an infinite loop:

stublet:
    extern main
    call main
    jmp $
  • extern main: Declares the main function as an external symbol.
  • call main: Calls the main function.
  • jmp $: Enters an infinite loop to prevent the CPU from executing undefined behavior after main returns.

5 Defining the BSS Section

The .bss section is used for uninitialized data. Here, it reserves 8KB of memory for the stack:

; BSS Section
SECTION .bss
    resb 8192 ; 8KB of memory reserved
_sys_stack:
; This line intentionally left blank
  • resb 8192: Reserves 8KB of uninitialized space.
  • _sys_stack: Label marking the beginning of the stack.

main.c:

#include <system.h>

/*
 * memcpy
 * Copy from source to destination. Assumes that
 * source and destination are not overlapping.
 */
unsigned char *
memcpy(
		unsigned char *dest,
		const unsigned char *src,
		int count
	  ) {
	int i;
	i = 0;
	for ( ; i < count; ++i ) {
		dest[i] = src[i];
		
	}
	return dest;
}

/*
 * memset
 * Set `count` bytes to `val`.
 */
unsigned char *
memset(
		unsigned char *dest,
		unsigned char val,
		int count
	  ) {
	int i;
	i = 0;
	for ( ; i < count; ++i ) {
		dest[i] = val;
	}
	return dest;
}

/*
 * memsetw
 * Set `count` shorts to `val`.
 */
unsigned short *
memsetw(
		unsigned short *dest,
		unsigned short val,
		int count
	  ) {
	int i;
	i = 0;
	for ( ; i < count; ++i ) {
		dest[i] = val;
	}
	return dest;
}

/*
 * strlen
 * Returns the length of a given `str`.
 */
int
strlen(
		const char *str
	  ) {
	int i = 0;
	while (str[i] != (char)0) {
		++i;
	}
	return i;
}

/*
 * inportb
 * Read from an I/O port.
 */
unsigned char
inportb(
		unsigned short _port
	   ) {
	unsigned char rv;
	__asm__ __volatile__ ("inb %1, %0" : "=a" (rv) : "dN" (_port));
	return rv;
}

/*
 * outportb
 * Write to an I/O port.
 */
void
outportb(
		unsigned short _port,
		unsigned char _data
		) {
	__asm__ __volatile__ ("outb %1, %0" : : "dN" (_port), "a" (_data));
}

/*
 * Kernel Entry Point
 */
int
main() {
	init_video();
	puts("Hello world!\n");
	for (;;);
	return 0;
}

This code snippet includes several basic utility function for memory operations, I/O port operations, and a simple kernel entry point for a bare-metal system.

Utility Functions:

1 memcpy:

The memcpy function copies count bytes from the source (src) to the destination (dest). It assumes that the source and destination do not overlap.

unsigned char *memcpy(unsigned char *dest, const unsigned char *src, int count) {
    int i;
    i = 0;
    for (; i < count; ++i) {
        dest[i] = src[i];
    }
    return dest;
}
  • Parameters:
    • dest: Destination buffer where data will be copied.
    • src: Source buffer from where data will be copied.
    • count: Number of bytes to copy.
  • Returns: The destination buffer.

2 memset:

The memset function sets count bytes of the destination buffer (dest) to the value val:

unsigned char *memset(unsigned char *dest, unsigned char val, int count) {
    int i;
    i = 0;
    for (; i < count; ++i) {
        dest[i] = val;
    }
    return dest;
}
  • Parameters:
    • dest: Destination buffer to set the value.
    • val: Value to set.
    • count: Number of bytes to set.
  • Returns: The destination buffer.

3 memsetw:

The memsetw function sets count shorts (2 bytes each) of the destination buffer (dest) to the value val.

unsigned short *memsetw(unsigned short *dest, unsigned short val, int count) {
    int i;
    i = 0;
    for (; i < count; ++i) {
        dest[i] = val;
    }
    return dest;
}
  • Parameters:
    • dest: Destination buffer to set the value.
    • val: Value to set.
    • count: Number of shorts to set.
  • Returns: The destination buffer.

4 strlen:

The strlen function calculates the length of the given string (str).

int strlen(const char *str) {
    int i = 0;
    while (str[i] != (char)0) {
        ++i;
    }
    return i;
}
  • Parameters:
    • str: Null-terminated string to calculate the length.
  • Returns: The length of the string.

5 inportb:

The inportb function reads a byte from an I/O port.

unsigned char inportb(unsigned short _port) {
    unsigned char rv;
    __asm__ __volatile__ ("inb %1, %0" : "=a" (rv) : "dN" (_port));
    return rv;
}
  • Parameters:
    • _port: I/O port address to read from.
  • Returns: The byte read from the I/O port.

6 outportb:

The outportb function writes a byte to an I/O port.

void outportb(unsigned short _port, unsigned char _data) {
    __asm__ __volatile__ ("outb %1, %0" : : "dN" (_port), "a" (_data));
}

Parameters:

  • _port: I/O port address to write to.
  • _data: Data byte to write.

Kernel Entry Point:

The main function is the entry point of the kernel. It initializes the video and prints "Hello world!" to the screen, then enters an infinite loop.

int main() {
    init_video();
    puts("Hello world!\n");
    for (;;);
    return 0;
}
  • init_video: Initializes the video display (implementation not shown in the provided code).
  • puts: Prints a string to the screen (implementation not shown in the provided code).
  • Infinite Loop: Prevents the kernel from exiting, keeping the system running indefinitely.

vga.c:

#include <system.h>

/*
 * Text pointer, background, foreground
 */
unsigned short * textmemptr;
int attrib = 0x0F;
int csr_x = 0, csr_y = 0;

/*
 * scroll
 * Scroll the screen
 */
void
scroll() {
	unsigned blank, temp;
	blank = 0x20 | (attrib << 8);
	if (csr_y >= 25) {
		/*
		 * Move the current text chunk that makes up the screen
		 * back in the buffer by one line.
		 */
		temp = csr_y - 25 + 1;
		memcpy(textmemptr, textmemptr + temp * 80, (25 - temp) * 80 * 2);
		/*
		 * Set the chunk of memory that occupies
		 * the last line of text to the blank character
		 */
		memsetw(textmemptr + (25 - temp) * 80, blank, 80);
		csr_y = 25 - 1;
	}
}

/*
 * move_csr
 * Update the hardware cursor
 */
void
move_csr() {
	unsigned temp;
	temp = csr_y * 80 + csr_x;
	
	/*
	 * Write stuff out.
	 */
	outportb(0x3D4, 14);
	outportb(0x3D5, temp >> 8);
	outportb(0x3D4, 15);
	outportb(0x3D5, temp);
}

/*
 * cls
 * Clear the screen
 */
void
cls() {
	unsigned blank;
	int i;
	blank = 0x20 | (attrib << 8);
	for (i = 0; i < 25; ++i) {
		memsetw(textmemptr + i * 80, blank, 80);
	}
	csr_x = 0;
	csr_y = 0;
	move_csr();
}

/*
 * putch
 * Puts a character to the screen
 */
void
putch(unsigned char c) {
	unsigned short *where;
	unsigned att = attrib << 8;
	if (c == 0x08) {
		/* Backspace */
		if (csr_x != 0) csr_x--;
	} else if (c == 0x09) {
		/* Tab */
		csr_x = (csr_x + 8) & ~(8 - 1);
	} else if (c == '\r') {
		/* Carriage return */
		csr_x = 0;
	} else if (c == '\n') {
		/* New line */
		csr_x = 0;
		csr_y++;
	} else if (c >= ' ') {
		where = textmemptr + (csr_y * 80 + csr_x);
		*where = c | att;
		csr_x++;
	}

	if (csr_x >= 80) {
		csr_x = 0;
		csr_y++;
	}
	scroll();
	move_csr();
}

/*
 * puts
 * Put string to screen
 */
void
puts(
 unsigned char * text
){ 
	int i;
	int len = strlen(text);
	for (i = 0; i < len; ++i) {
		putch(text[i]);
	}
}

/*
 * settextcolor
 * Sets the foreground and background color
 */
void
settextcolor(
unsigned char forecolor,
unsigned char backcolor
) {
	attrib = (backcolor << 4) | (forecolor & 0x0F);
}

/*
 * init_video
 * Initialize the VGA driver.
 */
void init_video() {
	textmemptr = (unsigned short *)0xB8000;
	cls();
}

This C code snippet is an implementation of a basic text-based VGA driver for a simple operating system kernel. It provides functions to manipulate the screen, such as scrolling, moving the cursor, clearing the screen, printing characters and strings, and setting text colors. Let's go through each part of the code in detail.

Global Variables

unsigned short *textmemptr;
int attrib = 0x0F;
int csr_x = 0, csr_y = 0;
  • textmemptr: A pointer to the start of video memory (typically at 0xB8000 for color text mode on VGA).
  • attrib: Stores the current text attributes (foreground and background color).
  • csr_x and csr_y: Track the cursor position on the screen.

scroll:

The scroll function scrolls the screen when the cursor goes beyond the last line.

void scroll() {
    unsigned blank, temp;
    blank = 0x20 | (attrib << 8); // Blank character with current attributes
    if (csr_y >= 25) {
        temp = csr_y - 25 + 1;
        memcpy(textmemptr, textmemptr + temp * 80, (25 - temp) * 80 * 2);
        memsetw(textmemptr + (25 - temp) * 80, blank, 80);
        csr_y = 25 - 1;
    }
}
  • blank: Represents a blank character with the current attributes.
  • temp: Calculates the number of lines to scroll.
  • memcpy: Moves the screen content up by one line.
  • memsetw: Clears the last line.

move_csr:

The move_csr function updates the hardware cursor position.

void move_csr() {
    unsigned temp;
    temp = csr_y * 80 + csr_x;
    outportb(0x3D4, 14);
    outportb(0x3D5, temp >> 8);
    outportb(0x3D4, 15);
    outportb(0x3D5, temp);
}
  • temp: Calculates the cursor position in video memory.
  • outportb: Writes the cursor position to the VGA hardware registers.

cls:

The cls function clears the screen by filling it with blank characters.

void cls() {
    unsigned blank;
    int i;
    blank = 0x20 | (attrib << 8);
    for (i = 0; i < 25; ++i) {
        memsetw(textmemptr + i * 80, blank, 80);
    }
    csr_x = 0;
    csr_y = 0;
    move_csr();
}
  • blank: Represents a blank character with the current attributes.
  • memsetw: Clears each line of the screen.
  • Resets the cursor position to the top-left corner and updates the hardware cursor.

putch:

The putch function prints a single character to the screen, handling special characters like backspace, tab, carriage return, and newline.

void putch(unsigned char c) {
    unsigned short *where;
    unsigned att = attrib << 8;
    if (c == 0x08) {
        if (csr_x != 0) csr_x--;
    } else if (c == 0x09) {
        csr_x = (csr_x + 8) & ~(8 - 1);
    } else if (c == '\r') {
        csr_x = 0;
    } else if (c == '\n') {
        csr_x = 0;
        csr_y++;
    } else if (c >= ' ') {
        where = textmemptr + (csr_y * 80 + csr_x);
        *where = c | att;
        csr_x++;
    }
    if (csr_x >= 80) {
        csr_x = 0;
        csr_y++;
    }
    scroll();
    move_csr();
}
  • Handles backspace (0x08), tab (0x09), carriage return ('\r'), and newline ('\n').
  • Prints regular characters to the current cursor position and updates the cursor.
  • Calls scroll and move_csr to handle screen scrolling and cursor updating.

puts:

The puts function prints a string to the screen.

void puts(unsigned char *text) {
    int i;
    int len = strlen(text);
    for (i = 0; i < len; ++i) {
        putch(text[i]);
    }
}
  • Iterates over the string and prints each character using putch.

settextcolor:

The settextcolor function sets the foreground and background color attributes.

void settextcolor(unsigned char forecolor, unsigned char backcolor) {
    attrib = (backcolor << 4) | (forecolor & 0x0F);
}
  • Combines the foreground and background colors into the attrib variable.

init_video

The init_video function initializes the VGA driver by setting the video memory pointer and clearing the screen.

void init_video() {
    textmemptr = (unsigned short *)0xB8000;
    cls();
}
  • Sets textmemptr to the base address of VGA text memory.
  • Calls cls to clear the screen.

system.h:

#ifndef __SYSTEM_H
#define __SYSTEM_H

/*

Include Guard
The include guard ensures that the contents of the header file are only included once during compilation, preventing multiple definition errors.

*/


/*
They are declared as extern to indicate that their definitions are provided elsewhere.

*/
/* Kernel Main */
extern unsigned char *memcpy(unsigned char *dest, const unsigned char *src, int count);
extern unsigned char *memset(unsigned char *dest, unsigned char val, int count);
extern unsigned short *memsetw(unsigned short *dest, unsigned short val, int count);
extern int strlen(const char *str);
extern unsigned char inportb (unsigned short _port);
extern void outportb (unsigned short _port, unsigned char _data);

/* VGA driver */
extern void cls();
extern void putch(unsigned char c);
extern void puts(unsigned char *str);
extern void settextcolor(unsigned char forecolor, unsigned char backcolor);
extern void init_video();

#endif

link.ld:

This linker script is used with the ld linker to produce the final binary image of the kernel.

OUTPUT_FORMAT("binary")
ENTRY(start)
phys = 0x00100000;
SECTIONS
{
	.text phys : AT(phys) {
		code = .;
		*(.text)
		*(.rodata)
		. = ALIGN(4096);
	}
	.data : AT(phys + (data - code))
	{
		data = .;
		*(.data)
		. = ALIGN(4096);
	}
	.bss : AT(phys + (bss - code))
	{
		bss = .;
		*(.bss)
		. = ALIGN(4096);
	}
	end = .;
}
  • Overview:
    • OUTPUT_FORMAT("binary"): Specifies that the output format should be a raw binary file.
    • ENTRY(start): Defines the entry point of the program, which is the start label.
    • phys: A variable representing the physical address where the sections will be loaded, set to 0x00100000 (1 MB).
    • SECTIONS: Begins the section layout definition.

SECTIONS Block

The SECTIONS block is where we define how different sections of the program (text, data, bss) are laid out in memory.

.text Section:

.text phys : AT(phys) {
	code = .;
	*(.text)
	*(.rodata)
	. = ALIGN(4096);
}
  • .text phys : AT(phys): Places the .text section at the physical address phys (1 MB).
  • code = .;: Marks the beginning of the .text section with the code symbol.
  • *(.text): Includes all .text sections from the input object files.
  • *(.rodata): Includes all .rodata (read-only data) sections from the input object files.
  • . = ALIGN(4096);: Aligns the current location to the next 4096-byte (4 KB) boundary.

.data Section:

.data : AT(phys + (data - code)) {
	data = .;
	*(.data)
	. = ALIGN(4096);
}
  • .data : AT(phys + (data - code)): Places the .data section at an address calculated by adding the offset of the .data section from the start of the .text section to phys.
  • data = .;: Marks the beginning of the .data section with the data symbol.
  • *(.data): Includes all .data sections from the input object files.
  • . = ALIGN(4096);: Aligns the current location to the next 4096-byte (4 KB) boundary.

.bss Section:

.bss : AT(phys + (bss - code)) {
	bss = .;
	*(.bss)
	. = ALIGN(4096);
}
  • .bss : AT(phys + (bss - code)): Places the .bss section at an address calculated by adding the offset of the .bss section from the start of the .text section to phys.
  • bss = .;: Marks the beginning of the .bss section with the bss symbol.
  • *(.bss): Includes all .bss sections from the input object files.
  • . = ALIGN(4096);: Aligns the current location to the next 4096-byte (4 KB) boundary.

End Symbol:

end = .;
  • end = .;: Defines the end symbol to mark the end of all sections.

grub.cfg:

The GRUB configuration file (grub.cfg) is crucial for booting your kernel. This file provides GRUB with the necessary instructions to load and execute your kernel.

	set timeout=5
	set default=0

	menuentry "My Multiboot Kernel" {
    	multiboot /boot/kernel.bin
    	boot
	}

Explanation

  • set timeout=5: Sets a 5-second timeout before the default menu entry is automatically selected.
  • set default=0: Sets the first menu entry as the default.
  • menuentry "My Multiboot Kernel": Defines a menu entry with the title "My Multiboot Kernel".
  • multiboot /boot/kernel.bin: Specifies the path to your kernel binary and indicates that it conforms to the Multiboot specification.
  • boot: Instructs GRUB to boot the specified kernel.

Makefile:

.PHONY: all clean install

all: kernel

run: install
	qemu-system-x86_64 -cdrom my_os.iso

install: kernel
	mkdir -p iso/boot/grub
	cp kernel iso/boot/kernel.bin
	cp grub.cfg iso/boot/grub/grub.cfg
	
	grub-mkrescue -o my_os.iso iso

kernel: start.o link.ld main.o vga.o
	ld -m elf_i386 -T link.ld -o kernel.bin start.o main.o vga.o

%.o: %.c
	gcc -Wall -m32 -fno-pie -O0 -fstrength-reduce -fomit-frame-pointer -finline-functions -nostdinc -fno-builtin -I./include -c -o $@ $<

start.o: start.asm
	nasm -f elf -o start.o start.asm

clean:
	rm -f *.o kernel.bin

Explanation of Options:

1 gcc Options:

  • -Wall: Enable all compiler's warning messages.
  • -m32: Generates code for a 32-bit environment.
  • -O0: Disables optimization, which means the compiler will generate code that is as straightforward as possible. This can be useful for debugging because the generated code closely corresponds to the source code.
    By default -O  Enables basic optimization (-O1), which can improve performance and reduce code size.
    • -O0: No Optimization
      • Effects: The compiler's default behavior is to not perform any optimizations. This makes compilation faster and the generated code closely follows the source code structure.
      • Use Case: This level is typically used during the development and debugging phases because it makes it easier to understand the generated code and correlate it with the source code.
    • -O1: Basic optimization.
      • Description: Basic optimization.
      • Effects: This level enables optimizations that do not require a significant amount of compilation time. It attempts to reduce code size and improve execution speed without affecting the debug-ability too much.
      • Use Case: This level is a good compromise between compilation time and execution performance. It is often used during development when some level of optimization is desired, but full optimization is not yet necessary.
    • -02: Moderate optimization.
      • Description: Moderate optimization.
      • Effects: This level enables nearly all supported optimizations that do not involve a space-speed trade-off. It improves execution speed and reduces code size more aggressively than -O1.
      • Use Case: This level is suitable for production builds where performance is important, and the extra compilation time is acceptable.
    • -03: High optimization.
      • Description: High optimization.
      • Effects: This level enables all optimizations from -O2 and also includes more aggressive optimizations that may increase both compilation time and the size of the generated code. It focuses on maximizing execution speed.
      • Use Case: This level is used for applications where execution performance is critical and the larger code size and longer compilation time are acceptable.
    • -Os: Optimize for size.
      • Description: Optimize for size.
      • Effects: This level enables all -O2 optimizations that do not increase code size. It also includes optimizations aimed at reducing code size.
      • Use Case: This level is used when memory footprint is a concern, such as in embedded systems or applications where minimizing the executable size is crucial.
    • -Ofast: High optimization disregarding strict standard compliance.
      • Description: High optimization disregarding strict standard compliance.
      • Effects: This level enables all -O3 optimizations and includes aggressive optimizations that may break strict standards compliance and result in non-portable code.
      • Use Case: This level is used for performance-critical applications where standard compliance is less important than execution speed.
    • -Og: Optimize debugging experience.
      • Description: Optimize debugging experience.
      • Effects: This level enables optimizations that do not interfere with debugging. It is designed to provide a good debugging experience while still performing some optimizations.
      • Use Case: This level is used during the development phase to balance between optimized code and ease of debugging.
  • -fstrength-reduce: Enable strength reduction optimization which replaces expensive operations with cheaper ones (e.g., replacing multiplications with additions).
  • -fomit-frame-pointer: Omits the frame pointer for functions that don't need one, which can make code slightly faster and smaller.
  • -finline-functions: Enable function inlining, which replaces function call with the actual code of the function.
  • -nostdinc: Prevents the compiler from searching standard directories for header files, requiring you to explicitly specify the include directories.
  • -fno-builtin: Disables recognition of built-in functions, forcing the compiler to generate function calls to the standard library functions.
  • I./include: Adds ./include to the list of directories to be searched for header files.
  • -c: Compiles the source file into an object file (i.e., does not link).
  • -o $@: Specifies the output file name, where $@ is a makefile variable representing the target file.
  • $<: Represents the first prerequisite in a makefile rule (usually the source file to be compiled).

2 nasm options:

  • nasm: This is the Netwide Assembler, a popular assembler for the x86 architecture. It converts assembly language source code into machine code.
  • -f elf: This option tells NASM to generate an object file in the ELF (Executable and Linkable Format) format. ELF is a common standard file format for executables, object code, shared libraries, and core dumps, used by many Unix-like operating systems, including Linux.
  • -o start.o: This option specifies the output file name. The -o flag is followed by the name of the object file to be generated. In this case, it is start.o.
  • start.asm: This is the input file, the assembly language source code that you want to assemble.

3 Complete MakeFile explanation:

.PHONY: all clean install
  • .PHONY declares that all, clean, and install are phony targets. These targets are not actual files but names for commands to be executed.

Targets and Rules:

all:

all: kernel
  • The all target depends on the kernel target. When you run make all, it will invoke the kernel target.

run:

run: install
	qemu-system-x86_64 -cdrom my_os.iso
  • The run target depends on the install target. After running install, it launches QEMU to run the OS using the generated ISO image my_os.iso.

install:

install: kernel
	mkdir -p iso/boot/grub
	cp kernel iso/boot/kernel.bin
	cp grub.cfg iso/boot/grub/grub.cfg
	grub-mkrescue -o my_os.iso iso
  • The install target depends on the kernel target.
  • It creates the necessary directories and copies the kernel binary and grub.cfg configuration file to the appropriate locations.
  • Finally, it uses grub-mkrescue to create a bootable ISO image named my_os.iso from the iso directory.

kernel:

kernel: start.o link.ld main.o vga.o
	ld -m elf_i386 -T link.ld -o kernel.bin start.o main.o vga.o
  • The kernel target depends on object files start.o, main.o, and vga.o, and the linker script link.ld.
  • It links these object files into a single kernel binary kernel.bin using the specified linker script.

Object File Compilation:

%.o: %.c
	gcc -Wall -m32 -fno-pie -O0 -fstrength-reduce -fomit-frame-pointer -finline-functions -nostdinc -fno-builtin -I./include -c -o $@ $<
  • This is a pattern rule for compiling .c files into .o object files.
  • %.o: %.c matches any .c file and compiles it into a corresponding .o file.
  • gcc is invoked with several options:
    • -Wall: Enables all warnings.
    • -m32: Generates 32-bit code.
    • -fno-pie: Disables Position Independent Executable code generation.
    • -O: Enables optimization.
    • -fstrength-reduce: Enables strength reduction optimization.
    • -fomit-frame-pointer: Omits the frame pointer.
    • -finline-functions: Enables function inlining.
    • -nostdinc: Prevents including standard system directories for headers.
    • -fno-builtin: Disables built-in functions.
    • -I./include: Specifies the include directory for header files.
    • -c: Compiles the source file without linking.
    • -o $@: Specifies the output file (target).
    • $<: Represents the first prerequisite (source file).

start.o:

start.o: start.asm
	nasm -f elf -o start.o start.asm
  • This rule compiles the start.asm assembly file into the start.o object file using NASM.
  • -f elf specifies the output format as ELF.

clean:

clean:
	rm -f *.o kernel.bin
  • The clean target removes all object files and the kernel.bin file.

Output

image-132.png

 

image-133.png