Post Stastics
- This post has 4331 words.
- Estimated read time is 20.62 minute(s).
In Chapter 21, we completed the process lifecycle:
create process load executable run user code exit process wait for status destroy process free user pages free page tables free page directory
Now we can replace the temporary TOYEXE format with the first real executable-loader milestone: ELF32.
ELF is the Executable and Linkable Format used by many Unix-like systems. The ELF specification defines the file header, program header table, and how loadable segments describe a process image; the generic ABI describes program headers as the structures a system uses to prepare a program for execution. (Linux Foundation Specs)
For this first milestone, we will support only a small subset:
32-bit ELF little-endian ET_EXEC EM_386 PT_LOAD segments static executable shape in-kernel byte-array image no relocations no dynamic linking no filesystem yet
That is enough to replace this:
TOYEXE header ↓ TOYEXE loader
with this:
ELF32 header ↓ program headers ↓ PT_LOAD segments ↓ entry point ↓ user process
The milestone output will look like:
Process test: starting ELF32 user program test ELF32: loaded PT_LOAD vaddr=0x40100000 filesz=256 memsz=320 Process: created pid=1 name=elf-demo user> toyix echo: toyix Syscall: process elf-demo pid=1 exited code 9 Process test: ELF32 load/read/write/sleep/exit cleanup sanity check passed
1. Why ELF now?
TOYEXE gave us the loader shape:
validate map copy zero BSS set entry set stack start process
ELF uses the same basic shape, but with a standard format:
ELF header tells us architecture, type, entry point, program header location program header table tells us what segments to load PT_LOAD segment tells us file offset, virtual address, file size, memory size, and flags
The System V Intel386 ABI supplement is the relevant processor-specific ABI family for 32-bit i386 ELF conventions. (SCO)
We are not yet trying to support every valid ELF file. We are building the smallest useful loader.
2. Patch overview
Add:
include/kernel/ ├── elf32.h └── elf_loader.h kernel/ └── elf_loader.c
Modify:
kernel/process.c Makefile tests/smoke.sh
Optionally keep the TOYEXE files in the tree as a historical reference, but remove kernel/toyexe.o from the build if it is no longer used.
3. Add include/kernel/elf32.h
This gives us our own freestanding ELF definitions instead of depending on a hosted system’s <elf.h>.
// include/kernel/elf32.h
#ifndef TOYIX_KERNEL_ELF32_H
#define TOYIX_KERNEL_ELF32_H
#include <stdint.h>
#define ELF_NIDENT 16u
#define EI_MAG0 0u
#define EI_MAG1 1u
#define EI_MAG2 2u
#define EI_MAG3 3u
#define EI_CLASS 4u
#define EI_DATA 5u
#define EI_VERSION 6u
#define EI_OSABI 7u
#define EI_ABIVERSION 8u
#define ELFMAG0 0x7Fu
#define ELFMAG1 'E'
#define ELFMAG2 'L'
#define ELFMAG3 'F'
#define ELFCLASS32 1u
#define ELFDATA2LSB 1u
#define EV_NONE 0u
#define EV_CURRENT 1u
#define ET_NONE 0u
#define ET_REL 1u
#define ET_EXEC 2u
#define ET_DYN 3u
#define EM_NONE 0u
#define EM_386 3u
#define PT_NULL 0u
#define PT_LOAD 1u
#define PF_X 1u
#define PF_W 2u
#define PF_R 4u
typedef uint16_t Elf32_Half;
typedef uint32_t Elf32_Word;
typedef int32_t Elf32_Sword;
typedef uint32_t Elf32_Addr;
typedef uint32_t Elf32_Off;
typedef struct elf32_ehdr {
unsigned char e_ident[ELF_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;
} __attribute__((packed)) elf32_ehdr_t;
typedef struct elf32_phdr {
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;
} __attribute__((packed)) elf32_phdr_t;
#endif
Why define these ourselves?
A kernel is freestanding.
That means we should not depend on the host system’s libc headers. We want the kernel source tree to define exactly the ABI structures it uses.
4. Add include/kernel/elf_loader.h
// include/kernel/elf_loader.h
#ifndef TOYIX_KERNEL_ELF_LOADER_H
#define TOYIX_KERNEL_ELF_LOADER_H
#include <stdint.h>
#include "kernel/process.h"
#define ELF_LOADER_OK 0
#define ELF_LOADER_ERR_INVALID -1
#define ELF_LOADER_ERR_UNSUPPORTED -2
#define ELF_LOADER_ERR_TOO_LARGE -3
#define ELF_LOADER_ERR_LOAD_FAILED -4
#define ELF_USER_STACK_TOP 0x70000000u
#define ELF_USER_STACK_SIZE 4096u
int elf_load_process(
process_t *process,
const uint8_t *image,
uint32_t image_size
);
process_t *elf_create_process(
const char *name,
const uint8_t *image,
uint32_t image_size
);
#endif
5. Add kernel/elf_loader.c
// kernel/elf_loader.c
#include <stddef.h>
#include <stdint.h>
#include "kernel/console.h"
#include "kernel/elf32.h"
#include "kernel/elf_loader.h"
#include "kernel/panic.h"
#include "kernel/pmm.h"
#include "kernel/process.h"
#define ELF_MAX_IMAGE_MEMORY (1024u * 1024u)
static uintptr_t align_down_page(uintptr_t value) {
return value & ~(uintptr_t)(PMM_PAGE_SIZE - 1u);
}
static uintptr_t align_up_page(uintptr_t value) {
return (value + PMM_PAGE_SIZE - 1u) &
~(uintptr_t)(PMM_PAGE_SIZE - 1u);
}
static int range_overflows(uint32_t start, uint32_t size) {
return start + size < start;
}
static int validate_elf_header(
const elf32_ehdr_t *header,
uint32_t image_size
) {
if (header == 0) {
return ELF_LOADER_ERR_INVALID;
}
if (image_size < sizeof(elf32_ehdr_t)) {
return ELF_LOADER_ERR_INVALID;
}
if (header->e_ident[EI_MAG0] != ELFMAG0 ||
header->e_ident[EI_MAG1] != ELFMAG1 ||
header->e_ident[EI_MAG2] != ELFMAG2 ||
header->e_ident[EI_MAG3] != ELFMAG3) {
return ELF_LOADER_ERR_INVALID;
}
if (header->e_ident[EI_CLASS] != ELFCLASS32) {
return ELF_LOADER_ERR_UNSUPPORTED;
}
if (header->e_ident[EI_DATA] != ELFDATA2LSB) {
return ELF_LOADER_ERR_UNSUPPORTED;
}
if (header->e_ident[EI_VERSION] != EV_CURRENT) {
return ELF_LOADER_ERR_UNSUPPORTED;
}
if (header->e_type != ET_EXEC) {
return ELF_LOADER_ERR_UNSUPPORTED;
}
if (header->e_machine != EM_386) {
return ELF_LOADER_ERR_UNSUPPORTED;
}
if (header->e_version != EV_CURRENT) {
return ELF_LOADER_ERR_UNSUPPORTED;
}
if (header->e_ehsize != sizeof(elf32_ehdr_t)) {
return ELF_LOADER_ERR_INVALID;
}
if (header->e_phoff == 0 || header->e_phnum == 0) {
return ELF_LOADER_ERR_INVALID;
}
if (header->e_phentsize != sizeof(elf32_phdr_t)) {
return ELF_LOADER_ERR_INVALID;
}
if (range_overflows(header->e_phoff,
header->e_phnum * sizeof(elf32_phdr_t))) {
return ELF_LOADER_ERR_INVALID;
}
if (header->e_phoff +
header->e_phnum * sizeof(elf32_phdr_t) > image_size) {
return ELF_LOADER_ERR_INVALID;
}
return ELF_LOADER_OK;
}
static int validate_load_segment(
const elf32_phdr_t *program_header,
uint32_t image_size
) {
if (program_header->p_type != PT_LOAD) {
return ELF_LOADER_OK;
}
if (program_header->p_memsz == 0) {
return ELF_LOADER_ERR_INVALID;
}
if (program_header->p_filesz > program_header->p_memsz) {
return ELF_LOADER_ERR_INVALID;
}
if (range_overflows(program_header->p_offset,
program_header->p_filesz)) {
return ELF_LOADER_ERR_INVALID;
}
if (program_header->p_offset + program_header->p_filesz > image_size) {
return ELF_LOADER_ERR_INVALID;
}
if (range_overflows(program_header->p_vaddr,
program_header->p_memsz)) {
return ELF_LOADER_ERR_INVALID;
}
if (program_header->p_memsz > ELF_MAX_IMAGE_MEMORY) {
return ELF_LOADER_ERR_TOO_LARGE;
}
if (program_header->p_vaddr < ADDRESS_SPACE_USER_BASE ||
program_header->p_vaddr >= ADDRESS_SPACE_USER_TOP) {
return ELF_LOADER_ERR_INVALID;
}
if (program_header->p_vaddr + program_header->p_memsz >
ADDRESS_SPACE_USER_TOP) {
return ELF_LOADER_ERR_INVALID;
}
/*
* First milestone restriction:
*
* We require page alignment for loadable segment virtual addresses.
* Real ELF loaders handle more combinations of p_vaddr, p_offset, and
* p_align. We will loosen this later.
*/
if ((program_header->p_vaddr & (PMM_PAGE_SIZE - 1u)) != 0) {
return ELF_LOADER_ERR_UNSUPPORTED;
}
return ELF_LOADER_OK;
}
static uint32_t segment_flags_to_process_flags(const elf32_phdr_t *phdr) {
uint32_t flags = ADDRESS_SPACE_FLAG_USER;
/*
* Our current process_map_user_region() maps pages writable. We still
* compute flags here because later chapters will add read-only text pages.
*/
if ((phdr->p_flags & PF_W) != 0) {
flags |= ADDRESS_SPACE_FLAG_WRITABLE;
} else {
flags |= ADDRESS_SPACE_FLAG_WRITABLE;
}
return flags;
}
static int load_segment(
process_t *process,
const uint8_t *image,
const elf32_phdr_t *phdr
) {
uintptr_t segment_start = align_down_page(phdr->p_vaddr);
uintptr_t segment_end = align_up_page(phdr->p_vaddr + phdr->p_memsz);
uint32_t runtime_size = (uint32_t)(segment_end - segment_start);
/*
* The process helper currently maps writable user pages. The computed
* flags will become important once process_map_user_region accepts page
* permissions.
*/
(void)segment_flags_to_process_flags(phdr);
if (process_map_user_region(process, segment_start, runtime_size) != 0) {
return ELF_LOADER_ERR_LOAD_FAILED;
}
if (process_copy_to_user_init(
process,
phdr->p_vaddr,
image + phdr->p_offset,
phdr->p_filesz
) != 0) {
return ELF_LOADER_ERR_LOAD_FAILED;
}
if (phdr->p_memsz > phdr->p_filesz) {
uintptr_t bss_start = phdr->p_vaddr + phdr->p_filesz;
uint32_t bss_size = phdr->p_memsz - phdr->p_filesz;
if (process_zero_user_init(process, bss_start, bss_size) != 0) {
return ELF_LOADER_ERR_LOAD_FAILED;
}
}
console_write("ELF32: loaded PT_LOAD vaddr=");
console_write_hex32(phdr->p_vaddr);
console_write(" filesz=");
console_write_u32_dec(phdr->p_filesz);
console_write(" memsz=");
console_write_u32_dec(phdr->p_memsz);
console_putc('\n');
return ELF_LOADER_OK;
}
int elf_load_process(
process_t *process,
const uint8_t *image,
uint32_t image_size
) {
if (process == 0 || image == 0) {
return ELF_LOADER_ERR_INVALID;
}
const elf32_ehdr_t *header = (const elf32_ehdr_t *)image;
int rc = validate_elf_header(header, image_size);
if (rc != ELF_LOADER_OK) {
return rc;
}
const elf32_phdr_t *program_headers =
(const elf32_phdr_t *)(image + header->e_phoff);
int saw_load = 0;
for (uint32_t i = 0; i < header->e_phnum; ++i) {
const elf32_phdr_t *phdr = &program_headers[i];
rc = validate_load_segment(phdr, image_size);
if (rc != ELF_LOADER_OK) {
return rc;
}
if (phdr->p_type == PT_LOAD) {
saw_load = 1;
}
}
if (!saw_load) {
return ELF_LOADER_ERR_INVALID;
}
/*
* Map stack before loading the image. If this fails, process creation
* fails before the thread can run.
*/
uintptr_t stack_top = ELF_USER_STACK_TOP;
uintptr_t stack_base = stack_top - ELF_USER_STACK_SIZE;
if (process_map_user_region(
process,
stack_base,
ELF_USER_STACK_SIZE
) != 0) {
return ELF_LOADER_ERR_LOAD_FAILED;
}
if (process_zero_user_init(
process,
stack_base,
ELF_USER_STACK_SIZE
) != 0) {
return ELF_LOADER_ERR_LOAD_FAILED;
}
process_set_user_stack(process, stack_base, stack_top);
for (uint32_t i = 0; i < header->e_phnum; ++i) {
const elf32_phdr_t *phdr = &program_headers[i];
if (phdr->p_type != PT_LOAD) {
continue;
}
rc = load_segment(process, image, phdr);
if (rc != ELF_LOADER_OK) {
return rc;
}
}
if (header->e_entry < ADDRESS_SPACE_USER_BASE ||
header->e_entry >= ADDRESS_SPACE_USER_TOP) {
return ELF_LOADER_ERR_INVALID;
}
process->user_code_base = header->e_entry;
process_set_user_entry(process, header->e_entry);
console_write("ELF32: entry=");
console_write_hex32(header->e_entry);
console_putc('\n');
return ELF_LOADER_OK;
}
process_t *elf_create_process(
const char *name,
const uint8_t *image,
uint32_t image_size
) {
process_t *process = process_create_empty(name);
int rc = elf_load_process(process, image, image_size);
if (rc != ELF_LOADER_OK) {
console_write("ELF32: load failed rc=");
console_write_u32_dec((uint32_t)(-rc));
console_putc('\n');
kernel_panic("ELF32 process load failed");
}
process_start_user(process);
return process;
}
6. Why this loader ignores section headers
ELF files have two major views:
program headers runtime loading view section headers linker/debugger/static-analysis view
The generic ABI describes the program header table as the array of structures used by the system to prepare a program for execution. For loading an executable process image, program headers are the part we care about first. (Linux Foundation Specs)
So this first loader ignores:
section headers symbol tables string tables relocation sections debug sections
That is normal for a minimal executable loader.
7. Replace the test image builder in kernel/process.c
Now we build a minimal ELF image instead of a TOYEXE image.
Update the includes.
Remove:
#include "kernel/toyexe.h"
Add:
#include "kernel/elf32.h" #include "kernel/elf_loader.h"
Now replace the TOYEXE-specific constants with ELF constants.
#define DEMO_SEGMENT_VA 0x40100000u #define DEMO_IMAGE_SIZE 256u #define DEMO_BSS_SIZE 64u #define DEMO_PROMPT_VA (DEMO_SEGMENT_VA + 0xA0u) #define DEMO_PREFIX_VA (DEMO_SEGMENT_VA + 0xA8u) #define DEMO_NEWLINE_VA (DEMO_SEGMENT_VA + 0xB0u) #define DEMO_INPUT_VA (DEMO_SEGMENT_VA + DEMO_IMAGE_SIZE) #define DEMO_INPUT_MAX 32u
Keep the emitter helpers from Chapter 20:
emit_u8() emit_u32() emit_mov_eax_imm32() emit_mov_ebx_imm32() emit_mov_ecx_imm32() emit_mov_edx_imm32() emit_int80()
Update build_stdio_demo_payload() so it references DEMO_SEGMENT_VA, not TOYEXE_USER_BASE.
static void build_stdio_demo_payload(uint8_t *program, uint32_t program_size) {
memset(program, 0x90, program_size);
uint32_t offset = 0;
/*
* write(1, "user> ", 6)
*/
emit_mov_eax_imm32(program, &offset, SYS_WRITE);
emit_mov_ebx_imm32(program, &offset, FD_STDOUT);
emit_mov_ecx_imm32(program, &offset, DEMO_PROMPT_VA);
emit_mov_edx_imm32(program, &offset, 6);
emit_int80(program, &offset);
/*
* bytes = read(0, input_buffer, 32)
*/
emit_mov_eax_imm32(program, &offset, SYS_READ);
emit_mov_ebx_imm32(program, &offset, FD_STDIN);
emit_mov_ecx_imm32(program, &offset, DEMO_INPUT_VA);
emit_mov_edx_imm32(program, &offset, DEMO_INPUT_MAX);
emit_int80(program, &offset);
/*
* mov esi, eax
*/
emit_u8(program, &offset, 0x89u);
emit_u8(program, &offset, 0xC6u);
/*
* write(1, "echo: ", 6)
*/
emit_mov_eax_imm32(program, &offset, SYS_WRITE);
emit_mov_ebx_imm32(program, &offset, FD_STDOUT);
emit_mov_ecx_imm32(program, &offset, DEMO_PREFIX_VA);
emit_mov_edx_imm32(program, &offset, 6);
emit_int80(program, &offset);
/*
* write(1, input_buffer, esi)
*/
emit_mov_eax_imm32(program, &offset, SYS_WRITE);
emit_mov_ebx_imm32(program, &offset, FD_STDOUT);
emit_mov_ecx_imm32(program, &offset, DEMO_INPUT_VA);
/*
* mov edx, esi
*/
emit_u8(program, &offset, 0x89u);
emit_u8(program, &offset, 0xF2u);
emit_int80(program, &offset);
/*
* write(1, "\n", 1)
*/
emit_mov_eax_imm32(program, &offset, SYS_WRITE);
emit_mov_ebx_imm32(program, &offset, FD_STDOUT);
emit_mov_ecx_imm32(program, &offset, DEMO_NEWLINE_VA);
emit_mov_edx_imm32(program, &offset, 1);
emit_int80(program, &offset);
/*
* sleep(3)
*/
emit_mov_eax_imm32(program, &offset, SYS_SLEEP);
emit_mov_ebx_imm32(program, &offset, 3);
emit_int80(program, &offset);
/*
* exit(9)
*/
emit_mov_eax_imm32(program, &offset, SYS_EXIT);
emit_mov_ebx_imm32(program, &offset, 9);
emit_int80(program, &offset);
/*
* jmp $
*/
emit_u8(program, &offset, 0xEBu);
emit_u8(program, &offset, 0xFEu);
if (offset >= 0xA0u) {
kernel_panic("ELF demo program overlapped data area");
}
const char prompt[] = "user> ";
const char prefix[] = "echo: ";
const char newline[] = "\n";
memcpy(
&program[DEMO_PROMPT_VA - DEMO_SEGMENT_VA],
prompt,
sizeof(prompt) - 1u
);
memcpy(
&program[DEMO_PREFIX_VA - DEMO_SEGMENT_VA],
prefix,
sizeof(prefix) - 1u
);
memcpy(
&program[DEMO_NEWLINE_VA - DEMO_SEGMENT_VA],
newline,
1u
);
}
The lower 0x80/0x88/0x90 data offsets look tidy on paper, but with the current emitted instruction stream they overlap the generated code. In the working code, the embedded strings start at 0xA0 instead so the payload and its data stay separated cleanly.
Now add the ELF image builder.
static uint32_t build_stdio_demo_elf(
uint8_t *image,
uint32_t image_capacity
) {
uint32_t payload_offset =
sizeof(elf32_ehdr_t) + sizeof(elf32_phdr_t);
uint32_t total_size = payload_offset + DEMO_IMAGE_SIZE;
if (image_capacity < total_size) {
kernel_panic("ELF demo image buffer too small");
}
memset(image, 0, image_capacity);
elf32_ehdr_t *ehdr = (elf32_ehdr_t *)image;
elf32_phdr_t *phdr =
(elf32_phdr_t *)(image + sizeof(elf32_ehdr_t));
ehdr->e_ident[EI_MAG0] = ELFMAG0;
ehdr->e_ident[EI_MAG1] = ELFMAG1;
ehdr->e_ident[EI_MAG2] = ELFMAG2;
ehdr->e_ident[EI_MAG3] = ELFMAG3;
ehdr->e_ident[EI_CLASS] = ELFCLASS32;
ehdr->e_ident[EI_DATA] = ELFDATA2LSB;
ehdr->e_ident[EI_VERSION] = EV_CURRENT;
ehdr->e_type = ET_EXEC;
ehdr->e_machine = EM_386;
ehdr->e_version = EV_CURRENT;
ehdr->e_entry = DEMO_SEGMENT_VA;
ehdr->e_phoff = sizeof(elf32_ehdr_t);
ehdr->e_shoff = 0;
ehdr->e_flags = 0;
ehdr->e_ehsize = sizeof(elf32_ehdr_t);
ehdr->e_phentsize = sizeof(elf32_phdr_t);
ehdr->e_phnum = 1;
ehdr->e_shentsize = 0;
ehdr->e_shnum = 0;
ehdr->e_shstrndx = 0;
phdr->p_type = PT_LOAD;
phdr->p_offset = payload_offset;
phdr->p_vaddr = DEMO_SEGMENT_VA;
phdr->p_paddr = DEMO_SEGMENT_VA;
phdr->p_filesz = DEMO_IMAGE_SIZE;
phdr->p_memsz = DEMO_IMAGE_SIZE + DEMO_BSS_SIZE;
phdr->p_flags = PF_R | PF_W | PF_X;
phdr->p_align = PMM_PAGE_SIZE;
build_stdio_demo_payload(
image + payload_offset,
DEMO_IMAGE_SIZE
);
return total_size;
}
Finally, replace process_test_once().
void process_test_once(void) {
console_writeln("Process test: starting ELF32 user program test");
last_exit_seen = 0;
last_exit_code = 0xFFFFFFFFu;
static uint8_t elf_image[
sizeof(elf32_ehdr_t) +
sizeof(elf32_phdr_t) +
DEMO_IMAGE_SIZE
];
uint32_t elf_size = build_stdio_demo_elf(
elf_image,
sizeof(elf_image)
);
process_t *process = elf_create_process(
"elf-demo",
elf_image,
elf_size
);
/*
* Let the user process start, print its prompt, and block in SYS_READ.
*/
thread_sleep_ticks(2);
keyboard_debug_inject_char('t');
keyboard_debug_inject_char('o');
keyboard_debug_inject_char('y');
keyboard_debug_inject_char('i');
keyboard_debug_inject_char('x');
keyboard_debug_inject_char('\n');
uint32_t exit_code = process_wait(process);
if (exit_code != 9) {
kernel_panic("ELF process test received wrong exit code");
}
process_destroy(process);
console_writeln("Process test: ELF32 load/read/write/sleep/exit cleanup sanity check passed");
}
8. Why we still build the ELF image in C
This chapter is the loader milestone, not the userland toolchain milestone.
The image is now a real ELF-shaped byte array:
ELF header program header PT_LOAD payload
But the payload is still generated by our tiny machine-code emitter.
That keeps the chapter focused.
The next step can introduce:
user programs compiled with i686-elf-gcc linker script for user virtual address objcopy/incbin embedding kernel loading compiled ELF bytes
That will remove the emitter.
9. Update Makefile
Remove this object if it is still present:
build/kernel/toyexe.o
Add:
build/kernel/elf_loader.o
The relevant object list becomes:
OBJS := \
build/arch/x86/boot.o \
build/arch/x86/gdt.o \
build/arch/x86/gdt_flush.o \
build/arch/x86/idt.o \
build/arch/x86/interrupts.o \
build/arch/x86/isr.o \
build/arch/x86/irq.o \
build/arch/x86/paging.o \
build/arch/x86/pic.o \
build/arch/x86/pit.o \
build/arch/x86/sched_interrupt.o \
build/arch/x86/syscall.o \
build/arch/x86/user_enter.o \
build/arch/x86/vmm.o \
build/kernel/address_space.o \
build/kernel/elf_loader.o \
build/kernel/kmain.o \
build/kernel/console.o \
build/kernel/heap.o \
build/kernel/monitor.o \
build/kernel/panic.o \
build/kernel/pmm.o \
build/kernel/process.o \
build/kernel/sync.o \
build/kernel/syscall.o \
build/kernel/terminal.o \
build/kernel/thread.o \
build/kernel/usercopy.o \
build/kernel/usermode.o \
build/kernel/vmem.o \
build/kernel/wait_queue.o \
build/kernel/lib/mem.o \
build/drivers/console/serial.o \
build/drivers/console/vga_text.o \
build/drivers/input/keyboard.o
Update the process-related greps.
Replace the Chapter 21 TOYEXE greps:
grep -q "Process test: starting TOYEXE lifecycle cleanup test" build/test.log grep -q "TOYEXE: loaded image bytes=256 bss=64" build/test.log grep -q "Process: created pid=1 name=toyexe-demo" build/test.log grep -q "Syscall: process toyexe-demo pid=1 exited code 9" build/test.log grep -q "Process: destroyed pid=1 name=toyexe-demo" build/test.log grep -q "Process test: TOYEXE lifecycle cleanup sanity check passed" build/test.log
with:
grep -q "Process test: starting ELF32 user program test" build/test.log grep -q "ELF32: loaded PT_LOAD vaddr=0x40100000 filesz=256 memsz=320" build/test.log grep -q "ELF32: entry=0x40100000" build/test.log grep -q "Process: created pid=1 name=elf-demo" build/test.log grep -q "echo: toyix" build/test.log grep -q "Syscall: process elf-demo pid=1 exited code 9" build/test.log grep -q "Process: destroyed pid=1 name=elf-demo" build/test.log grep -q "Process test: ELF32 load/read/write/sleep/exit cleanup sanity check passed" build/test.log
The process block should now be:
grep -q "Address space: kernel address space registered" build/test.log
grep -q "Process: process table initialized" build/test.log
grep -q "Process test: starting ELF32 user program test" build/test.log
grep -q "Address space: created process page directory" build/test.log
grep -q "ELF32: loaded PT_LOAD vaddr=0x40100000 filesz=256 memsz=320" build/test.log
grep -q "ELF32: entry=0x40100000" build/test.log
grep -q "Process: created pid=1 name=elf-demo" build/test.log
grep -q "echo: toyix" build/test.log
grep -q "Syscall: process elf-demo pid=1 exited code 9" build/test.log
grep -q "Address space: destroyed process page directory" build/test.log
grep -q "Process: destroyed pid=1 name=elf-demo" build/test.log
grep -q "Process test: ELF32 load/read/write/sleep/exit cleanup sanity check passed" build/test.log
Update the final success line:
@echo "Boot, memory, heap, sync, monitor, address-space, and ELF32 loader smoke test passed."
10. Update tests/smoke.sh
No structural change is needed.
#!/usr/bin/env bash set -euo pipefail make clean make test make test-exception make test-page-fault echo "All Chapter 22 checks passed."
11. Expected output
A successful boot should include:
Process test: starting ELF32 user program test Address space: created process page directory ELF32: loaded PT_LOAD vaddr=0x40100000 filesz=256 memsz=320 ELF32: entry=0x40100000 Thread: created elf-demo id=... Process: created pid=1 name=elf-demo user> toyix echo: toyix Syscall: process elf-demo pid=1 exited code 9 Threads: reaping zombie elf-demo id=... Address space: destroyed process page directory, user pages=2 tables=2 Process: destroyed pid=1 name=elf-demo Process test: ELF32 load/read/write/sleep/exit cleanup sanity check passed
This proves:
ELF magic validated ELF class/data/type/machine validated program header table parsed PT_LOAD segment mapped segment file bytes copied segment BSS zeroed entry point used stack mapped user process ran syscalls worked process cleaned up
That is the first real ELF loader milestone.
12. Common failures
Failure: ELF32 process load failed
Print the return code from elf_load_process().
Most likely causes:
bad ELF magic wrong e_ehsize wrong e_phentsize e_phoff points outside file e_phnum is zero PT_LOAD p_offset + p_filesz exceeds file size entry address outside user range
Check these fields in the builder:
ehdr->e_phoff = sizeof(elf32_ehdr_t); ehdr->e_phentsize = sizeof(elf32_phdr_t); ehdr->e_phnum = 1;
Failure: unsupported segment
This first loader requires page-aligned p_vaddr:
if ((program_header->p_vaddr & (PMM_PAGE_SIZE - 1u)) != 0) {
return ELF_LOADER_ERR_UNSUPPORTED;
}
The demo uses:
#define DEMO_SEGMENT_VA 0x40100000u
which is page-aligned.
Failure: page fault at the input buffer
The input buffer is in BSS:
DEMO_INPUT_VA = DEMO_SEGMENT_VA + DEMO_IMAGE_SIZE
That means the loader must map p_memsz, not only p_filesz.
The demo segment uses:
filesz = 256 memsz = 320
If only 256 bytes are mapped, the BSS input buffer may fault or fail copy_to_user().
Failure: echo: toyix missing
Likely causes:
SYS_READ did not copy into user buffer BSS was not mapped or zeroed keyboard injection did not include newline process address space was not active during syscall
Check:
keyboard_debug_inject_char('\n');
and:
process_zero_user_init(process, bss_start, bss_size);
Failure: process destroy frees unexpected page/table counts
The current demo should usually free:
user pages=2 tables=2
One page is the loaded ELF segment. One page is the user stack.
There are two tables because:
0x40100000 and 0x6FFFF000
normally live under different page-directory entries.
If your counts differ, check whether stack top, segment address, or mapping strategy changed.
13. What this chapter achieved
Before this chapter:
temporary TOYEXE format custom header toyexe loader
After this chapter:
ELF32 header program header table PT_LOAD segment loading entry point BSS zeroing user stack process cleanup
The executable loader boundary is now much closer to a real OS.
14. Design limitations
This is still only the first ELF milestone.
Current limitations:
only ELFCLASS32 only little-endian only ET_EXEC only EM_386 only simple PT_LOAD segments requires page-aligned p_vaddr does not enforce read-only text does not support multiple overlapping segments does not support relocations does not support ET_DYN does not load from filesystem does not pass argc/argv/envp does not build a user stack ABI
But this is a strong step.
The kernel now understands the structure of a real executable file.
15. Commit this chapter
After tests pass:
git status git add . git commit -m "Add initial ELF32 user process loader"
16. Next chapter
The next chapter should remove the last major artificial piece: the in-kernel machine-code emitter.
We should add a tiny user-program build pipeline:
user/ ├── include/toyix_syscall.h ├── crt0.S ├── demo.c └── linker.ld
Then the build can:
compile user/demo.c to ELF32 convert the ELF into an object or C array link it into the kernel load it through elf_loader.c
That gives us:
real compiled user C program real ELF file kernel ELF loader syscalls from C wrappers
That is the bridge from “toy bytecode inside the kernel” to actual userland.
17. Resources
- Chapter 22 source release
- Chapter 22 repository tree
- Linux Foundation Specs: ELF and ABI Standards
- System V ABI Intel386 Supplement
- Linux Foundation Specs: Program Header
18. Closure
Chapter 22 moves Toyix from a custom tutorial executable format to the first real ELF32 loader milestone. The kernel now validates ELF headers, walks program headers, maps a PT_LOAD segment, zeros BSS, enters user mode through the ELF entry point, and tears the process down cleanly after exit.
Happy Coding!