Up to this point we had our kernel as flat binary format. Which we just loaded at memory location 0xb000
by reading it from the disk and we directly jump to it thus executing the kernel code sequentially. This is very simplistic way it has its own pros and cons.
Let's first familiarize what's the pros and cons of this approach and what's the alternative of it.
🔰 Plain Binary Format
Definition:
Plain binary format refers to the simplest form of executable files that contain machine code directly executable by a computer's CPU. It lacks higher-level structures like headers or metadata found in more complex executable formats (e.g., ELF, COFF).
Characteristics:
- No Metadata: Plain binaries contain only raw machine code instructions. There are no headers or other metadata providing information about the executable.
- Fixed Memory Layout: The binary code is typically loaded into memory as a contiguous block starting at a specific address. The format assumes a fixed memory layout for code and data.
- Direct Execution: The CPU can directly execute instructions from the binary file. There's no need for additional processing or interpretation beyond what the CPU itself provides.
Using Plain Binary Format has both advantages and disadvantages:
Pros:
- Simplicity: Flat binaries are straightforward to load and execute since they contain raw machine code without additional structures like headers or metadata. This simplicity can reduce complexity in the initial stages of OS development.
- Performance: Kernel operations in flat binary format can achieve optimal performance because there are no additional layers of abstraction or interpretation required by the loader or runtime environment.
Cons:
- Security Risks: Flat binaries lack built-in security features such as memory protection and privilege separation, exposing the kernel to vulnerabilities like buffer overflows and arbitrary code execution attacks.
- Debugging Challenges: Debugging flat binaries can be more difficult compared to formats that include debugging symbols or structured metadata. Tools for debugging may be less effective, requiring additional effort to set up and use effectively.
🔰 ELF: The Alternative of the Flat Binary File Format
The Executable and Linkable Format (ELF) is a more complex, structured executable format widely used in Unix-based operating systems (e.g., Linux). It contains not only the machine code, but also metadata, such as sections, symbol tables, and relocation information, which assist both the loader and runtime environment in correctly executing the binary.
Characteristics:
- Metadata & Headers: ELF files contain extensive metadata, including headers that define the file structure, the starting point for execution, sections of code, data, and dynamic linking information.
- Multiple Sections: ELF files organize code, data, and other information into distinct sections (e.g.,
.text
,.data
,.bss
), allowing for efficient memory management, loading, and execution. - Relocation & Linking: ELF supports both static and dynamic linking, allowing for external libraries and modules to be referenced during execution. It can also include relocation information to adjust addresses at runtime.
Using ELF files in OS development also has pros and cons:
Pros:
- Modularity & Flexibility: ELF enables dynamic linking and shared libraries, making it easier to update or modify components of the system without needing to recompile the entire kernel. This provides more flexibility compared to flat binaries.
- Security Features: ELF files can support advanced security features like address space layout randomization (ASLR), memory protection, and control over permissions for code and data sections (read/write/execute). These features help in securing kernel space against common vulnerabilities.
- Debugging & Tool Support: ELF files can include debugging symbols, making it easier to use debugging tools like
gdb
or profiling tools. This structured format allows more precise control over diagnostics, performance analysis, and bug tracking.
Cons:
- Complexity: ELF is far more complex than plain binary formats, requiring more overhead during both compilation and loading. It necessitates an OS kernel capable of handling the additional layers of abstraction, which may be challenging during the initial development stages of an operating system.
- Performance Overhead: Due to the presence of headers, symbol tables, and support for dynamic linking, ELF introduces some performance overhead compared to flat binaries. This can slow down the execution, particularly for low-level operations like bootstrapping or bare-metal execution.
Shift to ELF
There are few steps needed to have a fully working ELF
kernel.
1 Port Kernel to ELF Format:
We currently have our merged kernel in plain binary format, but we need to convert it to the ELF format. At the moment, both kernel_entry
and kernel_main
are in ELF format before being merged into a single plain binary file using the LD linker. For the initial phase, we will focus on converting kernel_entry
to ELF format and invoking it from stage 2. We will address the linking of kernel_entry
and kernel_main
and generating an executable file from them in a later phase. At this stage, we will not implement calling kernel_main
from kernel_entry
.
2 Temporary Protected Mode:
As we are reading the kernel at location 0xB000
in stage 2. We will not change it, just do some addition after this process. We will just interpret the ELF
kernel loaded at this location and load it at higher memory by switching to temporary protected mode (just for loading the interpreting the elf kernel and loading at higher memory location).
Below will be the changes which we need to do in stage 2 :-
;; Just after loading the kernel at location 0xb000
;; We now have to get into the 32-bit land to copy the
;; kernel to 0x1000000, then get back to real mode,
;; to do other things.
; Go, Go, Go
mov eax, cr0
or eax, 1
mov cr0, eax
; Jump to temporary 32-bit
jmp CODE_DESC:Temp32Bit
; *******************************
;; The temporary 32 bit
BITS 32
;; Any 32-bit Includes
Temp32Bit:
; Disable Interrupts
cli
; Setup Segments and Stack
xor eax, eax
mov ax, DATA_DESC
mov ds, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov es, ax
mov esp, 0x7BFF
; Example: Print characters on screen
mov esi, 0xb8000 ; VGA text buffer address
mov byte [esi], '3' ; Print '3' with white text on black background
mov byte [esi+1], 0x07
mov byte [esi+2], '2' ; Print '2' next to '3'
mov byte [esi+3], 0x07
;; TODO
;; Code to interpret the ELF Kernel and load it to 0x1000000
jmp $
Explanation:
- Enable Protected Mode:
mov eax, cr0
: Load control register 0 (CR0) intoeax
.or eax, 1
: Set the least significant bit (PE, Protection Enable) to 1, enabling Protected Mode.mov cr0, eax
: Writeeax
back to CR0 to activate Protected Mode.
- Jump to 32-bit Code Segment:
jmp CODE_DESC:Temp32Bit
: This jumps to theTemp32Bit
label using a far jump to the code segment specified byCODE_DESC
.
- Temporary 32-bit Code Segment (
Temp32Bit
):- Disable Interrupts:
cli
instruction disables interrupts to ensure a stable environment during initialization. - Setup Segments and Stack:
xor eax, eax
: Cleareax
.mov ax, DATA_DESC
: Load the data segment descriptor value intoax
.mov ds, ax
,mov fs, ax
,mov gs, ax
,mov ss, ax
,mov es, ax
: Set all segment registers (ds
,fs
,gs
,ss
,es
) to the data segment descriptor value.mov esp, 0x7BFF
: Set the stack pointer (esp
) to a suitable address (0x7BFF
in this case). Adjust this according to your stack requirements.
- Example VGA Text Mode Output:
mov esi, 0xb8000
: VGA text buffer start address (assuming VGA text mode memory).- Printing characters ('3' and '2') to the screen using direct memory access (
mov byte [esi], '3'
).
- TODO:
- Here we have to do the elf loader things, which will interpret the loaded elf kernel and copy it to the higher memory.
- Infinite Loop:
jmp $
: Infinite loop ($
refers to the current address, effectively creating a halt).
- Disable Interrupts:
3 ELF Loader
You can get more information on ELF Format by going through this article:
A Little Summary of the ELF Format:
The ELF (Executable and Linkable Format) is a file format used on Unix-like operating systems for executable files, object code, shared libraries, and core dumps. It provides a standardized format for binary files, allowing different programs and libraries to interact seamlessly across various hardware and software platforms.
ELF 32-bit Format Overview
The ELF 32-bit format is designed for systems where integers are 32 bits wide. It consists of several key components:
1. ELF Header (Elf32_Ehdr
)
The ELF header is located at the beginning of the ELF file and provides essential information about the file's structure and how to interpret it. It includes fields such as:
typedef struct {
unsigned char e_ident[16];
unsigned short e_type;
unsigned short e_machine;
unsigned int e_version;
unsigned int e_entry;
unsigned int e_phoff;
unsigned int e_shoff;
unsigned int e_flags;
unsigned short e_ehsize;
unsigned short e_phentsize;
unsigned short e_phnum;
unsigned short e_shentsize;
unsigned short e_shnum;
unsigned short e_shstrndx;
} Elf32_Ehdr;
e_ident
: An array of bytes identifying the file as an ELF object.e_ident
: {0x7f, 'E', 'L', 'F', ...}
e_type
: Specifies the object file type (e.g., executable, shared object, relocatable).e_type
: 2 (Executable file)
e_machine
: Specifies the target architecture (e.g., Intel 80386, ARM).e_machine
: 3 (Intel 80386)
e_version
: Specifies the ELF version.e_entry
: The virtual address where the program should start executing.e_entry
: 0x08048000 (Entry point address)
e_phoff
: The file offset in bytes to the start of the program header table.e_phoff
: 52 (Offset to the Program Header Table)
e_shoff
: The file offset in bytes to the start of the section header table.e_shoff
: 0x1000 (Offset to the Section Header Table)
e_flags
: Processor-specific flags associated with the file.e_ehsize
: The size in bytes of the ELF header itself.e_phentsize
: The size in bytes of one entry in the program header table.e_phnum
: The number of entries in the program header table.e_phnum
: 2 (Number of Program Header entries)
e_shentsize
: The size in bytes of one entry in the section header table.e_shnum
: The number of entries in the section header table.e_shstrndx
: The index of the section header table entry that contains the section names.
2. Program Header Table (Elf32_Phdr
)
The program header table contains entries describing the various segments (chunks of the file to be loaded into memory) of the ELF file. Each entry (Elf32_Phdr
) includes:
typedef struct {
unsigned int p_type;
unsigned int p_offset;
unsigend int p_vaddr;
unsigned int p_paddr;
unsigned int p_filesz;
unsigned int p_memsz;
unsigned int p_flags;
unsigned int p_align;
} Elf32_Phdr;
p_type
: Specifies the type of segment (e.g.,PT_LOAD
for loadable segment,PT_DYNAMIC
for dynamic linking information).p_offset
: The offset in the file where the segment is located.p_vaddr
: The virtual address of the segment in memory.p_paddr
: The physical address of the segment (not used in most systems).p_filesz
: The size in bytes of the segment in the file.p_memsz
: The size in bytes of the segment in memory (may include padding).p_flags
: Flags describing the permissions and attributes of the segment (e.g., read, write, execute).p_align
: The alignment constraints of the segment in memory and in the file.
Example entries:
Segment 1 (.text segment)
p_type
: 1 (PT_LOAD)p_offset
: 0x100 (Offset in the file)p_vaddr
: 0x08048000 (Virtual address in memory)p_filesz
: 0x200 (Size in the file)p_memsz
: 0x200 (Size in memory)p_flags
: 5 (Read and Execute)
Segment 2 (.data and .bss segments)
p_type
: 1 (PT_LOAD)p_offset
: 0x300 (Offset in the file)p_vaddr
: 0x08049000 (Virtual address in memory)p_filesz
: 0x100 (Size in the file)p_memsz
: 0x200 (Size in memory, includes .bss)p_flags
: 6 (Read and Write)
3. Section Header Table (Elf32_Shdr
)
The section header table contains entries describing the sections (logical groupings of data or code) within the ELF file. Sections include code and data, symbol tables, string tables, and more.
These are used for linking and debugging.
typedef struct {
unsigned int sh_name;
unsigned int sh_type;
unsigned int sh_flags;
unsigned int sh_addr;
unsigned int sh_offset;
unsigned int sh_size;
unsigned int sh_link;
unsigned int sh_info;
unsigned int sh_addralign;
unsigned int sh_entsize;
} Elf32_Shdr;
Each entry (Elf32_Shdr
) includes:
sh_name
: The offset to a string in the.shstrtab
section that represents the name of this section.sh_type
: The type of the section (SHT_PROGBITS
for program data,SHT_SYMTAB
for symbol table, etc.).sh_flags
: Flags describing the attributes of the section (e.g., writable, executable).sh_addr
: The virtual address of the section in memory (if loaded).sh_offset
: The offset in the file where the section starts.sh_size
: The size in bytes of the section.sh_link
: Contains the section header table index link (e.g., forSHT_SYMTAB
, it contains the index of the associatedSHT_STRTAB
).sh_info
: Contains extra information about the section (e.g., for symbol tables, it contains one greater than the symbol table index of the last local symbol).sh_addralign
: Contains the required alignment of the section.sh_entsize
: Contains the size, in bytes, of each entry for sections that contain fixed-size entries.
Example sections:
- .text: Contains executable code
- .data: Contains initialized data
- .bss: Contains uninitialized data (zero-initialized at runtime)
4. Other Sections and Data Structures
- Represent logical groupings of data or code within the file.
- Each section has a specific purpose and attributes defined in its corresponding section header.
- Sections like
.text
(executable code),.data
(initialized data),.bss
(uninitialized data),.rodata
(read-only data), symbol table (.symtab
), and string table (.strtab
) are common.
Structure of ELF in C:
boot/stage2/elf.c
:
#ifndef __ELF__H
#define __ELF__H
#define ELFMAG0 0x7f
#define ELFMAG1 'E'
#define ELFMAG2 'L'
#define ELFMAG3 'F'
#define EI_NIDENT 16
typedef unsigned int Elf32_Word;
typedef unsigned int Elf32_Addr;
typedef unsigned int Elf32_Off;
typedef unsigned int Elf32_Sword;
typedef unsigned short Elf32_Half;
/*
* ELF Header
*/
typedef struct {
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry;
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Header;
/*
* e_type
*/
#define ET_NONE 0 /* No file type */
#define ET_REL 1 /* Relocatable file */
#define ET_EXEC 2 /* Executable file */
#define ET_DYN 3 /* Shared object file */
#define ET_CORE 4 /* Core file */
#define ET_LOPROC 0xff0 /* [Processor Specific] */
#define ET_HIPROC 0xfff /* [Processor Specific] */
/*
* Machine types
*/
#define EM_NONE 0
#define EM_386 3
#define EV_NONE 0
#define EV_CURRENT 1
/** Program Header */
typedef struct {
Elf32_Word p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
} Elf32_Phdr;
/* p_type values */
#define PT_NULL 0 /* Unused, skip me */
#define PT_LOAD 1 /* Loadable segment */
#define PT_DYNAMIC 2 /* Dynamic linking information */
#define PT_INTERP 3 /* Interpreter (null-terminated string, pathname) */
#define PT_NOTE 4 /* Auxillary information */
#define PT_SHLIB 5 /* Reserved. */
#define PT_PHDR 6 /* Oh, it's me. Hello! Back-reference to the header table itself */
#define PT_LOPROC 0x70000000
#define PT_HIPROC 0x7FFFFFFF
/** Section Header */
typedef struct {
Elf32_Word sh_name;
Elf32_Word sh_type;
Elf32_Word sh_flags;
Elf32_Addr sh_addr;
Elf32_Off sh_offset;
Elf32_Word sh_size;
Elf32_Word sh_link;
Elf32_Word sh_info;
Elf32_Word sh_addralign;
Elf32_Word sh_entsize;
} Elf32_Shdr;
typedef struct {
uint32_t id;
uint32_t ptr;
} Elf32_auxv;
typedef struct {
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Half st_shndx;
} Elf32_Sym;
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;
typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
Elf32_Off d_off;
} d_un;
} Elf32_Dyn;
/* sh_type values */
#define SHT_NONE 0
#define SHT_PROGBITS 1
#define SHT_SYMTAB 2
#define SHT_STRTAB 3
#define SHT_NOBITS 8
#define SHT_REL 9
#define ELF32_R_SYM(i) ((i) >> 8)
#define ELF32_R_TYPE(i) ((unsigned char)(i))
#define ELF32_R_INFO(s,t) (((s) << 8) + (unsigned char)(t))
#define ELF32_ST_BIND(i) ((i) >> 4)
#define ELF32_ST_TYPE(i) ((i) & 0xf)
#define ELF32_ST_INFO(b,t) (((b) << 4) + ((t) & 0xf))
#define STB_LOCAL 0
#define STB_GLOBAL 1
#define STB_WEAK 2
#define STB_NUM 3
#define STB_LOPROC 13
#define STB_HIPROC 15
#define STT_NOTYPE 0
#define STT_OBJECT 1
#define STT_FUNC 2
#define STT_SECTION 3
#define STT_FILE 4
#define STT_COMMON 5
#define STT_TLS 6
#define STT_NUM 7
#define STT_LOPROC 13
#define STT_HIPROC 15
ELF Loader:
// Define the memory start location where the ELF file is loaded
#define KERNEL_LOAD_START 0xb000
#define KERNEL_DEST_START 0x1000000
int load_elf32() {
Elf32_Header *header = (Elf32_Header *)KERNEL_LOAD_START;
if (header->e_ident[0] != ELFMAG0 ||
header->e_ident[1] != ELFMAG1 ||
header->e_ident[2] != ELFMAG2 ||
header->e_ident[3] != ELFMAG3) {
// ERROR not an ELF File
return 0;
}
uintptr_t entry = (uintptr_t)header->e_entry;
// Iterate through program headers
for (int i = 0; i < header->e_phnum; i++) {
// Calculate the address of the program header
Elf32_Phdr *phdr = (Elf32_Phdr *)((uint8_t *)header + header->e_phoff + i * header->e_phentsize);
// If the segment is loadable
if (phdr->p_type == PT_LOAD) {
// Calculate the source and destination addresses
uint8_t *src = (uint8_t *)KERNEL_LOAD_START + phdr->p_offset;
uint8_t *dst = (uint8_t *)phdr->p_vaddr;
// Copy the segment to the virtual address
memcpy(dst, src, phdr->p_filesz);
// Zero out the rest of the segment if p_memsz > p_filesz
memset(dst + phdr->p_filesz, 0, phdr->p_memsz - phdr->p_filesz);
}
}
return 1;
}
Modify Stage 2:
As our code in stage 2 was in assembly only. Now we have the elf.c
file which need to be linked with the stage2 code, so that we can call the load_elf32()
function from it. In order to do so, we will compile them individually and link with ld in plain binary format, because if we generate elf file of stage 2 then we would need elf parse in stage 1 as well. However we go simply way not to complex the things up.
;; Define external C function
extern load_elf32
; Inside of the Temp32Bit after setting up the stack do below things
call load_elf32
cmp eax, 1
jne .elf_error
jmp 0x1000000 ; jmp to the kernel
jmp $
.elf_error:
mov esi, 0xb8000
mov byte [esi], 'E'
mov byte [esi + 1], 0x04
jmp $
Along with it we have to remove the ORG
directives in the starting of the stage2.asm
and comment out the times STAGE_2_SIZE
instruction the very last line. Since we are compiling the assembly code and c code in ELF
and then linking them together as the stage2
flat binary code using LD
.
ORG
: This instruction is only applicable when we are generating plain binary file with the-f bin
option innasm
.- While this one is not valid for the
-f elf32
option. Thenasm
assembler will abort the assembling if this directive is encountered and show the error in console.
- While this one is not valid for the
times STAGE_2_SIZE - ($-$$) db 0
:- As it was for to pad the stage 2 to make it complete allowed 28 KB in size. It is a good things as it stops the assembling if the stage2.asm file exceeds this mark. However now we have assembly and c code so we can't set it in the assembly code only. Either we need a way to set this size limit in the
ld
script or for the time being we can just uncomment it because our stage 2 code right now is half to this limit.
- As it was for to pad the stage 2 to make it complete allowed 28 KB in size. It is a good things as it stops the assembling if the stage2.asm file exceeds this mark. However now we have assembly and c code so we can't set it in the assembly code only. Either we need a way to set this size limit in the
Compilation and Linking Stage2:
/* stage2 ld linker script*/
ENTRY(stage2_entry) /* Entry point is the 'start' label in stage2.asm */
SECTIONS
{
. = 0x0500; /* Start address for the binary */
.text : {
*(.text)
}
.rodata : {
*(.rodata)
}
.data : {
*(.data)
}
.bss : {
*(.bss)
}
}
stage2main.elf: boot/stage2/stage2.asm
nasm -f elf32 -I $(BOOT_STAGE_INCLUDE) -I $(BOOT_STAGE2_INCLUDE) -o build/$@ $<
elf.elf: boot/stage2/elf.c
gcc -m32 -fno-pie -ffreestanding -c boot/stage2/elf.c -o build/elf.elf
print.elf: boot/stage2/print.c
gcc -m32 -fno-pie -ffreestanding -c boot/stage2/print.c -o build/print.elf
stage2.bin: stage2main.elf elf.elf print.elf
ld -m elf_i386 -T boot/stage2/stage2.ld --oformat binary -o build/stage2.bin build/stage2main.elf build/elf.elf
Kernel Entry Things:
We will for the time being just show the welcome message in the kernel code and rest of things of previous code will be commented.
Some Finding
: You might be wondering it is easier step as our kernel_entry.asm code which was in assembly. Just comment unnecessary lines of code and print the welcome string, since the nasm
with -f elf
option generates ELF
file which we can load. But Nope, ELF
file generated with -f elf
option in nasm
is of type RELOCATABLE
, which is different to that of EXECUTABLE
.
Below are the screenshots for the kernel_entry.elf
information, produced with the nasm -f elf
option.


- Relocatable Type ELF:
- They are not meant to be executed directly. Instead, they are designed to be linked with other object files to produce an executable or shared library.
- Contain sections for code, data, and other resources, such as
.text
(code),.data
(initialized data), and.bss
(uninitialized data). - Include relocation entries that specify how addresses should be adjusted when the file is linked with other files.
- Symbol tables are included to resolve addresses during linking.
- Used as input to the linker (e.g.,
ld
or the compiler’s built-in linker) to produce an executable or a shared library. - Example:
gcc -c file.c
producesfile.o
, a relocatable ELF file.
- Executable Type ELF:
- Executable ELF files are the final output of the linking process.
- They are designed to be loaded into memory and executed by the operating system.
- Contain a program header table that specifies how the operating system should create a process image.
- Sections are organized into segments, which the operating system maps into memory.
- Symbol tables and relocation entries are typically not included, as addresses are already resolved.
- Produced by the linker from one or more relocatable ELF files.
- Example:
gcc file.o -o myprogram
producesmyprogram
, an executable ELF file.
So we would need Executable
type ELF
file of kernel_entry
. For that we would need the help of ld
linker. We will pass the generated kernel_entry.elf
file from the nasm -f elf
to the ld
linker along with the linker script specifying the ELF loading address and much more.
BITS 32
section .text
kernel_entry:
jmp start
;; Includes files
start:
call ClearScreen32
;; Print Welcome Message
mov esi , sKernelWelcomeStatement
mov bl, YELLOW
mov bh, BLACK
call PrintString32
jmp $
section .data
sKernelWelcomeStatement: db 'Welcome to ELF 32-bit Kernel Land.', 0
times 1024 - ($-$$) db 0
ENTRY(kernel_entry) /* Entry point is the 'start' label in stage2.asm */
SECTIONS
{
/* This is necessary as our kernel code will be parsed and loaded
at this location. */
. = 0x1000000; /* Start address for the binary */
.text : {
*(.text)
}
.rodata : {
*(.rodata)
}
.data : {
*(.data)
}
.bss : {
*(.bss)
}
}
Compilation and Linking ELF Kernel:
kernel_entry.elf: kernel_entry.asm
nasm -f elf32 -I $(BOOT_STAGE2_INCLUDE) $< -o build/kernel_entry.elf
theKernel: kernel_entry.elf
ld -m elf_i386 -T kernel.ld -o build/kernel.elf build/kernel_entry.elf
Here is the screenshot of the kernel.elf
file information generated using ld
.


4 Output
