- 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 20 — A Tiny Executable Format and User Program Loader
- 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
- Chapter 18 — File-Descriptor Syscalls and a Tiny User-Mode Console Program
- Chapter 19 — Per-Process Address Spaces and CR3 Switching
- Chapter 21 — Process Teardown and Address-Space Cleanup
- Chapter 23 — Building a Real User C Program and Embedding Its ELF
- Chapter 24 — User argc / argv and a Real Initial Stack
- Chapter 26 — Process Table, ps, runbg, and wait PID
- Chapter 27 — A Second User Program for Safe Background Execution
- Chapter 28 — Pattern-Based User Program Build System
- Chapter 29 — First Userland Runtime
- Chapter 30 — First User-Mode Shell
- Chapter 31 — SYS_EXEC, SYS_WAITPID, and Shell-Launched Programs
- Chapter 32 — Process Ownership, Waiting, and Job State
- Chapter 33 — Process Termination and Kill Checks
- Chapter 34 — First RAMFS and Core File APIs
- Chapter 35 — SYS_SEEK and Rewindable File Descriptors
- Chapter 36.5 – A Testing Detour and a Real Smoke Harness
- Chapter 37 — Directories, SYS_READDIR, and ls
- Chapter 38 — Turning /programs into a Real Directory
- Chapter 39 — Path-Based Program Launching with /programs
Post Stastics
- This post has 2061 words.
- Estimated read time is 9.81 minute(s).
Before reading the shell examples in this chapter, be explicit about the prompt transition:
toyix> run shell ush>
toyix> is the kernel monitor.
ush> is the user shell.
So when this chapter shows commands like:
ush> run /programs/counter alpha beta ush> runbg /programs/counter victim
those commands must be typed inside the user shell after toyix> run shell.
Two other constraints still apply in this chapter:
clear is a kernel monitor command, not a user-shell command file paths in ush are still absolute-only
So cat README is still wrong, and cat /README is the correct form.
Likewise, cat / fails because / is a directory, not a regular file.
In Chapter 38, /programs became a real directory:
ush> ls /programs file demo file counter file shell file fstest
That gave Toyix a better filesystem shape, but program launching still lived in the older world:
ush> run counter alpha beta
The shell knew program names.
The filesystem knew /programs/counter.
Those were still two different namespaces.
Chapter 39 connects them.
After this chapter, the existing launch commands can accept either form:
ush> run counter alpha beta ush> run /programs/counter alpha beta
Both launch the same embedded program.
We are still not loading ELF files from the filesystem yet. The kernel still launches from the embedded program registry. This chapter only teaches that registry-facing launch path how to recognize a narrow filesystem form:
/programs/<name>
That is enough to bridge the namespace we built in Chapter 38 to the execution path we built much earlier.
1. What this chapter adds
Modify:
include/kernel/program.h kernel/program.c tests/smoke.py README.md CHANGELOG.md index.md docs/roadmap.md
No new syscall number is needed.
No new shell built-in is needed.
No VFS node type changes are needed.
The shell already calls:
SYS_EXEC
with a string plus argv.
So the cleanest Chapter 39 design is:
teach the kernel launcher to accept either: counter /programs/counter
That means the shell commands stay stable:
run PROGRAM [ARGS...] runbg PROGRAM [ARGS...]
and only the meaning of the PROGRAM operand grows.
2. Why this is the right Chapter 39
At this point in development, Toyix already has:
an embedded program registry a user shell SYS_EXEC a real /programs directory
So the next literal step is not:
new ELF loading rules new executable file metadata new path search logic
It is simply:
accept /programs/<name> anywhere we already accept a registry program name
That keeps the system coherent.
The user sees a real filesystem path.
The kernel still launches through the same embedded program table.
Later chapters can add mode bits, current working directories, and general command lookup on top of this bridge.
3. Supported path rules
This chapter intentionally supports only a narrow path form.
Valid launch targets:
demo counter shell fstest /programs/demo /programs/counter /programs/shell /programs/fstest
Invalid launch targets:
/programs /programs/ /programs/missing /programs/counter/extra programs/counter ./counter /README
That narrowness is important.
We are not adding a general path resolver yet.
We are only saying:
if the input begins with /programs/ and has exactly one final name component, map that final component back to the embedded program registry
4. Update include/kernel/program.h
The public launcher API should reflect the broader input it now accepts.
Update the header so the names describe reality:
const embedded_program_t *program_find(const char *name);
const embedded_program_t *program_find_name_or_path(
const char *name_or_path
);
process_t *program_create_process(
const char *name_or_path,
int argc,
const char **argv
);
int program_run_background(
const char *name_or_path,
int argc,
const char **argv,
process_t **process_out
);
int program_run_foreground(
const char *name_or_path,
int argc,
const char **argv,
uint32_t *exit_code_out
);
The important change is semantic clarity.
Before this chapter, those functions expected:
plain registry names only
After this chapter, they accept:
either a plain registry name or a /programs/<name> path
5. Add a narrow resolver in kernel/program.c
Near the top of kernel/program.c, add a prefix constant:
#define PROGRAM_PATH_PREFIX "/programs/" #define PROGRAM_PATH_PREFIX_LEN 10u
Then add a helper that recognizes only this exact shape:
static const char *program_path_suffix(const char *name_or_path) {
if (name_or_path == 0) {
return 0;
}
if (kstrncmp(
name_or_path,
PROGRAM_PATH_PREFIX,
PROGRAM_PATH_PREFIX_LEN
) != 0) {
return 0;
}
const char *name = name_or_path + PROGRAM_PATH_PREFIX_LEN;
if (*name == '\0') {
return 0;
}
while (*name != '\0') {
if (*name == '/') {
return 0;
}
++name;
}
return name_or_path + PROGRAM_PATH_PREFIX_LEN;
}
This helper does three important things:
- It accepts only absolute
/programs/...inputs. - It rejects an empty tail like
/programs/. - It rejects nested paths like
/programs/counter/extra.
So:
/programs/counter
returns:
counter
but:
/programs/counter/extra
returns failure.
That keeps Chapter 39 intentionally narrow and prevents accidental early path-walking behavior from leaking into the launcher.
6. Add program_find_name_or_path()
Keep the old plain-name lookup:
const embedded_program_t *program_find(const char *name);
That function is still useful and still correct.
Add a second helper on top of it:
const embedded_program_t *program_find_name_or_path(
const char *name_or_path
) {
const char *path_name = program_path_suffix(name_or_path);
if (path_name != 0) {
return program_find(path_name);
}
return program_find(name_or_path);
}
This is the core bridge for the chapter.
It means these two calls behave the same:
program_find_name_or_path("counter")
program_find_name_or_path("/programs/counter")
Both end up returning the embedded counter entry.
But the helper still rejects:
/programs /programs/ /programs/missing /programs/counter/extra
because those either fail the narrow path check or fail the final registry lookup.
7. Route all launch helpers through that resolver
Now update the launch path itself.
In program_create_process(), change:
const embedded_program_t *program = program_find(name);
to:
const embedded_program_t *program =
program_find_name_or_path(name_or_path);
This one change automatically lifts the rest of the kernel launch path.
Because:
program_run_background() program_run_foreground() syscall_exec() shell run shell runbg
all eventually flow through program_create_process().
That is why Chapter 39 does not need a new syscall and does not need a new shell command.
The existing chain already exists:
shell run /programs/counter
-> toyix_exec("/programs/counter", argv, argc)
-> SYS_EXEC
-> program_run_background(...)
-> program_create_process(...)
We only needed to make the last kernel-side lookup step understand the new input form.
8. Preserve user-visible argv[0]
One subtle point matters here.
When a user types:
ush> run /programs/counter alpha beta
the program should still see:
argv[0] = /programs/counter
not:
argv[0] = counter
Why?
Because the kernel should normalize the launch target for lookup, but it should not silently rewrite the user’s argument vector unless that is architecturally required.
So the path is used for:
registry lookup
but the original argv is still passed through to the child.
That gives us a useful split:
internal process identity -> counter user-facing argv[0] -> /programs/counter
So the boot log will show:
Program: launching counter argc=3 ppid=...
while the child itself prints:
counter: argv[0]=/programs/counter
That is the correct Chapter 39 behavior.
9. Why the shell does not change in this chapter
Notice that user/shell.c does not need a code change for this milestone.
The shell already treats the first operand to run and runbg as an arbitrary string:
program_name = argv[1]; pid = toyix_exec(program_name, child_argv, (toyix_u32)child_argc);
That means the shell was already capable of passing:
counter /programs/counter
The only missing piece was the kernel-side interpretation.
This is a good example of a chapter where the right design is not to add another layer of shell parsing just because a new user-visible behavior appears.
The shell was already general enough.
The kernel lookup was the narrow part.
10. Update the scripted smoke flow carefully
This chapter also needs a testing adjustment.
The old scripted shell sequence already launched:
run counter alpha beta runbg counter victim kill 5 wait 5
That sequence relied on the exact number of child launches that happened before the background counter.
If we had added:
run counter ... run /programs/counter ...
then the later background process would no longer be PID 5, and the old injected:
kill 5 wait 5
would stop matching the intended child.
So instead of adding an extra launch, Chapter 39 should replace the foreground shell launch test with the path-based form:
run /programs/counter alpha beta
and then add a failure check that does not create a child:
run /programs/missing
This is the smooth version of the chapter because it tests the new feature while keeping the rest of the scripted flow stable.
That avoids repeating the Chapter 37-style mistake where documented and tested paths drifted apart.
11. Update program_test_once()
In kernel/program.c, change the injected shell command from:
inject_text("run counter alpha beta\n");
to:
inject_text("run /programs/counter alpha beta\n");
Then add a failing launch check before the existing background launch:
inject_text("run /programs/missing\n");
inject_text("runbg counter victim\n");
That gives the smoke harness three key Chapter 39 checks:
- A successful path-based foreground launch.
- A failed launch for a missing
/programspath. - No PID drift in the later
runbg counter/kill 5/wait 5sequence.
That third point matters because the tests should validate the new chapter, not accidentally rewrite the meaning of an unrelated later assertion.
12. Update tests/smoke.py
Now update the expected shell output.
The success lines should include:
shell: run /programs/counter pid= counter: argv[0]=/programs/counter shell: /programs/counter exited code 4
And the failure path should include:
run: failed to launch /programs/missing
The Chapter 38 strings:
shell: run counter pid= shell: counter exited code 4
should be replaced in the scripted shell portion because the new smoke flow is specifically checking path-based launch behavior now.
Also update the final success banner:
print("All Chapter 39 checks passed.")
That sounds small, but it matters when reading CI logs and release artifacts later.
13. What the user should see now
Inside the user shell:
ush> run /programs/counter alpha beta Program: launching counter argc=3 ppid=... shell: run /programs/counter pid=... counter: argc=3 counter: argv[0]=/programs/counter counter: argv[1]=alpha counter: argv[2]=beta counter: tick 1 counter: tick 2 counter: tick 3 shell: /programs/counter exited code 4
A missing path should fail cleanly:
ush> run /programs/missing run: failed to launch /programs/missing
And plain registry launches should still work:
ush> run counter alpha beta
That backward compatibility is intentional.
Chapter 39 extends the interface; it does not break the earlier one.
14. Why this chapter still is not filesystem execution
It is important to say this literally.
After Chapter 39, Toyix still does not do this:
open /programs/counter read an ELF image from that file load that file as the executable
Instead it does this:
recognize /programs/counter extract the tail name counter look up counter in the embedded program registry launch the embedded ELF image for counter
So the bridge is semantic, not yet storage-backed.
That is exactly the right scope for this chapter.
We already built the namespace in Chapter 38.
Chapter 39 lets the launch path speak that namespace.
Later chapters can make it more dynamic, more VFS-driven, and more Unix-like.
15. Files changed in this chapter
The final file list for this chapter should be:
include/kernel/program.h kernel/program.c tests/smoke.py README.md CHANGELOG.md index.md docs/roadmap.md articles/chapter_39.md
That is another sign the chapter is now scoped correctly.
The implementation is mostly:
kernel launch-path normalization scripted smoke updates documentation alignment
not a broad shell rewrite and not a new filesystem execution subsystem.
Next Chapter
Now that Toyix can launch programs through /programs/<name>, the next natural step is to make shell job control less PID-centric and more shell-centric.
That means the next chapter can resume our regularly scheduled programming by teaching the shell how to refer to jobs by shell-visible handles instead of raw numeric PIDs.
Resources
Closure
Chapter 39 adds the first real bridge from the /programs filesystem namespace to Toyix program execution by letting the existing launch path accept /programs/<name> wherever it already accepted a registry name, while keeping the shell interface stable and the smoke flow aligned with the actual process lifecycle.
Happy Coding!