In the last article, we searched for the binary kernel in the root directory of ISO9660 file system. On finding, we loaded the binary kernel at 0x300000 , after that we just jumped to it thus start executing the binary kernel.
This is the benefit of the binary kernel that it contains only the raw data, no meta-data. It is very simple to use. Just load it at some memory address and it is ready to run.
However, it does not contains required information, like its size, its name and you name it. To overcome this drawback and store some information along with raw-binary data, ELF format is used in Unix based operating systems.
ELF (Executable and Linkable Format)
The ELF (Executable and Linkable Format) file format is a widely used standard file format for executables, object code, shared libraries, and core dumps in Unix-like operating systems.
Structure: ELF files are divided into headers and sections. The ELF header at the beginning contains essential information about the file, such as its type (executable, shared object, etc.), architecture, entry point, and section header table offset. Sections hold various parts of the program or data.
Types: There are different types of ELF files:
Executable Files: These are directly executable by the operating system.
Shared Object Files: These are dynamically linked libraries.
Relocatable Files: These contain position-independent code and data that can be combined with other object files to create an executable or shared object.
Sections: ELF files typically have multiple sections:
Program Header Table: Describes the layout of the file in memory when it is loaded.
Section Header Table: Describes the layout of sections within the file, including their names, offsets, and sizes.
Data Sections: Hold executable code, data, symbols, debugging information, etc.
Uses: ELF files are used on Unix-like systems including Linux, BSD, and Solaris, among others. They provide a standardized way for executables, libraries, and debugging information to be structured and accessed by the operating system and related tools.
Tools: Various tools exist to manipulate ELF files, such as readelf for displaying ELF information, objdump for examining object files, and ld (the GNU linker) for linking ELF files.
Advantages: ELF is flexible, extensible, and suitable for a wide range of use cases from small embedded systems to large-scale server applications. Its design allows for efficient linking, loading, and execution of programs.
Difference Between Plain Binary and ELF Format
The main differences between plain binary and ELF (Executable and Linkable Format) can be summarized in several key aspects:
Structure and Metadata:
Plain Binary: A plain binary file is typically a raw sequence of machine code instructions and data. It lacks any structured metadata or headers that describe the file's content, such as information about sections, symbols, or how it should be loaded into memory.
ELF Format: ELF files, on the other hand, have a structured format that includes headers (ELF header, program header table, section header table) and sections. These headers provide essential metadata about the file, such as its type (executable, shared object, relocatable), the architecture it's compiled for, program entry point, section layouts, and more.
Usage and Flexibility:
Plain Binary: Plain binary files are straightforward and may be used for simple purposes where no additional metadata or functionality is needed beyond raw machine code execution.
ELF Format: ELF files are more versatile and can serve as executables, shared libraries (DLLs), or relocatable object files. This versatility allows for dynamic linking, shared library support, debugging information, and more sophisticated use cases in operating systems like Linux and other Unix-like systems.
Linking and Loading:
Plain Binary: Linking with plain binary files typically involves concatenating them or loading them into memory at specific addresses, with little to no dynamic linking or relocation capabilities.
ELF Format: ELF files support dynamic linking and relocation, allowing programs to reference symbols (functions and data) defined in other ELF files (shared libraries) and dynamically resolve those references during program execution.
Tool Support:
Plain Binary: Manipulating plain binary files usually requires custom tools or scripts tailored to the specific format and intended use.
ELF Format: ELF files benefit from extensive tool support on Unix-like systems. Tools like readelf, objdump, ld, and debuggers can parse ELF headers, analyze sections, extract symbols, and perform various other operations essential for software development and debugging.
Portability and Standards:
Plain Binary: Plain binary files are less standardized and may vary significantly between different platforms and compilers.
ELF Format: ELF files adhere to a well-defined standard, making them highly portable across different Unix-like systems and architectures, provided that the necessary tools and runtime support for ELF are available.
Difference of Loading and Executing Process of Plain Binary Format and ELF Format Kernel
The loading process of plain and elf format is exactly the same, which is like reading the file from the file system and reading it to some known memory location.
The difference lies that in elf format an extra step is involved, which is, we have to take care of parsing the loaded elf file. That is once we read the elf file at some memory location, we have to parse it by reading its program header and loading its segments into memory. Once done we can execute the elf file from the location where it is parsed.
Step-by-Step Process
1 Read the PVD of the ISO9660 File System
This step is same as of the last article post of loading and calling binary kernel. For summary in this step we read the LBA 16 of the ISO9660 disk to check for the PVD. If it is PVD, we got what we wanted otherwise read the next LBA until we got the PVD.
2 Parse the PVD to get the Root Directory Entries Record
This step is also same as of the loading binary kernel. From the last step we must have got the PVD which contains the metadata for the disk. At offset 156 of the PVD it contains the Root Directory Entry Record, which would be of size 34 bytes and it contains the extent location and size of the root directory entries which is an kind of array for the root entries, with each entry of 34 bytes.
3 Scan the Root Directory Entries for the Kernel
This step is also same as of loading the binary kernel. From the previous step we must have got the Root Directory Entries.
4 On Finding the Kernel, Load it to the Location 0x300000
This step is similar to that of loading binary kernel as well. In this step we scan the root directory entries (array of entries) for the our required elf file which is kernel. Once we got that, we load it to the location 0x300000.
5 Parse and Load the ELF Kernel at the Location 0x1000000
Now comes the different step to that of the loading binary kernel. Now we have got to parse the loaded elf kernel. We will parse and load the elf segments to location 0x1000000.
We would read the elf file from location 0x300000 and load all the segments to their respective positions as mentioned in the kernel.ld file which is 0x1000000.
ENTRY(kernel_entry) /* Entry point is the 'kernel_entry' label in stage2.asm */
SECTIONS
{
. = 0x1000000; /* Start address for the binary */
.text : {
*(.text)
}
.rodata : {
*(.rodata)
}
.data : {
*(.data)
}
.bss : {
*(.bss)
}
}
// Define the memory start location where the ELF file is loaded
#define KERNEL_ELF_READ 0x300000
#define KERNEL_DEST_START 0x1000000
int load_elf32()
{
boot_print("[load_elf32], Loading an ELF32 file.\n");
Elf32_Header *header = (Elf32_Header *)KERNEL_ELF_READ;
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
boot_print("[load_elf32], Not an ELF file.\n");
return 0; // return false
}
// 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 *)((unsigned char *)header + header->e_phoff + i * header->e_phentsize);
// If the segment is loadable
if (phdr->p_type == PT_LOAD)
{
boot_print("[load_elf32], Loading a Phdr.\n");
// Calculate the source and destination addresses
unsigned char *src = (unsigned char *)KERNEL_ELF_READ + phdr->p_offset;
// unsigned char *dst = (unsigned char *)KERNEL_DEST_START + phdr->p_vaddr;
unsigned char *dst = (unsigned char *)phdr->p_vaddr;
// Copy the segment to the virtual address
memcpyb(dst, src, phdr->p_filesz);
// Zero out the rest of the segment if p_memsz > p_filesz
memsetb(dst + phdr->p_filesz, 0, phdr->p_memsz - phdr->p_filesz);
}
}
boot_print("[load_elf32], Loading Elf32 done.\n");
return 1; // Return true
}
6 Call the Loaded Kernel
Now the last step is just to jump to that location where the kernel elf is parsed.
;; In assembly
jmp 0x1000000 ; jump to the location
// In C
void (*kernel_entry)(void) = (void (*)(void))0x1000000;
kernel_entry();
7 Output
Table of Contents
Your experience on this site will be improved by allowing cookies Cookie Policy