- Writing a Linux-style Operating System From Scratch
- Chapter 2 — GDT, IDT, and Surviving Your First Kernel Crash
- Chapter 3 — Hardware Interrupts: PIC, PIT Timer, and Keyboard Input
- Chapter 4 — Reading the Memory Map and Building a Physical Page Allocator
- Chapter 5 — Turning On Paging
- Chapter 6 — Building the First Kernel Heap
- Chapter 7 — A Real Virtual Memory Mapping Layer
- Chapter 8 – Moving the Heap onto Virtual Memory
- Chapter 9 — Cooperative Multitasking and Kernel Threads
- Chapter 10 — Timer-Driven Preemptive Multitasking
- Chapter 11 — Blocking Primitives, Sleep Queues, and Scheduler Hygiene
- Chapter 12 – Wait Queues and Blocking Keyboard Input
- Chapter 13 — Mutexes, Semaphores, and a Console Lock
- Chapter 14 — Terminal Line Discipline and a Kernel Monitor
- Chapter 15 — Command Tables, Argument Parsing, and Shift-Aware Keyboard Input
- Chapter 16 — Entering User Mode and Returning Through Syscalls
- Chapter 17 — Minimal Processes, User Memory Copying, and More Robust Syscalls
Post Stastics
- This post has 3865 words.
- Estimated read time is 18.40 minute(s).
In Chapter 16, we proved the biggest privilege milestone so far:
ring 0 kernel ↓ IRET into ring 3 ↓ user code executes ↓ int 0x80 ↓ kernel syscall handler
But the user-mode test was still mostly a raw thread experiment. The kernel did not yet have a real object representing a user program.
This chapter adds the first process abstraction.
A process will own:
PID name main thread exit state exit code user code mapping user stack mapping
We will also add safer user-memory access helpers:
copy_from_user() copy_to_user() user_string_length()
and improve syscalls from:
SYS_PUTC SYS_EXIT
to:
SYS_WRITE SYS_SLEEP SYS_EXIT
Intel’s manuals describe the IA-32 operating-system environment, including memory management, protection, task management, and interrupt/exception handling. Those are the architectural foundations behind user/supervisor page checks, privilege transitions, and syscall entry through an interrupt gate. (Intel)
1. What this chapter adds
Add:
include/kernel/ ├── process.h └── usercopy.h kernel/ ├── process.c └── usercopy.c
Modify:
arch/x86/vmm.h arch/x86/vmm.c include/kernel/vmem.h kernel/vmem.c include/kernel/thread.h kernel/thread.c include/kernel/syscall.h kernel/syscall.c include/kernel/usermode.h kernel/usermode.c kernel/kmain.c Makefile tests/smoke.sh
The new milestone output will look like:
Process test: starting user process syscall test Process: created pid=1 name=user-demo User process says hello through SYS_WRITE Syscall: process user-demo pid=1 exited code 7 Process test: user process syscall/write/sleep/exit sanity check passed
2. Why introduce process_t now?
A thread is an execution stream.
A process is an ownership container.
Right now the difference is small, but it will become critical.
A future process will own:
address space threads file descriptor table current directory signals/events exit status credentials resource limits
For this tutorial kernel, we start small:
process_t owns one user thread and a few user pages
That gives us a place to attach user-mode state without stuffing everything into thread_t.
3. Add include/kernel/process.h
// include/kernel/process.h
#ifndef TOYIX_KERNEL_PROCESS_H
#define TOYIX_KERNEL_PROCESS_H
#include <stdint.h>
struct thread;
typedef enum process_state {
PROCESS_NEW = 0,
PROCESS_RUNNING,
PROCESS_EXITED
} process_state_t;
typedef struct process {
uint32_t magic;
uint32_t pid;
const char *name;
process_state_t state;
struct thread *main_thread;
uint32_t exit_code;
int exited;
uintptr_t user_code_base;
uintptr_t user_stack_base;
uintptr_t user_stack_top;
} process_t;
void process_init_system(void);
process_t *process_create_user(
const char *name,
const uint8_t *program,
uint32_t program_size
);
process_t *process_current(void);
void process_exit_current(uint32_t exit_code);
uint32_t process_last_exit_code(void);
int process_last_exit_seen(void);
void process_test_once(void);
#endif
Why only one thread per process for now?
Because one thread is enough to prove:
process object exists thread is attached to process syscalls can identify current process process exit status is recorded
Multi-threaded processes can come later.
4. Update include/kernel/thread.h
Add a forward declaration and process pointer.
Near the top:
struct process;
Inside thread_t, add:
struct process *process;
Add these declarations:
void thread_set_process(thread_t *thread, struct process *process); struct process *thread_get_process(thread_t *thread);
Here is the relevant updated portion:
struct process;
typedef struct thread {
uint32_t magic;
uint32_t id;
const char *name;
thread_state_t state;
thread_context_t context;
void *stack_base;
uint32_t stack_size;
thread_entry_t entry;
void *arg;
struct process *process;
uint32_t wake_tick;
struct thread *next;
struct thread *prev;
} thread_t;
5. Update kernel/thread.c
Whenever a thread is initialized, set:
thread->process = 0;
In thread_create_internal(), add:
thread->process = 0;
In threading_init(), for the bootstrap thread:
bootstrap_thread.process = 0;
Then add these functions near thread_current():
void thread_set_process(thread_t *thread, struct process *process) {
validate_thread(thread);
thread->process = process;
}
struct process *thread_get_process(thread_t *thread) {
validate_thread(thread);
return thread->process;
}
Why store process on the thread?
When a syscall runs, the kernel can find the process through:
thread_current()->process
That lets the syscall layer know who called it.
Later, when we have many processes and many threads, this relationship becomes foundational.
6. Update arch/x86/vmm.h
Add a function to retrieve raw page flags.
// arch/x86/vmm.h
#ifndef TOYIX_ARCH_X86_VMM_H
#define TOYIX_ARCH_X86_VMM_H
#include <stdint.h>
#define VMM_OK 0
#define VMM_ERR_INVALID -1
#define VMM_ERR_NO_MEMORY -2
#define VMM_ERR_ALREADY_MAPPED -3
#define VMM_ERR_NOT_MAPPED -4
void vmm_init(void);
int vmm_map_page(
uintptr_t virtual_addr,
uintptr_t physical_addr,
uint32_t flags
);
int vmm_unmap_page(uintptr_t virtual_addr);
uintptr_t vmm_get_physical(uintptr_t virtual_addr);
uint32_t vmm_get_flags(uintptr_t virtual_addr);
void vmm_test_once(void);
#endif
7. Update arch/x86/vmm.c
Add this function after vmm_get_physical().
uint32_t vmm_get_flags(uintptr_t virtual_addr) {
uint32_t dir = directory_index(virtual_addr);
uint32_t tab = table_index(virtual_addr);
page_directory_entry_t pde = kernel_directory[dir];
if ((pde & X86_PAGE_PRESENT) == 0) {
return 0;
}
page_table_entry_t *table = table_from_pde(pde);
page_table_entry_t pte = table[tab];
if ((pte & X86_PAGE_PRESENT) == 0) {
return 0;
}
return pte & X86_PAGE_FLAGS_MASK;
}
Why flags matter
vmm_get_physical() tells us whether an address is mapped.
But for user-copy safety, we also need to know whether the page is mapped as user-accessible:
page present? page user-accessible?
The Intel architecture uses page-table permission bits as part of memory protection; Volume 3 documents the operating-system support environment where paging and protection are enforced. (Intel)
8. Update include/kernel/vmem.h
Add:
uint32_t vmem_get_flags(uintptr_t virtual_addr); int vmem_is_user_accessible(uintptr_t virtual_addr);
Full updated header:
// include/kernel/vmem.h
#ifndef TOYIX_KERNEL_VMEM_H
#define TOYIX_KERNEL_VMEM_H
#include <stdint.h>
#define VMEM_OK 0
#define VMEM_ERR_INVALID -1
#define VMEM_ERR_NO_MEMORY -2
#define VMEM_ERR_ALREADY_MAPPED -3
#define VMEM_ERR_NOT_MAPPED -4
#define VMEM_FLAG_WRITABLE 0x00000001u
#define VMEM_FLAG_USER 0x00000002u
void vmem_init(void);
int vmem_map_page(
uintptr_t virtual_addr,
uintptr_t physical_addr,
uint32_t flags
);
int vmem_unmap_page(uintptr_t virtual_addr);
uintptr_t vmem_get_physical(uintptr_t virtual_addr);
uint32_t vmem_get_flags(uintptr_t virtual_addr);
int vmem_is_user_accessible(uintptr_t virtual_addr);
void vmem_test_once(void);
#endif
9. Update kernel/vmem.c
Add this helper:
static uint32_t vmem_from_arch_flags(uint32_t arch_flags) {
uint32_t flags = 0;
if ((arch_flags & X86_PAGE_WRITABLE) != 0) {
flags |= VMEM_FLAG_WRITABLE;
}
if ((arch_flags & X86_PAGE_USER) != 0) {
flags |= VMEM_FLAG_USER;
}
return flags;
}
Then add:
uint32_t vmem_get_flags(uintptr_t virtual_addr) {
return vmem_from_arch_flags(vmm_get_flags(virtual_addr));
}
int vmem_is_user_accessible(uintptr_t virtual_addr) {
return (vmem_get_flags(virtual_addr) & VMEM_FLAG_USER) != 0;
}
Why translate flags here?
The process and syscall layers should not know that x86 calls the user page bit:
X86_PAGE_USER
They should only know the generic kernel meaning:
VMEM_FLAG_USER
This keeps the architecture boundary cleaner.
10. Add include/kernel/usercopy.h
// include/kernel/usercopy.h
#ifndef TOYIX_KERNEL_USERCOPY_H
#define TOYIX_KERNEL_USERCOPY_H
#include <stddef.h>
#include <stdint.h>
#define USERCOPY_OK 0
#define USERCOPY_ERR_FAULT -1
#define USERCOPY_ERR_TOO_LONG -2
int copy_from_user(void *kernel_dest, uintptr_t user_src, size_t size);
int copy_to_user(uintptr_t user_dest, const void *kernel_src, size_t size);
int user_string_length(
uintptr_t user_str,
size_t max_len,
size_t *length_out
);
#endif
11. Add kernel/usercopy.c
// kernel/usercopy.c
#include <stddef.h>
#include <stdint.h>
#include "kernel/string.h"
#include "kernel/usercopy.h"
#include "kernel/vmem.h"
static int user_range_accessible(uintptr_t user_addr, size_t size) {
if (size == 0) {
return 1;
}
if (user_addr + size < user_addr) {
return 0;
}
uintptr_t start = user_addr;
uintptr_t end = user_addr + size - 1u;
uintptr_t page = start & ~(uintptr_t)0xFFFu;
while (page <= end) {
if (!vmem_is_user_accessible(page)) {
return 0;
}
if (page > 0xFFFFFFFFu - 0x1000u) {
break;
}
page += 0x1000u;
}
return 1;
}
int copy_from_user(void *kernel_dest, uintptr_t user_src, size_t size) {
if (kernel_dest == 0 && size != 0) {
return USERCOPY_ERR_FAULT;
}
if (!user_range_accessible(user_src, size)) {
return USERCOPY_ERR_FAULT;
}
memcpy(kernel_dest, (const void *)user_src, size);
return USERCOPY_OK;
}
int copy_to_user(uintptr_t user_dest, const void *kernel_src, size_t size) {
if (kernel_src == 0 && size != 0) {
return USERCOPY_ERR_FAULT;
}
if (!user_range_accessible(user_dest, size)) {
return USERCOPY_ERR_FAULT;
}
memcpy((void *)user_dest, kernel_src, size);
return USERCOPY_OK;
}
int user_string_length(
uintptr_t user_str,
size_t max_len,
size_t *length_out
) {
if (length_out == 0) {
return USERCOPY_ERR_FAULT;
}
for (size_t i = 0; i < max_len; ++i) {
char ch;
if (copy_from_user(&ch, user_str + i, 1) != USERCOPY_OK) {
return USERCOPY_ERR_FAULT;
}
if (ch == '\0') {
*length_out = i;
return USERCOPY_OK;
}
}
return USERCOPY_ERR_TOO_LONG;
}
Why copy helpers matter
A syscall handler must not blindly trust a user pointer.
Bad user code may pass:
0x00000000 kernel address unmapped address string without terminator buffer crossing into unmapped memory
copy_from_user() and copy_to_user() are the boundary checks that keep syscall code from becoming a crash machine.
This is not fully hardened yet, but it establishes the right pattern.
12. Update include/kernel/syscall.h
Replace it with:
// include/kernel/syscall.h #ifndef TOYIX_KERNEL_SYSCALL_H #define TOYIX_KERNEL_SYSCALL_H #include "arch/x86/interrupts.h" #define SYS_PUTC 1u #define SYS_EXIT 2u #define SYS_WRITE 3u #define SYS_SLEEP 4u void syscall_handler(interrupt_frame_t *frame); #endif
We keep SYS_PUTC for compatibility with the Chapter 16 test, but the new user process will use SYS_WRITE.
13. Replace kernel/syscall.c
// kernel/syscall.c
#include <stdint.h>
#include "kernel/console.h"
#include "kernel/process.h"
#include "kernel/syscall.h"
#include "kernel/thread.h"
#include "kernel/usercopy.h"
#define SYSCALL_WRITE_MAX 256u
static void syscall_write(interrupt_frame_t *frame) {
uintptr_t user_buf = (uintptr_t)frame->ebx;
uint32_t length = frame->ecx;
if (length > SYSCALL_WRITE_MAX) {
length = SYSCALL_WRITE_MAX;
}
char buffer[SYSCALL_WRITE_MAX + 1u];
if (copy_from_user(buffer, user_buf, length) != USERCOPY_OK) {
frame->eax = 0xFFFFFFFFu;
return;
}
buffer[length] = '\0';
console_write(buffer);
frame->eax = length;
}
void syscall_handler(interrupt_frame_t *frame) {
if (frame == 0) {
return;
}
uint32_t number = frame->eax;
switch (number) {
case SYS_PUTC: {
char ch = (char)(frame->ebx & 0xFFu);
console_putc(ch);
frame->eax = 0;
return;
}
case SYS_WRITE:
syscall_write(frame);
return;
case SYS_SLEEP: {
uint32_t ticks = frame->ebx;
interrupts_enable();
thread_sleep_ticks(ticks);
frame->eax = 0;
return;
}
case SYS_EXIT: {
uint32_t exit_code = frame->ebx;
process_exit_current(exit_code);
thread_exit();
return;
}
default:
console_write("Syscall: unknown syscall ");
console_write_u32_dec(number);
console_putc('\n');
frame->eax = 0xFFFFFFFFu;
return;
}
}
Why SYS_SLEEP is interesting
SYS_SLEEP proves user code can call into the kernel and block the current user process thread.
That means:
user mode ↓ syscall kernel scheduler blocks thread ↓ timer wakes it return to user mode
This is a real OS behavior.
14. Replace include/kernel/usermode.h
// include/kernel/usermode.h #ifndef TOYIX_KERNEL_USERMODE_H #define TOYIX_KERNEL_USERMODE_H #include <stdint.h> void usermode_enter_current_process(void); #endif
15. Replace kernel/usermode.c
// kernel/usermode.c
#include <stdint.h>
#include "arch/x86/gdt.h"
#include "kernel/panic.h"
#include "kernel/process.h"
#include "kernel/thread.h"
#include "kernel/usermode.h"
extern void x86_enter_user_mode(uint32_t user_eip, uint32_t user_esp);
static uint32_t current_thread_kernel_stack_top(void) {
thread_t *self = thread_current();
if (self == 0 || self->stack_base == 0) {
kernel_panic("user mode entry requires current thread stack");
}
return (uint32_t)((uintptr_t)self->stack_base + self->stack_size);
}
void usermode_enter_current_process(void) {
process_t *process = process_current();
if (process == 0) {
kernel_panic("usermode entry without process");
}
tss_set_kernel_stack(current_thread_kernel_stack_top());
x86_enter_user_mode(
(uint32_t)process->user_code_base,
(uint32_t)process->user_stack_top
);
kernel_panic("x86_enter_user_mode returned unexpectedly");
}
16. Add kernel/process.c
// kernel/process.c
#include <stddef.h>
#include <stdint.h>
#include "kernel/console.h"
#include "kernel/heap.h"
#include "kernel/panic.h"
#include "kernel/pmm.h"
#include "kernel/process.h"
#include "kernel/string.h"
#include "kernel/syscall.h"
#include "kernel/thread.h"
#include "kernel/usermode.h"
#include "kernel/vmem.h"
#define PROCESS_MAGIC 0x50524F43u
#define USER_PROCESS_CODE_VA 0x40100000u
#define USER_PROCESS_STACK_VA 0x40101000u
#define USER_PROCESS_STACK_TOP 0x40102000u
static uint32_t next_pid;
static volatile uint32_t last_exit_code;
static volatile int last_exit_seen;
static void user_process_thread_entry(void *arg);
void process_init_system(void) {
next_pid = 1;
last_exit_code = 0xFFFFFFFFu;
last_exit_seen = 0;
console_writeln("Process: process table initialized");
}
static void map_user_page(uintptr_t virtual_addr) {
uintptr_t physical = pmm_alloc_page();
if (physical == PMM_INVALID_PAGE) {
kernel_panic("process could not allocate user page");
}
int rc = vmem_map_page(
virtual_addr,
physical,
VMEM_FLAG_WRITABLE | VMEM_FLAG_USER
);
if (rc != VMEM_OK) {
kernel_panic("process could not map user page");
}
memset((void *)virtual_addr, 0, PMM_PAGE_SIZE);
}
process_t *process_create_user(
const char *name,
const uint8_t *program,
uint32_t program_size
) {
if (program == 0 || program_size == 0) {
kernel_panic("process_create_user received empty program");
}
if (program_size > PMM_PAGE_SIZE) {
kernel_panic("process_create_user program too large for one page");
}
process_t *process = (process_t *)kcalloc(1, sizeof(process_t));
if (process == 0) {
kernel_panic("process_create_user could not allocate process object");
}
process->magic = PROCESS_MAGIC;
process->pid = next_pid++;
process->name = name != 0 ? name : "unnamed";
process->state = PROCESS_NEW;
process->exit_code = 0xFFFFFFFFu;
process->exited = 0;
process->user_code_base = USER_PROCESS_CODE_VA;
process->user_stack_base = USER_PROCESS_STACK_VA;
process->user_stack_top = USER_PROCESS_STACK_TOP;
map_user_page(process->user_code_base);
map_user_page(process->user_stack_base);
memcpy((void *)process->user_code_base, program, program_size);
thread_t *thread = thread_create(
process->name,
user_process_thread_entry,
process
);
thread_set_process(thread, process);
process->main_thread = thread;
process->state = PROCESS_RUNNING;
console_write("Process: created pid=");
console_write_u32_dec(process->pid);
console_write(" name=");
console_writeln(process->name);
return process;
}
process_t *process_current(void) {
thread_t *thread = thread_current();
if (thread == 0) {
return 0;
}
return (process_t *)thread_get_process(thread);
}
void process_exit_current(uint32_t exit_code) {
process_t *process = process_current();
if (process == 0) {
console_write("Syscall: kernel thread exit code ");
console_write_u32_dec(exit_code);
console_putc('\n');
last_exit_code = exit_code;
last_exit_seen = 1;
return;
}
process->exit_code = exit_code;
process->exited = 1;
process->state = PROCESS_EXITED;
last_exit_code = exit_code;
last_exit_seen = 1;
console_write("Syscall: process ");
console_write(process->name);
console_write(" pid=");
console_write_u32_dec(process->pid);
console_write(" exited code ");
console_write_u32_dec(exit_code);
console_putc('\n');
}
uint32_t process_last_exit_code(void) {
return (uint32_t)last_exit_code;
}
int process_last_exit_seen(void) {
return last_exit_seen;
}
static void user_process_thread_entry(void *arg) {
process_t *process = (process_t *)arg;
if (process == 0 || process->magic != PROCESS_MAGIC) {
kernel_panic("user process thread received invalid process");
}
usermode_enter_current_process();
kernel_panic("user process returned from user mode");
}
/*
* User program:
*
* SYS_WRITE("User process says hello through SYS_WRITE\n")
* SYS_SLEEP(3)
* SYS_EXIT(7)
*/
static const uint8_t user_process_demo[] = {
/*
* mov eax, SYS_WRITE
* mov ebx, message_addr
* mov ecx, message_len
* int 0x80
*/
0xB8, SYS_WRITE, 0x00, 0x00, 0x00,
0xBB, 0x40, 0x00, 0x10, 0x40,
0xB9, 0x2Au, 0x00, 0x00, 0x00,
0xCD, 0x80,
/*
* mov eax, SYS_SLEEP
* mov ebx, 3
* int 0x80
*/
0xB8, SYS_SLEEP, 0x00, 0x00, 0x00,
0xBB, 0x03, 0x00, 0x00, 0x00,
0xCD, 0x80,
/*
* mov eax, SYS_EXIT
* mov ebx, 7
* int 0x80
*/
0xB8, SYS_EXIT, 0x00, 0x00, 0x00,
0xBB, 0x07, 0x00, 0x00, 0x00,
0xCD, 0x80,
/*
* jmp $
*/
0xEB, 0xFE,
/*
* Padding to offset 0x40.
*/
0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90,
0x90,
/*
* Offset 0x40:
* "User process says hello through SYS_WRITE\n"
*
* Length is 42 bytes.
*/
'U','s','e','r',' ','p','r','o','c','e','s','s',' ',
's','a','y','s',' ','h','e','l','l','o',' ','t','h',
'r','o','u','g','h',' ','S','Y','S','_','W','R','I','T','E','\n'
};
void process_test_once(void) {
console_writeln("Process test: starting user process syscall test");
last_exit_seen = 0;
last_exit_code = 0xFFFFFFFFu;
process_create_user(
"user-demo",
user_process_demo,
sizeof(user_process_demo)
);
while (!last_exit_seen) {
thread_sleep_ticks(1);
thread_reap_zombies();
}
thread_reap_zombies();
if (last_exit_code != 7) {
kernel_panic("process test received wrong exit code");
}
console_writeln("Process test: user process syscall/write/sleep/exit sanity check passed");
}
Important note about the machine-code message address
The user program uses:
message address = 0x40100040
because:
USER_PROCESS_CODE_VA = 0x40100000 message offset = 0x40
This is crude, but good enough for one page of test code.
A real executable loader will relocate or load segments properly.
17. Update kernel/kmain.c
Add:
#include "kernel/process.h"
Then call:
process_init_system();
after threading is initialized.
Call:
process_test_once();
after the old user-mode test location.
Since Chapter 17 replaces usermode_test_once(), remove that call.
Relevant section:
threading_init(); process_init_system(); thread_test_once();
Later:
monitor_init(); monitor_test_once(); process_test_once(); monitor_start();
Full relevant flow
threading_init() process_init_system() thread tests ... terminal tests monitor tests process test start monitor idle forever
18. Update Makefile
Add:
build/kernel/process.o build/kernel/usercopy.o
Keep:
build/kernel/usermode.o
because it still contains the ring-3 entry helper.
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_asm.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/kmain.o \
build/kernel/idle.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 test greps.
Remove Chapter 16-specific greps:
grep -q "User mode test: preparing ring 3 program" build/test.log grep -q "User mode test: ring 3 syscall/exit sanity check passed" build/test.log
Add:
grep -q "Process: process table initialized" build/test.log grep -q "Process test: starting user process syscall test" build/test.log grep -q "Process: created pid=1 name=user-demo" build/test.log grep -q "User process says hello through SYS_WRITE" build/test.log grep -q "Syscall: process user-demo pid=1 exited code 7" build/test.log grep -q "Process test: user process syscall/write/sleep/exit sanity check passed" build/test.log
19. Expected output
A successful boot should include:
Process: process table initialized ... Process test: starting user process syscall test Process: created pid=1 name=user-demo User process says hello through SYS_WRITE Syscall: process user-demo pid=1 exited code 7 Threads: reaping zombie user-demo id=... Process test: user process syscall/write/sleep/exit sanity check passed Monitor: monitor thread started
That proves:
process object created user code mapped user stack mapped thread attached to process ring-3 code ran SYS_WRITE copied from user memory SYS_SLEEP blocked user thread SYS_EXIT recorded process exit status zombie thread was reaped
20. Common failures
Failure: SYS_WRITE prints garbage
Check the hardcoded message address:
0x40100040
and make sure the message really begins at offset 0x40 inside user_process_demo.
If you change the machine-code bytes before the message, update the offset.
Failure: copy_from_user() fails
Likely causes:
code page not mapped VMEM_FLAG_USER wrong message pointer message crosses into an unmapped page vmem_get_flags() not implemented correctly
Check:
vmem_map_page(..., VMEM_FLAG_WRITABLE | VMEM_FLAG_USER)
and:
vmem_is_user_accessible(page)
Failure: process exits but test never completes
Check:
last_exit_seen = 1;
inside process_exit_current().
Also make sure SYS_EXIT calls:
process_exit_current(exit_code); thread_exit();
Failure: SYS_SLEEP never wakes
That means the user thread blocked but the timer/sleep queue did not wake it.
Check:
thread_sleep_ticks(ticks);
inside the syscall handler.
The user process test calls SYS_SLEEP(3), so it requires interrupts and PIT scheduling to already be active.
21. What this chapter achieved
We now have the beginning of a process model:
process_t ↓ main user thread ↓ user code page ↓ user stack page ↓ syscalls ↓ exit status
The syscall layer now does a more realistic thing:
validate user pointer copy from user memory perform kernel operation return result in EAX
This is the foundation for:
file descriptors read/write syscalls ELF loading per-process address spaces waitpid exec fork-like cloning, much later
22. Design limitations
This is still not a Unix process model.
Current limitations:
all processes share the same page directory user pages are mapped globally no per-process address spaces no process list no process wait queue no parent/child relationship no file descriptors no ELF loader no user heap no argument vector
That is expected.
The next big architectural step is per-process address spaces. But before that, it is useful to add a simple file-like abstraction for stdout and keyboard input, because it makes user programs more interesting.
23. Next chapter
The next chapter can go in either of two directions.
The practical path:
file descriptors 0/1/2 SYS_READ SYS_WRITE using fd user program reads keyboard and echoes input
The deeper memory path:
per-process page directory kernel mappings cloned into each process CR3 switch on context switch separate user address spaces
I recommend the practical path first. It will give us a tiny user-mode console program before we tackle address-space isolation.
24. Resources
- Chapter 17 source release
- Chapter 17 repository tree
- Intel 64 and IA-32 Architectures Software Developer Manuals
- OSDev Wiki: System Calls
- OSDev Wiki: User Space
25. Closure
The kernel now has a process object for user programs, checked user-memory copying, and more realistic syscalls for writing, sleeping, and exiting. That gives later chapters a concrete place to attach file descriptors, address spaces, wait state, and executable loading.
Happy Coding!