Skip to content

Experimenting with TilekarOS

TilekarOS is designed to be a playground for learning OS internals. This guide shows you how to "mess" with the system, create tasks, and intentionally trigger failures to see how the kernel responds.

1. Creating Kernel Tasks (Ring 0)

Kernel tasks run with full privileges. They are easy to create and can access any part of the hardware.

How to add a task:

  1. Define a function in kernel/kernel.c:
    void my_experiment_task() {
        while (true) {
            printf("[Experiment] Running...\n");
            for (volatile int i = 0; i < 1000000; i++); // Busy wait
            task_yield(NULL); // Give up CPU
        }
    }
    
  2. Register it in kernel_main():
    task_create(my_experiment_task, 0);
    

Voluntary vs. Preemptive:

  • task_yield(NULL): Manually tells the scheduler to pick the next task.
  • Preemption: If you don't call yield, the Timer Interrupt (IRQ 0) will eventually force a context switch after its time slice.

2. Creating User Tasks (Ring 3)

User tasks are isolated and run in their own virtual address space. They cannot access kernel memory or execute privileged instructions.

The Lifecycle of a User Task

In TilekarOS, a user task is typically defined as a block of code (assembly or C) that is copied into a new address space.

Step 1: Define the User Code

User tasks use System Calls to interact with the kernel. See user_process.asm for a complete example of a user task using syscalls to list directories.

Code Preview: user_process.asm
[BITS 32]

section .text
global _start_user_task
global _end_user_task

_start_user_task:
    ; 1. Create directory /TESTDIR
    mov eax, 6          ; SYS_MKDIR
    call .get_dir_path
    db "/TESTDIR", 0
.get_dir_path:
    pop ebx
    int 0x80

    ; 2. Open root directory /
    mov eax, 3          ; SYS_OPEN
    call .get_root_path
    db "/", 0
.get_root_path:
    pop ebx
    mov ecx, 0
    int 0x80
    push eax            ; Save root FD

    ; 3. List root directory
    mov eax, 1          ; SYS_WRITE
    mov ebx, 1
    call .get_msg_list_root
    db "Root Directory Listing:", 10, 0
.get_msg_list_root:
    pop ecx
    mov edx, 24
    int 0x80

    sub esp, 264        ; vfs_dirent_t buffer
    mov edi, 0          ; index
.loop_root:
    mov eax, 9          ; SYS_READDIR
    mov ebx, [esp + 264] ; root FD
    mov ecx, edi        ; index
    mov edx, esp        ; out buffer
    int 0x80
    cmp eax, 0
    jne .done_root

    ; Print entry name
    mov eax, 1          ; SYS_WRITE
    mov ebx, 1
    mov ecx, esp        ; dirent->name
    mov edx, 256        ; max name len
    int 0x80

    mov eax, 1          ; SYS_WRITE
    mov ebx, 1
    call .get_newline
    db 10
.get_newline:
    pop ecx
    mov edx, 1
    int 0x80

    inc edi
    jmp .loop_root
.done_root:
    add esp, 264

    ; 4. Close root FD
    mov eax, 5          ; SYS_CLOSE
    pop ebx
    int 0x80

    ; 5. Delete directory /TESTDIR (using unlink for now as rmdir is alias)
    ; Actually, let's just exit
    mov eax, 0          ; SYS_EXIT
    int 0x80

_end_user_task:

Step 2: Launch the Task

In kernel_main, use the extern symbols to pass the code block to task_create_user:

extern char _start_user_task;
extern char _end_user_task;

task_create_user(&_start_user_task, &_end_user_task);

What happens under the hood?

  1. Memory Mapping: The code block is copied to a new page directory starting at 0x08048000.
  2. Stack Setup: A user-mode stack is automatically mapped at 0xB0000000.
  3. Ring Transition: The kernel uses the iret instruction to drop the CPU's privilege level to Ring 3.
  4. TSS Update: The Task State Segment (TSS) is updated with the task's kernel stack pointer (esp0) so the CPU can transition back to Ring 0 during interrupts or syscalls.

3. How to Make Things Fail

Testing the kernel's robustness is part of the fun. Here is how you can trigger common CPU exceptions.

A. Division by Zero (ISR 0)

Add this to any task:

volatile int a = 5;
volatile int b = 0;
volatile int c = a / b;

B. Page Fault (ISR 14)

  • From Kernel: *(int*)0x0 = 123; (Accessing NULL).
  • From User: Trying to read kernel memory (addresses above 0xC0000000).

C. General Protection Fault (ISR 13)

Triggered in User Mode if you try to execute privileged instructions:

// Inside a User Task
asm volatile("hlt"); // Only allowed in Ring 0


4. Debugging Tips

Headless Logging (Serial COM1)

TilekarOS mirrors all printf output to the serial port 0x3F8. Add -serial stdio to QEMU to see these logs in your terminal.


References