Multitasking and Executables¶
TilekarOS supports Preemptive Multitasking for both Ring 0 (Kernel) and Ring 3 (User) tasks. This guide details how tasks are created, scheduled, and how ELF binaries are loaded.
1. Task Management¶
Each process in TilekarOS is represented by a task_t structure, also known as a Task Control Block (TCB).
Task State Machine¶
stateDiagram-v2
[*] --> CREATED: task_create()
CREATED --> READY: Added to Queue
READY --> RUNNING: Scheduler Picks
RUNNING --> READY: Timer Interrupt (Preemption)
RUNNING --> READY: task_yield()
RUNNING --> ZOMBIE: task_exit()
ZOMBIE --> [*]: Cleanup by Next Task
Task Control Block (TCB) Structure¶
classDiagram
class task_t {
+uint32_t id
+uint32_t esp
+uint32_t ebp
+uint32_t eip
+page_directory_t* page_dir
+void* stack_base
+task_state_t state
+int priority
+struct task_t* next
}
Task Lifecycle:¶
- Creation:
task_create()allocates atask_tand a 4KB kernel stack. - Ready: The task is added to a Round-Robin circular queue.
- Running: The scheduler picks the next task and performs a context switch.
- Exiting:
task_exit()marks the task as aZOMBIE. - Cleanup: The next task to run cleans up the zombie's memory.
See task.c for the full state management logic.
2. Context Switching¶
Source File: task.asm
Context switching is the "magic" that allows multiple tasks to share one CPU. It happens in three scenarios:
- Timer Interrupt: Preempts a task after 100ms.
- task_yield(): A task voluntarily gives up the CPU.
- task_exit(): A task finishes its work.
Assembly Breakdown (context_switch):¶
The context_switch function saves the current CPU state onto the task's stack and restores the next task's state.
Code Preview: task.asm
[bits 32]
struc registers_t
.gs: resd 1
.fs: resd 1
.es: resd 1
.ds: resd 1
.edi: resd 1
.esi: resd 1
.ebp: resd 1
.esp: resd 1
.ebx: resd 1
.edx: resd 1
.ecx: resd 1
.eax: resd 1
.eip: resd 1
.cs: resd 1
.eflags: resd 1
endstruc
section .text
global context_switch
global get_cr3
; get_cr3() -> returns the value of CR3 register
get_cr3:
mov eax, cr3
ret
; context_switch(uint32_t** current_esp, uint32_t* next_esp, uint32_t next_cr3, uint32_t intr_num)
context_switch:
; The stack initially has the return EIP at [esp].
; We need to simulate an interrupt frame so we can return via iret.
; Pop the return EIP so we can push it properly with CS and EFLAGS.
pop eax ; EAX = Return EIP
; Push standard interrupt frame (EFLAGS, CS, EIP)
pushfd ; Push EFLAGS
push cs ; Push CS
push eax ; Push EIP
; Save general purpose registers (pushad)
pushad
; Save segment registers
push ds
push es
push fs
push gs
; Stack layout analysis:
; Before pop eax:
; [esp+0] = Return EIP
; [esp+4] = current_esp (arg1)
; [esp+8] = next_esp (arg2)
; [esp+12] = next_cr3 (arg3)
; [esp+16] = intr_num (arg4)
; After pop eax:
; [esp+0] = current_esp (arg1)
; [esp+4] = next_esp (arg2)
; [esp+8] = next_cr3 (arg3)
; [esp+12] = intr_num (arg4)
; After pushing 15 dwords (60 bytes) - EFLAGS(1), CS(1), EIP(1), pushad(8), segs(4):
; [esp+registers_t_size] = current_esp (arg1)
; [esp+registers_t_size+4] = next_esp (arg2)
; [esp+registers_t_size+8] = next_cr3 (arg3)
; [esp+registers_t_size+12] = intr_num (arg4)
mov eax, [esp + registers_t_size] ; current_esp pointer (arg1)
; If the current_esp pointer is NULL (dummy), don't save ESP
test eax, eax
jz .skip_save
mov [eax], esp ; Save current ESP into current task's kernel_stack
.skip_save:
; Load next CR3
mov edx, [esp + registers_t_size + 8] ; next_cr3 (arg3)
; Read current cr3 to see if we actually need to change it
; Avoiding unnecessary CR3 loads prevents TLB flushes
mov eax, cr3
cmp eax, edx
je .skip_cr3_load
mov cr3, edx ; Load new page directory
.skip_cr3_load:
; Read intr_num (arg4) before switching the stack pointer
mov ebx, [esp + registers_t_size + 12]
; Load next task's kernel_stack into ESP
mov esp, [esp + registers_t_size + 4]
; Send EOI if intr_num >= 32
cmp ebx, 32
jl .no_eoi
; We are safe to call pic_send_eoi because popad hasn't happened yet!
; Any clobbered EAX, ECX, EDX will be safely overwritten by the upcoming popad.
push ebx ; Argument intr_num
extern pic_send_eoi
call pic_send_eoi
add esp, 4
.no_eoi:
; Restore segment registers for the next task
pop gs
pop fs
pop es
pop ds
; Restore general purpose registers for the next task
popad
; Return to the next task using iret!
; This will pop EIP, CS, and EFLAGS from the next task's stack.
iret
| Stack Level | Saved Data |
|---|---|
| High Memory | EFLAGS, CS, EIP (Interrupt Frame) |
| General Purpose (EAX, EBX...) | |
| Low Memory | Segment Registers (DS, ES, FS, GS) |
3. ELF Loader¶
TilekarOS can load and execute ELF32 (Executable and Linkable Format) binaries.
How it works:¶
- Header Check:
elf_check_supported()verifies the magic number (0x7F 'E' 'L' 'F') and architecture. - Segment Mapping:
elf_load_segments()iterates through the Program Headers and mapsPT_LOADsegments into the task's address space. - Execution: The kernel jumps to the
e_entryaddress specified in the ELF header.
Refer to elf.c for the loading implementation.
Security: Ring 3 Transition
When launching a user-mode task, the kernel uses the iret instruction to "fake" an interrupt return, dropping the CPU's privilege from Ring 0 to Ring 3.
4. Test/Example: Creating a Background Task¶
This C snippet shows how to spawn a kernel task that runs in the background while the main kernel continues:
void background_worker() {
while (1) {
printf("[Worker] Doing periodic maintenance...\n");
task_yield(NULL); // Give other tasks a turn
}
}
void start_demo() {
task_create(background_worker, 0); // 0 = Kernel Privilege
}
5. Guide: ELF User Task Integration¶
This guide explains how to compile a test user process from assembly to an ELF file, embed it into the kernel, and load it.
Step 1: Create the User Task Assembly (user_task.asm.ignore)¶
[bits 32]
section .text
global _start
_start:
; A simple infinite loop for the test user process
mov eax, 0xCAFEBABE
.loop:
jmp .loop
Step 2: Compile and Link to ELF¶
nasm -f elf32 kernel/user_task.asm.ignore -o kernel/user_task.o
clang -target i386-pc-none-elf -march=i386 -nostdlib -static -Wl,-e,_start kernel/user_task.o -o kernel/user_task.elf
Step 3: Embed the ELF in the Kernel (user_process.asm)¶
Create a bridge assembly file to include the ELF binary.
[bits 32]
section .rodata
global _start_elf_user_task
global _end_elf_user_task
_start_elf_user_task:
incbin "/path/to/TilekarOS/kernel/user_task.elf"
_end_elf_user_task:
Step 4: Load the ELF Task in the Kernel (kernel.c)¶
extern char _start_elf_user_task;
extern char _end_elf_user_task;
// Inside kernel_main
uint32_t elf_size = (uint32_t)(&_end_elf_user_task - &_start_elf_user_task);
task_create_elf(&_start_elf_user_task, 3); // 3 for user mode, 0 for kernel mode